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