Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/browser/parts/titlebar/windowTitle.ts
5319 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { localize } from '../../../../nls.js';
7
import { dirname, basename } from '../../../../base/common/resources.js';
8
import { ITitleProperties, ITitleVariable } from './titlebarPart.js';
9
import { IConfigurationService, IConfigurationChangeEvent, isConfigured } from '../../../../platform/configuration/common/configuration.js';
10
import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js';
11
import { Registry } from '../../../../platform/registry/common/platform.js';
12
import { IEditorService } from '../../../services/editor/common/editorService.js';
13
import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';
14
import { EditorResourceAccessor, Verbosity, SideBySideEditor } from '../../../common/editor.js';
15
import { IBrowserWorkbenchEnvironmentService } from '../../../services/environment/browser/environmentService.js';
16
import { IWorkspaceContextService, WorkbenchState, IWorkspaceFolder } from '../../../../platform/workspace/common/workspace.js';
17
import { isWindows, isWeb, isMacintosh, isNative } from '../../../../base/common/platform.js';
18
import { URI } from '../../../../base/common/uri.js';
19
import { trim } from '../../../../base/common/strings.js';
20
import { template } from '../../../../base/common/labels.js';
21
import { ILabelService, Verbosity as LabelVerbosity } from '../../../../platform/label/common/label.js';
22
import { Emitter } from '../../../../base/common/event.js';
23
import { RunOnceScheduler } from '../../../../base/common/async.js';
24
import { IProductService } from '../../../../platform/product/common/productService.js';
25
import { Schemas } from '../../../../base/common/network.js';
26
import { getVirtualWorkspaceLocation } from '../../../../platform/workspace/common/virtualWorkspace.js';
27
import { IUserDataProfileService } from '../../../services/userDataProfile/common/userDataProfile.js';
28
import { IViewsService } from '../../../services/views/common/viewsService.js';
29
import { ICodeEditor, isCodeEditor, isDiffEditor } from '../../../../editor/browser/editorBrowser.js';
30
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
31
import { getWindowById } from '../../../../base/browser/dom.js';
32
import { CodeWindow } from '../../../../base/browser/window.js';
33
import { IDecorationsService } from '../../../services/decorations/common/decorations.js';
34
import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js';
35
36
const enum WindowSettingNames {
37
titleSeparator = 'window.titleSeparator',
38
title = 'window.title',
39
}
40
41
export const defaultWindowTitle = (() => {
42
if (isMacintosh && isNative) {
43
return '${activeEditorShort}${separator}${rootName}${separator}${profileName}'; // macOS has native dirty indicator
44
}
45
46
const base = '${dirty}${activeEditorShort}${separator}${rootName}${separator}${profileName}${separator}${appName}';
47
if (isWeb) {
48
return base + '${separator}${remoteName}'; // Web: always show remote name
49
}
50
51
return base;
52
})();
53
export const defaultWindowTitleSeparator = isMacintosh ? ' \u2014 ' : ' - ';
54
55
export class WindowTitle extends Disposable {
56
57
private static readonly NLS_USER_IS_ADMIN = isWindows ? localize('userIsAdmin', "[Administrator]") : localize('userIsSudo', "[Superuser]");
58
private static readonly NLS_EXTENSION_HOST = localize('devExtensionWindowTitlePrefix', "[Extension Development Host]");
59
private static readonly TITLE_DIRTY = '\u25cf ';
60
61
private readonly properties: ITitleProperties = { isPure: true, isAdmin: false, prefix: undefined };
62
private readonly variables = new Map<string /* context key */, string /* name */>();
63
64
private readonly activeEditorListeners = this._register(new DisposableStore());
65
private readonly titleUpdater = this._register(new RunOnceScheduler(() => this.doUpdateTitle(), 0));
66
67
private readonly onDidChangeEmitter = this._register(new Emitter<void>());
68
readonly onDidChange = this.onDidChangeEmitter.event;
69
70
get value() { return this.title ?? ''; }
71
get workspaceName() { return this.labelService.getWorkspaceLabel(this.contextService.getWorkspace()); }
72
get fileName() {
73
const activeEditor = this.editorService.activeEditor;
74
if (!activeEditor) {
75
return undefined;
76
}
77
const fileName = activeEditor.getTitle(Verbosity.SHORT);
78
const dirty = activeEditor?.isDirty() && !activeEditor.isSaving() ? WindowTitle.TITLE_DIRTY : '';
79
return `${dirty}${fileName}`;
80
}
81
82
private title: string | undefined;
83
84
private titleIncludesFocusedView: boolean = false;
85
private titleIncludesEditorState: boolean = false;
86
87
private readonly windowId: number;
88
89
constructor(
90
targetWindow: CodeWindow,
91
@IConfigurationService protected readonly configurationService: IConfigurationService,
92
@IContextKeyService private readonly contextKeyService: IContextKeyService,
93
@IEditorService private readonly editorService: IEditorService,
94
@IBrowserWorkbenchEnvironmentService protected readonly environmentService: IBrowserWorkbenchEnvironmentService,
95
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
96
@ILabelService private readonly labelService: ILabelService,
97
@IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService,
98
@IProductService private readonly productService: IProductService,
99
@IViewsService private readonly viewsService: IViewsService,
100
@IDecorationsService private readonly decorationsService: IDecorationsService,
101
@IAccessibilityService private readonly accessibilityService: IAccessibilityService
102
) {
103
super();
104
105
this.windowId = targetWindow.vscodeWindowId;
106
107
this.checkTitleVariables();
108
109
this.registerListeners();
110
}
111
112
private registerListeners(): void {
113
this._register(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationChanged(e)));
114
this._register(this.editorService.onDidActiveEditorChange(() => this.onActiveEditorChange()));
115
this._register(this.contextService.onDidChangeWorkspaceFolders(() => this.titleUpdater.schedule()));
116
this._register(this.contextService.onDidChangeWorkbenchState(() => this.titleUpdater.schedule()));
117
this._register(this.contextService.onDidChangeWorkspaceName(() => this.titleUpdater.schedule()));
118
this._register(this.labelService.onDidChangeFormatters(() => this.titleUpdater.schedule()));
119
this._register(this.userDataProfileService.onDidChangeCurrentProfile(() => this.titleUpdater.schedule()));
120
this._register(this.viewsService.onDidChangeFocusedView(() => {
121
if (this.titleIncludesFocusedView) {
122
this.titleUpdater.schedule();
123
}
124
}));
125
this._register(this.contextKeyService.onDidChangeContext(e => {
126
if (e.affectsSome(this.variables)) {
127
this.titleUpdater.schedule();
128
}
129
}));
130
this._register(this.accessibilityService.onDidChangeScreenReaderOptimized(() => this.titleUpdater.schedule()));
131
}
132
133
private onConfigurationChanged(event: IConfigurationChangeEvent): void {
134
const affectsTitleConfiguration = event.affectsConfiguration(WindowSettingNames.title);
135
if (affectsTitleConfiguration) {
136
this.checkTitleVariables();
137
}
138
139
if (affectsTitleConfiguration || event.affectsConfiguration(WindowSettingNames.titleSeparator)) {
140
this.titleUpdater.schedule();
141
}
142
}
143
144
private checkTitleVariables(): void {
145
const titleTemplate = this.configurationService.getValue<unknown>(WindowSettingNames.title);
146
if (typeof titleTemplate === 'string') {
147
this.titleIncludesFocusedView = titleTemplate.includes('${focusedView}');
148
this.titleIncludesEditorState = titleTemplate.includes('${activeEditorState}');
149
}
150
}
151
152
private onActiveEditorChange(): void {
153
154
// Dispose old listeners
155
this.activeEditorListeners.clear();
156
157
// Calculate New Window Title
158
this.titleUpdater.schedule();
159
160
// Apply listener for dirty and label changes
161
const activeEditor = this.editorService.activeEditor;
162
if (activeEditor) {
163
this.activeEditorListeners.add(activeEditor.onDidChangeDirty(() => this.titleUpdater.schedule()));
164
this.activeEditorListeners.add(activeEditor.onDidChangeLabel(() => this.titleUpdater.schedule()));
165
}
166
167
// Apply listeners for tracking focused code editor
168
if (this.titleIncludesFocusedView) {
169
const activeTextEditorControl = this.editorService.activeTextEditorControl;
170
const textEditorControls: ICodeEditor[] = [];
171
if (isCodeEditor(activeTextEditorControl)) {
172
textEditorControls.push(activeTextEditorControl);
173
} else if (isDiffEditor(activeTextEditorControl)) {
174
textEditorControls.push(activeTextEditorControl.getOriginalEditor(), activeTextEditorControl.getModifiedEditor());
175
}
176
177
for (const textEditorControl of textEditorControls) {
178
this.activeEditorListeners.add(textEditorControl.onDidBlurEditorText(() => this.titleUpdater.schedule()));
179
this.activeEditorListeners.add(textEditorControl.onDidFocusEditorText(() => this.titleUpdater.schedule()));
180
}
181
}
182
183
// Apply listener for decorations to track editor state
184
if (this.titleIncludesEditorState) {
185
this.activeEditorListeners.add(this.decorationsService.onDidChangeDecorations(() => this.titleUpdater.schedule()));
186
}
187
}
188
189
private doUpdateTitle(): void {
190
const title = this.getFullWindowTitle();
191
if (title !== this.title) {
192
193
// Always set the native window title to identify us properly to the OS
194
let nativeTitle = title;
195
if (!trim(nativeTitle)) {
196
nativeTitle = this.productService.nameLong;
197
}
198
199
const window = getWindowById(this.windowId, true).window;
200
if (!window.document.title && isMacintosh && nativeTitle === this.productService.nameLong) {
201
// TODO@electron macOS: if we set a window title for
202
// the first time and it matches the one we set in
203
// `windowImpl.ts` somehow the window does not appear
204
// in the "Windows" menu. As such, we set the title
205
// briefly to something different to ensure macOS
206
// recognizes we have a window.
207
// See: https://github.com/microsoft/vscode/issues/191288
208
window.document.title = `${this.productService.nameLong} ${WindowTitle.TITLE_DIRTY}`;
209
}
210
211
window.document.title = nativeTitle;
212
this.title = title;
213
214
this.onDidChangeEmitter.fire();
215
}
216
}
217
218
private getFullWindowTitle(): string {
219
const { prefix, suffix } = this.getTitleDecorations();
220
221
let title = this.getWindowTitle() || this.productService.nameLong;
222
if (prefix) {
223
title = `${prefix} ${title}`;
224
}
225
226
if (suffix) {
227
title = `${title} ${suffix}`;
228
}
229
230
// Replace non-space whitespace
231
return title.replace(/[^\S ]/g, ' ');
232
}
233
234
getTitleDecorations() {
235
let prefix: string | undefined;
236
let suffix: string | undefined;
237
238
if (this.properties.prefix) {
239
prefix = this.properties.prefix;
240
}
241
242
if (this.environmentService.isExtensionDevelopment) {
243
prefix = !prefix
244
? WindowTitle.NLS_EXTENSION_HOST
245
: `${WindowTitle.NLS_EXTENSION_HOST} - ${prefix}`;
246
}
247
248
if (this.properties.isAdmin) {
249
suffix = WindowTitle.NLS_USER_IS_ADMIN;
250
}
251
252
return { prefix, suffix };
253
}
254
255
updateProperties(properties: ITitleProperties): void {
256
const isAdmin = typeof properties.isAdmin === 'boolean' ? properties.isAdmin : this.properties.isAdmin;
257
const isPure = typeof properties.isPure === 'boolean' ? properties.isPure : this.properties.isPure;
258
const prefix = typeof properties.prefix === 'string' ? properties.prefix : this.properties.prefix;
259
260
if (isAdmin !== this.properties.isAdmin || isPure !== this.properties.isPure || prefix !== this.properties.prefix) {
261
this.properties.isAdmin = isAdmin;
262
this.properties.isPure = isPure;
263
this.properties.prefix = prefix;
264
265
this.titleUpdater.schedule();
266
}
267
}
268
269
registerVariables(variables: ITitleVariable[]): void {
270
let changed = false;
271
272
for (const { name, contextKey } of variables) {
273
if (!this.variables.has(contextKey)) {
274
this.variables.set(contextKey, name);
275
276
changed = true;
277
}
278
}
279
280
if (changed) {
281
this.titleUpdater.schedule();
282
}
283
}
284
285
/**
286
* Possible template values:
287
*
288
* {activeEditorLong}: e.g. /Users/Development/myFolder/myFileFolder/myFile.txt
289
* {activeEditorMedium}: e.g. myFolder/myFileFolder/myFile.txt
290
* {activeEditorShort}: e.g. myFile.txt
291
* {activeEditorLanguageId}: e.g. typescript
292
* {activeFolderLong}: e.g. /Users/Development/myFolder/myFileFolder
293
* {activeFolderMedium}: e.g. myFolder/myFileFolder
294
* {activeFolderShort}: e.g. myFileFolder
295
* {rootName}: e.g. myFolder1, myFolder2, myFolder3
296
* {rootPath}: e.g. /Users/Development
297
* {folderName}: e.g. myFolder
298
* {folderPath}: e.g. /Users/Development/myFolder
299
* {appName}: e.g. VS Code
300
* {remoteName}: e.g. SSH
301
* {dirty}: indicator
302
* {focusedView}: e.g. Terminal
303
* {separator}: conditional separator
304
* {activeEditorState}: e.g. Modified
305
*/
306
getWindowTitle(): string {
307
const editor = this.editorService.activeEditor;
308
const workspace = this.contextService.getWorkspace();
309
310
// Compute root
311
let root: URI | undefined;
312
if (workspace.configuration) {
313
root = workspace.configuration;
314
} else if (workspace.folders.length) {
315
root = workspace.folders[0].uri;
316
}
317
318
// Compute active editor folder
319
const editorResource = EditorResourceAccessor.getOriginalUri(editor, { supportSideBySide: SideBySideEditor.PRIMARY });
320
let editorFolderResource = editorResource ? dirname(editorResource) : undefined;
321
if (editorFolderResource?.path === '.') {
322
editorFolderResource = undefined;
323
}
324
325
// Compute folder resource
326
// Single Root Workspace: always the root single workspace in this case
327
// Otherwise: root folder of the currently active file if any
328
let folder: IWorkspaceFolder | undefined = undefined;
329
if (this.contextService.getWorkbenchState() === WorkbenchState.FOLDER) {
330
folder = workspace.folders[0];
331
} else if (editorResource) {
332
folder = this.contextService.getWorkspaceFolder(editorResource) ?? undefined;
333
}
334
335
// Compute remote
336
// vscode-remtoe: use as is
337
// otherwise figure out if we have a virtual folder opened
338
let remoteName: string | undefined = undefined;
339
if (this.environmentService.remoteAuthority && !isWeb) {
340
remoteName = this.labelService.getHostLabel(Schemas.vscodeRemote, this.environmentService.remoteAuthority);
341
} else {
342
const virtualWorkspaceLocation = getVirtualWorkspaceLocation(workspace);
343
if (virtualWorkspaceLocation) {
344
remoteName = this.labelService.getHostLabel(virtualWorkspaceLocation.scheme, virtualWorkspaceLocation.authority);
345
}
346
}
347
348
// Variables
349
const activeEditorShort = editor ? editor.getTitle(Verbosity.SHORT) : '';
350
const activeEditorMedium = editor ? editor.getTitle(Verbosity.MEDIUM) : activeEditorShort;
351
const activeEditorLong = editor ? editor.getTitle(Verbosity.LONG) : activeEditorMedium;
352
const activeFolderShort = editorFolderResource ? basename(editorFolderResource) : '';
353
const activeFolderMedium = editorFolderResource ? this.labelService.getUriLabel(editorFolderResource, { relative: true }) : '';
354
const activeFolderLong = editorFolderResource ? this.labelService.getUriLabel(editorFolderResource) : '';
355
const rootName = this.labelService.getWorkspaceLabel(workspace);
356
const rootNameShort = this.labelService.getWorkspaceLabel(workspace, { verbose: LabelVerbosity.SHORT });
357
const rootPath = root ? this.labelService.getUriLabel(root) : '';
358
const folderName = folder ? folder.name : '';
359
const folderPath = folder ? this.labelService.getUriLabel(folder.uri) : '';
360
const dirty = editor?.isDirty() && !editor.isSaving() ? WindowTitle.TITLE_DIRTY : '';
361
const appName = this.productService.nameLong;
362
const profileName = this.userDataProfileService.currentProfile.isDefault ? '' : this.userDataProfileService.currentProfile.name;
363
const focusedView: string = this.viewsService.getFocusedViewName();
364
const activeEditorState = editorResource ? this.decorationsService.getDecoration(editorResource, false)?.tooltip : undefined;
365
const activeEditorLanguageId = this.editorService.activeTextEditorLanguageId;
366
367
const variables: Record<string, string> = {};
368
for (const [contextKey, name] of this.variables) {
369
variables[name] = this.contextKeyService.getContextKeyValue(contextKey) ?? '';
370
}
371
372
let titleTemplate = this.configurationService.getValue<string>(WindowSettingNames.title);
373
if (typeof titleTemplate !== 'string') {
374
titleTemplate = defaultWindowTitle;
375
}
376
377
if (!this.titleIncludesEditorState && this.accessibilityService.isScreenReaderOptimized() && this.configurationService.getValue('accessibility.windowTitleOptimized')) {
378
titleTemplate += '${separator}${activeEditorState}';
379
}
380
381
let separator = this.configurationService.getValue<string>(WindowSettingNames.titleSeparator);
382
if (typeof separator !== 'string') {
383
separator = defaultWindowTitleSeparator;
384
}
385
386
return template(titleTemplate, {
387
...variables,
388
activeEditorShort,
389
activeEditorLong,
390
activeEditorMedium,
391
activeEditorLanguageId,
392
activeFolderShort,
393
activeFolderMedium,
394
activeFolderLong,
395
rootName,
396
rootPath,
397
rootNameShort,
398
folderName,
399
folderPath,
400
dirty,
401
appName,
402
remoteName,
403
profileName,
404
focusedView,
405
activeEditorState,
406
separator: { label: separator }
407
});
408
}
409
410
isCustomTitleFormat(): boolean {
411
if (this.accessibilityService.isScreenReaderOptimized() || this.titleIncludesEditorState) {
412
return true;
413
}
414
const title = this.configurationService.inspect<string>(WindowSettingNames.title);
415
const titleSeparator = this.configurationService.inspect<string>(WindowSettingNames.titleSeparator);
416
417
if (isConfigured(title) || isConfigured(titleSeparator)) {
418
return true;
419
}
420
421
// Check if the default value is overridden from the configuration registry
422
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
423
const configurationProperties = configurationRegistry.getConfigurationProperties();
424
return title.defaultValue !== configurationProperties[WindowSettingNames.title]?.defaultDefaultValue;
425
}
426
}
427
428