Skip to content

Commit

Permalink
Pull out and test TerminalProfileQuickpick (#136680)
Browse files Browse the repository at this point in the history
  • Loading branch information
meganrogge committed Nov 9, 2021
1 parent 13bf498 commit 8b0720d
Show file tree
Hide file tree
Showing 6 changed files with 420 additions and 241 deletions.
8 changes: 7 additions & 1 deletion src/vs/workbench/contrib/terminal/browser/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { URI } from 'vs/base/common/uri';
import { FindReplaceState } from 'vs/editor/contrib/find/findState';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IShellLaunchConfig, ITerminalDimensions, ITerminalLaunchError, ITerminalProfile, ITerminalTabLayoutInfoById, TerminalIcon, TitleEventSource, TerminalShellType, IExtensionTerminalProfile, TerminalLocation, ProcessPropertyType, ProcessCapability, IProcessPropertyMap } from 'vs/platform/terminal/common/terminal';
import { ICommandTracker, INavigationMode, IRemoteTerminalAttachTarget, IStartExtensionTerminalRequest, ITerminalConfigHelper, ITerminalFont, ITerminalBackend, ITerminalProcessExtHostProxy } from 'vs/workbench/contrib/terminal/common/terminal';
import { ICommandTracker, INavigationMode, IRemoteTerminalAttachTarget, IStartExtensionTerminalRequest, ITerminalConfigHelper, ITerminalFont, ITerminalBackend, ITerminalProcessExtHostProxy, IRegisterContributedProfileArgs } from 'vs/workbench/contrib/terminal/common/terminal';
import type { Terminal as XTermTerminal } from 'xterm';
import type { SearchAddon as XTermSearchAddon } from 'xterm-addon-search';
import type { Unicode11Addon as XTermUnicode11Addon } from 'xterm-addon-unicode11';
Expand All @@ -19,6 +19,7 @@ import { IEditableData } from 'vs/workbench/common/views';
import { DeserializedTerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/terminalEditorSerializer';
import { TerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/terminalEditorInput';
import { EditorGroupColumn } from 'vs/workbench/services/editor/common/editorGroupColumn';
import { IKeyMods } from 'vs/platform/quickinput/common/quickInput';

export const ITerminalService = createDecorator<ITerminalService>('terminalService');
export const ITerminalEditorService = createDecorator<ITerminalEditorService>('terminalEditorService');
Expand Down Expand Up @@ -73,6 +74,11 @@ export const enum Direction {
Down = 3
}

export interface IQuickPickTerminalObject {
config: IRegisterContributedProfileArgs | ITerminalProfile | { profile: IExtensionTerminalProfile, options: { icon?: string, color?: string } } | undefined,
keyMods: IKeyMods | undefined
}

export interface ITerminalGroup {
activeInstance: ITerminalInstance | undefined;
terminalInstances: ITerminalInstance[];
Expand Down
240 changes: 240 additions & 0 deletions src/vs/workbench/contrib/terminal/browser/terminalProfileQuickpick.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { iconRegistry, Codicon } from 'vs/base/common/codicons';
import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IQuickInputService, IKeyMods, IPickOptions, IQuickPickSeparator, IQuickInputButton, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput';
import { IExtensionTerminalProfile, ITerminalProfile, ITerminalProfileObject, TerminalSettingPrefix } from 'vs/platform/terminal/common/terminal';
import { getUriClasses, getColorClass, getColorStyleElement } from 'vs/workbench/contrib/terminal/browser/terminalIcon';
import { configureTerminalProfileIcon } from 'vs/workbench/contrib/terminal/browser/terminalIcons';
import * as nls from 'vs/nls';
import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService';
import { ITerminalProfileService } from 'vs/workbench/contrib/terminal/common/terminal';
import { IQuickPickTerminalObject } from 'vs/workbench/contrib/terminal/browser/terminal';


type DefaultProfileName = string;
export class TerminalProfileQuickpick {
constructor(
@ITerminalProfileService private readonly _terminalProfileService: ITerminalProfileService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
@IQuickInputService private readonly _quickInputService: IQuickInputService,
@IThemeService private readonly _themeService: IThemeService
) { }

async showAndGetResult(type: 'setDefault' | 'createInstance'): Promise<IQuickPickTerminalObject | DefaultProfileName | undefined> {
const platformKey = await this._terminalProfileService.getPlatformKey();
const profilesKey = TerminalSettingPrefix.Profiles + platformKey;
const result = await this._createAndShow(type);
const defaultProfileKey = `${TerminalSettingPrefix.DefaultProfile}${platformKey}`;
if (!result) {
return;
}
if (type === 'setDefault') {
if ('command' in result.profile) {
return; // Should never happen
} else if ('id' in result.profile) {
// extension contributed profile
await this._configurationService.updateValue(defaultProfileKey, result.profile.title, ConfigurationTarget.USER);
return {
config: {
extensionIdentifier: result.profile.extensionIdentifier,
id: result.profile.id,
title: result.profile.title,
options: {
color: result.profile.color,
icon: result.profile.icon
}
},
keyMods: result.keyMods
};
}

// Add the profile to settings if necessary
if ('isAutoDetected' in result.profile) {
const profilesConfig = await this._configurationService.getValue(profilesKey);
if (typeof profilesConfig === 'object') {
const newProfile: ITerminalProfileObject = {
path: result.profile.path
};
if (result.profile.args) {
newProfile.args = result.profile.args;
}
(profilesConfig as { [key: string]: ITerminalProfileObject })[result.profile.profileName] = newProfile;
}
await this._configurationService.updateValue(profilesKey, profilesConfig, ConfigurationTarget.USER);
}
// Set the default profile
await this._configurationService.updateValue(defaultProfileKey, result.profileName, ConfigurationTarget.USER);
} else if (type === 'createInstance') {
if ('id' in result.profile) {
return {
config: {
extensionIdentifier: result.profile.extensionIdentifier,
id: result.profile.id,
title: result.profile.title,
options: {
icon: result.profile.icon,
color: result.profile.color,
}
},
keyMods: result.keyMods
};
} else {
return { config: result.profile, keyMods: result.keyMods };
}
}
// for tests
return 'profileName' in result.profile ? result.profile.profileName : result.profile.title;
}

private async _createAndShow(type: 'setDefault' | 'createInstance'): Promise<IProfileQuickPickItem | undefined> {
const platformKey = await this._terminalProfileService.getPlatformKey();
const profiles = this._terminalProfileService.availableProfiles;
const profilesKey = TerminalSettingPrefix.Profiles + platformKey;
const defaultProfileName = this._terminalProfileService.getDefaultProfileName();
let keyMods: IKeyMods | undefined;
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;
}
if ('id' in context.item.profile) {
return;
}
const configProfiles: { [key: string]: any } = this._configurationService.getValue(TerminalSettingPrefix.Profiles + platformKey);
const existingProfiles = !!configProfiles ? Object.keys(configProfiles) : [];
const name = await this._quickInputService.input({
prompt: nls.localize('enterTerminalProfileName', "Enter terminal profile name"),
value: context.item.profile.profileName,
validateInput: async input => {
if (existingProfiles.includes(input)) {
return nls.localize('terminalProfileAlreadyExists', "A terminal profile already exists with that name");
}
return undefined;
}
});
if (!name) {
return;
}
const newConfigValue: { [key: string]: ITerminalProfileObject } = { ...configProfiles } ?? {};
newConfigValue[name] = {
path: context.item.profile.path,
args: context.item.profile.args
};
await this._configurationService.updateValue(profilesKey, newConfigValue, ConfigurationTarget.USER);
},
onKeyMods: mods => keyMods = mods
};

// Build quick pick items
const quickPickItems: (IProfileQuickPickItem | IQuickPickSeparator)[] = [];
const configProfiles = profiles.filter(e => !e.isAutoDetected);
const autoDetectedProfiles = profiles.filter(e => e.isAutoDetected);

if (configProfiles.length > 0) {
quickPickItems.push({ type: 'separator', label: nls.localize('terminalProfiles', "profiles") });
quickPickItems.push(...this._sortProfileQuickPickItems(configProfiles.map(e => this._createProfileQuickPickItem(e)), defaultProfileName!));
}

quickPickItems.push({ type: 'separator', label: nls.localize('ICreateContributedTerminalProfileOptions', "contributed") });
const contributedProfiles: IProfileQuickPickItem[] = [];
for (const contributed of this._terminalProfileService.contributedProfiles) {
if (typeof contributed.icon === 'string' && contributed.icon.startsWith('$(')) {
contributed.icon = contributed.icon.substring(2, contributed.icon.length - 1);
}
const icon = contributed.icon && typeof contributed.icon === 'string' ? (iconRegistry.get(contributed.icon) || Codicon.terminal) : Codicon.terminal;
const uriClasses = getUriClasses(contributed, this._themeService.getColorTheme().type, true);
const colorClass = getColorClass(contributed);
const iconClasses = [];
if (uriClasses) {
iconClasses.push(...uriClasses);
}
if (colorClass) {
iconClasses.push(colorClass);
}
contributedProfiles.push({
label: `$(${icon.id}) ${contributed.title}`,
profile: {
extensionIdentifier: contributed.extensionIdentifier,
title: contributed.title,
icon: contributed.icon,
id: contributed.id,
color: contributed.color
},
profileName: contributed.title,
iconClasses
});
}

if (contributedProfiles.length > 0) {
quickPickItems.push(...this._sortProfileQuickPickItems(contributedProfiles, defaultProfileName!));
}

if (autoDetectedProfiles.length > 0) {
quickPickItems.push({ type: 'separator', label: nls.localize('terminalProfiles.detected', "detected") });
quickPickItems.push(...this._sortProfileQuickPickItems(autoDetectedProfiles.map(e => this._createProfileQuickPickItem(e)), defaultProfileName!));
}
const styleElement = getColorStyleElement(this._themeService.getColorTheme());
document.body.appendChild(styleElement);

const result = await this._quickInputService.pick(quickPickItems, options);
document.body.removeChild(styleElement);
if (!result) {
return undefined;
}
if (keyMods) {
result.keyMods = keyMods;
}
return result;
}

private _createProfileQuickPickItem(profile: ITerminalProfile): IProfileQuickPickItem {
const buttons: IQuickInputButton[] = [{
iconClass: ThemeIcon.asClassName(configureTerminalProfileIcon),
tooltip: nls.localize('createQuickLaunchProfile', "Configure Terminal Profile")
}];
const icon = (profile.icon && ThemeIcon.isThemeIcon(profile.icon)) ? profile.icon : Codicon.terminal;
const label = `$(${icon.id}) ${profile.profileName}`;
const colorClass = getColorClass(profile);
const iconClasses = [];
if (colorClass) {
iconClasses.push(colorClass);
}

if (profile.args) {
if (typeof profile.args === 'string') {
return { label, description: `${profile.path} ${profile.args}`, profile, profileName: profile.profileName, buttons, iconClasses };
}
const argsString = profile.args.map(e => {
if (e.includes(' ')) {
return `"${e.replace('/"/g', '\\"')}"`;
}
return e;
}).join(' ');
return { label, description: `${profile.path} ${argsString}`, profile, profileName: profile.profileName, buttons, iconClasses };
}
return { label, description: profile.path, profile, profileName: profile.profileName, buttons, iconClasses };
}

private _sortProfileQuickPickItems(items: IProfileQuickPickItem[], defaultProfileName: string) {
return items.sort((a, b) => {
if (b.profileName === defaultProfileName) {
return 1;
}
if (a.profileName === defaultProfileName) {
return -1;
}
return a.profileName.localeCompare(b.profileName);
});
}
}

