未验证 提交 3d81ac21 编写于 作者: B Benjamin Pasero 提交者: GitHub

Leverage notification progress support for progress API (#45958)

* Leverage notification progress support for progress API

fixes #44090

* only allow positive total/worked values

* drop total/worked in favour of percentage

* make sure to dispose CancellationTokenSource after use

* make the cancellation button optional
上级 5a27354d
......@@ -66,13 +66,16 @@ export enum ProgressLocation {
Explorer = 1,
Scm = 3,
Extensions = 5,
Window = 10
Window = 10,
Notification = 15
}
export interface IProgressOptions {
location: ProgressLocation;
title?: string;
tooltip?: string;
source?: string;
total?: number;
cancellable?: boolean;
}
export interface IProgressStep {
......@@ -86,5 +89,5 @@ export interface IProgressService2 {
_serviceBrand: any;
withProgress<P extends Thenable<R>, R=any>(options: IProgressOptions, task: (progress: IProgress<IProgressStep>) => P): P;
withProgress<P extends Thenable<R>, R=any>(options: IProgressOptions, task: (progress: IProgress<IProgressStep>) => P, onDidCancel?: () => void): P;
}
......@@ -5154,9 +5154,18 @@ declare module 'vscode' {
*
* @param task A callback returning a promise. Progress state can be reported with
* the provided [progress](#Progress)-object.
*
* To report discrete progress, use `percentage` to indicate how much work has been completed. Each call with
* a `percentage` value will be summed up and reflected as overall progress until 100% is reached. Note that
* currently only `ProgressLocation.Notification` is capable of showing discrete progress.
*
* To monitor if the operation has been cancelled by the user, use the provided [`CancellationToken`](#CancellationToken).
* Note that currently only `ProgressLocation.Notification` is supporting to show a cancel button to cancel the
* long running operation.
*
* @return The thenable the task-callback returned.
*/
export function withProgress<R>(options: ProgressOptions, task: (progress: Progress<{ message?: string; }>) => Thenable<R>): Thenable<R>;
export function withProgress<R>(options: ProgressOptions, task: (progress: Progress<{ message?: string; percentage?: number }>, token: CancellationToken) => Thenable<R>): Thenable<R>;
/**
* Creates a status bar [item](#StatusBarItem).
......@@ -5393,14 +5402,19 @@ declare module 'vscode' {
/**
* Show progress for the source control viewlet, as overlay for the icon and as progress bar
* inside the viewlet (when visible).
* inside the viewlet (when visible). Neither supports cancellation nor discrete progress.
*/
SourceControl = 1,
/**
* Show progress in the status bar of the editor.
* Show progress in the status bar of the editor. Neither supports cancellation nor discrete progress.
*/
Window = 10
Window = 10,
/**
* Show progress as notifiation with an optional cancel button. Supports to show infinite and discrete progress.
*/
Notification = 15
}
/**
......@@ -5418,6 +5432,14 @@ declare module 'vscode' {
* operation.
*/
title?: string;
/**
* Controls if a cancel button should show to allow the user to
* cancel the long running operation. Note that currently only
* `ProgressLocation.Notification` is supporting to show a cancel
* button.
*/
cancellable?: boolean;
}
/**
......
......@@ -5,7 +5,7 @@
'use strict';
import { IProgressService2, IProgress, IProgressOptions, IProgressStep } from 'vs/platform/progress/common/progress';
import { MainThreadProgressShape, MainContext, IExtHostContext } from '../node/extHost.protocol';
import { MainThreadProgressShape, MainContext, IExtHostContext, ExtHostProgressShape, ExtHostContext } from '../node/extHost.protocol';
import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers';
@extHostNamedCustomer(MainContext.MainThreadProgress)
......@@ -13,11 +13,13 @@ export class MainThreadProgress implements MainThreadProgressShape {
private _progressService: IProgressService2;
private _progress = new Map<number, { resolve: Function, progress: IProgress<IProgressStep> }>();
private _proxy: ExtHostProgressShape;
constructor(
extHostContext: IExtHostContext,
@IProgressService2 progressService: IProgressService2
) {
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostProgress);
this._progressService = progressService;
}
......@@ -28,7 +30,8 @@ export class MainThreadProgress implements MainThreadProgressShape {
$startProgress(handle: number, options: IProgressOptions): void {
const task = this._createTask(handle);
this._progressService.withProgress(options, task);
this._progressService.withProgress(options, task, () => this._proxy.$acceptProgressCanceled(handle));
}
$progressReport(handle: number, message: IProgressStep): void {
......
......@@ -118,6 +118,7 @@ export function createApiFactory(
const extHostTask = rpcProtocol.set(ExtHostContext.ExtHostTask, new ExtHostTask(rpcProtocol, extHostWorkspace));
const extHostWindow = rpcProtocol.set(ExtHostContext.ExtHostWindow, new ExtHostWindow(rpcProtocol));
rpcProtocol.set(ExtHostContext.ExtHostExtensionService, extensionService);
const extHostProgress = rpcProtocol.set(ExtHostContext.ExtHostProgress, new ExtHostProgress(rpcProtocol.getProxy(MainContext.MainThreadProgress)));
// Check that no named customers are missing
const expected: ProxyIdentifier<any>[] = Object.keys(ExtHostContext).map((key) => ExtHostContext[key]);
......@@ -127,7 +128,6 @@ export function createApiFactory(
const extHostMessageService = new ExtHostMessageService(rpcProtocol);
const extHostDialogs = new ExtHostDialogs(rpcProtocol);
const extHostStatusBar = new ExtHostStatusBar(rpcProtocol);
const extHostProgress = new ExtHostProgress(rpcProtocol.getProxy(MainContext.MainThreadProgress));
const extHostOutputService = new ExtHostOutputService(rpcProtocol);
const extHostLanguages = new ExtHostLanguages(rpcProtocol);
......@@ -391,7 +391,7 @@ export function createApiFactory(
console.warn(`[Deprecation Warning] function 'withScmProgress' is deprecated and should no longer be used. Use 'withProgress' instead.`);
return extHostProgress.withProgress(extension, { location: extHostTypes.ProgressLocation.SourceControl }, (progress, token) => task({ report(n: number) { /*noop*/ } }));
},
withProgress<R>(options: vscode.ProgressOptions, task: (progress: vscode.Progress<{ message?: string; percentage?: number }>) => Thenable<R>) {
withProgress<R>(options: vscode.ProgressOptions, task: (progress: vscode.Progress<{ message?: string; worked?: number }>, token: vscode.CancellationToken) => Thenable<R>) {
return extHostProgress.withProgress(extension, options, task);
},
createOutputChannel(name: string): vscode.OutputChannel {
......
......@@ -809,6 +809,10 @@ export interface ExtHostLogServiceShape {
$setLevel(level: LogLevel);
}
export interface ExtHostProgressShape {
$acceptProgressCanceled(handle: number): void;
}
// --- proxy identifiers
export const MainContext = {
......@@ -866,5 +870,6 @@ export const ExtHostContext = {
ExtHostTask: createExtId<ExtHostTaskShape>('ExtHostTask'),
ExtHostWorkspace: createExtId<ExtHostWorkspaceShape>('ExtHostWorkspace'),
ExtHostWindow: createExtId<ExtHostWindowShape>('ExtHostWindow'),
ExtHostWebviews: createExtId<ExtHostWebviewsShape>('ExtHostWebviews')
ExtHostWebviews: createExtId<ExtHostWebviewsShape>('ExtHostWebviews'),
ExtHostProgress: createMainId<ExtHostProgressShape>('ExtHostProgress')
};
......@@ -4,16 +4,19 @@
*--------------------------------------------------------------------------------------------*/
'use strict';
import { Progress, ProgressOptions, CancellationToken } from 'vscode';
import { MainThreadProgressShape } from './extHost.protocol';
import { Progress, ProgressOptions } from 'vscode';
import { MainThreadProgressShape, ExtHostProgressShape } from './extHost.protocol';
import { ProgressLocation } from './extHostTypeConverters';
import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions';
import { IProgressStep } from 'vs/platform/progress/common/progress';
import { localize } from 'vs/nls';
import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation';
export class ExtHostProgress {
export class ExtHostProgress implements ExtHostProgressShape {
private _proxy: MainThreadProgressShape;
private _handles: number = 0;
private _mapHandleToCancellationSource: Map<number, CancellationTokenSource> = new Map();
constructor(proxy: MainThreadProgressShape) {
this._proxy = proxy;
......@@ -21,12 +24,18 @@ export class ExtHostProgress {
withProgress<R>(extension: IExtensionDescription, options: ProgressOptions, task: (progress: Progress<IProgressStep>, token: CancellationToken) => Thenable<R>): Thenable<R> {
const handle = this._handles++;
const { title, location } = options;
this._proxy.$startProgress(handle, { location: ProgressLocation.from(location), title, tooltip: extension.name });
return this._withProgress(handle, task);
const { title, location, cancellable } = options;
const source = localize('extensionSource', "{0} (Extension)", extension.displayName || extension.name);
this._proxy.$startProgress(handle, { location: ProgressLocation.from(location), title, source, cancellable });
return this._withProgress(handle, task, cancellable);
}
private _withProgress<R>(handle: number, task: (progress: Progress<IProgressStep>, token: CancellationToken) => Thenable<R>): Thenable<R> {
private _withProgress<R>(handle: number, task: (progress: Progress<IProgressStep>, token: CancellationToken) => Thenable<R>, cancellable: boolean): Thenable<R> {
let source: CancellationTokenSource;
if (cancellable) {
source = new CancellationTokenSource();
this._mapHandleToCancellationSource.set(handle, source);
}
const progress = {
report: (p: IProgressStep) => {
......@@ -34,17 +43,33 @@ export class ExtHostProgress {
}
};
const progressEnd = (handle: number): void => {
this._proxy.$progressEnd(handle);
this._mapHandleToCancellationSource.delete(handle);
if (source) {
source.dispose();
}
};
let p: Thenable<R>;
try {
p = task(progress, null);
p = task(progress, cancellable ? source.token : CancellationToken.None);
} catch (err) {
this._proxy.$progressEnd(handle);
progressEnd(handle);
throw err;
}
p.then(result => this._proxy.$progressEnd(handle), err => this._proxy.$progressEnd(handle));
p.then(result => progressEnd(handle), err => progressEnd(handle));
return p;
}
public $acceptProgressCanceled(handle: number): void {
const source = this._mapHandleToCancellationSource.get(handle);
if (source) {
source.cancel();
this._mapHandleToCancellationSource.delete(handle);
}
}
}
......@@ -606,6 +606,7 @@ export namespace ProgressLocation {
switch (loc) {
case types.ProgressLocation.SourceControl: return MainProgressLocation.Scm;
case types.ProgressLocation.Window: return MainProgressLocation.Window;
case types.ProgressLocation.Notification: return MainProgressLocation.Notification;
}
return undefined;
}
......
......@@ -1616,6 +1616,7 @@ export class Task implements vscode.Task {
export enum ProgressLocation {
SourceControl = 1,
Window = 10,
Notification = 15
}
export class TreeItem {
......
......@@ -585,7 +585,7 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService,
return this.progressService.withProgress({
location: ProgressLocation.Extensions,
title: nls.localize('installingVSIXExtension', 'Installing extension from VSIX...'),
tooltip: `${extension}`
source: `${extension}`
}, () => this.extensionService.install(extension).then(() => null));
}
......@@ -607,7 +607,7 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService,
return this.progressService.withProgress({
location: ProgressLocation.Extensions,
title: nls.localize('installingMarketPlaceExtension', 'Installing extension from Marketplace....'),
tooltip: `${extension.id}`
source: `${extension.id}`
}, () => this.extensionService.installFromGallery(gallery).then(() => null));
}
......@@ -648,7 +648,7 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService,
return this.progressService.withProgress({
location: ProgressLocation.Extensions,
title: nls.localize('uninstallingExtension', 'Uninstalling extension....'),
tooltip: `${local.identifier.id}`
source: `${local.identifier.id}`
}, () => this.extensionService.uninstall(local));
}
......@@ -666,7 +666,7 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService,
return this.progressService.withProgress({
location: ProgressLocation.Extensions,
tooltip: `${local.identifier.id}`
source: `${local.identifier.id}`
}, () => this.extensionService.reinstallFromGallery(local).then(() => null));
}
......
......@@ -16,6 +16,9 @@ import { StatusbarAlignment, IStatusbarRegistry, StatusbarItemDescriptor, Extens
import { TPromise } from 'vs/base/common/winjs.base';
import { always } from 'vs/base/common/async';
import { ProgressBadge, IActivityService } from 'vs/workbench/services/activity/common/activity';
import { INotificationService, Severity, INotificationHandle, INotificationActions } from 'vs/platform/notification/common/notification';
import { Action } from 'vs/base/common/actions';
import { once } from 'vs/base/common/event';
class WindowProgressItem implements IStatusbarItem {
......@@ -78,15 +81,18 @@ export class ProgressService2 implements IProgressService2 {
constructor(
@IActivityService private readonly _activityBar: IActivityService,
@IViewletService private readonly _viewletService: IViewletService
@IViewletService private readonly _viewletService: IViewletService,
@INotificationService private readonly _notificationService: INotificationService
) {
//
}
withProgress<P extends Thenable<R>, R=any>(options: IProgressOptions, task: (progress: IProgress<IProgressStep>) => P): P {
withProgress<P extends Thenable<R>, R=any>(options: IProgressOptions, task: (progress: IProgress<IProgressStep>) => P, onDidCancel?: () => void): P {
const { location } = options;
switch (location) {
case ProgressLocation.Notification:
return this._withNotificationProgress(options, task, onDidCancel);
case ProgressLocation.Window:
return this._withWindowProgress(options, task);
case ProgressLocation.Explorer:
......@@ -101,7 +107,7 @@ export class ProgressService2 implements IProgressService2 {
}
}
private _withWindowProgress<P extends Thenable<R>, R=any>(options: IProgressOptions, callback: (progress: IProgress<{ message?: string, percentage?: number }>) => P): P {
private _withWindowProgress<P extends Thenable<R>, R=any>(options: IProgressOptions, callback: (progress: IProgress<{ message?: string }>) => P): P {
const task: [IProgressOptions, Progress<IProgressStep>] = [options, new Progress<IProgressStep>(() => this._updateWindowProgress())];
......@@ -150,8 +156,8 @@ export class ProgressService2 implements IProgressService2 {
if (options.title && options.title !== title) {
title = localize('progress.subtitle', "{0} - {1}", options.title, title);
}
if (options.tooltip) {
title = localize('progress.title', "{0}: {1}", options.tooltip, title);
if (options.source) {
title = localize('progress.title', "{0}: {1}", options.source, title);
}
WindowProgressItem.Instance.text = text;
......@@ -160,7 +166,95 @@ export class ProgressService2 implements IProgressService2 {
}
}
private _withViewletProgress<P extends Thenable<R>, R=any>(viewletId: string, task: (progress: IProgress<{ message?: string, percentage?: number }>) => P): P {
private _withNotificationProgress<P extends Thenable<R>, R=any>(options: IProgressOptions, callback: (progress: IProgress<{ message?: string, percentage?: number }>) => P, onDidCancel?: () => void): P {
const toDispose: IDisposable[] = [];
const createNotification = (message: string, percentage?: number): INotificationHandle => {
if (!message) {
return undefined; // we need a message at least
}
const actions: INotificationActions = { primary: [] };
if (options.cancellable) {
const cancelAction = new class extends Action {
constructor() {
super('progress.cancel', localize('cancel', "Cancel"), null, true);
}
run(): TPromise<any> {
if (typeof onDidCancel === 'function') {
onDidCancel();
}
return TPromise.as(undefined);
}
};
toDispose.push(cancelAction);
actions.primary.push(cancelAction);
}
const handle = this._notificationService.notify({
severity: Severity.Info,
message: options.title,
source: options.source,
actions
});
updateProgress(handle, percentage);
once(handle.onDidDispose)(() => {
dispose(toDispose);
});
return handle;
};
const updateProgress = (notification: INotificationHandle, percentage?: number): void => {
if (typeof percentage === 'number' && percentage > 0) {
notification.progress.total(100); // always percentage based
notification.progress.worked(percentage);
} else {
notification.progress.infinite();
}
};
let handle: INotificationHandle;
const updateNotification = (message?: string, percentage?: number): void => {
if (!handle) {
handle = createNotification(message, percentage);
} else {
if (typeof message === 'string') {
handle.updateMessage(message);
}
if (typeof percentage === 'number') {
updateProgress(handle, percentage);
}
}
};
// Show initially
updateNotification(options.title);
// Update based on progress
const p = callback({
report: progress => {
updateNotification(progress.message, progress.percentage);
}
});
// Show progress for at least 800ms and then hide once done or canceled
always(TPromise.join([TPromise.timeout(800), p]), () => {
if (handle) {
handle.dispose();
}
});
return p;
}
private _withViewletProgress<P extends Thenable<R>, R=any>(viewletId: string, task: (progress: IProgress<{ message?: string }>) => P): P {
const promise = task(emptyProgress);
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册