diff --git a/src/vs/workbench/api/browser/mainThreadSaveParticipant.ts b/src/vs/workbench/api/browser/mainThreadSaveParticipant.ts index 62282e00c5f..ad1cc921d17 100644 --- a/src/vs/workbench/api/browser/mainThreadSaveParticipant.ts +++ b/src/vs/workbench/api/browser/mainThreadSaveParticipant.ts @@ -27,7 +27,7 @@ import { ICommandService, CommandsRegistry } from 'vs/platform/commands/common/c import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; -import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; +import { IProgressService, ProgressLocation, IProgressStep, IProgress } from 'vs/platform/progress/common/progress'; import { extHostCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { ISaveParticipant, IResolvedTextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; @@ -37,6 +37,8 @@ import { IStatusbarService, StatusbarAlignment } from 'vs/workbench/services/sta import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { SettingsEditor2 } from 'vs/workbench/contrib/preferences/browser/settingsEditor2'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { canceled, isPromiseCanceledError } from 'vs/base/common/errors'; export interface ICodeActionsOnSaveOptions { [kind: string]: boolean; @@ -48,8 +50,8 @@ class SaveParticipantError extends Error { } } -export interface ISaveParticipantParticipant extends ISaveParticipant { - // progressMessage: string; +export interface ISaveParticipantParticipant { + participate(model: IResolvedTextFileEditorModel, env: { reason: SaveReason }, progress: IProgress, token: CancellationToken): Promise; } class TrimWhitespaceParticipant implements ISaveParticipantParticipant { @@ -92,7 +94,7 @@ class TrimWhitespaceParticipant implements ISaveParticipantParticipant { return; // Nothing to do } - model.pushEditOperations(prevSelection, ops, (edits) => prevSelection); + model.pushEditOperations(prevSelection, ops, (_edits) => prevSelection); } } @@ -123,7 +125,7 @@ export class FinalNewLineParticipant implements ISaveParticipantParticipant { // Nothing } - async participate(model: IResolvedTextFileEditorModel, env: { reason: SaveReason; }): Promise { + async participate(model: IResolvedTextFileEditorModel, _env: { reason: SaveReason; }): Promise { if (this.configurationService.getValue('files.insertFinalNewline', { overrideIdentifier: model.textEditorModel.getLanguageIdentifier().language, resource: model.resource })) { this.doInsertFinalNewLine(model.textEditorModel); } @@ -209,7 +211,7 @@ export class TrimFinalNewLinesParticipant implements ISaveParticipantParticipant return; } - model.pushEditOperations(prevSelection, [EditOperation.delete(deletionRange)], edits => prevSelection); + model.pushEditOperations(prevSelection, [EditOperation.delete(deletionRange)], _edits => prevSelection); if (editor) { editor.setSelections(prevSelection); @@ -227,7 +229,7 @@ class FormatOnSaveParticipant implements ISaveParticipantParticipant { // Nothing } - async participate(editorModel: IResolvedTextFileEditorModel, env: { reason: SaveReason; }): Promise { + async participate(editorModel: IResolvedTextFileEditorModel, env: { reason: SaveReason; }, progress: IProgress, token: CancellationToken): Promise { const model = editorModel.textEditorModel; const overrides = { overrideIdentifier: model.getLanguageIdentifier().language, resource: model.uri }; @@ -237,9 +239,12 @@ class FormatOnSaveParticipant implements ISaveParticipantParticipant { } return new Promise((resolve, reject) => { - const source = new CancellationTokenSource(); + + progress.report({ message: localize('formatting', "Formatting") }); + + const source = new CancellationTokenSource(token); const editorOrModel = findEditor(model, this._codeEditorService) || model; - const timeout = this._configurationService.getValue('editor.formatOnSaveTimeout', overrides); + const timeout = this._configurationService.getValue('editor.formatOnSaveTimeout', overrides) * 1000; const request = this._instantiationService.invokeFunction(formatDocumentWithSelectedProvider, editorOrModel, FormattingMode.Silent, source.token); setTimeout(() => { @@ -255,7 +260,7 @@ class FormatOnSaveParticipant implements ISaveParticipantParticipant { } } -class CodeActionOnSaveParticipant implements ISaveParticipant { +class CodeActionOnSaveParticipant implements ISaveParticipantParticipant { constructor( @IBulkEditService private readonly _bulkEditService: IBulkEditService, @@ -264,7 +269,7 @@ class CodeActionOnSaveParticipant implements ISaveParticipant { @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { } - async participate(editorModel: IResolvedTextFileEditorModel, env: { reason: SaveReason; }): Promise { + async participate(editorModel: IResolvedTextFileEditorModel, env: { reason: SaveReason; }, progress: IProgress, token: CancellationToken): Promise { if (env.reason === SaveReason.AUTO) { return undefined; } @@ -300,10 +305,12 @@ class CodeActionOnSaveParticipant implements ISaveParticipant { .filter(x => setting[x] === false) .map(x => new CodeActionKind(x)); - const tokenSource = new CancellationTokenSource(); + const tokenSource = new CancellationTokenSource(token); const timeout = this._configurationService.getValue('editor.codeActionsOnSaveTimeout', settingsOverrides); + progress.report({ message: localize('codeaction', "Quick Fixes") }); + return Promise.race([ new Promise((_resolve, reject) => setTimeout(() => { @@ -354,7 +361,7 @@ class ExtHostSaveParticipant implements ISaveParticipantParticipant { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostDocumentSaveParticipant); } - async participate(editorModel: IResolvedTextFileEditorModel, env: { reason: SaveReason; }): Promise { + async participate(editorModel: IResolvedTextFileEditorModel, env: { reason: SaveReason; }, _progress: IProgress, token: CancellationToken): Promise { if (!shouldSynchronizeModel(editorModel.textEditorModel)) { // the model never made it to the extension @@ -363,6 +370,9 @@ class ExtHostSaveParticipant implements ISaveParticipantParticipant { } return new Promise((resolve, reject) => { + + token.onCancellationRequested(() => reject(canceled())); + setTimeout( () => reject(new SaveParticipantError(localize('timeout.onWillSave', "Aborted onWillSaveTextDocument-event after 1750ms"))), 1750 @@ -388,7 +398,8 @@ export class SaveParticipant implements ISaveParticipant { @IInstantiationService instantiationService: IInstantiationService, @IProgressService private readonly _progressService: IProgressService, @IStatusbarService private readonly _statusbarService: IStatusbarService, - @ILogService private readonly _logService: ILogService + @ILogService private readonly _logService: ILogService, + @ILabelService private readonly _labelService: ILabelService, ) { this._saveParticipants = new IdleValue(() => [ instantiationService.createInstance(TrimWhitespaceParticipant), @@ -408,23 +419,41 @@ export class SaveParticipant implements ISaveParticipant { } async participate(model: IResolvedTextFileEditorModel, env: { reason: SaveReason; }): Promise { - return this._progressService.withProgress({ location: ProgressLocation.Window }, async progress => { - progress.report({ message: localize('saveParticipants', "Running Save Participants...") }); + + const cts = new CancellationTokenSource(); + + return this._progressService.withProgress({ + title: localize('saveParticipants', "Running Save Participants for '{0}'", this._labelService.getUriLabel(model.resource, { relative: true })), + location: ProgressLocation.Notification, + cancellable: true, + delay: model.isDirty() ? 3000 : 5000 + }, async progress => { let firstError: SaveParticipantError | undefined; - for (let p of this._saveParticipants.getValue()) { + + if (cts.token.isCancellationRequested) { + break; + } + try { - await p.participate(model, env); + await p.participate(model, env, progress, cts.token); + } catch (err) { - this._logService.warn(err); - firstError = !firstError && err instanceof SaveParticipantError ? err : firstError; + if (!isPromiseCanceledError(err)) { + this._logService.warn(err); + firstError = !firstError && err instanceof SaveParticipantError ? err : firstError; + } } } if (firstError) { this._showParticipantError(firstError); } + + }, () => { + // user cancel + cts.dispose(true); }); }