Skip to content

Commit f4909e0

Browse files
committed
Implement most of command line confidence API
Part of microsoft#145234
1 parent b2c4302 commit f4909e0

File tree

10 files changed

+126
-31
lines changed

10 files changed

+126
-31
lines changed

src/vs/platform/terminal/common/capabilities/capabilities.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ export interface IPartialCommandDetectionCapability {
240240
interface IBaseTerminalCommand {
241241
// Mandatory
242242
command: string;
243+
commandLineConfidence: 'low' | 'medium' | 'high';
243244
isTrusted: boolean;
244245
timestamp: number;
245246
duration: number;

src/vs/platform/terminal/common/capabilities/commandDetection/terminalCommand.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type { IBuffer, IBufferLine, Terminal } from '@xterm/headless';
1212

1313
export interface ITerminalCommandProperties {
1414
command: string;
15+
commandLineConfidence: 'low' | 'medium' | 'high';
1516
isTrusted: boolean;
1617
timestamp: number;
1718
duration: number;
@@ -33,6 +34,7 @@ export interface ITerminalCommandProperties {
3334
export class TerminalCommand implements ITerminalCommand {
3435

3536
get command() { return this._properties.command; }
37+
get commandLineConfidence() { return this._properties.commandLineConfidence; }
3638
get isTrusted() { return this._properties.isTrusted; }
3739
get timestamp() { return this._properties.timestamp; }
3840
get duration() { return this._properties.duration; }
@@ -71,6 +73,7 @@ export class TerminalCommand implements ITerminalCommand {
7173
const executedMarker = serialized.executedLine !== undefined ? xterm.registerMarker(serialized.executedLine - (buffer.baseY + buffer.cursorY)) : undefined;
7274
const newCommand = new TerminalCommand(xterm, {
7375
command: isCommandStorageDisabled ? '' : serialized.command,
76+
commandLineConfidence: serialized.commandLineConfidence ?? 'low',
7477
isTrusted: serialized.isTrusted,
7578
promptStartMarker,
7679
marker,
@@ -99,6 +102,7 @@ export class TerminalCommand implements ITerminalCommand {
99102
executedLine: this.executedMarker?.line,
100103
executedX: this.executedX,
101104
command: isCommandStorageDisabled ? '' : this.command,
105+
commandLineConfidence: isCommandStorageDisabled ? 'low' : this.commandLineConfidence,
102106
isTrusted: this.isTrusted,
103107
cwd: this.cwd,
104108
exitCode: this.exitCode,
@@ -265,6 +269,7 @@ export class PartialTerminalCommand implements ICurrentPartialCommand {
265269

266270
cwd?: string;
267271
command?: string;
272+
commandLineConfidence?: 'low' | 'medium' | 'high';
268273

269274
isTrusted?: boolean;
270275
isInvalid?: boolean;
@@ -287,6 +292,7 @@ export class PartialTerminalCommand implements ICurrentPartialCommand {
287292
executedLine: undefined,
288293
executedX: undefined,
289294
command: '',
295+
commandLineConfidence: 'low',
290296
isTrusted: true,
291297
cwd,
292298
exitCode: undefined,
@@ -306,6 +312,7 @@ export class PartialTerminalCommand implements ICurrentPartialCommand {
306312
if ((this.command !== undefined && !this.command.startsWith('\\')) || ignoreCommandLine) {
307313
return new TerminalCommand(this._xterm, {
308314
command: ignoreCommandLine ? '' : (this.command || ''),
315+
commandLineConfidence: ignoreCommandLine ? 'low' : (this.commandLineConfidence || 'low'),
309316
isTrusted: !!this.isTrusted,
310317
promptStartMarker: this.promptStartMarker,
311318
marker: this.commandStartMarker,

src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,14 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe
8787
) {
8888
super();
8989

90+
this.onCommandExecuted(command => {
91+
// Pull command line from the buffer if it was not set explicitly
92+
if (command.commandLineConfidence !== 'high') {
93+
94+
console.log('commandLineConfidence !== high', command.command);
95+
}
96+
});
97+
9098
// Set up platform-specific behaviors
9199
const that = this;
92100
this._ptyHeuristicsHooks = new class implements ICommandDetectionHeuristicsHooks {
@@ -353,6 +361,7 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe
353361
setCommandLine(commandLine: string, isTrusted: boolean) {
354362
this._logService.debug('CommandDetectionCapability#setCommandLine', commandLine, isTrusted);
355363
this._currentCommand.command = commandLine;
364+
this._currentCommand.commandLineConfidence = 'high';
356365
this._currentCommand.isTrusted = isTrusted;
357366
}
358367

src/vs/workbench/api/browser/mainThreadTerminalShellIntegration.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { ExtHostContext, MainContext, type ExtHostTerminalShellIntegrationShape,
1111
import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal';
1212
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
1313
import { extHostNamedCustomer, type IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers';
14+
import { TerminalShellExecutionCommandLineConfidence } from 'vs/workbench/api/common/extHostTypes';
1415

1516
@extHostNamedCustomer(MainContext.MainThreadTerminalShellIntegration)
1617
export class MainThreadTerminalShellIntegration extends Disposable implements MainThreadTerminalShellIntegrationShape {
@@ -46,14 +47,14 @@ export class MainThreadTerminalShellIntegration extends Disposable implements Ma
4647
}
4748
// String paths are not exposed in the extension API
4849
currentCommand = e.data;
49-
this._proxy.$shellExecutionStart(e.instance.instanceId, e.data.command, this._convertCwdToUri(e.data.cwd));
50+
this._proxy.$shellExecutionStart(e.instance.instanceId, e.data.command, convertToExtHostCommandLineConfidence(e.data), this._convertCwdToUri(e.data.cwd));
5051
}));
5152

5253
// onDidEndTerminalShellExecution
5354
const commandDetectionEndEvent = this._store.add(this._terminalService.createOnInstanceCapabilityEvent(TerminalCapability.CommandDetection, e => e.onCommandFinished));
5455
this._store.add(commandDetectionEndEvent.event(e => {
5556
currentCommand = undefined;
56-
this._proxy.$shellExecutionEnd(e.instance.instanceId, e.data.command, e.data.exitCode);
57+
this._proxy.$shellExecutionEnd(e.instance.instanceId, e.data.command, convertToExtHostCommandLineConfidence(e.data), e.data.exitCode);
5758
}));
5859

5960
// onDidChangeTerminalShellIntegration via cwd
@@ -80,3 +81,15 @@ export class MainThreadTerminalShellIntegration extends Disposable implements Ma
8081
return cwd ? URI.file(cwd) : undefined;
8182
}
8283
}
84+
85+
function convertToExtHostCommandLineConfidence(command: ITerminalCommand): TerminalShellExecutionCommandLineConfidence {
86+
switch (command.commandLineConfidence) {
87+
case 'high':
88+
return TerminalShellExecutionCommandLineConfidence.High;
89+
case 'medium':
90+
return TerminalShellExecutionCommandLineConfidence.Medium;
91+
case 'low':
92+
default:
93+
return TerminalShellExecutionCommandLineConfidence.Low;
94+
}
95+
}

src/vs/workbench/api/common/extHost.api.impl.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1601,6 +1601,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
16011601
TerminalLocation: extHostTypes.TerminalLocation,
16021602
TerminalProfile: extHostTypes.TerminalProfile,
16031603
TerminalExitReason: extHostTypes.TerminalExitReason,
1604+
TerminalShellExecutionCommandLineConfidence: extHostTypes.TerminalShellExecutionCommandLineConfidence,
16041605
TextDocumentSaveReason: extHostTypes.TextDocumentSaveReason,
16051606
TextEdit: extHostTypes.TextEdit,
16061607
SnippetTextEdit: extHostTypes.SnippetTextEdit,

src/vs/workbench/api/common/extHost.protocol.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ import { CandidatePort } from 'vs/workbench/services/remote/common/tunnelModel';
8181
import { IFileQueryBuilderOptions, ITextQueryBuilderOptions } from 'vs/workbench/services/search/common/queryBuilder';
8282
import * as search from 'vs/workbench/services/search/common/search';
8383
import { ISaveProfileResult } from 'vs/workbench/services/userDataProfile/common/userDataProfile';
84+
import type { TerminalShellExecutionCommandLineConfidence } from 'vscode';
8485

8586
export interface IWorkspaceData extends IStaticWorkspaceData {
8687
folders: { uri: UriComponents; name: string; index: number }[];
@@ -2258,8 +2259,8 @@ export interface ExtHostTerminalServiceShape {
22582259

22592260
export interface ExtHostTerminalShellIntegrationShape {
22602261
$shellIntegrationChange(instanceId: number): void;
2261-
$shellExecutionStart(instanceId: number, commandLine: string | undefined, cwd: UriComponents | undefined): void;
2262-
$shellExecutionEnd(instanceId: number, commandLine: string | undefined, exitCode: number | undefined): void;
2262+
$shellExecutionStart(instanceId: number, commandLineValue: string, commandLineConfidence: TerminalShellExecutionCommandLineConfidence, cwd: UriComponents | undefined): void;
2263+
$shellExecutionEnd(instanceId: number, commandLineValue: string, commandLineConfidence: TerminalShellExecutionCommandLineConfidence, exitCode: number | undefined): void;
22632264
$shellExecutionData(instanceId: number, data: string): void;
22642265
$cwdChange(instanceId: number, cwd: UriComponents | undefined): void;
22652266
$closeTerminal(instanceId: number): void;

src/vs/workbench/api/common/extHostTerminalShellIntegration.ts

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import type * as vscode from 'vscode';
7+
import { TerminalShellExecutionCommandLineConfidence } from './extHostTypes';
78
import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
89
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
910
import { MainContext, type ExtHostTerminalShellIntegrationShape, type MainThreadTerminalShellIntegrationShape } from 'vs/workbench/api/common/extHost.protocol';
@@ -103,16 +104,24 @@ export class ExtHostTerminalShellIntegration extends Disposable implements IExtH
103104
});
104105
}
105106

106-
public $shellExecutionStart(instanceId: number, commandLine: string, cwd: URI | undefined): void {
107+
public $shellExecutionStart(instanceId: number, commandLineValue: string, commandLineConfidence: TerminalShellExecutionCommandLineConfidence, cwd: URI | undefined): void {
107108
// Force shellIntegration creation if it hasn't been created yet, this could when events
108109
// don't come through on startup
109110
if (!this._activeShellIntegrations.has(instanceId)) {
110111
this.$shellIntegrationChange(instanceId);
111112
}
113+
const commandLine: vscode.TerminalShellExecutionCommandLine = {
114+
value: commandLineValue,
115+
confidence: commandLineConfidence
116+
};
112117
this._activeShellIntegrations.get(instanceId)?.startShellExecution(commandLine, cwd);
113118
}
114119

115-
public $shellExecutionEnd(instanceId: number, commandLine: string | undefined, exitCode: number | undefined): void {
120+
public $shellExecutionEnd(instanceId: number, commandLineValue: string, commandLineConfidence: TerminalShellExecutionCommandLineConfidence, exitCode: number | undefined): void {
121+
const commandLine: vscode.TerminalShellExecutionCommandLine = {
122+
value: commandLineValue,
123+
confidence: commandLineConfidence
124+
};
116125
this._activeShellIntegrations.get(instanceId)?.endShellExecution(commandLine, exitCode);
117126
}
118127

@@ -160,23 +169,34 @@ class InternalTerminalShellIntegration extends Disposable {
160169
get cwd(): URI | undefined {
161170
return that._cwd;
162171
},
163-
executeCommand(commandLine): vscode.TerminalShellExecution {
164-
that._onDidRequestShellExecution.fire(commandLine);
172+
// executeCommand(commandLine: string): vscode.TerminalShellExecution;
173+
// executeCommand(executable: string, args: string[]): vscode.TerminalShellExecution;
174+
executeCommand(commandLineOrExecutable: string, args?: string[]): vscode.TerminalShellExecution {
175+
let commandLineValue: string = commandLineOrExecutable;
176+
if (args) {
177+
commandLineValue += ` "${args.map(e => `${e.replaceAll('"', '\\"')}`).join('" "')}"`;
178+
}
179+
180+
that._onDidRequestShellExecution.fire(commandLineValue);
165181
// Fire the event in a microtask to allow the extension to use the execution before
166182
// the start event fires
183+
const commandLine: vscode.TerminalShellExecutionCommandLine = {
184+
value: commandLineValue,
185+
confidence: TerminalShellExecutionCommandLineConfidence.High
186+
};
167187
const execution = that.startShellExecution(commandLine, that._cwd, true).value;
168188
that._ignoreNextExecution = true;
169189
return execution;
170190
}
171191
};
172192
}
173193

174-
startShellExecution(commandLine: string, cwd: URI | undefined, fireEventInMicrotask?: boolean): InternalTerminalShellExecution {
194+
startShellExecution(commandLine: vscode.TerminalShellExecutionCommandLine, cwd: URI | undefined, fireEventInMicrotask?: boolean): InternalTerminalShellExecution {
175195
if (this._ignoreNextExecution && this._currentExecution) {
176196
this._ignoreNextExecution = false;
177197
} else {
178198
if (this._currentExecution) {
179-
this._currentExecution.endExecution(undefined, undefined);
199+
this._currentExecution.endExecution(undefined);
180200
this._onDidRequestEndExecution.fire({ execution: this._currentExecution.value, exitCode: undefined });
181201
}
182202
const currentExecution = this._currentExecution = new InternalTerminalShellExecution(this._terminal, commandLine, cwd);
@@ -193,9 +213,9 @@ class InternalTerminalShellIntegration extends Disposable {
193213
this.currentExecution?.emitData(data);
194214
}
195215

196-
endShellExecution(commandLine: string | undefined, exitCode: number | undefined): void {
216+
endShellExecution(commandLine: vscode.TerminalShellExecutionCommandLine | undefined, exitCode: number | undefined): void {
197217
if (this._currentExecution) {
198-
this._currentExecution.endExecution(commandLine, exitCode);
218+
this._currentExecution.endExecution(commandLine);
199219
this._onDidRequestEndExecution.fire({ execution: this._currentExecution.value, exitCode });
200220
this._currentExecution = undefined;
201221
}
@@ -224,15 +244,15 @@ class InternalTerminalShellExecution {
224244

225245
constructor(
226246
readonly terminal: vscode.Terminal,
227-
private _commandLine: string | undefined,
247+
private _commandLine: vscode.TerminalShellExecutionCommandLine,
228248
readonly cwd: URI | undefined,
229249
) {
230250
const that = this;
231251
this.value = {
232252
get terminal(): vscode.Terminal {
233253
return that.terminal;
234254
},
235-
get commandLine(): string | undefined {
255+
get commandLine(): vscode.TerminalShellExecutionCommandLine {
236256
return that._commandLine;
237257
},
238258
get cwd(): URI | undefined {
@@ -258,7 +278,7 @@ class InternalTerminalShellExecution {
258278
this._dataStream?.emitData(data);
259279
}
260280

261-
endExecution(commandLine: string | undefined, exitCode: number | undefined): void {
281+
endExecution(commandLine: vscode.TerminalShellExecutionCommandLine | undefined): void {
262282
if (commandLine) {
263283
this._commandLine = commandLine;
264284
}

src/vs/workbench/api/common/extHostTypes.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2001,6 +2001,12 @@ export enum TerminalExitReason {
20012001
Extension = 4
20022002
}
20032003

2004+
export enum TerminalShellExecutionCommandLineConfidence {
2005+
Low = 0,
2006+
Medium = 1,
2007+
High = 2
2008+
}
2009+
20042010
export class TerminalLink implements vscode.TerminalLink {
20052011
constructor(
20062012
public startIndex: number,

src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkOpeners.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ suite('Workbench - TerminalLinkOpeners', () => {
136136
// Set a fake detected command starting as line 0 to establish the cwd
137137
commandDetection.setCommands([new TerminalCommand(xterm, {
138138
command: '',
139+
commandLineConfidence: 'low',
139140
exitCode: 0,
140141
commandStartLineContent: '',
141142
markProperties: {},
@@ -277,6 +278,7 @@ suite('Workbench - TerminalLinkOpeners', () => {
277278
// Set a fake detected command starting as line 0 to establish the cwd
278279
commandDetection.setCommands([new TerminalCommand(xterm, {
279280
command: '',
281+
commandLineConfidence: 'low',
280282
isTrusted: true,
281283
cwd,
282284
timestamp: 0,
@@ -538,6 +540,7 @@ suite('Workbench - TerminalLinkOpeners', () => {
538540
commandStartLineContent: '',
539541
markProperties: {},
540542
command: '',
543+
commandLineConfidence: 'low',
541544
isTrusted: true,
542545
cwd,
543546
executedX: undefined,

src/vscode-dts/vscode.proposed.terminalShellIntegration.d.ts

Lines changed: 50 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,13 @@ declare module 'vscode' {
1717
readonly terminal: Terminal;
1818

1919
/**
20-
* The full command line that was executed, including both the command and arguments.
21-
* The accuracy of this value depends on the shell integration implementation:
22-
*
23-
* - It may be undefined or the empty string until {@link onDidEndTerminalShellExecution} is
24-
* fired.
25-
* - It may be inaccurate initially if the command line is pulled from the buffer directly
26-
* via the shell integration prompt markers.
27-
* - It may contain line continuation characters and/or parts of the right prompt.
28-
* - It may be inaccurate if the shell integration does not support command line reporting.
29-
*/
30-
// TODO: Remove | undefined - this will be the empty string if the command start and end is the same position
20+
* The full command line that was executed, including both the command and arguments. The
21+
* {@link TerminalShellExecutionCommandLineConfidence confidence} of this value depends on
22+
* the specific shell's shell integration implementation. This value may become more
23+
* accurate after {@link onDidEndTerminalShellExecution} is fired.
24+
*/
3125
// TODO: Implement command line fetching via buffer markers
32-
// TODO: Quality/confidence 3x:
33-
// - Top: shell integration reporting
34-
// - Middle: Not multi-line, command start is not on the left-most column
35-
// - Bottom: Multi-line or command start is on the left-most column
36-
readonly commandLine: string | undefined;
26+
readonly commandLine: TerminalShellExecutionCommandLine;
3727

3828
/**
3929
* The working directory that was reported by the shell when this command executed. This
@@ -58,9 +48,53 @@ declare module 'vscode' {
5848
* }
5949
*/
6050
// TODO: read? "data" typically means Uint8Array. What's the encoding of the string? Usage here will typically be checking for substrings
51+
// TODO: dispose function?
6152
readData(): AsyncIterable<string>;
6253
}
6354

55+
/**
56+
* A command line that was executed in a terminal.
57+
*/
58+
export interface TerminalShellExecutionCommandLine {
59+
/**
60+
* The full command line that was executed, including both the command and its arguments.
61+
*/
62+
value: string;
63+
64+
/**
65+
* The confidence of the command line value which is determined by how the value was
66+
* obtained. This depends upon the implementation of the shell integration script.
67+
*/
68+
confidence: TerminalShellExecutionCommandLineConfidence;
69+
}
70+
71+
/**
72+
* The confidence of a {@link TerminalShellExecutionCommandLine} value.
73+
*/
74+
enum TerminalShellExecutionCommandLineConfidence {
75+
/**
76+
* The command line value confidence is low. This means that the value was read from the
77+
* terminal buffer using markers reported by the shell integration script. Additionally the
78+
* command either started on the very left-most column which is unusual, or the command is
79+
* multi-line which is more difficult to accurately detect due to line continuation
80+
* characters and right prompts.
81+
*/
82+
Low = 0,
83+
84+
/**
85+
* The command line value confidence is medium. This means that the value was read from the
86+
* terminal buffer using markers reported by the shell integration script. The command is
87+
* single-line and does not start on the very left-most column (which is unusual).
88+
*/
89+
Medium = 1,
90+
91+
/**
92+
* The command line value confidence is high. This means that the value was explicitly send
93+
* from the shell integration script.
94+
*/
95+
High = 2
96+
}
97+
6498
export interface Terminal {
6599
/**
66100
* An object that contains [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration)-powered

0 commit comments

Comments
 (0)