Merge pull request #124754 from microsoft/tyriar/profile_api

Terminal profile contributions
This commit is contained in:
Megan Rogge 2021-05-27 19:01:23 -05:00 committed by GitHub
commit 29cad8fb5e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 249 additions and 66 deletions

View file

@ -916,6 +916,27 @@ 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.
* @param id The ID of the contributed terminal profile.
* @param provider The terminal profile provider.
*/
export function registerTerminalProfileProvider(id: string, provider: TerminalProfileProvider): Disposable;
}
export interface TerminalProfileProvider {
/**
* Provide terminal profile options for the requested terminal.
* @param token A cancellation token that indicates the result is no longer needed.
*/
provideProfileOptions(token: CancellationToken): ProviderResult<TerminalOptions | ExtensionTerminalOptions>;
}
//#endregion
// eslint-disable-next-line vscode-dts-region-comments
//#region @jrieken -> exclusive document filters

View file

@ -33,6 +33,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape
private _extHostTerminalIds = new Map<string, number>();
private readonly _toDispose = new DisposableStore();
private readonly _terminalProcessProxies = new Map<number, ITerminalProcessExtHostProxy>();
private readonly _profileProviders = new Map<string, IDisposable>();
private _dataEventTracker: TerminalDataEventTracker | undefined;
/**
* A single shared terminal link provider for the exthost. When an ext registers a link
@ -142,7 +143,16 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape
isExtensionOwnedTerminal: launchConfig.isExtensionOwnedTerminal,
useShellEnvironment: launchConfig.useShellEnvironment
};
const terminal = this._terminalService.createTerminal(shellLaunchConfig);
let terminal: ITerminalInstance | undefined;
if (launchConfig.isSplitTerminal) {
const activeInstance = this._terminalService.getActiveInstance();
if (activeInstance) {
terminal = withNullAsUndefined(this._terminalService.splitInstance(activeInstance, shellLaunchConfig));
}
}
if (!terminal) {
terminal = this._terminalService.createTerminal(shellLaunchConfig);
}
this._extHostTerminalIds.set(extHostTerminalId, terminal.instanceId);
}
@ -201,6 +211,18 @@ 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, {
createContributedTerminalProfile: async (isSplitTerminal) => this._proxy.$createContributedProfileTerminal(id, isSplitTerminal)
}));
}
public $unregisterProfileProvider(id: string): void {
this._profileProviders.get(id)?.dispose();
this._profileProviders.delete(id);
}
private _onActiveTerminalChanged(terminalId: number | null): void {
this._proxy.$acceptActiveTerminalChanged(terminalId);
}

View file

@ -661,8 +661,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<any>): vscode.Disposable {
return extHostTreeViews.registerTreeDataProvider(viewId, treeDataProvider, extension);

View file

@ -472,6 +472,7 @@ export interface TerminalLaunchConfig {
isFeatureTerminal?: boolean;
isExtensionOwnedTerminal?: boolean;
useShellEnvironment?: boolean;
isSplitTerminal?: boolean;
}
export interface MainThreadTerminalServiceShape extends IDisposable {
@ -485,6 +486,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
@ -1721,6 +1724,7 @@ export interface ExtHostTerminalServiceShape {
$activateLink(id: number, linkId: number): void;
$initEnvironmentVariableCollections(collections: [string, ISerializableEnvironmentVariableCollection][]): void;
$acceptDefaultProfile(profile: ITerminalProfile, automationProfile: ITerminalProfile): void;
$createContributedProfileTerminal(id: string, isSplitTerminal: boolean): Promise<void>;
}
export interface ExtHostSCMShape {

View file

@ -18,9 +18,10 @@ import { serializeEnvironmentVariableCollection } from 'vs/workbench/contrib/ter
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { generateUuid } from 'vs/base/common/uuid';
import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable';
import { IProcessReadyEvent, IShellLaunchConfigDto, ITerminalChildProcess, ITerminalDimensionsOverride, ITerminalEnvironment, ITerminalLaunchError, ITerminalProfile, TerminalShellType } from 'vs/platform/terminal/common/terminal';
import { IProcessReadyEvent, IShellLaunchConfigDto, ITerminalChildProcess, ITerminalDimensionsOverride, ITerminalLaunchError, ITerminalProfile, TerminalShellType } from 'vs/platform/terminal/common/terminal';
import { TerminalDataBufferer } from 'vs/platform/terminal/common/terminalDataBuffering';
import { ThemeIcon } from 'vs/platform/theme/common/themeService';
import { withNullAsUndefined } from 'vs/base/common/types';
export interface IExtHostTerminalService extends ExtHostTerminalServiceShape, IDisposable {
@ -42,12 +43,14 @@ 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;
}
export interface ITerminalInternalOptions {
isFeatureTerminal?: boolean;
useShellEnvironment?: boolean;
isSplitTerminal?: boolean;
}
export const IExtHostTerminalService = createDecorator<IExtHostTerminalService>('IExtHostTerminalService');
@ -118,30 +121,39 @@ export class ExtHostTerminal {
}
public async create(
shellPath?: string,
shellArgs?: string[] | string,
cwd?: string | URI,
env?: ITerminalEnvironment,
icon?: URI | { light: URI; dark: URI } | ThemeIcon,
initialText?: string,
waitOnExit?: boolean,
strictEnv?: boolean,
hideFromUser?: boolean,
isFeatureTerminal?: boolean,
isExtensionOwnedTerminal?: boolean,
useShellEnvironment?: boolean
options: vscode.TerminalOptions,
internalOptions?: ITerminalInternalOptions,
): Promise<void> {
if (typeof this._id !== 'string') {
throw new Error('Terminal has already been created');
}
await this._proxy.$createTerminal(this._id, { name: this._name, shellPath, shellArgs, cwd, env, icon, initialText, waitOnExit, strictEnv, hideFromUser, isFeatureTerminal, isExtensionOwnedTerminal, useShellEnvironment });
await this._proxy.$createTerminal(this._id, {
name: options.name,
shellPath: withNullAsUndefined(options.shellPath),
shellArgs: withNullAsUndefined(options.shellArgs),
cwd: withNullAsUndefined(options.cwd),
env: withNullAsUndefined(options.env),
icon: withNullAsUndefined(options.iconPath),
initialText: withNullAsUndefined(options.message),
strictEnv: withNullAsUndefined(options.strictEnv),
hideFromUser: withNullAsUndefined(options.hideFromUser),
isFeatureTerminal: withNullAsUndefined(internalOptions?.isFeatureTerminal),
isExtensionOwnedTerminal: true,
useShellEnvironment: withNullAsUndefined(internalOptions?.useShellEnvironment),
isSplitTerminal: withNullAsUndefined(internalOptions?.isSplitTerminal)
});
}
public async createExtensionTerminal(iconPath?: URI | { light: URI; dark: URI } | ThemeIcon): Promise<number> {
public async createExtensionTerminal(isSplitTerminal?: boolean, iconPath?: URI | { light: URI; dark: URI } | ThemeIcon): Promise<number> {
if (typeof this._id !== 'string') {
throw new Error('Terminal has already been created');
}
await this._proxy.$createTerminal(this._id, { name: this._name, isExtensionCustomPtyTerminal: true, icon: iconPath });
await this._proxy.$createTerminal(this._id, {
name: this._name,
isExtensionCustomPtyTerminal: true,
icon: iconPath,
isSplitTerminal
});
// At this point, the id has been set via `$acceptTerminalOpened`
if (typeof this._id === 'string') {
throw new Error('Terminal creation failed');
@ -302,6 +314,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I
private readonly _bufferer: TerminalDataBufferer;
private readonly _linkProviders: Set<vscode.TerminalLinkProvider> = new Set();
private readonly _profileProviders: Map<string, vscode.TerminalProfileProvider> = new Map();
private readonly _terminalLinkCache: Map<number, Map<number, ICachedLinkEntry>> = new Map();
private readonly _terminalLinkCancellationSource: Map<number, CancellationTokenSource> = new Map();
@ -353,10 +366,10 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I
return profile?.args || [];
}
public createExtensionTerminal(options: vscode.ExtensionTerminalOptions): vscode.Terminal {
public createExtensionTerminal(options: vscode.ExtensionTerminalOptions, internalOptions?: ITerminalInternalOptions): vscode.Terminal {
const terminal = new ExtHostTerminal(this._proxy, generateUuid(), options, options.name);
const p = new ExtHostPseudoterminal(options.pty);
terminal.createExtensionTerminal(options.iconPath).then(id => {
terminal.createExtensionTerminal(internalOptions?.isSplitTerminal, options.iconPath).then(id => {
const disposable = this._setupExtHostProcessListeners(id, p);
this._terminalProcessDisposables[id] = disposable;
});
@ -571,6 +584,33 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I
});
}
public registerProfileProvider(id: string, provider: vscode.TerminalProfileProvider): vscode.Disposable {
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 $createContributedProfileTerminal(id: string, isSplitTerminal: boolean): Promise<void> {
const token = new CancellationTokenSource().token;
const options = await this._profileProviders.get(id)?.provideProfileOptions(token);
if (token.isCancellationRequested) {
return;
}
if (!options) {
throw new Error(`No terminal profile options provided for id "${id}"`);
}
if ('pty' in options) {
this.createExtensionTerminal(options, { isSplitTerminal });
}
this.createTerminalFromOptions(options, { isSplitTerminal });
}
public async $provideLinks(terminalId: number, line: string): Promise<ITerminalLinkDto[]> {
const terminal = this._getTerminalById(terminalId);
if (!terminal) {

View file

@ -3,7 +3,6 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { withNullAsUndefined } from 'vs/base/common/types';
import { generateUuid } from 'vs/base/common/uuid';
import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService';
import { BaseExtHostTerminalService, ExtHostTerminal, ITerminalInternalOptions } from 'vs/workbench/api/common/extHostTerminalService';
@ -18,29 +17,13 @@ export class ExtHostTerminalService extends BaseExtHostTerminalService {
}
public createTerminal(name?: string, shellPath?: string, shellArgs?: string[] | string): vscode.Terminal {
const terminal = new ExtHostTerminal(this._proxy, generateUuid(), { name, shellPath, shellArgs }, name);
this._terminals.push(terminal);
terminal.create(shellPath, shellArgs);
return terminal.value;
return this.createTerminalFromOptions({ name, shellPath, shellArgs });
}
public createTerminalFromOptions(options: vscode.TerminalOptions, internalOptions?: ITerminalInternalOptions): vscode.Terminal {
const terminal = new ExtHostTerminal(this._proxy, generateUuid(), options, options.name);
this._terminals.push(terminal);
terminal.create(
withNullAsUndefined(options.shellPath),
withNullAsUndefined(options.shellArgs),
withNullAsUndefined(options.cwd),
withNullAsUndefined(options.env),
withNullAsUndefined(options.iconPath),
withNullAsUndefined(options.message),
/*options.waitOnExit*/ undefined,
withNullAsUndefined(options.strictEnv),
withNullAsUndefined(options.hideFromUser),
withNullAsUndefined(internalOptions?.isFeatureTerminal),
true,
withNullAsUndefined(internalOptions?.useShellEnvironment)
);
terminal.create(options, internalOptions);
return terminal.value;
}
}

