Path: blob/main/src/vs/workbench/browser/parts/titlebar/windowTitle.ts
5319 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { localize } from '../../../../nls.js';6import { dirname, basename } from '../../../../base/common/resources.js';7import { ITitleProperties, ITitleVariable } from './titlebarPart.js';8import { IConfigurationService, IConfigurationChangeEvent, isConfigured } from '../../../../platform/configuration/common/configuration.js';9import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js';10import { Registry } from '../../../../platform/registry/common/platform.js';11import { IEditorService } from '../../../services/editor/common/editorService.js';12import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';13import { EditorResourceAccessor, Verbosity, SideBySideEditor } from '../../../common/editor.js';14import { IBrowserWorkbenchEnvironmentService } from '../../../services/environment/browser/environmentService.js';15import { IWorkspaceContextService, WorkbenchState, IWorkspaceFolder } from '../../../../platform/workspace/common/workspace.js';16import { isWindows, isWeb, isMacintosh, isNative } from '../../../../base/common/platform.js';17import { URI } from '../../../../base/common/uri.js';18import { trim } from '../../../../base/common/strings.js';19import { template } from '../../../../base/common/labels.js';20import { ILabelService, Verbosity as LabelVerbosity } from '../../../../platform/label/common/label.js';21import { Emitter } from '../../../../base/common/event.js';22import { RunOnceScheduler } from '../../../../base/common/async.js';23import { IProductService } from '../../../../platform/product/common/productService.js';24import { Schemas } from '../../../../base/common/network.js';25import { getVirtualWorkspaceLocation } from '../../../../platform/workspace/common/virtualWorkspace.js';26import { IUserDataProfileService } from '../../../services/userDataProfile/common/userDataProfile.js';27import { IViewsService } from '../../../services/views/common/viewsService.js';28import { ICodeEditor, isCodeEditor, isDiffEditor } from '../../../../editor/browser/editorBrowser.js';29import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';30import { getWindowById } from '../../../../base/browser/dom.js';31import { CodeWindow } from '../../../../base/browser/window.js';32import { IDecorationsService } from '../../../services/decorations/common/decorations.js';33import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js';3435const enum WindowSettingNames {36titleSeparator = 'window.titleSeparator',37title = 'window.title',38}3940export const defaultWindowTitle = (() => {41if (isMacintosh && isNative) {42return '${activeEditorShort}${separator}${rootName}${separator}${profileName}'; // macOS has native dirty indicator43}4445const base = '${dirty}${activeEditorShort}${separator}${rootName}${separator}${profileName}${separator}${appName}';46if (isWeb) {47return base + '${separator}${remoteName}'; // Web: always show remote name48}4950return base;51})();52export const defaultWindowTitleSeparator = isMacintosh ? ' \u2014 ' : ' - ';5354export class WindowTitle extends Disposable {5556private static readonly NLS_USER_IS_ADMIN = isWindows ? localize('userIsAdmin', "[Administrator]") : localize('userIsSudo', "[Superuser]");57private static readonly NLS_EXTENSION_HOST = localize('devExtensionWindowTitlePrefix', "[Extension Development Host]");58private static readonly TITLE_DIRTY = '\u25cf ';5960private readonly properties: ITitleProperties = { isPure: true, isAdmin: false, prefix: undefined };61private readonly variables = new Map<string /* context key */, string /* name */>();6263private readonly activeEditorListeners = this._register(new DisposableStore());64private readonly titleUpdater = this._register(new RunOnceScheduler(() => this.doUpdateTitle(), 0));6566private readonly onDidChangeEmitter = this._register(new Emitter<void>());67readonly onDidChange = this.onDidChangeEmitter.event;6869get value() { return this.title ?? ''; }70get workspaceName() { return this.labelService.getWorkspaceLabel(this.contextService.getWorkspace()); }71get fileName() {72const activeEditor = this.editorService.activeEditor;73if (!activeEditor) {74return undefined;75}76const fileName = activeEditor.getTitle(Verbosity.SHORT);77const dirty = activeEditor?.isDirty() && !activeEditor.isSaving() ? WindowTitle.TITLE_DIRTY : '';78return `${dirty}${fileName}`;79}8081private title: string | undefined;8283private titleIncludesFocusedView: boolean = false;84private titleIncludesEditorState: boolean = false;8586private readonly windowId: number;8788constructor(89targetWindow: CodeWindow,90@IConfigurationService protected readonly configurationService: IConfigurationService,91@IContextKeyService private readonly contextKeyService: IContextKeyService,92@IEditorService private readonly editorService: IEditorService,93@IBrowserWorkbenchEnvironmentService protected readonly environmentService: IBrowserWorkbenchEnvironmentService,94@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,95@ILabelService private readonly labelService: ILabelService,96@IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService,97@IProductService private readonly productService: IProductService,98@IViewsService private readonly viewsService: IViewsService,99@IDecorationsService private readonly decorationsService: IDecorationsService,100@IAccessibilityService private readonly accessibilityService: IAccessibilityService101) {102super();103104this.windowId = targetWindow.vscodeWindowId;105106this.checkTitleVariables();107108this.registerListeners();109}110111private registerListeners(): void {112this._register(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationChanged(e)));113this._register(this.editorService.onDidActiveEditorChange(() => this.onActiveEditorChange()));114this._register(this.contextService.onDidChangeWorkspaceFolders(() => this.titleUpdater.schedule()));115this._register(this.contextService.onDidChangeWorkbenchState(() => this.titleUpdater.schedule()));116this._register(this.contextService.onDidChangeWorkspaceName(() => this.titleUpdater.schedule()));117this._register(this.labelService.onDidChangeFormatters(() => this.titleUpdater.schedule()));118this._register(this.userDataProfileService.onDidChangeCurrentProfile(() => this.titleUpdater.schedule()));119this._register(this.viewsService.onDidChangeFocusedView(() => {120if (this.titleIncludesFocusedView) {121this.titleUpdater.schedule();122}123}));124this._register(this.contextKeyService.onDidChangeContext(e => {125if (e.affectsSome(this.variables)) {126this.titleUpdater.schedule();127}128}));129this._register(this.accessibilityService.onDidChangeScreenReaderOptimized(() => this.titleUpdater.schedule()));130}131132private onConfigurationChanged(event: IConfigurationChangeEvent): void {133const affectsTitleConfiguration = event.affectsConfiguration(WindowSettingNames.title);134if (affectsTitleConfiguration) {135this.checkTitleVariables();136}137138if (affectsTitleConfiguration || event.affectsConfiguration(WindowSettingNames.titleSeparator)) {139this.titleUpdater.schedule();140}141}142143private checkTitleVariables(): void {144const titleTemplate = this.configurationService.getValue<unknown>(WindowSettingNames.title);145if (typeof titleTemplate === 'string') {146this.titleIncludesFocusedView = titleTemplate.includes('${focusedView}');147this.titleIncludesEditorState = titleTemplate.includes('${activeEditorState}');148}149}150151private onActiveEditorChange(): void {152153// Dispose old listeners154this.activeEditorListeners.clear();155156// Calculate New Window Title157this.titleUpdater.schedule();158159// Apply listener for dirty and label changes160const activeEditor = this.editorService.activeEditor;161if (activeEditor) {162this.activeEditorListeners.add(activeEditor.onDidChangeDirty(() => this.titleUpdater.schedule()));163this.activeEditorListeners.add(activeEditor.onDidChangeLabel(() => this.titleUpdater.schedule()));164}165166// Apply listeners for tracking focused code editor167if (this.titleIncludesFocusedView) {168const activeTextEditorControl = this.editorService.activeTextEditorControl;169const textEditorControls: ICodeEditor[] = [];170if (isCodeEditor(activeTextEditorControl)) {171textEditorControls.push(activeTextEditorControl);172} else if (isDiffEditor(activeTextEditorControl)) {173textEditorControls.push(activeTextEditorControl.getOriginalEditor(), activeTextEditorControl.getModifiedEditor());174}175176for (const textEditorControl of textEditorControls) {177this.activeEditorListeners.add(textEditorControl.onDidBlurEditorText(() => this.titleUpdater.schedule()));178this.activeEditorListeners.add(textEditorControl.onDidFocusEditorText(() => this.titleUpdater.schedule()));179}180}181182// Apply listener for decorations to track editor state183if (this.titleIncludesEditorState) {184this.activeEditorListeners.add(this.decorationsService.onDidChangeDecorations(() => this.titleUpdater.schedule()));185}186}187188private doUpdateTitle(): void {189const title = this.getFullWindowTitle();190if (title !== this.title) {191192// Always set the native window title to identify us properly to the OS193let nativeTitle = title;194if (!trim(nativeTitle)) {195nativeTitle = this.productService.nameLong;196}197198const window = getWindowById(this.windowId, true).window;199if (!window.document.title && isMacintosh && nativeTitle === this.productService.nameLong) {200// TODO@electron macOS: if we set a window title for201// the first time and it matches the one we set in202// `windowImpl.ts` somehow the window does not appear203// in the "Windows" menu. As such, we set the title204// briefly to something different to ensure macOS205// recognizes we have a window.206// See: https://github.com/microsoft/vscode/issues/191288207window.document.title = `${this.productService.nameLong} ${WindowTitle.TITLE_DIRTY}`;208}209210window.document.title = nativeTitle;211this.title = title;212213this.onDidChangeEmitter.fire();214}215}216217private getFullWindowTitle(): string {218const { prefix, suffix } = this.getTitleDecorations();219220let title = this.getWindowTitle() || this.productService.nameLong;221if (prefix) {222title = `${prefix} ${title}`;223}224225if (suffix) {226title = `${title} ${suffix}`;227}228229// Replace non-space whitespace230return title.replace(/[^\S ]/g, ' ');231}232233getTitleDecorations() {234let prefix: string | undefined;235let suffix: string | undefined;236237if (this.properties.prefix) {238prefix = this.properties.prefix;239}240241if (this.environmentService.isExtensionDevelopment) {242prefix = !prefix243? WindowTitle.NLS_EXTENSION_HOST244: `${WindowTitle.NLS_EXTENSION_HOST} - ${prefix}`;245}246247if (this.properties.isAdmin) {248suffix = WindowTitle.NLS_USER_IS_ADMIN;249}250251return { prefix, suffix };252}253254updateProperties(properties: ITitleProperties): void {255const isAdmin = typeof properties.isAdmin === 'boolean' ? properties.isAdmin : this.properties.isAdmin;256const isPure = typeof properties.isPure === 'boolean' ? properties.isPure : this.properties.isPure;257const prefix = typeof properties.prefix === 'string' ? properties.prefix : this.properties.prefix;258259if (isAdmin !== this.properties.isAdmin || isPure !== this.properties.isPure || prefix !== this.properties.prefix) {260this.properties.isAdmin = isAdmin;261this.properties.isPure = isPure;262this.properties.prefix = prefix;263264this.titleUpdater.schedule();265}266}267268registerVariables(variables: ITitleVariable[]): void {269let changed = false;270271for (const { name, contextKey } of variables) {272if (!this.variables.has(contextKey)) {273this.variables.set(contextKey, name);274275changed = true;276}277}278279if (changed) {280this.titleUpdater.schedule();281}282}283284/**285* Possible template values:286*287* {activeEditorLong}: e.g. /Users/Development/myFolder/myFileFolder/myFile.txt288* {activeEditorMedium}: e.g. myFolder/myFileFolder/myFile.txt289* {activeEditorShort}: e.g. myFile.txt290* {activeEditorLanguageId}: e.g. typescript291* {activeFolderLong}: e.g. /Users/Development/myFolder/myFileFolder292* {activeFolderMedium}: e.g. myFolder/myFileFolder293* {activeFolderShort}: e.g. myFileFolder294* {rootName}: e.g. myFolder1, myFolder2, myFolder3295* {rootPath}: e.g. /Users/Development296* {folderName}: e.g. myFolder297* {folderPath}: e.g. /Users/Development/myFolder298* {appName}: e.g. VS Code299* {remoteName}: e.g. SSH300* {dirty}: indicator301* {focusedView}: e.g. Terminal302* {separator}: conditional separator303* {activeEditorState}: e.g. Modified304*/305getWindowTitle(): string {306const editor = this.editorService.activeEditor;307const workspace = this.contextService.getWorkspace();308309// Compute root310let root: URI | undefined;311if (workspace.configuration) {312root = workspace.configuration;313} else if (workspace.folders.length) {314root = workspace.folders[0].uri;315}316317// Compute active editor folder318const editorResource = EditorResourceAccessor.getOriginalUri(editor, { supportSideBySide: SideBySideEditor.PRIMARY });319let editorFolderResource = editorResource ? dirname(editorResource) : undefined;320if (editorFolderResource?.path === '.') {321editorFolderResource = undefined;322}323324// Compute folder resource325// Single Root Workspace: always the root single workspace in this case326// Otherwise: root folder of the currently active file if any327let folder: IWorkspaceFolder | undefined = undefined;328if (this.contextService.getWorkbenchState() === WorkbenchState.FOLDER) {329folder = workspace.folders[0];330} else if (editorResource) {331folder = this.contextService.getWorkspaceFolder(editorResource) ?? undefined;332}333334// Compute remote335// vscode-remtoe: use as is336// otherwise figure out if we have a virtual folder opened337let remoteName: string | undefined = undefined;338if (this.environmentService.remoteAuthority && !isWeb) {339remoteName = this.labelService.getHostLabel(Schemas.vscodeRemote, this.environmentService.remoteAuthority);340} else {341const virtualWorkspaceLocation = getVirtualWorkspaceLocation(workspace);342if (virtualWorkspaceLocation) {343remoteName = this.labelService.getHostLabel(virtualWorkspaceLocation.scheme, virtualWorkspaceLocation.authority);344}345}346347// Variables348const activeEditorShort = editor ? editor.getTitle(Verbosity.SHORT) : '';349const activeEditorMedium = editor ? editor.getTitle(Verbosity.MEDIUM) : activeEditorShort;350const activeEditorLong = editor ? editor.getTitle(Verbosity.LONG) : activeEditorMedium;351const activeFolderShort = editorFolderResource ? basename(editorFolderResource) : '';352const activeFolderMedium = editorFolderResource ? this.labelService.getUriLabel(editorFolderResource, { relative: true }) : '';353const activeFolderLong = editorFolderResource ? this.labelService.getUriLabel(editorFolderResource) : '';354const rootName = this.labelService.getWorkspaceLabel(workspace);355const rootNameShort = this.labelService.getWorkspaceLabel(workspace, { verbose: LabelVerbosity.SHORT });356const rootPath = root ? this.labelService.getUriLabel(root) : '';357const folderName = folder ? folder.name : '';358const folderPath = folder ? this.labelService.getUriLabel(folder.uri) : '';359const dirty = editor?.isDirty() && !editor.isSaving() ? WindowTitle.TITLE_DIRTY : '';360const appName = this.productService.nameLong;361const profileName = this.userDataProfileService.currentProfile.isDefault ? '' : this.userDataProfileService.currentProfile.name;362const focusedView: string = this.viewsService.getFocusedViewName();363const activeEditorState = editorResource ? this.decorationsService.getDecoration(editorResource, false)?.tooltip : undefined;364const activeEditorLanguageId = this.editorService.activeTextEditorLanguageId;365366const variables: Record<string, string> = {};367for (const [contextKey, name] of this.variables) {368variables[name] = this.contextKeyService.getContextKeyValue(contextKey) ?? '';369}370371let titleTemplate = this.configurationService.getValue<string>(WindowSettingNames.title);372if (typeof titleTemplate !== 'string') {373titleTemplate = defaultWindowTitle;374}375376if (!this.titleIncludesEditorState && this.accessibilityService.isScreenReaderOptimized() && this.configurationService.getValue('accessibility.windowTitleOptimized')) {377titleTemplate += '${separator}${activeEditorState}';378}379380let separator = this.configurationService.getValue<string>(WindowSettingNames.titleSeparator);381if (typeof separator !== 'string') {382separator = defaultWindowTitleSeparator;383}384385return template(titleTemplate, {386...variables,387activeEditorShort,388activeEditorLong,389activeEditorMedium,390activeEditorLanguageId,391activeFolderShort,392activeFolderMedium,393activeFolderLong,394rootName,395rootPath,396rootNameShort,397folderName,398folderPath,399dirty,400appName,401remoteName,402profileName,403focusedView,404activeEditorState,405separator: { label: separator }406});407}408409isCustomTitleFormat(): boolean {410if (this.accessibilityService.isScreenReaderOptimized() || this.titleIncludesEditorState) {411return true;412}413const title = this.configurationService.inspect<string>(WindowSettingNames.title);414const titleSeparator = this.configurationService.inspect<string>(WindowSettingNames.titleSeparator);415416if (isConfigured(title) || isConfigured(titleSeparator)) {417return true;418}419420// Check if the default value is overridden from the configuration registry421const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);422const configurationProperties = configurationRegistry.getConfigurationProperties();423return title.defaultValue !== configurationProperties[WindowSettingNames.title]?.defaultDefaultValue;424}425}426427428