Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/onboardDebug/node/debuggableCommandIdentifier.ts
13399 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
// import * as vscode from 'vscode';
7
import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService';
8
import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext';
9
import { IFileSystemService } from '../../../platform/filesystem/common/fileSystemService';
10
import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService';
11
import { createServiceIdentifier } from '../../../util/common/services';
12
import { distinct } from '../../../util/vs/base/common/arrays';
13
import { CancellationToken } from '../../../util/vs/base/common/cancellation';
14
import { Disposable } from '../../../util/vs/base/common/lifecycle';
15
import * as path from '../../../util/vs/base/common/path';
16
import { URI } from '../../../util/vs/base/common/uri';
17
import { ILanguageToolsProvider } from './languageToolsProvider';
18
19
export interface IDebuggableCommandIdentifier {
20
readonly _serviceBrand: undefined;
21
22
/**
23
* Gets whether the given command, run in the given directory, might be debuggable.
24
*/
25
isDebuggable(cwd: URI | undefined, command: string, token: CancellationToken): Promise<boolean>;
26
}
27
28
export const IDebuggableCommandIdentifier = createServiceIdentifier<IDebuggableCommandIdentifier>('IDebuggableCommandIdentifier');
29
30
export class DebuggableCommandIdentifier extends Disposable implements IDebuggableCommandIdentifier {
31
declare readonly _serviceBrand: undefined;
32
33
private recentlySeenLanguages = new Set<string>();
34
35
constructor(
36
@IConfigurationService private readonly configurationService: IConfigurationService,
37
@IVSCodeExtensionContext private readonly context: IVSCodeExtensionContext,
38
@IWorkspaceService workspaceService: IWorkspaceService,
39
@ILanguageToolsProvider private readonly languageToolsProvider: ILanguageToolsProvider,
40
@IFileSystemService private readonly fileSystemService: IFileSystemService,
41
) {
42
super();
43
this._register(workspaceService.onDidOpenTextDocument(e => {
44
if (!KNOWN_DEBUGGABLE_LANGUAGES.includes(e.languageId)) {
45
this.recentlySeenLanguages.add(e.languageId);
46
}
47
}));
48
}
49
50
/**
51
* @inheritdoc
52
*
53
* This logic is as follows:
54
*
55
* - If the user has configured specific inclusions or exclusions for
56
* the command, then use those.
57
* - If the command being run is a relative path or within the CWD, assume
58
* it's debuggable. This might be native code compiled to an executable.
59
* - If one of the debuggable commands we know about matches, then it's
60
* debuggable.
61
* - If the user has interacted with languages for which we don't know the
62
* appropriate debuggable commands, ask the language model and update
63
* our storage.
64
*
65
*/
66
public async isDebuggable(cwd: URI | undefined, commandLine: string, token: CancellationToken): Promise<boolean> {
67
if (!this.isGloballyEnabled()) {
68
return false;
69
}
70
71
const command = extractCommandNameFromCLI(commandLine).toLowerCase();
72
73
return this.getSpecificTreatment(command)
74
?? this.isWellKnownCommand(command)
75
?? await this.isWorkspaceLocal(cwd, command)
76
?? await this.isModelSuggestedCommand(command, token)
77
?? false;
78
}
79
80
private isGloballyEnabled() {
81
return this.configurationService.getConfig(ConfigKey.TerminalToDebuggerEnabled);
82
}
83
84
private async isWorkspaceLocal(cwd: URI | undefined, command: string): Promise<true | undefined> {
85
const abs = path.isAbsolute(command) ? URI.file(command) : cwd && URI.joinPath(cwd, command);
86
87
if (!abs) {
88
return undefined;
89
}
90
91
try {
92
await this.fileSystemService.stat(abs);
93
return true;
94
} catch {
95
// no-op
96
}
97
}
98
99
private async isModelSuggestedCommand(command: string, token: CancellationToken) {
100
const known = this.loadModelKnownCommands();
101
102
// check ones we queried for previously and don't query for them again.
103
for (const language of known.languages) {
104
this.recentlySeenLanguages.delete(language);
105
}
106
if (known.commands.some(c => this.commandIncludes(command, c))) {
107
return true;
108
}
109
if (!this.recentlySeenLanguages.size) {
110
return false;
111
}
112
113
const languages = [...this.recentlySeenLanguages];
114
this.recentlySeenLanguages.clear();
115
const { commands, ok } = await this.languageToolsProvider.getToolsForLanguages(languages, token);
116
117
if (ok) {
118
this.storeModelKnownCommands({
119
languages: known.languages.concat(languages),
120
commands: distinct(known.commands.concat(commands)),
121
});
122
}
123
124
return commands.some(c => this.commandIncludes(command, c));
125
}
126
127
private isWellKnownCommand(command: string): boolean | undefined {
128
// an 'include' check to handle things like pip3 vs pip
129
return KNOWN_DEBUGGABLE_COMMANDS.some(tool => this.commandIncludes(command, tool)) || undefined;
130
}
131
132
private getSpecificTreatment(command: string): boolean | undefined {
133
const patterns = this.configurationService.getConfig(ConfigKey.Advanced.TerminalToDebuggerPatterns);
134
for (const pattern of patterns) {
135
if (pattern.startsWith('!') && this.commandIncludes(command, pattern)) {
136
return false;
137
} else if (this.commandIncludes(command, pattern)) {
138
return true;
139
}
140
}
141
}
142
143
private commandIncludes(command: string, needle: string) {
144
const idx = command.indexOf(needle);
145
return idx >= 0 &&
146
(idx === 0 || command[idx - 1] === ' ') &&
147
(idx + needle.length === command.length || command[idx + needle.length] === ' ');
148
}
149
150
private loadModelKnownCommands() {
151
return this.context.globalState.get<IKnownCommandsState>(DEBUGGABLE_COMMAND_STORAGE_KEY, {
152
languages: [],
153
commands: [],
154
});
155
}
156
157
private storeModelKnownCommands(commands: IKnownCommandsState) {
158
return this.context.globalState.update(DEBUGGABLE_COMMAND_STORAGE_KEY, commands);
159
}
160
}
161
162
interface IKnownCommandsState {
163
languages: string[];
164
commands: string[];
165
}
166
167
const DEBUGGABLE_COMMAND_STORAGE_KEY = 'chat.debuggableCommands';
168
169
function extractCommandNameFromCLI(command: string) {
170
// todo: support less common cases of quoting and environment variables
171
const re = /\s*([^\s]+)/;
172
const match = re.exec(command);
173
return match ? match[1] : command;
174
}
175
176
/**
177
* Seed some built-in patterns to avoid LM lookups for common cases.
178
* Generated in test/simulation/debugTools.stest.ts, do not edit directly!
179
*/
180
const KNOWN_DEBUGGABLE_COMMANDS = ['abap', 'ant', 'automake', 'autotools', 'ava', 'babel', 'bcp', 'behat', 'behave', 'biber', 'bibtex', 'bmake', 'boot', 'broccoli-sass', 'browserify', 'build_runner', 'bundler', 'busted', 'cabal', 'cargo', 'cargo-bench', 'cargo-fuzz', 'cargo-make', 'cargo-run', 'cargo-test', 'cargo-watch', 'carthage', 'carton', 'clang', 'clippy-driver', 'clj', 'clojure', 'cmake', 'cocoapods', 'codeception', 'common_test', 'composer', 'conan', 'coverage', 'cpan', 'cpanm', 'csc', 'ct_run', 'ctest', 'cucumber', 'cuda-gdb', 'cuda-memcheck', 'cypress', 'dart', 'dart-sass', 'dart2js', 'dartanalyzer', 'dartdevc', 'db2cli', 'ddemangle', 'devenv', 'devtools', 'dfix', 'dialyzer', 'dmd', 'doctest', 'dotnet', 'dotnet-script', 'dotnet-test-nunit', 'dotnet-test-xunit', 'dpp', 'dscanner', 'dsymutil', 'dub', 'dune', 'dustmite', 'dvilualatex', 'dvipdf', 'dvipdfmx', 'dvips', 'erl', 'erlang', 'erlc', 'esbuild', 'escript', 'eunit', 'eyeglass', 'fastlane', 'fennel', 'flutter', 'forever', 'fpc', 'fsharpc', 'fsi', 'g', 'gaiden', 'gcc', 'gcov', 'gdb', 'gdc', 'ghc', 'ghcid', 'gmake', 'gmaven', 'go', 'gpars', 'gradle', 'grape', 'griffon', 'grinder', 'grip', 'groovy', 'groovyc', 'grunt', 'grunt-sass', 'gulp', 'gulp-sass', 'hdevtools', 'hlint', 'hspec', 'irb', 'isql', 'jasmine', 'java', 'javac', 'jazzy', 'jdeps', 'jest', 'jlink', 'julia', 'junit', 'kaocha', 'karma', 'kobalt', 'kotest', 'kotlin-dsl', 'kotlinc', 'kscript', 'latexmk', 'lazbuild', 'lcov', 'ld', 'ldc2', 'ldoc', 'leiningen', 'lldb', 'lua', 'luacheck', 'luajit', 'lualatex', 'luarocks', 'luaunit', 'make', 'markdown', 'markdown-it', 'markdown-pdf', 'marked', 'matlab', 'maven', 'mbuild', 'mcc', 'md2pdf', 'mdbook', 'merlin', 'mex', 'midje', 'minitest', 'mlint', 'mmake', 'mocha', 'mockk', 'mono', 'moonscript', 'msbuild', 'mssql-cli', 'mstest', 'multimarkdown', 'mysql', 'ncu', 'ninja', 'nmake', 'node', 'node-sass', 'nose', 'npm', 'npx', 'nrepl', 'nsight', 'nsys', 'nunit-console', 'nvcc', 'ocamlbuild', 'ocamlc', 'ocamldebug', 'ocamlfind', 'ocamlopt', 'ocamlrun', 'opam', 'otest', 'otool', 'paket', 'panda', 'pandoc', 'parcel', 'pdflatex', 'perl', 'perl6', 'perlbrew', 'pgbench', 'phing', 'php', 'php-cs-fixer', 'phpcs', 'phpdbg', 'phpstan', 'phpunit', 'pip', 'pipenv', 'plackup', 'playwright', 'pm2', 'pmake', 'powershell', 'ppc386', 'ppcrossarm', 'ppcrossavr', 'ppcrossmips', 'ppcrossppc', 'ppcrosssparc', 'ppcrosswin32', 'ppcrossx64', 'protractor', 'prove', 'pry', 'psql', 'psysh', 'pub', 'pwsh', 'pytest', 'python', 'qmake', 'quickcheck', 'rails', 'rake', 'rakudo', 'rdmd', 'react-scripts', 'rebar3', 'relx', 'remake', 'rollup', 'rspec', 'rubocop', 'ruby', 'runghc', 'rustc', 'rustup', 'sass', 'sassc', 'scons', 'showdown', 'sinatra', 'speclj', 'spek', 'spock', 'spring-boot', 'sqlcmd', 'sqlite3', 'sqsh', 'stack', 'svelte-kit', 'swift', 'swiftc', 'test', 'test-runner', 'testng', 'testthat', 'tools', 'torch', 'tox', 'ts-node', 'tsc', 'unittest', 'utop', 'valgrind', 'vbc', 'virtualenv', 'vite', 'vstest', 'vue-cli-service', 'vue-test-utils', 'webdev', 'webpack', 'x', 'xcodebuild', 'xctest', 'xelatex', 'xunit', 'yarn', 'zef', 'zig'];
181
182
/**
183
* Languages the {@link KNOWN_DEBUGGABLE_COMMANDS} covers.
184
* Generated in test/simulation/debugTools.stest.ts, do not edit directly!
185
*/
186
const KNOWN_DEBUGGABLE_LANGUAGES = ['abap', 'bat', 'bibtex', 'c', 'clojure', 'code-refactoring', 'coffeescript', 'cpp', 'csharp', 'css', 'cuda-cpp', 'd', 'dart', 'diff', 'dockercompose', 'dockerfile', 'erlang', 'fsharp', 'git-commit', 'git-rebase', 'github-issues', 'go', 'graphql', 'groovy', 'haml', 'handlebars', 'haskell', 'html', 'ini', 'jade', 'java', 'javascript', 'javascriptreact', 'json', 'jsonc', 'julia', 'kotlin', 'latex', 'less', 'log', 'lua', 'makefile', 'markdown', 'matlab', 'objective-c', 'objective-cpp', 'ocaml', 'pascal', 'perl', 'perl6', 'php', 'pip-requirements', 'plaintext', 'powershell', 'pug', 'python', 'r', 'razor', 'ruby', 'rust', 'sass', 'scss', 'shaderlab', 'shellscript', 'slim', 'snippets', 'sql', 'stylus', 'svelte', 'swift', 'tex', 'text', 'toml', 'typescript', 'typescriptreact', 'vb', 'vue', 'vue-html', 'xml', 'xsl', 'yaml', 'zig'];
187
188