Skip to content

Commit

Permalink
Update custom editor api
Browse files Browse the repository at this point in the history
For microsoft#77131

- Use a class for `CustomDocument` instead of an interface. Extensions can now add their own data to a `CustomDocument` by sublassing

- Renamed `resolveCustomDocument` to `openCustomDocument` and require that extensions return a `CustomDocument`

- Exposed edits on `CustomDocument`

- Made the third parameter of `registerCustomEditorProvider` a generic options bag that takes a `webviewOptions`
  • Loading branch information
mjbvz committed Mar 23, 2020
1 parent 414fc3c commit 579dab3
Show file tree
Hide file tree
Showing 8 changed files with 223 additions and 198 deletions.
4 changes: 2 additions & 2 deletions extensions/image-preview/src/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ export class PreviewManager implements vscode.CustomEditorProvider {
private readonly zoomStatusBarEntry: ZoomStatusBarEntry,
) { }

public async resolveCustomDocument(_document: vscode.CustomDocument): Promise<void> {
// noop
public async openCustomDocument(uri: vscode.Uri) {
return new vscode.CustomDocument(PreviewManager.viewType, uri);
}

public async resolveCustomEditor(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,16 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview

private _activePreview: DynamicMarkdownPreview | undefined = undefined;

private readonly customEditorViewType = 'vscode.markdown.preview.editor';

public constructor(
private readonly _contentProvider: MarkdownContentProvider,
private readonly _logger: Logger,
private readonly _contributions: MarkdownContributionProvider
) {
super();
this._register(vscode.window.registerWebviewPanelSerializer(DynamicMarkdownPreview.viewType, this));
this._register(vscode.window.registerCustomEditorProvider('vscode.markdown.preview.editor', this));
this._register(vscode.window.registerCustomEditorProvider(this.customEditorViewType, this));
}

public refresh() {
Expand Down Expand Up @@ -148,8 +150,8 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
this.registerDynamicPreview(preview);
}

public async resolveCustomDocument(_document: vscode.CustomDocument): Promise<void> {
// noop
public async openCustomDocument(uri: vscode.Uri) {
return new vscode.CustomDocument(this.customEditorViewType, uri);
}

public async resolveCustomTextEditor(
Expand Down
84 changes: 47 additions & 37 deletions src/vs/vscode.proposed.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1289,17 +1289,11 @@ declare module 'vscode' {

//#region Custom editors: https://github.com/microsoft/vscode/issues/77131

// TODO:
// - Think about where a rename would live.
// - Think about handling go to line? (add other editor options? reveal?)
// - Should we expose edits?
// - More properties from `TextDocument`?

/**
* Defines the editing capability of a custom webview editor. This allows the webview editor to hook into standard
* editor events such as `undo` or `save`.
*
* @param EditType Type of edits.
* @param EditType Type of edits used for the documents this delegate handles.
*/
interface CustomEditorEditingDelegate<EditType = unknown> {
/**
Expand All @@ -1310,7 +1304,7 @@ declare module 'vscode' {
*
* @return Thenable signaling that the save has completed.
*/
save(document: CustomDocument, cancellation: CancellationToken): Thenable<void>;
save(document: CustomDocument<EditType>, cancellation: CancellationToken): Thenable<void>;

/**
* Save the existing resource at a new path.
Expand All @@ -1320,7 +1314,7 @@ declare module 'vscode' {
*
* @return Thenable signaling that the save has completed.
*/
saveAs(document: CustomDocument, targetResource: Uri): Thenable<void>;
saveAs(document: CustomDocument<EditType>, targetResource: Uri): Thenable<void>;

/**
* Event triggered by extensions to signal to VS Code that an edit has occurred.
Expand All @@ -1337,7 +1331,7 @@ declare module 'vscode' {
*
* @return Thenable signaling that the change has completed.
*/
applyEdits(document: CustomDocument, edits: readonly EditType[]): Thenable<void>;
applyEdits(document: CustomDocument<EditType>, edits: readonly EditType[]): Thenable<void>;

/**
* Undo a set of edits.
Expand All @@ -1349,7 +1343,7 @@ declare module 'vscode' {
*
* @return Thenable signaling that the change has completed.
*/
undoEdits(document: CustomDocument, edits: readonly EditType[]): Thenable<void>;
undoEdits(document: CustomDocument<EditType>, edits: readonly EditType[]): Thenable<void>;

/**
* Revert the file to its last saved state.
Expand All @@ -1359,7 +1353,7 @@ declare module 'vscode' {
*
* @return Thenable signaling that the change has completed.
*/
revert(document: CustomDocument, edits: CustomDocumentRevert<EditType>): Thenable<void>;
revert(document: CustomDocument<EditType>, edits: CustomDocumentRevert<EditType>): Thenable<void>;

/**
* Back up the resource in its current state.
Expand All @@ -1380,22 +1374,25 @@ declare module 'vscode' {
* in an operation that takes time to complete, your extension may decide to finish the ongoing backup rather
* than cancelling it to ensure that VS Code has some valid backup.
*/
backup(document: CustomDocument, cancellation: CancellationToken): Thenable<void>;
backup(document: CustomDocument<EditType>, cancellation: CancellationToken): Thenable<void>;
}

/**
* Event triggered by extensions to signal to VS Code that an edit has occurred on a CustomDocument``.
* Event triggered by extensions to signal to VS Code that an edit has occurred on a `CustomDocument`.
*
* @param EditType Type of edits used for the document.
*/
interface CustomDocumentEditEvent<EditType = unknown> {
/**
* Document the edit is for.
*/
readonly document: CustomDocument;
readonly document: CustomDocument<EditType>;

/**
* Object that describes the edit.
*
* Edit objects are passed back to your extension in `undoEdits`, `applyEdits`, and `revert`.
* Edit objects are passed back to your extension in `CustomEditorEditingDelegate.undoEdits`,
* `CustomEditorEditingDelegate.applyEdits`, and `CustomEditorEditingDelegate.revert`.
*/
readonly edit: EditType;

Expand Down Expand Up @@ -1423,13 +1420,19 @@ declare module 'vscode' {
/**
* Represents a custom document used by a `CustomEditorProvider`.
*
* Custom documents are only used within a given `CustomEditorProvider`. The lifecycle of a
* `CustomDocument` is managed by VS Code. When no more references remain to a given `CustomDocument`,
* then it is disposed of.
* All custom documents must subclass `CustomDocument`. Custom documents are only used within a given
* `CustomEditorProvider`. The lifecycle of a `CustomDocument` is managed by VS Code. When no more references
* remain to a `CustomDocument`, it is disposed of.
*
* @param UserDataType Type of custom object that extensions can store on the document.
* @param EditType Type of edits used in this document.
*/
interface CustomDocument<UserDataType = unknown> {
class CustomDocument<EditType = unknown> {
/**
* @param viewType The associated uri for this document.
* @param uri The associated viewType for this document.
*/
constructor(viewType: string, uri: Uri);

/**
* The associated viewType for this document.
*/
Expand All @@ -1446,12 +1449,17 @@ declare module 'vscode' {
readonly onDidDispose: Event<void>;

/**
* Custom data that an extension can store on the document.
* List of edits from document open to the document's current state.
*/
userData?: UserDataType;
readonly appliedEdits: ReadonlyArray<EditType>;

// TODO: Should we expose edits here?
// This could be helpful for tracking the life cycle of edits
/**
* List of edits from document open to the document's last saved point.
*
* The save point will be behind `appliedEdits` if the user saves and then continues editing,
* or in front of the last entry in `appliedEdits` if the user saves and then hits undo.
*/
readonly savedEdits: ReadonlyArray<EditType>;
}

/**
Expand All @@ -1463,7 +1471,8 @@ declare module 'vscode' {
* You should use custom text based editors when dealing with binary files or more complex scenarios. For simple text
* based documents, use [`WebviewTextEditorProvider`](#WebviewTextEditorProvider) instead.
*/
export interface CustomEditorProvider {
export interface CustomEditorProvider<EditType = unknown> {

/**
* Resolve the model for a given resource.
*
Expand All @@ -1472,18 +1481,18 @@ declare module 'vscode' {
* If all editors for a given resource are closed, the `CustomDocument` is disposed of. Opening an editor at
* this point will trigger another call to `resolveCustomDocument`.
*
* @param document Document to resolve.
* @param uri Uri of the document to open.
* @param token A cancellation token that indicates the result is no longer needed.
*
* @return The capabilities of the resolved document.
* @return The custom document.
*/
resolveCustomDocument(document: CustomDocument, token: CancellationToken): Thenable<void>; // TODO: rename to open?
openCustomDocument(uri: Uri, token: CancellationToken): Thenable<CustomDocument<EditType>>;

/**
* Resolve a webview editor for a given resource.
*
* This is called when a user first opens a resource for a `CustomTextEditorProvider`, or if they reopen an
* existing editor using this `CustomTextEditorProvider`.
* This is called when a user first opens a resource for a `CustomEditorProvider`, or if they reopen an
* existing editor using this `CustomEditorProvider`.
*
* To resolve a webview editor, the provider must fill in its initial html content and hook up all
* the event listeners it is interested it. The provider can also hold onto the `WebviewPanel` to use later,
Expand All @@ -1495,14 +1504,14 @@ declare module 'vscode' {
*
* @return Thenable indicating that the webview editor has been resolved.
*/
resolveCustomEditor(document: CustomDocument, webviewPanel: WebviewPanel, token: CancellationToken): Thenable<void>;
resolveCustomEditor(document: CustomDocument<EditType>, webviewPanel: WebviewPanel, token: CancellationToken): Thenable<void>;

/**
* Defines the editing capability of a custom webview document.
*
* When not provided, the document is considered readonly.
*/
readonly editingDelegate?: CustomEditorEditingDelegate;
readonly editingDelegate?: CustomEditorEditingDelegate<EditType>;
}

/**
Expand All @@ -1516,6 +1525,7 @@ declare module 'vscode' {
* For binary files or more specialized use cases, see [CustomEditorProvider](#CustomEditorProvider).
*/
export interface CustomTextEditorProvider {

/**
* Resolve a webview editor for a given text resource.
*
Expand Down Expand Up @@ -1549,8 +1559,6 @@ declare module 'vscode' {
* @return Thenable indicating that the webview editor has been moved.
*/
moveCustomTextEditor?(newDocument: TextDocument, existingWebviewPanel: WebviewPanel, token: CancellationToken): Thenable<void>;

// TODO: handlesMove?: boolean;
}

namespace window {
Expand All @@ -1560,14 +1568,16 @@ declare module 'vscode' {
* @param viewType Type of the webview editor provider. This should match the `viewType` from the
* `package.json` contributions.
* @param provider Provider that resolves editors.
* @param webviewOptions Content settings for the webview panels that the provider is given.
* @param options Options for the provider
*
* @return Disposable that unregisters the provider.
*/
export function registerCustomEditorProvider(
viewType: string,
provider: CustomEditorProvider | CustomTextEditorProvider,
webviewOptions?: WebviewPanelOptions, // TODO: move this onto provider?
options?: {
readonly webviewOptions?: WebviewPanelOptions;
}
): Disposable;
}

Expand Down
14 changes: 11 additions & 3 deletions src/vs/workbench/api/browser/mainThreadWebview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -663,13 +663,21 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod
}

const undoneEdit = this._edits[this._currentEditIndex];
await this._proxy.$undo(this._realResource, this.viewType, undoneEdit);
await this._proxy.$undo(this._realResource, this.viewType, undoneEdit, this.getEditState());

this.change(() => {
--this._currentEditIndex;
});
}

private getEditState(): extHostProtocol.CustomDocumentEditState {
return {
allEdits: this._edits,
currentIndex: this._currentEditIndex,
saveIndex: this._savePoint,
};
}

private async redo(): Promise<void> {
if (!this._editable) {
return;
Expand All @@ -681,7 +689,7 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod
}

const redoneEdit = this._edits[this._currentEditIndex + 1];
await this._proxy.$redo(this._realResource, this.viewType, redoneEdit);
await this._proxy.$redo(this._realResource, this.viewType, redoneEdit, this.getEditState());
this.change(() => {
++this._currentEditIndex;
});
Expand Down Expand Up @@ -728,7 +736,7 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod
editsToRedo = this._edits.slice(this._currentEditIndex, this._savePoint);
}

this._proxy.$revert(this._realResource, this.viewType, { undoneEdits: editsToUndo, redoneEdits: editsToRedo });
this._proxy.$revert(this._realResource, this.viewType, { undoneEdits: editsToUndo, redoneEdits: editsToRedo }, this.getEditState());
this.change(() => {
this._currentEditIndex = this._savePoint;
this.spliceEdits();
Expand Down
7 changes: 4 additions & 3 deletions src/vs/workbench/api/common/extHost.api.impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -583,9 +583,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
registerWebviewPanelSerializer: (viewType: string, serializer: vscode.WebviewPanelSerializer) => {
return extHostWebviews.registerWebviewPanelSerializer(extension, viewType, serializer);
},
registerCustomEditorProvider: (viewType: string, provider: vscode.CustomEditorProvider | vscode.CustomTextEditorProvider, options?: vscode.WebviewPanelOptions) => {
registerCustomEditorProvider: (viewType: string, provider: vscode.CustomEditorProvider | vscode.CustomTextEditorProvider, options?: { webviewOptions?: vscode.WebviewPanelOptions }) => {
checkProposedApiEnabled(extension);
return extHostWebviews.registerCustomEditorProvider(extension, viewType, provider, options);
return extHostWebviews.registerCustomEditorProvider(extension, viewType, provider, options?.webviewOptions);
},
registerDecorationProvider(provider: vscode.DecorationProvider) {
checkProposedApiEnabled(extension);
Expand Down Expand Up @@ -1030,7 +1030,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
ColorThemeKind: extHostTypes.ColorThemeKind,
TimelineItem: extHostTypes.TimelineItem,
CellKind: extHostTypes.CellKind,
CellOutputKind: extHostTypes.CellOutputKind
CellOutputKind: extHostTypes.CellOutputKind,
CustomDocument: extHostTypes.CustomDocument,
};
};
}
Expand Down
12 changes: 9 additions & 3 deletions src/vs/workbench/api/common/extHost.protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,12 @@ export interface WebviewPanelViewStateData {
};
}

export interface CustomDocumentEditState {
readonly allEdits: readonly number[];
readonly currentIndex: number;
readonly saveIndex: number;
}

export interface ExtHostWebviewsShape {
$onMessage(handle: WebviewPanelHandle, message: any): void;
$onMissingCsp(handle: WebviewPanelHandle, extensionId: string): void;
Expand All @@ -634,9 +640,9 @@ export interface ExtHostWebviewsShape {
$createWebviewCustomEditorDocument(resource: UriComponents, viewType: string, cancellation: CancellationToken): Promise<{ editable: boolean }>;
$disposeWebviewCustomEditorDocument(resource: UriComponents, viewType: string): Promise<void>;

$undo(resource: UriComponents, viewType: string, editId: number): Promise<void>;
$redo(resource: UriComponents, viewType: string, editId: number): Promise<void>;
$revert(resource: UriComponents, viewType: string, changes: { undoneEdits: number[], redoneEdits: number[] }): Promise<void>;
$undo(resource: UriComponents, viewType: string, editId: number, state: CustomDocumentEditState): Promise<void>;
$redo(resource: UriComponents, viewType: string, editId: number, state: CustomDocumentEditState): Promise<void>;
$revert(resource: UriComponents, viewType: string, changes: { undoneEdits: number[], redoneEdits: number[] }, state: CustomDocumentEditState): Promise<void>;
$disposeEdits(resourceComponents: UriComponents, viewType: string, editIds: number[]): void;

$onSave(resource: UriComponents, viewType: string, cancellation: CancellationToken): Promise<void>;
Expand Down
Loading

0 comments on commit 579dab3

Please sign in to comment.