diff --git a/build/lib/eslint/vscode-dts-event-naming.js b/build/lib/eslint/vscode-dts-event-naming.js index c93c1818305..388ccf2f804 100644 --- a/build/lib/eslint/vscode-dts-event-naming.js +++ b/build/lib/eslint/vscode-dts-event-naming.js @@ -27,13 +27,7 @@ module.exports = new (_a = class ApiEventNaming { ['TSTypeAnnotation TSTypeReference Identifier[name="Event"]']: (node) => { var _a, _b; const def = (_b = (_a = node.parent) === null || _a === void 0 ? void 0 : _a.parent) === null || _b === void 0 ? void 0 : _b.parent; - let ident; - if ((def === null || def === void 0 ? void 0 : def.type) === experimental_utils_1.AST_NODE_TYPES.Identifier) { - ident = def; - } - else if (((def === null || def === void 0 ? void 0 : def.type) === experimental_utils_1.AST_NODE_TYPES.TSPropertySignature || (def === null || def === void 0 ? void 0 : def.type) === experimental_utils_1.AST_NODE_TYPES.ClassProperty) && def.key.type === experimental_utils_1.AST_NODE_TYPES.Identifier) { - ident = def.key; - } + const ident = this.getIdent(def); if (!ident) { // event on unknown structure... return context.report({ @@ -76,6 +70,18 @@ module.exports = new (_a = class ApiEventNaming { } }; } + getIdent(def) { + if (!def) { + return; + } + if (def.type === experimental_utils_1.AST_NODE_TYPES.Identifier) { + return def; + } + else if ((def.type === experimental_utils_1.AST_NODE_TYPES.TSPropertySignature || def.type === experimental_utils_1.AST_NODE_TYPES.ClassProperty) && def.key.type === experimental_utils_1.AST_NODE_TYPES.Identifier) { + return def.key; + } + return this.getIdent(def.parent); + } }, _a._nameRegExp = /on(Did|Will)([A-Z][a-z]+)([A-Z][a-z]+)?/, _a); diff --git a/build/lib/eslint/vscode-dts-event-naming.ts b/build/lib/eslint/vscode-dts-event-naming.ts index 6543c4586bc..5ed8818fe44 100644 --- a/build/lib/eslint/vscode-dts-event-naming.ts +++ b/build/lib/eslint/vscode-dts-event-naming.ts @@ -32,14 +32,7 @@ export = new class ApiEventNaming implements eslint.Rule.RuleModule { ['TSTypeAnnotation TSTypeReference Identifier[name="Event"]']: (node: any) => { const def = (node).parent?.parent?.parent; - let ident: TSESTree.Identifier | undefined; - - if (def?.type === AST_NODE_TYPES.Identifier) { - ident = def; - - } else if ((def?.type === AST_NODE_TYPES.TSPropertySignature || def?.type === AST_NODE_TYPES.ClassProperty) && def.key.type === AST_NODE_TYPES.Identifier) { - ident = def.key; - } + const ident = this.getIdent(def); if (!ident) { // event on unknown structure... @@ -87,5 +80,19 @@ export = new class ApiEventNaming implements eslint.Rule.RuleModule { } }; } + + private getIdent(def: TSESTree.Node | undefined): TSESTree.Identifier | undefined { + if (!def) { + return; + } + + if (def.type === AST_NODE_TYPES.Identifier) { + return def; + } else if ((def.type === AST_NODE_TYPES.TSPropertySignature || def.type === AST_NODE_TYPES.ClassProperty) && def.key.type === AST_NODE_TYPES.Identifier) { + return def.key; + } + + return this.getIdent(def.parent); + } }; diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 402b886a0a8..4895a8c3646 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -1176,6 +1176,8 @@ declare module 'vscode' { /** * Event triggered by extensions to signal to VS Code that an edit has occurred on an [`EditableCustomDocument`](#EditableCustomDocument). + * + * @see [`EditableCustomDocument.onDidChange`](#EditableCustomDocument.onDidChange). */ interface CustomDocumentEditEvent { /** @@ -1200,6 +1202,16 @@ declare module 'vscode' { readonly label?: string; } + /** + * Event triggered by extensions to signal to VS Code that the content of a [`EditableCustomDocument`](#EditableCustomDocument) + * has changed. + * + * @see [`EditableCustomDocument.onDidChange`](#EditableCustomDocument.onDidChange). + */ + interface CustomDocumentContentChangeEvent { + // marker interface + } + /** * A backup for an [`EditableCustomDocument`](#EditableCustomDocument). */ @@ -1265,10 +1277,20 @@ declare module 'vscode' { * anything from changing some text, to cropping an image, to reordering a list. Your extension is free to * define what an edit is and what data is stored on each edit. * - * Firing this will cause VS Code to mark the editors as being dirty. This also allows the user to then undo and - * redo the edit in the custom editor. + * Firing `onDidChange` causes VS Code to mark the editors as being dirty. This is cleared when the user either + * saves or reverts the file. + * + * Editors that support undo/redo must fire a `CustomDocumentEditEvent` whenever an edit happens. This allows + * users to undo and redo the edit using VS Code's standard VS Code keyboard shortcuts. VS Code will also mark + * the editor as no longer being dirty if the user undoes all edits to the last saved state. + * + * Editors that support editing but cannot use VS Code's standard undo/redo mechanism must fire a `CustomDocumentContentChangeEvent`. + * The only way for a user to clear the dirty state of an editor that does not support undo/redo is to either + * `save` or `revert` the file. + * + * An editor should only ever fire `CustomDocumentEditEvent` events, or only ever fire `CustomDocumentContentChangeEvent` events. */ - readonly onDidEdit: Event; + readonly onDidChange: Event | Event; /** * Revert a custom editor to its last saved state. diff --git a/src/vs/workbench/api/browser/mainThreadWebview.ts b/src/vs/workbench/api/browser/mainThreadWebview.ts index cc4ccd35397..c4e2bd7823b 100644 --- a/src/vs/workbench/api/browser/mainThreadWebview.ts +++ b/src/vs/workbench/api/browser/mainThreadWebview.ts @@ -425,15 +425,15 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma } public async $onDidEdit(resourceComponents: UriComponents, viewType: string, editId: number, label: string | undefined): Promise { - const resource = URI.revive(resourceComponents); - const model = await this._customEditorService.models.get(resource, viewType); - if (!model || !(model instanceof MainThreadCustomEditorModel)) { - throw new Error('Could not find model for webview editor'); - } - + const model = await this.getCustomEditorModel(resourceComponents, viewType); model.pushEdit(editId, label); } + public async $onContentChange(resourceComponents: UriComponents, viewType: string): Promise { + const model = await this.getCustomEditorModel(resourceComponents, viewType); + model.changeContent(); + } + private hookupWebviewEventDelegate(handle: extHostProtocol.WebviewPanelHandle, input: WebviewInput) { const disposables = new DisposableStore(); @@ -540,6 +540,15 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma return this._webviewInputs.getInputForHandle(handle); } + private async getCustomEditorModel(resourceComponents: UriComponents, viewType: string) { + const resource = URI.revive(resourceComponents); + const model = await this._customEditorService.models.get(resource, viewType); + if (!model || !(model instanceof MainThreadCustomEditorModel)) { + throw new Error('Could not find model for webview editor'); + } + return model; + } + private static getWebviewResolvedFailedContent(viewType: string) { return ` @@ -597,11 +606,12 @@ namespace HotExitState { class MainThreadCustomEditorModel extends Disposable implements ICustomEditorModel, IWorkingCopy { private _hotExitState: HotExitState.State = HotExitState.Allowed; - private _fromBackup: boolean = false; + private readonly _fromBackup: boolean = false; private _currentEditIndex: number = -1; private _savePoint: number = -1; private readonly _edits: Array = []; + private _isDirtyFromContentChange = false; private _ongoingSave?: CancelablePromise; @@ -676,6 +686,9 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod } public isDirty(): boolean { + if (this._isDirtyFromContentChange) { + return true; + } if (this._edits.length > 0) { return this._savePoint !== this._currentEditIndex; } @@ -717,6 +730,12 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod }); } + public changeContent() { + this.change(() => { + this._isDirtyFromContentChange = true; + }); + } + private async undo(): Promise { if (!this._editable) { return; @@ -779,12 +798,13 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod return; } - if (this._currentEditIndex === this._savePoint) { + if (this._currentEditIndex === this._savePoint && !this._isDirtyFromContentChange) { return; } this._proxy.$revert(this._editorResource, this.viewType, CancellationToken.None); this.change(() => { + this._isDirtyFromContentChange = false; this._currentEditIndex = this._savePoint; this.spliceEdits(); }); @@ -805,6 +825,7 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod this._ongoingSave = savePromise; this.change(() => { + this._isDirtyFromContentChange = false; this._savePoint = this._currentEditIndex; }); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index bf71fbba84b..98d588e1a0c 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -615,6 +615,7 @@ export interface MainThreadWebviewsShape extends IDisposable { $unregisterEditorProvider(viewType: string): void; $onDidEdit(resource: UriComponents, viewType: string, editId: number, label: string | undefined): void; + $onContentChange(resource: UriComponents, viewType: string): void; } export interface WebviewPanelViewStateData { diff --git a/src/vs/workbench/api/common/extHostWebview.ts b/src/vs/workbench/api/common/extHostWebview.ts index 5e88b452eea..b9812521084 100644 --- a/src/vs/workbench/api/common/extHostWebview.ts +++ b/src/vs/workbench/api/common/extHostWebview.ts @@ -570,9 +570,13 @@ export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape { const documentEntry = this._documents.add(viewType, document); if (this.isEditable(document)) { - document.onDidEdit(e => { - const editId = documentEntry.addEdit(e); - this._proxy.$onDidEdit(document.uri, viewType, editId, e.label); + document.onDidChange(e => { + if (isEditEvent(e)) { + const editId = documentEntry.addEdit(e); + this._proxy.$onDidEdit(document.uri, viewType, editId, e.label); + } else { + this._proxy.$onContentChange(document.uri, viewType); + } }); } @@ -708,7 +712,7 @@ export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape { } private isEditable(document: vscode.CustomDocument): document is vscode.EditableCustomDocument { - return !!(document as vscode.EditableCustomDocument).onDidEdit; + return !!(document as vscode.EditableCustomDocument).onDidChange; } private getEditableCustomDocument(viewType: string, resource: UriComponents): vscode.EditableCustomDocument { @@ -744,3 +748,8 @@ function getDefaultLocalResourceRoots( extension.extensionLocation, ]; } + +function isEditEvent(e: vscode.CustomDocumentContentChangeEvent | vscode.CustomDocumentEditEvent): e is vscode.CustomDocumentEditEvent { + return typeof (e as vscode.CustomDocumentEditEvent).undo === 'function' + && typeof (e as vscode.CustomDocumentEditEvent).redo === 'function'; +}