From b05778eb907c25ed7546dd73511f2aa14683af84 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 7 Feb 2024 21:41:17 +0100 Subject: [PATCH] Workbench - ability to contribute window title variables (#204538) --------- Co-authored-by: Benjamin Pasero --- .../src/settingsDocumentHelper.ts | 2 + .../browser/parts/titlebar/titlebarPart.ts | 36 ++++++++++++++ .../browser/parts/titlebar/windowTitle.ts | 34 ++++++++++++- .../browser/workbench.contribution.ts | 2 + .../workbench/contrib/scm/browser/activity.ts | 49 ++++++++++++++++--- 5 files changed, 115 insertions(+), 8 deletions(-) diff --git a/extensions/configuration-editing/src/settingsDocumentHelper.ts b/extensions/configuration-editing/src/settingsDocumentHelper.ts index 110494fdb3e..bbf77e6017e 100644 --- a/extensions/configuration-editing/src/settingsDocumentHelper.ts +++ b/extensions/configuration-editing/src/settingsDocumentHelper.ts @@ -120,6 +120,8 @@ export class SettingsDocument { completions.push(this.newSimpleCompletionItem(getText('remoteName'), range, vscode.l10n.t("e.g. SSH"))); completions.push(this.newSimpleCompletionItem(getText('dirty'), range, vscode.l10n.t("an indicator for when the active editor has unsaved changes"))); completions.push(this.newSimpleCompletionItem(getText('separator'), range, vscode.l10n.t("a conditional separator (' - ') that only shows when surrounded by variables with values"))); + completions.push(this.newSimpleCompletionItem(getText('activeRepositoryName'), range, vscode.l10n.t("the name of the active repository (e.g. vscode)"))); + completions.push(this.newSimpleCompletionItem(getText('activeRepositoryBranchName'), range, vscode.l10n.t("the name of the active branch in the active repository (e.g. main)"))); return completions; } diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index 7e481cf928d..cbe98d2e852 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -55,6 +55,11 @@ import { mainWindow } from 'vs/base/browser/window'; import { ACCOUNTS_ACTIVITY_TILE_ACTION, GLOBAL_ACTIVITY_TITLE_ACTION } from 'vs/workbench/browser/parts/titlebar/titlebarActions'; import { IView } from 'vs/base/browser/ui/grid/grid'; +export interface ITitleVariable { + readonly name: string; + readonly contextKey: string; +} + export interface ITitleProperties { isPure?: boolean; isAdmin?: boolean; @@ -72,6 +77,11 @@ export interface ITitlebarPart extends IDisposable { * Update some environmental title properties. */ updateProperties(properties: ITitleProperties): void; + + /** + * Adds variables to be supported in the window title. + */ + registerVariables(variables: ITitleVariable[]): void; } export class BrowserTitleService extends MultiWindowParts implements ITitleService { @@ -134,6 +144,14 @@ export class BrowserTitleService extends MultiWindowParts i disposables.add(Event.runAndSubscribe(titlebarPart.onDidChange, () => titlebarPartContainer.style.height = `${titlebarPart.height}px`)); titlebarPart.create(titlebarPartContainer); + if (this.properties) { + titlebarPart.updateProperties(this.properties); + } + + if (this.variables.length) { + titlebarPart.registerVariables(this.variables); + } + Event.once(titlebarPart.onWillDispose)(() => disposables.dispose()); return titlebarPart; @@ -150,12 +168,26 @@ export class BrowserTitleService extends MultiWindowParts i readonly onMenubarVisibilityChange = this.mainPart.onMenubarVisibilityChange; + private properties: ITitleProperties | undefined = undefined; + updateProperties(properties: ITitleProperties): void { + this.properties = properties; + for (const part of this.parts) { part.updateProperties(properties); } } + private variables: ITitleVariable[] = []; + + registerVariables(variables: ITitleVariable[]): void { + this.variables.push(...variables); + + for (const part of this.parts) { + part.registerVariables(variables); + } + } + //#endregion } @@ -379,6 +411,10 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { this.windowTitle.updateProperties(properties); } + registerVariables(variables: ITitleVariable[]): void { + this.windowTitle.registerVariables(variables); + } + protected override createContentArea(parent: HTMLElement): HTMLElement { this.element = parent; this.rootContainer = append(parent, $('.titlebar-container')); diff --git a/src/vs/workbench/browser/parts/titlebar/windowTitle.ts b/src/vs/workbench/browser/parts/titlebar/windowTitle.ts index fffceb67baf..0628c153307 100644 --- a/src/vs/workbench/browser/parts/titlebar/windowTitle.ts +++ b/src/vs/workbench/browser/parts/titlebar/windowTitle.ts @@ -5,7 +5,7 @@ import { localize } from 'vs/nls'; import { dirname, basename } from 'vs/base/common/resources'; -import { ITitleProperties } from 'vs/workbench/browser/parts/titlebar/titlebarPart'; +import { ITitleProperties, ITitleVariable } from 'vs/workbench/browser/parts/titlebar/titlebarPart'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; @@ -26,6 +26,7 @@ import { getVirtualWorkspaceLocation } from 'vs/platform/workspace/common/virtua import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; import { ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; const enum WindowSettingNames { titleSeparator = 'window.titleSeparator', @@ -39,6 +40,8 @@ export class WindowTitle extends Disposable { private static readonly TITLE_DIRTY = '\u25cf '; private readonly properties: ITitleProperties = { isPure: true, isAdmin: false, prefix: undefined }; + private readonly variables = new Map(); + private readonly activeEditorListeners = this._register(new DisposableStore()); private readonly titleUpdater = this._register(new RunOnceScheduler(() => this.doUpdateTitle(), 0)); @@ -66,6 +69,7 @@ export class WindowTitle extends Disposable { private readonly targetWindow: Window, editorGroupsContainer: IEditorGroupsContainer | 'main', @IConfigurationService protected readonly configurationService: IConfigurationService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, @IEditorService editorService: IEditorService, @IBrowserWorkbenchEnvironmentService protected readonly environmentService: IBrowserWorkbenchEnvironmentService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @@ -95,6 +99,11 @@ export class WindowTitle extends Disposable { this.titleUpdater.schedule(); } })); + this._register(this.contextKeyService.onDidChangeContext(e => { + if (e.affectsSome(this.variables)) { + this.titleUpdater.schedule(); + } + })); } private onConfigurationChanged(event: IConfigurationChangeEvent): void { @@ -223,6 +232,22 @@ export class WindowTitle extends Disposable { } } + registerVariables(variables: ITitleVariable[]): void { + let changed = false; + + for (const { name, contextKey } of variables) { + if (!this.variables.has(contextKey)) { + this.variables.set(contextKey, name); + + changed = true; + } + } + + if (changed) { + this.titleUpdater.schedule(); + } + } + /** * Possible template values: * @@ -303,6 +328,12 @@ export class WindowTitle extends Disposable { const titleTemplate = this.configurationService.getValue(WindowSettingNames.title); const focusedView: string = this.viewsService.getFocusedViewName(); + // Variables (contributed) + const contributedVariables: { [key: string]: string } = {}; + for (const [contextKey, name] of this.variables) { + contributedVariables[name] = this.contextKeyService.getContextKeyValue(contextKey) ?? ''; + } + return template(titleTemplate, { activeEditorShort, activeEditorLong, @@ -320,6 +351,7 @@ export class WindowTitle extends Disposable { remoteName, profileName, focusedView, + ...contributedVariables, separator: { label: separator } }); } diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index d9cfbe24654..f2888621120 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -611,6 +611,8 @@ const registry = Registry.as(ConfigurationExtensions.Con localize('remoteName', "`${remoteName}`: e.g. SSH"), localize('dirty', "`${dirty}`: an indicator for when the active editor has unsaved changes."), localize('focusedView', "`${focusedView}`: the name of the view that is currently focused."), + localize('activeRepositoryName', "`${activeRepositoryName}`: the name of the active repository (e.g. vscode)."), + localize('activeRepositoryBranchName', "`${activeRepositoryBranchName}`: the name of the active branch in the active repository (e.g. main)."), localize('separator', "`${separator}`: a conditional separator (\" - \") that only shows when surrounded by variables with values or static text.") ].join('\n- '); // intentionally concatenated to not produce a string that is too long for translations diff --git a/src/vs/workbench/contrib/scm/browser/activity.ts b/src/vs/workbench/contrib/scm/browser/activity.ts index 68a280a56bc..402df518c99 100644 --- a/src/vs/workbench/contrib/scm/browser/activity.ts +++ b/src/vs/workbench/contrib/scm/browser/activity.ts @@ -10,7 +10,7 @@ import { Event } from 'vs/base/common/event'; import { VIEW_PANE_ID, ISCMService, ISCMRepository, ISCMViewService } from 'vs/workbench/contrib/scm/common/scm'; import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IStatusbarEntry, IStatusbarService, StatusbarAlignment as MainThreadStatusBarAlignment } from 'vs/workbench/services/statusbar/browser/statusbar'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -18,6 +18,7 @@ import { EditorResourceAccessor } from 'vs/workbench/common/editor'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { Schemas } from 'vs/base/common/network'; import { Iterable } from 'vs/base/common/iterator'; +import { ITitleService } from 'vs/workbench/services/title/browser/titleService'; function getCount(repository: ISCMRepository): number { if (typeof repository.provider.count === 'number') { @@ -27,10 +28,18 @@ function getCount(repository: ISCMRepository): number { } } +const ContextKeys = { + ActiveRepositoryName: new RawContextKey('scmActiveRepositoryName', ''), + ActiveRepositoryBranchName: new RawContextKey('scmActiveRepositoryBranchName', ''), +}; + export class SCMStatusController implements IWorkbenchContribution { + private activeRepositoryNameContextKey: IContextKey; + private activeRepositoryBranchNameContextKey: IContextKey; + private statusBarDisposable: IDisposable = Disposable.None; - private focusDisposable: IDisposable = Disposable.None; + private focusDisposables = new DisposableStore(); private focusedRepository: ISCMRepository | undefined = undefined; private readonly badgeDisposable = new MutableDisposable(); private readonly disposables = new DisposableStore(); @@ -43,7 +52,9 @@ export class SCMStatusController implements IWorkbenchContribution { @IActivityService private readonly activityService: IActivityService, @IEditorService private readonly editorService: IEditorService, @IConfigurationService private readonly configurationService: IConfigurationService, - @IUriIdentityService private readonly uriIdentityService: IUriIdentityService + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + @IContextKeyService contextKeyService: IContextKeyService, + @ITitleService titleService: ITitleService ) { this.scmService.onDidAddRepository(this.onDidAddRepository, this, this.disposables); this.scmService.onDidRemoveRepository(this.onDidRemoveRepository, this, this.disposables); @@ -55,6 +66,14 @@ export class SCMStatusController implements IWorkbenchContribution { this.onDidAddRepository(repository); } + this.activeRepositoryNameContextKey = ContextKeys.ActiveRepositoryName.bindTo(contextKeyService); + this.activeRepositoryBranchNameContextKey = ContextKeys.ActiveRepositoryBranchName.bindTo(contextKeyService); + + titleService.registerVariables([ + { name: 'activeRepositoryName', contextKey: ContextKeys.ActiveRepositoryName.key }, + { name: 'activeRepositoryBranchName', contextKey: ContextKeys.ActiveRepositoryBranchName.key, } + ]); + this.scmViewService.onDidFocusRepository(this.focusRepository, this, this.disposables); this.focusRepository(this.scmViewService.focusedRepository); @@ -125,17 +144,33 @@ export class SCMStatusController implements IWorkbenchContribution { return; } - this.focusDisposable.dispose(); + this.focusDisposables.clear(); this.focusedRepository = repository; - if (repository && repository.provider.onDidChangeStatusBarCommands) { - this.focusDisposable = repository.provider.onDidChangeStatusBarCommands(() => this.renderStatusBar(repository)); + if (repository) { + if (repository.provider.onDidChangeStatusBarCommands) { + this.focusDisposables.add(repository.provider.onDidChangeStatusBarCommands(() => this.renderStatusBar(repository))); + } + + this.focusDisposables.add(repository.provider.onDidChangeHistoryProvider(() => { + if (repository.provider.historyProvider) { + this.focusDisposables.add(repository.provider.historyProvider.onDidChangeCurrentHistoryItemGroup(() => this.updateContextKeys(repository))); + } + + this.updateContextKeys(repository); + })); } + this.updateContextKeys(repository); this.renderStatusBar(repository); this.renderActivityCount(); } + private updateContextKeys(repository: ISCMRepository | undefined): void { + this.activeRepositoryNameContextKey.set(repository?.provider.name ?? ''); + this.activeRepositoryBranchNameContextKey.set(repository?.provider.historyProvider?.currentHistoryItemGroup?.label ?? ''); + } + private renderStatusBar(repository: ISCMRepository | undefined): void { this.statusBarDisposable.dispose(); @@ -204,7 +239,7 @@ export class SCMStatusController implements IWorkbenchContribution { } dispose(): void { - this.focusDisposable.dispose(); + this.focusDisposables.dispose(); this.statusBarDisposable.dispose(); this.badgeDisposable.dispose(); this.disposables.dispose();