Skip to content

Commit

Permalink
Merge pull request #208257 from microsoft/tyriar/145234
Browse files Browse the repository at this point in the history
Terminal shell integration proposed api
  • Loading branch information
Tyriar committed Mar 21, 2024
2 parents 508e038 + 79ee97f commit 733b8aa
Show file tree
Hide file tree
Showing 17 changed files with 683 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ export interface ICommandDetectionCapability {
readonly currentCommand: ICurrentPartialCommand | undefined;
readonly onCommandStarted: Event<ITerminalCommand>;
readonly onCommandFinished: Event<ITerminalCommand>;
readonly onCommandExecuted: Event<void>;
readonly onCommandExecuted: Event<ITerminalCommand>;
readonly onCommandInvalidated: Event<ITerminalCommand[]>;
readonly onCurrentCommandInvalidated: Event<ICommandInvalidationRequest>;
setCwd(value: string): void;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe
readonly onBeforeCommandFinished = this._onBeforeCommandFinished.event;
private readonly _onCommandFinished = this._register(new Emitter<ITerminalCommand>());
readonly onCommandFinished = this._onCommandFinished.event;
private readonly _onCommandExecuted = this._register(new Emitter<void>());
private readonly _onCommandExecuted = this._register(new Emitter<ITerminalCommand>());
readonly onCommandExecuted = this._onCommandExecuted.event;
private readonly _onCommandInvalidated = this._register(new Emitter<ITerminalCommand[]>());
readonly onCommandInvalidated = this._onCommandInvalidated.event;
Expand Down Expand Up @@ -408,7 +408,7 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe
interface ICommandDetectionHeuristicsHooks {
readonly onCurrentCommandInvalidatedEmitter: Emitter<ICommandInvalidationRequest>;
readonly onCommandStartedEmitter: Emitter<ITerminalCommand>;
readonly onCommandExecutedEmitter: Emitter<void>;
readonly onCommandExecutedEmitter: Emitter<ITerminalCommand>;
readonly dimensions: ITerminalDimensions;
readonly isCommandStorageDisabled: boolean;

Expand Down Expand Up @@ -495,7 +495,7 @@ class UnixPtyHeuristics extends Disposable {
if (y === commandExecutedLine) {
currentCommand.command += this._terminal.buffer.active.getLine(commandExecutedLine)?.translateToString(true, undefined, currentCommand.commandExecutedX) || '';
}
this._hooks.onCommandExecutedEmitter.fire();
this._hooks.onCommandExecutedEmitter.fire(currentCommand as ITerminalCommand);
}
}

Expand Down Expand Up @@ -733,7 +733,7 @@ class WindowsPtyHeuristics extends Disposable {
this._onCursorMoveListener.clear();
this._evaluateCommandMarkers();
this._capability.currentCommand.commandExecutedX = this._terminal.buffer.active.cursorX;
this._hooks.onCommandExecutedEmitter.fire();
this._hooks.onCommandExecutedEmitter.fire(this._capability.currentCommand as ITerminalCommand);
this._logService.debug('CommandDetectionCapability#handleCommandExecuted', this._capability.currentCommand.commandExecutedX, this._capability.currentCommand.commandExecutedMarker?.line);
}

Expand Down Expand Up @@ -827,7 +827,7 @@ class WindowsPtyHeuristics extends Disposable {
}
this._capability.currentCommand.commandExecutedMarker = this._hooks.commandMarkers[this._hooks.commandMarkers.length - 1];
// Fire this now to prevent issues like #197409
this._hooks.onCommandExecutedEmitter.fire();
this._hooks.onCommandExecutedEmitter.fire(this._capability.currentCommand as ITerminalCommand);
}

