diff --git a/src/vs/platform/terminal/common/terminalProfiles.ts b/src/vs/platform/terminal/common/terminalProfiles.ts index 63f49db9bd1..970db1c3b28 100644 --- a/src/vs/platform/terminal/common/terminalProfiles.ts +++ b/src/vs/platform/terminal/common/terminalProfiles.ts @@ -4,7 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { Codicon } from 'vs/base/common/codicons'; -import { IExtensionTerminalProfile, ITerminalProfile } from 'vs/platform/terminal/common/terminal'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import { IExtensionTerminalProfile, ITerminalProfile, TerminalIcon } from 'vs/platform/terminal/common/terminal'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; export function createProfileSchemaEnums(detectedProfiles: ITerminalProfile[], extensionProfiles?: readonly IExtensionTerminalProfile[]): { @@ -56,3 +57,60 @@ function createExtensionProfileDescription(profile: IExtensionTerminalProfile): let description = `$(${ThemeIcon.isThemeIcon(profile.icon) ? profile.icon.id : profile.icon ? profile.icon : Codicon.terminal.id}) ${profile.title}\n- extensionIdenfifier: ${profile.extensionIdentifier}`; return description; } + + +export function terminalProfileArgsMatch(args1: string | string[] | undefined, args2: string | string[] | undefined): boolean { + if (!args1 && !args2) { + return true; + } else if (typeof args1 === 'string' && typeof args2 === 'string') { + return args1 === args2; + } else if (Array.isArray(args1) && Array.isArray(args2)) { + if (args1.length !== args2.length) { + return false; + } + for (let i = 0; i < args1.length; i++) { + if (args1[i] !== args2[i]) { + return false; + } + } + return true; + } + return false; +} + +export function terminalIconsEqual(iconOne?: TerminalIcon, iconTwo?: TerminalIcon): boolean { + if (!iconOne && !iconTwo) { + return true; + } else if (!iconOne || !iconTwo) { + return false; + } + + if (ThemeIcon.isThemeIcon(iconOne) && ThemeIcon.isThemeIcon(iconTwo)) { + return iconOne.id === iconTwo.id && iconOne.color === iconTwo.color; + } + if (typeof iconOne === 'object' && iconOne && 'light' in iconOne && 'dark' in iconOne + && typeof iconTwo === 'object' && iconTwo && 'light' in iconTwo && 'dark' in iconTwo) { + const castedIcon = (iconOne as { light: unknown, dark: unknown }); + const castedIconTwo = (iconTwo as { light: unknown, dark: unknown }); + if ((URI.isUri(castedIcon.light) || isUriComponents(castedIcon.light)) && (URI.isUri(castedIcon.dark) || isUriComponents(castedIcon.dark)) + && (URI.isUri(castedIconTwo.light) || isUriComponents(castedIconTwo.light)) && (URI.isUri(castedIconTwo.dark) || isUriComponents(castedIconTwo.dark))) { + return castedIcon.light.path === castedIconTwo.light.path && castedIcon.dark.path === castedIconTwo.dark.path; + } + } + if ((URI.isUri(iconOne) && URI.isUri(iconTwo)) || (isUriComponents(iconOne) || isUriComponents(iconTwo))) { + const castedIcon = (iconOne as { scheme: unknown, path: unknown }); + const castedIconTwo = (iconTwo as { scheme: unknown, path: unknown }); + return castedIcon.path === castedIconTwo.path && castedIcon.scheme === castedIconTwo.scheme; + } + + return false; +} + + +export function isUriComponents(thing: unknown): thing is UriComponents { + if (!thing) { + return false; + } + return typeof (thing).path === 'string' && + typeof (thing).scheme === 'string'; +} diff --git a/src/vs/workbench/api/browser/mainThreadTerminalService.ts b/src/vs/workbench/api/browser/mainThreadTerminalService.ts index fb0827fa9dd..4e8435e598e 100644 --- a/src/vs/workbench/api/browser/mainThreadTerminalService.ts +++ b/src/vs/workbench/api/browser/mainThreadTerminalService.ts @@ -16,7 +16,7 @@ import { ITerminalEditorService, ITerminalExternalLinkProvider, ITerminalGroupSe import { TerminalProcessExtHostProxy } from 'vs/workbench/contrib/terminal/browser/terminalProcessExtHostProxy'; import { IEnvironmentVariableService, ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { deserializeEnvironmentVariableCollection, serializeEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableShared'; -import { IStartExtensionTerminalRequest, ITerminalProcessExtHostProxy, ITerminalProfileResolverService } from 'vs/workbench/contrib/terminal/common/terminal'; +import { IStartExtensionTerminalRequest, ITerminalProcessExtHostProxy, ITerminalProfileResolverService, ITerminalProfileService } from 'vs/workbench/contrib/terminal/common/terminal'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { withNullAsUndefined } from 'vs/base/common/types'; import { OperatingSystem, OS } from 'vs/base/common/platform'; @@ -57,7 +57,8 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape @ITerminalProfileResolverService private readonly _terminalProfileResolverService: ITerminalProfileResolverService, @IRemoteAgentService remoteAgentService: IRemoteAgentService, @ITerminalGroupService private readonly _terminalGroupService: ITerminalGroupService, - @ITerminalEditorService private readonly _terminalEditorService: ITerminalEditorService + @ITerminalEditorService private readonly _terminalEditorService: ITerminalEditorService, + @ITerminalProfileService private readonly _terminalProfileService: ITerminalProfileService ) { this._proxy = _extHostContext.getProxy(ExtHostContext.ExtHostTerminalService); @@ -97,7 +98,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape this._os = env?.os || OS; this._updateDefaultProfile(); }); - this._terminalService.onDidChangeAvailableProfiles(() => this._updateDefaultProfile()); + this._terminalProfileService.onDidChangeAvailableProfiles(() => this._updateDefaultProfile()); } public dispose(): void { @@ -226,7 +227,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape public $registerProfileProvider(id: string, extensionIdentifier: string): void { // Proxy profile provider requests through the extension host - this._profileProviders.set(id, this._terminalService.registerTerminalProfileProvider(extensionIdentifier, id, { + this._profileProviders.set(id, this._terminalProfileService.registerTerminalProfileProvider(extensionIdentifier, id, { createContributedTerminalProfile: async (options) => { return this._proxy.$createContributedProfileTerminal(id, options); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts index 52d28939c1f..50650bbed36 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts @@ -17,7 +17,7 @@ import { getQuickNavigateHandler } from 'vs/workbench/browser/quickaccess'; import { Extensions as ViewContainerExtensions, IViewContainersRegistry, ViewContainerLocation, IViewsRegistry } from 'vs/workbench/common/views'; import { registerTerminalActions, terminalSendSequenceCommand } from 'vs/workbench/contrib/terminal/browser/terminalActions'; import { TerminalViewPane } from 'vs/workbench/contrib/terminal/browser/terminalView'; -import { TERMINAL_VIEW_ID, TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal'; +import { TERMINAL_VIEW_ID, TerminalCommandId, ITerminalProfileService } from 'vs/workbench/contrib/terminal/common/terminal'; import { registerColors } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry'; import { setupTerminalCommands } from 'vs/workbench/contrib/terminal/browser/terminalCommands'; import { TerminalService } from 'vs/workbench/contrib/terminal/browser/terminalService'; @@ -45,6 +45,7 @@ import { TerminalEditorService } from 'vs/workbench/contrib/terminal/browser/ter import { TerminalInputSerializer } from 'vs/workbench/contrib/terminal/browser/terminalEditorSerializer'; import { TerminalGroupService } from 'vs/workbench/contrib/terminal/browser/terminalGroupService'; import { TerminalContextKeys, TerminalContextKeyStrings } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; +import { TerminalProfileService } from 'vs/workbench/contrib/terminal/browser/terminalProfileService'; // Register services registerSingleton(ITerminalService, TerminalService, true); @@ -52,6 +53,7 @@ registerSingleton(ITerminalEditorService, TerminalEditorService, true); registerSingleton(ITerminalGroupService, TerminalGroupService, true); registerSingleton(IRemoteTerminalService, RemoteTerminalService); registerSingleton(ITerminalInstanceService, TerminalInstanceService, true); +registerSingleton(ITerminalProfileService, TerminalProfileService, true); // Register quick accesses const quickAccessRegistry = (Registry.as(QuickAccessExtensions.Quickaccess)); diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index c39a1b1e393..063909586ad 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -8,7 +8,7 @@ import { IDisposable } from 'vs/base/common/lifecycle'; 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, ITerminalChildProcess, ITerminalDimensions, ITerminalLaunchError, ITerminalProfile, ITerminalTabLayoutInfoById, TerminalIcon, TitleEventSource, TerminalShellType, IExtensionTerminalProfile, TerminalLocation, ICreateContributedTerminalProfileOptions, ProcessPropertyType, ProcessCapability } from 'vs/platform/terminal/common/terminal'; +import { IShellLaunchConfig, ITerminalChildProcess, ITerminalDimensions, ITerminalLaunchError, ITerminalProfile, ITerminalTabLayoutInfoById, TerminalIcon, TitleEventSource, TerminalShellType, IExtensionTerminalProfile, TerminalLocation, ProcessPropertyType, ProcessCapability } from 'vs/platform/terminal/common/terminal'; import { ICommandTracker, INavigationMode, IOffProcessTerminalService, IRemoteTerminalAttachTarget, IStartExtensionTerminalRequest, ITerminalConfigHelper, ITerminalFont, ITerminalProcessExtHostProxy } from 'vs/workbench/contrib/terminal/common/terminal'; import type { Terminal as XTermTerminal } from 'xterm'; import type { SearchAddon as XTermSearchAddon } from 'xterm-addon-search'; @@ -106,9 +106,6 @@ export interface ITerminalService extends ITerminalInstanceHost { configHelper: ITerminalConfigHelper; isProcessSupportRegistered: boolean; readonly connectionState: TerminalConnectionState; - readonly availableProfiles: ITerminalProfile[]; - readonly contributedProfiles: IExtensionTerminalProfile[]; - readonly profilesReady: Promise; readonly defaultLocation: TerminalLocation; initializeTerminals(): Promise; @@ -126,7 +123,6 @@ export interface ITerminalService extends ITerminalInstanceHost { onDidInputInstanceData: Event; onDidRegisterProcessSupport: Event; onDidChangeConnectionState: Event; - onDidChangeAvailableProfiles: Event; /** * Creates a terminal. @@ -169,8 +165,6 @@ export interface ITerminalService extends ITerminalInstanceHost { */ registerLinkProvider(linkProvider: ITerminalExternalLinkProvider): IDisposable; - registerTerminalProfileProvider(extensionIdenfifier: string, id: string, profileProvider: ITerminalProfileProvider): IDisposable; - showProfileQuickPick(type: 'setDefault' | 'createInstance', cwd?: string | URI): Promise; setContainers(panelContainer: HTMLElement, terminalContainer: HTMLElement): void; @@ -186,7 +180,6 @@ export interface ITerminalService extends ITerminalInstanceHost { getInstanceHost(target: ITerminalLocationOptions | undefined): ITerminalInstanceHost; getFindHost(instance?: ITerminalInstance): ITerminalFindHost; - getDefaultProfileName(): string; resolveLocation(location?: ITerminalLocationOptions): TerminalLocation | undefined setNativeDelegate(nativeCalls: ITerminalServiceNativeDelegate): void; } @@ -346,10 +339,6 @@ export interface ITerminalExternalLinkProvider { provideLinks(instance: ITerminalInstance, line: string): Promise; } -export interface ITerminalProfileProvider { - createContributedTerminalProfile(options: ICreateContributedTerminalProfileOptions): Promise; -} - export interface ITerminalLink { /** The startIndex of the link in the line. */ startIndex: number; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index 447a43ba178..1bacb7cd81c 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -35,7 +35,7 @@ import { ResourceContextKey } from 'vs/workbench/common/resources'; import { FindInFilesCommand, IFindInFilesArgs } from 'vs/workbench/contrib/search/browser/searchActions'; import { Direction, ICreateTerminalOptions, IRemoteTerminalService, ITerminalGroupService, ITerminalInstance, ITerminalInstanceService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalQuickAccessProvider } from 'vs/workbench/contrib/terminal/browser/terminalQuickAccess'; -import { ILocalTerminalService, IRemoteTerminalAttachTarget, ITerminalConfigHelper, TerminalCommandId, TERMINAL_ACTION_CATEGORY } from 'vs/workbench/contrib/terminal/common/terminal'; +import { ILocalTerminalService, IRemoteTerminalAttachTarget, ITerminalConfigHelper, ITerminalProfileService, TerminalCommandId, TERMINAL_ACTION_CATEGORY } from 'vs/workbench/contrib/terminal/common/terminal'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; import { createProfileSchemaEnums } from 'vs/platform/terminal/common/terminalProfiles'; import { terminalStrings } from 'vs/workbench/contrib/terminal/common/terminalStrings'; @@ -1982,6 +1982,7 @@ export function registerTerminalActions() { } async run(accessor: ServicesAccessor, item?: string) { const terminalService = accessor.get(ITerminalService); + const terminalProfileService = accessor.get(ITerminalProfileService); const terminalGroupService = accessor.get(ITerminalGroupService); if (!item || !item.split) { return Promise.resolve(null); @@ -2000,7 +2001,7 @@ export function registerTerminalActions() { return terminalGroupService.showPanel(true); } - const quickSelectProfiles = terminalService.availableProfiles; + const quickSelectProfiles = terminalProfileService.availableProfiles; // Remove 'New ' from the selected item to get the profile name const profileSelection = item.substring(4); @@ -2109,6 +2110,7 @@ export function refreshTerminalActions(detectedProfiles: ITerminalProfile[]) { } async run(accessor: ServicesAccessor, eventOrOptionsOrProfile: MouseEvent | ICreateTerminalOptions | ITerminalProfile | { profileName: string } | undefined, profile?: ITerminalProfile) { const terminalService = accessor.get(ITerminalService); + const terminalProfileService = accessor.get(ITerminalProfileService); if (!terminalService.isProcessSupportRegistered) { return; @@ -2124,7 +2126,7 @@ export function refreshTerminalActions(detectedProfiles: ITerminalProfile[]) { let cwd: string | URI | undefined; if (typeof eventOrOptionsOrProfile === 'object' && eventOrOptionsOrProfile && 'profileName' in eventOrOptionsOrProfile) { - const config = terminalService.availableProfiles.find(profile => profile.profileName === eventOrOptionsOrProfile.profileName); + const config = terminalProfileService.availableProfiles.find(profile => profile.profileName === eventOrOptionsOrProfile.profileName); if (!config) { throw new Error(`Could not find terminal profile "${eventOrOptionsOrProfile.profileName}"`); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts b/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts index e95ff7bcb4d..124238bcac6 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts @@ -23,7 +23,7 @@ import { ITerminalEditorService, ITerminalService } from 'vs/workbench/contrib/t import { TerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/terminalEditorInput'; import { TerminalFindWidget } from 'vs/workbench/contrib/terminal/browser/terminalFindWidget'; import { getTerminalActionBarArgs } from 'vs/workbench/contrib/terminal/browser/terminalMenus'; -import { ITerminalProfileResolverService, TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal'; +import { ITerminalProfileResolverService, ITerminalProfileService, TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { isLinux, isMacintosh } from 'vs/base/common/platform'; import { BrowserFeatures } from 'vs/base/browser/canIUse'; @@ -69,7 +69,8 @@ export class TerminalEditor extends EditorPane { @IMenuService menuService: IMenuService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IContextMenuService private readonly _contextMenuService: IContextMenuService, - @INotificationService private readonly _notificationService: INotificationService + @INotificationService private readonly _notificationService: INotificationService, + @ITerminalProfileService private readonly _terminalProfileService: ITerminalProfileService ) { super(TerminalEditor.ID, telemetryService, themeService, storageService); this._findState = new FindReplaceState(); @@ -201,7 +202,7 @@ export class TerminalEditor extends EditorPane { switch (action.id) { case TerminalCommandId.CreateWithProfileButton: { const location = { viewColumn: ACTIVE_GROUP }; - const actions = getTerminalActionBarArgs(location, this._terminalService.availableProfiles, this._getDefaultProfileName(), this._terminalService.contributedProfiles, this._instantiationService, this._terminalService, this._contextKeyService, this._commandService, this._dropdownMenu); + const actions = getTerminalActionBarArgs(location, this._terminalProfileService.availableProfiles, this._getDefaultProfileName(), this._terminalProfileService.contributedProfiles, this._instantiationService, this._terminalService, this._contextKeyService, this._commandService, this._dropdownMenu); const button = this._instantiationService.createInstance(DropdownWithPrimaryActionViewItem, actions.primaryAction, actions.dropdownAction, actions.dropdownMenuActions, actions.className, this._contextMenuService, {}); return button; } @@ -212,7 +213,7 @@ export class TerminalEditor extends EditorPane { private _getDefaultProfileName(): string { let defaultProfileName; try { - defaultProfileName = this._terminalService.getDefaultProfileName(); + defaultProfileName = this._terminalProfileService.getDefaultProfileName(); } catch (e) { defaultProfileName = this._terminalProfileResolverService.defaultProfileName; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 1f6f73de943..f8bba94afd2 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -18,7 +18,7 @@ import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/c import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ILogService } from 'vs/platform/log/common/log'; -import { INotificationService, IPromptChoice, NeverShowAgainScope, Severity } from 'vs/platform/notification/common/notification'; +import { INotificationService, IPromptChoice, Severity } from 'vs/platform/notification/common/notification'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { activeContrastBorder, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground } from 'vs/platform/theme/common/colorRegistry'; import { ICssStyleCollector, IColorTheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; @@ -38,7 +38,7 @@ import { TypeAheadAddon } from 'vs/workbench/contrib/terminal/browser/terminalTy import { BrowserFeatures } from 'vs/base/browser/canIUse'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { IEnvironmentVariableInfo } from 'vs/workbench/contrib/terminal/common/environmentVariable'; -import { IProcessDataEvent, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, TerminalShellType, TerminalSettingId, TitleEventSource, TerminalIcon, TerminalSettingPrefix, ITerminalProfileObject, ProcessPropertyType, ProcessCapability, IProcessPropertyMap, TerminalLocation } from 'vs/platform/terminal/common/terminal'; +import { IProcessDataEvent, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, TerminalShellType, TerminalSettingId, TitleEventSource, TerminalIcon, TerminalLocation, ProcessPropertyType, ProcessCapability, IProcessPropertyMap } from 'vs/platform/terminal/common/terminal'; import { IProductService } from 'vs/platform/product/common/productService'; import { formatMessageForTerminal } from 'vs/workbench/contrib/terminal/common/terminalStrings'; import { AutoOpenBarrier, Promises } from 'vs/base/common/async'; @@ -65,10 +65,6 @@ import { ScrollbarVisibility } from 'vs/base/common/scrollable'; import { LineDataEventAddon } from 'vs/workbench/contrib/terminal/browser/xterm/lineDataEventAddon'; import { XtermTerminal } from 'vs/workbench/contrib/terminal/browser/xterm/xtermTerminal'; -const SHOULD_PROMPT_FOR_PROFILE_MIGRATION_KEY = 'terminals.integrated.profile-migration'; - -let migrationMessageShown = false; - const enum Constants { /** * The maximum amount of milliseconds to wait for a container before starting to create the @@ -411,7 +407,6 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { window.clearTimeout(initialDataEventsTimeout); } })); - this.showProfileMigrationNotification(); } private _getIcon(): TerminalIcon | undefined { @@ -439,53 +434,6 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._register(disposable); } - async showProfileMigrationNotification(): Promise { - const platform = this._getPlatformKey(); - const shouldMigrateToProfile = (!!this._configurationService.getValue(TerminalSettingPrefix.Shell + platform) || - !!this._configurationService.inspect(TerminalSettingPrefix.ShellArgs + platform).userValue) && - !!this._configurationService.getValue(TerminalSettingPrefix.DefaultProfile + platform); - if (shouldMigrateToProfile && this._storageService.getBoolean(SHOULD_PROMPT_FOR_PROFILE_MIGRATION_KEY, StorageScope.WORKSPACE, true) && !migrationMessageShown) { - this._notificationService.prompt( - Severity.Info, - nls.localize('terminalProfileMigration', "The terminal is using deprecated shell/shellArgs settings, do you want to migrate it to a profile?"), - [ - { - label: nls.localize('migrateToProfile', "Migrate"), - run: async () => { - const shell = this._configurationService.getValue(TerminalSettingPrefix.Shell + platform); - const shellArgs = this._configurationService.getValue(TerminalSettingPrefix.ShellArgs + platform); - const profile = await this._terminalProfileResolverService.createProfileFromShellAndShellArgs(shell, shellArgs); - if (typeof profile === 'string') { - await this._configurationService.updateValue(TerminalSettingPrefix.DefaultProfile + platform, profile); - this._logService.trace(`migrated from shell/shellArgs, using existing profile ${profile}`); - } else { - const profiles = { ...this._configurationService.inspect>(TerminalSettingPrefix.Profiles + platform).userValue } || {}; - const profileConfig: ITerminalProfileObject = { path: profile.path }; - if (profile.args) { - profileConfig.args = profile.args; - } - profiles[profile.profileName] = profileConfig; - await this._configurationService.updateValue(TerminalSettingPrefix.Profiles + platform, profiles); - await this._configurationService.updateValue(TerminalSettingPrefix.DefaultProfile + platform, profile.profileName); - this._logService.trace(`migrated from shell/shellArgs, ${shell} ${shellArgs} to profile ${JSON.stringify(profile)}`); - } - await this._configurationService.updateValue(TerminalSettingPrefix.Shell + platform, undefined); - await this._configurationService.updateValue(TerminalSettingPrefix.ShellArgs + platform, undefined); - } - } as IPromptChoice, - ], - { - neverShowAgain: { id: SHOULD_PROMPT_FOR_PROFILE_MIGRATION_KEY, scope: NeverShowAgainScope.WORKSPACE } - } - ); - migrationMessageShown = true; - } - } - - private _getPlatformKey(): string { - return isWindows ? 'windows' : (isMacintosh ? 'osx' : 'linux'); - } - private _initDimensions(): void { // The terminal panel needs to have been created if (!this._container) { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts b/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts index fd5bb6b319b..6ba70f5bb24 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts @@ -9,20 +9,25 @@ import { withNullAsUndefined } from 'vs/base/common/types'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService } from 'vs/platform/log/common/log'; import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { IRemoteTerminalService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { IRemoteTerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { IProcessEnvironment, OperatingSystem, OS } from 'vs/base/common/platform'; -import { IShellLaunchConfig, ITerminalProfile, TerminalIcon, TerminalSettingId, TerminalSettingPrefix } from 'vs/platform/terminal/common/terminal'; -import { IShellLaunchConfigResolveOptions, ITerminalProfileResolverService } from 'vs/workbench/contrib/terminal/common/terminal'; +import { IShellLaunchConfig, ITerminalProfile, ITerminalProfileObject, TerminalIcon, TerminalSettingId, TerminalSettingPrefix } from 'vs/platform/terminal/common/terminal'; +import { IShellLaunchConfigResolveOptions, ITerminalProfileResolverService, ITerminalProfileService } from 'vs/workbench/contrib/terminal/common/terminal'; import * as path from 'vs/base/common/path'; import { Codicon, iconRegistry } from 'vs/base/common/codicons'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { debounce } from 'vs/base/common/decorators'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { URI, UriComponents } from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { equals } from 'vs/base/common/arrays'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import Severity from 'vs/base/common/severity'; +import { INotificationService, IPromptChoice, NeverShowAgainScope } from 'vs/platform/notification/common/notification'; +import { localize } from 'vs/nls'; import { deepClone } from 'vs/base/common/objects'; +import { terminalProfileArgsMatch, isUriComponents } from 'vs/platform/terminal/common/terminalProfiles'; export interface IProfileContextProvider { getDefaultSystemShell: (remoteAuthority: string | undefined, os: OperatingSystem) => Promise; @@ -31,6 +36,16 @@ export interface IProfileContextProvider { const generatedProfileName = 'Generated'; +/* +* Resolves terminal shell launch config and terminal +* profiles for the given operating system, +* environment, and user configuration +*/ + +const SHOULD_PROMPT_FOR_PROFILE_MIGRATION_KEY = 'terminals.integrated.profile-migration'; + +let migrationMessageShown = false; + export abstract class BaseTerminalProfileResolverService implements ITerminalProfileResolverService { declare _serviceBrand: undefined; @@ -45,9 +60,11 @@ export abstract class BaseTerminalProfileResolverService implements ITerminalPro private readonly _configurationResolverService: IConfigurationResolverService, private readonly _historyService: IHistoryService, private readonly _logService: ILogService, - private readonly _terminalService: ITerminalService, + private readonly _terminalProfileService: ITerminalProfileService, private readonly _workspaceContextService: IWorkspaceContextService, - private readonly _remoteAgentService: IRemoteAgentService + private readonly _remoteAgentService: IRemoteAgentService, + private readonly _storageService: IStorageService, + private readonly _notificationService: INotificationService ) { if (this._remoteAgentService.getConnection()) { this._remoteAgentService.getEnvironment().then(env => this._primaryBackendOs = env?.os || OS); @@ -61,7 +78,8 @@ export abstract class BaseTerminalProfileResolverService implements ITerminalPro this._refreshDefaultProfileName(); } }); - this._terminalService.onDidChangeAvailableProfiles(() => this._refreshDefaultProfileName()); + this._terminalProfileService.onDidChangeAvailableProfiles(() => this._refreshDefaultProfileName()); + this.showProfileMigrationNotification(); } @debounce(200) @@ -133,7 +151,6 @@ export abstract class BaseTerminalProfileResolverService implements ITerminalPro } } - async getDefaultShell(options: IShellLaunchConfigResolveOptions): Promise { return (await this.getDefaultProfile(options)).path; } @@ -160,35 +177,18 @@ export abstract class BaseTerminalProfileResolverService implements ITerminalPro if (ThemeIcon.isThemeIcon(icon)) { return icon; } - if (URI.isUri(icon) || this._isUriComponents(icon)) { + if (URI.isUri(icon) || isUriComponents(icon)) { return URI.revive(icon); } if (typeof icon === 'object' && icon && 'light' in icon && 'dark' in icon) { const castedIcon = (icon as { light: unknown, dark: unknown }); - if ((URI.isUri(castedIcon.light) || this._isUriComponents(castedIcon.light)) && (URI.isUri(castedIcon.dark) || this._isUriComponents(castedIcon.dark))) { + if ((URI.isUri(castedIcon.light) || isUriComponents(castedIcon.light)) && (URI.isUri(castedIcon.dark) || isUriComponents(castedIcon.dark))) { return { light: URI.revive(castedIcon.light), dark: URI.revive(castedIcon.dark) }; } } return undefined; } - private _isUriComponents(thing: unknown): thing is UriComponents { - if (!thing) { - return false; - } - return typeof (thing).path === 'string' && - typeof (thing).scheme === 'string'; - } - - private _setIconForAutomation(options: IShellLaunchConfigResolveOptions, profile: ITerminalProfile): ITerminalProfile { - if (options.allowAutomationShell) { - const profileClone = deepClone(profile); - profileClone.icon = Codicon.tools; - return profileClone; - } - return profile; - } - private async _getUnresolvedDefaultProfile(options: IShellLaunchConfigResolveOptions): Promise { // If automation shell is allowed, prefer that if (options.allowAutomationShell) { @@ -207,7 +207,7 @@ export abstract class BaseTerminalProfileResolverService implements ITerminalPro // Return the real default profile if it exists and is valid, wait for profiles to be ready // if the window just opened - await this._terminalService.profilesReady; + await this._terminalProfileService.profilesReady; const defaultProfile = this._getUnresolvedRealDefaultProfile(options.os); if (defaultProfile) { return this._setIconForAutomation(options, defaultProfile); @@ -218,10 +218,19 @@ export abstract class BaseTerminalProfileResolverService implements ITerminalPro return this._setIconForAutomation(options, await this._getUnresolvedFallbackDefaultProfile(options)); } + private _setIconForAutomation(options: IShellLaunchConfigResolveOptions, profile: ITerminalProfile): ITerminalProfile { + if (options.allowAutomationShell) { + const profileClone = deepClone(profile); + profileClone.icon = Codicon.tools; + return profileClone; + } + return profile; + } + private _getUnresolvedRealDefaultProfile(os: OperatingSystem): ITerminalProfile | undefined { const defaultProfileName = this._configurationService.getValue(`${TerminalSettingPrefix.DefaultProfile}${this._getOsKey(os)}`); if (defaultProfileName && typeof defaultProfileName === 'string') { - return this._terminalService.availableProfiles.find(e => e.profileName === defaultProfileName); + return this._terminalProfileService.availableProfiles.find(e => e.profileName === defaultProfileName); } return undefined; } @@ -270,7 +279,7 @@ export abstract class BaseTerminalProfileResolverService implements ITerminalPro const executable = await this._context.getDefaultSystemShell(options.remoteAuthority, options.os); // Try select an existing profile to fallback to, based on the default system shell - let existingProfile = this._terminalService.availableProfiles.find(e => path.parse(e.path).name === path.parse(executable).name); + let existingProfile = this._terminalProfileService.availableProfiles.find(e => path.parse(e.path).name === path.parse(executable).name); if (existingProfile) { if (options.allowAutomationShell) { existingProfile = deepClone(existingProfile); @@ -417,7 +426,7 @@ export abstract class BaseTerminalProfileResolverService implements ITerminalPro } async createProfileFromShellAndShellArgs(shell?: unknown, shellArgs?: unknown): Promise { - const detectedProfile = this._terminalService.availableProfiles?.find(p => { + const detectedProfile = this._terminalProfileService.availableProfiles?.find(p => { if (p.path !== shell) { return false; } @@ -439,7 +448,7 @@ export abstract class BaseTerminalProfileResolverService implements ITerminalPro args, isDefault: true }; - if (detectedProfile && detectedProfile.profileName === createdProfile.profileName && detectedProfile.path === createdProfile.path && this._argsMatch(detectedProfile.args, createdProfile.args)) { + if (detectedProfile && detectedProfile.profileName === createdProfile.profileName && detectedProfile.path === createdProfile.path && terminalProfileArgsMatch(detectedProfile.args, createdProfile.args)) { return detectedProfile.profileName; } return createdProfile; @@ -455,23 +464,46 @@ export abstract class BaseTerminalProfileResolverService implements ITerminalPro return false; } - private _argsMatch(args1: string | string[] | undefined, args2: string | string[] | undefined): boolean { - if (!args1 && !args2) { - return true; - } else if (typeof args1 === 'string' && typeof args2 === 'string') { - return args1 === args2; - } else if (Array.isArray(args1) && Array.isArray(args2)) { - if (args1.length !== args2.length) { - return false; - } - for (let i = 0; i < args1.length; i++) { - if (args1[i] !== args2[i]) { - return false; + async showProfileMigrationNotification(): Promise { + const shouldMigrateToProfile = (!!this._configurationService.getValue(TerminalSettingPrefix.Shell + this._primaryBackendOs) || + !!this._configurationService.inspect(TerminalSettingPrefix.ShellArgs + this._primaryBackendOs).userValue) && + !!this._configurationService.getValue(TerminalSettingPrefix.DefaultProfile + this._primaryBackendOs); + if (shouldMigrateToProfile && this._storageService.getBoolean(SHOULD_PROMPT_FOR_PROFILE_MIGRATION_KEY, StorageScope.WORKSPACE, true) && !migrationMessageShown) { + this._notificationService.prompt( + Severity.Info, + localize('terminalProfileMigration', "The terminal is using deprecated shell/shellArgs settings, do you want to migrate it to a profile?"), + [ + { + label: localize('migrateToProfile', "Migrate"), + run: async () => { + const shell = this._configurationService.getValue(TerminalSettingPrefix.Shell + this._primaryBackendOs); + const shellArgs = this._configurationService.getValue(TerminalSettingPrefix.ShellArgs + this._primaryBackendOs); + const profile = await this.createProfileFromShellAndShellArgs(shell, shellArgs); + if (typeof profile === 'string') { + await this._configurationService.updateValue(TerminalSettingPrefix.DefaultProfile + this._primaryBackendOs, profile); + this._logService.trace(`migrated from shell/shellArgs, using existing profile ${profile}`); + } else { + const profiles = { ...this._configurationService.inspect>(TerminalSettingPrefix.Profiles + this._primaryBackendOs).userValue } || {}; + const profileConfig: ITerminalProfileObject = { path: profile.path }; + if (profile.args) { + profileConfig.args = profile.args; + } + profiles[profile.profileName] = profileConfig; + await this._configurationService.updateValue(TerminalSettingPrefix.Profiles + this._primaryBackendOs, profiles); + await this._configurationService.updateValue(TerminalSettingPrefix.DefaultProfile + this._primaryBackendOs, profile.profileName); + this._logService.trace(`migrated from shell/shellArgs, ${shell} ${shellArgs} to profile ${JSON.stringify(profile)}`); + } + await this._configurationService.updateValue(TerminalSettingPrefix.Shell + this._primaryBackendOs, undefined); + await this._configurationService.updateValue(TerminalSettingPrefix.ShellArgs + this._primaryBackendOs, undefined); + } + } as IPromptChoice, + ], + { + neverShowAgain: { id: SHOULD_PROMPT_FOR_PROFILE_MIGRATION_KEY, scope: NeverShowAgainScope.WORKSPACE } } - } - return true; + ); + migrationMessageShown = true; } - return false; } } @@ -483,9 +515,11 @@ export class BrowserTerminalProfileResolverService extends BaseTerminalProfileRe @IHistoryService historyService: IHistoryService, @ILogService logService: ILogService, @IRemoteTerminalService remoteTerminalService: IRemoteTerminalService, - @ITerminalService terminalService: ITerminalService, + @ITerminalProfileService terminalProfileService: ITerminalProfileService, @IWorkspaceContextService workspaceContextService: IWorkspaceContextService, - @IRemoteAgentService remoteAgentService: IRemoteAgentService + @IRemoteAgentService remoteAgentService: IRemoteAgentService, + @IStorageService storageService: IStorageService, + @INotificationService notificationService: INotificationService ) { super( { @@ -507,9 +541,11 @@ export class BrowserTerminalProfileResolverService extends BaseTerminalProfileRe configurationResolverService, historyService, logService, - terminalService, + terminalProfileService, workspaceContextService, - remoteAgentService + remoteAgentService, + storageService, + notificationService ); } } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProfileService.ts b/src/vs/workbench/contrib/terminal/browser/terminalProfileService.ts new file mode 100644 index 00000000000..d61b881ec22 --- /dev/null +++ b/src/vs/workbench/contrib/terminal/browser/terminalProfileService.ts @@ -0,0 +1,225 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { equals } from 'vs/base/common/arrays'; +import { AutoOpenBarrier } from 'vs/base/common/async'; +import { throttle } from 'vs/base/common/decorators'; +import { Emitter, Event } from 'vs/base/common/event'; +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 { optional } from 'vs/platform/instantiation/common/instantiation'; +import { ITerminalProfile, IExtensionTerminalProfile, TerminalSettingPrefix, TerminalSettingId, ICreateContributedTerminalProfileOptions, 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 { IRemoteTerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { refreshTerminalActions } from 'vs/workbench/contrib/terminal/browser/terminalActions'; +import { ILocalTerminalService, IOffProcessTerminalService, 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'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; + +/* +* Links TerminalService with TerminalProfileResolverService +* and keeps the available terminal profiles updated +*/ +export class TerminalProfileService implements ITerminalProfileService { + private _ifNoProfilesTryAgain: boolean = true; + private _webExtensionContributedProfileContextKey: IContextKey; + private _profilesReadyBarrier: AutoOpenBarrier; + private _availableProfiles: ITerminalProfile[] | undefined; + private _contributedProfiles: IExtensionTerminalProfile[] = []; + private _defaultProfileName?: string; + private readonly _profileProviders: Map> = new Map(); + private readonly _primaryOffProcessTerminalService?: IOffProcessTerminalService; + + private readonly _onDidChangeAvailableProfiles = new Emitter(); + get onDidChangeAvailableProfiles(): Event { return this._onDidChangeAvailableProfiles.event; } + + get profilesReady(): Promise { return this._profilesReadyBarrier.wait().then(() => { }); } + get availableProfiles(): ITerminalProfile[] { + this.refreshAvailableProfiles(); + return this._availableProfiles || []; + } + get contributedProfiles(): IExtensionTerminalProfile[] { + return this._contributedProfiles || []; + } + constructor( + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @ITerminalContributionService private readonly _terminalContributionService: ITerminalContributionService, + @IExtensionService private readonly _extensionService: IExtensionService, + @IRemoteAgentService private _remoteAgentService: IRemoteAgentService, + @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, + @IRemoteTerminalService private readonly _remoteTerminalService: IRemoteTerminalService, + @optional(ILocalTerminalService) private readonly _localTerminalService: ILocalTerminalService + ) { + // in web, we don't want to show the dropdown unless there's a web extension + // that contributes a profile + this._extensionService.onDidChangeExtensions(() => this.refreshAvailableProfiles()); + + this._configurationService.onDidChangeConfiguration(async e => { + const platformKey = await this._getPlatformKey(); + if (e.affectsConfiguration(TerminalSettingPrefix.DefaultProfile + platformKey) || + e.affectsConfiguration(TerminalSettingPrefix.Profiles + platformKey) || + e.affectsConfiguration(TerminalSettingId.UseWslProfiles)) { + this.refreshAvailableProfiles(); + } + }); + this._webExtensionContributedProfileContextKey = TerminalContextKeys.webExtensionContributedProfile.bindTo(this._contextKeyService); + + this._primaryOffProcessTerminalService = !!this._environmentService.remoteAuthority ? this._remoteTerminalService : (this._localTerminalService || this._remoteTerminalService); + // Wait up to 5 seconds for profiles to be ready so it's assured that we know the actual + // default terminal before launching the first terminal. This isn't expected to ever take + // this long. + this._profilesReadyBarrier = new AutoOpenBarrier(5000); + this.refreshAvailableProfiles(); + } + + _serviceBrand: undefined; + + getDefaultProfileName(): string { + if (!this._defaultProfileName) { + throw new Error('no default profile'); + } + return this._defaultProfileName; + } + + @throttle(2000) + refreshAvailableProfiles(): void { + this._refreshAvailableProfilesNow(); + } + + protected async _refreshAvailableProfilesNow(): Promise { + const profiles = await this._detectProfiles(); + if (profiles.length === 0 && this._ifNoProfilesTryAgain) { + // available profiles get updated when a terminal is created + // or relevant config changes. + // if there are no profiles, we want to refresh them again + // since terminal creation can't happen in this case and users + // might not think to try changing the config + this._ifNoProfilesTryAgain = false; + await this._refreshAvailableProfilesNow(); + } + const profilesChanged = !(equals(profiles, this._availableProfiles, profilesEqual)); + const contributedProfilesChanged = await this._updateContributedProfiles(); + if (profilesChanged || contributedProfilesChanged) { + this._availableProfiles = profiles; + this._onDidChangeAvailableProfiles.fire(this._availableProfiles); + this._profilesReadyBarrier.open(); + this._updateWebContextKey(); + await this._refreshPlatformConfig(profiles); + } + } + + private async _updateContributedProfiles(): Promise { + 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)) { + if (value === null) { + excludedContributedProfiles.push(profileName); + } + } + const filteredContributedProfiles = Array.from(this._terminalContributionService.terminalProfiles.filter(p => !excludedContributedProfiles.includes(p.title))); + const contributedProfilesChanged = !equals(filteredContributedProfiles, this._contributedProfiles, contributedProfilesEqual); + this._contributedProfiles = filteredContributedProfiles; + return contributedProfilesChanged; + } + + getContributedProfileProvider(extensionIdentifier: string, id: string): ITerminalProfileProvider | undefined { + const extMap = this._profileProviders.get(extensionIdentifier); + return extMap?.get(id); + } + + private async _detectProfiles(includeDetectedProfiles?: boolean): Promise { + if (!this._primaryOffProcessTerminalService) { + return this._availableProfiles || []; + } + const platform = await this._getPlatformKey(); + this._defaultProfileName = this._configurationService.getValue(`${TerminalSettingPrefix.DefaultProfile}${platform}`); + return this._primaryOffProcessTerminalService?.getProfiles(this._configurationService.getValue(`${TerminalSettingPrefix.Profiles}${platform}`), this._defaultProfileName, includeDetectedProfiles); + } + + private _updateWebContextKey(): void { + this._webExtensionContributedProfileContextKey.set(isWeb && this._contributedProfiles.length > 0); + } + + private async _refreshPlatformConfig(profiles: ITerminalProfile[]) { + const env = await this._remoteAgentService.getEnvironment(); + registerTerminalDefaultProfileConfiguration({ os: env?.os || OS, profiles }, this._contributedProfiles); + refreshTerminalActions(profiles); + } + + private async _getPlatformKey(): Promise { + const env = await this._remoteAgentService.getEnvironment(); + if (env) { + return env.os === OperatingSystem.Windows ? 'windows' : (env.os === OperatingSystem.Macintosh ? 'osx' : 'linux'); + } + return isWindows ? 'windows' : (isMacintosh ? 'osx' : 'linux'); + } + + registerTerminalProfileProvider(extensionIdentifier: string, id: string, profileProvider: ITerminalProfileProvider): IDisposable { + let extMap = this._profileProviders.get(extensionIdentifier); + if (!extMap) { + extMap = new Map(); + this._profileProviders.set(extensionIdentifier, extMap); + } + extMap.set(id, profileProvider); + return toDisposable(() => this._profileProviders.delete(id)); + } + + async registerContributedProfile(extensionIdentifier: string, id: string, title: string, options: ICreateContributedTerminalProfileOptions): Promise { + 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 + }; + + (profilesConfig as { [key: string]: ITerminalProfileObject })[title] = newProfile; + } + await this._configurationService.updateValue(`${TerminalSettingPrefix.Profiles}${platformKey}`, profilesConfig, ConfigurationTarget.USER); + return; + } + + async getContributedDefaultProfile(shellLaunchConfig: IShellLaunchConfig): Promise { + // 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 defaultProfileName = this._configurationService.getValue(`${TerminalSettingPrefix.DefaultProfile}${key}`); + const contributedDefaultProfile = this.contributedProfiles.find(p => p.title === defaultProfileName); + return contributedDefaultProfile; + } + return undefined; + } +} + +function profilesEqual(one: ITerminalProfile, other: ITerminalProfile) { + return one.profileName === other.profileName && + terminalProfileArgsMatch(one.args, other.args) && + one.color === other.color && + terminalIconsEqual(one.icon, other.icon) && + one.isAutoDetected === other.isAutoDetected && + one.isDefault === other.isDefault && + one.overrideName === other.overrideName && + one.path === other.path; +} + +function contributedProfilesEqual(one: IExtensionTerminalProfile, other: IExtensionTerminalProfile) { + return one.extensionIdentifier === other.extensionIdentifier && + one.color === other.color && + one.icon === other.icon && + one.id === other.id && + one.title === other.title; +} diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index 62e954abeea..1af150af022 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -4,14 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; -import { AutoOpenBarrier, timeout } from 'vs/base/common/async'; +import { timeout } from 'vs/base/common/async'; import { Codicon, iconRegistry } from 'vs/base/common/codicons'; -import { debounce, throttle } from 'vs/base/common/decorators'; +import { debounce } from 'vs/base/common/decorators'; import { Emitter, Event } from 'vs/base/common/event'; import { dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; -import { equals } from 'vs/base/common/objects'; -import { isMacintosh, isWeb, isWindows, OperatingSystem, OS } from 'vs/base/common/platform'; +import { isMacintosh, isWeb, isWindows, OperatingSystem } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { FindReplaceState } from 'vs/editor/contrib/find/findState'; import * as nls from 'vs/nls'; @@ -20,36 +19,33 @@ import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/c import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IInstantiationService, optional } from 'vs/platform/instantiation/common/instantiation'; import { ILabelService } from 'vs/platform/label/common/label'; -import { INotificationService } from 'vs/platform/notification/common/notification'; import { IKeyMods, IPickOptions, IQuickInputButton, IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { ICreateContributedTerminalProfileOptions, IExtensionTerminalProfile, IShellLaunchConfig, ITerminalLaunchError, ITerminalProfile, ITerminalProfileObject, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, TerminalLocation, TerminalLocationString, TerminalSettingId, TerminalSettingPrefix } from 'vs/platform/terminal/common/terminal'; -import { registerTerminalDefaultProfileConfiguration } from 'vs/platform/terminal/common/terminalPlatformConfiguration'; +import { ICreateContributedTerminalProfileOptions, IExtensionTerminalProfile, IShellLaunchConfig, ITerminalLaunchError, ITerminalProfile, ITerminalProfileObject, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, TerminalLocation, TerminalLocationString, TerminalSettingPrefix } from 'vs/platform/terminal/common/terminal'; import { iconForeground } from 'vs/platform/theme/common/colorRegistry'; import { IconDefinition } from 'vs/platform/theme/common/iconRegistry'; import { ColorScheme } from 'vs/platform/theme/common/theme'; import { IThemeService, Themable, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { VirtualWorkspaceContext } from 'vs/workbench/browser/contextkeys'; import { IEditableData, IViewsService } from 'vs/workbench/common/views'; -import { ICreateTerminalOptions, IRemoteTerminalService, IRequestAddInstanceToGroupEvent, ITerminalEditorService, ITerminalExternalLinkProvider, ITerminalFindHost, ITerminalGroup, ITerminalGroupService, ITerminalInstance, ITerminalInstanceHost, ITerminalInstanceService, ITerminalLocationOptions, ITerminalProfileProvider, ITerminalService, ITerminalServiceNativeDelegate, TerminalConnectionState, TerminalEditorLocation } from 'vs/workbench/contrib/terminal/browser/terminal'; -import { refreshTerminalActions } from 'vs/workbench/contrib/terminal/browser/terminalActions'; +import { ICreateTerminalOptions, IRemoteTerminalService, IRequestAddInstanceToGroupEvent, ITerminalEditorService, ITerminalExternalLinkProvider, ITerminalFindHost, ITerminalGroup, ITerminalGroupService, ITerminalInstance, ITerminalInstanceHost, ITerminalInstanceService, ITerminalLocationOptions, ITerminalService, ITerminalServiceNativeDelegate, TerminalConnectionState, TerminalEditorLocation } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper'; import { TerminalEditor } from 'vs/workbench/contrib/terminal/browser/terminalEditor'; import { getColorClass, getColorStyleContent, getColorStyleElement, getUriClasses } from 'vs/workbench/contrib/terminal/browser/terminalIcon'; import { configureTerminalProfileIcon } from 'vs/workbench/contrib/terminal/browser/terminalIcons'; import { getInstanceFromResource, getTerminalUri, parseTerminalUri } from 'vs/workbench/contrib/terminal/browser/terminalUri'; import { TerminalViewPane } from 'vs/workbench/contrib/terminal/browser/terminalView'; -import { ILocalTerminalService, IOffProcessTerminalService, IRemoteTerminalAttachTarget, IStartExtensionTerminalRequest, ITerminalConfigHelper, ITerminalProcessExtHostProxy, TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal'; +import { ILocalTerminalService, IOffProcessTerminalService, IRemoteTerminalAttachTarget, IStartExtensionTerminalRequest, ITerminalConfigHelper, ITerminalProcessExtHostProxy, ITerminalProfileService, TERMINAL_VIEW_ID } 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 { formatMessageForTerminal, terminalStrings } from 'vs/workbench/contrib/terminal/common/terminalStrings'; import { IEditorResolverService, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { ILifecycleService, ShutdownReason, WillShutdownEvent } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; export class TerminalService implements ITerminalService { declare _serviceBrand: undefined; @@ -57,22 +53,16 @@ export class TerminalService implements ITerminalService { private _hostActiveTerminals: Map = new Map(); private _isShuttingDown: boolean; - private _ifNoProfilesTryAgain: boolean = true; private _backgroundedTerminalInstances: ITerminalInstance[] = []; private _backgroundedTerminalDisposables: Map = new Map(); private _findState: FindReplaceState; - private readonly _profileProviders: Map> = new Map(); private _linkProviders: Set = new Set(); private _linkProviderDisposables: Map = new Map(); private _processSupportContextKey: IContextKey; - private _webExtensionContributedProfileContextKey: IContextKey; + private _terminalHasBeenCreated: IContextKey; private readonly _localTerminalService?: ILocalTerminalService; private readonly _primaryOffProcessTerminalService?: IOffProcessTerminalService; - private _defaultProfileName?: string; - private _profilesReadyBarrier: AutoOpenBarrier; - private _availableProfiles: ITerminalProfile[] | undefined; - private _contributedProfiles: IExtensionTerminalProfile[] = []; private _configHelper: TerminalConfigHelper; private _remoteTerminalsInitPromise: Promise | undefined; private _localTerminalsInitPromise: Promise | undefined; @@ -84,14 +74,7 @@ export class TerminalService implements ITerminalService { get isProcessSupportRegistered(): boolean { return !!this._processSupportContextKey.get(); } get connectionState(): TerminalConnectionState { return this._connectionState; } - get profilesReady(): Promise { return this._profilesReadyBarrier.wait().then(() => { }); } - get availableProfiles(): ITerminalProfile[] { - this._refreshAvailableProfiles(); - return this._availableProfiles || []; - } - get contributedProfiles(): IExtensionTerminalProfile[] { - return this._contributedProfiles || []; - } + get configHelper(): ITerminalConfigHelper { return this._configHelper; } get instances(): ITerminalInstance[] { return this._terminalGroupService.instances.concat(this._terminalEditorService.instances); @@ -153,8 +136,6 @@ export class TerminalService implements ITerminalService { get onDidRegisterProcessSupport(): Event { return this._onDidRegisterProcessSupport.event; } private readonly _onDidChangeConnectionState = new Emitter(); get onDidChangeConnectionState(): Event { return this._onDidChangeConnectionState.event; } - private readonly _onDidChangeAvailableProfiles = new Emitter(); - get onDidChangeAvailableProfiles(): Event { return this._onDidChangeAvailableProfiles.event; } constructor( @IContextKeyService private _contextKeyService: IContextKeyService, @@ -169,15 +150,15 @@ export class TerminalService implements ITerminalService { @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, @IRemoteTerminalService private readonly _remoteTerminalService: IRemoteTerminalService, @ITelemetryService private readonly _telemetryService: ITelemetryService, - @ITerminalContributionService private readonly _terminalContributionService: ITerminalContributionService, @ITerminalEditorService private readonly _terminalEditorService: ITerminalEditorService, @ITerminalGroupService private readonly _terminalGroupService: ITerminalGroupService, @ITerminalInstanceService private readonly _terminalInstanceService: ITerminalInstanceService, @IEditorResolverService editorResolverService: IEditorResolverService, @IEditorGroupsService private readonly _editorGroupsService: IEditorGroupsService, + @IThemeService private readonly _themeService: IThemeService, + @ITerminalProfileService private readonly _terminalProfileService: ITerminalProfileService, @IExtensionService private readonly _extensionService: IExtensionService, @INotificationService private readonly _notificationService: INotificationService, - @IThemeService private readonly _themeService: IThemeService, @optional(ILocalTerminalService) localTerminalService: ILocalTerminalService ) { this._localTerminalService = localTerminalService; @@ -216,6 +197,10 @@ export class TerminalService implements ITerminalService { } }; }); + // the below avoids having to poll routinely. + // we update detected profiles when an instance is created so that, + // for example, we detect if you've installed a pwsh + this.onDidCreateInstance(() => this._terminalProfileService.refreshAvailableProfiles()); this._forwardInstanceHostEvents(this._terminalGroupService); this._forwardInstanceHostEvents(this._terminalEditorService); @@ -225,15 +210,6 @@ export class TerminalService implements ITerminalService { this._onDidCreateInstance.fire(instance); }); - // the below avoids having to poll routinely. - // we update detected profiles when an instance is created so that, - // for example, we detect if you've installed a pwsh - this.onDidCreateInstance(() => this._refreshAvailableProfiles()); - - // in web, we don't want to show the dropdown unless there's a web extension - // that contributes a profile - this._extensionService.onDidChangeExtensions(() => this._refreshAvailableProfiles()); - this.onDidReceiveInstanceLinks(instance => this._setInstanceLinkProviders(instance)); // Hide the panel if there are no more instances, provided that VS Code is not shutting @@ -248,21 +224,11 @@ export class TerminalService implements ITerminalService { this._handleInstanceContextKeys(); this._processSupportContextKey = TerminalContextKeys.processSupported.bindTo(this._contextKeyService); this._processSupportContextKey.set(!isWeb || this._remoteAgentService.getConnection() !== null); - this._webExtensionContributedProfileContextKey = TerminalContextKeys.webExtensionContributedProfile.bindTo(this._contextKeyService); this._terminalHasBeenCreated = TerminalContextKeys.terminalHasBeenCreated.bindTo(this._contextKeyService); lifecycleService.onBeforeShutdown(async e => e.veto(this._onBeforeShutdown(e.reason), 'veto.terminal')); lifecycleService.onWillShutdown(e => this._onWillShutdown(e)); - this._configurationService.onDidChangeConfiguration(async e => { - const platformKey = await this._getPlatformKey(); - if (e.affectsConfiguration(TerminalSettingPrefix.DefaultProfile + platformKey) || - e.affectsConfiguration(TerminalSettingPrefix.Profiles + platformKey) || - e.affectsConfiguration(TerminalSettingId.UseWslProfiles)) { - await this._refreshAvailableProfiles(); - } - }); - // Register a resource formatter for terminal URIs labelService.registerFormatter({ scheme: Schemas.vscodeTerminal, @@ -307,12 +273,6 @@ export class TerminalService implements ITerminalService { } }); - // Wait up to 5 seconds for profiles to be ready so it's assured that we know the actual - // default terminal before launching the first terminal. This isn't expected to ever take - // this long. - this._profilesReadyBarrier = new AutoOpenBarrier(5000); - this._refreshAvailableProfiles(); - // Create async as the class depends on `this` timeout(0).then(() => this._instantiationService.createInstance(TerminalEditorStyle, document.head)); } @@ -362,6 +322,23 @@ export class TerminalService implements ITerminalService { } } + async createContributedTerminalProfile(extensionIdentifier: string, id: string, options: ICreateContributedTerminalProfileOptions): Promise { + await this._extensionService.activateByEvent(`onTerminalProfile:${id}`); + + const profileProvider = this._terminalProfileService.getContributedProfileProvider(extensionIdentifier, id); + if (!profileProvider) { + this._notificationService.error(`No terminal profile provider registered for id "${id}"`); + return; + } + try { + await profileProvider.createContributedTerminalProfile(options); + this._terminalGroupService.setActiveInstanceByIndex(this._terminalGroupService.instances.length - 1); + await this._terminalGroupService.activeInstance?.focusWhenReady(); + } catch (e) { + this._notificationService.error(e.message); + } + } + async safeDisposeTerminal(instance: ITerminalInstance): Promise { // Confirm on kill in the editor is handled by the editor input if (instance.target !== TerminalLocation.Editor && @@ -503,70 +480,6 @@ export class TerminalService implements ITerminalService { }); } - @throttle(2000) - private _refreshAvailableProfiles(): void { - this._refreshAvailableProfilesNow(); - } - - private async _refreshAvailableProfilesNow(): Promise { - const platformKey = await this._getPlatformKey(); - const profiles = await this._detectProfiles(); - const profilesChanged = !equals(profiles, this._availableProfiles); - const excludedContributedProfiles: string[] = []; - const configProfiles: { [key: string]: any } = this._configurationService.getValue(TerminalSettingPrefix.Profiles + platformKey); - for (const [profileName, value] of Object.entries(configProfiles)) { - if (value === null) { - excludedContributedProfiles.push(profileName); - } - } - const filteredContributedProfiles = Array.from(this._terminalContributionService.terminalProfiles.filter(p => !excludedContributedProfiles.includes(p.title))); - const contributedProfilesChanged = !equals(filteredContributedProfiles, this._contributedProfiles); - - if (profiles.length === 0 && this._ifNoProfilesTryAgain) { - // available profiles get updated when a terminal is created - // or relevant config changes. - // if there are no profiles, we want to refresh them again - // since terminal creation can't happen in this case and users - // might not think to try changing the config - this._ifNoProfilesTryAgain = false; - await this._refreshAvailableProfilesNow(); - } - if (profilesChanged || contributedProfilesChanged) { - this._availableProfiles = profiles; - this._contributedProfiles = filteredContributedProfiles; - this._onDidChangeAvailableProfiles.fire(this._availableProfiles); - this._profilesReadyBarrier.open(); - this._updateWebContextKey(); - await this._refreshPlatformConfig(profiles); - } - } - - private _updateWebContextKey(): void { - this._webExtensionContributedProfileContextKey.set(isWeb && this._contributedProfiles.length > 0); - } - - private async _refreshPlatformConfig(profiles: ITerminalProfile[]) { - const env = await this._remoteAgentService.getEnvironment(); - registerTerminalDefaultProfileConfiguration({ os: env?.os || OS, profiles }, this._contributedProfiles); - refreshTerminalActions(profiles); - } - - private async _detectProfiles(includeDetectedProfiles?: boolean): Promise { - if (!this._primaryOffProcessTerminalService) { - return this._availableProfiles || []; - } - const platform = await this._getPlatformKey(); - this._defaultProfileName = this._configurationService.getValue(`${TerminalSettingPrefix.DefaultProfile}${platform}`); - return this._primaryOffProcessTerminalService?.getProfiles(this._configurationService.getValue(`${TerminalSettingPrefix.Profiles}${platform}`), this._defaultProfileName, includeDetectedProfiles); - } - - getDefaultProfileName(): string { - if (!this._defaultProfileName) { - throw new Error('no default profile'); - } - return this._defaultProfileName; - } - private _onBeforeShutdown(reason: ShutdownReason): boolean | Promise { // Never veto on web as this would block all windows from being closed. This disables // process revive as we can't handle it on shutdown. @@ -885,16 +798,6 @@ export class TerminalService implements ITerminalService { }; } - registerTerminalProfileProvider(extensionIdentifierenfifier: string, id: string, profileProvider: ITerminalProfileProvider): IDisposable { - let extMap = this._profileProviders.get(extensionIdentifierenfifier); - if (!extMap) { - extMap = new Map(); - this._profileProviders.set(extensionIdentifierenfifier, extMap); - } - extMap.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); @@ -903,7 +806,6 @@ export class TerminalService implements ITerminalService { } } - // TODO: Remove this, it should live in group/editor servioce private _getIndexFromId(terminalId: number): number { let terminalIndex = -1; @@ -943,7 +845,7 @@ export class TerminalService implements ITerminalService { async showProfileQuickPick(type: 'setDefault' | 'createInstance', cwd?: string | URI): Promise { let keyMods: IKeyMods | undefined; - const profiles = await this._detectProfiles(true); + const profiles = this._terminalProfileService.availableProfiles; const platformKey = await this._getPlatformKey(); const profilesKey = `${TerminalSettingPrefix.Profiles}${platformKey}`; const defaultProfileKey = `${TerminalSettingPrefix.DefaultProfile}${platformKey}`; @@ -995,7 +897,7 @@ export class TerminalService implements ITerminalService { quickPickItems.push({ type: 'separator', label: nls.localize('ICreateContributedTerminalProfileOptions', "contributed") }); const contributedProfiles: IProfileQuickPickItem[] = []; - for (const contributed of this.contributedProfiles) { + 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); } @@ -1044,7 +946,7 @@ export class TerminalService implements ITerminalService { let instance; if ('id' in value.profile) { - await this._createContributedTerminalProfile(value.profile.extensionIdentifier, value.profile.id, { + await this.createContributedTerminalProfile(value.profile.extensionIdentifier, value.profile.id, { icon: value.profile.icon, color: value.profile.color, location: !!(keyMods?.alt && activeInstance) ? { splitActiveTerminal: true } : this.defaultLocation @@ -1071,7 +973,7 @@ export class TerminalService implements ITerminalService { // extension contributed profile await this._configurationService.updateValue(defaultProfileKey, value.profile.title, ConfigurationTarget.USER); - this._registerContributedProfile(value.profile.extensionIdentifier, value.profile.id, value.profile.title, { + this._terminalProfileService.registerContributedProfile(value.profile.extensionIdentifier, value.profile.id, value.profile.title, { color: value.profile.color, icon: value.profile.icon }); @@ -1127,41 +1029,6 @@ export class TerminalService implements ITerminalService { return instance?.target === TerminalLocation.Editor ? this._terminalEditorService : this._terminalGroupService; } - private async _createContributedTerminalProfile(extensionIdentifier: string, id: string, options: ICreateContributedTerminalProfileOptions): Promise { - await this._extensionService.activateByEvent(`onTerminalProfile:${id}`); - const extMap = this._profileProviders.get(extensionIdentifier); - const profileProvider = extMap?.get(id); - if (!profileProvider) { - this._notificationService.error(`No terminal profile provider registered for id "${id}"`); - return; - } - try { - await profileProvider.createContributedTerminalProfile(options); - this._terminalGroupService.setActiveInstanceByIndex(this.instances.length - 1); - await this.activeInstance?.focusWhenReady(); - } catch (e) { - this._notificationService.error(e.message); - } - } - - private async _registerContributedProfile(extensionIdentifier: string, id: string, title: string, options: ICreateContributedTerminalProfileOptions): Promise { - 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 - }; - - (profilesConfig as { [key: string]: ITerminalProfileObject })[title] = newProfile; - } - await this._configurationService.updateValue(`${TerminalSettingPrefix.Profiles}${platformKey}`, profilesConfig, ConfigurationTarget.USER); - return; - } - private _createProfileQuickPickItem(profile: ITerminalProfile): IProfileQuickPickItem { const buttons: IQuickInputButton[] = [{ iconClass: ThemeIcon.asClassName(configureTerminalProfileIcon), @@ -1231,32 +1098,19 @@ export class TerminalService implements ITerminalService { return {}; } - private async _getContributedDefaultProfile(shellLaunchConfig: IShellLaunchConfig): Promise { - // 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 defaultProfileName = this._configurationService.getValue(`${TerminalSettingPrefix.DefaultProfile}${key}`); - const contributedDefaultProfile = this._terminalContributionService.terminalProfiles.find(p => p.title === defaultProfileName); - return contributedDefaultProfile; - } - return undefined; - } - - async createTerminal(options?: ICreateTerminalOptions): Promise { // Await the initialization of available profiles as long as this is not a pty terminal or a // local terminal in a remote workspace as profile won't be used in those cases and these // terminals need to be launched before remote connections are established. - if (!this._availableProfiles) { + if (!this._terminalProfileService.availableProfiles) { const isPtyTerminal = options?.config && 'customPtyImplementation' in options.config; const isLocalInRemoteTerminal = this._remoteAgentService.getConnection() && URI.isUri(options?.cwd) && options?.cwd.scheme === Schemas.vscodeFileResource; if (!isPtyTerminal && !isLocalInRemoteTerminal) { - await this._refreshAvailableProfilesNow(); + await this._terminalProfileService.refreshAvailableProfiles(); } } - const config = options?.config || this._availableProfiles?.find(p => p.profileName === this._defaultProfileName); + const config = options?.config || this._terminalProfileService.availableProfiles?.find(p => p.profileName === this._terminalProfileService.getDefaultProfileName()); const shellLaunchConfig = config && 'extensionIdentifier' in config ? {} : this._convertProfileToShellLaunchConfig(config || {}); // Get the contributed profile if it was provided @@ -1264,7 +1118,7 @@ export class TerminalService implements ITerminalService { // Get the default profile as a contributed profile if it exists if (!contributedProfile && (!options || !options.config)) { - contributedProfile = await this._getContributedDefaultProfile(shellLaunchConfig); + contributedProfile = await this._terminalProfileService.getContributedDefaultProfile(shellLaunchConfig); } // Launch the contributed profile @@ -1277,7 +1131,7 @@ export class TerminalService implements ITerminalService { } else { location = typeof options?.location === 'object' && 'viewColumn' in options.location ? options.location : resolvedLocation; } - await this._createContributedTerminalProfile(contributedProfile.extensionIdentifier, contributedProfile.id, { + await this.createContributedTerminalProfile(contributedProfile.extensionIdentifier, contributedProfile.id, { icon: contributedProfile.icon, color: contributedProfile.color, location @@ -1442,6 +1296,7 @@ class TerminalEditorStyle extends Themable { container: HTMLElement, @ITerminalService private readonly _terminalService: ITerminalService, @IThemeService private readonly _themeService: IThemeService, + @ITerminalProfileService private readonly _terminalProfileService: ITerminalProfileService ) { super(_themeService); this._registerListeners(); @@ -1455,7 +1310,7 @@ class TerminalEditorStyle extends Themable { this._register(this._terminalService.onDidChangeInstanceIcon(() => this.updateStyles())); this._register(this._terminalService.onDidChangeInstanceColor(() => this.updateStyles())); this._register(this._terminalService.onDidCreateInstance(() => this.updateStyles())); - this._register(this._terminalService.onDidChangeAvailableProfiles(() => this.updateStyles())); + this._register(this._terminalProfileService.onDidChangeAvailableProfiles(() => this.updateStyles())); } override updateStyles(): void { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalView.ts b/src/vs/workbench/contrib/terminal/browser/terminalView.ts index 8b0099365a4..75d335bae1b 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalView.ts @@ -22,7 +22,7 @@ import { IViewDescriptorService } from 'vs/workbench/common/views'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { PANEL_BACKGROUND, SIDE_BAR_BACKGROUND, EDITOR_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/common/theme'; import { IMenu, IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; -import { ITerminalProfileResolverService, TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal'; +import { ITerminalProfileResolverService, ITerminalProfileService, TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal'; import { TerminalSettingId, ITerminalProfile, TerminalLocation } from 'vs/platform/terminal/common/terminal'; import { ActionViewItem, SelectActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { attachSelectBoxStyler, attachStylerCallback } from 'vs/platform/theme/common/styler'; @@ -74,6 +74,7 @@ export class TerminalViewPane extends ViewPane { @IOpenerService openerService: IOpenerService, @IMenuService private readonly _menuService: IMenuService, @ICommandService private readonly _commandService: ICommandService, + @ITerminalProfileService private readonly _terminalProfileService: ITerminalProfileService, @ITerminalProfileResolverService private readonly _terminalProfileResolverService: ITerminalProfileResolverService ) { super(options, keybindingService, _contextMenuService, configurationService, _contextKeyService, viewDescriptorService, _instantiationService, openerService, themeService, telemetryService); @@ -98,7 +99,7 @@ export class TerminalViewPane extends ViewPane { }); this._dropdownMenu = this._register(this._menuService.createMenu(MenuId.TerminalNewDropdownContext, this._contextKeyService)); this._singleTabMenu = this._register(this._menuService.createMenu(MenuId.TerminalInlineTabContext, this._contextKeyService)); - this._register(this._terminalService.onDidChangeAvailableProfiles(profiles => this._updateTabActionBar(profiles))); + this._register(this._terminalProfileService.onDidChangeAvailableProfiles(profiles => this._updateTabActionBar(profiles))); } override renderBody(container: HTMLElement): void { @@ -202,9 +203,9 @@ export class TerminalViewPane extends ViewPane { this._tabButtons.dispose(); } - const actions = getTerminalActionBarArgs(TerminalLocation.Panel, this._terminalService.availableProfiles, this._getDefaultProfileName(), this._terminalService.contributedProfiles, this._instantiationService, this._terminalService, this._contextKeyService, this._commandService, this._dropdownMenu); + const actions = getTerminalActionBarArgs(TerminalLocation.Panel, this._terminalProfileService.availableProfiles, this._getDefaultProfileName(), this._terminalProfileService.contributedProfiles, this._instantiationService, this._terminalService, this._contextKeyService, this._commandService, this._dropdownMenu); this._tabButtons = new DropdownWithPrimaryActionViewItem(actions.primaryAction, actions.dropdownAction, actions.dropdownMenuActions, actions.className, this._contextMenuService, {}, this._keybindingService, this._notificationService, this._contextKeyService); - this._updateTabActionBar(this._terminalService.availableProfiles); + this._updateTabActionBar(this._terminalProfileService.availableProfiles); return this._tabButtons; } } @@ -214,7 +215,7 @@ export class TerminalViewPane extends ViewPane { private _getDefaultProfileName(): string { let defaultProfileName; try { - defaultProfileName = this._terminalService.getDefaultProfileName(); + defaultProfileName = this._terminalProfileService.getDefaultProfileName(); } catch (e) { defaultProfileName = this._terminalProfileResolverService.defaultProfileName; } @@ -226,7 +227,7 @@ export class TerminalViewPane extends ViewPane { } private _updateTabActionBar(profiles: ITerminalProfile[]): void { - const actions = getTerminalActionBarArgs(TerminalLocation.Panel, profiles, this._getDefaultProfileName(), this._terminalService.contributedProfiles, this._instantiationService, this._terminalService, this._contextKeyService, this._commandService, this._dropdownMenu); + const actions = getTerminalActionBarArgs(TerminalLocation.Panel, profiles, this._getDefaultProfileName(), this._terminalProfileService.contributedProfiles, this._instantiationService, this._terminalService, this._contextKeyService, this._commandService, this._dropdownMenu); this._tabButtons?.update(actions.dropdownAction, actions.dropdownMenuActions); } @@ -290,7 +291,8 @@ class SwitchTerminalActionViewItem extends SelectActionViewItem { @ITerminalService private readonly _terminalService: ITerminalService, @ITerminalGroupService private readonly _terminalGroupService: ITerminalGroupService, @IThemeService private readonly _themeService: IThemeService, - @IContextViewService contextViewService: IContextViewService + @IContextViewService contextViewService: IContextViewService, + @ITerminalProfileService terminalProfileService: ITerminalProfileService ) { super(null, action, getTerminalSelectOpenItems(_terminalService, _terminalGroupService), _terminalGroupService.activeGroupIndex, contextViewService, { ariaLabel: nls.localize('terminals', 'Open Terminals.'), optionsAsChildren: true }); this._register(_terminalService.onDidChangeInstances(() => this._updateItems(), this)); @@ -299,7 +301,7 @@ class SwitchTerminalActionViewItem extends SelectActionViewItem { this._register(_terminalService.onDidChangeInstanceTitle(() => this._updateItems(), this)); this._register(_terminalGroupService.onDidChangeGroups(() => this._updateItems(), this)); this._register(_terminalService.onDidChangeConnectionState(() => this._updateItems(), this)); - this._register(_terminalService.onDidChangeAvailableProfiles(() => this._updateItems(), this)); + this._register(terminalProfileService.onDidChangeAvailableProfiles(() => this._updateItems(), this)); this._register(_terminalService.onDidChangeInstancePrimaryStatus(() => this._updateItems(), this)); this._register(attachSelectBoxStyler(this.selectBox, this._themeService)); } diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index 7b4d65c6d23..7bf68c1f8ac 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -8,7 +8,7 @@ import { Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; import { IProcessEnvironment, OperatingSystem } from 'vs/base/common/platform'; import { IExtensionPointDescriptor } from 'vs/workbench/services/extensions/common/extensionsRegistry'; -import { IProcessDataEvent, IProcessReadyEvent, IShellLaunchConfig, ITerminalChildProcess, ITerminalLaunchError, ITerminalProfile, ITerminalProfileObject, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, TerminalIcon, TerminalLocationString, IProcessProperty, TitleEventSource, ProcessPropertyType, IFixedTerminalDimensions } from 'vs/platform/terminal/common/terminal'; +import { IProcessDataEvent, IProcessReadyEvent, IShellLaunchConfig, ITerminalChildProcess, ITerminalLaunchError, ITerminalProfile, ITerminalProfileObject, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, TerminalIcon, TerminalLocationString, IProcessProperty, TitleEventSource, ProcessPropertyType, IFixedTerminalDimensions, IExtensionTerminalProfile, ICreateContributedTerminalProfileOptions } from 'vs/platform/terminal/common/terminal'; import { IEnvironmentVariableInfo } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { URI } from 'vs/base/common/uri'; @@ -56,6 +56,25 @@ export interface ITerminalProfileResolverService { createProfileFromShellAndShellArgs(shell?: unknown, shellArgs?: unknown): Promise; } +export const ITerminalProfileService = createDecorator('terminalProfileService'); +export interface ITerminalProfileService { + readonly _serviceBrand: undefined; + readonly availableProfiles: ITerminalProfile[]; + readonly contributedProfiles: IExtensionTerminalProfile[]; + readonly profilesReady: Promise; + refreshAvailableProfiles(): void; + getDefaultProfileName(): string; + onDidChangeAvailableProfiles: Event; + getContributedDefaultProfile(shellLaunchConfig: IShellLaunchConfig): Promise; + registerContributedProfile(extensionIdentifier: string, id: string, title: string, options: ICreateContributedTerminalProfileOptions): Promise; + getContributedProfileProvider(extensionIdentifier: string, id: string): ITerminalProfileProvider | undefined; + registerTerminalProfileProvider(extensionIdentifier: string, id: string, profileProvider: ITerminalProfileProvider): IDisposable; +} + +export interface ITerminalProfileProvider { + createContributedTerminalProfile(options: ICreateContributedTerminalProfileOptions): Promise; +} + export interface IShellLaunchConfigResolveOptions { remoteAuthority: string | undefined; os: OperatingSystem; diff --git a/src/vs/workbench/contrib/terminal/electron-sandbox/terminalProfileResolverService.ts b/src/vs/workbench/contrib/terminal/electron-sandbox/terminalProfileResolverService.ts index efd4d3a99f9..5b06d449e3d 100644 --- a/src/vs/workbench/contrib/terminal/electron-sandbox/terminalProfileResolverService.ts +++ b/src/vs/workbench/contrib/terminal/electron-sandbox/terminalProfileResolverService.ts @@ -5,10 +5,12 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService } from 'vs/platform/log/common/log'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IStorageService } from 'vs/platform/storage/common/storage'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IRemoteTerminalService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { IRemoteTerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { BaseTerminalProfileResolverService } from 'vs/workbench/contrib/terminal/browser/terminalProfileResolverService'; -import { ILocalTerminalService } from 'vs/workbench/contrib/terminal/common/terminal'; +import { ILocalTerminalService, ITerminalProfileService } from 'vs/workbench/contrib/terminal/common/terminal'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; @@ -20,11 +22,13 @@ export class ElectronTerminalProfileResolverService extends BaseTerminalProfileR @IConfigurationService configurationService: IConfigurationService, @IHistoryService historyService: IHistoryService, @ILogService logService: ILogService, - @ITerminalService terminalService: ITerminalService, + @IWorkspaceContextService workspaceContextService: IWorkspaceContextService, @ILocalTerminalService localTerminalService: ILocalTerminalService, @IRemoteTerminalService remoteTerminalService: IRemoteTerminalService, - @IWorkspaceContextService workspaceContextService: IWorkspaceContextService, - @IRemoteAgentService remoteAgentService: IRemoteAgentService + @ITerminalProfileService terminalProfileService: ITerminalProfileService, + @IRemoteAgentService remoteAgentService: IRemoteAgentService, + @IStorageService storageService: IStorageService, + @INotificationService notificationService: INotificationService ) { super( { @@ -44,9 +48,11 @@ export class ElectronTerminalProfileResolverService extends BaseTerminalProfileR configurationResolverService, historyService, logService, - terminalService, + terminalProfileService, workspaceContextService, - remoteAgentService + remoteAgentService, + storageService, + notificationService ); } } diff --git a/src/vs/workbench/contrib/terminal/test/browser/terminalProfileService.test.ts b/src/vs/workbench/contrib/terminal/test/browser/terminalProfileService.test.ts new file mode 100644 index 00000000000..deeac9972e4 --- /dev/null +++ b/src/vs/workbench/contrib/terminal/test/browser/terminalProfileService.test.ts @@ -0,0 +1,154 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { ILocalTerminalService, IOffProcessTerminalService, ITerminalConfiguration } from 'vs/workbench/contrib/terminal/common/terminal'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { TestExtensionService } from 'vs/workbench/test/common/workbenchTestServices'; +import { TerminalProfileService } from 'vs/workbench/contrib/terminal/browser/terminalProfileService'; +import { ITerminalContributionService } from 'vs/workbench/contrib/terminal/common/terminalExtensionPoints'; +import { IExtensionTerminalProfile, ITerminalProfile } from 'vs/platform/terminal/common/terminal'; +import { IRemoteTerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { isLinux, isWindows, OperatingSystem } from 'vs/base/common/platform'; +import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { TestEnvironmentService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment'; +import { ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { Codicon } from 'vs/base/common/codicons'; +import { deepStrictEqual } from 'assert'; +class TestTerminalProfileService extends TerminalProfileService { + hasRefreshedProfiles: Promise | undefined; + + override refreshAvailableProfiles(): void { + this.hasRefreshedProfiles = this._refreshAvailableProfilesNow(); + } +} +class TestTerminalContributionService implements ITerminalContributionService { + _serviceBrand: undefined; + terminalProfiles: readonly IExtensionTerminalProfile[] = []; + setProfiles(profiles: IExtensionTerminalProfile[]): void { + this.terminalProfiles = profiles; + } +} + +class TestOffProcessTerminalService implements Partial { + private _profiles: ITerminalProfile[] = []; + async getProfiles(profiles: unknown, defaultProfile: unknown, includeDetectedProfiles?: boolean): Promise { + return this._profiles; + } + setProfiles(profiles: ITerminalProfile[]) { + this._profiles = profiles; + } +} + +class TestRemoteAgentService implements Partial { + private _os: OperatingSystem | undefined; + setEnvironment(os: OperatingSystem) { + this._os = os; + } + async getEnvironment(): Promise { + return { os: this._os } as IRemoteAgentEnvironment; + } +} + +const defaultTerminalConfig: Partial = { profiles: { windows: {}, linux: {}, osx: {} } }; + +suite('TerminalProfileService', () => { + let configurationService: TestConfigurationService; + let terminalProfileService: TestTerminalProfileService; + let remoteAgentService: TestRemoteAgentService; + + setup(async () => { + configurationService = new TestConfigurationService({ terminal: { integrated: defaultTerminalConfig } }); + remoteAgentService = new TestRemoteAgentService(); + + let instantiationService = new TestInstantiationService(); + let terminalContributionService = new TestTerminalContributionService(); + let extensionService = new TestExtensionService(); + let localTerminalService = new TestOffProcessTerminalService(); + let remoteTerminalService = new TestOffProcessTerminalService(); + let contextKeyService = new MockContextKeyService(); + + instantiationService.stub(IContextKeyService, contextKeyService); + instantiationService.stub(IExtensionService, extensionService); + instantiationService.stub(IConfigurationService, configurationService); + instantiationService.stub(IRemoteAgentService, remoteAgentService); + instantiationService.stub(ITerminalContributionService, terminalContributionService); + instantiationService.stub(ILocalTerminalService, localTerminalService); + instantiationService.stub(IRemoteTerminalService, remoteTerminalService); + instantiationService.stub(IWorkbenchEnvironmentService, TestEnvironmentService); + terminalProfileService = instantiationService.createInstance(TestTerminalProfileService); + localTerminalService.setProfiles([{ + profileName: 'PowerShell', + path: 'C:\\Powershell.exe', + isDefault: true, + icon: ThemeIcon.asThemeIcon(Codicon.terminalPowershell) + }]); + remoteTerminalService.setProfiles([]); + terminalContributionService.setProfiles([{ + extensionIdentifier: 'ms-vscode.js-debug-nightly', + icon: 'debug', + id: 'extension.js-debug.debugTerminal', + title: 'JavaScript Debug Terminal' + }]); + if (isWindows) { + remoteAgentService.setEnvironment(OperatingSystem.Windows); + } else if (isLinux) { + remoteAgentService.setEnvironment(OperatingSystem.Linux); + } else { + remoteAgentService.setEnvironment(OperatingSystem.Macintosh); + } + }); + + test('should filter out contributed profiles set to null', async () => { + await configurationService.setUserConfiguration('terminal', { + integrated: { + profiles: { + windows: { + 'JavaScript Debug Terminal': null + }, + linux: { + 'JavaScript Debug Terminal': null + }, + osx: { + 'JavaScript Debug Terminal': null + } + } + } + }); + configurationService.onDidChangeConfigurationEmitter.fire({ affectsConfiguration: () => true } as any); + terminalProfileService.refreshAvailableProfiles(); + await terminalProfileService.hasRefreshedProfiles; + deepStrictEqual(terminalProfileService.availableProfiles, [{ + profileName: 'PowerShell', + path: 'C:\\Powershell.exe', + isDefault: true, + icon: ThemeIcon.asThemeIcon(Codicon.terminalPowershell) + }]); + deepStrictEqual(terminalProfileService.contributedProfiles, []); + }); + test('should include contributed profiles', async () => { + configurationService.setUserConfiguration('terminal', { integrated: defaultTerminalConfig }); + terminalProfileService.refreshAvailableProfiles(); + await terminalProfileService.hasRefreshedProfiles; + deepStrictEqual(terminalProfileService.availableProfiles, [{ + profileName: 'PowerShell', + path: 'C:\\Powershell.exe', + isDefault: true, + icon: ThemeIcon.asThemeIcon(Codicon.terminalPowershell) + }]); + deepStrictEqual(terminalProfileService.contributedProfiles, [{ + extensionIdentifier: 'ms-vscode.js-debug-nightly', + icon: 'debug', + id: 'extension.js-debug.debugTerminal', + title: 'JavaScript Debug Terminal' + }]); + }); +});