diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 94096b84ecf..b57531bf26d 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -916,6 +916,25 @@ declare module 'vscode' { //#endregion + //#region Terminal profile provider https://github.com/microsoft/vscode/issues/120369 + + export namespace window { + /** + * Registers a provider for a contributed terminal profile. + */ + export function registerTerminalProfileProvider(id: string, provider: TerminalProfileProvider): Disposable; + // TODO: id -> profileId, profileType? + } + + export interface TerminalProfileProvider { + /** + * Provide terminal profile options for the requested terminal. + */ + provideProfileOptions(token: CancellationToken): ProviderResult; + } + + //#endregion + // eslint-disable-next-line vscode-dts-region-comments //#region @jrieken -> exclusive document filters diff --git a/src/vs/workbench/api/browser/mainThreadTerminalService.ts b/src/vs/workbench/api/browser/mainThreadTerminalService.ts index 56d4075670c..7db6d7492fb 100644 --- a/src/vs/workbench/api/browser/mainThreadTerminalService.ts +++ b/src/vs/workbench/api/browser/mainThreadTerminalService.ts @@ -33,6 +33,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape private _extHostTerminalIds = new Map(); private readonly _toDispose = new DisposableStore(); private readonly _terminalProcessProxies = new Map(); + private readonly _profileProviders = new Map(); private _dataEventTracker: TerminalDataEventTracker | undefined; /** * A single shared terminal link provider for the exthost. When an ext registers a link @@ -200,6 +201,21 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape this._terminalService.registerProcessSupport(isSupported); } + public $registerProfileProvider(id: string): void { + // Proxy profile provider requests through the extension host + this._profileProviders.set(id, this._terminalService.registerTerminalProfileProvider(id, { + provideProfile: async () => { + console.log('provide profile', id); + return { name: 'My fake profile' }; + } + })); + } + + public $unregisterProfileProvider(id: string): void { + this._profileProviders.get(id)?.dispose(); + this._profileProviders.delete(id); + } + private _onActiveTerminalChanged(terminalId: number | null): void { this._proxy.$acceptActiveTerminalChanged(terminalId); } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 9463dbd723f..d147dcb5aca 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -651,8 +651,11 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I } return extHostTerminalService.createTerminal(nameOrOptions, shellPath, shellArgs); }, - registerTerminalLinkProvider(handler: vscode.TerminalLinkProvider): vscode.Disposable { - return extHostTerminalService.registerLinkProvider(handler); + registerTerminalLinkProvider(provider: vscode.TerminalLinkProvider): vscode.Disposable { + return extHostTerminalService.registerLinkProvider(provider); + }, + registerTerminalProfileProvider(id: string, provider: vscode.TerminalProfileProvider): vscode.Disposable { + return extHostTerminalService.registerProfileProvider(id, provider); }, registerTreeDataProvider(viewId: string, treeDataProvider: vscode.TreeDataProvider): vscode.Disposable { return extHostTreeViews.registerTreeDataProvider(viewId, treeDataProvider, extension); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 3bea115021f..3e39346d27d 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -476,6 +476,8 @@ export interface MainThreadTerminalServiceShape extends IDisposable { $startLinkProvider(): void; $stopLinkProvider(): void; $registerProcessSupport(isSupported: boolean): void; + $registerProfileProvider(id: string): void; + $unregisterProfileProvider(id: string): void; $setEnvironmentVariableCollection(extensionIdentifier: string, persistent: boolean, collection: ISerializableEnvironmentVariableCollection | undefined): void; // Process diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index be6ce95e65f..d440b89ada0 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -42,6 +42,7 @@ export interface IExtHostTerminalService extends ExtHostTerminalServiceShape, ID getDefaultShell(useAutomationShell: boolean): string; getDefaultShellArgs(useAutomationShell: boolean): string[] | string; registerLinkProvider(provider: vscode.TerminalLinkProvider): vscode.Disposable; + registerProfileProvider(id: string, provider: vscode.TerminalProfileProvider): vscode.Disposable; getEnvironmentVariableCollection(extension: IExtensionDescription, persistent?: boolean): vscode.EnvironmentVariableCollection; } @@ -296,6 +297,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I private readonly _bufferer: TerminalDataBufferer; private readonly _linkProviders: Set = new Set(); + private readonly _profileProviders: Map = new Map(); private readonly _terminalLinkCache: Map> = new Map(); private readonly _terminalLinkCancellationSource: Map = new Map(); @@ -564,6 +566,19 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I }); } + public registerProfileProvider(id: string, provider: vscode.TerminalProfileProvider): vscode.Disposable { + console.log('registerProfileProvider', id); + if (this._profileProviders.has(id)) { + throw new Error(`Terminal profile provider "${id}" already registered`); + } + this._profileProviders.set(id, provider); + this._proxy.$registerProfileProvider(id); + return new VSCodeDisposable(() => { + this._profileProviders.delete(id); + this._proxy.$unregisterProfileProvider(id); + }); + } + public async $provideLinks(terminalId: number, line: string): Promise { const terminal = this._getTerminalById(terminalId); if (!terminal) { diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index 00c2d9bbc06..5c1f3a4da31 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -198,6 +198,8 @@ export interface ITerminalService { */ registerLinkProvider(linkProvider: ITerminalExternalLinkProvider): IDisposable; + registerTerminalProfileProvider(id: string, profileProvider: ITerminalProfileProvider): IDisposable; + showProfileQuickPick(type: 'setDefault' | 'createInstance', cwd?: string | URI): Promise; getGroupForInstance(instance: ITerminalInstance): ITerminalGroup | undefined; @@ -221,6 +223,10 @@ export interface ITerminalExternalLinkProvider { provideLinks(instance: ITerminalInstance, line: string): Promise; } +export interface ITerminalProfileProvider { + provideProfile(): Promise; +} + export interface ITerminalLink { /** The startIndex of the link in the line. */ startIndex: number; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index fef476e350e..8ad6f68dc24 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -6,7 +6,7 @@ import { AutoOpenBarrier, timeout } from 'vs/base/common/async'; import { debounce, throttle } from 'vs/base/common/decorators'; import { Emitter, Event } from 'vs/base/common/event'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { isMacintosh, isWeb, isWindows, OperatingSystem, OS } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { FindReplaceState } from 'vs/editor/contrib/find/findState'; @@ -20,12 +20,12 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ILocalTerminalService, IOffProcessTerminalService, IShellLaunchConfig, ITerminalLaunchError, ITerminalProfile, ITerminalProfileObject, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, TerminalSettingId } from 'vs/platform/terminal/common/terminal'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { IViewDescriptorService, IViewsService, ViewContainerLocation } from 'vs/workbench/common/views'; -import { IRemoteTerminalService, ITerminalExternalLinkProvider, ITerminalInstance, ITerminalService, ITerminalGroup, TerminalConnectionState } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { IRemoteTerminalService, ITerminalExternalLinkProvider, ITerminalInstance, ITerminalService, ITerminalGroup, TerminalConnectionState, ITerminalProfileProvider } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper'; import { TerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminalInstance'; import { TerminalGroup } from 'vs/workbench/contrib/terminal/browser/terminalGroup'; import { TerminalViewPane } from 'vs/workbench/contrib/terminal/browser/terminalView'; -import { IRemoteTerminalAttachTarget, IStartExtensionTerminalRequest, ITerminalConfigHelper, ITerminalProcessExtHostProxy, KEYBINDING_CONTEXT_TERMINAL_ALT_BUFFER_ACTIVE, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_IS_OPEN, KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED, KEYBINDING_CONTEXT_TERMINAL_SHELL_TYPE, TERMINAL_VIEW_ID, KEYBINDING_CONTEXT_TERMINAL_COUNT, ITerminalTypeContribution, KEYBINDING_CONTEXT_TERMINAL_TABS_MOUSE, KEYBINDING_CONTEXT_TERMINAL_GROUP_COUNT } from 'vs/workbench/contrib/terminal/common/terminal'; +import { IRemoteTerminalAttachTarget, IStartExtensionTerminalRequest, ITerminalConfigHelper, ITerminalProcessExtHostProxy, KEYBINDING_CONTEXT_TERMINAL_ALT_BUFFER_ACTIVE, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_IS_OPEN, KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED, KEYBINDING_CONTEXT_TERMINAL_SHELL_TYPE, TERMINAL_VIEW_ID, KEYBINDING_CONTEXT_TERMINAL_COUNT, ITerminalTypeContribution, KEYBINDING_CONTEXT_TERMINAL_TABS_MOUSE, KEYBINDING_CONTEXT_TERMINAL_GROUP_COUNT, ITerminalProfileContribution } from 'vs/workbench/contrib/terminal/common/terminal'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { ILifecycleService, ShutdownReason, WillShutdownEvent } from 'vs/workbench/services/lifecycle/common/lifecycle'; @@ -41,6 +41,8 @@ import { VirtualWorkspaceContext } from 'vs/workbench/browser/contextkeys'; import { formatMessageForTerminal } from 'vs/workbench/contrib/terminal/common/terminalStrings'; import { Orientation } from 'vs/base/browser/ui/sash/sash'; import { registerTerminalDefaultProfileConfiguration } from 'vs/platform/terminal/common/terminalPlatformConfiguration'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { INotificationService } from 'vs/platform/notification/common/notification'; export class TerminalService implements ITerminalService { declare _serviceBrand: undefined; @@ -56,6 +58,7 @@ export class TerminalService implements ITerminalService { private _findState: FindReplaceState; private _activeGroupIndex: number; private _activeInstanceIndex: number; + private _profileProviders: Map = new Map(); private _linkProviders: Set = new Set(); private _linkProviderDisposables: Map = new Map(); private _processSupportContextKey: IContextKey; @@ -144,6 +147,8 @@ export class TerminalService implements ITerminalService { @ITelemetryService private readonly _telemetryService: ITelemetryService, @ITerminalContributionService private readonly _terminalContributionService: ITerminalContributionService, @ICommandService private readonly _commandService: ICommandService, + @IExtensionService private readonly _extensionService: IExtensionService, + @INotificationService private readonly _notificationService: INotificationService, @optional(ILocalTerminalService) localTerminalService: ILocalTerminalService ) { this._localTerminalService = localTerminalService; @@ -776,6 +781,11 @@ export class TerminalService implements ITerminalService { }; } + registerTerminalProfileProvider(id: string, profileProvider: ITerminalProfileProvider): IDisposable { + this._profileProviders.set(id, profileProvider); + return toDisposable(() => this._profileProviders.delete(id)); + } + private _setInstanceLinkProviders(instance: ITerminalInstance): void { for (const linkProvider of this._linkProviders) { const disposables = this._linkProviderDisposables.get(linkProvider); @@ -865,6 +875,9 @@ export class TerminalService implements ITerminalService { if ('command' in context.item.profile) { return; } + if ('id' in context.item.profile) { + return; + } const configKey = `terminal.integrated.profiles.${platformKey}`; const configProfiles = this._configurationService.getValue<{ [key: string]: ITerminalProfileObject }>(configKey); const existingProfiles = configProfiles ? Object.keys(configProfiles) : []; @@ -909,7 +922,14 @@ export class TerminalService implements ITerminalService { profile: contributed }); } - console.log('profiles', this._terminalContributionService.terminalProfiles); + console.log('this._terminalContributionService.terminalProfiles', this._terminalContributionService.terminalProfiles); + for (const contributed of this._terminalContributionService.terminalProfiles) { + const icon = contributed.icon ? (iconRegistry.get(contributed.icon) || Codicon.terminal) : Codicon.terminal; + quickPickItems.push({ + label: `$(${icon.id}) ${contributed.title}`, + profile: contributed + }); + } } if (autoDetectedProfiles.length > 0) { quickPickItems.push({ type: 'separator', label: nls.localize('terminalProfiles.detected', "detected") }); @@ -926,16 +946,33 @@ export class TerminalService implements ITerminalService { return this._commandService.executeCommand(value.profile.command); } - let instance; const activeInstance = this.getActiveInstance(); - if (keyMods?.alt && activeInstance) { - // create split, only valid if there's an active instance - if (activeInstance) { - instance = this.splitInstance(activeInstance, value.profile, cwd); + let instance; + + if ('id' in value.profile) { + await this._extensionService.activateByEvent(`onTerminalProfile:${value.profile.id}`); + const profileProvider = this._profileProviders.get(value.profile.id); + if (!profileProvider) { + this._notificationService.error(`No terminal profile provider registered for id "${value.profile.id}"`); + return; } + const slc = await profileProvider.provideProfile(); + if (keyMods?.alt && activeInstance) { + // create split, only valid if there's an active instance + instance = this.splitInstance(activeInstance, slc); + } else { + instance = this.createTerminal(slc); + } + return; } else { - instance = this.createTerminal(value.profile, cwd); + if (keyMods?.alt && activeInstance) { + // create split, only valid if there's an active instance + instance = this.splitInstance(activeInstance, value.profile, cwd); + } else { + instance = this.createTerminal(value.profile, cwd); + } } + if (instance) { this.showPanel(true); this.setActiveInstance(instance); @@ -945,6 +982,9 @@ export class TerminalService implements ITerminalService { if ('command' in value.profile) { return; // Should never happen } + if ('id' in value.profile) { + return; // Should never happen + } // Add the profile to settings if necessary if (value.profile.isAutoDetected) { const profilesConfig = await this._configurationService.getValue(`terminal.integrated.profiles.${platformKey}`); @@ -1139,7 +1179,7 @@ export class TerminalService implements ITerminalService { } interface IProfileQuickPickItem extends IQuickPickItem { - profile: ITerminalProfile | ITerminalTypeContribution; + profile: ITerminalProfile | ITerminalTypeContribution | ITerminalProfileContribution; } interface IInstanceLocation {