diff --git a/src/vs/platform/terminal/common/capabilities/capabilities.ts b/src/vs/platform/terminal/common/capabilities/capabilities.ts index 8c6575f872e..dc0db6c0bb1 100644 --- a/src/vs/platform/terminal/common/capabilities/capabilities.ts +++ b/src/vs/platform/terminal/common/capabilities/capabilities.ts @@ -240,6 +240,7 @@ export interface IPartialCommandDetectionCapability { interface IBaseTerminalCommand { // Mandatory command: string; + commandLineConfidence: 'low' | 'medium' | 'high'; isTrusted: boolean; timestamp: number; duration: number; diff --git a/src/vs/platform/terminal/common/capabilities/commandDetection/terminalCommand.ts b/src/vs/platform/terminal/common/capabilities/commandDetection/terminalCommand.ts index afcae005819..d49d69e61a5 100644 --- a/src/vs/platform/terminal/common/capabilities/commandDetection/terminalCommand.ts +++ b/src/vs/platform/terminal/common/capabilities/commandDetection/terminalCommand.ts @@ -12,6 +12,7 @@ import type { IBuffer, IBufferLine, Terminal } from '@xterm/headless'; export interface ITerminalCommandProperties { command: string; + commandLineConfidence: 'low' | 'medium' | 'high'; isTrusted: boolean; timestamp: number; duration: number; @@ -33,6 +34,7 @@ export interface ITerminalCommandProperties { export class TerminalCommand implements ITerminalCommand { get command() { return this._properties.command; } + get commandLineConfidence() { return this._properties.commandLineConfidence; } get isTrusted() { return this._properties.isTrusted; } get timestamp() { return this._properties.timestamp; } get duration() { return this._properties.duration; } @@ -71,6 +73,7 @@ export class TerminalCommand implements ITerminalCommand { const executedMarker = serialized.executedLine !== undefined ? xterm.registerMarker(serialized.executedLine - (buffer.baseY + buffer.cursorY)) : undefined; const newCommand = new TerminalCommand(xterm, { command: isCommandStorageDisabled ? '' : serialized.command, + commandLineConfidence: serialized.commandLineConfidence ?? 'low', isTrusted: serialized.isTrusted, promptStartMarker, marker, @@ -99,6 +102,7 @@ export class TerminalCommand implements ITerminalCommand { executedLine: this.executedMarker?.line, executedX: this.executedX, command: isCommandStorageDisabled ? '' : this.command, + commandLineConfidence: isCommandStorageDisabled ? 'low' : this.commandLineConfidence, isTrusted: this.isTrusted, cwd: this.cwd, exitCode: this.exitCode, @@ -265,6 +269,7 @@ export class PartialTerminalCommand implements ICurrentPartialCommand { cwd?: string; command?: string; + commandLineConfidence?: 'low' | 'medium' | 'high'; isTrusted?: boolean; isInvalid?: boolean; @@ -287,6 +292,7 @@ export class PartialTerminalCommand implements ICurrentPartialCommand { executedLine: undefined, executedX: undefined, command: '', + commandLineConfidence: 'low', isTrusted: true, cwd, exitCode: undefined, @@ -306,6 +312,7 @@ export class PartialTerminalCommand implements ICurrentPartialCommand { if ((this.command !== undefined && !this.command.startsWith('\\')) || ignoreCommandLine) { return new TerminalCommand(this._xterm, { command: ignoreCommandLine ? '' : (this.command || ''), + commandLineConfidence: ignoreCommandLine ? 'low' : (this.commandLineConfidence || 'low'), isTrusted: !!this.isTrusted, promptStartMarker: this.promptStartMarker, marker: this.commandStartMarker, diff --git a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts index 4a9fe8305bd..3e16cbd70df 100644 --- a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts +++ b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts @@ -87,6 +87,14 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe ) { super(); + this.onCommandExecuted(command => { + // Pull command line from the buffer if it was not set explicitly + if (command.commandLineConfidence !== 'high') { + + console.log('commandLineConfidence !== high', command.command); + } + }); + // Set up platform-specific behaviors const that = this; this._ptyHeuristicsHooks = new class implements ICommandDetectionHeuristicsHooks { @@ -353,6 +361,7 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe setCommandLine(commandLine: string, isTrusted: boolean) { this._logService.debug('CommandDetectionCapability#setCommandLine', commandLine, isTrusted); this._currentCommand.command = commandLine; + this._currentCommand.commandLineConfidence = 'high'; this._currentCommand.isTrusted = isTrusted; } diff --git a/src/vs/workbench/api/browser/mainThreadTerminalShellIntegration.ts b/src/vs/workbench/api/browser/mainThreadTerminalShellIntegration.ts index 97057f0628b..763b9bd0f0f 100644 --- a/src/vs/workbench/api/browser/mainThreadTerminalShellIntegration.ts +++ b/src/vs/workbench/api/browser/mainThreadTerminalShellIntegration.ts @@ -11,6 +11,7 @@ import { ExtHostContext, MainContext, type ExtHostTerminalShellIntegrationShape, import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { extHostNamedCustomer, type IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; +import { TerminalShellExecutionCommandLineConfidence } from 'vs/workbench/api/common/extHostTypes'; @extHostNamedCustomer(MainContext.MainThreadTerminalShellIntegration) export class MainThreadTerminalShellIntegration extends Disposable implements MainThreadTerminalShellIntegrationShape { @@ -46,14 +47,14 @@ export class MainThreadTerminalShellIntegration extends Disposable implements Ma } // String paths are not exposed in the extension API currentCommand = e.data; - this._proxy.$shellExecutionStart(e.instance.instanceId, e.data.command, this._convertCwdToUri(e.data.cwd)); + this._proxy.$shellExecutionStart(e.instance.instanceId, e.data.command, convertToExtHostCommandLineConfidence(e.data), this._convertCwdToUri(e.data.cwd)); })); // onDidEndTerminalShellExecution const commandDetectionEndEvent = this._store.add(this._terminalService.createOnInstanceCapabilityEvent(TerminalCapability.CommandDetection, e => e.onCommandFinished)); this._store.add(commandDetectionEndEvent.event(e => { currentCommand = undefined; - this._proxy.$shellExecutionEnd(e.instance.instanceId, e.data.command, e.data.exitCode); + this._proxy.$shellExecutionEnd(e.instance.instanceId, e.data.command, convertToExtHostCommandLineConfidence(e.data), e.data.exitCode); })); // onDidChangeTerminalShellIntegration via cwd @@ -80,3 +81,15 @@ export class MainThreadTerminalShellIntegration extends Disposable implements Ma return cwd ? URI.file(cwd) : undefined; } } + +function convertToExtHostCommandLineConfidence(command: ITerminalCommand): TerminalShellExecutionCommandLineConfidence { + switch (command.commandLineConfidence) { + case 'high': + return TerminalShellExecutionCommandLineConfidence.High; + case 'medium': + return TerminalShellExecutionCommandLineConfidence.Medium; + case 'low': + default: + return TerminalShellExecutionCommandLineConfidence.Low; + } +} diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 1599c9e4500..e732118e7e9 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1601,6 +1601,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I TerminalLocation: extHostTypes.TerminalLocation, TerminalProfile: extHostTypes.TerminalProfile, TerminalExitReason: extHostTypes.TerminalExitReason, + TerminalShellExecutionCommandLineConfidence: extHostTypes.TerminalShellExecutionCommandLineConfidence, TextDocumentSaveReason: extHostTypes.TextDocumentSaveReason, TextEdit: extHostTypes.TextEdit, SnippetTextEdit: extHostTypes.SnippetTextEdit, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index b07d4b49db8..bd0145150da 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -81,6 +81,7 @@ import { CandidatePort } from 'vs/workbench/services/remote/common/tunnelModel'; import { IFileQueryBuilderOptions, ITextQueryBuilderOptions } from 'vs/workbench/services/search/common/queryBuilder'; import * as search from 'vs/workbench/services/search/common/search'; import { ISaveProfileResult } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import type { TerminalShellExecutionCommandLineConfidence } from 'vscode'; export interface IWorkspaceData extends IStaticWorkspaceData { folders: { uri: UriComponents; name: string; index: number }[]; @@ -2258,8 +2259,8 @@ export interface ExtHostTerminalServiceShape { export interface ExtHostTerminalShellIntegrationShape { $shellIntegrationChange(instanceId: number): void; - $shellExecutionStart(instanceId: number, commandLine: string | undefined, cwd: UriComponents | undefined): void; - $shellExecutionEnd(instanceId: number, commandLine: string | undefined, exitCode: number | undefined): void; + $shellExecutionStart(instanceId: number, commandLineValue: string, commandLineConfidence: TerminalShellExecutionCommandLineConfidence, cwd: UriComponents | undefined): void; + $shellExecutionEnd(instanceId: number, commandLineValue: string, commandLineConfidence: TerminalShellExecutionCommandLineConfidence, exitCode: number | undefined): void; $shellExecutionData(instanceId: number, data: string): void; $cwdChange(instanceId: number, cwd: UriComponents | undefined): void; $closeTerminal(instanceId: number): void; diff --git a/src/vs/workbench/api/common/extHostTerminalShellIntegration.ts b/src/vs/workbench/api/common/extHostTerminalShellIntegration.ts index f6ad6799612..83e04bf9d58 100644 --- a/src/vs/workbench/api/common/extHostTerminalShellIntegration.ts +++ b/src/vs/workbench/api/common/extHostTerminalShellIntegration.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import type * as vscode from 'vscode'; +import { TerminalShellExecutionCommandLineConfidence } from './extHostTypes'; import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { MainContext, type ExtHostTerminalShellIntegrationShape, type MainThreadTerminalShellIntegrationShape } from 'vs/workbench/api/common/extHost.protocol'; @@ -103,16 +104,24 @@ export class ExtHostTerminalShellIntegration extends Disposable implements IExtH }); } - public $shellExecutionStart(instanceId: number, commandLine: string, cwd: URI | undefined): void { + public $shellExecutionStart(instanceId: number, commandLineValue: string, commandLineConfidence: TerminalShellExecutionCommandLineConfidence, cwd: URI | undefined): void { // Force shellIntegration creation if it hasn't been created yet, this could when events // don't come through on startup if (!this._activeShellIntegrations.has(instanceId)) { this.$shellIntegrationChange(instanceId); } + const commandLine: vscode.TerminalShellExecutionCommandLine = { + value: commandLineValue, + confidence: commandLineConfidence + }; this._activeShellIntegrations.get(instanceId)?.startShellExecution(commandLine, cwd); } - public $shellExecutionEnd(instanceId: number, commandLine: string | undefined, exitCode: number | undefined): void { + public $shellExecutionEnd(instanceId: number, commandLineValue: string, commandLineConfidence: TerminalShellExecutionCommandLineConfidence, exitCode: number | undefined): void { + const commandLine: vscode.TerminalShellExecutionCommandLine = { + value: commandLineValue, + confidence: commandLineConfidence + }; this._activeShellIntegrations.get(instanceId)?.endShellExecution(commandLine, exitCode); } @@ -160,10 +169,21 @@ class InternalTerminalShellIntegration extends Disposable { get cwd(): URI | undefined { return that._cwd; }, - executeCommand(commandLine): vscode.TerminalShellExecution { - that._onDidRequestShellExecution.fire(commandLine); + // executeCommand(commandLine: string): vscode.TerminalShellExecution; + // executeCommand(executable: string, args: string[]): vscode.TerminalShellExecution; + executeCommand(commandLineOrExecutable: string, args?: string[]): vscode.TerminalShellExecution { + let commandLineValue: string = commandLineOrExecutable; + if (args) { + commandLineValue += ` "${args.map(e => `${e.replaceAll('"', '\\"')}`).join('" "')}"`; + } + + that._onDidRequestShellExecution.fire(commandLineValue); // Fire the event in a microtask to allow the extension to use the execution before // the start event fires + const commandLine: vscode.TerminalShellExecutionCommandLine = { + value: commandLineValue, + confidence: TerminalShellExecutionCommandLineConfidence.High + }; const execution = that.startShellExecution(commandLine, that._cwd, true).value; that._ignoreNextExecution = true; return execution; @@ -171,12 +191,12 @@ class InternalTerminalShellIntegration extends Disposable { }; } - startShellExecution(commandLine: string, cwd: URI | undefined, fireEventInMicrotask?: boolean): InternalTerminalShellExecution { + startShellExecution(commandLine: vscode.TerminalShellExecutionCommandLine, cwd: URI | undefined, fireEventInMicrotask?: boolean): InternalTerminalShellExecution { if (this._ignoreNextExecution && this._currentExecution) { this._ignoreNextExecution = false; } else { if (this._currentExecution) { - this._currentExecution.endExecution(undefined, undefined); + this._currentExecution.endExecution(undefined); this._onDidRequestEndExecution.fire({ execution: this._currentExecution.value, exitCode: undefined }); } const currentExecution = this._currentExecution = new InternalTerminalShellExecution(this._terminal, commandLine, cwd); @@ -193,9 +213,9 @@ class InternalTerminalShellIntegration extends Disposable { this.currentExecution?.emitData(data); } - endShellExecution(commandLine: string | undefined, exitCode: number | undefined): void { + endShellExecution(commandLine: vscode.TerminalShellExecutionCommandLine | undefined, exitCode: number | undefined): void { if (this._currentExecution) { - this._currentExecution.endExecution(commandLine, exitCode); + this._currentExecution.endExecution(commandLine); this._onDidRequestEndExecution.fire({ execution: this._currentExecution.value, exitCode }); this._currentExecution = undefined; } @@ -224,7 +244,7 @@ class InternalTerminalShellExecution { constructor( readonly terminal: vscode.Terminal, - private _commandLine: string | undefined, + private _commandLine: vscode.TerminalShellExecutionCommandLine, readonly cwd: URI | undefined, ) { const that = this; @@ -232,7 +252,7 @@ class InternalTerminalShellExecution { get terminal(): vscode.Terminal { return that.terminal; }, - get commandLine(): string | undefined { + get commandLine(): vscode.TerminalShellExecutionCommandLine { return that._commandLine; }, get cwd(): URI | undefined { @@ -258,7 +278,7 @@ class InternalTerminalShellExecution { this._dataStream?.emitData(data); } - endExecution(commandLine: string | undefined, exitCode: number | undefined): void { + endExecution(commandLine: vscode.TerminalShellExecutionCommandLine | undefined): void { if (commandLine) { this._commandLine = commandLine; } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 52ede752dd3..d82ecb98502 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -2001,6 +2001,12 @@ export enum TerminalExitReason { Extension = 4 } +export enum TerminalShellExecutionCommandLineConfidence { + Low = 0, + Medium = 1, + High = 2 +} + export class TerminalLink implements vscode.TerminalLink { constructor( public startIndex: number, diff --git a/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkOpeners.test.ts b/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkOpeners.test.ts index 14a3eecb92b..f5032e258ac 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkOpeners.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkOpeners.test.ts @@ -136,6 +136,7 @@ suite('Workbench - TerminalLinkOpeners', () => { // Set a fake detected command starting as line 0 to establish the cwd commandDetection.setCommands([new TerminalCommand(xterm, { command: '', + commandLineConfidence: 'low', exitCode: 0, commandStartLineContent: '', markProperties: {}, @@ -277,6 +278,7 @@ suite('Workbench - TerminalLinkOpeners', () => { // Set a fake detected command starting as line 0 to establish the cwd commandDetection.setCommands([new TerminalCommand(xterm, { command: '', + commandLineConfidence: 'low', isTrusted: true, cwd, timestamp: 0, @@ -538,6 +540,7 @@ suite('Workbench - TerminalLinkOpeners', () => { commandStartLineContent: '', markProperties: {}, command: '', + commandLineConfidence: 'low', isTrusted: true, cwd, executedX: undefined, diff --git a/src/vscode-dts/vscode.proposed.terminalShellIntegration.d.ts b/src/vscode-dts/vscode.proposed.terminalShellIntegration.d.ts index 01eaef5220a..2ad7f3a33a1 100644 --- a/src/vscode-dts/vscode.proposed.terminalShellIntegration.d.ts +++ b/src/vscode-dts/vscode.proposed.terminalShellIntegration.d.ts @@ -17,23 +17,13 @@ declare module 'vscode' { readonly terminal: Terminal; /** - * The full command line that was executed, including both the command and arguments. - * The accuracy of this value depends on the shell integration implementation: - * - * - It may be undefined or the empty string until {@link onDidEndTerminalShellExecution} is - * fired. - * - It may be inaccurate initially if the command line is pulled from the buffer directly - * via the shell integration prompt markers. - * - It may contain line continuation characters and/or parts of the right prompt. - * - It may be inaccurate if the shell integration does not support command line reporting. + * The full command line that was executed, including both the command and arguments. The + * {@link TerminalShellExecutionCommandLineConfidence confidence} of this value depends on + * the specific shell's shell integration implementation. This value may become more + * accurate after {@link onDidEndTerminalShellExecution} is fired. */ - // TODO: Remove | undefined - this will be the empty string if the command start and end is the same position // TODO: Implement command line fetching via buffer markers - // TODO: Quality/confidence 3x: - // - Top: shell integration reporting - // - Middle: Not multi-line, command start is not on the left-most column - // - Bottom: Multi-line or command start is on the left-most column - readonly commandLine: string | undefined; + readonly commandLine: TerminalShellExecutionCommandLine; /** * The working directory that was reported by the shell when this command executed. This @@ -58,9 +48,53 @@ declare module 'vscode' { * } */ // TODO: read? "data" typically means Uint8Array. What's the encoding of the string? Usage here will typically be checking for substrings + // TODO: dispose function? readData(): AsyncIterable; } + /** + * A command line that was executed in a terminal. + */ + export interface TerminalShellExecutionCommandLine { + /** + * The full command line that was executed, including both the command and its arguments. + */ + value: string; + + /** + * The confidence of the command line value which is determined by how the value was + * obtained. This depends upon the implementation of the shell integration script. + */ + confidence: TerminalShellExecutionCommandLineConfidence; + } + + /** + * The confidence of a {@link TerminalShellExecutionCommandLine} value. + */ + enum TerminalShellExecutionCommandLineConfidence { + /** + * The command line value confidence is low. This means that the value was read from the + * terminal buffer using markers reported by the shell integration script. Additionally the + * command either started on the very left-most column which is unusual, or the command is + * multi-line which is more difficult to accurately detect due to line continuation + * characters and right prompts. + */ + Low = 0, + + /** + * The command line value confidence is medium. This means that the value was read from the + * terminal buffer using markers reported by the shell integration script. The command is + * single-line and does not start on the very left-most column (which is unusual). + */ + Medium = 1, + + /** + * The command line value confidence is high. This means that the value was explicitly send + * from the shell integration script. + */ + High = 2 + } + export interface Terminal { /** * An object that contains [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration)-powered