Compare commits

...

2 commits

6 changed files with 98 additions and 24 deletions

View file

@ -6,7 +6,7 @@
import { IAction } from 'vs/base/common/actions';
import { DeferredPromise } from 'vs/base/common/async';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
export const IProgressService = createDecorator<IProgressService>('progressService');
@ -23,6 +23,12 @@ export interface IProgressService {
task: (progress: IProgress<IProgressStep>) => Promise<R>,
onDidCancel?: (choice?: number) => void
): Promise<R>;
registerProgressLocation(location: string, handle: ICustomProgressLocation): IDisposable;
}
export interface ICustomProgressLocation {
startProgress(): { progress: IProgress<IProgressStep>, token?: CancellationToken, stop(): void };
}
export interface IProgressIndicator {

View file

@ -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<number, CancellationTokenSource>();
private _handles: number = 0;
private _mapHandleToCancellationSource: Map<number, CancellationTokenSource> = 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<R>;
@ -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;
}

View file

@ -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<string, IStatusbarEntryAccessor>();
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),

View file

@ -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<void>;
onDidChangeBusy: Event<ILanguageStatus>;
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<ILanguageStatus>();
readonly onDidChange: Event<any> = this._provider.onDidChange;
private readonly _busyStatus = new Map<ILanguageStatus, number>();
private readonly _onDidChangeBusy = new Emitter<ILanguageStatus>();
readonly onDidChangeBusy: Event<ILanguageStatus> = 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[] {

View file

@ -7,7 +7,7 @@ import 'vs/css!./media/progressService';
import { localize } from 'vs/nls';
import { IDisposable, dispose, DisposableStore, Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { IProgressService, IProgressOptions, IProgressStep, ProgressLocation, IProgress, Progress, IProgressCompositeOptions, IProgressNotificationOptions, IProgressRunner, IProgressIndicator, IProgressWindowOptions, IProgressDialogOptions } from 'vs/platform/progress/common/progress';
import { IProgressService, IProgressOptions, IProgressStep, ProgressLocation, IProgress, Progress, IProgressCompositeOptions, IProgressNotificationOptions, IProgressRunner, IProgressIndicator, IProgressWindowOptions, IProgressDialogOptions, ICustomProgressLocation } from 'vs/platform/progress/common/progress';
import { StatusbarAlignment, IStatusbarService, IStatusbarEntryAccessor, IStatusbarEntry } from 'vs/workbench/services/statusbar/browser/statusbar';
import { DeferredPromise, RunOnceScheduler, timeout } from 'vs/base/common/async';
import { ProgressBadge, IActivityService } from 'vs/workbench/services/activity/common/activity';
@ -26,10 +26,12 @@ import { parseLinkedText } from 'vs/base/common/linkedText';
import { IViewsService, IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views';
import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite';
export class ProgressService extends Disposable implements IProgressService {
export class ProgressService implements IProgressService {
declare readonly _serviceBrand: undefined;
private readonly customProgessLocations = new Map<string, ICustomProgressLocation>();
constructor(
@IActivityService private readonly activityService: IActivityService,
@IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService,
@ -41,7 +43,15 @@ export class ProgressService extends Disposable implements IProgressService {
@IThemeService private readonly themeService: IThemeService,
@IKeybindingService private readonly keybindingService: IKeybindingService
) {
super();
}
registerProgressLocation(location: string, handle: ICustomProgressLocation): IDisposable {
if (this.customProgessLocations.has(location)) {
throw new Error(`${location} already used`);
}
this.customProgessLocations.set(location, handle);
return toDisposable(() => this.customProgessLocations.delete(location));
}
async withProgress<R = unknown>(options: IProgressOptions, task: (progress: IProgress<IProgressStep>) => Promise<R>, onDidCancel?: (choice?: number) => void): Promise<R> {
@ -60,6 +70,14 @@ export class ProgressService extends Disposable implements IProgressService {
return this.withViewProgress(location, task, { ...options, location });
}
const customLocation = this.customProgessLocations.get(location);
if (customLocation) {
const obj = customLocation.startProgress();
const promise = task(obj.progress);
promise.finally(() => obj.stop());
return promise;
}
throw new Error(`Bad progress location: ${location}`);
}

View file

@ -441,6 +441,10 @@ export class TestProgressService implements IProgressService {
declare readonly _serviceBrand: undefined;
registerProgressLocation(): IDisposable {
return Disposable.None;
}
withProgress(
options: IProgressOptions | IProgressDialogOptions | IProgressWindowOptions | IProgressNotificationOptions | IProgressCompositeOptions,
task: (progress: IProgress<IProgressStep>) => Promise<any>,