Add CustomDocumentContentChangeEvent

For #77131

- Rename `onDidEdit` to `onDidChange`
- Allow custom editors to fire a `CustomDocumentContentChangeEvent` that only marks the editor as dirty but does not enable undo/redo. The only way for editor to get out of this dirty state is to either save or revert the file
This commit is contained in:
Matt Bierner 2020-04-16 15:43:28 -07:00
parent 90bc36a982
commit 5426f5ff70
6 changed files with 96 additions and 30 deletions

View file

@ -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);

View file

@ -32,14 +32,7 @@ export = new class ApiEventNaming implements eslint.Rule.RuleModule {
['TSTypeAnnotation TSTypeReference Identifier[name="Event"]']: (node: any) => {
const def = (<TSESTree.Identifier>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);
}
};

View file

@ -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<CustomDocumentEditEvent>;
readonly onDidChange: Event<CustomDocumentEditEvent> | Event<CustomDocumentContentChangeEvent>;
/**
* Revert a custom editor to its last saved state.

View file

@ -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<void> {
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<void> {
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 `<!DOCTYPE html>
<html>
@ -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<number> = [];
private _isDirtyFromContentChange = false;
private _ongoingSave?: CancelablePromise<void>;
@ -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<void> {
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;
});

View file

@ -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 {

View file

@ -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';
}