Skip to content

Commit

Permalink
Merge pull request #120562 from microsoft/tyriar/terminal_icon
Browse files Browse the repository at this point in the history
Add support for terminal icons
  • Loading branch information
Tyriar committed Apr 5, 2021
2 parents a2056a8 + 192e362 commit 3028779
Show file tree
Hide file tree
Showing 16 changed files with 156 additions and 33 deletions.
6 changes: 6 additions & 0 deletions src/vs/platform/terminal/common/terminal.ts
Expand Up @@ -281,6 +281,12 @@ export interface IShellLaunchConfig {
* Whether this terminal was created by an extension.
*/
isExtensionOwnedTerminal?: boolean;

/**
* The codicon ID to use for this terminal. If not specified it will use the default fallback
* icon.
*/
icon?: string;
}

export interface IShellLaunchConfigDto {
Expand Down
11 changes: 11 additions & 0 deletions src/vs/vscode.proposed.d.ts
Expand Up @@ -825,6 +825,17 @@ declare module 'vscode' {

//#endregion

//#region Terminal icon https://github.com/microsoft/vscode/issues/120538

export interface TerminalOptions {
/**
* A codicon ID to associate with this terminal.
*/
readonly icon?: string;
}

//#endregion

// eslint-disable-next-line vscode-dts-region-comments
//#region @jrieken -> exclusive document filters

Expand Down
1 change: 1 addition & 0 deletions src/vs/workbench/api/browser/mainThreadTerminalService.ts
Expand Up @@ -123,6 +123,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape
executable: launchConfig.shellPath,
args: launchConfig.shellArgs,
cwd: typeof launchConfig.cwd === 'string' ? launchConfig.cwd : URI.revive(launchConfig.cwd),
icon: launchConfig.icon,
initialText: launchConfig.initialText,
waitOnExit: launchConfig.waitOnExit,
ignoreConfigurationCwd: true,
Expand Down
3 changes: 3 additions & 0 deletions src/vs/workbench/api/common/extHost.api.impl.ts
Expand Up @@ -636,6 +636,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
if (nameOrOptions.initialText) {
checkProposedApiEnabled(extension);
}
if (nameOrOptions.icon) {
checkProposedApiEnabled(extension);
}
return extHostTerminalService.createTerminalFromOptions(nameOrOptions);
}
return extHostTerminalService.createTerminal(nameOrOptions, shellPath, shellArgs);
Expand Down
1 change: 1 addition & 0 deletions src/vs/workbench/api/common/extHost.protocol.ts
Expand Up @@ -455,6 +455,7 @@ export interface TerminalLaunchConfig {
shellArgs?: string[] | string;
cwd?: string | UriComponents;
env?: ITerminalEnvironment;
icon?: string;
initialText?: string;
waitOnExit?: boolean;
strictEnv?: boolean;
Expand Down
3 changes: 2 additions & 1 deletion src/vs/workbench/api/common/extHostTerminalService.ts
Expand Up @@ -118,6 +118,7 @@ export class ExtHostTerminal {
shellArgs?: string[] | string,
cwd?: string | URI,
env?: ITerminalEnvironment,
icon?: string,
initialText?: string,
waitOnExit?: boolean,
strictEnv?: boolean,
Expand All @@ -128,7 +129,7 @@ export class ExtHostTerminal {
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, initialText, waitOnExit, strictEnv, hideFromUser, isFeatureTerminal, isExtensionOwnedTerminal });
await this._proxy.$createTerminal(this._id, { name: this._name, shellPath, shellArgs, cwd, env, icon, initialText, waitOnExit, strictEnv, hideFromUser, isFeatureTerminal, isExtensionOwnedTerminal });
}

public async createExtensionTerminal(): Promise<number> {
Expand Down
1 change: 1 addition & 0 deletions src/vs/workbench/api/node/extHostTerminalService.ts
Expand Up @@ -65,6 +65,7 @@ export class ExtHostTerminalService extends BaseExtHostTerminalService {
withNullAsUndefined(options.shellArgs),
withNullAsUndefined(options.cwd),
withNullAsUndefined(options.env),
withNullAsUndefined(options.icon),
withNullAsUndefined(options.initialText),
/*options.waitOnExit*/ undefined,
withNullAsUndefined(options.strictEnv),
Expand Down
9 changes: 9 additions & 0 deletions src/vs/workbench/contrib/terminal/browser/terminal.ts
Expand Up @@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { Codicon } from 'vs/base/common/codicons';
import { Event } from 'vs/base/common/event';
import { IDisposable } from 'vs/base/common/lifecycle';
import { IProcessEnvironment, Platform } from 'vs/base/common/platform';
Expand Down Expand Up @@ -107,6 +108,12 @@ export interface ITerminalService {
*/
createTerminal(shell?: IShellLaunchConfig): ITerminalInstance;

/**
* Creates a terminal.
* @param profile The profile to launch the terminal with.
*/
createTerminal(profile: ITerminalProfile): ITerminalInstance;

/**
* Creates a raw terminal instance, this should not be used outside of the terminal part.
*/
Expand All @@ -119,6 +126,7 @@ export interface ITerminalService {
setActiveInstanceByIndex(terminalIndex: number): void;
getActiveOrCreateInstance(): ITerminalInstance;
splitInstance(instance: ITerminalInstance, shell?: IShellLaunchConfig): ITerminalInstance | null;
splitInstance(instance: ITerminalInstance, profile: ITerminalProfile): ITerminalInstance | null;

/**
* Perform an action with the active terminal instance, if the terminal does
Expand Down Expand Up @@ -244,6 +252,7 @@ export interface ITerminalInstance {
readonly rows: number;
readonly maxCols: number;
readonly maxRows: number;
readonly icon: Codicon;

/**
* The process ID of the shell process, this is undefined when there is no process associated
Expand Down
8 changes: 4 additions & 4 deletions src/vs/workbench/contrib/terminal/browser/terminalActions.ts
Expand Up @@ -1513,11 +1513,11 @@ export function registerTerminalActions() {
// Remove 'New ' from the selected item to get the profile name
const profileSelection = item.substring(4);
if (quickSelectProfiles) {
const launchConfig = quickSelectProfiles.find(profile => profile.profileName === profileSelection);
if (launchConfig) {
const workspaceShellAllowed = terminalService.configHelper.checkIsProcessLaunchSafe(undefined, launchConfig);
const profile = quickSelectProfiles.find(profile => profile.profileName === profileSelection);
if (profile) {
const workspaceShellAllowed = terminalService.configHelper.checkIsProcessLaunchSafe(undefined, profile);
if (workspaceShellAllowed) {
const instance = terminalService.createTerminal({ executable: launchConfig.path, args: launchConfig.args, name: launchConfig.overrideName ? launchConfig.profileName : undefined });
const instance = terminalService.createTerminal(profile);
terminalService.setActiveInstance(instance);
}
} else {
Expand Down
2 changes: 2 additions & 0 deletions src/vs/workbench/contrib/terminal/browser/terminalInstance.ts
Expand Up @@ -51,6 +51,7 @@ import { IProcessDataEvent, IShellLaunchConfig, ITerminalDimensionsOverride, ITe
import { IProductService } from 'vs/platform/product/common/productService';
import { formatMessageForTerminal } from 'vs/workbench/contrib/terminal/common/terminalStrings';
import { AutoOpenBarrier } from 'vs/base/common/async';
import { Codicon, iconRegistry } from 'vs/base/common/codicons';

// How long in milliseconds should an average frame take to render for a notification to appear
// which suggests the fallback DOM-based renderer
Expand Down Expand Up @@ -169,6 +170,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
public get commandTracker(): CommandTrackerAddon | undefined { return this._commandTrackerAddon; }
public get navigationMode(): INavigationMode | undefined { return this._navigationModeAddon; }
public get isDisconnected(): boolean { return this._processManager.isDisconnected; }
public get icon(): Codicon { return this.shellLaunchConfig.icon ? (iconRegistry.get(this.shellLaunchConfig.icon) || Codicon.terminal) : Codicon.terminal; }

private readonly _onExit = new Emitter<number | undefined>();
public get onExit(): Event<number | undefined> { return this._onExit.event; }
Expand Down
Expand Up @@ -32,7 +32,7 @@ export class TerminalQuickAccessProvider extends PickerQuickAccessProvider<IPick
const terminalTab = terminalTabs[tabIndex];
for (let terminalIndex = 0; terminalIndex < terminalTab.terminalInstances.length; terminalIndex++) {
const terminal = terminalTab.terminalInstances[terminalIndex];
const label = `${tabIndex + 1}.${terminalIndex + 1}: ${terminal.title}`;
const label = `${tabIndex + 1}.${terminalIndex + 1}: $(${terminal.icon.id}) ${terminal.title}`;

const highlights = matchesFuzzy(filter, label, true);
if (highlights) {
Expand Down
82 changes: 64 additions & 18 deletions src/vs/workbench/contrib/terminal/browser/terminalService.ts
Expand Up @@ -25,7 +25,7 @@ import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/term
import { TerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminalInstance';
import { TerminalTab } from 'vs/workbench/contrib/terminal/browser/terminalTab';
import { TerminalViewPane } from 'vs/workbench/contrib/terminal/browser/terminalView';
import { IAvailableProfilesRequest, IRemoteTerminalAttachTarget, ITerminalProfile, IStartExtensionTerminalRequest, ITerminalConfigHelper, ITerminalNativeWindowsDelegate, ITerminalProcessExtHostProxy, KEYBINDING_CONTEXT_TERMINAL_ALT_BUFFER_ACTIVE, KEYBINDING_CONTEXT_TERMINAL_FIND_VISIBLE, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_IS_OPEN, KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED, KEYBINDING_CONTEXT_TERMINAL_SHELL_TYPE, LinuxDistro, TERMINAL_VIEW_ID, ITerminalProfileObject, ITerminalExecutable, ITerminalProfileSource } from 'vs/workbench/contrib/terminal/common/terminal';
import { IAvailableProfilesRequest, IRemoteTerminalAttachTarget, ITerminalProfile, IStartExtensionTerminalRequest, ITerminalConfigHelper, ITerminalNativeWindowsDelegate, ITerminalProcessExtHostProxy, KEYBINDING_CONTEXT_TERMINAL_ALT_BUFFER_ACTIVE, KEYBINDING_CONTEXT_TERMINAL_FIND_VISIBLE, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_IS_OPEN, KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED, KEYBINDING_CONTEXT_TERMINAL_SHELL_TYPE, LinuxDistro, TERMINAL_VIEW_ID, ITerminalProfileObject, ITerminalExecutable, ITerminalProfileSource, ITerminalTypeContribution } from 'vs/workbench/contrib/terminal/common/terminal';
import { escapeNonWindowsPath } from 'vs/workbench/contrib/terminal/common/terminalEnvironment';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
Expand All @@ -34,6 +34,9 @@ import { ILifecycleService, ShutdownReason, WillShutdownEvent } from 'vs/workben
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
import { configureTerminalProfileIcon } from 'vs/workbench/contrib/terminal/browser/terminalIcons';
import { equals } from 'vs/base/common/objects';
import { Codicon, iconRegistry } from 'vs/base/common/codicons';
import { ITerminalContributionService } from 'vs/workbench/contrib/terminal/common/terminalExtensionPoints';
import { ICommandService } from 'vs/platform/commands/common/commands';

interface IExtHostReadyEntry {
promise: Promise<void>;
Expand Down Expand Up @@ -126,6 +129,8 @@ export class TerminalService implements ITerminalService {
@IRemoteTerminalService private readonly _remoteTerminalService: IRemoteTerminalService,
@ITelemetryService private readonly _telemetryService: ITelemetryService,
@IExtensionService private readonly _extensionService: IExtensionService,
@ITerminalContributionService private readonly _terminalContributionService: ITerminalContributionService,
@ICommandService private readonly _commandService: ICommandService,
@optional(ILocalTerminalService) localTerminalService: ILocalTerminalService
) {
this._localTerminalService = localTerminalService;
Expand Down Expand Up @@ -644,13 +649,15 @@ export class TerminalService implements ITerminalService {
this.setActiveTabByIndex(newIndex);
}

public splitInstance(instanceToSplit: ITerminalInstance, shellLaunchConfig: IShellLaunchConfig = {}): ITerminalInstance | null {
public splitInstance(instanceToSplit: ITerminalInstance, shellLaunchConfig?: IShellLaunchConfig): ITerminalInstance | null;
public splitInstance(instanceToSplit: ITerminalInstance, profile: ITerminalProfile): ITerminalInstance | null
public splitInstance(instanceToSplit: ITerminalInstance, shellLaunchConfigOrProfile: IShellLaunchConfig | ITerminalProfile = {}): ITerminalInstance | null {
const tab = this._getTabForInstance(instanceToSplit);
if (!tab) {
return null;
}

const instance = tab.split(shellLaunchConfig);
const instance = tab.split(this._convertProfileToShellLaunchConfig(shellLaunchConfigOrProfile));

this._initInstanceListeners(instance);
this._onInstancesChanged.fire();
Expand Down Expand Up @@ -851,6 +858,9 @@ export class TerminalService implements ITerminalService {
const options: IPickOptions<IProfileQuickPickItem> = {
placeHolder: type === 'createInstance' ? nls.localize('terminal.integrated.selectProfileToCreate', "Select the terminal profile to create") : nls.localize('terminal.integrated.chooseDefaultProfile', "Select your default terminal profile"),
onDidTriggerItemButton: async (context) => {
if ('command' in context.item.profile) {
return;
}
const configKey = `terminal.integrated.profiles.${platformKey}`;
const configProfiles = this._configurationService.inspect<{ [key: string]: ITerminalProfileObject }>(configKey);
const existingProfiles = configProfiles.userValue ? Object.keys(configProfiles.userValue) : [];
Expand Down Expand Up @@ -885,7 +895,18 @@ export class TerminalService implements ITerminalService {
quickPickItems.push({ type: 'separator', label: nls.localize('terminalProfiles', "profiles") });
quickPickItems.push(...configProfiles.map(e => this._createProfileQuickPickItem(e)));
}
if (configProfiles.length > 0) {
// Add contributed profiles, these cannot be defaults
if (type === 'createInstance') {
quickPickItems.push({ type: 'separator', label: nls.localize('terminalProfiles.contributed', "contributed") });
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)));
}
Expand All @@ -895,23 +916,29 @@ export class TerminalService implements ITerminalService {
return;
}
if (type === 'createInstance') {
const launchConfig = { executable: value.profile.path, args: value.profile.args, name: value.profile.overrideName ? value.profile.profileName : undefined };
// TODO: How to support alt here?
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, launchConfig);
instance = this.splitInstance(activeInstance, value.profile);
}
} else {
instance = this.createTerminal(launchConfig);
instance = this.createTerminal(value.profile);
}
if (instance) {
this.showPanel(true);
this.setActiveInstance(instance);
}
} else {
// setDefault
} else { // setDefault
if ('command' in value.profile) {
return; // Should never happen
}
await this._configurationService.updateValue(`terminal.integrated.shell.${platformKey}`, value.profile.path, ConfigurationTarget.USER);
await this._configurationService.updateValue(`terminal.integrated.shellArgs.${platformKey}`, value.profile.args, ConfigurationTarget.USER);
}
Expand All @@ -922,19 +949,21 @@ export class TerminalService implements ITerminalService {
iconClass: ThemeIcon.asClassName(configureTerminalProfileIcon),
tooltip: nls.localize('createQuickLaunchProfile', "Configure Terminal Profile")
}];
const icon = profile.icon ? (iconRegistry.get(profile.icon) || Codicon.terminal) : Codicon.terminal;
const label = `$(${icon.id}) ${profile.profileName}`;
if (profile.args) {
if (typeof profile.args === 'string') {
return { label: profile.profileName, description: `${profile.path} ${profile.args}`, profile, buttons };
return { label, description: `${profile.path} ${profile.args}`, profile, buttons };
}
const argsString = profile.args.map(e => {
if (e.includes(' ')) {
return `"${e.replace('/"/g', '\\"')}"`;
}
return e;
}).join(' ');
return { label: profile.profileName, description: `${profile.path} ${argsString}`, profile, buttons };
return { label, description: `${profile.path} ${argsString}`, profile, buttons };
}
return { label: profile.profileName, description: profile.path, profile, buttons };
return { label, description: profile.path, profile, buttons };
}

public createInstance(container: HTMLElement | undefined, shellLaunchConfig: IShellLaunchConfig): ITerminalInstance {
Expand All @@ -950,18 +979,35 @@ export class TerminalService implements ITerminalService {
return instance;
}

public createTerminal(shell: IShellLaunchConfig = {}): ITerminalInstance {
if (!shell.isExtensionCustomPtyTerminal && !this.isProcessSupportRegistered) {
private _convertProfileToShellLaunchConfig(shellLaunchConfigOrProfile?: IShellLaunchConfig | ITerminalProfile): IShellLaunchConfig {
if (shellLaunchConfigOrProfile && 'profileName' in shellLaunchConfigOrProfile) {
const profile = shellLaunchConfigOrProfile;
return {
executable: profile.path,
args: profile.args,
icon: profile.icon,
name: profile.overrideName ? profile.profileName : undefined
};
}
return shellLaunchConfigOrProfile || {};
}

public createTerminal(shellLaunchConfig?: IShellLaunchConfig): ITerminalInstance;
public createTerminal(profile: ITerminalProfile): ITerminalInstance;
public createTerminal(shellLaunchConfigOrProfile: IShellLaunchConfig | ITerminalProfile): ITerminalInstance {
const shellLaunchConfig = this._convertProfileToShellLaunchConfig(shellLaunchConfigOrProfile);

if (!shellLaunchConfig.isExtensionCustomPtyTerminal && !this.isProcessSupportRegistered) {
throw new Error('Could not create terminal when process support is not registered');
}
if (shell.hideFromUser) {
const instance = this.createInstance(undefined, shell);
if (shellLaunchConfig.hideFromUser) {
const instance = this.createInstance(undefined, shellLaunchConfig);
this._backgroundedTerminalInstances.push(instance);
this._initInstanceListeners(instance);
return instance;
}

const terminalTab = this._instantiationService.createInstance(TerminalTab, this._terminalContainer, shell);
const terminalTab = this._instantiationService.createInstance(TerminalTab, this._terminalContainer, shellLaunchConfig);
this._terminalTabs.push(terminalTab);

const instance = terminalTab.terminalInstances[0];
Expand Down Expand Up @@ -1042,5 +1088,5 @@ export class TerminalService implements ITerminalService {
}

interface IProfileQuickPickItem extends IQuickPickItem {
profile: ITerminalProfile;
profile: ITerminalProfile | ITerminalTypeContribution;
}
8 changes: 8 additions & 0 deletions src/vs/workbench/contrib/terminal/common/terminal.ts
Expand Up @@ -238,6 +238,7 @@ export interface ITerminalProfile {
isWorkspaceProfile?: boolean;
args?: string | string[] | undefined;
overrideName?: boolean;
icon?: string;
}

export const enum ProfileSource {
Expand All @@ -250,13 +251,15 @@ export interface ITerminalExecutable {
args?: string | string[] | undefined;
isAutoDetected?: boolean;
overrideName?: boolean;
icon?: string;
}

export interface ITerminalProfileSource {
source: ProfileSource;
isAutoDetected?: boolean;
overrideName?: boolean;
args?: string | string[] | undefined;
icon?: string;
}

export type ITerminalProfileObject = ITerminalExecutable | ITerminalProfileSource | null;
Expand Down Expand Up @@ -583,6 +586,7 @@ export interface ITerminalContributions {
export interface ITerminalTypeContribution {
title: string;
command: string;
icon?: string;
}

export const terminalContributionsDescriptor: IExtensionPointDescriptor = {
Expand All @@ -607,6 +611,10 @@ export const terminalContributionsDescriptor: IExtensionPointDescriptor = {
description: nls.localize('vscode.extension.contributes.terminal.types.title', "Title for this type of terminal."),
type: 'string',
},
icon: {
description: nls.localize('vscode.extension.contributes.terminal.types.icon', "A codicon to associate with this terminal type."),
type: 'string',
},
},
},
},
Expand Down

0 comments on commit 3028779

Please sign in to comment.