diff --git a/src/vs/workbench/api/browser/mainThreadTerminalService.ts b/src/vs/workbench/api/browser/mainThreadTerminalService.ts index c98bc39f97c..e3f0e5d402c 100644 --- a/src/vs/workbench/api/browser/mainThreadTerminalService.ts +++ b/src/vs/workbench/api/browser/mainThreadTerminalService.ts @@ -308,7 +308,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape // event. const instance = this._terminalService.getInstanceFromId(terminalId); if (instance) { - instance.setTitle(title, TitleEventSource.Api); + instance.refreshTabLabels(title, TitleEventSource.Api); } } diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index bccf0f3c89a..f26a59487ec 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, ITerminalProfileType, TerminalLocation, ICreateContributedTerminalProfileOptions, ProcessPropertyType } from 'vs/platform/terminal/common/terminal'; +import { IShellLaunchConfig, ITerminalChildProcess, ITerminalDimensions, ITerminalLaunchError, ITerminalProfile, ITerminalTabLayoutInfoById, TerminalIcon, TitleEventSource, TerminalShellType, IExtensionTerminalProfile, ITerminalProfileType, TerminalLocation, ICreateContributedTerminalProfileOptions, ProcessPropertyType, ProcessCapability } from 'vs/platform/terminal/common/terminal'; import { ICommandTracker, INavigationMode, IOffProcessTerminalService, IRemoteTerminalAttachTarget, IStartExtensionTerminalRequest, ITerminalConfigHelper, ITerminalProcessExtHostProxy } from 'vs/workbench/contrib/terminal/common/terminal'; import type { Terminal as XTermTerminal } from 'xterm'; import type { SearchAddon as XTermSearchAddon } from 'xterm-addon-search'; @@ -401,6 +401,14 @@ export interface ITerminalInstance { readonly icon?: TerminalIcon; readonly color?: string; + readonly processName: string; + readonly sequence?: string; + readonly staticTitle?: string; + readonly workspaceFolder?: string; + readonly cwd?: string; + readonly initialCwd?: string; + readonly capabilities: ProcessCapability[]; + readonly statusList: ITerminalStatusList; /** @@ -724,9 +732,9 @@ export interface ITerminalInstance { relaunch(): void; /** - * Sets the title of the terminal instance. + * Sets the title and description of the terminal instance's label. */ - setTitle(title: string, eventSource: TitleEventSource): void; + refreshTabLabels(title: string, eventSource: TitleEventSource): void; waitForTitle(): Promise; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index ae788de30f7..de81e6e78ff 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -1256,7 +1256,7 @@ export function registerTerminalActions() { notificationService.warn(localize('workbench.action.terminal.renameWithArg.noName', "No name argument provided")); return; } - accessor.get(ITerminalService).activeInstance?.setTitle(args.name, TitleEventSource.Api); + accessor.get(ITerminalService).activeInstance?.refreshTabLabels(args.name, TitleEventSource.Api); } }); registerAction2(class extends Action2 { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 9b11c21cdb9..b31e8a0416a 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -69,7 +69,7 @@ import { getTerminalResourcesFromDragEvent, getTerminalUri } from 'vs/workbench/ import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { TerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/terminalEditorInput'; import { isSafari } from 'vs/base/browser/browser'; -import { template } from 'vs/base/common/labels'; +import { ISeparator, template } from 'vs/base/common/labels'; // How long in milliseconds should an average frame take to render for a notification to appear // which suggests the fallback DOM-based renderer @@ -164,16 +164,17 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { private _hasHadInput: boolean; - private _capabilities: ProcessCapability[] = []; - - private _workspaceFolder: string = ''; readonly statusList: ITerminalStatusList; disableLayout: boolean = false; - private _description: string | undefined = undefined; - get description(): string | undefined { return this._description || this.shellLaunchConfig.description; } - private _processName: string | undefined = undefined; - private _sequence: string | undefined = undefined; + + private _capabilities: ProcessCapability[] = []; + private _description?: string; + private _processName: string = ''; + private _sequence?: string; + private _staticTitle?: string; + private _workspaceFolder?: string; + private _labelComputer?: TerminalLabelComputer; target?: TerminalLocation; get instanceId(): number { return this._instanceId; } @@ -222,6 +223,14 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { get icon(): TerminalIcon | undefined { return this._getIcon(); } get color(): string | undefined { return this._getColor(); } + get processName(): string { return this._processName; } + get sequence(): string | undefined { return this._sequence; } + get staticTitle(): string | undefined { return this._staticTitle; } + get workspaceFolder(): string | undefined { return this._workspaceFolder; } + get cwd(): string | undefined { return this._cwd; } + get initialCwd(): string | undefined { return this._initialCwd; } + get capabilities(): ProcessCapability[] { return this._capabilities; } + get description(): string | undefined { return this._description || this.shellLaunchConfig.description; } // The onExit event is special in that it fires and is disposed after the terminal instance // itself is disposed private readonly _onExit = new Emitter(); @@ -322,7 +331,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // When a custom pty is used set the name immediately so it gets passed over to the exthost // and is available when Pseudoterminal.open fires. if (this.shellLaunchConfig.customPtyImplementation) { - this.setTitle(this._shellLaunchConfig.name, TitleEventSource.Api); + this.refreshTabLabels(this._shellLaunchConfig.name, TitleEventSource.Api); } this.statusList = this._instantiationService.createInstance(TerminalStatusList); @@ -341,7 +350,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // Re-establish the title after reconnect if (this.shellLaunchConfig.attachPersistentProcess) { - this.setTitle(this.shellLaunchConfig.attachPersistentProcess.title, this.shellLaunchConfig.attachPersistentProcess.titleSource); + this.refreshTabLabels(this.shellLaunchConfig.attachPersistentProcess.title, this.shellLaunchConfig.attachPersistentProcess.titleSource); } }); @@ -371,7 +380,14 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { if (e.affectsConfiguration('editor.accessibilitySupport')) { this.updateAccessibilitySupport(); } + if ( + e.affectsConfiguration(TerminalSettingId.TerminalTitle) || + e.affectsConfiguration(TerminalSettingId.TerminalTitleSeparator) || + e.affectsConfiguration(TerminalSettingId.TerminalDescription)) { + this._labelComputer?.refreshLabel(); + } })); + this._workspaceContextService.onDidChangeWorkspaceFolders(() => this._labelComputer?.refreshLabel()); // Clear out initial data events after 10 seconds, hopefully extension hosts are up and // running at that point. @@ -1167,25 +1183,33 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._capabilities = e.capabilities; // Set the initial name based on the _resolved_ shell launch config, this will also // ensure the resolved icon gets shown + if (!this._labelComputer) { + this._labelComputer = this._register(new TerminalLabelComputer(this._configHelper, this, this._workspaceContextService)); + this._labelComputer.onDidChangeLabel(e => { + this._title = e.title; + this._description = e.description; + this._onTitleChanged.fire(this); + }); + } this._processManager.onDidChangeProperty(e => { if (e.type === ProcessPropertyType.Cwd) { this._cwd = e.value; - this.setTitle(this.title, TitleEventSource.Api); + this._labelComputer?.refreshLabel(); } else if (e.type === ProcessPropertyType.InitialCwd) { this._initialCwd = e.value; this._cwd = this._initialCwd; - this.setTitle(this.title, TitleEventSource.Api); + this.refreshTabLabels(this.title, TitleEventSource.Api); } }); if (this._shellLaunchConfig.name) { - this.setTitle(this._shellLaunchConfig.name, TitleEventSource.Api); + this.refreshTabLabels(this._shellLaunchConfig.name, TitleEventSource.Api); } else { // Only listen for process title changes when a name is not provided if (this._configHelper.config.tabs.title.includes('${sequence}') || this._configHelper.config.tabs.description.includes('${sequence}')) { // Set the title to the first event if the sequence hasn't set it yet Event.once(this._processManager.onProcessTitle)(e => { if (!this._title) { - this.setTitle(e, TitleEventSource.Sequence); + this.refreshTabLabels(e, TitleEventSource.Sequence); } }); // Listen to xterm.js' sequence title change event, trigger this async to ensure @@ -1196,8 +1220,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { }); }); } else { - this.setTitle(this._shellLaunchConfig.executable, TitleEventSource.Process); - this._messageTitleDisposable = this._processManager.onProcessTitle(title => this.setTitle(title ? title : '', TitleEventSource.Process)); + this.refreshTabLabels(this._shellLaunchConfig.executable, TitleEventSource.Process); + this._messageTitleDisposable = this._processManager.onProcessTitle(title => this.refreshTabLabels(title ? title : '', TitleEventSource.Process)); } } }); @@ -1468,7 +1492,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { private _onTitleChange(title: string): void { if (this.isTitleSetByProcess) { - this.setTitle(title, TitleEventSource.Sequence); + this.refreshTabLabels(title, TitleEventSource.Sequence); } } @@ -1755,9 +1779,26 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } } - setTitle(title: string | undefined, eventSource: TitleEventSource): void { + refreshTabLabels(title: string | undefined, eventSource: TitleEventSource): void { + title = this._updateTitleProperties(title, eventSource); + const titleChanged = title !== this._title; + this._title = title; + this._labelComputer?.refreshLabel(); + this._setAriaLabel(this._xterm, this._instanceId, this._title); + + if (this._titleReadyComplete) { + this._titleReadyComplete(title); + this._titleReadyComplete = undefined; + } + + if (titleChanged) { + this._onTitleChanged.fire(this); + } + } + + private _updateTitleProperties(title: string | undefined, eventSource: TitleEventSource): string { if (!title) { - return; + return this._processName; } switch (eventSource) { case TitleEventSource.Process: @@ -1777,7 +1818,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { case TitleEventSource.Api: // If the title has not been set by the API or the rename command, unregister the handler that // automatically updates the terminal name - this._processName = title; + this._staticTitle = title; dispose(this._messageTitleDisposable); this._messageTitleDisposable = undefined; break; @@ -1791,72 +1832,10 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } break; } - // Remove special characters that could mess with rendering - title = title.replace(/[\n\r\t]/g, ''); - title = this._customizeTitle(title, eventSource); - this._title = title; this._titleSource = eventSource; - this._setAriaLabel(this._xterm, this._instanceId, this._title); - - if (this._titleReadyComplete) { - this._titleReadyComplete(title); - this._titleReadyComplete = undefined; - } - this._onTitleChanged.fire(this); - } - - private _customizeTitle(title: string, eventSource: TitleEventSource): string { - if (eventSource === TitleEventSource.Api) { - return title; - } - const cwd = this._cwd || this._initialCwd || ''; - const properties = { - cwd, - cwdFolder: this.getCwdFolder(), - workspaceFolder: this._workspaceFolder, - local: this.shellLaunchConfig.description === 'Local' ? 'Local' : undefined, - process: this._processName, - sequence: this._sequence, - task: this.shellLaunchConfig.description === 'Task' ? 'Task' : undefined, - separator: { label: this._configHelper.config.tabs.separator } - }; - title = template(this._configHelper.config.tabs.title, properties); - const description = template(this._configHelper.config.tabs.description, properties); - const titleChanged = title !== this._title || description !== this.description || eventSource === TitleEventSource.Config; - if (!title || !titleChanged) { - return title; - } - this._description = description; return title; } - getCwdFolder(): string { - const cwd = this._cwd || this._initialCwd; - if (!cwd || - !this._capabilities.includes(ProcessCapability.CwdDetection) || - this._workspaceContextService.getWorkspace().folders.length === 0 || - (this._workspaceContextService.getWorkspace().folders.length === 1 && this._equalIgnoringSlashes(this._configHelper.config.cwd || this._workspaceContextService.getWorkspace().folders[0].uri.toString(), cwd))) { - return ''; - } - return path.basename(cwd); - } - - private _equalIgnoringSlashes(workspaceUri: string, cwd: string): boolean { - let workspacePaths = workspaceUri.includes('/') ? workspaceUri.split('/') : workspaceUri.split('\\'); - let cwdPaths = cwd.includes('/') ? cwd.split('/') : cwd.split('\\'); - workspacePaths = workspacePaths.slice(4); - cwdPaths = cwdPaths.slice(1); - if (workspacePaths.length !== cwdPaths.length) { - return false; - } - for (let i = 0; i < cwdPaths.length; i++) { - if (workspacePaths[i] !== cwdPaths[i]) { - return false; - } - } - return true; - } - waitForTitle(): Promise { return this._titleReadyPromise; } @@ -2023,7 +2002,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { }); } if (title) { - this.setTitle(title, TitleEventSource.Api); + this.refreshTabLabels(title, TitleEventSource.Api); } } @@ -2275,3 +2254,76 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) = `); } }); + +export interface ITerminalLabelTemplateProperties { + cwd?: string | null | undefined; + cwdFolder?: string | null | undefined; + workspaceFolder?: string | null | undefined; + local?: string | null | undefined; + process?: string | null | undefined; + sequence?: string | null | undefined; + task?: string | null | undefined; + separator?: string | ISeparator | null | undefined; +} + +const enum TerminalLabelType { + Title = 'title', + Description = 'description' +} + +export class TerminalLabelComputer extends Disposable { + private _title: string = ''; + private _description: string = ''; + get title(): string | undefined { return this._title; } + get description(): string | undefined { return this._description; } + + private readonly _onDidChangeLabel = this._register(new Emitter<{ title: string, description: string }>()); + readonly onDidChangeLabel = this._onDidChangeLabel.event; + constructor( + private readonly _configHelper: TerminalConfigHelper, + private readonly _instance: Pick, + @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService + ) { + super(); + } + refreshLabel(): void { + this._title = this.computeLabel(this._configHelper.config.tabs.title, TerminalLabelType.Title); + this._description = this.computeLabel(this._configHelper.config.tabs.description, TerminalLabelType.Description); + if (this._title !== this._instance.title || this._description !== this._instance.description) { + this._onDidChangeLabel.fire({ title: this._title, description: this._description }); + } + } + + computeLabel( + labelTemplate: string, + labelType: TerminalLabelType + ) { + const templateProperties: ITerminalLabelTemplateProperties = { + cwd: this._instance.cwd || this._instance.initialCwd || '', + cwdFolder: '', + workspaceFolder: this._instance.workspaceFolder, + local: this._instance.shellLaunchConfig.description === 'Local' ? 'Local' : undefined, + process: this._instance.processName, + sequence: this._instance.sequence, + task: this._instance.shellLaunchConfig.description === 'Task' ? 'Task' : undefined, + separator: { label: this._configHelper.config.tabs.separator } + }; + if (!labelTemplate) { + return ''; + } + if (this._instance.staticTitle && labelType === TerminalLabelType.Title) { + return this._instance.staticTitle.replace(/[\n\r\t]/g, '') || templateProperties.process?.replace(/[\n\r\t]/g, '') || ''; + } + + if (!templateProperties.cwd || !this._instance.capabilities.includes(ProcessCapability.CwdDetection) || + (this._workspaceContextService.getWorkspace().folders.length <= 1 && + (templateProperties.cwd === (this._configHelper.config.cwd || (this._workspaceContextService.getWorkspace().folders[0].uri.fsPath))))) { + templateProperties.cwdFolder = ''; + } else if (templateProperties.cwd && typeof templateProperties.cwd === 'string') { + templateProperties.cwdFolder = path.basename(templateProperties.cwd); + } + //Remove special characters that could mess with rendering + const label = template(labelTemplate, (templateProperties as unknown) as { [key: string]: string | ISeparator | undefined | null; }).replace(/[\n\r\t]/g, ''); + return label === '' && labelType === TerminalLabelType.Title ? (this._instance.processName || '') : label; + } +} diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index 354985b734f..d65c6cde6c0 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -23,7 +23,7 @@ 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, ITerminalProfileType, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, TerminalLocation, TerminalLocationString, TerminalSettingId, TerminalSettingPrefix, TitleEventSource } from 'vs/platform/terminal/common/terminal'; +import { ICreateContributedTerminalProfileOptions, IExtensionTerminalProfile, IShellLaunchConfig, ITerminalLaunchError, ITerminalProfile, ITerminalProfileObject, ITerminalProfileType, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, TerminalLocation, TerminalLocationString, TerminalSettingId, TerminalSettingPrefix } from 'vs/platform/terminal/common/terminal'; import { registerTerminalDefaultProfileConfiguration } from 'vs/platform/terminal/common/terminalPlatformConfiguration'; import { iconForeground } from 'vs/platform/theme/common/colorRegistry'; import { IconDefinition } from 'vs/platform/theme/common/iconRegistry'; @@ -257,14 +257,8 @@ export class TerminalService implements ITerminalService { e.affectsConfiguration(TerminalSettingPrefix.Profiles + platformKey) || e.affectsConfiguration(TerminalSettingId.UseWslProfiles)) { this._refreshAvailableProfiles(); - } else if ( - e.affectsConfiguration(TerminalSettingId.TerminalTitle) || - e.affectsConfiguration(TerminalSettingId.TerminalTitleSeparator) || - e.affectsConfiguration(TerminalSettingId.TerminalDescription)) { - this._updateTitles(); } }); - workspaceContextService.onDidChangeWorkspaceFolders(() => this._updateTitles()); // Register a resource formatter for terminal URIs labelService.registerFormatter({ @@ -319,12 +313,6 @@ export class TerminalService implements ITerminalService { return this._primaryOffProcessTerminalService; } - private _updateTitles(): void { - for (const instance of this.instances) { - instance.setTitle(instance.title, TitleEventSource.Config); - } - } - private _forwardInstanceHostEvents(host: ITerminalInstanceHost) { host.onDidChangeInstances(this._onDidChangeInstances.fire, this._onDidChangeInstances); host.onDidDisposeInstance(this._onDidDisposeInstance.fire, this._onDidDisposeInstance); diff --git a/src/vs/workbench/contrib/terminal/test/browser/terminalInstance.test.ts b/src/vs/workbench/contrib/terminal/test/browser/terminalInstance.test.ts new file mode 100644 index 00000000000..bb086b2e453 --- /dev/null +++ b/src/vs/workbench/contrib/terminal/test/browser/terminalInstance.test.ts @@ -0,0 +1,201 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { strictEqual } from 'assert'; +import { isWindows } from 'vs/base/common/platform'; +import { TerminalLabelComputer } from 'vs/workbench/contrib/terminal/browser/terminalInstance'; +import { IWorkspaceContextService, toWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { Workspace } from 'vs/platform/workspace/test/common/testWorkspace'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { ProcessCapability } from 'vs/platform/terminal/common/terminal'; +import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices'; +import { fixPath, getUri } from 'vs/workbench/contrib/search/test/browser/queryBuilder.test'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper'; +import { ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { basename } from 'path'; + +function createInstance(partial?: Partial): Pick { + return { + shellLaunchConfig: {}, + cwd: 'cwd', + initialCwd: undefined, + processName: '', + sequence: undefined, + workspaceFolder: undefined, + staticTitle: undefined, + capabilities: isWindows ? [] : [ProcessCapability.CwdDetection], + title: '', + description: '', + ...partial + }; +} +const root1 = '/foo/root1'; +const ROOT_1 = fixPath(root1); +const root2 = '/foo/root2'; +const ROOT_2 = fixPath(root2); +const emptyRoot = '/foo'; +const ROOT_EMPTY = fixPath(emptyRoot); +suite('Workbench - TerminalInstance', () => { + suite('refreshLabel', () => { + let configurationService: TestConfigurationService; + let terminalLabelComputer: TerminalLabelComputer; + let instantiationService: TestInstantiationService; + let mockContextService: TestContextService; + let mockMultiRootContextService: TestContextService; + let emptyContextService: TestContextService; + let mockWorkspace: Workspace; + let mockMultiRootWorkspace: Workspace; + let emptyWorkspace: Workspace; + let capabilities: ProcessCapability[]; + let configHelper: TerminalConfigHelper; + setup(async () => { + instantiationService = new TestInstantiationService(); + instantiationService.stub(IWorkspaceContextService, new TestContextService()); + capabilities = isWindows ? [] : [ProcessCapability.CwdDetection]; + + const ROOT_1_URI = getUri(ROOT_1); + mockContextService = new TestContextService(); + mockWorkspace = new Workspace('workspace', [toWorkspaceFolder(ROOT_1_URI)]); + mockContextService.setWorkspace(mockWorkspace); + + const ROOT_2_URI = getUri(ROOT_2); + mockMultiRootContextService = new TestContextService(); + mockMultiRootWorkspace = new Workspace('multi-root-workspace', [toWorkspaceFolder(ROOT_1_URI), toWorkspaceFolder(ROOT_2_URI)]); + mockMultiRootContextService.setWorkspace(mockMultiRootWorkspace); + + const ROOT_EMPTY_URI = getUri(ROOT_EMPTY); + emptyContextService = new TestContextService(); + emptyWorkspace = new Workspace('empty workspace', [], ROOT_EMPTY_URI); + emptyContextService.setWorkspace(emptyWorkspace); + }); + + test('should resolve to "" when the template variables are empty', () => { + configurationService = new TestConfigurationService({ terminal: { integrated: { tabs: { separator: ' - ', title: '', description: '' } } } }); + configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!); + terminalLabelComputer = new TerminalLabelComputer(configHelper, createInstance({ capabilities, processName: '' }), mockContextService); + terminalLabelComputer.refreshLabel(); + // TODO: + // terminalLabelComputer.onLabelChanged(e => { + // strictEqual(e.title, ''); + // strictEqual(e.description, ''); + // }); + strictEqual(terminalLabelComputer.title, ''); + strictEqual(terminalLabelComputer.description, ''); + }); + test('should resolve cwd', () => { + configurationService = new TestConfigurationService({ terminal: { integrated: { tabs: { separator: ' - ', title: '${cwd}', description: '${cwd}' } } } }); + configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!); + terminalLabelComputer = new TerminalLabelComputer(configHelper, createInstance({ capabilities, cwd: ROOT_1 }), mockContextService); + terminalLabelComputer.refreshLabel(); + strictEqual(terminalLabelComputer.title, ROOT_1); + strictEqual(terminalLabelComputer.description, ROOT_1); + }); + test('should resolve cwdFolder in a single root workspace if cwd differs from root', () => { + configurationService = new TestConfigurationService({ terminal: { integrated: { tabs: { separator: ' - ', title: '${process}', description: '${cwdFolder}' } } } }); + configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!); + terminalLabelComputer = new TerminalLabelComputer(configHelper, createInstance({ capabilities, cwd: ROOT_2, processName: 'zsh' }), mockContextService); + terminalLabelComputer.refreshLabel(); + if (isWindows) { + strictEqual(terminalLabelComputer.title, 'zsh'); + strictEqual(terminalLabelComputer.description, ''); + } else { + strictEqual(terminalLabelComputer.title, 'zsh'); + strictEqual(terminalLabelComputer.description, basename(ROOT_2)); + } + }); + test('should resolve workspaceFolder', () => { + configurationService = new TestConfigurationService({ terminal: { integrated: { tabs: { separator: ' - ', title: '${workspaceFolder}', description: '${workspaceFolder}' } } } }); + configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!); + terminalLabelComputer = new TerminalLabelComputer(configHelper, createInstance({ capabilities, processName: 'zsh', workspaceFolder: 'folder' }), mockContextService); + terminalLabelComputer.refreshLabel(); + strictEqual(terminalLabelComputer.title, 'folder'); + strictEqual(terminalLabelComputer.description, 'folder'); + }); + test('should resolve local', () => { + configurationService = new TestConfigurationService({ terminal: { integrated: { tabs: { separator: ' - ', title: '${local}', description: '${local}' } } } }); + configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!); + terminalLabelComputer = new TerminalLabelComputer(configHelper, createInstance({ capabilities, processName: 'zsh', shellLaunchConfig: { description: 'Local' } }), mockContextService); + terminalLabelComputer.refreshLabel(); + strictEqual(terminalLabelComputer.title, 'Local'); + strictEqual(terminalLabelComputer.description, 'Local'); + }); + test('should resolve process', () => { + configurationService = new TestConfigurationService({ terminal: { integrated: { tabs: { separator: ' - ', title: '${process}', description: '${process}' } } } }); + configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!); + terminalLabelComputer = new TerminalLabelComputer(configHelper, createInstance({ capabilities, processName: 'zsh' }), mockContextService); + terminalLabelComputer.refreshLabel(); + strictEqual(terminalLabelComputer.title, 'zsh'); + strictEqual(terminalLabelComputer.description, 'zsh'); + }); + test('should resolve sequence', () => { + configurationService = new TestConfigurationService({ terminal: { integrated: { tabs: { separator: ' - ', title: '${sequence}', description: '${sequence}' } } } }); + configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!); + terminalLabelComputer = new TerminalLabelComputer(configHelper, createInstance({ capabilities, sequence: 'sequence' }), mockContextService); + terminalLabelComputer.refreshLabel(); + strictEqual(terminalLabelComputer.title, 'sequence'); + strictEqual(terminalLabelComputer.description, 'sequence'); + }); + test('should resolve task', () => { + configurationService = new TestConfigurationService({ terminal: { integrated: { tabs: { separator: ' ~ ', title: '${process}${separator}${task}', description: '${task}' } } } }); + configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!); + terminalLabelComputer = new TerminalLabelComputer(configHelper, createInstance({ capabilities, processName: 'zsh', shellLaunchConfig: { description: 'Task' } }), mockContextService); + terminalLabelComputer.refreshLabel(); + strictEqual(terminalLabelComputer.title, 'zsh ~ Task'); + strictEqual(terminalLabelComputer.description, 'Task'); + }); + test('should resolve separator', () => { + configurationService = new TestConfigurationService({ terminal: { integrated: { tabs: { separator: ' ~ ', title: '${separator}', description: '${separator}' } } } }); + configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!); + terminalLabelComputer = new TerminalLabelComputer(configHelper, createInstance({ capabilities, processName: 'zsh', shellLaunchConfig: { description: 'Task' } }), mockContextService); + terminalLabelComputer.refreshLabel(); + strictEqual(terminalLabelComputer.title, 'zsh'); + strictEqual(terminalLabelComputer.description, ''); + }); + test('should always return static title when specified', () => { + configurationService = new TestConfigurationService({ terminal: { integrated: { tabs: { separator: ' ~ ', title: '${process}', description: '${workspaceFolder}' } } } }); + configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!); + terminalLabelComputer = new TerminalLabelComputer(configHelper, createInstance({ capabilities, processName: 'process', workspaceFolder: 'folder', staticTitle: 'my-title' }), mockContextService); + terminalLabelComputer.refreshLabel(); + strictEqual(terminalLabelComputer.title, 'my-title'); + strictEqual(terminalLabelComputer.description, 'folder'); + }); + test('should provide cwdFolder for all cwds only when in multi-root', () => { + configurationService = new TestConfigurationService({ terminal: { integrated: { tabs: { separator: ' ~ ', title: '${process}${separator}${cwdFolder}', description: '${cwdFolder}' } }, cwd: ROOT_1 } }); + configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!); + terminalLabelComputer = new TerminalLabelComputer(configHelper, createInstance({ capabilities, processName: 'process', workspaceFolder: 'folder', cwd: ROOT_1 }), mockContextService); + terminalLabelComputer.refreshLabel(); + // single-root, cwd is same as root + strictEqual(terminalLabelComputer.title, 'process'); + strictEqual(terminalLabelComputer.description, ''); + // multi-root + configurationService = new TestConfigurationService({ terminal: { integrated: { tabs: { separator: ' ~ ', title: '${process}${separator}${cwdFolder}', description: '${cwdFolder}' } }, cwd: ROOT_1 } }); + configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!); + terminalLabelComputer = new TerminalLabelComputer(configHelper, createInstance({ capabilities, processName: 'process', workspaceFolder: 'folder', cwd: ROOT_2 }), mockMultiRootContextService); + terminalLabelComputer.refreshLabel(); + if (isWindows) { + strictEqual(terminalLabelComputer.title, 'process'); + strictEqual(terminalLabelComputer.description, ''); + } else { + strictEqual(terminalLabelComputer.title, 'process ~ root2'); + strictEqual(terminalLabelComputer.description, 'root2'); + } + }); + test.skip('should hide cwdFolder in empty workspaces when cwd matches the workspace\'s default cwd ($HOME or $HOMEDRIVE$HOMEPATH)', async () => { + configurationService = new TestConfigurationService({ terminal: { integrated: { tabs: { separator: ' ~ ', title: '${process}${separator}${cwdFolder}', description: '${cwdFolder}' } }, cwd: ROOT_1 } }); + configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!); + terminalLabelComputer = new TerminalLabelComputer(configHelper, createInstance({ capabilities, processName: 'process', workspaceFolder: 'folder', cwd: ROOT_EMPTY }), emptyContextService); + terminalLabelComputer.refreshLabel(); + strictEqual(terminalLabelComputer.title, 'process'); + strictEqual(terminalLabelComputer.description, ''); + if (!isWindows) { + terminalLabelComputer = new TerminalLabelComputer(configHelper, createInstance({ capabilities, processName: 'process', workspaceFolder: 'folder', cwd: ROOT_1 }), emptyContextService); + terminalLabelComputer.refreshLabel(); + strictEqual(terminalLabelComputer.title, 'process'); + strictEqual(terminalLabelComputer.description, ROOT_1); + } + }); + }); +});