private _cursorOnNextLine(): boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,10 @@ const enum VSCodeOscPt {
/**
* Explicitly set the command line. This helps workaround performance and reliability problems
* with parsing out the command, such as conpty not guaranteeing the position of the sequence or
* the shell not guaranteeing that the entire command is even visible.
* the shell not guaranteeing that the entire command is even visible. Ideally this is called
* immediately before {@link CommandExecuted}, immediately before {@link CommandFinished} will
* also work but that means terminal will only know the accurate command line when the command is
* finished.
*
* The command line can escape ascii characters using the `\xAB` format, where AB are the
* hexadecimal representation of the character code (case insensitive), and escape the `\`
Expand Down
1 change: 1 addition & 0 deletions src/vs/workbench/api/browser/extensionHost.contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import './mainThreadStatusBar';
import './mainThreadStorage';
import './mainThreadTelemetry';
import './mainThreadTerminalService';
import './mainThreadTerminalShellIntegration';
import './mainThreadTheming';
import './mainThreadTreeViews';
import './mainThreadDownloadService';
Expand Down
1 change: 0 additions & 1 deletion src/vs/workbench/api/browser/mainThreadTerminalService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import { ITerminalLinkProviderService } from 'vs/workbench/contrib/terminalContr
import { ITerminalQuickFixService, ITerminalQuickFix, TerminalQuickFixType } from 'vs/workbench/contrib/terminalContrib/quickFix/browser/quickFix';
import { TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities';


@extHostNamedCustomer(MainContext.MainThreadTerminalService)
export class MainThreadTerminalService implements MainThreadTerminalServiceShape {

Expand Down
74 changes: 74 additions & 0 deletions src/vs/workbench/api/browser/mainThreadTerminalShellIntegration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { Event } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { TerminalCapability, type ITerminalCommand } from 'vs/platform/terminal/common/capabilities/capabilities';
import { ExtHostContext, MainContext, type ExtHostTerminalShellIntegrationShape, type MainThreadTerminalShellIntegrationShape } from 'vs/workbench/api/common/extHost.protocol';
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';

@extHostNamedCustomer(MainContext.MainThreadTerminalShellIntegration)
export class MainThreadTerminalShellIntegration extends Disposable implements MainThreadTerminalShellIntegrationShape {
private readonly _proxy: ExtHostTerminalShellIntegrationShape;

constructor(
extHostContext: IExtHostContext,
@ITerminalService private readonly _terminalService: ITerminalService,
@IWorkbenchEnvironmentService workbenchEnvironmentService: IWorkbenchEnvironmentService
) {
super();

this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTerminalShellIntegration);

// onDidChangeTerminalShellIntegration
const onDidAddCommandDetection = this._terminalService.createOnInstanceEvent(instance => {
return Event.map(
Event.filter(instance.capabilities.onDidAddCapabilityType, e => {
return e === TerminalCapability.CommandDetection;
}, this._store), () => instance
);
});
this._store.add(onDidAddCommandDetection(e => this._proxy.$shellIntegrationChange(e.instanceId)));

// onDidStartTerminalShellExecution
const commandDetectionStartEvent = this._store.add(this._terminalService.createOnInstanceCapabilityEvent(TerminalCapability.CommandDetection, e => e.onCommandExecuted));
let currentCommand: ITerminalCommand | undefined;
this._store.add(commandDetectionStartEvent.event(e => {
// Prevent duplicate events from being sent in case command detection double fires the
// event
if (e.data === currentCommand) {
return;
}
currentCommand = e.data;
this._proxy.$shellExecutionStart(e.instance.instanceId, e.data.command, 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);
}));

// onDidChangeTerminalShellIntegration via cwd
const cwdChangeEvent = this._store.add(this._terminalService.createOnInstanceCapabilityEvent(TerminalCapability.CwdDetection, e => e.onDidChangeCwd));
this._store.add(cwdChangeEvent.event(e => this._proxy.$cwdChange(e.instance.instanceId, e.data)));

// Clean up after dispose
this._store.add(this._terminalService.onDidDisposeInstance(e => this._proxy.$closeTerminal(e.instanceId)));

// TerminalShellExecution.createDataStream
// TODO: Support this on remote; it should go via the server
if (!workbenchEnvironmentService.remoteAuthority) {
this._store.add(this._terminalService.onAnyInstanceData(e => this._proxy.$shellExecutionData(e.instance.instanceId, e.data)));
}
}

$executeCommand(terminalId: number, commandLine: string): void {
this._terminalService.getInstanceFromId(terminalId)?.runCommand(commandLine, true);
}
}
14 changes: 14 additions & 0 deletions src/vs/workbench/api/common/extHost.api.impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ import { checkProposedApiEnabled, isProposedApiEnabled } from 'vs/workbench/serv
import { ProxyIdentifier } from 'vs/workbench/services/extensions/common/proxyIdentifier';
import { TextSearchCompleteMessageType } from 'vs/workbench/services/search/common/searchExtTypes';
import type * as vscode from 'vscode';
import { IExtHostTerminalShellIntegration } from 'vs/workbench/api/common/extHostTerminalShellIntegration';

export interface IExtensionRegistries {
mine: ExtensionDescriptionRegistry;
Expand Down Expand Up @@ -167,6 +168,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
const extHostDocumentsAndEditors = rpcProtocol.set(ExtHostContext.ExtHostDocumentsAndEditors, accessor.get(IExtHostDocumentsAndEditors));
const extHostCommands = rpcProtocol.set(ExtHostContext.ExtHostCommands, accessor.get(IExtHostCommands));
const extHostTerminalService = rpcProtocol.set(ExtHostContext.ExtHostTerminalService, accessor.get(IExtHostTerminalService));
const extHostTerminalShellIntegration = rpcProtocol.set(ExtHostContext.ExtHostTerminalShellIntegration, accessor.get(IExtHostTerminalShellIntegration));
const extHostDebugService = rpcProtocol.set(ExtHostContext.ExtHostDebugService, accessor.get(IExtHostDebugService));
const extHostSearch = rpcProtocol.set(ExtHostContext.ExtHostSearch, accessor.get(IExtHostSearch));
const extHostTask = rpcProtocol.set(ExtHostContext.ExtHostTask, accessor.get(IExtHostTask));
Expand Down Expand Up @@ -746,6 +748,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(extHostTerminalShellIntegration.onDidChangeTerminalShellIntegration)(listener, thisArg, disposables);
},
onDidStartTerminalShellExecution(listener, thisArg?, disposables?) {
checkProposedApiEnabled(extension, 'terminalShellIntegration');
return _asExtensionEvent(extHostTerminalShellIntegration.onDidStartTerminalShellExecution)(listener, thisArg, disposables);
},
onDidEndTerminalShellExecution(listener, thisArg?, disposables?) {
checkProposedApiEnabled(extension, 'terminalShellIntegration');
return _asExtensionEvent(extHostTerminalShellIntegration.onDidEndTerminalShellExecution)(listener, thisArg, disposables);
},
get state() {
return extHostWindow.getState(extension);
},
Expand Down
2 changes: 2 additions & 0 deletions src/vs/workbench/api/common/extHost.common.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { ExtHostLocalizationService, IExtHostLocalizationService } from 'vs/work
import { ExtHostManagedSockets, IExtHostManagedSockets } from 'vs/workbench/api/common/extHostManagedSockets';
import { ExtHostAuthentication, IExtHostAuthentication } from 'vs/workbench/api/common/extHostAuthentication';
import { ExtHostLanguageModels, IExtHostLanguageModels } from 'vs/workbench/api/common/extHostLanguageModels';
import { IExtHostTerminalShellIntegration, ExtHostTerminalShellIntegration } from 'vs/workbench/api/common/extHostTerminalShellIntegration';

registerSingleton(IExtHostLocalizationService, ExtHostLocalizationService, InstantiationType.Delayed);
registerSingleton(ILoggerService, ExtHostLoggerService, InstantiationType.Delayed);
Expand All @@ -49,6 +50,7 @@ registerSingleton(IExtHostSearch, ExtHostSearch, InstantiationType.Eager);
registerSingleton(IExtHostStorage, ExtHostStorage, InstantiationType.Eager);
registerSingleton(IExtHostTask, WorkerExtHostTask, InstantiationType.Eager);
registerSingleton(IExtHostTerminalService, WorkerExtHostTerminalService, InstantiationType.Eager);
registerSingleton(IExtHostTerminalShellIntegration, ExtHostTerminalShellIntegration, InstantiationType.Eager);
registerSingleton(IExtHostTunnelService, ExtHostTunnelService, InstantiationType.Eager);
registerSingleton(IExtHostWindow, ExtHostWindow, InstantiationType.Eager);
registerSingleton(IExtHostWorkspace, ExtHostWorkspace, InstantiationType.Eager);
Expand Down
15 changes: 15 additions & 0 deletions src/vs/workbench/api/common/extHost.protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,10 @@ export interface MainThreadTerminalServiceShape extends IDisposable {
$sendProcessExit(terminalId: number, exitCode: number | undefined): void;
}

export interface MainThreadTerminalShellIntegrationShape extends IDisposable {
$executeCommand(terminalId: number, commandLine: string): void;
}

export type TransferQuickPickItemOrSeparator = TransferQuickPickItem | quickInput.IQuickPickSeparator;
export interface TransferQuickPickItem {
handle: number;
Expand Down Expand Up @@ -2262,6 +2266,15 @@ export interface ExtHostTerminalServiceShape {
$provideTerminalQuickFixes(id: string, matchResult: TerminalCommandMatchResultDto, token: CancellationToken): Promise<SingleOrMany<TerminalQuickFix> | undefined>;
}

export interface ExtHostTerminalShellIntegrationShape {
$shellIntegrationChange(instanceId: number): void;
$shellExecutionStart(instanceId: number, commandLine: string | undefined, cwd: UriComponents | string | undefined): void;
$shellExecutionEnd(instanceId: number, commandLine: string | undefined, exitCode: number | undefined): void;
$shellExecutionData(instanceId: number, data: string): void;
$cwdChange(instanceId: number, cwd: UriComponents | string): void;
$closeTerminal(instanceId: number): void;
}

export interface ExtHostSCMShape {
$provideOriginalResource(sourceControlHandle: number, uri: UriComponents, token: CancellationToken): Promise<UriComponents | null>;
$onInputBoxValueChange(sourceControlHandle: number, value: string): void;
Expand Down Expand Up @@ -2813,6 +2826,7 @@ export const MainContext = {
MainThreadSpeech: createProxyIdentifier<MainThreadSpeechShape>('MainThreadSpeechProvider'),
MainThreadTelemetry: createProxyIdentifier<MainThreadTelemetryShape>('MainThreadTelemetry'),
MainThreadTerminalService: createProxyIdentifier<MainThreadTerminalServiceShape>('MainThreadTerminalService'),
MainThreadTerminalShellIntegration: createProxyIdentifier<MainThreadTerminalShellIntegrationShape>('MainThreadTerminalShellIntegration'),
MainThreadWebviews: createProxyIdentifier<MainThreadWebviewsShape>('MainThreadWebviews'),
MainThreadWebviewPanels: createProxyIdentifier<MainThreadWebviewPanelsShape>('MainThreadWebviewPanels'),
MainThreadWebviewViews: createProxyIdentifier<MainThreadWebviewViewsShape>('MainThreadWebviewViews'),
Expand Down Expand Up @@ -2873,6 +2887,7 @@ export const ExtHostContext = {
ExtHostExtensionService: createProxyIdentifier<ExtHostExtensionServiceShape>('ExtHostExtensionService'),
ExtHostLogLevelServiceShape: createProxyIdentifier<ExtHostLogLevelServiceShape>('ExtHostLogLevelServiceShape'),
ExtHostTerminalService: createProxyIdentifier<ExtHostTerminalServiceShape>('ExtHostTerminalService'),
ExtHostTerminalShellIntegration: createProxyIdentifier<ExtHostTerminalShellIntegrationShape>('ExtHostTerminalShellIntegration'),
ExtHostSCM: createProxyIdentifier<ExtHostSCMShape>('ExtHostSCM'),
ExtHostSearch: createProxyIdentifier<ExtHostSearchShape>('ExtHostSearch'),
ExtHostTask: createProxyIdentifier<ExtHostTaskShape>('ExtHostTask'),
Expand Down

0 comments on commit 733b8aa

Please sign in to comment.