export interface IProfileQuickPickItem extends IQuickPickItem {
profile: ITerminalProfile | IExtensionTerminalProfile;
profileName: string;
keyMods?: IKeyMods | undefined;
}
30 changes: 15 additions & 15 deletions src/vs/workbench/contrib/terminal/browser/terminalProfileService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { isMacintosh, isWeb, isWindows, OperatingSystem, OS } from 'vs/base/common/platform';
import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { ITerminalProfile, IExtensionTerminalProfile, TerminalSettingPrefix, TerminalSettingId, ICreateContributedTerminalProfileOptions, ITerminalProfileObject, IShellLaunchConfig } from 'vs/platform/terminal/common/terminal';
import { ITerminalProfile, IExtensionTerminalProfile, TerminalSettingPrefix, TerminalSettingId, ITerminalProfileObject, IShellLaunchConfig } from 'vs/platform/terminal/common/terminal';
import { registerTerminalDefaultProfileConfiguration } from 'vs/platform/terminal/common/terminalPlatformConfiguration';
import { terminalIconsEqual, terminalProfileArgsMatch } from 'vs/platform/terminal/common/terminalProfiles';
import { ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal';
import { refreshTerminalActions } from 'vs/workbench/contrib/terminal/browser/terminalActions';
import { ITerminalProfileProvider, ITerminalProfileService } from 'vs/workbench/contrib/terminal/common/terminal';
import { IRegisterContributedProfileArgs, ITerminalProfileProvider, ITerminalProfileService } from 'vs/workbench/contrib/terminal/common/terminal';
import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey';
import { ITerminalContributionService } from 'vs/workbench/contrib/terminal/common/terminalExtensionPoints';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
Expand Down Expand Up @@ -61,7 +61,7 @@ export class TerminalProfileService implements ITerminalProfileService {
this._extensionService.onDidChangeExtensions(() => this.refreshAvailableProfiles());

this._configurationService.onDidChangeConfiguration(async e => {
const platformKey = await this._getPlatformKey();
const platformKey = await this.getPlatformKey();
if (e.affectsConfiguration(TerminalSettingPrefix.DefaultProfile + platformKey) ||
e.affectsConfiguration(TerminalSettingPrefix.Profiles + platformKey) ||
e.affectsConfiguration(TerminalSettingId.UseWslProfiles)) {
Expand Down Expand Up @@ -112,7 +112,7 @@ export class TerminalProfileService implements ITerminalProfileService {
}

private async _updateContributedProfiles(): Promise<boolean> {
const platformKey = await this._getPlatformKey();
const platformKey = await this.getPlatformKey();
const excludedContributedProfiles: string[] = [];
const configProfiles: { [key: string]: any } = this._configurationService.getValue(TerminalSettingPrefix.Profiles + platformKey);
for (const [profileName, value] of Object.entries(configProfiles)) {
Expand All @@ -136,7 +136,7 @@ export class TerminalProfileService implements ITerminalProfileService {
if (!primaryBackend) {
return this._availableProfiles || [];
}
const platform = await this._getPlatformKey();
const platform = await this.getPlatformKey();
this._defaultProfileName = this._configurationService.getValue(`${TerminalSettingPrefix.DefaultProfile}${platform}`) ?? undefined;
return primaryBackend.getProfiles(this._configurationService.getValue(`${TerminalSettingPrefix.Profiles}${platform}`), this._defaultProfileName, includeDetectedProfiles);
}
Expand All @@ -151,7 +151,7 @@ export class TerminalProfileService implements ITerminalProfileService {
refreshTerminalActions(profiles);
}

private async _getPlatformKey(): Promise<string> {
async getPlatformKey(): Promise<string> {
const env = await this._remoteAgentService.getEnvironment();
if (env) {
return env.os === OperatingSystem.Windows ? 'windows' : (env.os === OperatingSystem.Macintosh ? 'osx' : 'linux');
Expand All @@ -169,19 +169,19 @@ export class TerminalProfileService implements ITerminalProfileService {
return toDisposable(() => this._profileProviders.delete(id));
}

async registerContributedProfile(extensionIdentifier: string, id: string, title: string, options: ICreateContributedTerminalProfileOptions): Promise<void> {
const platformKey = await this._getPlatformKey();
async registerContributedProfile(args: IRegisterContributedProfileArgs): Promise<void> {
const platformKey = await this.getPlatformKey();
const profilesConfig = await this._configurationService.getValue(`${TerminalSettingPrefix.Profiles}${platformKey}`);
if (typeof profilesConfig === 'object') {
const newProfile: IExtensionTerminalProfile = {
extensionIdentifier: extensionIdentifier,
icon: options.icon,
id,
title: title,
color: options.color
extensionIdentifier: args.extensionIdentifier,
icon: args.options.icon,
id: args.id,
title: args.title,
color: args.options.color
};

(profilesConfig as { [key: string]: ITerminalProfileObject })[title] = newProfile;
(profilesConfig as { [key: string]: ITerminalProfileObject })[args.title] = newProfile;
}
await this._configurationService.updateValue(`${TerminalSettingPrefix.Profiles}${platformKey}`, profilesConfig, ConfigurationTarget.USER);
return;
Expand All @@ -191,7 +191,7 @@ export class TerminalProfileService implements ITerminalProfileService {
// prevents recursion with the MainThreadTerminalService call to create terminal
// and defers to the provided launch config when an executable is provided
if (shellLaunchConfig && !shellLaunchConfig.extHostTerminalId && !('executable' in shellLaunchConfig)) {
const key = await this._getPlatformKey();
const key = await this.getPlatformKey();
const defaultProfileName = this._configurationService.getValue(`${TerminalSettingPrefix.DefaultProfile}${key}`);
const contributedDefaultProfile = this.contributedProfiles.find(p => p.title === defaultProfileName);
return contributedDefaultProfile;
Expand Down

0 comments on commit 8b0720d

Please sign in to comment.