Skip to content

Commit b05778e

Browse files
lszomorubpasero
andauthored
Workbench - ability to contribute window title variables (microsoft#204538)
--------- Co-authored-by: Benjamin Pasero <benjamin.pasero@microsoft.com>
1 parent c4a1ed8 commit b05778e

File tree

5 files changed

+115
-8
lines changed

5 files changed

+115
-8
lines changed

extensions/configuration-editing/src/settingsDocumentHelper.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ export class SettingsDocument {
120120
completions.push(this.newSimpleCompletionItem(getText('remoteName'), range, vscode.l10n.t("e.g. SSH")));
121121
completions.push(this.newSimpleCompletionItem(getText('dirty'), range, vscode.l10n.t("an indicator for when the active editor has unsaved changes")));
122122
completions.push(this.newSimpleCompletionItem(getText('separator'), range, vscode.l10n.t("a conditional separator (' - ') that only shows when surrounded by variables with values")));
123+
completions.push(this.newSimpleCompletionItem(getText('activeRepositoryName'), range, vscode.l10n.t("the name of the active repository (e.g. vscode)")));
124+
completions.push(this.newSimpleCompletionItem(getText('activeRepositoryBranchName'), range, vscode.l10n.t("the name of the active branch in the active repository (e.g. main)")));
123125

124126
return completions;
125127
}

src/vs/workbench/browser/parts/titlebar/titlebarPart.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ import { mainWindow } from 'vs/base/browser/window';
5555
import { ACCOUNTS_ACTIVITY_TILE_ACTION, GLOBAL_ACTIVITY_TITLE_ACTION } from 'vs/workbench/browser/parts/titlebar/titlebarActions';
5656
import { IView } from 'vs/base/browser/ui/grid/grid';
5757

58+
export interface ITitleVariable {
59+
readonly name: string;
60+
readonly contextKey: string;
61+
}
62+
5863
export interface ITitleProperties {
5964
isPure?: boolean;
6065
isAdmin?: boolean;
@@ -72,6 +77,11 @@ export interface ITitlebarPart extends IDisposable {
7277
* Update some environmental title properties.
7378
*/
7479
updateProperties(properties: ITitleProperties): void;
80+
81+
/**
82+
* Adds variables to be supported in the window title.
83+
*/
84+
registerVariables(variables: ITitleVariable[]): void;
7585
}
7686

7787
export class BrowserTitleService extends MultiWindowParts<BrowserTitlebarPart> implements ITitleService {
@@ -134,6 +144,14 @@ export class BrowserTitleService extends MultiWindowParts<BrowserTitlebarPart> i
134144
disposables.add(Event.runAndSubscribe(titlebarPart.onDidChange, () => titlebarPartContainer.style.height = `${titlebarPart.height}px`));
135145
titlebarPart.create(titlebarPartContainer);
136146

147+
if (this.properties) {
148+
titlebarPart.updateProperties(this.properties);
149+
}
150+
151+
if (this.variables.length) {
152+
titlebarPart.registerVariables(this.variables);
153+
}
154+
137155
Event.once(titlebarPart.onWillDispose)(() => disposables.dispose());
138156

139157
return titlebarPart;
@@ -150,12 +168,26 @@ export class BrowserTitleService extends MultiWindowParts<BrowserTitlebarPart> i
150168

151169
readonly onMenubarVisibilityChange = this.mainPart.onMenubarVisibilityChange;
152170

171+
private properties: ITitleProperties | undefined = undefined;
172+
153173
updateProperties(properties: ITitleProperties): void {
174+
this.properties = properties;
175+
154176
for (const part of this.parts) {
155177
part.updateProperties(properties);
156178
}
157179
}
158180

181+
private variables: ITitleVariable[] = [];
182+
183+
registerVariables(variables: ITitleVariable[]): void {
184+
this.variables.push(...variables);
185+
186+
for (const part of this.parts) {
187+
part.registerVariables(variables);
188+
}
189+
}
190+
159191
//#endregion
160192
}
161193

@@ -379,6 +411,10 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart {
379411
this.windowTitle.updateProperties(properties);
380412
}
381413

414+
registerVariables(variables: ITitleVariable[]): void {
415+
this.windowTitle.registerVariables(variables);
416+
}
417+
382418
protected override createContentArea(parent: HTMLElement): HTMLElement {
383419
this.element = parent;
384420
this.rootContainer = append(parent, $('.titlebar-container'));

src/vs/workbench/browser/parts/titlebar/windowTitle.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import { localize } from 'vs/nls';
77
import { dirname, basename } from 'vs/base/common/resources';
8-
import { ITitleProperties } from 'vs/workbench/browser/parts/titlebar/titlebarPart';
8+
import { ITitleProperties, ITitleVariable } from 'vs/workbench/browser/parts/titlebar/titlebarPart';
99
import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration';
1010
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
1111
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
@@ -26,6 +26,7 @@ import { getVirtualWorkspaceLocation } from 'vs/platform/workspace/common/virtua
2626
import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile';
2727
import { IViewsService } from 'vs/workbench/services/views/common/viewsService';
2828
import { ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser';
29+
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
2930

3031
const enum WindowSettingNames {
3132
titleSeparator = 'window.titleSeparator',
@@ -39,6 +40,8 @@ export class WindowTitle extends Disposable {
3940
private static readonly TITLE_DIRTY = '\u25cf ';
4041

4142
private readonly properties: ITitleProperties = { isPure: true, isAdmin: false, prefix: undefined };
43+
private readonly variables = new Map<string /* context key */, string /* name */>();
44+
4245
private readonly activeEditorListeners = this._register(new DisposableStore());
4346
private readonly titleUpdater = this._register(new RunOnceScheduler(() => this.doUpdateTitle(), 0));
4447

@@ -66,6 +69,7 @@ export class WindowTitle extends Disposable {
6669
private readonly targetWindow: Window,
6770
editorGroupsContainer: IEditorGroupsContainer | 'main',
6871
@IConfigurationService protected readonly configurationService: IConfigurationService,
72+
@IContextKeyService private readonly contextKeyService: IContextKeyService,
6973
@IEditorService editorService: IEditorService,
7074
@IBrowserWorkbenchEnvironmentService protected readonly environmentService: IBrowserWorkbenchEnvironmentService,
7175
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
@@ -95,6 +99,11 @@ export class WindowTitle extends Disposable {
9599
this.titleUpdater.schedule();
96100
}
97101
}));
102+
this._register(this.contextKeyService.onDidChangeContext(e => {
103+
if (e.affectsSome(this.variables)) {
104+
this.titleUpdater.schedule();
105+
}
106+
}));
98107
}
99108

100109
private onConfigurationChanged(event: IConfigurationChangeEvent): void {
@@ -223,6 +232,22 @@ export class WindowTitle extends Disposable {
223232
}
224233
}
225234

235+
registerVariables(variables: ITitleVariable[]): void {
236+
let changed = false;
237+
238+
for (const { name, contextKey } of variables) {
239+
if (!this.variables.has(contextKey)) {
240+
this.variables.set(contextKey, name);
241+
242+
changed = true;
243+
}
244+
}
245+
246+
if (changed) {
247+
this.titleUpdater.schedule();
248+
}
249+
}
250+
226251
/**
227252
* Possible template values:
228253
*
@@ -303,6 +328,12 @@ export class WindowTitle extends Disposable {
303328
const titleTemplate = this.configurationService.getValue<string>(WindowSettingNames.title);
304329
const focusedView: string = this.viewsService.getFocusedViewName();
305330

331+
// Variables (contributed)
332+
const contributedVariables: { [key: string]: string } = {};
333+
for (const [contextKey, name] of this.variables) {
334+
contributedVariables[name] = this.contextKeyService.getContextKeyValue(contextKey) ?? '';
335+
}
336+
306337
return template(titleTemplate, {
307338
activeEditorShort,
308339
activeEditorLong,
@@ -320,6 +351,7 @@ export class WindowTitle extends Disposable {
320351
remoteName,
321352
profileName,
322353
focusedView,
354+
...contributedVariables,
323355
separator: { label: separator }
324356
});
325357
}

src/vs/workbench/browser/workbench.contribution.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -611,6 +611,8 @@ const registry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Con
611611
localize('remoteName', "`${remoteName}`: e.g. SSH"),
612612
localize('dirty', "`${dirty}`: an indicator for when the active editor has unsaved changes."),
613613
localize('focusedView', "`${focusedView}`: the name of the view that is currently focused."),
614+
localize('activeRepositoryName', "`${activeRepositoryName}`: the name of the active repository (e.g. vscode)."),
615+
localize('activeRepositoryBranchName', "`${activeRepositoryBranchName}`: the name of the active branch in the active repository (e.g. main)."),
614616
localize('separator', "`${separator}`: a conditional separator (\" - \") that only shows when surrounded by variables with values or static text.")
615617
].join('\n- '); // intentionally concatenated to not produce a string that is too long for translations
616618

src/vs/workbench/contrib/scm/browser/activity.ts

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@ import { Event } from 'vs/base/common/event';
1010
import { VIEW_PANE_ID, ISCMService, ISCMRepository, ISCMViewService } from 'vs/workbench/contrib/scm/common/scm';
1111
import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity';
1212
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
13-
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
13+
import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
1414
import { IStatusbarEntry, IStatusbarService, StatusbarAlignment as MainThreadStatusBarAlignment } from 'vs/workbench/services/statusbar/browser/statusbar';
1515
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
1616
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
1717
import { EditorResourceAccessor } from 'vs/workbench/common/editor';
1818
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
1919
import { Schemas } from 'vs/base/common/network';
2020
import { Iterable } from 'vs/base/common/iterator';
21+
import { ITitleService } from 'vs/workbench/services/title/browser/titleService';
2122

2223
function getCount(repository: ISCMRepository): number {
2324
if (typeof repository.provider.count === 'number') {
@@ -27,10 +28,18 @@ function getCount(repository: ISCMRepository): number {
2728
}
2829
}
2930

31+
const ContextKeys = {
32+
ActiveRepositoryName: new RawContextKey<string>('scmActiveRepositoryName', ''),
33+
ActiveRepositoryBranchName: new RawContextKey<string>('scmActiveRepositoryBranchName', ''),
34+
};
35+
3036
export class SCMStatusController implements IWorkbenchContribution {
3137

38+
private activeRepositoryNameContextKey: IContextKey<string>;
39+
private activeRepositoryBranchNameContextKey: IContextKey<string>;
40+
3241
private statusBarDisposable: IDisposable = Disposable.None;
33-
private focusDisposable: IDisposable = Disposable.None;
42+
private focusDisposables = new DisposableStore();
3443
private focusedRepository: ISCMRepository | undefined = undefined;
3544
private readonly badgeDisposable = new MutableDisposable<IDisposable>();
3645
private readonly disposables = new DisposableStore();
@@ -43,7 +52,9 @@ export class SCMStatusController implements IWorkbenchContribution {
4352
@IActivityService private readonly activityService: IActivityService,
4453
@IEditorService private readonly editorService: IEditorService,
4554
@IConfigurationService private readonly configurationService: IConfigurationService,
46-
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService
55+
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
56+
@IContextKeyService contextKeyService: IContextKeyService,
57+
@ITitleService titleService: ITitleService
4758
) {
4859
this.scmService.onDidAddRepository(this.onDidAddRepository, this, this.disposables);
4960
this.scmService.onDidRemoveRepository(this.onDidRemoveRepository, this, this.disposables);
@@ -55,6 +66,14 @@ export class SCMStatusController implements IWorkbenchContribution {
5566
this.onDidAddRepository(repository);
5667
}
5768

69+
this.activeRepositoryNameContextKey = ContextKeys.ActiveRepositoryName.bindTo(contextKeyService);
70+
this.activeRepositoryBranchNameContextKey = ContextKeys.ActiveRepositoryBranchName.bindTo(contextKeyService);
71+
72+
titleService.registerVariables([
73+
{ name: 'activeRepositoryName', contextKey: ContextKeys.ActiveRepositoryName.key },
74+
{ name: 'activeRepositoryBranchName', contextKey: ContextKeys.ActiveRepositoryBranchName.key, }
75+
]);
76+
5877
this.scmViewService.onDidFocusRepository(this.focusRepository, this, this.disposables);
5978
this.focusRepository(this.scmViewService.focusedRepository);
6079

@@ -125,17 +144,33 @@ export class SCMStatusController implements IWorkbenchContribution {
125144
return;
126145
}
127146

128-
this.focusDisposable.dispose();
147+
this.focusDisposables.clear();
129148
this.focusedRepository = repository;
130149

131-
if (repository && repository.provider.onDidChangeStatusBarCommands) {
132-
this.focusDisposable = repository.provider.onDidChangeStatusBarCommands(() => this.renderStatusBar(repository));
150+
if (repository) {
151+
if (repository.provider.onDidChangeStatusBarCommands) {
152+
this.focusDisposables.add(repository.provider.onDidChangeStatusBarCommands(() => this.renderStatusBar(repository)));
153+
}
154+
155+
this.focusDisposables.add(repository.provider.onDidChangeHistoryProvider(() => {
156+
if (repository.provider.historyProvider) {
157+
this.focusDisposables.add(repository.provider.historyProvider.onDidChangeCurrentHistoryItemGroup(() => this.updateContextKeys(repository)));
158+
}
159+
160+
this.updateContextKeys(repository);
161+
}));
133162
}
134163

164+
this.updateContextKeys(repository);
135165
this.renderStatusBar(repository);
136166
this.renderActivityCount();
137167
}
138168

169+
private updateContextKeys(repository: ISCMRepository | undefined): void {
170+
this.activeRepositoryNameContextKey.set(repository?.provider.name ?? '');
171+
this.activeRepositoryBranchNameContextKey.set(repository?.provider.historyProvider?.currentHistoryItemGroup?.label ?? '');
172+
}
173+
139174
private renderStatusBar(repository: ISCMRepository | undefined): void {
140175
this.statusBarDisposable.dispose();
141176

@@ -204,7 +239,7 @@ export class SCMStatusController implements IWorkbenchContribution {
204239
}
205240

206241
dispose(): void {
207-
this.focusDisposable.dispose();
242+
this.focusDisposables.dispose();
208243
this.statusBarDisposable.dispose();
209244
this.badgeDisposable.dispose();
210245
this.disposables.dispose();

0 commit comments

Comments
 (0)