View file

@ -141,6 +141,8 @@ export interface ITerminalService {
*/
createTerminal(profile: ITerminalProfile): ITerminalInstance;
createContributedTerminalProfile(id: string, isSplitTerminal: boolean): Promise<void>;
/**
* Creates a raw terminal instance, this should not be used outside of the terminal part.
*/
@ -199,6 +201,8 @@ export interface ITerminalService {
*/
registerLinkProvider(linkProvider: ITerminalExternalLinkProvider): IDisposable;
registerTerminalProfileProvider(id: string, profileProvider: ITerminalProfileProvider): IDisposable;
showProfileQuickPick(type: 'setDefault' | 'createInstance', cwd?: string | URI): Promise<ITerminalInstance | undefined>;
getGroupForInstance(instance: ITerminalInstance): ITerminalGroup | undefined;
@ -224,6 +228,10 @@ export interface ITerminalExternalLinkProvider {
provideLinks(instance: ITerminalInstance, line: string): Promise<ITerminalLink[] | undefined>;
}
export interface ITerminalProfileProvider {
createContributedTerminalProfile(isSplitTerminal: boolean): Promise<void>;
}
export interface ITerminalLink {
/** The startIndex of the link in the line. */
startIndex: number;

View file

@ -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';
@ -19,13 +19,13 @@ import { IKeyMods, IPickOptions, IQuickInputButton, IQuickInputService, IQuickPi
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { ILocalTerminalService, IOffProcessTerminalService, IShellLaunchConfig, ITerminalLaunchError, ITerminalProfile, ITerminalProfileObject, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, TerminalSettingId, TerminalSettingPrefix } from 'vs/platform/terminal/common/terminal';
import { ThemeIcon } from 'vs/platform/theme/common/themeService';
import { IRemoteTerminalService, ITerminalExternalLinkProvider, ITerminalInstance, ITerminalService, ITerminalGroup, TerminalConnectionState, ITerminalProfileProvider } from 'vs/workbench/contrib/terminal/browser/terminal';
import { IEditableData, IViewDescriptorService, IViewsService, ViewContainerLocation } from 'vs/workbench/common/views';
import { IRemoteTerminalService, ITerminalExternalLinkProvider, ITerminalInstance, ITerminalService, ITerminalGroup, TerminalConnectionState } 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 readonly _profileProviders: Map<string, ITerminalProfileProvider> = new Map();
private _linkProviders: Set<ITerminalExternalLinkProvider> = new Set();
private _linkProviderDisposables: Map<ITerminalExternalLinkProvider, IDisposable[]> = new Map();
private _processSupportContextKey: IContextKey<boolean>;
@ -146,6 +149,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;
@ -343,18 +348,23 @@ export class TerminalService implements ITerminalService {
@throttle(2000)
private async _refreshAvailableProfiles(): Promise<void> {
const result = await this._detectProfiles();
if (!equals(result, this._availableProfiles)) {
const profilesChanged = !equals(result, this._availableProfiles);
if (profilesChanged) {
this._availableProfiles = result;
this._onDidChangeAvailableProfiles.fire(this._availableProfiles);
this._profilesReadyBarrier.open();
const env = await this._remoteAgentService.getEnvironment();
registerTerminalDefaultProfileConfiguration({
os: env?.os || OS,
profiles: this._availableProfiles
});
await this._refreshPlatformConfig();
}
}
private async _refreshPlatformConfig() {
const env = await this._remoteAgentService.getEnvironment();
registerTerminalDefaultProfileConfiguration({
os: env?.os || OS,
profiles: this._availableProfiles!
});
}
private async _detectProfiles(includeDetectedProfiles?: boolean): Promise<ITerminalProfile[]> {
const offProcService = this._offProcessTerminalService;
if (!offProcService) {
@ -793,6 +803,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);
@ -882,6 +897,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) : [];
@ -916,17 +934,32 @@ export class TerminalService implements ITerminalService {
quickPickItems.push({ type: 'separator', label: nls.localize('terminalProfiles', "profiles") });
quickPickItems.push(...configProfiles.map(e => this._createProfileQuickPickItem(e)));
}
// Add contributed profiles, these cannot be defaults
// Add contributed profiles
if (type === 'createInstance') {
quickPickItems.push({ type: 'separator', label: nls.localize('terminalProfiles.contributed', "contributed") });
for (const contributed of this._terminalContributionService.terminalTypes) {
if (this._terminalContributionService.terminalProfiles.length > 0 || this._terminalContributionService.terminalTypes.length > 0) {
quickPickItems.push({ type: 'separator', label: nls.localize('terminalProfiles.contributed', "contributed") });
}
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
});
}
// Add contributed types (legacy), these cannot be defaults
if (type === 'createInstance') {
for (const contributed of this._terminalContributionService.terminalTypes) {
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") });
quickPickItems.push(...autoDetectedProfiles.map(e => this._createProfileQuickPickItem(e)));
@ -937,30 +970,36 @@ export class TerminalService implements ITerminalService {
return;
}
if (type === 'createInstance') {
// TODO: How to support alt here?
// Legacy implementation - remove when js-debug adopts new
if ('command' in value.profile) {
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.createContributedTerminalProfile(value.profile.id, !!(keyMods?.alt && activeInstance));
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);
return instance;
}
} else { // setDefault
if ('command' in value.profile) {
if ('command' in value.profile || '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}`);
@ -981,6 +1020,16 @@ export class TerminalService implements ITerminalService {
return undefined;
}
async createContributedTerminalProfile(id: string, isSplitTerminal: boolean): Promise<void> {
await this._extensionService.activateByEvent(`onTerminalProfile:${id}`);
const profileProvider = this._profileProviders.get(id);
if (!profileProvider) {
this._notificationService.error(`No terminal profile provider registered for id "${id}"`);
return;
}
await profileProvider.createContributedTerminalProfile(isSplitTerminal);
}
private _createProfileQuickPickItem(profile: ITerminalProfile): IProfileQuickPickItem {
const buttons: IQuickInputButton[] = [{
iconClass: ThemeIcon.asClassName(configureTerminalProfileIcon),
@ -1155,7 +1204,7 @@ export class TerminalService implements ITerminalService {
}
interface IProfileQuickPickItem extends IQuickPickItem {
profile: ITerminalProfile | ITerminalTypeContribution;
profile: ITerminalProfile | ITerminalTypeContribution | ITerminalProfileContribution;
}
interface IInstanceLocation {

View file

@ -229,6 +229,11 @@ export class TerminalViewPane extends ViewPane {
dropdownActions.push(new MenuItemAction({ id: contributed.command, title: contributed.title, category: TerminalTabContextMenuGroup.Profile }, undefined, undefined, this._contextKeyService, this._commandService));
}
for (const contributed of this._terminalContributionService.terminalProfiles) {
dropdownActions.push(new Action(TerminalCommandId.NewWithProfile, contributed.title, undefined, true, () => this._terminalService.createContributedTerminalProfile(contributed.id, false)));
submenuActions.push(new Action(TerminalCommandId.NewWithProfile, contributed.title, undefined, true, () => this._terminalService.createContributedTerminalProfile(contributed.id, true)));
}
if (dropdownActions.length > 0) {
dropdownActions.push(new SubmenuAction('split.profile', 'Split...', submenuActions));
dropdownActions.push(new Separator());

View file

@ -575,7 +575,9 @@ export const DEFAULT_COMMANDS_TO_SKIP_SHELL: string[] = [
];
export interface ITerminalContributions {
/** @deprecated */
types?: ITerminalTypeContribution[];
profiles?: ITerminalProfileContribution[];
}
export interface ITerminalTypeContribution {
@ -584,6 +586,12 @@ export interface ITerminalTypeContribution {
icon?: string;
}
export interface ITerminalProfileContribution {
title: string;
id: string;
icon?: string;
}
export const terminalContributionsDescriptor: IExtensionPointDescriptor = {
extensionPoint: 'terminal',
defaultExtensionKind: 'workspace',
@ -613,6 +621,28 @@ export const terminalContributionsDescriptor: IExtensionPointDescriptor = {
},
},
},
profiles: {
type: 'array',
description: nls.localize('vscode.extension.contributes.terminal.profiles', "Defines additional terminal profiles that the user can create."),
items: {
type: 'object',
required: ['id', 'title'],
properties: {
command: {
description: nls.localize('vscode.extension.contributes.terminal.profiles.id', "The ID of the terminal profile provider."),
type: 'string',
},
title: {
description: nls.localize('vscode.extension.contributes.terminal.profiles.title', "Title for this terminal profile."),
type: 'string',
},
icon: {
description: nls.localize('vscode.extension.contributes.terminal.profiles.icon', "A codicon to associate with this terminal profile."),
type: 'string',
},
},
},
},
},
},
};

View file

@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as extensionsRegistry from 'vs/workbench/services/extensions/common/extensionsRegistry';
import { ITerminalTypeContribution, ITerminalContributions, terminalContributionsDescriptor } from 'vs/workbench/contrib/terminal/common/terminal';
import { ITerminalTypeContribution, ITerminalContributions, terminalContributionsDescriptor, ITerminalProfileContribution } from 'vs/workbench/contrib/terminal/common/terminal';
import { flatten } from 'vs/base/common/arrays';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
@ -15,6 +15,7 @@ export interface ITerminalContributionService {
readonly _serviceBrand: undefined;
readonly terminalTypes: ReadonlyArray<ITerminalTypeContribution>;
readonly terminalProfiles: ReadonlyArray<ITerminalProfileContribution>;
}
export const ITerminalContributionService = createDecorator<ITerminalContributionService>('terminalContributionsService');
@ -23,10 +24,10 @@ export class TerminalContributionService implements ITerminalContributionService
declare _serviceBrand: undefined;
private _terminalTypes: ReadonlyArray<ITerminalTypeContribution> = [];
get terminalTypes() { return this._terminalTypes; }
get terminalTypes() {
return this._terminalTypes;
}
private _terminalProfiles: ReadonlyArray<ITerminalProfileContribution> = [];
get terminalProfiles() { return this._terminalProfiles; }
constructor() {
terminalsExtPoint.setHandler(contributions => {
@ -46,6 +47,18 @@ export class TerminalContributionService implements ITerminalContributionService
return e;
}) || [];
}));
this._terminalProfiles = flatten(contributions.filter(c => c.description.enableProposedApi).map(c => {
return c.value?.profiles?.map(e => {
// Only support $(id) for now, without that it should point to a path to be
// consistent with other icon APIs
if (e.icon && e.icon.startsWith('$(') && e.icon.endsWith(')')) {
e.icon = e.icon.substr(2, e.icon.length - 3);
} else {
e.icon = undefined;
}
return e;
}) || [];
}));
});
}
}

View file

@ -325,6 +325,11 @@ export const schema: IJSONSchema = {
description: nls.localize('vscode.extension.activationEvents.onRenderer', 'An activation event emitted whenever a notebook output renderer is used.'),
body: 'onRenderer:${11:rendererId}'
},
{
label: 'onTerminalProfile',
body: 'onTerminalProfile:${1:terminalType}',
description: nls.localize('vscode.extension.activationEvents.onTerminalProfile', 'An activation event emitted when a specific terminal profile is launched.'),
},
{
label: '*',
description: nls.localize('vscode.extension.activationEvents.star', 'An activation event emitted on VS Code startup. To ensure a great end user experience, please use this activation event in your extension only when no other activation events combination works in your use-case.'),