Workbench - ability to contribute window title variables (#204538)

---------

Co-authored-by: Benjamin Pasero <benjamin.pasero@microsoft.com>
This commit is contained in:
Ladislau Szomoru 2024-02-07 21:41:17 +01:00 committed by GitHub
parent c4a1ed80ec
commit b05778eb90
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 115 additions and 8 deletions

View file

@ -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;
}

View file

@ -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<BrowserTitlebarPart> implements ITitleService {
@ -134,6 +144,14 @@ export class BrowserTitleService extends MultiWindowParts<BrowserTitlebarPart> 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<BrowserTitlebarPart> 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'));

View file

@ -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<string /* context key */, string /* name */>();
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<string>(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 }
});
}

View file

@ -611,6 +611,8 @@ const registry = Registry.as<IConfigurationRegistry>(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

View file

@ -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<string>('scmActiveRepositoryName', ''),
ActiveRepositoryBranchName: new RawContextKey<string>('scmActiveRepositoryBranchName', ''),
};
export class SCMStatusController implements IWorkbenchContribution {
private activeRepositoryNameContextKey: IContextKey<string>;
private activeRepositoryBranchNameContextKey: IContextKey<string>;
private statusBarDisposable: IDisposable = Disposable.None;
private focusDisposable: IDisposable = Disposable.None;
private focusDisposables = new DisposableStore();
private focusedRepository: ISCMRepository | undefined = undefined;
private readonly badgeDisposable = new MutableDisposable<IDisposable>();
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();