diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index 779a759dcbb..93db9989b4d 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -3557,6 +3557,27 @@ declare namespace vscode { contentChanges: TextDocumentContentChangeEvent[]; } + /** + * Represents reasons why a text document is saved. + */ + export enum TextDocumentSaveReason { + + /** + * Explicitly triggered, e.g. by the user pressing save or by an API call. + */ + Explicit = 1, + + /** + * Automatic after a delay. + */ + Auto = 2, + + /** + * When the editor lost focus. + */ + FocusOut = 3 + } + /** * An event that is fired when a [document](#TextDocument) will be saved. * @@ -3571,6 +3592,11 @@ declare namespace vscode { */ document: vscode.TextDocument; + /** + * The reason why save was triggered. + */ + reason: TextDocumentSaveReason; + /** * Allows to pause the event loop and to apply [pre-save-edits](#TextEdit). * Edits of subsequent calls to this function will be applied in order. The diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index 2615b5447b8..1f0b80ffa12 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -84,6 +84,7 @@ export class ExtHostAPIImplementation { OverviewRulerLane: typeof vscode.OverviewRulerLane; TextEditorRevealType: typeof vscode.TextEditorRevealType; EndOfLine: typeof vscode.EndOfLine; + TextDocumentSaveReason: typeof vscode.TextDocumentSaveReason; TextEditorCursorStyle: typeof vscode.TextEditorCursorStyle; TextEditorSelectionChangeKind: typeof vscode.TextEditorSelectionChangeKind; commands: typeof vscode.commands; @@ -169,6 +170,7 @@ export class ExtHostAPIImplementation { this.EndOfLine = extHostTypes.EndOfLine; this.TextEditorCursorStyle = EditorCommon.TextEditorCursorStyle; this.TextEditorSelectionChangeKind = extHostTypes.TextEditorSelectionChangeKind; + this.TextDocumentSaveReason = extHostTypes.TextDocumentSaveReason; // env namespace let telemetryInfo: ITelemetryInfo; diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 36d24bcd965..63118392664 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -21,7 +21,7 @@ import {Position as EditorPosition} from 'vs/platform/editor/common/editor'; import {IMessage, IExtensionDescription} from 'vs/platform/extensions/common/extensions'; import {StatusbarAlignment as MainThreadStatusBarAlignment} from 'vs/platform/statusbar/common/statusbar'; import {ITelemetryInfo} from 'vs/platform/telemetry/common/telemetry'; -import {ICommandHandlerDescription} from 'vs/platform/commands/common/commands'; +import { ICommandHandlerDescription } from 'vs/platform/commands/common/commands'; import * as editorCommon from 'vs/editor/common/editorCommon'; import * as modes from 'vs/editor/common/modes'; @@ -30,6 +30,7 @@ import {IResourceEdit} from 'vs/editor/common/services/bulkEdit'; import {ConfigurationTarget} from 'vs/workbench/services/configuration/common/configurationEditing'; import {IPickOpenEntry, IPickOptions} from 'vs/workbench/services/quickopen/common/quickOpenService'; +import { SaveReason } from 'vs/workbench/parts/files/common/files'; import {IWorkspaceSymbol} from 'vs/workbench/parts/search/common/search'; import {IApplyEditsOptions, TextEditorRevealType, ITextEditorConfigurationUpdate, IResolvedTextEditorConfiguration, ISelectionChangeEvent} from './mainThreadEditorsTracker'; @@ -233,7 +234,7 @@ export abstract class ExtHostDocumentsShape { } export abstract class ExtHostDocumentSaveParticipantShape { - $participateInSave(resource: URI): TPromise { throw ni(); } + $participateInSave(resource: URI, reason: SaveReason): TPromise { throw ni(); } } export interface ITextEditorAddData { diff --git a/src/vs/workbench/api/node/extHostDocumentSaveParticipant.ts b/src/vs/workbench/api/node/extHostDocumentSaveParticipant.ts index db93e089961..879ff76d236 100644 --- a/src/vs/workbench/api/node/extHostDocumentSaveParticipant.ts +++ b/src/vs/workbench/api/node/extHostDocumentSaveParticipant.ts @@ -12,9 +12,10 @@ import { illegalState } from 'vs/base/common/errors'; import { TPromise } from 'vs/base/common/winjs.base'; import { MainThreadWorkspaceShape, ExtHostDocumentSaveParticipantShape } from 'vs/workbench/api/node/extHost.protocol'; import { TextEdit } from 'vs/workbench/api/node/extHostTypes'; -import { fromRange } from 'vs/workbench/api/node/extHostTypeConverters'; +import { fromRange, TextDocumentSaveReason } from 'vs/workbench/api/node/extHostTypeConverters'; import { IResourceEdit } from 'vs/editor/common/services/bulkEdit'; import { ExtHostDocuments } from 'vs/workbench/api/node/extHostDocuments'; +import { SaveReason } from 'vs/workbench/parts/files/common/files'; declare class WeakMap { // delete(key: K): boolean; @@ -57,7 +58,7 @@ export class ExtHostDocumentSaveParticipant extends ExtHostDocumentSaveParticipa }; } - $participateInSave(resource: URI): TPromise { + $participateInSave(resource: URI, reason: SaveReason): TPromise { const entries = this._callbacks.entries(); let didTimeout = false; @@ -72,21 +73,21 @@ export class ExtHostDocumentSaveParticipant extends ExtHostDocumentSaveParticipa } const document = this._documents.getDocumentData(resource).document; - return this._deliverEventAsyncAndBlameBadListeners(fn, thisArg, document); + return this._deliverEventAsyncAndBlameBadListeners(fn, thisArg, { document, reason: TextDocumentSaveReason.to(reason) }); }; })); return always(promise, () => clearTimeout(didTimeoutHandle)); } - private _deliverEventAsyncAndBlameBadListeners(listener: Function, thisArg: any, document: vscode.TextDocument): TPromise { + private _deliverEventAsyncAndBlameBadListeners(listener: Function, thisArg: any, stubEvent: vscode.TextDocumentWillSaveEvent): TPromise { const errors = this._badListeners.get(listener); if (errors > this._thresholds.errors) { // bad listener - ignore return TPromise.wrap(false); } - return this._deliverEventAsync(listener, thisArg, document).then(() => { + return this._deliverEventAsync(listener, thisArg, stubEvent).then(() => { // don't send result across the wire return true; @@ -104,14 +105,16 @@ export class ExtHostDocumentSaveParticipant extends ExtHostDocumentSaveParticipa }); } - private _deliverEventAsync(listener: Function, thisArg: any, document: vscode.TextDocument): TPromise { + private _deliverEventAsync(listener: Function, thisArg: any, stubEvent: vscode.TextDocumentWillSaveEvent): TPromise { const promises: TPromise[] = []; + const {document, reason} = stubEvent; const {version} = document; const event = Object.freeze({ document, + reason, waitUntil(p: Thenable) { if (Object.isFrozen(promises)) { throw illegalState('waitUntil can not be called async'); diff --git a/src/vs/workbench/api/node/extHostTypeConverters.ts b/src/vs/workbench/api/node/extHostTypeConverters.ts index 1dbc263d4df..01a9145f80c 100644 --- a/src/vs/workbench/api/node/extHostTypeConverters.ts +++ b/src/vs/workbench/api/node/extHostTypeConverters.ts @@ -16,6 +16,7 @@ import {IPosition, ISelection, IRange, IDecorationOptions, ISingleEditOperation} import {IWorkspaceSymbol} from 'vs/workbench/parts/search/common/search'; import * as vscode from 'vscode'; import URI from 'vs/base/common/uri'; +import { SaveReason } from 'vs/workbench/parts/files/common/files'; export interface PositionLike { line: number; @@ -436,3 +437,18 @@ export namespace Command { return result; } } + +export namespace TextDocumentSaveReason { + + export function to(reason: SaveReason): vscode.TextDocumentSaveReason { + switch (reason) { + case SaveReason.AUTO: + return types.TextDocumentSaveReason.Auto; + case SaveReason.EXPLICIT: + return types.TextDocumentSaveReason.Explicit; + case SaveReason.FOCUS_CHANGE: + case SaveReason.WINDOW_CHANGE: + return types.TextDocumentSaveReason.FocusOut; + } + } +} \ No newline at end of file diff --git a/src/vs/workbench/api/node/extHostTypes.ts b/src/vs/workbench/api/node/extHostTypes.ts index 0d606e961dc..8fa4f72f48b 100644 --- a/src/vs/workbench/api/node/extHostTypes.ts +++ b/src/vs/workbench/api/node/extHostTypes.ts @@ -825,6 +825,12 @@ export enum EndOfLine { CRLF = 2 } +export enum TextDocumentSaveReason { + Explicit = 1, + Auto = 2, + FocusOut = 3 +} + export enum TextEditorRevealType { Default = 0, InCenter = 1, diff --git a/src/vs/workbench/api/node/mainThreadSaveParticipant.ts b/src/vs/workbench/api/node/mainThreadSaveParticipant.ts index 22669230b3c..f894fa5daed 100644 --- a/src/vs/workbench/api/node/mainThreadSaveParticipant.ts +++ b/src/vs/workbench/api/node/mainThreadSaveParticipant.ts @@ -20,6 +20,7 @@ import {EditOperationsCommand} from 'vs/editor/contrib/format/common/formatComma import {IConfigurationService} from 'vs/platform/configuration/common/configuration'; import {TextFileEditorModel} from 'vs/workbench/parts/files/common/editors/textFileEditorModel'; import {ExtHostContext, ExtHostDocumentSaveParticipantShape} from './extHost.protocol'; +import { SaveReason } from 'vs/workbench/parts/files/common/files'; class TrimWhitespaceParticipant implements ISaveParticipant { @@ -30,9 +31,9 @@ class TrimWhitespaceParticipant implements ISaveParticipant { // Nothing } - public participate(model: ITextFileEditorModel, env: { isAutoSaved: boolean }): any { + public participate(model: ITextFileEditorModel, env: { reason: SaveReason }): any { if (this.configurationService.lookup('files.trimTrailingWhitespace').value) { - this.doTrimTrailingWhitespace(model.textEditorModel, env.isAutoSaved); + this.doTrimTrailingWhitespace(model.textEditorModel, env.reason === SaveReason.AUTO); } } @@ -84,9 +85,11 @@ class FormatOnSaveParticipant implements ISaveParticipant { // Nothing } - participate(editorModel: ITextFileEditorModel, env: { isAutoSaved: boolean }): TPromise { + participate(editorModel: ITextFileEditorModel, env: { reason: SaveReason }): TPromise { + + if (env.reason === SaveReason.AUTO + || !this._configurationService.lookup('editor.formatOnSave').value) { - if (env.isAutoSaved || !this._configurationService.lookup('editor.formatOnSave').value) { return; } @@ -145,8 +148,8 @@ class ExtHostSaveParticipant implements ISaveParticipant { this._proxy = threadService.get(ExtHostContext.ExtHostDocumentSaveParticipant); } - participate(editorModel: ITextFileEditorModel, env: { isAutoSaved: boolean }): TPromise { - return this._proxy.$participateInSave(editorModel.getResource()); + participate(editorModel: ITextFileEditorModel, env: { reason: SaveReason }): TPromise { + return this._proxy.$participateInSave(editorModel.getResource(), env.reason); } } @@ -169,7 +172,7 @@ export class SaveParticipant implements ISaveParticipant { // Hook into model TextFileEditorModel.setSaveParticipant(this); } - participate(model: ITextFileEditorModel, env: { isAutoSaved: boolean }): TPromise { + participate(model: ITextFileEditorModel, env: { reason: SaveReason }): TPromise { const promiseFactory = this._saveParticipants.map(p => () => { return TPromise.as(p.participate(model, env)).then(undefined, err => { // console.error(err); diff --git a/src/vs/workbench/parts/files/common/editors/textFileEditorModel.ts b/src/vs/workbench/parts/files/common/editors/textFileEditorModel.ts index 5c507f77c5a..ba61631308e 100644 --- a/src/vs/workbench/parts/files/common/editors/textFileEditorModel.ts +++ b/src/vs/workbench/parts/files/common/editors/textFileEditorModel.ts @@ -450,7 +450,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil if (TextFileEditorModel.saveParticipant && !this.lifecycleService.willShutdown) { saveParticipantPromise = TPromise.as(undefined).then(() => { this.blockModelContentChange = true; - return TextFileEditorModel.saveParticipant.participate(this, { isAutoSaved: reason === SaveReason.AUTO }); + return TextFileEditorModel.saveParticipant.participate(this, { reason }); }).then(() => { this.blockModelContentChange = false; return this.versionId; diff --git a/src/vs/workbench/parts/files/common/files.ts b/src/vs/workbench/parts/files/common/files.ts index fa98be4cd76..b25d71ee21a 100644 --- a/src/vs/workbench/parts/files/common/files.ts +++ b/src/vs/workbench/parts/files/common/files.ts @@ -112,7 +112,7 @@ export interface ISaveParticipant { /** * Participate in a save of a model. Allows to change the model before it is being saved to disk. */ - participate(model: ITextFileEditorModel, env: { isAutoSaved: boolean }): TPromise; + participate(model: ITextFileEditorModel, env: { reason: SaveReason }): TPromise; } /** @@ -234,10 +234,10 @@ export enum AutoSaveMode { } export enum SaveReason { - EXPLICIT, - AUTO, - FOCUS_CHANGE, - WINDOW_CHANGE + EXPLICIT = 1, + AUTO = 2, + FOCUS_CHANGE = 3, + WINDOW_CHANGE = 4 } export interface IFileEditorDescriptor extends IEditorDescriptor { diff --git a/src/vs/workbench/test/node/api/extHostDocumentSaveParticipant.test.ts b/src/vs/workbench/test/node/api/extHostDocumentSaveParticipant.test.ts index 464c8da3333..de00812a88f 100644 --- a/src/vs/workbench/test/node/api/extHostDocumentSaveParticipant.test.ts +++ b/src/vs/workbench/test/node/api/extHostDocumentSaveParticipant.test.ts @@ -8,12 +8,13 @@ import * as assert from 'assert'; import URI from 'vs/base/common/uri'; import {TPromise} from 'vs/base/common/winjs.base'; import {ExtHostDocuments} from 'vs/workbench/api/node/extHostDocuments'; -import {TextEdit, Position} from 'vs/workbench/api/node/extHostTypes'; +import {TextDocumentSaveReason, TextEdit, Position} from 'vs/workbench/api/node/extHostTypes'; import {MainThreadWorkspaceShape} from 'vs/workbench/api/node/extHost.protocol'; import {ExtHostDocumentSaveParticipant} from 'vs/workbench/api/node/extHostDocumentSaveParticipant'; import {OneGetThreadService} from './testThreadService'; import * as EditorCommon from 'vs/editor/common/editorCommon'; import {IResourceEdit} from 'vs/editor/common/services/bulkEdit'; +import { SaveReason } from 'vs/workbench/parts/files/common/files'; suite('ExtHostDocumentSaveParticipant', () => { @@ -46,7 +47,7 @@ suite('ExtHostDocumentSaveParticipant', () => { test('no listeners, no problem', () => { const participant = new ExtHostDocumentSaveParticipant(documents, workspace); - return participant.$participateInSave(resource).then(() => assert.ok(true)); + return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => assert.ok(true)); }); test('event delivery', () => { @@ -57,10 +58,11 @@ suite('ExtHostDocumentSaveParticipant', () => { event = e; }); - return participant.$participateInSave(resource).then(() => { + return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => { sub.dispose(); assert.ok(event); + assert.equal(event.reason, TextDocumentSaveReason.Explicit); assert.equal(typeof event.waitUntil, 'function'); }); }); @@ -73,7 +75,7 @@ suite('ExtHostDocumentSaveParticipant', () => { event = e; }); - return participant.$participateInSave(resource).then(() => { + return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => { sub.dispose(); assert.ok(event); @@ -88,7 +90,7 @@ suite('ExtHostDocumentSaveParticipant', () => { throw new Error('💀'); }); - return participant.$participateInSave(resource).then(values => { + return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(values => { sub.dispose(); const [first] = values; @@ -107,7 +109,7 @@ suite('ExtHostDocumentSaveParticipant', () => { event = e; }); - return participant.$participateInSave(resource).then(() => { + return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => { sub1.dispose(); sub2.dispose(); @@ -127,7 +129,7 @@ suite('ExtHostDocumentSaveParticipant', () => { assert.equal(counter++, 1); }); - return participant.$participateInSave(resource).then(() => { + return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => { sub1.dispose(); sub2.dispose(); }); @@ -143,10 +145,10 @@ suite('ExtHostDocumentSaveParticipant', () => { }); return TPromise.join([ - participant.$participateInSave(resource), - participant.$participateInSave(resource), - participant.$participateInSave(resource), - participant.$participateInSave(resource) + participant.$participateInSave(resource, SaveReason.EXPLICIT), + participant.$participateInSave(resource, SaveReason.EXPLICIT), + participant.$participateInSave(resource, SaveReason.EXPLICIT), + participant.$participateInSave(resource, SaveReason.EXPLICIT) ]).then(values => { sub.dispose(); @@ -172,7 +174,7 @@ suite('ExtHostDocumentSaveParticipant', () => { callCount += 1; }); - return participant.$participateInSave(resource).then(values => { + return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(values => { sub1.dispose(); sub2.dispose(); sub3.dispose(); @@ -195,7 +197,7 @@ suite('ExtHostDocumentSaveParticipant', () => { event.waitUntil(TPromise.timeout(10)); }); - return participant.$participateInSave(resource).then(() => { + return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => { sub.dispose(); }); @@ -219,7 +221,7 @@ suite('ExtHostDocumentSaveParticipant', () => { })); }); - return participant.$participateInSave(resource).then(() => { + return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => { sub.dispose(); }); }); @@ -231,7 +233,7 @@ suite('ExtHostDocumentSaveParticipant', () => { event.waitUntil(TPromise.timeout(15)); }); - return participant.$participateInSave(resource).then(values => { + return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(values => { sub.dispose(); const [first] = values; @@ -251,7 +253,7 @@ suite('ExtHostDocumentSaveParticipant', () => { event = e; }); - return participant.$participateInSave(resource).then(() => { + return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => { assert.ok(event); sub1.dispose(); sub2.dispose(); @@ -272,7 +274,7 @@ suite('ExtHostDocumentSaveParticipant', () => { e.waitUntil(TPromise.as([TextEdit.insert(new Position(0, 0), 'bar')])); }); - return participant.$participateInSave(resource).then(() => { + return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => { sub.dispose(); assert.equal(edits.length, 1); @@ -302,7 +304,7 @@ suite('ExtHostDocumentSaveParticipant', () => { e.waitUntil(TPromise.as([TextEdit.insert(new Position(0, 0), 'bar')])); }); - return participant.$participateInSave(resource).then(values => { + return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(values => { sub.dispose(); assert.equal(edits, undefined); @@ -346,7 +348,7 @@ suite('ExtHostDocumentSaveParticipant', () => { e.waitUntil(TPromise.as([TextEdit.insert(new Position(0, 0), 'bar')])); }); - return participant.$participateInSave(resource).then(values => { + return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(values => { sub1.dispose(); sub2.dispose();