Skip to content

Commit 7c1fa38

Browse files
authored
Merge pull request microsoft#209946 from microsoft/tyriar/145234_confidence
Add command line confidence to shellIntegration API
2 parents 9cc18d7 + 37e5596 commit 7c1fa38

File tree

13 files changed

+240
-53
lines changed

13 files changed

+240
-53
lines changed

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

Lines changed: 2 additions & 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;
@@ -262,6 +263,7 @@ export interface ITerminalCommand extends IBaseTerminalCommand {
262263
readonly aliases?: string[][];
263264
readonly wasReplayed?: boolean;
264265

266+
extractCommandLine(): string;
265267
getOutput(): string | undefined;
266268
getOutputMatch(outputMatcher: ITerminalOutputMatcher): ITerminalOutputMatch | undefined;
267269
hasOutput(): boolean;

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

Lines changed: 36 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,
@@ -109,6 +113,10 @@ export class TerminalCommand implements ITerminalCommand {
109113
};
110114
}
111115

116+
extractCommandLine(): string {
117+
return extractCommandLine(this._xterm.buffer.active, this._xterm.cols, this.marker, this.startX, this.executedMarker, this.executedX);
118+
}
119+
112120
getOutput(): string | undefined {
113121
if (!this.executedMarker || !this.endMarker) {
114122
return undefined;
@@ -265,6 +273,7 @@ export class PartialTerminalCommand implements ICurrentPartialCommand {
265273

266274
cwd?: string;
267275
command?: string;
276+
commandLineConfidence?: 'low' | 'medium' | 'high';
268277

269278
isTrusted?: boolean;
270279
isInvalid?: boolean;
@@ -287,6 +296,7 @@ export class PartialTerminalCommand implements ICurrentPartialCommand {
287296
executedLine: undefined,
288297
executedX: undefined,
289298
command: '',
299+
commandLineConfidence: 'low',
290300
isTrusted: true,
291301
cwd,
292302
exitCode: undefined,
@@ -306,6 +316,7 @@ export class PartialTerminalCommand implements ICurrentPartialCommand {
306316
if ((this.command !== undefined && !this.command.startsWith('\\')) || ignoreCommandLine) {
307317
return new TerminalCommand(this._xterm, {
308318
command: ignoreCommandLine ? '' : (this.command || ''),
319+
commandLineConfidence: ignoreCommandLine ? 'low' : (this.commandLineConfidence || 'low'),
309320
isTrusted: !!this.isTrusted,
310321
promptStartMarker: this.promptStartMarker,
311322
marker: this.commandStartMarker,
@@ -337,6 +348,10 @@ export class PartialTerminalCommand implements ICurrentPartialCommand {
337348
}
338349
}
339350

351+
extractCommandLine(): string {
352+
return extractCommandLine(this._xterm.buffer.active, this._xterm.cols, this.commandStartMarker, this.commandStartX, this.commandExecutedMarker, this.commandExecutedX);
353+
}
354+
340355
getPromptRowCount(): number {
341356
return getPromptRowCount(this, this._xterm.buffer.active);
342357
}
@@ -346,6 +361,27 @@ export class PartialTerminalCommand implements ICurrentPartialCommand {
346361
}
347362
}
348363

364+
function extractCommandLine(
365+
buffer: IBuffer,
366+
cols: number,
367+
commandStartMarker: IXtermMarker | undefined,
368+
commandStartX: number | undefined,
369+
commandExecutedMarker: IXtermMarker | undefined,
370+
commandExecutedX: number | undefined
371+
): string {
372+
if (!commandStartMarker || !commandExecutedMarker || commandStartX === undefined || commandExecutedX === undefined) {
373+
return '';
374+
}
375+
let content = '';
376+
for (let i = commandStartMarker.line; i <= commandExecutedMarker.line; i++) {
377+
const line = buffer.getLine(i);
378+
if (line) {
379+
content += line.translateToString(true, i === commandStartMarker.line ? commandStartX : 0, i === commandExecutedMarker.line ? commandExecutedX : cols);
380+
}
381+
}
382+
return content;
383+
}
384+
349385
function getXtermLineContent(buffer: IBuffer, lineStart: number, lineEnd: number, cols: number): string {
350386
// Cap the maximum number of lines generated to prevent potential performance problems. This is
351387
// more of a sanity check as the wrapped line should already be trimmed down at this point.

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

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

90+
// Pull command line from the buffer if it was not set explicitly
91+
this._register(this.onCommandExecuted(command => {
92+
if (command.commandLineConfidence !== 'high') {
93+
// HACK: onCommandExecuted actually fired with PartialTerminalCommand
94+
const typedCommand = (command as ITerminalCommand | PartialTerminalCommand);
95+
command.command = typedCommand.extractCommandLine();
96+
command.commandLineConfidence = 'low';
97+
98+
// ITerminalCommand
99+
if ('getOutput' in typedCommand) {
100+
if (
101+
// Markers exist
102+
typedCommand.promptStartMarker && typedCommand.marker && typedCommand.executedMarker &&
103+
// Single line command
104+
command.command.indexOf('\n') === -1 &&
105+
// Start marker is not on the left-most column
106+
typedCommand.startX !== undefined && typedCommand.startX > 0
107+
) {
108+
command.commandLineConfidence = 'medium';
109+
}
110+
}
111+
// PartialTerminalCommand
112+
else {
113+
if (
114+
// Markers exist
115+
typedCommand.promptStartMarker && typedCommand.commandStartMarker && typedCommand.commandExecutedMarker &&
116+
// Single line command
117+
command.command.indexOf('\n') === -1 &&
118+
// Start marker is not on the left-most column
119+
typedCommand.commandStartX !== undefined && typedCommand.commandStartX > 0
120+
) {
121+
command.commandLineConfidence = 'medium';
122+
}
123+
}
124+
}
125+
}));
126+
90127
// Set up platform-specific behaviors
91128
const that = this;
92129
this._ptyHeuristicsHooks = new class implements ICommandDetectionHeuristicsHooks {
@@ -353,6 +390,7 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe
353390
setCommandLine(commandLine: string, isTrusted: boolean) {
354391
this._logService.debug('CommandDetectionCapability#setCommandLine', commandLine, isTrusted);
355392
this._currentCommand.command = commandLine;
393+
this._currentCommand.commandLineConfidence = 'high';
356394
this._currentCommand.isTrusted = isTrusted;
357395
}
358396

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), e.data.isTrusted, 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.isTrusted, 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, isTrusted: boolean, cwd: UriComponents | undefined): void;
2263+
$shellExecutionEnd(instanceId: number, commandLineValue: string, commandLineConfidence: TerminalShellExecutionCommandLineConfidence, isTrusted: boolean, 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: 42 additions & 23 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';
@@ -17,7 +18,7 @@ export interface IExtHostTerminalShellIntegration extends ExtHostTerminalShellIn
1718
readonly _serviceBrand: undefined;
1819

1920
readonly onDidChangeTerminalShellIntegration: Event<vscode.TerminalShellIntegrationChangeEvent>;
20-
readonly onDidStartTerminalShellExecution: Event<vscode.TerminalShellExecution>;
21+
readonly onDidStartTerminalShellExecution: Event<vscode.TerminalShellExecutionStartEvent>;
2122
readonly onDidEndTerminalShellExecution: Event<vscode.TerminalShellExecutionEndEvent>;
2223
}
2324
export const IExtHostTerminalShellIntegration = createDecorator<IExtHostTerminalShellIntegration>('IExtHostTerminalShellIntegration');
@@ -32,7 +33,7 @@ export class ExtHostTerminalShellIntegration extends Disposable implements IExtH
3233

3334
protected readonly _onDidChangeTerminalShellIntegration = new Emitter<vscode.TerminalShellIntegrationChangeEvent>();
3435
readonly onDidChangeTerminalShellIntegration = this._onDidChangeTerminalShellIntegration.event;
35-
protected readonly _onDidStartTerminalShellExecution = new Emitter<vscode.TerminalShellExecution>();
36+
protected readonly _onDidStartTerminalShellExecution = new Emitter<vscode.TerminalShellExecutionStartEvent>();
3637
readonly onDidStartTerminalShellExecution = this._onDidStartTerminalShellExecution.event;
3738
protected readonly _onDidEndTerminalShellExecution = new Emitter<vscode.TerminalShellExecutionEndEvent>();
3839
readonly onDidEndTerminalShellExecution = this._onDidEndTerminalShellExecution.event;
@@ -103,16 +104,26 @@ 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, isTrusted: boolean, 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+
isTrusted
117+
};
112118
this._activeShellIntegrations.get(instanceId)?.startShellExecution(commandLine, cwd);
113119
}
114120

115-
public $shellExecutionEnd(instanceId: number, commandLine: string | undefined, exitCode: number | undefined): void {
121+
public $shellExecutionEnd(instanceId: number, commandLineValue: string, commandLineConfidence: TerminalShellExecutionCommandLineConfidence, isTrusted: boolean, exitCode: number | undefined): void {
122+
const commandLine: vscode.TerminalShellExecutionCommandLine = {
123+
value: commandLineValue,
124+
confidence: commandLineConfidence,
125+
isTrusted
126+
};
116127
this._activeShellIntegrations.get(instanceId)?.endShellExecution(commandLine, exitCode);
117128
}
118129

@@ -151,7 +162,7 @@ class InternalTerminalShellIntegration extends Disposable {
151162

152163
constructor(
153164
private readonly _terminal: vscode.Terminal,
154-
private readonly _onDidStartTerminalShellExecution: Emitter<vscode.TerminalShellExecution>
165+
private readonly _onDidStartTerminalShellExecution: Emitter<vscode.TerminalShellExecutionStartEvent>
155166
) {
156167
super();
157168

@@ -160,30 +171,42 @@ class InternalTerminalShellIntegration extends Disposable {
160171
get cwd(): URI | undefined {
161172
return that._cwd;
162173
},
163-
executeCommand(commandLine): vscode.TerminalShellExecution {
164-
that._onDidRequestShellExecution.fire(commandLine);
174+
// executeCommand(commandLine: string): vscode.TerminalShellExecution;
175+
// executeCommand(executable: string, args: string[]): vscode.TerminalShellExecution;
176+
executeCommand(commandLineOrExecutable: string, args?: string[]): vscode.TerminalShellExecution {
177+
let commandLineValue: string = commandLineOrExecutable;
178+
if (args) {
179+
commandLineValue += ` "${args.map(e => `${e.replaceAll('"', '\\"')}`).join('" "')}"`;
180+
}
181+
182+
that._onDidRequestShellExecution.fire(commandLineValue);
165183
// Fire the event in a microtask to allow the extension to use the execution before
166184
// the start event fires
185+
const commandLine: vscode.TerminalShellExecutionCommandLine = {
186+
value: commandLineValue,
187+
confidence: TerminalShellExecutionCommandLineConfidence.High,
188+
isTrusted: true
189+
};
167190
const execution = that.startShellExecution(commandLine, that._cwd, true).value;
168191
that._ignoreNextExecution = true;
169192
return execution;
170193
}
171194
};
172195
}
173196

174-
startShellExecution(commandLine: string, cwd: URI | undefined, fireEventInMicrotask?: boolean): InternalTerminalShellExecution {
197+
startShellExecution(commandLine: vscode.TerminalShellExecutionCommandLine, cwd: URI | undefined, fireEventInMicrotask?: boolean): InternalTerminalShellExecution {
175198
if (this._ignoreNextExecution && this._currentExecution) {
176199
this._ignoreNextExecution = false;
177200
} else {
178201
if (this._currentExecution) {
179-
this._currentExecution.endExecution(undefined, undefined);
180-
this._onDidRequestEndExecution.fire({ execution: this._currentExecution.value, exitCode: undefined });
202+
this._currentExecution.endExecution(undefined);
203+
this._onDidRequestEndExecution.fire({ terminal: this._terminal, shellIntegration: this.value, execution: this._currentExecution.value, exitCode: undefined });
181204
}
182-
const currentExecution = this._currentExecution = new InternalTerminalShellExecution(this._terminal, commandLine, cwd);
205+
const currentExecution = this._currentExecution = new InternalTerminalShellExecution(commandLine, cwd);
183206
if (fireEventInMicrotask) {
184-
queueMicrotask(() => this._onDidStartTerminalShellExecution.fire(currentExecution.value));
207+
queueMicrotask(() => this._onDidStartTerminalShellExecution.fire({ terminal: this._terminal, shellIntegration: this.value, execution: currentExecution.value }));
185208
} else {
186-
this._onDidStartTerminalShellExecution.fire(this._currentExecution.value);
209+
this._onDidStartTerminalShellExecution.fire({ terminal: this._terminal, shellIntegration: this.value, execution: this._currentExecution.value });
187210
}
188211
}
189212
return this._currentExecution;
@@ -193,10 +216,10 @@ class InternalTerminalShellIntegration extends Disposable {
193216
this.currentExecution?.emitData(data);
194217
}
195218

196-
endShellExecution(commandLine: string | undefined, exitCode: number | undefined): void {
219+
endShellExecution(commandLine: vscode.TerminalShellExecutionCommandLine | undefined, exitCode: number | undefined): void {
197220
if (this._currentExecution) {
198-
this._currentExecution.endExecution(commandLine, exitCode);
199-
this._onDidRequestEndExecution.fire({ execution: this._currentExecution.value, exitCode });
221+
this._currentExecution.endExecution(commandLine);
222+
this._onDidRequestEndExecution.fire({ terminal: this._terminal, shellIntegration: this.value, execution: this._currentExecution.value, exitCode });
200223
this._currentExecution = undefined;
201224
}
202225
}
@@ -223,16 +246,12 @@ class InternalTerminalShellExecution {
223246
readonly value: vscode.TerminalShellExecution;
224247

225248
constructor(
226-
readonly terminal: vscode.Terminal,
227-
private _commandLine: string | undefined,
249+
private _commandLine: vscode.TerminalShellExecutionCommandLine,
228250
readonly cwd: URI | undefined,
229251
) {
230252
const that = this;
231253
this.value = {
232-
get terminal(): vscode.Terminal {
233-
return that.terminal;
234-
},
235-
get commandLine(): string | undefined {
254+
get commandLine(): vscode.TerminalShellExecutionCommandLine {
236255
return that._commandLine;
237256
},
238257
get cwd(): URI | undefined {
@@ -258,7 +277,7 @@ class InternalTerminalShellExecution {
258277
this._dataStream?.emitData(data);
259278
}
260279

261-
endExecution(commandLine: string | undefined, exitCode: number | undefined): void {
280+
endExecution(commandLine: vscode.TerminalShellExecutionCommandLine | undefined): void {
262281
if (commandLine) {
263282
this._commandLine = commandLine;
264283
}

0 commit comments

Comments
 (0)