diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 53b6cb09800..3bd912bc419 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -746,6 +746,18 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'terminalExecuteCommandEvent'); return _asExtensionEvent(extHostTerminalService.onDidExecuteTerminalCommand)(listener, thisArg, disposables); }, + onDidChangeTerminalShellIntegration(listener, thisArg?, disposables?) { + checkProposedApiEnabled(extension, 'terminalShellIntegration'); + return _asExtensionEvent(extHostTerminalService.onDidChangeTerminalShellIntegration)(listener, thisArg, disposables); + }, + onDidStartTerminalShellExecution(listener, thisArg?, disposables?) { + checkProposedApiEnabled(extension, 'terminalShellIntegration'); + return _asExtensionEvent(extHostTerminalService.onDidStartTerminalShellExecution)(listener, thisArg, disposables); + }, + onDidEndTerminalShellExecution(listener, thisArg?, disposables?) { + checkProposedApiEnabled(extension, 'terminalShellIntegration'); + return _asExtensionEvent(extHostTerminalService.onDidEndTerminalShellExecution)(listener, thisArg, disposables); + }, get state() { return extHostWindow.getState(extension); }, diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index cbe28de19ca..84754c6ac9b 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -44,6 +44,10 @@ export interface IExtHostTerminalService extends ExtHostTerminalServiceShape, ID readonly onDidExecuteTerminalCommand: Event; readonly onDidChangeShell: Event; + readonly onDidChangeTerminalShellIntegration: Event; + readonly onDidStartTerminalShellExecution: Event; + readonly onDidEndTerminalShellExecution: Event; + createTerminal(name?: string, shellPath?: string, shellArgs?: readonly string[] | string): vscode.Terminal; createTerminalFromOptions(options: vscode.TerminalOptions, internalOptions?: ITerminalInternalOptions): vscode.Terminal; createExtensionTerminal(options: vscode.ExtensionTerminalOptions): vscode.Terminal; @@ -118,6 +122,10 @@ export class ExtHostTerminal { get selection(): string | undefined { return that._selection; }, + get shellIntegration(): vscode.TerminalShellIntegration | undefined { + // TODO: Impl + return undefined; + }, sendText(text: string, shouldExecute: boolean = true): void { that._checkDisposed(); that._proxy.$sendText(that._id, text, shouldExecute); @@ -412,6 +420,13 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I }); readonly onDidExecuteTerminalCommand = this._onDidExecuteCommand.event; + protected readonly _onDidChangeTerminalShellIntegration = new Emitter(); + readonly onDidChangeTerminalShellIntegration = this._onDidChangeTerminalShellIntegration.event; + protected readonly _onDidStartTerminalShellExecution = new Emitter(); + readonly onDidStartTerminalShellExecution = this._onDidStartTerminalShellExecution.event; + protected readonly _onDidEndTerminalShellExecution = new Emitter(); + readonly onDidEndTerminalShellExecution = this._onDidEndTerminalShellExecution.event; + constructor( supportsProcesses: boolean, @IExtHostCommands private readonly _extHostCommands: IExtHostCommands, @@ -453,6 +468,13 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I } } }); + + setTimeout(() => { + console.log('*** TEST CODE'); + this.onDidChangeTerminalShellIntegration(e => { + console.log('*** onDidChangeTerminalShellIntegration', e); + }); + }, 2000); } public abstract createTerminal(name?: string, shellPath?: string, shellArgs?: string[] | string): vscode.Terminal; diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index 62e37286be5..e304a464abd 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -113,6 +113,7 @@ export const allApiProposals = Object.freeze({ terminalExecuteCommandEvent: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalExecuteCommandEvent.d.ts', terminalQuickFixProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalQuickFixProvider.d.ts', terminalSelection: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalSelection.d.ts', + terminalShellIntegration: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalShellIntegration.d.ts', testObserver: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testObserver.d.ts', textSearchProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.textSearchProvider.d.ts', timeline: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.timeline.d.ts', diff --git a/src/vscode-dts/vscode.proposed.terminalExecuteCommandEvent.d.ts b/src/vscode-dts/vscode.proposed.terminalExecuteCommandEvent.d.ts index 4b3aa64400e..5d28eb36a80 100644 --- a/src/vscode-dts/vscode.proposed.terminalExecuteCommandEvent.d.ts +++ b/src/vscode-dts/vscode.proposed.terminalExecuteCommandEvent.d.ts @@ -40,6 +40,8 @@ declare module 'vscode' { * * Note that this event will not fire if the executed command exits the shell, listen to * {@link onDidCloseTerminal} to handle that case. + * + * @deprecated Use {@link window.onDidStartTerminalShellExecution} */ export const onDidExecuteTerminalCommand: Event; } diff --git a/src/vscode-dts/vscode.proposed.terminalShellIntegration.d.ts b/src/vscode-dts/vscode.proposed.terminalShellIntegration.d.ts new file mode 100644 index 00000000000..bb99ff4f114 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.terminalShellIntegration.d.ts @@ -0,0 +1,195 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/145234 + + export interface TerminalShellExecution { + /** + * The {@link Terminal} the command was executed in. + */ + terminal: Terminal; + + /** + * The full command line that was executed, including both the command and arguments. + */ + commandLine: string | undefined; + + /** + * The working directory that was reported by the shell when this command executed. This + * will be a {@link Uri} if the string reported by the shell can reliably be mapped to the + * connected machine. + */ + cwd: Uri | string | undefined; + + /** + * The exit code reported by the shell. + */ + exitCode: Thenable; + + /** + * A per-extension stream of raw data (including escape sequences) that is written to the + * terminal. This will only include data that was written after `stream` was called for the + * first time, ie. you must call `dataStream` immediately after the command is executed via + * {@link executeCommand} or {@link onDidStartTerminalShellExecution} to not miss any data. + * + * @example + * // Log all data written to the terminal for a command + * const command = term.shellIntegration.executeCommand({ commandLine: 'echo "Hello world"' }); + * for await (const data of command.dataStream) { + * console.log(data); + * } + */ + dataStream: AsyncIterator; + } + + export interface Terminal { + /** + * An object that contains [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration)-powered + * features for the terminal. This will always be undefined immediately after the terminal + * is created. Listen to {@link window.onDidActivateTerminalShellIntegration} to be notified + * when shell integration is activated for a terminal. + */ + shellIntegration: TerminalShellIntegration | undefined; + } + + export interface TerminalShellIntegration { + // TODO: Is this fine to share the TerminalShellIntegrationChangeEvent event? + // TODO: Should we have TerminalShellExecution.cwd if this exists? + /** + * The current working directory of the terminal. This will be a {@link Uri} if the string + * reported by the shell can reliably be mapped to the connected machine. + */ + cwd: Uri | string | undefined; + + /** + * Execute a command, sending ^C as necessary to interrupt any running command if needed. + * + * *Note* This is not guaranteed to work as [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration) + * must be activated. Check whether {@link TerminalShellExecution.exitCode} is rejected to + * verify whether it was successful. + * + * @param commandLine The command line to execute, this is the exact text that will be sent + * to the terminal. + * + * @example + * // Execute a command in a terminal immediately after being created + * const myTerm = window.createTerminal(); + * window.onDidActivateTerminalShellIntegration(async ({ terminal, shellIntegration }) => { + * if (terminal === myTerm) { + * const command = shellIntegration.executeCommand('echo "Hello world"'); + * const code = await command.exitCode; + * console.log(`Command exited with code ${code}`); + * } + * })); + * // Fallback to sendText if there is no shell integration within 3 seconds of launching + * setTimeout(() => { + * if (!myTerm.shellIntegration) { + * myTerm.sendText('echo "Hello world"'); + * // Without shell integration, we can't know when the command has finished or what the + * // exit code was. + * } + * }, 3000); + * + * @example + * // Send command to terminal that has been alive for a while + * const commandLine = 'echo "Hello world"'; + * if (term.shellIntegration) { + * const command = term.shellIntegration.executeCommand({ commandLine }); + * const code = await command.exitCode; + * console.log(`Command exited with code ${code}`); + * } else { + * term.sendText(commandLine); + * // Without shell integration, we can't know when the command has finished or what the + * // exit code was. + * } + */ + executeCommand(commandLine: string): TerminalShellExecution; + + /** + * Execute a command, sending ^C as necessary to interrupt any running command if needed. + * + * *Note* This is not guaranteed to work as [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration) + * must be activated. Check whether {@link TerminalShellExecution.exitCode} is rejected to + * verify whether it was successful. + * + * @param command A command to run. + * @param args Arguments to launch the executable with which will be automatically escaped + * based on the executable type. + * + * @example + * // Execute a command in a terminal immediately after being created + * const myTerm = window.createTerminal(); + * window.onDidActivateTerminalShellIntegration(async ({ terminal, shellIntegration }) => { + * if (terminal === myTerm) { + * const command = shellIntegration.executeCommand({ + * command: 'echo', + * args: ['Hello world'] + * }); + * const code = await command.exitCode; + * console.log(`Command exited with code ${code}`); + * } + * })); + * // Fallback to sendText if there is no shell integration within 3 seconds of launching + * setTimeout(() => { + * if (!myTerm.shellIntegration) { + * myTerm.sendText('echo "Hello world"'); + * // Without shell integration, we can't know when the command has finished or what the + * // exit code was. + * } + * }, 3000); + * + * @example + * // Send command to terminal that has been alive for a while + * const commandLine = 'echo "Hello world"'; + * if (term.shellIntegration) { + * const command = term.shellIntegration.executeCommand({ + * command: 'echo', + * args: ['Hello world'] + * }); + * const code = await command.exitCode; + * console.log(`Command exited with code ${code}`); + * } else { + * term.sendText(commandLine); + * // Without shell integration, we can't know when the command has finished or what the + * // exit code was. + * } + */ + executeCommand(executable: string, args: string[]): TerminalShellExecution; + } + + export interface TerminalShellIntegrationChangeEvent { + /** + * The terminal that shell integration has been activated in. + */ + terminal: Terminal; + /** + * The shell integration object. + */ + shellIntegration: TerminalShellIntegration; + } + + export namespace window { + /** + * Fires when shell integration activates or one of its properties changes in a terminal. + */ + export const onDidChangeTerminalShellIntegration: Event; + + /** + * This will be fired when a terminal command is started. This event will fire only when + * [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration) is + * activated for the terminal. + */ + export const onDidStartTerminalShellExecution: Event; + + /** + * This will be fired when a terminal command is ended. This event will fire only when + * [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration) is + * activated for the terminal. + */ + export const onDidEndTerminalShellExecution: Event; + } +}