Add experimental moveCustomTextEditor hook

For #77131

Adds a hook that lets extensions preserve the webview for a custom editor across a rename
This commit is contained in:
Matt Bierner
2020-03-13 17:49:52 -07:00
parent c81074fb70
commit 041a5907b1
10 changed files with 111 additions and 26 deletions

View File

@@ -1473,6 +1473,21 @@ declare module 'vscode' {
* @return Thenable indicating that the webview editor has been resolved.
*/
resolveCustomTextEditor(document: TextDocument, webviewPanel: WebviewPanel): Thenable<void>;
/**
* TODO: discuss this at api sync.
*
* Handle when the underlying resource for a custom editor is renamed.
*
* This allows the webview for the editor be preserved throughout the rename. If this method is not implemented,
* VS Code will destory the previous custom editor and create a replacement one.
*
* @param newDocument New text document to use for the custom editor.
* @param existingWebviewPanel Webview panel for the custom editor.
*
* @return Thenable indicating that the webview editor has been moved.
*/
moveCustomTextEditor?(newDocument: TextDocument, existingWebviewPanel: WebviewPanel): Thenable<void>;
}
namespace window {

View File

@@ -576,6 +576,10 @@ export interface WebviewExtensionDescription {
readonly location: UriComponents;
}
export interface CustomTextEditorCapabilities {
readonly supportsMove?: boolean;
}
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;
@@ -591,7 +595,7 @@ export interface MainThreadWebviewsShape extends IDisposable {
$registerSerializer(viewType: string): void;
$unregisterSerializer(viewType: string): void;
$registerTextEditorProvider(extension: WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions): void;
$registerTextEditorProvider(extension: WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions, capabilities: CustomTextEditorCapabilities): void;
$registerCustomEditorProvider(extension: WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions): void;
$unregisterEditorProvider(viewType: string): void;
@@ -627,6 +631,8 @@ export interface ExtHostWebviewsShape {
$onSaveAs(resource: UriComponents, viewType: string, targetResource: UriComponents): Promise<void>;
$backup(resource: UriComponents, viewType: string, cancellation: CancellationToken): Promise<void>;
$onMoveCustomEditor(handle: WebviewPanelHandle, newResource: UriComponents, viewType: string): Promise<void>;
}
export interface MainThreadUrlsShape extends IDisposable {

View File

@@ -488,7 +488,9 @@ export class ExtHostWebviews implements ExtHostWebviewsShape {
const disposables = new DisposableStore();
if ('resolveCustomTextEditor' in provider) {
disposables.add(this._editorProviders.addTextProvider(viewType, extension, provider));
this._proxy.$registerTextEditorProvider(toExtensionData(extension), viewType, options);
this._proxy.$registerTextEditorProvider(toExtensionData(extension), viewType, options, {
supportsMove: !!provider.moveCustomTextEditor,
});
} else {
disposables.add(this._editorProviders.addCustomProvider(viewType, extension, provider));
this._proxy.$registerCustomEditorProvider(toExtensionData(extension), viewType, options);
@@ -663,6 +665,26 @@ export class ExtHostWebviews implements ExtHostWebviewsShape {
document._disposeEdits(editIds);
}
async $onMoveCustomEditor(handle: string, newResourceComponents: UriComponents, viewType: string): Promise<void> {
const entry = this._editorProviders.get(viewType);
if (!entry) {
throw new Error(`No provider found for '${viewType}'`);
}
if (!(entry.provider as vscode.CustomTextEditorProvider).moveCustomTextEditor) {
throw new Error(`Provider does not implement move '${viewType}'`);
}
const webview = this.getWebviewPanel(handle);
if (!webview) {
throw new Error(`No webview found`);
}
const resource = URI.revive(newResourceComponents);
const document = this._extHostDocuments.getDocument(resource);
await (entry.provider as vscode.CustomTextEditorProvider).moveCustomTextEditor!(document, webview);
}
async $undo(resourceComponents: UriComponents, viewType: string, editId: number): Promise<void> {
const document = this.getCustomDocument(viewType, resourceComponents);
return document._undo(editId);

View File

@@ -186,7 +186,7 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, {
}
}
const newEditorInput = customEditorService.createInput(targetResource, toggleView, activeGroup);
const newEditorInput = customEditorService.createInput(targetResource, toggleView, activeGroup.id);
editorService.replaceEditors([{
editor: activeEditor,

View File

@@ -161,6 +161,12 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput {
}
move(group: GroupIdentifier, newResource: URI): { editor: IEditorInput } | undefined {
if (!this._moveHandler) {
return {
editor: this.customEditorService.createInput(newResource, this.viewType, group)
};
}
this._moveHandler(newResource);
const newEditor = this.tryMoveWebview(group, newResource);
if (!newEditor) {
return;
@@ -171,12 +177,12 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput {
private tryMoveWebview(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(CustomEditorInput,
uri,
this.viewType,
this.id,
new Lazy(() => webview));
new Lazy(() => undefined!)); // this webview is replaced in the transfer call
this.transfer(newInput);
newInput.updateGroup(groupId);
return newInput;
}
@@ -193,4 +199,21 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput {
assertIsDefined(this._modelRef);
this.undoRedoService.redo(this.resource);
}
private _moveHandler?: (newResource: URI) => void;
public onMove(handler: (newResource: URI) => void): void {
// TODO: Move this to the service
this._moveHandler = handler;
}
protected transfer(other: CustomEditorInput): CustomEditorInput | undefined {
if (!super.transfer(other)) {
return;
}
other._moveHandler = this._moveHandler;
this._moveHandler = undefined;
return other;
}
}

View File

@@ -21,7 +21,7 @@ import * as colorRegistry from 'vs/platform/theme/common/colorRegistry';
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor';
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { EditorInput, EditorOptions, IEditorInput, IEditorPane } from 'vs/workbench/common/editor';
import { EditorInput, EditorOptions, IEditorInput, IEditorPane, GroupIdentifier } from 'vs/workbench/common/editor';
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
import { webviewEditorsExtensionPoint } from 'vs/workbench/contrib/customEditor/browser/extensionPoint';
import { CONTEXT_CUSTOM_EDITORS, CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE, CustomEditorInfo, CustomEditorInfoCollection, CustomEditorPriority, CustomEditorSelector, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor';
@@ -238,14 +238,14 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ
return this.promptOpenWith(resource, options, group);
}
const input = this.createInput(resource, viewType, group);
const input = this.createInput(resource, viewType, group?.id);
return this.openEditorForResource(resource, input, options, group);
}
public createInput(
resource: URI,
viewType: string,
group: IEditorGroup | undefined,
group: GroupIdentifier | undefined,
options?: { readonly customClasses: string; },
): IEditorInput {
if (viewType === defaultEditorId) {
@@ -257,8 +257,8 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ
return this.webviewService.createWebviewOverlay(id, { customClasses: options?.customClasses }, {});
});
const input = this.instantiationService.createInstance(CustomEditorInput, resource, viewType, id, webview);
if (group) {
input.updateGroup(group.id);
if (typeof group !== 'undefined') {
input.updateGroup(group);
}
return input;
}
@@ -327,7 +327,7 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ
}
const moveResult = editor.move(group.id, newResource);
const replacement = moveResult ? moveResult.editor : this.createInput(newResource, editor.viewType, group);
const replacement = moveResult ? moveResult.editor : this.createInput(newResource, editor.viewType, group.id);
this.editorService.replaceEditors([{
editor: editor,
@@ -492,7 +492,7 @@ export class CustomEditorContribution extends Disposable implements IWorkbenchCo
return undefined;
}
const input = this.customEditorService.createInput(resource, bestAvailableEditor.id, group, { customClasses });
const input = this.customEditorService.createInput(resource, bestAvailableEditor.id, group.id, { customClasses });
if (input instanceof EditorInput) {
return input;
}

View File

@@ -6,14 +6,14 @@
import { distinct, mergeSort } from 'vs/base/common/arrays';
import { Event } from 'vs/base/common/event';
import * as glob from 'vs/base/common/glob';
import { IDisposable, IReference } from 'vs/base/common/lifecycle';
import { basename } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { ITextEditorOptions } from 'vs/platform/editor/common/editor';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IEditorPane, IEditorInput, IRevertOptions, ISaveOptions } from 'vs/workbench/common/editor';
import { GroupIdentifier, IEditorInput, IEditorPane, IRevertOptions, ISaveOptions } from 'vs/workbench/common/editor';
import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IDisposable, IReference } from 'vs/base/common/lifecycle';
export const ICustomEditorService = createDecorator<ICustomEditorService>('customEditorService');
@@ -29,7 +29,7 @@ export interface ICustomEditorService {
getContributedCustomEditors(resource: URI): CustomEditorInfoCollection;
getUserConfiguredCustomEditors(resource: URI): CustomEditorInfoCollection;
createInput(resource: URI, viewType: string, group: IEditorGroup | undefined, options?: { readonly customClasses: string }): IEditorInput;
createInput(resource: URI, viewType: string, group: GroupIdentifier | undefined, options?: { readonly customClasses: string }): IEditorInput;
openWith(resource: URI, customEditorViewType: string, options?: ITextEditorOptions, group?: IEditorGroup): Promise<IEditorPane | undefined>;
promptOpenWith(resource: URI, options?: ITextEditorOptions, group?: IEditorGroup): Promise<IEditorPane | undefined>;

View File

@@ -109,7 +109,8 @@ export class WebviewEditor extends BaseEditor {
return;
}
if (this.webview) {
const alreadyOwnsWebview = input instanceof WebviewInput && input.webview === this.webview;
if (this.webview && !alreadyOwnsWebview) {
this.webview.release(this);
}
@@ -125,7 +126,9 @@ export class WebviewEditor extends BaseEditor {
input.updateGroup(this.group.id);
}
this.claimWebview(input);
if (!alreadyOwnsWebview) {
this.claimWebview(input);
}
if (this._dimension) {
this.layout(this._dimension);
}

View File

@@ -18,8 +18,9 @@ export class WebviewInput extends EditorInput {
private _iconPath?: WebviewIcons;
private _group?: GroupIdentifier;
private readonly _webview: Lazy<WebviewOverlay>;
private _didSomeoneTakeMyWebview = false;
private _webview: Lazy<WebviewOverlay>;
private _hasTransfered = false;
get resource() {
return URI.from({
@@ -42,8 +43,8 @@ export class WebviewInput extends EditorInput {
dispose() {
if (!this.isDisposed()) {
if (!this._didSomeoneTakeMyWebview) {
this._webview?.rawValue?.dispose();
if (!this._hasTransfered) {
this._webview.rawValue?.dispose();
}
}
super.dispose();
@@ -107,11 +108,12 @@ export class WebviewInput extends EditorInput {
return false;
}
protected takeOwnershipOfWebview(): WebviewOverlay | undefined {
if (this._didSomeoneTakeMyWebview) {
protected transfer(other: WebviewInput): WebviewInput | undefined {
if (this._hasTransfered) {
return undefined;
}
this._didSomeoneTakeMyWebview = true;
return this.webview;
this._hasTransfered = true;
other._webview = this._webview;
return other;
}
}

View File

@@ -113,11 +113,25 @@ export class LazilyResolvedWebviewEditorInput extends WebviewInput {
super(id, viewType, name, webview, webviewService);
}
#resolved = false;
@memoize
public async resolve() {
await this._webviewWorkbenchService.resolveWebview(this);
if (!this.#resolved) {
this.#resolved = true;
await this._webviewWorkbenchService.resolveWebview(this);
}
return super.resolve();
}
protected transfer(other: LazilyResolvedWebviewEditorInput): WebviewInput | undefined {
if (!super.transfer(other)) {
return;
}
other.#resolved = this.#resolved;
return other;
}
}