Skip to content

Commit 733b8aa

Browse files
authored
Merge pull request microsoft#208257 from microsoft/tyriar/145234
Terminal shell integration proposed api
2 parents 508e038 + 79ee97f commit 733b8aa

17 files changed

+683
-45
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ export interface ICommandDetectionCapability {
175175
readonly currentCommand: ICurrentPartialCommand | undefined;
176176
readonly onCommandStarted: Event<ITerminalCommand>;
177177
readonly onCommandFinished: Event<ITerminalCommand>;
178-
readonly onCommandExecuted: Event<void>;
178+
readonly onCommandExecuted: Event<ITerminalCommand>;
179179
readonly onCommandInvalidated: Event<ITerminalCommand[]>;
180180
readonly onCurrentCommandInvalidated: Event<ICommandInvalidationRequest>;
181181
setCwd(value: string): void;

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe
7474
readonly onBeforeCommandFinished = this._onBeforeCommandFinished.event;
7575
private readonly _onCommandFinished = this._register(new Emitter<ITerminalCommand>());
7676
readonly onCommandFinished = this._onCommandFinished.event;
77-
private readonly _onCommandExecuted = this._register(new Emitter<void>());
77+
private readonly _onCommandExecuted = this._register(new Emitter<ITerminalCommand>());
7878
readonly onCommandExecuted = this._onCommandExecuted.event;
7979
private readonly _onCommandInvalidated = this._register(new Emitter<ITerminalCommand[]>());
8080
readonly onCommandInvalidated = this._onCommandInvalidated.event;
@@ -408,7 +408,7 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe
408408
interface ICommandDetectionHeuristicsHooks {
409409
readonly onCurrentCommandInvalidatedEmitter: Emitter<ICommandInvalidationRequest>;
410410
readonly onCommandStartedEmitter: Emitter<ITerminalCommand>;
411-
readonly onCommandExecutedEmitter: Emitter<void>;
411+
readonly onCommandExecutedEmitter: Emitter<ITerminalCommand>;
412412
readonly dimensions: ITerminalDimensions;
413413
readonly isCommandStorageDisabled: boolean;
414414

@@ -495,7 +495,7 @@ class UnixPtyHeuristics extends Disposable {
495495
if (y === commandExecutedLine) {
496496
currentCommand.command += this._terminal.buffer.active.getLine(commandExecutedLine)?.translateToString(true, undefined, currentCommand.commandExecutedX) || '';
497497
}
498-
this._hooks.onCommandExecutedEmitter.fire();
498+
this._hooks.onCommandExecutedEmitter.fire(currentCommand as ITerminalCommand);
499499
}
500500
}
501501

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

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

833833
private _cursorOnNextLine(): boolean {

src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,10 @@ const enum VSCodeOscPt {
9797
/**
9898
* Explicitly set the command line. This helps workaround performance and reliability problems
9999
* with parsing out the command, such as conpty not guaranteeing the position of the sequence or
100-
* the shell not guaranteeing that the entire command is even visible.
100+
* the shell not guaranteeing that the entire command is even visible. Ideally this is called
101+
* immediately before {@link CommandExecuted}, immediately before {@link CommandFinished} will
102+
* also work but that means terminal will only know the accurate command line when the command is
103+
* finished.
101104
*
102105
* The command line can escape ascii characters using the `\xAB` format, where AB are the
103106
* hexadecimal representation of the character code (case insensitive), and escape the `\`

src/vs/workbench/api/browser/extensionHost.contribution.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ import './mainThreadStatusBar';
5959
import './mainThreadStorage';
6060
import './mainThreadTelemetry';
6161
import './mainThreadTerminalService';
62+
import './mainThreadTerminalShellIntegration';
6263
import './mainThreadTheming';
6364
import './mainThreadTreeViews';
6465
import './mainThreadDownloadService';

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import { ITerminalLinkProviderService } from 'vs/workbench/contrib/terminalContr
2525
import { ITerminalQuickFixService, ITerminalQuickFix, TerminalQuickFixType } from 'vs/workbench/contrib/terminalContrib/quickFix/browser/quickFix';
2626
import { TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities';
2727

28-
2928
@extHostNamedCustomer(MainContext.MainThreadTerminalService)
3029
export class MainThreadTerminalService implements MainThreadTerminalServiceShape {
3130

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { Event } from 'vs/base/common/event';
7+
import { Disposable } from 'vs/base/common/lifecycle';
8+
import { TerminalCapability, type ITerminalCommand } from 'vs/platform/terminal/common/capabilities/capabilities';
9+
import { ExtHostContext, MainContext, type ExtHostTerminalShellIntegrationShape, type MainThreadTerminalShellIntegrationShape } from 'vs/workbench/api/common/extHost.protocol';
10+
import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal';
11+
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
12+
import { extHostNamedCustomer, type IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers';
13+
14+
@extHostNamedCustomer(MainContext.MainThreadTerminalShellIntegration)
15+
export class MainThreadTerminalShellIntegration extends Disposable implements MainThreadTerminalShellIntegrationShape {
16+
private readonly _proxy: ExtHostTerminalShellIntegrationShape;
17+
18+
constructor(
19+
extHostContext: IExtHostContext,
20+
@ITerminalService private readonly _terminalService: ITerminalService,
21+
@IWorkbenchEnvironmentService workbenchEnvironmentService: IWorkbenchEnvironmentService
22+
) {
23+
super();
24+
25+
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTerminalShellIntegration);
26+
27+
// onDidChangeTerminalShellIntegration
28+
const onDidAddCommandDetection = this._terminalService.createOnInstanceEvent(instance => {
29+
return Event.map(
30+
Event.filter(instance.capabilities.onDidAddCapabilityType, e => {
31+
return e === TerminalCapability.CommandDetection;
32+
}, this._store), () => instance
33+
);
34+
});
35+
this._store.add(onDidAddCommandDetection(e => this._proxy.$shellIntegrationChange(e.instanceId)));
36+
37+
// onDidStartTerminalShellExecution
38+
const commandDetectionStartEvent = this._store.add(this._terminalService.createOnInstanceCapabilityEvent(TerminalCapability.CommandDetection, e => e.onCommandExecuted));
39+
let currentCommand: ITerminalCommand | undefined;
40+
this._store.add(commandDetectionStartEvent.event(e => {
41+
// Prevent duplicate events from being sent in case command detection double fires the
42+
// event
43+
if (e.data === currentCommand) {
44+
return;
45+
}
46+
currentCommand = e.data;
47+
this._proxy.$shellExecutionStart(e.instance.instanceId, e.data.command, e.data.cwd);
48+
}));
49+
50+
// onDidEndTerminalShellExecution
51+
const commandDetectionEndEvent = this._store.add(this._terminalService.createOnInstanceCapabilityEvent(TerminalCapability.CommandDetection, e => e.onCommandFinished));
52+
this._store.add(commandDetectionEndEvent.event(e => {
53+
currentCommand = undefined;
54+
this._proxy.$shellExecutionEnd(e.instance.instanceId, e.data.command, e.data.exitCode);
55+
}));
56+
57+
// onDidChangeTerminalShellIntegration via cwd
58+
const cwdChangeEvent = this._store.add(this._terminalService.createOnInstanceCapabilityEvent(TerminalCapability.CwdDetection, e => e.onDidChangeCwd));
59+
this._store.add(cwdChangeEvent.event(e => this._proxy.$cwdChange(e.instance.instanceId, e.data)));
60+
61+
// Clean up after dispose
62+
this._store.add(this._terminalService.onDidDisposeInstance(e => this._proxy.$closeTerminal(e.instanceId)));
63+
64+
// TerminalShellExecution.createDataStream
65+
// TODO: Support this on remote; it should go via the server
66+
if (!workbenchEnvironmentService.remoteAuthority) {
67+
this._store.add(this._terminalService.onAnyInstanceData(e => this._proxy.$shellExecutionData(e.instance.instanceId, e.data)));
68+
}
69+
}
70+
71+
$executeCommand(terminalId: number, commandLine: string): void {
72+
this._terminalService.getInstanceFromId(terminalId)?.runCommand(commandLine, true);
73+
}
74+
}

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ import { checkProposedApiEnabled, isProposedApiEnabled } from 'vs/workbench/serv
108108
import { ProxyIdentifier } from 'vs/workbench/services/extensions/common/proxyIdentifier';
109109
import { TextSearchCompleteMessageType } from 'vs/workbench/services/search/common/searchExtTypes';
110110
import type * as vscode from 'vscode';
111+
import { IExtHostTerminalShellIntegration } from 'vs/workbench/api/common/extHostTerminalShellIntegration';
111112

112113
export interface IExtensionRegistries {
113114
mine: ExtensionDescriptionRegistry;
@@ -167,6 +168,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
167168
const extHostDocumentsAndEditors = rpcProtocol.set(ExtHostContext.ExtHostDocumentsAndEditors, accessor.get(IExtHostDocumentsAndEditors));
168169
const extHostCommands = rpcProtocol.set(ExtHostContext.ExtHostCommands, accessor.get(IExtHostCommands));
169170
const extHostTerminalService = rpcProtocol.set(ExtHostContext.ExtHostTerminalService, accessor.get(IExtHostTerminalService));
171+
const extHostTerminalShellIntegration = rpcProtocol.set(ExtHostContext.ExtHostTerminalShellIntegration, accessor.get(IExtHostTerminalShellIntegration));
170172
const extHostDebugService = rpcProtocol.set(ExtHostContext.ExtHostDebugService, accessor.get(IExtHostDebugService));
171173
const extHostSearch = rpcProtocol.set(ExtHostContext.ExtHostSearch, accessor.get(IExtHostSearch));
172174
const extHostTask = rpcProtocol.set(ExtHostContext.ExtHostTask, accessor.get(IExtHostTask));
@@ -746,6 +748,18 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
746748
checkProposedApiEnabled(extension, 'terminalExecuteCommandEvent');
747749
return _asExtensionEvent(extHostTerminalService.onDidExecuteTerminalCommand)(listener, thisArg, disposables);
748750
},
751+
onDidChangeTerminalShellIntegration(listener, thisArg?, disposables?) {
752+
checkProposedApiEnabled(extension, 'terminalShellIntegration');
753+
return _asExtensionEvent(extHostTerminalShellIntegration.onDidChangeTerminalShellIntegration)(listener, thisArg, disposables);
754+
},
755+
onDidStartTerminalShellExecution(listener, thisArg?, disposables?) {
756+
checkProposedApiEnabled(extension, 'terminalShellIntegration');
757+
return _asExtensionEvent(extHostTerminalShellIntegration.onDidStartTerminalShellExecution)(listener, thisArg, disposables);
758+
},
759+
onDidEndTerminalShellExecution(listener, thisArg?, disposables?) {
760+
checkProposedApiEnabled(extension, 'terminalShellIntegration');
761+
return _asExtensionEvent(extHostTerminalShellIntegration.onDidEndTerminalShellExecution)(listener, thisArg, disposables);
762+
},
749763
get state() {
750764
return extHostWindow.getState(extension);
751765
},

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { ExtHostLocalizationService, IExtHostLocalizationService } from 'vs/work
3030
import { ExtHostManagedSockets, IExtHostManagedSockets } from 'vs/workbench/api/common/extHostManagedSockets';
3131
import { ExtHostAuthentication, IExtHostAuthentication } from 'vs/workbench/api/common/extHostAuthentication';
3232
import { ExtHostLanguageModels, IExtHostLanguageModels } from 'vs/workbench/api/common/extHostLanguageModels';
33+
import { IExtHostTerminalShellIntegration, ExtHostTerminalShellIntegration } from 'vs/workbench/api/common/extHostTerminalShellIntegration';
3334

3435
registerSingleton(IExtHostLocalizationService, ExtHostLocalizationService, InstantiationType.Delayed);
3536
registerSingleton(ILoggerService, ExtHostLoggerService, InstantiationType.Delayed);
@@ -49,6 +50,7 @@ registerSingleton(IExtHostSearch, ExtHostSearch, InstantiationType.Eager);
4950
registerSingleton(IExtHostStorage, ExtHostStorage, InstantiationType.Eager);
5051
registerSingleton(IExtHostTask, WorkerExtHostTask, InstantiationType.Eager);
5152
registerSingleton(IExtHostTerminalService, WorkerExtHostTerminalService, InstantiationType.Eager);
53+
registerSingleton(IExtHostTerminalShellIntegration, ExtHostTerminalShellIntegration, InstantiationType.Eager);
5254
registerSingleton(IExtHostTunnelService, ExtHostTunnelService, InstantiationType.Eager);
5355
registerSingleton(IExtHostWindow, ExtHostWindow, InstantiationType.Eager);
5456
registerSingleton(IExtHostWorkspace, ExtHostWorkspace, InstantiationType.Eager);

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,10 @@ export interface MainThreadTerminalServiceShape extends IDisposable {
537537
$sendProcessExit(terminalId: number, exitCode: number | undefined): void;
538538
}
539539

540+
export interface MainThreadTerminalShellIntegrationShape extends IDisposable {
541+
$executeCommand(terminalId: number, commandLine: string): void;
542+
}
543+
540544
export type TransferQuickPickItemOrSeparator = TransferQuickPickItem | quickInput.IQuickPickSeparator;
541545
export interface TransferQuickPickItem {
542546
handle: number;
@@ -2262,6 +2266,15 @@ export interface ExtHostTerminalServiceShape {
22622266
$provideTerminalQuickFixes(id: string, matchResult: TerminalCommandMatchResultDto, token: CancellationToken): Promise<SingleOrMany<TerminalQuickFix> | undefined>;
22632267
}
22642268

2269+
export interface ExtHostTerminalShellIntegrationShape {
2270+
$shellIntegrationChange(instanceId: number): void;
2271+
$shellExecutionStart(instanceId: number, commandLine: string | undefined, cwd: UriComponents | string | undefined): void;
2272+
$shellExecutionEnd(instanceId: number, commandLine: string | undefined, exitCode: number | undefined): void;
2273+
$shellExecutionData(instanceId: number, data: string): void;
2274+
$cwdChange(instanceId: number, cwd: UriComponents | string): void;
2275+
$closeTerminal(instanceId: number): void;
2276+
}
2277+
22652278
export interface ExtHostSCMShape {
22662279
$provideOriginalResource(sourceControlHandle: number, uri: UriComponents, token: CancellationToken): Promise<UriComponents | null>;
22672280
$onInputBoxValueChange(sourceControlHandle: number, value: string): void;
@@ -2813,6 +2826,7 @@ export const MainContext = {
28132826
MainThreadSpeech: createProxyIdentifier<MainThreadSpeechShape>('MainThreadSpeechProvider'),
28142827
MainThreadTelemetry: createProxyIdentifier<MainThreadTelemetryShape>('MainThreadTelemetry'),
28152828
MainThreadTerminalService: createProxyIdentifier<MainThreadTerminalServiceShape>('MainThreadTerminalService'),
2829+
MainThreadTerminalShellIntegration: createProxyIdentifier<MainThreadTerminalShellIntegrationShape>('MainThreadTerminalShellIntegration'),
28162830
MainThreadWebviews: createProxyIdentifier<MainThreadWebviewsShape>('MainThreadWebviews'),
28172831
MainThreadWebviewPanels: createProxyIdentifier<MainThreadWebviewPanelsShape>('MainThreadWebviewPanels'),
28182832
MainThreadWebviewViews: createProxyIdentifier<MainThreadWebviewViewsShape>('MainThreadWebviewViews'),
@@ -2873,6 +2887,7 @@ export const ExtHostContext = {
28732887
ExtHostExtensionService: createProxyIdentifier<ExtHostExtensionServiceShape>('ExtHostExtensionService'),
28742888
ExtHostLogLevelServiceShape: createProxyIdentifier<ExtHostLogLevelServiceShape>('ExtHostLogLevelServiceShape'),
28752889
ExtHostTerminalService: createProxyIdentifier<ExtHostTerminalServiceShape>('ExtHostTerminalService'),
2890+
ExtHostTerminalShellIntegration: createProxyIdentifier<ExtHostTerminalShellIntegrationShape>('ExtHostTerminalShellIntegration'),
28762891
ExtHostSCM: createProxyIdentifier<ExtHostSCMShape>('ExtHostSCM'),
28772892
ExtHostSearch: createProxyIdentifier<ExtHostSearchShape>('ExtHostSearch'),
28782893
ExtHostTask: createProxyIdentifier<ExtHostTaskShape>('ExtHostTask'),

0 commit comments

Comments
 (0)