diff --git a/src/vs/platform/progress/common/progress.ts b/src/vs/platform/progress/common/progress.ts index 11d9b6d18d2..9d5c2002223 100644 --- a/src/vs/platform/progress/common/progress.ts +++ b/src/vs/platform/progress/common/progress.ts @@ -27,11 +27,9 @@ export interface IProgressService { registerProgressLocation(location: string, handle: ICustomProgressLocation): IDisposable; } -export type ICustomProgressLocation = ( - options: IProgressOptions | IProgressDialogOptions | IProgressNotificationOptions | IProgressWindowOptions | IProgressCompositeOptions, - task: (progress: IProgress) => Promise, - onDidCancel?: (choice?: number) => void -) => Promise; +export interface ICustomProgressLocation { + startProgress(): { progress: IProgress, token?: CancellationToken, stop(): void }; +} export interface IProgressIndicator { diff --git a/src/vs/workbench/api/common/extHostProgress.ts b/src/vs/workbench/api/common/extHostProgress.ts index f91fff1f76d..a69bccb90ea 100644 --- a/src/vs/workbench/api/common/extHostProgress.ts +++ b/src/vs/workbench/api/common/extHostProgress.ts @@ -15,9 +15,9 @@ import { onUnexpectedExternalError } from 'vs/base/common/errors'; export class ExtHostProgress implements ExtHostProgressShape { - private _proxy: MainThreadProgressShape; + private readonly _proxy: MainThreadProgressShape; + private readonly _mapHandleToCancellationSource = new Map(); private _handles: number = 0; - private _mapHandleToCancellationSource: Map = new Map(); constructor(proxy: MainThreadProgressShape) { this._proxy = proxy; @@ -42,9 +42,7 @@ export class ExtHostProgress implements ExtHostProgressShape { const progressEnd = (handle: number): void => { this._proxy.$progressEnd(handle); this._mapHandleToCancellationSource.delete(handle); - if (source) { - source.dispose(); - } + source?.dispose(); }; let p: Thenable; @@ -56,7 +54,7 @@ export class ExtHostProgress implements ExtHostProgressShape { throw err; } - p.then(result => progressEnd(handle), err => progressEnd(handle)); + Promise.resolve(p).finally(() => progressEnd(handle)); return p; } diff --git a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts index 41967cf8bf7..08f230531f8 100644 --- a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts +++ b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts @@ -66,8 +66,9 @@ class EditorStatusContribution implements IWorkbenchContribution { _storageService.onDidChangeValue(this._handleStorageChange, this, this._disposables); this._restoreState(); - _languageStatusService.onDidChange(this._update, this, this._disposables); - _editorService.onDidActiveEditorChange(this._update, this, this._disposables); + _editorService.onDidActiveEditorChange(() => this._update(), this, this._disposables); + _languageStatusService.onDidChange(() => this._update(), this, this._disposables); + _languageStatusService.onDidChangeBusy(() => this._update(true), this, this._disposables); this._update(); _statusBarService.onDidChangeEntryVisibility(e => { @@ -136,11 +137,11 @@ class EditorStatusContribution implements IWorkbenchContribution { return new LanguageStatusViewModel(combined, dedicated); } - private _update(): void { + private _update(force?: boolean): void { const model = this._createViewModel(); - if (this._model?.isEqual(model)) { + if (this._model?.isEqual(model) && !force) { return; } @@ -160,18 +161,21 @@ class EditorStatusContribution implements IWorkbenchContribution { const showSeverity = first.severity >= Severity.Warning; const text = EditorStatusContribution._severityToComboCodicon(first.severity); + let isBusy = false; const ariaLabels: string[] = []; const element = document.createElement('div'); for (const status of model.combined) { - element.appendChild(this._renderStatus(status, showSeverity, this._renderDisposables)); + const thisIsBusy = this._languageStatusService.isBusy(status); + element.appendChild(this._renderStatus(status, showSeverity, thisIsBusy, this._renderDisposables)); ariaLabels.push(this._asAriaLabel(status)); + isBusy = isBusy || thisIsBusy; } const props: IStatusbarEntry = { name: localize('langStatus.name', "Editor Language Status"), ariaLabel: localize('langStatus.aria', "Editor Language Status: {0}", ariaLabels.join(', next: ')), tooltip: element, command: ShowTooltipCommand, - text, + text: isBusy ? `${text}\u00A0\u00A0$(loading~spin)` : text, }; if (!this._combinedEntry) { this._combinedEntry = this._statusBarService.addEntry(props, EditorStatusContribution._id, StatusbarAlignment.RIGHT, { id: 'status.editor.mode', alignment: StatusbarAlignment.LEFT, compact: true }); @@ -183,7 +187,7 @@ class EditorStatusContribution implements IWorkbenchContribution { // dedicated status bar items are shows as-is in the status bar const newDedicatedEntries = new Map(); for (const status of model.dedicated) { - const props = EditorStatusContribution._asStatusbarEntry(status); + const props = EditorStatusContribution._asStatusbarEntry(status, this._languageStatusService.isBusy(status)); let entry = this._dedicatedEntries.get(status.id); if (!entry) { entry = this._statusBarService.addEntry(props, status.id, StatusbarAlignment.RIGHT, 100.09999); @@ -197,7 +201,7 @@ class EditorStatusContribution implements IWorkbenchContribution { this._dedicatedEntries = newDedicatedEntries; } - private _renderStatus(status: ILanguageStatus, showSeverity: boolean, store: DisposableStore): HTMLElement { + private _renderStatus(status: ILanguageStatus, showSeverity: boolean, isBusy: boolean, store: DisposableStore): HTMLElement { const parent = document.createElement('div'); parent.classList.add('hover-language-status'); @@ -219,6 +223,9 @@ class EditorStatusContribution implements IWorkbenchContribution { const label = document.createElement('span'); label.classList.add('label'); + if (isBusy) { + dom.append(label, ...renderLabelWithIcons('$(loading~spin)\u00A0\u00A0')); + } dom.append(label, ...renderLabelWithIcons(status.label)); left.appendChild(label); @@ -297,7 +304,7 @@ class EditorStatusContribution implements IWorkbenchContribution { // --- - private static _asStatusbarEntry(item: ILanguageStatus): IStatusbarEntry { + private static _asStatusbarEntry(item: ILanguageStatus, isBusy: boolean): IStatusbarEntry { let color: ThemeColor | undefined; let backgroundColor: ThemeColor | undefined; @@ -311,7 +318,7 @@ class EditorStatusContribution implements IWorkbenchContribution { return { name: localize('name.pattern', '{0} (Language Status)', item.name), - text: item.label, + text: isBusy ? `${item.label}\u00A0\u00A0$(loading~spin)` : item.label, ariaLabel: item.accessibilityInfo?.label ?? item.label, role: item.accessibilityInfo?.role, tooltip: item.command?.tooltip || new MarkdownString(item.detail, true), diff --git a/src/vs/workbench/services/languageStatus/common/languageStatusService.ts b/src/vs/workbench/services/languageStatus/common/languageStatusService.ts index e9899764933..7aaceaf0e01 100644 --- a/src/vs/workbench/services/languageStatus/common/languageStatusService.ts +++ b/src/vs/workbench/services/languageStatus/common/languageStatusService.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from 'vs/base/common/cancellation'; -import { Event } from 'vs/base/common/event'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import { Emitter, Event } from 'vs/base/common/event'; +import { combinedDisposable, IDisposable } from 'vs/base/common/lifecycle'; import Severity from 'vs/base/common/severity'; import { compare } from 'vs/base/common/strings'; import { ITextModel } from 'vs/editor/common/model'; @@ -15,6 +15,7 @@ import { LanguageSelector } from 'vs/editor/common/modes/languageSelector'; import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IProgressService, Progress } from 'vs/platform/progress/common/progress'; export interface ILanguageStatus { readonly id: string; @@ -40,9 +41,13 @@ export interface ILanguageStatusService { onDidChange: Event; + onDidChangeBusy: Event; + addStatus(status: ILanguageStatus): IDisposable; getLanguageStatus(model: ITextModel): ILanguageStatus[]; + + isBusy(status: ILanguageStatus): boolean; } @@ -51,11 +56,47 @@ class LanguageStatusServiceImpl implements ILanguageStatusService { declare _serviceBrand: undefined; private readonly _provider = new LanguageFeatureRegistry(); - readonly onDidChange: Event = this._provider.onDidChange; + private readonly _busyStatus = new Map(); + private readonly _onDidChangeBusy = new Emitter(); + readonly onDidChangeBusy: Event = this._onDidChangeBusy.event; + + constructor(@IProgressService private readonly _progressService: IProgressService) { } + + + isBusy(status: ILanguageStatus): boolean { + return (this._busyStatus.get(status) ?? 0) > 0; + } + addStatus(status: ILanguageStatus): IDisposable { - return this._provider.register(status.selector, status); + const d1 = this._provider.register(status.selector, status); + const d2 = this._progressService.registerProgressLocation(status.id, { + startProgress: () => { + let value = this._busyStatus.get(status); + if (value === undefined) { + this._busyStatus.set(status, 1); + this._onDidChangeBusy.fire(status); + } else { + this._busyStatus.set(status, value + 1); + } + return { + progress: new Progress(_data => { }), + stop: () => { + let value = this._busyStatus.get(status); + if (value !== undefined) { + if (value === 1) { + this._busyStatus.delete(status); + this._onDidChangeBusy.fire(status); + } else { + this._busyStatus.set(status, value - 1); + } + } + } + }; + } + }); + return combinedDisposable(d1, d2); } getLanguageStatus(model: ITextModel): ILanguageStatus[] { diff --git a/src/vs/workbench/services/progress/browser/progressService.ts b/src/vs/workbench/services/progress/browser/progressService.ts index 0089ae4c6f8..51988a7ffbe 100644 --- a/src/vs/workbench/services/progress/browser/progressService.ts +++ b/src/vs/workbench/services/progress/browser/progressService.ts @@ -72,7 +72,10 @@ export class ProgressService implements IProgressService { const customLocation = this.customProgessLocations.get(location); if (customLocation) { - return customLocation(options, task, onDidCancel); + const obj = customLocation.startProgress(); + const promise = task(obj.progress); + promise.finally(() => obj.stop()); + return promise; } throw new Error(`Bad progress location: ${location}`);