diff --git a/src/vs/base/common/lifecycle.ts b/src/vs/base/common/lifecycle.ts index 8d1cbd5b996..651c5fa22a4 100644 --- a/src/vs/base/common/lifecycle.ts +++ b/src/vs/base/common/lifecycle.ts @@ -206,43 +206,6 @@ export class MutableDisposable implements IDisposable { } } -/** - * Wrapper class that stores a disposable that is not currently "owned" by anyone. - * - * Example use cases: - * - * - Express that a function/method will take ownership of a disposable parameter. - * - Express that a function returns a disposable that the caller must explicitly take ownership of. - */ -export class UnownedDisposable extends Disposable { - private _hasBeenAcquired = false; - private _value?: T; - - public constructor(value: T) { - super(); - this._value = value; - } - - public acquire(): T { - if (this._hasBeenAcquired) { - throw new Error('This disposable has already been acquired'); - } - this._hasBeenAcquired = true; - const value = this._value!; - this._value = undefined; - return value; - } - - public dispose() { - super.dispose(); - if (!this._hasBeenAcquired) { - this._hasBeenAcquired = true; - this._value!.dispose(); - this._value = undefined; - } - } -} - export interface IReference extends IDisposable { readonly object: T; } diff --git a/src/vs/workbench/api/browser/mainThreadWebview.ts b/src/vs/workbench/api/browser/mainThreadWebview.ts index a27f62c02d5..1ba9ed80383 100644 --- a/src/vs/workbench/api/browser/mainThreadWebview.ts +++ b/src/vs/workbench/api/browser/mainThreadWebview.ts @@ -278,7 +278,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma model.onWillSave(e => { e.waitUntil(this._proxy.$onSave(handle)); }); model.onWillSaveAs(e => { e.waitUntil(this._proxy.$onSaveAs(handle, e.resource.toJSON(), e.targetResource.toJSON())); }); - webviewInput.onDispose(() => { + webviewInput.onDisposeWebview(() => { this._customEditorService.models.disposeModel(model); }); @@ -326,7 +326,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma private hookupWebviewEventDelegate(handle: extHostProtocol.WebviewPanelHandle, input: WebviewInput) { input.webview.onDidClickLink((uri: URI) => this.onDidClickLink(handle, uri)); input.webview.onMessage((message: any) => this._proxy.$onMessage(handle, message)); - input.onDispose(() => { + input.onDisposeWebview(() => { this._proxy.$onDidDisposeWebviewPanel(handle).finally(() => { this._webviewInputs.delete(handle); }); diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index c2f430cfefe..d3d6a4e6693 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -398,6 +398,11 @@ export interface IEditorInput extends IDisposable { */ saveAs(groupId: GroupIdentifier, options?: ISaveOptions): Promise; + /** + * Handles when the input is replaced, such as by renaming its backing resource. + */ + handleMove?(groupId: GroupIdentifier, uri: URI, options?: ITextEditorOptions): IEditorInput | undefined; + /** * Reverts this input. */ diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts index 397cae24e28..a6d5a037799 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts @@ -5,12 +5,12 @@ import { memoize } from 'vs/base/common/decorators'; import { Lazy } from 'vs/base/common/lazy'; -import { UnownedDisposable } from 'vs/base/common/lifecycle'; import { basename } from 'vs/base/common/path'; import { isEqual } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { IEditorModel } from 'vs/platform/editor/common/editor'; +import { IEditorModel, ITextEditorOptions } from 'vs/platform/editor/common/editor'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILabelService } from 'vs/platform/label/common/label'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { GroupIdentifier, IEditorInput, IRevertOptions, ISaveOptions, Verbosity } from 'vs/workbench/common/editor'; @@ -18,6 +18,7 @@ import { ICustomEditorModel, ICustomEditorService } from 'vs/workbench/contrib/c import { WebviewEditorOverlay } from 'vs/workbench/contrib/webview/browser/webview'; import { IWebviewWorkbenchService, LazilyResolvedWebviewEditorInput } from 'vs/workbench/contrib/webview/browser/webviewWorkbenchService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { assertIsDefined } from 'vs/base/common/types'; export class CustomFileEditorInput extends LazilyResolvedWebviewEditorInput { @@ -30,9 +31,10 @@ export class CustomFileEditorInput extends LazilyResolvedWebviewEditorInput { resource: URI, viewType: string, id: string, - webview: Lazy>, + webview: Lazy, @ILifecycleService lifecycleService: ILifecycleService, @IWebviewWorkbenchService webviewWorkbenchService: IWebviewWorkbenchService, + @IInstantiationService private readonly instantiationService: IInstantiationService, @ILabelService private readonly labelService: ILabelService, @ICustomEditorService private readonly customEditorService: ICustomEditorService, @IEditorService private readonly editorService: IEditorService, @@ -150,4 +152,13 @@ export class CustomFileEditorInput extends LazilyResolvedWebviewEditorInput { return this.fileDialogService.pickFileToSave({});//this.getSaveDialogOptions(defaultUri, availableFileSystems)); } + + public handleMove(groupId: GroupIdentifier, uri: URI, options?: ITextEditorOptions): IEditorInput | undefined { + const webview = assertIsDefined(this.takeOwnershipOfWebview()); + return this.instantiationService.createInstance(CustomFileEditorInput, + uri, + this.viewType, + this.id, + new Lazy(() => webview)); + } } diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts index 8739a05780a..866f96ef5f5 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { UnownedDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -48,7 +47,7 @@ export class CustomEditorInputFactory extends WebviewEditorInputFactory { location: data.extensionLocation, id: data.extensionId } : undefined, data.group); - return new UnownedDisposable(webviewInput.webview); + return webviewInput.webview; }); const customInput = this._instantiationService.createInstance(CustomFileEditorInput, URI.from((data as any).editorResource), data.viewType, id, webview); diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts index 814c4d6b301..9e87a796443 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts @@ -6,7 +6,7 @@ import { coalesce, distinct, find, mergeSort } from 'vs/base/common/arrays'; import * as glob from 'vs/base/common/glob'; import { Lazy } from 'vs/base/common/lazy'; -import { Disposable, UnownedDisposable } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; import { basename, isEqual } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; @@ -201,7 +201,7 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ ): CustomFileEditorInput { const id = generateUuid(); const webview = new Lazy(() => { - return new UnownedDisposable(this.webviewService.createWebviewEditorOverlay(id, { customClasses: options?.customClasses }, {})); + return this.webviewService.createWebviewEditorOverlay(id, { customClasses: options?.customClasses }, {}); }); const input = this.instantiationService.createInstance(CustomFileEditorInput, resource, viewType, id, webview); if (group) { diff --git a/src/vs/workbench/contrib/files/browser/editors/fileEditorTracker.ts b/src/vs/workbench/contrib/files/browser/editors/fileEditorTracker.ts index ca9dba559e2..bcc39a73b37 100644 --- a/src/vs/workbench/contrib/files/browser/editors/fileEditorTracker.ts +++ b/src/vs/workbench/contrib/files/browser/editors/fileEditorTracker.ts @@ -27,7 +27,7 @@ import { IEditorGroupsService, IEditorGroup } from 'vs/workbench/services/editor import { ResourceQueue, timeout } from 'vs/base/common/async'; import { onUnexpectedError } from 'vs/base/common/errors'; import { withNullAsUndefined } from 'vs/base/common/types'; -import { EditorActivation } from 'vs/platform/editor/common/editor'; +import { EditorActivation, ITextEditorOptions } from 'vs/platform/editor/common/editor'; export class FileEditorTracker extends Disposable implements IWorkbenchContribution { @@ -215,8 +215,8 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut private handleMovedFileInOpenedEditors(oldResource: URI, newResource: URI): void { this.editorGroupService.groups.forEach(group => { group.editors.forEach(editor => { - if (editor instanceof FileEditorInput) { - const resource = editor.getResource(); + const resource = editor.getResource(); + if (resource && (editor instanceof FileEditorInput || editor.handleMove)) { // Update Editor if file (or any parent of the input) got renamed or moved if (resources.isEqualOrParent(resource, oldResource)) { @@ -228,15 +228,27 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut reopenFileResource = resources.joinPath(newResource, resource.path.substr(index + oldResource.path.length + 1)); // parent folder got moved } + const options: ITextEditorOptions = { + preserveFocus: true, + pinned: group.isPinned(editor), + index: group.getIndexOfEditor(editor), + inactive: !group.isActive(editor), + }; + + if (editor.handleMove) { + const replacement = editor.handleMove(group.id, reopenFileResource, options); + if (replacement) { + this.editorService.replaceEditors([{ editor, replacement }], group); + return; + } + } + this.editorService.replaceEditors([{ editor: { resource }, replacement: { resource: reopenFileResource, options: { - preserveFocus: true, - pinned: group.isPinned(editor), - index: group.getIndexOfEditor(editor), - inactive: !group.isActive(editor), + ...options, viewState: this.getViewStateFor(oldResource, group) } }, diff --git a/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts b/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts index 945e6be9f4b..16b5b7e7d1d 100644 --- a/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts +++ b/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts @@ -99,7 +99,7 @@ export class DynamicWebviewEditorOverlay extends Disposable implements WebviewEd if (this._options.tryRestoreScrollPosition) { webview.initialScrollProgress = this._initialScrollProgress; } - this._webview.value.mountTo(this.container); + webview.mountTo(this.container); // Forward events from inner webview to outer listeners this._webviewEvents.clear(); diff --git a/src/vs/workbench/contrib/webview/browser/webviewEditorInput.ts b/src/vs/workbench/contrib/webview/browser/webviewEditorInput.ts index 4577a20a046..eb930086188 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewEditorInput.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewEditorInput.ts @@ -6,12 +6,12 @@ import * as dom from 'vs/base/browser/dom'; import { memoize } from 'vs/base/common/decorators'; import { Lazy } from 'vs/base/common/lazy'; -import { UnownedDisposable as Unowned } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { IEditorModel } from 'vs/platform/editor/common/editor'; import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { EditorInput, EditorModel, GroupIdentifier, IEditorInput, Verbosity } from 'vs/workbench/common/editor'; import { WebviewEditorOverlay } from 'vs/workbench/contrib/webview/browser/webview'; +import { Emitter } from 'vs/base/common/event'; const WebviewPanelResourceScheme = 'webview-panel'; @@ -70,20 +70,31 @@ export class WebviewInput extends EditorInput { private _name: string; private _iconPath?: { light: URI, dark: URI }; private _group?: GroupIdentifier; + private readonly _webview: Lazy; + private _didSomeoneTakeMyWebview = false; + + private readonly _onDisposeWebview = this._register(new Emitter()); + readonly onDisposeWebview = this._onDisposeWebview.event; constructor( public readonly id: string, public readonly viewType: string, name: string, - webview: Lazy>, + webview: Lazy, @ILifecycleService private readonly lifecycleService: ILifecycleService, ) { super(); - this._name = name; + this._webview = webview; + } - this._webview = webview.map(value => this._register(value.acquire())); // The input owns this webview + dispose() { + super.dispose(); + if (!this._didSomeoneTakeMyWebview) { + this._webview?.rawValue?.dispose(); + this._onDisposeWebview.fire(); + } } public getTypeId(): string { @@ -119,7 +130,7 @@ export class WebviewInput extends EditorInput { } public get extension() { - return this._webview.getValue().extension; + return this.webview.extension; } public get iconPath() { @@ -150,4 +161,12 @@ export class WebviewInput extends EditorInput { public supportsSplitEditor() { return false; } + + protected takeOwnershipOfWebview(): WebviewEditorOverlay | undefined { + if (this._didSomeoneTakeMyWebview) { + return undefined; + } + this._didSomeoneTakeMyWebview = true; + return this.webview; + } } diff --git a/src/vs/workbench/contrib/webview/browser/webviewWorkbenchService.ts b/src/vs/workbench/contrib/webview/browser/webviewWorkbenchService.ts index ee635fbe60a..67011891137 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewWorkbenchService.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewWorkbenchService.ts @@ -6,7 +6,7 @@ import { equals } from 'vs/base/common/arrays'; import { memoize } from 'vs/base/common/decorators'; import { Lazy } from 'vs/base/common/lazy'; -import { IDisposable, toDisposable, UnownedDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { values } from 'vs/base/common/map'; import { isEqual } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; @@ -108,7 +108,7 @@ export class LazilyResolvedWebviewEditorInput extends WebviewInput { id: string, viewType: string, name: string, - webview: Lazy>, + webview: Lazy, @IWebviewWorkbenchService private readonly _webviewWorkbenchService: IWebviewWorkbenchService, @ILifecycleService lifeCycleService: ILifecycleService, ) { @@ -161,7 +161,7 @@ export class WebviewEditorService implements IWebviewWorkbenchService { options: WebviewInputOptions, extension: WebviewExtensionDescription | undefined, ): WebviewInput { - const webview = new Lazy(() => new UnownedDisposable(this.createWebiew(id, extension, options))); + const webview = new Lazy(() => this.createWebiew(id, extension, options)); const webviewInput = this._instantiationService.createInstance(WebviewInput, id, viewType, title, webview); this._editorService.openEditor(webviewInput, { pinned: true, @@ -206,7 +206,7 @@ export class WebviewEditorService implements IWebviewWorkbenchService { const webview = new Lazy(() => { const webview = this.createWebiew(id, extension, options); webview.state = state; - return new UnownedDisposable(webview); + return webview; }); const webviewInput = this._instantiationService.createInstance(LazilyResolvedWebviewEditorInput, id, viewType, title, webview);