提交 99d5733e 编写于 作者: M Matt Bierner

New iteration of webview editor API proposal

For #77131

**Motivation**
While our existing webview editor API proposal more or less works, building an editable webview editor is fairly tricky using it! This is especially true for simple text based editors.

It'd also be nice if we could get bi-directional live editing for text files. For example, if I open the same file in a webview editor and in VS Code's normal editor, edits on either side should be reflected in the other. While this can sort of be implemented using the existing API, it has some big limitations

**Overview of changes**
To address these problems, we've decided have two types of webview editors:

- Text based webview editors. These editors used a `TextDocument` as their data model, which considerably simplifies implementing an editable webview. In almost all cases, this should be what you use for text files

- Complex webview editors. This is basically the existing proposed API. This gives extension hooks into all the VS Code events, such as `save`, `undo`, and so on. These should be used for binary files or in very complex text editor cases.

Both editor types now have an explicit model layer based on documents. Text editor use `TextDocument` for this, while custom editors use `WebviewEditorCustomDocument`. This replaces the delegate based approach previously used.
上级 1a6b2ea3
......@@ -669,6 +669,7 @@
"create",
"delete",
"dispose",
"edit",
"end",
"expand",
"hide",
......
......@@ -27,11 +27,15 @@ export class PreviewManager implements vscode.WebviewCustomEditorProvider {
private readonly zoomStatusBarEntry: ZoomStatusBarEntry,
) { }
public async resolveWebviewEditor(
resource: vscode.Uri,
public async provideWebviewCustomEditorDocument(resource: vscode.Uri) {
return vscode.window.createWebviewEditorCustomDocument(PreviewManager.viewType, resource, undefined, {});
}
public async resolveWebviewCustomEditor(
document: vscode.WebviewEditorCustomDocument,
webviewEditor: vscode.WebviewPanel,
): Promise<void> {
const preview = new Preview(this.extensionRoot, resource, webviewEditor, this.sizeStatusBarEntry, this.binarySizeStatusBarEntry, this.zoomStatusBarEntry);
const preview = new Preview(this.extensionRoot, document.uri, webviewEditor, this.sizeStatusBarEntry, this.binarySizeStatusBarEntry, this.zoomStatusBarEntry);
this._previews.add(preview);
this.setActivePreview(preview);
......
......@@ -148,12 +148,16 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
this.registerDynamicPreview(preview);
}
public async resolveWebviewEditor(
resource: vscode.Uri,
public async provideWebviewCustomEditorDocument(resource: vscode.Uri) {
return vscode.window.createWebviewEditorCustomDocument('vscode.markdown.preview.editor', resource, undefined, {});
}
public async resolveWebviewCustomEditor(
document: vscode.WebviewEditorCustomDocument,
webview: vscode.WebviewPanel
): Promise<void> {
const preview = DynamicMarkdownPreview.revive(
{ resource, locked: false, resourceColumn: vscode.ViewColumn.One },
{ resource: document.uri, locked: false, resourceColumn: vscode.ViewColumn.One },
webview,
this._contentProvider,
this._previewConfigurations,
......
......@@ -1187,65 +1187,77 @@ declare module 'vscode' {
//#region Custom editors: https://github.com/microsoft/vscode/issues/77131
// TODO:
// - Naming!
// - Think about where a rename would live.
// - Think about handling go to line?
// - Should we expose edits?
// - More properties from `TextDocument`?
/**
* Defines the editing functionality of a webview editor. This allows the webview editor to hook into standard
* Defines the capabilities of a custom webview editor.
*/
interface WebviewCustomEditorCapabilities {
/**
* Defines the editing capability of a custom webview document.
*
* When not provided, the document is considered readonly.
*/
readonly editing?: WebviewCustomEditorEditingCapability;
}
/**
* 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.
*/
interface WebviewCustomEditorEditingDelegate<EditType> {
interface WebviewCustomEditorEditingCapability<EditType = unknown> {
/**
* Save a resource.
*
* @param resource Resource being saved.
* Save the resource.
*
* @return Thenable signaling that the save has completed.
*/
save(resource: Uri): Thenable<void>;
save(): Thenable<void>;
/**
* Save an existing resource at a new path.
* Save the existing resource at a new path.
*
* @param resource Resource being saved.
* @param targetResource Location to save to.
*
* @return Thenable signaling that the save has completed.
*/
saveAs(resource: Uri, targetResource: Uri): Thenable<void>;
saveAs(targetResource: Uri): Thenable<void>;
/**
* Event triggered by extensions to signal to VS Code that an edit has occurred.
*/
// TODO@matt
// eslint-disable-next-line vscode-dts-event-naming
readonly onEdit: Event<{ readonly resource: Uri, readonly edit: EditType }>;
readonly onDidEdit: Event<EditType>;
/**
* Apply a set of edits.
*
* Note that is not invoked when `onEdit` is called as `onEdit` implies also updating the view to reflect the edit.
* Note that is not invoked when `onDidEdit` is called because `onDidEdit` implies also updating the view to reflect the edit.
*
* @param resource Resource being edited.
* @param edit Array of edits. Sorted from oldest to most recent.
*
* @return Thenable signaling that the change has completed.
*/
applyEdits(resource: Uri, edits: readonly EditType[]): Thenable<void>;
applyEdits(edits: readonly EditType[]): Thenable<void>;
/**
* Undo a set of edits.
*
* This is triggered when a user undoes an edit or when revert is called on a file.
*
* @param resource Resource being edited.
* @param edit Array of edits. Sorted from most recent to oldest.
*
* @return Thenable signaling that the change has completed.
*/
undoEdits(resource: Uri, edits: readonly EditType[]): Thenable<void>;
undoEdits(edits: readonly EditType[]): Thenable<void>;
/**
* Back up `resource` in its current state.
* Back up the resource in its current state.
*
* Backups are used for hot exit and to prevent data loss. Your `backup` method should persist the resource in
* its current state, i.e. with the edits applied. Most commonly this means saving the resource to disk in
......@@ -1257,57 +1269,159 @@ declare module 'vscode' {
* made in quick succession, `backup` is only triggered after the last one. `backup` is not invoked when
* `auto save` is enabled (since auto save already persists resource ).
*
* @param resource The resource to back up.
* @param cancellation Token that signals the current backup since a new backup is coming in. It is up to your
* extension to decided how to respond to cancellation. If for example your extension is backing up a large file
* 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?(resource: Uri, cancellation: CancellationToken): Thenable<boolean>;
backup(cancellation: CancellationToken): Thenable<boolean>;
}
/**
* Represents a custom document for a custom webview editor.
*
* Custom documents are only used within a given `WebviewCustomEditorProvider`. The lifecycle of a
* `WebviewEditorCustomDocument` is managed by VS Code. When more more references remain to a given `WebviewEditorCustomDocument`
* then it is disposed of.
*
* @param UserDataType Type of custom object that extensions can store on the document.
*/
interface WebviewEditorCustomDocument<UserDataType = unknown> {
/**
* The associated viewType for this document.
*/
readonly viewType: string;
/**
* The associated uri for this document.
*/
readonly uri: Uri;
/**
* Event fired when there are no more references to the `WebviewEditorCustomDocument`.
*/
readonly onDidDispose: Event<void>;
/**
* Custom data that an extension can store on the document.
*/
readonly userData: UserDataType;
// TODO: Should we expose edits here?
// This could be helpful for tracking the life cycle of edits
}
/**
* Provider for webview editors that use a custom data model.
*
* Custom webview editors use [`WebviewEditorCustomDocument`](#WebviewEditorCustomDocument) as their data model.
* This gives extensions full control over actions such as edit, save, and backup.
*
* 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 WebviewCustomEditorProvider {
/**
* Create the model for a given
*
* @param document Resource being resolved.
*/
provideWebviewCustomEditorDocument(resource: Uri): Thenable<WebviewEditorCustomDocument>;
/**
* Resolve a webview editor for a given resource.
*
* To resolve a webview editor, a provider must fill in its initial html content and hook up all
* the event listeners it is interested it. The provider should also take ownership of the passed in `WebviewPanel`.
*
* @param resource Resource being resolved.
* @param document Document for resource being resolved.
* @param webview Webview being resolved. The provider should take ownership of this webview.
*
* @return Thenable indicating that the webview editor has been resolved.
*/
resolveWebviewEditor(
resource: Uri,
webview: WebviewPanel,
): Thenable<void>;
resolveWebviewCustomEditor(document: WebviewEditorCustomDocument, webview: WebviewPanel): Thenable<void>;
}
/**
* Provider for text based webview editors.
*
* Text based webview editors use a [`TextDocument`](#TextDocument) as their data model. This considerably simplifies
* implementing a webview editor as it allows VS Code to handle many common operations such as
* undo and backup. The provider is responsible for synchronizing text changes between the webview and the `TextDocument`.
*
* You should use text based webview editors when dealing with text based file formats, such as `xml` or `json`.
* For binary files or more specialized use cases, see [WebviewCustomEditorProvider](#WebviewCustomEditorProvider).
*/
export interface WebviewTextEditorProvider {
/**
* Controls the editing functionality of a webview editor. This allows the webview editor to hook into standard
* editor events such as `undo` or `save`.
* Resolve a webview editor for a given resource.
*
* WebviewEditors that do not have `editingCapability` are considered to be readonly. Users can still interact
* with readonly editors, but these editors will not integrate with VS Code's standard editor functionality.
* 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 should also take ownership of the passed in `WebviewPanel`.
*
* @param document Resource being resolved.
* @param webview Webview being resolved. The provider should take ownership of this webview.
*
* @return Thenable indicating that the webview editor has been resolved.
*/
readonly editingDelegate?: WebviewCustomEditorEditingDelegate<unknown>;
resolveWebviewTextEditor(document: TextDocument, webview: WebviewPanel): Thenable<void>;
}
namespace window {
/**
* Register a new provider for webview editors of a given type.
* Register a new provider for text based webview editors.
*
* @param viewType Type of the webview editor provider.
* @param provider Resolves webview editors.
* @param options Content settings for a webview panels the provider is given.
* @param viewType Type of the webview editor provider. This should match the `viewType` from the
* `package.json` contributions
* @param provider Provider that resolves webview editors.
* @param webviewOptions Content settings for the webview panels that provider is given.
*
* @return Disposable that unregisters the `WebviewTextEditorProvider`.
*/
export function registerWebviewTextEditorProvider(
viewType: string,
provider: WebviewTextEditorProvider,
webviewOptions?: WebviewPanelOptions,
): Disposable;
/**
* Register a new provider for custom webview editors.
*
* @param viewType Type of the webview editor provider. This should match the `viewType` from the
* `package.json` contributions.
* @param provider Provider that resolves webview editors.
* @param webviewOptions Content settings for the webview panels that provider is given.
*
* @return Disposable that unregisters the `WebviewCustomEditorProvider`.
*/
export function registerWebviewCustomEditorProvider(
viewType: string,
provider: WebviewCustomEditorProvider,
options?: WebviewPanelOptions,
webviewOptions?: WebviewPanelOptions,
): Disposable;
/**
* Create a new `WebviewEditorCustomDocument`.
*
* Note that this method only creates a custom document object. To have it be registered with VS Code, you
* must return the document from `WebviewCustomEditorProvider.provideWebviewCustomEditorDocument`.
*
* @param viewType Type of the webview editor provider. This should match the `viewType` from the
* `package.json` contributions.
* @param uri The document's resource.
* @param userData Custom data attached to the document.
* @param capabilities Controls the editing functionality of a webview editor. This allows the webview
* editor to hook into standard editor events such as `undo` or `save`.
*
* WebviewEditors that do not have `editingCapability` are considered to be readonly. Users can still interact
* with readonly editors, but these editors will not integrate with VS Code's standard editor functionality.
*/
export function createWebviewEditorCustomDocument<UserDataType>(
viewType: string,
uri: Uri,
userData: UserDataType,
capabilities: WebviewCustomEditorCapabilities
): WebviewEditorCustomDocument<UserDataType>;
}
//#endregion
......
......@@ -21,7 +21,7 @@ import * as extHostProtocol from 'vs/workbench/api/common/extHost.protocol';
import { editorGroupToViewColumn, EditorViewColumn, viewColumnToEditorGroup } from 'vs/workbench/api/common/shared/editor';
import { IEditorInput } from 'vs/workbench/common/editor';
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
import { CustomFileEditorInput } from 'vs/workbench/contrib/customEditor/browser/customEditorInput';
import { CustomEditorInput, ModelType } from 'vs/workbench/contrib/customEditor/browser/customEditorInput';
import { ICustomEditorModel, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor';
import { WebviewExtensionDescription } from 'vs/workbench/contrib/webview/browser/webview';
import { WebviewInput } from 'vs/workbench/contrib/webview/browser/webviewEditorInput';
......@@ -97,7 +97,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma
private readonly _webviewInputs = new WebviewInputStore();
private readonly _revivers = new Map<string, IDisposable>();
private readonly _editorProviders = new Map<string, IDisposable>();
private readonly _customEditorModels = new Map<ICustomEditorModel, { referenceCount: number }>();
private readonly _customEditorModels = new Map<string, { referenceCount: number }>();
constructor(
context: extHostProtocol.IExtHostContext,
......@@ -121,7 +121,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma
// This should trigger the real reviver to be registered from the extension host side.
this._register(_webviewWorkbenchService.registerResolver({
canResolve: (webview: WebviewInput) => {
if (webview instanceof CustomFileEditorInput) {
if (webview instanceof CustomEditorInput) {
extensionService.activateByEvent(`onWebviewEditor:${webview.viewType}`);
return false;
}
......@@ -256,7 +256,20 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma
this._revivers.delete(viewType);
}
public $registerEditorProvider(extensionData: extHostProtocol.WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions, capabilities: readonly extHostProtocol.WebviewEditorCapabilities[]): void {
public $registerTextEditorProvider(extensionData: extHostProtocol.WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions): void {
return this.registerEditorProvider(ModelType.Text, extensionData, viewType, options);
}
public $registerCustomEditorProvider(extensionData: extHostProtocol.WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions): void {
return this.registerEditorProvider(ModelType.Custom, extensionData, viewType, options);
}
public registerEditorProvider(
modelType: ModelType,
extensionData: extHostProtocol.WebviewExtensionDescription,
viewType: string,
options: modes.IWebviewPanelOptions,
): void {
if (this._editorProviders.has(viewType)) {
throw new Error(`Provider for ${viewType} already registered`);
}
......@@ -265,21 +278,25 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma
this._editorProviders.set(viewType, this._webviewWorkbenchService.registerResolver({
canResolve: (webviewInput) => {
return webviewInput instanceof CustomFileEditorInput && webviewInput.viewType === viewType;
return webviewInput instanceof CustomEditorInput && webviewInput.viewType === viewType;
},
resolveWebview: async (webviewInput: CustomFileEditorInput) => {
resolveWebview: async (webviewInput: CustomEditorInput) => {
const handle = webviewInput.id;
this._webviewInputs.add(handle, webviewInput);
this.hookupWebviewEventDelegate(handle, webviewInput);
webviewInput.webview.options = options;
webviewInput.webview.extension = extension;
webviewInput.modelType = modelType;
const resource = webviewInput.resource;
const model = await this.retainCustomEditorModel(webviewInput, resource, viewType, capabilities);
webviewInput.onDisposeWebview(() => {
this.releaseCustomEditorModel(model);
});
if (modelType === ModelType.Custom) {
const model = await this.retainCustomEditorModel(webviewInput, resource, viewType);
webviewInput.onDisposeWebview(() => {
this.releaseCustomEditorModel(model);
});
}
try {
await this._proxy.$resolveWebviewEditor(
......@@ -311,33 +328,26 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma
this._customEditorService.models.disposeAllModelsForView(viewType);
}
private async retainCustomEditorModel(webviewInput: WebviewInput, resource: URI, viewType: string, capabilities: readonly extHostProtocol.WebviewEditorCapabilities[]) {
private async retainCustomEditorModel(webviewInput: WebviewInput, resource: URI, viewType: string) {
const model = await this._customEditorService.models.resolve(webviewInput.resource, webviewInput.viewType);
const existingEntry = this._customEditorModels.get(model);
const key = viewType + resource.toString();
const existingEntry = this._customEditorModels.get(key);
if (existingEntry) {
++existingEntry.referenceCount;
// no need to hook up listeners again
return model;
}
this._customEditorModels.set(key, { referenceCount: 1 });
const { editable } = await this._proxy.$createWebviewCustomEditorDocument(resource, viewType);
this._customEditorModels.set(model, { referenceCount: 1 });
const capabilitiesSet = new Set(capabilities);
const isEditable = capabilitiesSet.has(extHostProtocol.WebviewEditorCapabilities.Editable);
if (isEditable) {
model.onUndo(e => {
this._proxy.$undoEdits(resource, viewType, e.edits);
if (editable) {
model.onUndo(() => {
this._proxy.$undo(resource, viewType);
});
model.onDisposeEdits(e => {
this._proxy.$disposeEdits(e.edits);
});
model.onApplyEdit(e => {
if (e.trigger !== model) {
this._proxy.$applyEdits(resource, viewType, e.edits);
}
model.onRedo(() => {
this._proxy.$redo(resource, viewType);
});
model.onWillSave(e => {
......@@ -347,7 +357,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma
// Save as should always be implemented even if the model is readonly
model.onWillSaveAs(e => {
if (isEditable) {
if (editable) {
e.waitUntil(this._proxy.$onSaveAs(e.resource.toJSON(), viewType, e.targetResource.toJSON()));
} else {
// Since the editor is readonly, just copy the file over
......@@ -355,36 +365,37 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma
}
});
if (capabilitiesSet.has(extHostProtocol.WebviewEditorCapabilities.SupportsHotExit)) {
model.onBackup(() => {
return createCancelablePromise(token =>
this._proxy.$backup(model.resource.toJSON(), viewType, token));
});
}
model.onBackup(() => {
return createCancelablePromise(token =>
this._proxy.$backup(model.resource.toJSON(), viewType, token));
});
return model;
}
private async releaseCustomEditorModel(model: ICustomEditorModel) {
const entry = this._customEditorModels.get(model);
const key = model.viewType + model.resource;
const entry = this._customEditorModels.get(key);
if (!entry) {
return;
throw new Error('Model not found');
}
--entry.referenceCount;
if (entry.referenceCount <= 0) {
this._proxy.$disposeWebviewCustomEditorDocument(model.resource, model.viewType);
this._customEditorService.models.disposeModel(model);
this._customEditorModels.delete(model);
this._customEditorModels.delete(key);
}
}
public $onEdit(resource: UriComponents, viewType: string, editId: number): void {
public $onDidChangeCustomDocumentState(resource: UriComponents, viewType: string, state: { dirty: boolean }) {
const model = this._customEditorService.models.get(URI.revive(resource), viewType);
if (!model) {
throw new Error('Could not find model for webview editor');
}
model.pushEdit(editId, model);
model.setDirty(state.dirty);
}
private hookupWebviewEventDelegate(handle: extHostProtocol.WebviewPanelHandle, input: WebviewInput) {
......
......@@ -113,7 +113,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
const extHostOutputService = rpcProtocol.set(ExtHostContext.ExtHostOutputService, accessor.get(IExtHostOutputService));
// manually create and register addressable instances
const extHostWebviews = rpcProtocol.set(ExtHostContext.ExtHostWebviews, new ExtHostWebviews(rpcProtocol, initData.environment, extHostWorkspace, extHostLogService, extHostApiDeprecation));
const extHostUrls = rpcProtocol.set(ExtHostContext.ExtHostUrls, new ExtHostUrls(rpcProtocol));
const extHostDocuments = rpcProtocol.set(ExtHostContext.ExtHostDocuments, new ExtHostDocuments(rpcProtocol, extHostDocumentsAndEditors));
const extHostDocumentContentProviders = rpcProtocol.set(ExtHostContext.ExtHostDocumentContentProviders, new ExtHostDocumentContentProvider(rpcProtocol, extHostDocumentsAndEditors, extHostLogService));
......@@ -134,6 +133,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
const extHostTheming = rpcProtocol.set(ExtHostContext.ExtHostTheming, new ExtHostTheming(rpcProtocol));
const extHostAuthentication = rpcProtocol.set(ExtHostContext.ExtHostAuthentication, new ExtHostAuthentication(rpcProtocol));
const extHostTimeline = rpcProtocol.set(ExtHostContext.ExtHostTimeline, new ExtHostTimeline(rpcProtocol, extHostCommands));
const extHostWebviews = rpcProtocol.set(ExtHostContext.ExtHostWebviews, new ExtHostWebviews(rpcProtocol, initData.environment, extHostWorkspace, extHostLogService, extHostApiDeprecation, extHostDocuments));
// Check that no named customers are missing
const expected: ProxyIdentifier<any>[] = values(ExtHostContext);
......@@ -561,10 +561,18 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
registerWebviewPanelSerializer: (viewType: string, serializer: vscode.WebviewPanelSerializer) => {
return extHostWebviews.registerWebviewPanelSerializer(extension, viewType, serializer);
},
registerWebviewTextEditorProvider: (viewType: string, provider: vscode.WebviewTextEditorProvider, options?: vscode.WebviewPanelOptions) => {
checkProposedApiEnabled(extension);
return extHostWebviews.registerWebviewTextEditorProvider(extension, viewType, provider, options);
},
registerWebviewCustomEditorProvider: (viewType: string, provider: vscode.WebviewCustomEditorProvider, options?: vscode.WebviewPanelOptions) => {
checkProposedApiEnabled(extension);
return extHostWebviews.registerWebviewCustomEditorProvider(extension, viewType, provider, options);
},
createWebviewEditorCustomDocument: <UserDataType>(viewType: string, resource: vscode.Uri, userData: UserDataType, capabilities: vscode.WebviewCustomEditorCapabilities) => {
checkProposedApiEnabled(extension);
return extHostWebviews.createWebviewEditorCustomDocument<UserDataType>(viewType, resource, userData, capabilities);
},
registerDecorationProvider(provider: vscode.DecorationProvider) {
checkProposedApiEnabled(extension);
return extHostDecorations.registerDecorationProvider(provider, extension.identifier);
......
......@@ -572,11 +572,6 @@ export interface WebviewExtensionDescription {
readonly location: UriComponents;
}
export enum WebviewEditorCapabilities {
Editable,
SupportsHotExit,
}
export interface MainThreadWebviewsShape extends IDisposable {
$createWebviewPanel(extension: WebviewExtensionDescription, handle: WebviewPanelHandle, viewType: string, title: string, showOptions: WebviewPanelShowOptions, options: modes.IWebviewPanelOptions & modes.IWebviewOptions): void;
$disposeWebview(handle: WebviewPanelHandle): void;
......@@ -592,10 +587,11 @@ export interface MainThreadWebviewsShape extends IDisposable {
$registerSerializer(viewType: string): void;
$unregisterSerializer(viewType: string): void;
$registerEditorProvider(extension: WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions, capabilities: readonly WebviewEditorCapabilities[]): void;
$registerTextEditorProvider(extension: WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions): void;
$registerCustomEditorProvider(extension: WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions): void;
$unregisterEditorProvider(viewType: string): void;
$onEdit(resource: UriComponents, viewType: string, editId: number): void;
$onDidChangeCustomDocumentState(resource: UriComponents, viewType: string, state: { dirty: boolean }): void;
}
export interface WebviewPanelViewStateData {
......@@ -613,12 +609,14 @@ export interface ExtHostWebviewsShape {
$onDidDisposeWebviewPanel(handle: WebviewPanelHandle): Promise<void>;
$deserializeWebviewPanel(newWebviewHandle: WebviewPanelHandle, viewType: string, title: string, state: any, position: EditorViewColumn, options: modes.IWebviewOptions & modes.IWebviewPanelOptions): Promise<void>;
$resolveWebviewEditor(resource: UriComponents, newWebviewHandle: WebviewPanelHandle, viewType: string, title: string, position: EditorViewColumn, options: modes.IWebviewOptions & modes.IWebviewPanelOptions): Promise<void>;
$undoEdits(resource: UriComponents, viewType: string, editIds: readonly number[]): void;
$applyEdits(resource: UriComponents, viewType: string, editIds: readonly number[]): void;
$disposeEdits(editIds: readonly number[]): void;
$resolveWebviewEditor(resource: UriComponents, newWebviewHandle: WebviewPanelHandle, viewType: string, title: string, position: EditorViewColumn, options: modes.IWebviewOptions & modes.IWebviewPanelOptions): Promise<void>;
$createWebviewCustomEditorDocument(resource: UriComponents, viewType: string): Promise<{ editable: boolean }>;
$disposeWebviewCustomEditorDocument(resource: UriComponents, viewType: string): Promise<void>;
$undo(resource: UriComponents, viewType: string): void;
$redo(resource: UriComponents, viewType: string): void;
$revert(resource: UriComponents, viewType: string): void;
$onSave(resource: UriComponents, viewType: string): Promise<void>;
$onSaveAs(resource: UriComponents, viewType: string, targetResource: UriComponents): Promise<void>;
......
......@@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CancellationToken } from 'vs/base/common/cancellation';
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { URI, UriComponents } from 'vs/base/common/uri';
......@@ -10,16 +11,15 @@ import { generateUuid } from 'vs/base/common/uuid';
import * as modes from 'vs/editor/common/modes';
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { ILogService } from 'vs/platform/log/common/log';
import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService';
import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments';
import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters';
import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace';
import { EditorViewColumn } from 'vs/workbench/api/common/shared/editor';
import { asWebviewUri, WebviewInitData } from 'vs/workbench/api/common/shared/webview';
import type * as vscode from 'vscode';
import { Cache } from './cache';
import { ExtHostWebviewsShape, IMainContext, MainContext, MainThreadWebviewsShape, WebviewEditorCapabilities, WebviewPanelHandle, WebviewPanelViewStateData } from './extHost.protocol';
import { ExtHostWebviewsShape, IMainContext, MainContext, MainThreadWebviewsShape, WebviewExtensionDescription, WebviewPanelHandle, WebviewPanelViewStateData } from './extHost.protocol';
import { Disposable as VSCodeDisposable } from './extHostTypes';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService';
type IconPath = URI | { light: URI, dark: URI };
......@@ -245,6 +245,193 @@ export class ExtHostWebviewEditor extends Disposable implements vscode.WebviewPa
}
}
type EditType = unknown;
class WebviewEditorCustomDocument extends Disposable implements vscode.WebviewEditorCustomDocument {
private _currentEditIndex: number = -1;
private _savePoint: number = -1;
private readonly _edits: Array<EditType> = [];
constructor(
private readonly _proxy: MainThreadWebviewsShape,
public readonly viewType: string,
public readonly uri: vscode.Uri,
public readonly userData: unknown,
public readonly _capabilities: vscode.WebviewCustomEditorCapabilities,
) {
super();
// Hook up events
_capabilities.editing?.onDidEdit(edit => {
this.pushEdit(edit, this);
});
}
//#region Public API
#_onDidDispose = this._register(new Emitter<void>());
public readonly onDidDispose = this.#_onDidDispose.event;
//#endregion
dispose() {
this.#_onDidDispose.fire();
super.dispose();
}
private pushEdit(edit: EditType, trigger: any) {
this.spliceEdits(edit);
this._currentEditIndex = this._edits.length - 1;
this.updateState();
// this._onApplyEdit.fire({ edits: [edit], trigger });
}
private updateState() {
const dirty = this._edits.length > 0 && this._savePoint !== this._currentEditIndex;
this._proxy.$onDidChangeCustomDocumentState(this.uri, this.viewType, { dirty });
}
private spliceEdits(editToInsert?: EditType) {
const start = this._currentEditIndex + 1;
const toRemove = this._edits.length - this._currentEditIndex;
editToInsert
? this._edits.splice(start, toRemove, editToInsert)
: this._edits.splice(start, toRemove);
}
revert() {
const editing = this.getEditingCapability();
if (this._currentEditIndex === this._savePoint) {
return true;
}
if (this._currentEditIndex >= this._savePoint) {
const editsToUndo = this._edits.slice(this._savePoint, this._currentEditIndex);
editing.undoEdits(editsToUndo.reverse());
} else if (this._currentEditIndex < this._savePoint) {
const editsToRedo = this._edits.slice(this._currentEditIndex, this._savePoint);
editing.applyEdits(editsToRedo);
}
this._currentEditIndex = this._savePoint;
this.spliceEdits();
this.updateState();
return true;
}
undo() {
const editing = this.getEditingCapability();
if (this._currentEditIndex < 0) {
// nothing to undo
return;
}
const undoneEdit = this._edits[this._currentEditIndex];
--this._currentEditIndex;
editing.undoEdits([undoneEdit]);
this.updateState();
}
redo() {
const editing = this.getEditingCapability();
if (this._currentEditIndex >= this._edits.length - 1) {
// nothing to redo
return;
}
++this._currentEditIndex;
const redoneEdit = this._edits[this._currentEditIndex];
editing.applyEdits([redoneEdit]);
this.updateState();
}
save() {
return this.getEditingCapability().save();
}
saveAs(target: vscode.Uri) {
return this.getEditingCapability().saveAs(target);
}
backup(cancellation: CancellationToken): boolean | PromiseLike<boolean> {
throw new Error('Method not implemented.');
}
private getEditingCapability(): vscode.WebviewCustomEditorEditingCapability {
if (!this._capabilities.editing) {
throw new Error('Document is not editable');
}
return this._capabilities.editing;
}
}
class WebviewDocumentStore {
private readonly _documents = new Map<string, WebviewEditorCustomDocument>();
public get(viewType: string, resource: vscode.Uri): WebviewEditorCustomDocument | undefined {
return this._documents.get(this.key(viewType, resource));
}
public add(document: WebviewEditorCustomDocument) {
const key = this.key(document.viewType, document.uri);
if (this._documents.has(key)) {
throw new Error(`Document already exists for viewType:${document.viewType} resource:${document.uri}`);
}
this._documents.set(key, document);
}
public delete(document: WebviewEditorCustomDocument) {
const key = this.key(document.viewType, document.uri);
this._documents.delete(key);
}
private key(viewType: string, resource: vscode.Uri): string {
return `${viewType}@@@${resource.toString}`;
}
}
const enum WebviewEditorType {
Text,
Custom
}
type ProviderEntry = {
readonly extension: IExtensionDescription;
readonly type: WebviewEditorType.Text;
readonly provider: vscode.WebviewTextEditorProvider;
} | {
readonly extension: IExtensionDescription;
readonly type: WebviewEditorType.Custom;
readonly provider: vscode.WebviewCustomEditorProvider;
};
class EditorProviderStore {
private readonly _providers = new Map<string, ProviderEntry>();
public addTextProvider(viewType: string, extension: IExtensionDescription, provider: vscode.WebviewTextEditorProvider): vscode.Disposable {
return this.add(WebviewEditorType.Text, viewType, extension, provider);
}
public addCustomProvider(viewType: string, extension: IExtensionDescription, provider: vscode.WebviewCustomEditorProvider): vscode.Disposable {
return this.add(WebviewEditorType.Custom, viewType, extension, provider);
}
public get(viewType: string): ProviderEntry | undefined {
return this._providers.get(viewType);
}
private add(type: WebviewEditorType, viewType: string, extension: IExtensionDescription, provider: vscode.WebviewTextEditorProvider | vscode.WebviewCustomEditorProvider): vscode.Disposable {
if (this._providers.has(viewType)) {
throw new Error(`Provider for viewType:${viewType} already registered`);
}
this._providers.set(viewType, { type, extension, provider } as ProviderEntry);
return new VSCodeDisposable(() => this._providers.delete(viewType));
}
}
export class ExtHostWebviews implements ExtHostWebviewsShape {
private static newHandle(): WebviewPanelHandle {
......@@ -259,12 +446,9 @@ export class ExtHostWebviews implements ExtHostWebviewsShape {
readonly extension: IExtensionDescription;
}>();
private readonly _editorProviders = new Map<string, {
readonly provider: vscode.WebviewCustomEditorProvider;
readonly extension: IExtensionDescription;
}>();
private readonly _editorProviders = new EditorProviderStore();
private readonly _edits = new Cache<unknown>('edits');
private readonly _documents = new WebviewDocumentStore();
constructor(
mainContext: IMainContext,
......@@ -272,6 +456,7 @@ export class ExtHostWebviews implements ExtHostWebviewsShape {
private readonly workspace: IExtHostWorkspace | undefined,
private readonly _logService: ILogService,
private readonly _deprecationService: IExtHostApiDeprecationService,
private readonly _extHostDocuments: ExtHostDocuments,
) {
this._proxy = mainContext.getProxy(MainContext.MainThreadWebviews);
}
......@@ -290,7 +475,7 @@ export class ExtHostWebviews implements ExtHostWebviewsShape {
};
const handle = ExtHostWebviews.newHandle();
this._proxy.$createWebviewPanel({ id: extension.identifier, location: extension.extensionLocation }, handle, viewType, title, webviewShowOptions, convertWebviewOptions(extension, this.workspace, options));
this._proxy.$createWebviewPanel(toExtensionData(extension), handle, viewType, title, webviewShowOptions, convertWebviewOptions(extension, this.workspace, options));
const webview = new ExtHostWebview(handle, this._proxy, options, this.initData, this.workspace, extension, this._deprecationService);
const panel = new ExtHostWebviewEditor(handle, this._proxy, viewType, title, viewColumn, options, webview);
......@@ -316,31 +501,45 @@ export class ExtHostWebviews implements ExtHostWebviewsShape {
});
}
public registerWebviewCustomEditorProvider(
public registerWebviewTextEditorProvider(
extension: IExtensionDescription,
viewType: string,
provider: vscode.WebviewCustomEditorProvider,
options?: vscode.WebviewPanelOptions,
provider: vscode.WebviewTextEditorProvider,
options: vscode.WebviewPanelOptions | undefined = {}
): vscode.Disposable {
if (this._editorProviders.has(viewType)) {
throw new Error(`Editor provider for '${viewType}' already registered`);
}
this._editorProviders.set(viewType, { extension, provider, });
this._proxy.$registerEditorProvider({ id: extension.identifier, location: extension.extensionLocation }, viewType, options || {}, this.getCapabilites(provider));
const unregisterProvider = this._editorProviders.addTextProvider(viewType, extension, provider);
this._proxy.$registerTextEditorProvider(toExtensionData(extension), viewType, options);
// Hook up events
provider?.editingDelegate?.onEdit(({ edit, resource }) => {
const id = this._edits.add([edit]);
this._proxy.$onEdit(resource, viewType, id);
return new VSCodeDisposable(() => {
unregisterProvider.dispose();
this._proxy.$unregisterEditorProvider(viewType);
});
}
public registerWebviewCustomEditorProvider(
extension: IExtensionDescription,
viewType: string,
provider: vscode.WebviewCustomEditorProvider,
options: vscode.WebviewPanelOptions | undefined = {},
): vscode.Disposable {
const unregisterProvider = this._editorProviders.addCustomProvider(viewType, extension, provider);
this._proxy.$registerCustomEditorProvider(toExtensionData(extension), viewType, options);
return new VSCodeDisposable(() => {
this._editorProviders.delete(viewType);
unregisterProvider.dispose();
this._proxy.$unregisterEditorProvider(viewType);
});
}
public createWebviewEditorCustomDocument<UserDataType>(
viewType: string,
resource: vscode.Uri,
userData: UserDataType,
capabilities: vscode.WebviewCustomEditorCapabilities,
): vscode.WebviewEditorCustomDocument<UserDataType> {
return Object.seal(new WebviewEditorCustomDocument(this._proxy, viewType, resource, userData, capabilities) as vscode.WebviewEditorCustomDocument<UserDataType>);
}
public $onMessage(
handle: WebviewPanelHandle,
message: any
......@@ -421,6 +620,40 @@ export class ExtHostWebviews implements ExtHostWebviewsShape {
await serializer.deserializeWebviewPanel(revivedPanel, state);
}
async $createWebviewCustomEditorDocument(resource: UriComponents, viewType: string) {
const entry = this._editorProviders.get(viewType);
if (!entry) {
throw new Error(`No provider found for '${viewType}'`);
}
if (entry.type !== WebviewEditorType.Custom) {
throw new Error(`Invalid provide type for '${viewType}'`);
}
const revivedResource = URI.revive(resource);
const document = await entry.provider.provideWebviewCustomEditorDocument(revivedResource) as WebviewEditorCustomDocument;
this._documents.add(document);
return {
editable: !!document._capabilities.editing
};
}
async $disposeWebviewCustomEditorDocument(resource: UriComponents, viewType: string): Promise<void> {
const entry = this._editorProviders.get(viewType);
if (!entry) {
throw new Error(`No provider found for '${viewType}'`);
}
if (entry.type !== WebviewEditorType.Custom) {
throw new Error(`Invalid provider type for '${viewType}'`);
}
const revivedResource = URI.revive(resource);
const document = this.getDocument(viewType, revivedResource);
this._documents.delete(document);
document.dispose();
}
async $resolveWebviewEditor(
resource: UriComponents,
handle: WebviewPanelHandle,
......@@ -431,83 +664,81 @@ export class ExtHostWebviews implements ExtHostWebviewsShape {
): Promise<void> {
const entry = this._editorProviders.get(viewType);
if (!entry) {
return Promise.reject(new Error(`No provider found for '${viewType}'`));
throw new Error(`No provider found for '${viewType}'`);
}
const { provider, extension } = entry;
const webview = new ExtHostWebview(handle, this._proxy, options, this.initData, this.workspace, extension, this._deprecationService);
const webview = new ExtHostWebview(handle, this._proxy, options, this.initData, this.workspace, entry.extension, this._deprecationService);
const revivedPanel = new ExtHostWebviewEditor(handle, this._proxy, viewType, title, typeof position === 'number' && position >= 0 ? typeConverters.ViewColumn.to(position) : undefined, options, webview);
this._webviewPanels.set(handle, revivedPanel);
const revivedResource = URI.revive(resource);
await provider.resolveWebviewEditor(revivedResource, revivedPanel);
}
$undoEdits(resourceComponents: UriComponents, viewType: string, editIds: readonly number[]): void {
const provider = this.getEditorProvider(viewType);
if (!provider?.editingDelegate) {
return;
switch (entry.type) {
case WebviewEditorType.Custom:
{
const document = this.getDocument(viewType, revivedResource);
return entry.provider.resolveWebviewCustomEditor(document, revivedPanel);
}
case WebviewEditorType.Text:
{
await this._extHostDocuments.ensureDocumentData(revivedResource);
const document = this._extHostDocuments.getDocument(revivedResource);
return entry.provider.resolveWebviewTextEditor(document, revivedPanel);
}
default:
{
throw new Error('Unknown webview provider type');
}
}
const resource = URI.revive(resourceComponents);
const edits = editIds.map(id => this._edits.get(id, 0));
provider.editingDelegate.undoEdits(resource, edits);
}
$applyEdits(resourceComponents: UriComponents, viewType: string, editIds: readonly number[]): void {
const provider = this.getEditorProvider(viewType);
if (!provider?.editingDelegate) {
return;
}
async $undo(resourceComponents: UriComponents, viewType: string): Promise<void> {
const document = this.getDocument(viewType, resourceComponents);
document.undo();
}
const resource = URI.revive(resourceComponents);
const edits = editIds.map(id => this._edits.get(id, 0));
provider.editingDelegate.applyEdits(resource, edits);
async $redo(resourceComponents: UriComponents, viewType: string): Promise<void> {
const document = this.getDocument(viewType, resourceComponents);
document.redo();
}
$disposeEdits(editIds: readonly number[]): void {
for (const edit of editIds) {
this._edits.delete(edit);
}
async $revert(resourceComponents: UriComponents, viewType: string): Promise<void> {
const document = this.getDocument(viewType, resourceComponents);
document.revert();
}
async $onSave(resource: UriComponents, viewType: string): Promise<void> {
const provider = this.getEditorProvider(viewType);
return provider?.editingDelegate?.save(URI.revive(resource));
async $onSave(resourceComponents: UriComponents, viewType: string): Promise<void> {
const document = this.getDocument(viewType, resourceComponents);
document.save();
}
async $onSaveAs(resource: UriComponents, viewType: string, targetResource: UriComponents): Promise<void> {
const provider = this.getEditorProvider(viewType);
return provider?.editingDelegate?.saveAs(URI.revive(resource), URI.revive(targetResource));
async $onSaveAs(resourceComponents: UriComponents, viewType: string, targetResource: UriComponents): Promise<void> {
const document = this.getDocument(viewType, resourceComponents);
return document.saveAs(URI.revive(targetResource));
}
async $backup(resource: UriComponents, viewType: string, cancellation: CancellationToken): Promise<boolean> {
const provider = this.getEditorProvider(viewType);
if (!provider?.editingDelegate?.backup) {
return false;
}
return provider.editingDelegate.backup(URI.revive(resource), cancellation);
async $backup(resourceComponents: UriComponents, viewType: string, cancellation: CancellationToken): Promise<boolean> {
const document = this.getDocument(viewType, resourceComponents);
return document.backup(cancellation);
}
private getWebviewPanel(handle: WebviewPanelHandle): ExtHostWebviewEditor | undefined {
return this._webviewPanels.get(handle);
}
private getEditorProvider(viewType: string): vscode.WebviewCustomEditorProvider | undefined {
return this._editorProviders.get(viewType)?.provider;
}
private getCapabilites(capabilities: vscode.WebviewCustomEditorProvider) {
const declaredCapabilites: WebviewEditorCapabilities[] = [];
if (capabilities.editingDelegate) {
declaredCapabilites.push(WebviewEditorCapabilities.Editable);
}
if (capabilities.editingDelegate?.backup) {
declaredCapabilites.push(WebviewEditorCapabilities.SupportsHotExit);
private getDocument(viewType: string, resource: UriComponents): WebviewEditorCustomDocument {
const document = this._documents.get(viewType, URI.revive(resource));
if (!document) {
throw new Error('No webview editor custom document found');
}
return declaredCapabilites;
return document;
}
}
function toExtensionData(extension: IExtensionDescription): WebviewExtensionDescription {
return { id: extension.identifier, location: extension.extensionLocation };
}
function convertWebviewOptions(
extension: IExtensionDescription,
workspace: IExtHostWorkspace | undefined,
......
......@@ -15,7 +15,7 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation
import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { EditorViewColumn, viewColumnToEditorGroup } from 'vs/workbench/api/common/shared/editor';
import { IEditorCommandsContext } from 'vs/workbench/common/editor';
import { CustomFileEditorInput } from 'vs/workbench/contrib/customEditor/browser/customEditorInput';
import { CustomEditorInput } from 'vs/workbench/contrib/customEditor/browser/customEditorInput';
import { defaultEditorId } from 'vs/workbench/contrib/customEditor/browser/customEditors';
import { CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE, CONTEXT_HAS_CUSTOM_EDITORS, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor';
import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
......@@ -114,19 +114,11 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, {
}
public runCommand(accessor: ServicesAccessor): void {
const customEditorService = accessor.get<ICustomEditorService>(ICustomEditorService);
const activeCustomEditor = customEditorService.activeCustomEditor;
if (!activeCustomEditor) {
return;
}
const model = customEditorService.models.get(activeCustomEditor.resource, activeCustomEditor.viewType);
if (!model) {
return;
const editorService = accessor.get<IEditorService>(IEditorService);
const activeInput = editorService.activeControl?.input;
if (activeInput instanceof CustomEditorInput) {
activeInput.undo();
}
model.undo();
}
}).register();
......@@ -149,19 +141,11 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, {
}
public runCommand(accessor: ServicesAccessor): void {
const customEditorService = accessor.get<ICustomEditorService>(ICustomEditorService);
const activeCustomEditor = customEditorService.activeCustomEditor;
if (!activeCustomEditor) {
return;
}
const model = customEditorService.models.get(activeCustomEditor.resource, activeCustomEditor.viewType);
if (!model) {
return;
const editorService = accessor.get<IEditorService>(IEditorService);
const activeInput = editorService.activeControl?.input;
if (activeInput instanceof CustomEditorInput) {
activeInput.redo();
}
model.redo();
}
}).register();
......@@ -193,7 +177,7 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, {
const customEditorService = accessor.get<ICustomEditorService>(ICustomEditorService);
let toggleView = defaultEditorId;
if (!(activeEditor instanceof CustomFileEditorInput)) {
if (!(activeEditor instanceof CustomEditorInput)) {
const bestAvailableEditor = customEditorService.getContributedCustomEditors(targetResource).bestAvailableEditor;
if (bestAvailableEditor) {
toggleView = bestAvailableEditor.id;
......
......@@ -16,20 +16,26 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
import { ILabelService } from 'vs/platform/label/common/label';
import { GroupIdentifier, IEditorInput, IRevertOptions, ISaveOptions, Verbosity } from 'vs/workbench/common/editor';
import { ICustomEditorModel, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor';
import { WebviewEditorOverlay, IWebviewService } from 'vs/workbench/contrib/webview/browser/webview';
import { IWebviewService, WebviewEditorOverlay } from 'vs/workbench/contrib/webview/browser/webview';
import { IWebviewWorkbenchService, LazilyResolvedWebviewEditorInput } from 'vs/workbench/contrib/webview/browser/webviewWorkbenchService';
import { AutoSaveMode, IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { AutoSaveMode, IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
export const enum ModelType {
Custom = 'custom',
Text = 'text',
}
export class CustomEditorInput extends LazilyResolvedWebviewEditorInput {
export class CustomFileEditorInput extends LazilyResolvedWebviewEditorInput {
public static typeId = 'workbench.editors.webviewEditor';
private readonly _editorResource: URI;
get resource() { return this._editorResource; }
private _model?: ICustomEditorModel;
private _model?: { readonly type: ModelType.Custom, readonly model: ICustomEditorModel } | { readonly type: ModelType.Text };
constructor(
resource: URI,
......@@ -43,14 +49,18 @@ export class CustomFileEditorInput extends LazilyResolvedWebviewEditorInput {
@ICustomEditorService private readonly customEditorService: ICustomEditorService,
@IFileDialogService private readonly fileDialogService: IFileDialogService,
@IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService,
@IEditorService private readonly editorService: IEditorService
@IEditorService private readonly editorService: IEditorService,
@ITextFileService private readonly textFileService: ITextFileService,
) {
super(id, viewType, '', webview, webviewService, webviewWorkbenchService);
this._editorResource = resource;
}
public modelType?: ModelType;
public getTypeId(): string {
return CustomFileEditorInput.typeId;
return CustomEditorInput.typeId;
}
public supportsSplitEditor() {
......@@ -62,13 +72,8 @@ export class CustomFileEditorInput extends LazilyResolvedWebviewEditorInput {
return basename(this.labelService.getUriLabel(this.resource));
}
@memoize
getDescription(): string | undefined {
return super.getDescription();
}
matches(other: IEditorInput): boolean {
return this === other || (other instanceof CustomFileEditorInput
return this === other || (other instanceof CustomEditorInput
&& this.viewType === other.viewType
&& isEqual(this.resource, other.resource));
}
......@@ -101,11 +106,24 @@ export class CustomFileEditorInput extends LazilyResolvedWebviewEditorInput {
}
public isReadonly(): boolean {
return false;
return false; // TODO
}
public isDirty(): boolean {
return this._model ? this._model.isDirty() : false;
if (!this._model) {
return false;
}
switch (this._model.type) {
case ModelType.Text:
return this.textFileService.isDirty(this.resource);
case ModelType.Custom:
return this._model.model.isDirty();
default:
throw new Error('Unknown model type');
}
}
public isSaving(): boolean {
......@@ -125,12 +143,20 @@ export class CustomFileEditorInput extends LazilyResolvedWebviewEditorInput {
return undefined;
}
const result = await this._model.save(options);
if (!result) {
return undefined;
switch (this._model.type) {
case ModelType.Text:
{
const result = await this.textFileService.save(this.resource, options);
return result ? this : undefined;
}
case ModelType.Custom:
{
const result = await this._model.model.save(options);
return result ? this : undefined;
}
default:
throw new Error('Unknown model type');
}
return this;
}
public async saveAs(groupId: GroupIdentifier, options?: ISaveOptions): Promise<IEditorInput | undefined> {
......@@ -144,31 +170,81 @@ export class CustomFileEditorInput extends LazilyResolvedWebviewEditorInput {
return undefined; // save cancelled
}
if (!await this._model.saveAs(this._editorResource, target, options)) {
return undefined;
switch (this._model.type) {
case ModelType.Text:
if (!await this.textFileService.saveAs(this.resource, target, options)) {
return undefined;
}
break;
case ModelType.Custom:
if (!await this._model.model.saveAs(this._editorResource, target, options)) {
return undefined;
}
break;
default:
throw new Error('Unknown model type');
}
return this.handleMove(groupId, target) || this.editorService.createInput({ resource: target, forceFile: true });
}
public revert(group: GroupIdentifier, options?: IRevertOptions): Promise<boolean> {
return this._model ? this._model.revert(options) : Promise.resolve(false);
public async revert(group: GroupIdentifier, options?: IRevertOptions): Promise<boolean> {
if (!this._model) {
return false;
}
switch (this._model.type) {
case ModelType.Text:
return this.textFileService.revert(this.resource, options);
case ModelType.Custom:
return this._model.model.revert(options);
default:
throw new Error('Unknown model type');
}
}
public async resolve(): Promise<IEditorModel> {
this._model = await this.customEditorService.models.resolve(this.resource, this.viewType);
this._register(this._model.onDidChangeDirty(() => this._onDidChangeDirty.fire()));
const editorModel = await super.resolve();
if (!this._model) {
switch (this.modelType) {
case ModelType.Custom:
const model = await this.customEditorService.models.resolve(this.resource, this.viewType);
this._model = { type: ModelType.Custom, model };
this._register(model.onDidChangeDirty(() => this._onDidChangeDirty.fire()));
break;
case ModelType.Text:
this._model = { type: ModelType.Text, };
this.textFileService.files.onDidChangeDirty(e => {
if (isEqual(this.resource, e.resource)) {
this._onDidChangeDirty.fire();
}
});
break;
default:
throw new Error('Unknown model type');
}
}
if (this.isDirty()) {
this._onDidChangeDirty.fire();
}
return await super.resolve();
return editorModel;
}
public handleMove(groupId: GroupIdentifier, uri: URI, options?: ITextEditorOptions): IEditorInput | undefined {
const editorInfo = this.customEditorService.getCustomEditor(this.viewType);
if (editorInfo?.matches(uri)) {
const webview = assertIsDefined(this.takeOwnershipOfWebview());
const newInput = this.instantiationService.createInstance(CustomFileEditorInput,
const newInput = this.instantiationService.createInstance(CustomEditorInput,
uri,
this.viewType,
generateUuid(),
......@@ -178,4 +254,42 @@ export class CustomFileEditorInput extends LazilyResolvedWebviewEditorInput {
}
return undefined;
}
public undo(): void {
if (!this._model) {
return;
}
switch (this._model.type) {
case ModelType.Custom:
this._model.model.undo();
return;
case ModelType.Text:
this.textFileService.files.get(this.resource)?.textEditorModel?.undo();
return;
default:
throw new Error('Unknown model type');
}
}
public redo(): void {
if (!this._model) {
return;
}
switch (this._model.type) {
case ModelType.Custom:
this._model.model.redo();
return;
case ModelType.Text:
this.textFileService.files.get(this.resource)?.textEditorModel?.redo();
return;
default:
throw new Error('Unknown model type');
}
}
}
......@@ -6,14 +6,14 @@
import { URI } from 'vs/base/common/uri';
import { generateUuid } from 'vs/base/common/uuid';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { CustomFileEditorInput } from 'vs/workbench/contrib/customEditor/browser/customEditorInput';
import { CustomEditorInput } from 'vs/workbench/contrib/customEditor/browser/customEditorInput';
import { WebviewEditorInputFactory } from 'vs/workbench/contrib/webview/browser/webviewEditorInputFactory';
import { IWebviewWorkbenchService } from 'vs/workbench/contrib/webview/browser/webviewWorkbenchService';
import { Lazy } from 'vs/base/common/lazy';
export class CustomEditorInputFactory extends WebviewEditorInputFactory {
public static readonly ID = CustomFileEditorInput.typeId;
public static readonly ID = CustomEditorInput.typeId;
public constructor(
@IInstantiationService private readonly _instantiationService: IInstantiationService,
......@@ -22,10 +22,11 @@ export class CustomEditorInputFactory extends WebviewEditorInputFactory {
super(webviewWorkbenchService);
}
public serialize(input: CustomFileEditorInput): string | undefined {
public serialize(input: CustomEditorInput): string | undefined {
const data = {
...this.toJson(input),
editorResource: input.resource.toJSON()
editorResource: input.resource.toJSON(),
modelType: input.modelType
};
try {
......@@ -38,7 +39,7 @@ export class CustomEditorInputFactory extends WebviewEditorInputFactory {
public deserialize(
_instantiationService: IInstantiationService,
serializedEditorInput: string
): CustomFileEditorInput {
): CustomEditorInput {
const data = this.fromJson(serializedEditorInput);
const id = data.id || generateUuid();
......@@ -50,10 +51,13 @@ export class CustomEditorInputFactory extends WebviewEditorInputFactory {
return webviewInput.webview;
});
const customInput = this._instantiationService.createInstance(CustomFileEditorInput, URI.from((data as any).editorResource), data.viewType, id, webview);
const customInput = this._instantiationService.createInstance(CustomEditorInput, URI.from((data as any).editorResource), data.viewType, id, webview);
if (typeof data.group === 'number') {
customInput.updateGroup(data.group);
}
if ((data as any).modelType) {
customInput.modelType = (data as any).modelType;
}
return customInput;
}
}
......@@ -7,7 +7,7 @@ import { coalesce } from 'vs/base/common/arrays';
import { Emitter } from 'vs/base/common/event';
import { Lazy } from 'vs/base/common/lazy';
import { Disposable } from 'vs/base/common/lifecycle';
import { basename, isEqual, extname } from 'vs/base/common/resources';
import { basename, extname, isEqual } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import { generateUuid } from 'vs/base/common/uuid';
import * as nls from 'vs/nls';
......@@ -31,7 +31,7 @@ import { IWebviewService, webviewHasOwnEditFunctionsContext } from 'vs/workbench
import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IEditorService, IOpenEditorOverride } from 'vs/workbench/services/editor/common/editorService';
import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService';
import { CustomFileEditorInput } from './customEditorInput';
import { CustomEditorInput } from './customEditorInput';
export const defaultEditorId = 'default';
......@@ -138,7 +138,7 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ
public get activeCustomEditor(): ICustomEditor | undefined {
const activeInput = this.editorService.activeControl?.input;
if (!(activeInput instanceof CustomFileEditorInput)) {
if (!(activeInput instanceof CustomEditorInput)) {
return undefined;
}
const resource = activeInput.resource;
......@@ -175,7 +175,7 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ
let currentlyOpenedEditorType: undefined | string;
for (const editor of group ? group.editors : []) {
if (editor.resource && isEqual(editor.resource, resource)) {
currentlyOpenedEditorType = editor instanceof CustomFileEditorInput ? editor.viewType : defaultEditorId;
currentlyOpenedEditorType = editor instanceof CustomEditorInput ? editor.viewType : defaultEditorId;
break;
}
}
......@@ -271,7 +271,7 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ
const webview = new Lazy(() => {
return this.webviewService.createWebviewEditorOverlay(id, { customClasses: options?.customClasses }, {});
});
const input = this.instantiationService.createInstance(CustomFileEditorInput, resource, viewType, id, webview);
const input = this.instantiationService.createInstance(CustomEditorInput, resource, viewType, id, webview);
if (group) {
input.updateGroup(group.id);
}
......@@ -297,7 +297,7 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ
options: options ? EditorOptions.create(options) : undefined,
}], targetGroup);
if (existing instanceof CustomFileEditorInput) {
if (existing instanceof CustomEditorInput) {
existing.dispose();
}
}
......@@ -321,14 +321,14 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ
...this.getUserConfiguredCustomEditors(resource).allEditors,
];
this._hasCustomEditor.set(possibleEditors.length > 0);
this._focusedCustomEditorIsEditable.set(activeControl?.input instanceof CustomFileEditorInput);
this._focusedCustomEditorIsEditable.set(activeControl?.input instanceof CustomEditorInput);
this._webviewHasOwnEditFunctions.set(possibleEditors.length > 0);
}
private handleMovedFileInOpenedFileEditors(oldResource: URI, newResource: URI): void {
for (const group of this.editorGroupService.groups) {
for (const editor of group.editors) {
if (!(editor instanceof CustomFileEditorInput)) {
if (!(editor instanceof CustomEditorInput)) {
continue;
}
......@@ -371,7 +371,7 @@ export class CustomEditorContribution extends Disposable implements IWorkbenchCo
}));
this._register(this.editorService.onDidCloseEditor(({ editor }) => {
if (!(editor instanceof CustomFileEditorInput)) {
if (!(editor instanceof CustomEditorInput)) {
return;
}
......@@ -386,7 +386,7 @@ export class CustomEditorContribution extends Disposable implements IWorkbenchCo
options: ITextEditorOptions | undefined,
group: IEditorGroup
): IOpenEditorOverride | undefined {
if (editor instanceof CustomFileEditorInput) {
if (editor instanceof CustomEditorInput) {
if (editor.group === group.id) {
// No need to do anything
return undefined;
......@@ -483,7 +483,7 @@ export class CustomEditorContribution extends Disposable implements IWorkbenchCo
group: IEditorGroup
): IOpenEditorOverride | undefined {
const getCustomEditorOverrideForSubInput = (subInput: IEditorInput, customClasses: string): EditorInput | undefined => {
if (subInput instanceof CustomFileEditorInput) {
if (subInput instanceof CustomEditorInput) {
return undefined;
}
const resource = subInput.resource;
......
......@@ -17,7 +17,7 @@ import { CustomEditorInputFactory } from 'vs/workbench/contrib/customEditor/brow
import { ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor';
import { WebviewEditor } from 'vs/workbench/contrib/webview/browser/webviewEditor';
import './commands';
import { CustomFileEditorInput } from './customEditorInput';
import { CustomEditorInput } from './customEditorInput';
import { CustomEditorContribution, customEditorsAssociationsKey, CustomEditorService } from './customEditors';
registerSingleton(ICustomEditorService, CustomEditorService);
......@@ -31,7 +31,7 @@ Registry.as<IEditorRegistry>(EditorExtensions.Editors).registerEditor(
WebviewEditor.ID,
'Webview Editor',
), [
new SyncDescriptor(CustomFileEditorInput)
new SyncDescriptor(CustomEditorInput)
]);
Registry.as<IEditorInputFactoryRegistry>(EditorInputExtensions.EditorInputFactories).registerEditorInputFactory(
......
......@@ -43,8 +43,6 @@ export interface ICustomEditorService {
promptOpenWith(resource: URI, options?: ITextEditorOptions, group?: IEditorGroup): Promise<IEditor | undefined>;
}
export type CustomEditorEdit = number;
export interface ICustomEditorModelManager {
get(resource: URI, viewType: string): ICustomEditorModel | undefined;
......@@ -69,23 +67,22 @@ export interface CustomEditorSaveAsEvent {
export interface ICustomEditorModel extends IWorkingCopy {
readonly viewType: string;
readonly onUndo: Event<{ edits: readonly CustomEditorEdit[], trigger: any | undefined }>;
readonly onApplyEdit: Event<{ edits: readonly CustomEditorEdit[], trigger: any | undefined }>;
readonly onDisposeEdits: Event<{ edits: readonly CustomEditorEdit[] }>;
readonly onUndo: Event<void>;
readonly onRedo: Event<void>;
readonly onRevert: Event<void>;
readonly onWillSave: Event<CustomEditorSaveEvent>;
readonly onWillSaveAs: Event<CustomEditorSaveAsEvent>;
onBackup(f: () => CancelablePromise<boolean>): void;
setDirty(dirty: boolean): void;
undo(): void;
redo(): void;
revert(options?: IRevertOptions): Promise<boolean>;
save(options?: ISaveOptions): Promise<boolean>;
saveAs(resource: URI, targetResource: URI, currentOptions?: ISaveOptions): Promise<boolean>;
pushEdit(edit: CustomEditorEdit, trigger: any): void;
}
export const enum CustomEditorPriority {
......
......@@ -8,7 +8,7 @@ import { Emitter, Event } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { IRevertOptions, ISaveOptions } from 'vs/workbench/common/editor';
import { CustomEditorEdit, CustomEditorSaveAsEvent, CustomEditorSaveEvent, ICustomEditorModel } from 'vs/workbench/contrib/customEditor/common/customEditor';
import { CustomEditorSaveAsEvent, CustomEditorSaveEvent, ICustomEditorModel } from 'vs/workbench/contrib/customEditor/common/customEditor';
import { IWorkingCopyBackup, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService';
import { ILabelService } from 'vs/platform/label/common/label';
import { basename } from 'vs/base/common/path';
......@@ -38,10 +38,8 @@ namespace HotExitState {
export class CustomEditorModel extends Disposable implements ICustomEditorModel {
private _currentEditIndex: number = -1;
private _savePoint: number = -1;
private readonly _edits: Array<CustomEditorEdit> = [];
private _hotExitState: HotExitState.State = HotExitState.NotSupported;
private _dirty = false;
constructor(
public readonly viewType: string,
......@@ -51,11 +49,6 @@ export class CustomEditorModel extends Disposable implements ICustomEditorModel
super();
}
dispose() {
this._onDisposeEdits.fire({ edits: this._edits });
super.dispose();
}
//#region IWorkingCopy
public get resource() {
......@@ -71,31 +64,31 @@ export class CustomEditorModel extends Disposable implements ICustomEditorModel
}
public isDirty(): boolean {
return this._edits.length > 0 && this._savePoint !== this._currentEditIndex;
return this._dirty;
}
protected readonly _onDidChangeDirty: Emitter<void> = this._register(new Emitter<void>());
private readonly _onDidChangeDirty: Emitter<void> = this._register(new Emitter<void>());
readonly onDidChangeDirty: Event<void> = this._onDidChangeDirty.event;
protected readonly _onDidChangeContent: Emitter<void> = this._register(new Emitter<void>());
private readonly _onDidChangeContent: Emitter<void> = this._register(new Emitter<void>());
readonly onDidChangeContent: Event<void> = this._onDidChangeContent.event;
//#endregion
protected readonly _onUndo = this._register(new Emitter<{ edits: readonly CustomEditorEdit[], trigger: any | undefined }>());
readonly onUndo = this._onUndo.event;
private readonly _onUndo = this._register(new Emitter<void>());
public readonly onUndo = this._onUndo.event;
protected readonly _onApplyEdit = this._register(new Emitter<{ edits: readonly CustomEditorEdit[], trigger: any | undefined }>());
readonly onApplyEdit = this._onApplyEdit.event;
private readonly _onRedo = this._register(new Emitter<void>());
public readonly onRedo = this._onRedo.event;
protected readonly _onDisposeEdits = this._register(new Emitter<{ edits: readonly CustomEditorEdit[] }>());
readonly onDisposeEdits = this._onDisposeEdits.event;
private readonly _onRevert = this._register(new Emitter<void>());
public readonly onRevert = this._onRevert.event;
protected readonly _onWillSave = this._register(new Emitter<CustomEditorSaveEvent>());
readonly onWillSave = this._onWillSave.event;
private readonly _onWillSave = this._register(new Emitter<CustomEditorSaveEvent>());
public readonly onWillSave = this._onWillSave.event;
protected readonly _onWillSaveAs = this._register(new Emitter<CustomEditorSaveAsEvent>());
readonly onWillSaveAs = this._onWillSaveAs.event;
private readonly _onWillSaveAs = this._register(new Emitter<CustomEditorSaveAsEvent>());
public readonly onWillSaveAs = this._onWillSaveAs.event;
private _onBackup: undefined | (() => CancelablePromise<boolean>);
......@@ -110,38 +103,30 @@ export class CustomEditorModel extends Disposable implements ICustomEditorModel
}
}
public pushEdit(edit: CustomEditorEdit, trigger: any) {
this.spliceEdits(edit);
public setDirty(dirty: boolean): void {
this._onDidChangeContent.fire();
this._currentEditIndex = this._edits.length - 1;
this.updateDirty();
this._onApplyEdit.fire({ edits: [edit], trigger });
this.updateContentChanged();
if (this._dirty !== dirty) {
this._dirty = dirty;
this._onDidChangeDirty.fire();
}
}
private spliceEdits(editToInsert?: CustomEditorEdit) {
const start = this._currentEditIndex + 1;
const toRemove = this._edits.length - this._currentEditIndex;
const removedEdits = editToInsert
? this._edits.splice(start, toRemove, editToInsert)
: this._edits.splice(start, toRemove);
if (removedEdits.length) {
this._onDisposeEdits.fire({ edits: removedEdits });
public async revert(_options?: IRevertOptions) {
if (!this._dirty) {
return true;
}
this._onRevert.fire();
return true;
}
private updateDirty() {
// TODO@matt this should to be more fine grained and avoid
// emitting events if there was no change actually
this._onDidChangeDirty.fire();
public undo() {
this._onUndo.fire();
}
private updateContentChanged() {
// TODO@matt revisit that this method is being called correctly
// on each case of content change within the custom editor
this._onDidChangeContent.fire();
public redo() {
this._onRedo.fire();
}
public async save(_options?: ISaveOptions): Promise<boolean> {
......@@ -158,8 +143,7 @@ export class CustomEditorModel extends Disposable implements ICustomEditorModel
return false;
}
this._savePoint = this._currentEditIndex;
this.updateDirty();
this.setDirty(false);
return true;
}
......@@ -179,62 +163,11 @@ export class CustomEditorModel extends Disposable implements ICustomEditorModel
return false;
}
this._savePoint = this._currentEditIndex;
this.updateDirty();
return true;
}
public async revert(_options?: IRevertOptions) {
if (this._currentEditIndex === this._savePoint) {
return true;
}
if (this._currentEditIndex >= this._savePoint) {
const editsToUndo = this._edits.slice(this._savePoint, this._currentEditIndex);
this._onUndo.fire({ edits: editsToUndo.reverse(), trigger: undefined });
} else if (this._currentEditIndex < this._savePoint) {
const editsToRedo = this._edits.slice(this._currentEditIndex, this._savePoint);
this._onApplyEdit.fire({ edits: editsToRedo, trigger: undefined });
}
this._currentEditIndex = this._savePoint;
this.spliceEdits();
this.setDirty(false);
this.updateDirty();
this.updateContentChanged();
return true;
}
public undo() {
if (this._currentEditIndex < 0) {
// nothing to undo
return;
}
const undoneEdit = this._edits[this._currentEditIndex];
--this._currentEditIndex;
this._onUndo.fire({ edits: [undoneEdit], trigger: undefined });
this.updateDirty();
this.updateContentChanged();
}
public redo() {
if (this._currentEditIndex >= this._edits.length - 1) {
// nothing to redo
return;
}
++this._currentEditIndex;
const redoneEdit = this._edits[this._currentEditIndex];
this._onApplyEdit.fire({ edits: [redoneEdit], trigger: undefined });
this.updateDirty();
this.updateContentChanged();
}
public async backup(): Promise<IWorkingCopyBackup> {
if (this._hotExitState === HotExitState.NotSupported) {
throw new Error('Not supported');
......
......@@ -13,19 +13,33 @@ import { ExtHostWebviews } from 'vs/workbench/api/common/extHostWebview';
import { EditorViewColumn } from 'vs/workbench/api/common/shared/editor';
import { mock } from 'vs/workbench/test/browser/api/mock';
import { SingleProxyRPCProtocol } from './testRPCProtocol';
import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors';
import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments';
import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService';
import { IExtHostContext } from 'vs/workbench/api/common/extHost.protocol';
import { NullApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService';
suite('ExtHostWebview', () => {
let rpcProtocol: (IExtHostRpcService & IExtHostContext) | undefined;
let extHostDocuments: ExtHostDocuments | undefined;
setup(() => {
const shape = createNoopMainThreadWebviews();
rpcProtocol = SingleProxyRPCProtocol(shape);
const extHostDocumentsAndEditors = new ExtHostDocumentsAndEditors(rpcProtocol, new NullLogService());
extHostDocuments = new ExtHostDocuments(rpcProtocol, extHostDocumentsAndEditors);
});
test('Cannot register multiple serializers for the same view type', async () => {
const viewType = 'view.type';
const shape = createNoopMainThreadWebviews();
const extHostWebviews = new ExtHostWebviews(SingleProxyRPCProtocol(shape), {
const extHostWebviews = new ExtHostWebviews(rpcProtocol!, {
webviewCspSource: '',
webviewResourceRoot: '',
isExtensionDevelopmentDebug: false,
}, undefined, new NullLogService(), NullApiDeprecationService);
}, undefined, new NullLogService(), NullApiDeprecationService, extHostDocuments!);
let lastInvokedDeserializer: vscode.WebviewPanelSerializer | undefined = undefined;
......@@ -58,12 +72,11 @@ suite('ExtHostWebview', () => {
});
test('asWebviewUri for desktop vscode-resource scheme', () => {
const shape = createNoopMainThreadWebviews();
const extHostWebviews = new ExtHostWebviews(SingleProxyRPCProtocol(shape), {
const extHostWebviews = new ExtHostWebviews(rpcProtocol!, {
webviewCspSource: '',
webviewResourceRoot: 'vscode-resource://{{resource}}',
isExtensionDevelopmentDebug: false,
}, undefined, new NullLogService(), NullApiDeprecationService);
}, undefined, new NullLogService(), NullApiDeprecationService, extHostDocuments!);
const webview = extHostWebviews.createWebviewPanel({} as any, 'type', 'title', 1, {});
assert.strictEqual(
......@@ -98,13 +111,11 @@ suite('ExtHostWebview', () => {
});
test('asWebviewUri for web endpoint', () => {
const shape = createNoopMainThreadWebviews();
const extHostWebviews = new ExtHostWebviews(SingleProxyRPCProtocol(shape), {
const extHostWebviews = new ExtHostWebviews(rpcProtocol!, {
webviewCspSource: '',
webviewResourceRoot: `https://{{uuid}}.webview.contoso.com/commit/{{resource}}`,
isExtensionDevelopmentDebug: false,
}, undefined, new NullLogService(), NullApiDeprecationService);
}, undefined, new NullLogService(), NullApiDeprecationService, extHostDocuments!);
const webview = extHostWebviews.createWebviewPanel({} as any, 'type', 'title', 1, {});
function stripEndpointUuid(input: string) {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册