diff --git a/src/vs/platform/terminal/common/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index 6557e21eb52..9b248c22573 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/src/vs/platform/terminal/common/terminal.ts @@ -10,6 +10,13 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { IGetTerminalLayoutInfoArgs, IProcessDetails, IPtyHostProcessReplayEvent, ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; +export const enum TerminalSettingPrefix { + Shell = 'terminal.integrated.shell.', + ShellArgs = 'terminal.integrated.shellArgs.', + DefaultProfile = 'terminal.integrated.defaultProfile.', + Profiles = 'terminal.integrated.profiles.' +} + export const enum TerminalSettingId { ShellLinux = 'terminal.integrated.shell.linux', ShellMacOs = 'terminal.integrated.shell.osx', diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index e62c2050680..acef7a71f90 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, Severity } from 'vs/platform/notification/common/notification'; +import { INotificationService, IPromptChoice, NeverShowAgainScope, 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'; @@ -46,7 +46,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 } from 'vs/platform/terminal/common/terminal'; +import { IProcessDataEvent, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, TerminalShellType, TerminalSettingId, TitleEventSource, TerminalIcon, TerminalSettingPrefix } 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 } from 'vs/base/common/async'; @@ -66,6 +66,8 @@ import { getColorClass } from 'vs/workbench/contrib/terminal/browser/terminalIco const SLOW_CANVAS_RENDER_THRESHOLD = 50; const NUMBER_OF_FRAMES_TO_MEASURE = 20; +const SHOULD_PROMPT_FOR_PROFILE_MIGRATION_KEY = 'terminals.integrated.profile-migration'; + const enum Constants { /** * The maximum amount of milliseconds to wait for a container before starting to create the @@ -326,6 +328,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { window.clearTimeout(initialDataEventsTimeout); } })); + this.showProfileMigrationNotification(); } private _getIcon(): TerminalIcon | undefined { @@ -353,6 +356,44 @@ 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.getValue(TerminalSettingPrefix.ShellArgs + platform)) && + !!this._configurationService.getValue(TerminalSettingPrefix.DefaultProfile + platform); + if (shouldMigrateToProfile && this._storageService.getBoolean(SHOULD_PROMPT_FOR_PROFILE_MIGRATION_KEY, StorageScope.WORKSPACE, true)) { + 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 (profile) { + this._configurationService.updateValue(TerminalSettingPrefix.DefaultProfile + platform, profile.profileName); + this._configurationService.updateValue(TerminalSettingPrefix.Shell + platform, null); + this._configurationService.updateValue(TerminalSettingPrefix.ShellArgs + platform, null); + this._logService.trace(`migrated from shell/shellArgs, ${shell} ${shellArgs} to profile ${JSON.stringify(profile)}`); + } else { + this._logService.trace('migration from shell/shellArgs to profile did not occur bc created profile was an exact match for existing one', shell, shellArgs); + } + } + } as IPromptChoice, + ], + { + neverShowAgain: { id: SHOULD_PROMPT_FOR_PROFILE_MIGRATION_KEY, scope: NeverShowAgainScope.WORKSPACE } + } + ); + } + } + + 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 fd297b78720..02d2ef1e4fc 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts @@ -410,6 +410,45 @@ export abstract class BaseTerminalProfileResolverService implements ITerminalPro } return value; } + + async createProfileFromShellAndShellArgs(shell?: unknown, shellArgs?: unknown): Promise { + const detectedProfile = this._terminalService.availableProfiles?.find(p => p.path === shell); + const fallbackProfile = (await this.getDefaultProfile({ + remoteAuthority: this._remoteAgentService.getConnection()?.remoteAuthority, + os: this._primaryBackendOs! + })); + const profile = detectedProfile || fallbackProfile; + const args = this._isValidShellArgs(shellArgs, this._primaryBackendOs!) ? shellArgs : profile.args; + const createdProfile = { + profileName: profile.profileName, + path: profile.path, + args, + isDefault: true + }; + if (detectedProfile && detectedProfile.profileName === createdProfile.profileName && detectedProfile.path === createdProfile.path && this._argsMatch(detectedProfile.args, createdProfile.args)) { + return undefined; + } + return createdProfile; + } + + 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; + } + } + return true; + } + return false; + } } export class BrowserTerminalProfileResolverService extends BaseTerminalProfileResolverService { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index ce934e8dc6b..86412f46dbd 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -17,7 +17,7 @@ import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IInstantiationService, optional } from 'vs/platform/instantiation/common/instantiation'; import { IKeyMods, IPickOptions, IQuickInputButton, IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { ILocalTerminalService, IOffProcessTerminalService, IShellLaunchConfig, ITerminalLaunchError, ITerminalProfile, ITerminalProfileObject, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, TerminalSettingId } from 'vs/platform/terminal/common/terminal'; +import { ILocalTerminalService, IOffProcessTerminalService, IShellLaunchConfig, ITerminalLaunchError, ITerminalProfile, ITerminalProfileObject, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, TerminalSettingId, TerminalSettingPrefix } from 'vs/platform/terminal/common/terminal'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { IEditableData, IViewDescriptorService, IViewsService, ViewContainerLocation } from 'vs/workbench/common/views'; import { IRemoteTerminalService, ITerminalExternalLinkProvider, ITerminalInstance, ITerminalService, ITerminalGroup, TerminalConnectionState } from 'vs/workbench/contrib/terminal/browser/terminal'; @@ -177,12 +177,8 @@ export class TerminalService implements ITerminalService { lifecycleService.onWillShutdown(e => this._onWillShutdown(e)); this._configurationService.onDidChangeConfiguration(async e => { - if (e.affectsConfiguration(TerminalSettingId.ProfilesWindows) || - e.affectsConfiguration(TerminalSettingId.ProfilesMacOs) || - e.affectsConfiguration(TerminalSettingId.ProfilesLinux) || - e.affectsConfiguration(TerminalSettingId.DefaultProfileWindows) || - e.affectsConfiguration(TerminalSettingId.DefaultProfileMacOs) || - e.affectsConfiguration(TerminalSettingId.DefaultProfileLinux) || + if (e.affectsConfiguration(TerminalSettingPrefix.DefaultProfile + this._getPlatformKey()) || + e.affectsConfiguration(TerminalSettingPrefix.Profiles + this._getPlatformKey()) || e.affectsConfiguration(TerminalSettingId.UseWslProfiles)) { this._refreshAvailableProfiles(); } diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index a553bd3a868..a5c363f577b 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -114,6 +114,7 @@ export interface ITerminalProfileResolverService { // TODO: Remove when workspace trust is enabled getSafeConfigValue(key: string, os: OperatingSystem): unknown | undefined; getSafeConfigValueFullKey(key: string): unknown | undefined; + createProfileFromShellAndShellArgs(shell?: unknown, shellArgs?: unknown): Promise; } export interface IShellLaunchConfigResolveOptions { diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 18435c9d20c..3dc739082b1 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -1591,6 +1591,7 @@ export class TestTerminalProfileResolverService implements ITerminalProfileResol async getEnvironment(): Promise { return process.env; } getSafeConfigValue(key: string, os: OperatingSystem): unknown | undefined { return undefined; } getSafeConfigValueFullKey(key: string): unknown | undefined { return undefined; } + createProfileFromShellAndShellArgs(shell?: unknown, shellArgs?: unknown): Promise { throw new Error('Method not implemented.'); } } export class TestLocalTerminalService implements ILocalTerminalService {