diff --git a/src/vs/workbench/api/browser/mainThreadWebview.ts b/src/vs/workbench/api/browser/mainThreadWebview.ts index 773222635a11332379ed0eb76515acdc87805961..125faebe116eddbc8df2da85e4addebcfffe0dd3 100644 --- a/src/vs/workbench/api/browser/mainThreadWebview.ts +++ b/src/vs/workbench/api/browser/mainThreadWebview.ts @@ -3,31 +3,37 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { createCancelablePromise } from 'vs/base/common/async'; +import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable, DisposableStore, IDisposable, IReference } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; +import { basename } from 'vs/base/common/path'; import { isWeb } from 'vs/base/common/platform'; import { URI, UriComponents } from 'vs/base/common/uri'; import * as modes from 'vs/editor/common/modes'; import { localize } from 'vs/nls'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { IFileService } from 'vs/platform/files/common/files'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ILabelService } from 'vs/platform/label/common/label'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IProductService } from 'vs/platform/product/common/productService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; 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 { IEditorInput, IRevertOptions, ISaveOptions } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; -import { CustomEditorInput, ModelType } from 'vs/workbench/contrib/customEditor/browser/customEditorInput'; +import { CustomEditorInput } from 'vs/workbench/contrib/customEditor/browser/customEditorInput'; import { ICustomEditorModel, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; +import { CustomTextEditorModel } from 'vs/workbench/contrib/customEditor/common/customTextEditorModel'; import { WebviewExtensionDescription, WebviewIcons } from 'vs/workbench/contrib/webview/browser/webview'; import { WebviewInput } from 'vs/workbench/contrib/webview/browser/webviewEditorInput'; import { ICreateWebViewShowOptions, IWebviewWorkbenchService, WebviewInputOptions } from 'vs/workbench/contrib/webview/browser/webviewWorkbenchService'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IWorkingCopy, IWorkingCopyBackup, IWorkingCopyService, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { extHostNamedCustomer } from '../common/extHostCustomers'; /** @@ -79,6 +85,11 @@ class WebviewViewTypeTransformer { } } +const enum ModelType { + Custom, + Text, +} + const webviewPanelViewType = new WebviewViewTypeTransformer('mainThreadWebview-'); @extHostNamedCustomer(extHostProtocol.MainContext.MainThreadWebviews) @@ -96,7 +107,6 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma private readonly _webviewInputs = new WebviewInputStore(); private readonly _revivers = new Map(); private readonly _editorProviders = new Map(); - private readonly _customEditorModels = new Map(); constructor( context: extHostProtocol.IExtHostContext, @@ -108,7 +118,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma @IProductService private readonly _productService: IProductService, @ITelemetryService private readonly _telemetryService: ITelemetryService, @IWebviewWorkbenchService private readonly _webviewWorkbenchService: IWebviewWorkbenchService, - @IFileService private readonly _fileService: IFileService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { super(); @@ -286,16 +296,13 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma webviewInput.webview.options = options; webviewInput.webview.extension = extension; - webviewInput.modelType = modelType; const resource = webviewInput.resource; - if (modelType === ModelType.Custom) { - const model = await this.retainCustomEditorModel(webviewInput, resource, viewType); - webviewInput.onDisposeWebview(() => { - this.releaseCustomEditorModel(model); - }); - } + const modelRef = await this.getOrCreateCustomEditorModel(modelType, webviewInput, resource, viewType); + webviewInput.onDisposeWebview(() => { + modelRef.dispose(); + }); try { await this._proxy.$resolveWebviewEditor( @@ -327,71 +334,27 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma this._customEditorService.models.disposeAllModelsForView(viewType); } - private async retainCustomEditorModel(webviewInput: WebviewInput, resource: URI, viewType: string) { - const model = await this._customEditorService.models.resolve(webviewInput.resource, webviewInput.viewType); - - 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); - - if (editable) { - model.onUndo(() => { - this._proxy.$undo(resource, viewType); - }); - - model.onRedo(() => { - this._proxy.$redo(resource, viewType); - }); - - model.onWillSave(e => { - e.waitUntil(this._proxy.$onSave(resource.toJSON(), viewType)); - }); + private async getOrCreateCustomEditorModel( + modelType: ModelType, + webviewInput: WebviewInput, + resource: URI, + viewType: string, + ): Promise> { + const existingModel = this._customEditorService.models.tryRetain(webviewInput.resource, webviewInput.viewType); + if (existingModel) { + return existingModel; } - // Save as should always be implemented even if the model is readonly - model.onWillSaveAs(e => { - 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 - e.waitUntil(this._fileService.copy(e.resource, e.targetResource, false /* overwrite */)); - } - }); - - model.onBackup(() => { - return createCancelablePromise(token => - this._proxy.$backup(model.resource.toJSON(), viewType, token)); - }); + const model = modelType === ModelType.Text + ? CustomTextEditorModel.create(this._instantiationService, viewType, resource) + : MainThreadCustomEditorModel.create(this._instantiationService, this._proxy, viewType, resource); - return model; - } - - private async releaseCustomEditorModel(model: ICustomEditorModel) { - const key = model.viewType + model.resource; - const entry = this._customEditorModels.get(key); - if (!entry) { - 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(key); - } + return this._customEditorService.models.add(resource, viewType, model); } - - - public $onDidChangeCustomDocumentState(resource: UriComponents, viewType: string, state: { dirty: boolean }) { - const model = this._customEditorService.models.get(URI.revive(resource), viewType); - if (!model) { + public async $onDidChangeCustomDocumentState(resource: UriComponents, viewType: string, state: { dirty: boolean }) { + const model = await this._customEditorService.models.get(URI.revive(resource), viewType); + if (!model || !(model instanceof MainThreadCustomEditorModel)) { throw new Error('Could not find model for webview editor'); } model.setDirty(state.dirty); @@ -515,3 +478,152 @@ function reviveWebviewIcon( ? { light: URI.revive(value.light), dark: URI.revive(value.dark) } : undefined; } + +namespace HotExitState { + export const enum Type { + Allowed, + NotAllowed, + Pending, + } + + export const Allowed = Object.freeze({ type: Type.Allowed } as const); + export const NotAllowed = Object.freeze({ type: Type.NotAllowed } as const); + + export class Pending { + readonly type = Type.Pending; + + constructor( + public readonly operation: CancelablePromise, + ) { } + } + + export type State = typeof Allowed | typeof NotAllowed | Pending; +} + +class MainThreadCustomEditorModel extends Disposable implements ICustomEditorModel, IWorkingCopy { + + private _hotExitState: HotExitState.State = HotExitState.Allowed; + private _dirty = false; + + public static async create(instantiationService: IInstantiationService, proxy: extHostProtocol.ExtHostWebviewsShape, viewType: string, resource: URI) { + const { editable } = await proxy.$createWebviewCustomEditorDocument(resource, viewType); + return instantiationService.createInstance(MainThreadCustomEditorModel, proxy, viewType, resource, editable); + } + + constructor( + private readonly _proxy: extHostProtocol.ExtHostWebviewsShape, + public readonly viewType: string, + private readonly _resource: URI, + private readonly _editable: boolean, + @IWorkingCopyService workingCopyService: IWorkingCopyService, + @ILabelService private readonly _labelService: ILabelService, + @IFileService private readonly _fileService: IFileService, + ) { + super(); + this._register(workingCopyService.registerWorkingCopy(this)); + } + + dispose() { + this._proxy.$disposeWebviewCustomEditorDocument(this.resource, this.viewType); + super.dispose(); + } + + //#region IWorkingCopy + + public get resource() { + return this._resource; + } + + public get name() { + return basename(this._labelService.getUriLabel(this._resource)); + } + + public get capabilities(): WorkingCopyCapabilities { + return 0; + } + + public isDirty(): boolean { + return this._dirty; + } + + private readonly _onDidChangeDirty: Emitter = this._register(new Emitter()); + readonly onDidChangeDirty: Event = this._onDidChangeDirty.event; + + private readonly _onDidChangeContent: Emitter = this._register(new Emitter()); + readonly onDidChangeContent: Event = this._onDidChangeContent.event; + + //#endregion + + public setDirty(dirty: boolean): void { + this._onDidChangeContent.fire(); + + if (this._dirty !== dirty) { + this._dirty = dirty; + this._onDidChangeDirty.fire(); + } + } + + public async revert(_options?: IRevertOptions) { + this._proxy.$revert(this.resource, this.viewType); + } + + public undo() { + this._proxy.$undo(this.resource, this.viewType); + } + + public redo() { + this._proxy.$redo(this.resource, this.viewType); + } + + public async save(_options?: ISaveOptions): Promise { + await this._proxy.$onSave(this.resource, this.viewType); + this.setDirty(false); + return true; + } + + public async saveAs(resource: URI, targetResource: URI, _options?: ISaveOptions): Promise { + if (!this._editable) { + // Since the editor is readonly, just copy the file over + await this._fileService.copy(resource, targetResource, false /* overwrite */); + return true; + } + + await this._proxy.$onSaveAs(this.resource, this.viewType, targetResource); + this.setDirty(false); + return true; + } + + public async backup(): Promise { + if (this._hotExitState.type === HotExitState.Type.Pending) { + this._hotExitState.operation.cancel(); + } + + const pendingState = new HotExitState.Pending( + createCancelablePromise(token => + this._proxy.$backup(this.resource.toJSON(), this.viewType, token))); + this._hotExitState = pendingState; + + try { + await pendingState.operation; + // Make sure state has not changed in the meantime + if (this._hotExitState === pendingState) { + this._hotExitState = HotExitState.Allowed; + } + } catch (e) { + // Make sure state has not changed in the meantime + if (this._hotExitState === pendingState) { + this._hotExitState = HotExitState.NotAllowed; + } + } + + if (this._hotExitState === HotExitState.Allowed) { + return { + meta: { + viewType: this.viewType, + } + }; + } + + throw new Error('Cannot back up in this state'); + } +} diff --git a/src/vs/workbench/api/common/extHostWebview.ts b/src/vs/workbench/api/common/extHostWebview.ts index 17b6b4719ecb97f876195bca8cd2c122c5cb94ec..988358cbfe7d96bff79d35b4961a240c5f74b733 100644 --- a/src/vs/workbench/api/common/extHostWebview.ts +++ b/src/vs/workbench/api/common/extHostWebview.ts @@ -657,7 +657,7 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { } const revivedResource = URI.revive(resource); - const document = this.getDocument(viewType, revivedResource); + const document = this.getCustomDocument(viewType, revivedResource); this._documents.delete(document); document.dispose(); } @@ -684,12 +684,11 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { switch (entry.type) { case WebviewEditorType.Custom: { - const document = this.getDocument(viewType, revivedResource); + const document = this.getCustomDocument(viewType, revivedResource); return entry.provider.resolveCustomEditor(document, revivedPanel); } case WebviewEditorType.Text: { - await this._extHostDocuments.ensureDocumentData(revivedResource); const document = this._extHostDocuments.getDocument(revivedResource); return entry.provider.resolveCustomTextEditor(document, revivedPanel); } @@ -701,32 +700,32 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { } async $undo(resourceComponents: UriComponents, viewType: string): Promise { - const document = this.getDocument(viewType, resourceComponents); + const document = this.getCustomDocument(viewType, resourceComponents); document._undo(); } async $redo(resourceComponents: UriComponents, viewType: string): Promise { - const document = this.getDocument(viewType, resourceComponents); + const document = this.getCustomDocument(viewType, resourceComponents); document._redo(); } async $revert(resourceComponents: UriComponents, viewType: string): Promise { - const document = this.getDocument(viewType, resourceComponents); + const document = this.getCustomDocument(viewType, resourceComponents); document._revert(); } async $onSave(resourceComponents: UriComponents, viewType: string): Promise { - const document = this.getDocument(viewType, resourceComponents); + const document = this.getCustomDocument(viewType, resourceComponents); document._save(); } async $onSaveAs(resourceComponents: UriComponents, viewType: string, targetResource: UriComponents): Promise { - const document = this.getDocument(viewType, resourceComponents); + const document = this.getCustomDocument(viewType, resourceComponents); return document._saveAs(URI.revive(targetResource)); } async $backup(resourceComponents: UriComponents, viewType: string, cancellation: CancellationToken): Promise { - const document = this.getDocument(viewType, resourceComponents); + const document = this.getCustomDocument(viewType, resourceComponents); return document._backup(cancellation); } @@ -734,7 +733,7 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { return this._webviewPanels.get(handle); } - private getDocument(viewType: string, resource: UriComponents): CustomDocument { + private getCustomDocument(viewType: string, resource: UriComponents): CustomDocument { const document = this._documents.get(viewType, URI.revive(resource)); if (!document) { throw new Error('No webview editor custom document found'); diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts index d55b558f42bb8a18ce97f1bb86f3a896809e5556..5f73bb25c619c63f18c4a83796857697e8568424 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts @@ -20,22 +20,16 @@ import { IWebviewService, WebviewEditorOverlay } from 'vs/workbench/contrib/webv import { IWebviewWorkbenchService, LazilyResolvedWebviewEditorInput } from 'vs/workbench/contrib/webview/browser/webviewWorkbenchService'; 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', -} +import { IReference } from 'vs/base/common/lifecycle'; export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { - public static typeId = 'workbench.editors.webviewEditor'; private readonly _editorResource: URI; get resource() { return this._editorResource; } - private _model?: { readonly type: ModelType.Custom, readonly model: ICustomEditorModel } | { readonly type: ModelType.Text }; + private _modelRef?: IReference; constructor( resource: URI, @@ -50,15 +44,11 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { @IFileDialogService private readonly fileDialogService: IFileDialogService, @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService, @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 CustomEditorInput.typeId; } @@ -110,20 +100,10 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { } public isDirty(): boolean { - if (!this._model) { + if (!this._modelRef) { 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'); - } + return this._modelRef.object.isDirty(); } public isSaving(): boolean { @@ -139,28 +119,16 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { } public async save(groupId: GroupIdentifier, options?: ISaveOptions): Promise { - if (!this._model) { + if (!this._modelRef) { 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'); - } + const result = await this._modelRef.object.save(options); + return result ? this : undefined; } public async saveAs(groupId: GroupIdentifier, options?: ISaveOptions): Promise { - if (!this._model) { + if (!this._modelRef) { return undefined; } @@ -170,66 +138,25 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { return undefined; // save cancelled } - 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'); + if (!await this._modelRef.object.saveAs(this._editorResource, target, options)) { + return undefined; } return this.handleMove(groupId, target) || this.editorService.createInput({ resource: target, forceFile: true }); } public async revert(group: GroupIdentifier, options?: IRevertOptions): Promise { - if (!this._model) { - return; - } - - 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'); - } + return this._modelRef?.object.revert(options); } public async resolve(): Promise { 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._modelRef) { + const modelRef = await this.customEditorService.models.tryRetain(this.resource, this.viewType); + if (modelRef) { + this._modelRef = modelRef; + this._register(this._modelRef); + this._register(this._modelRef.object.onDidChangeDirty(() => this._onDidChangeDirty.fire())); } } @@ -256,40 +183,10 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { } 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'); - } + this._modelRef?.object.undo(); } 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'); - } + this._modelRef?.object.redo(); } } diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts index 558dc3d380df4c6d3e54e2b358db8b2e0d773482..3938e3b5cac7f419ba9a40ffe9f6c5bf25a962d4 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts @@ -26,7 +26,6 @@ export class CustomEditorInputFactory extends WebviewEditorInputFactory { const data = { ...this.toJson(input), editorResource: input.resource.toJSON(), - modelType: input.modelType }; try { @@ -55,9 +54,6 @@ export class CustomEditorInputFactory extends WebviewEditorInputFactory { if (typeof data.group === 'number') { customInput.updateGroup(data.group); } - if ((data as any).modelType) { - customInput.modelType = (data as any).modelType; - } return customInput; } } diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts index ca4581ede56ff36e69c8edd8edafc4d042d519b5..42b0b448f1a1366aad584ff79d1c32e82cc2deaa 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts @@ -16,7 +16,6 @@ import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/c import { IEditorOptions, ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { FileOperation, IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ILabelService } from 'vs/platform/label/common/label'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import * as colorRegistry from 'vs/platform/theme/common/colorRegistry'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; @@ -25,12 +24,11 @@ import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { EditorInput, EditorOptions, IEditor, IEditorInput } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { webviewEditorsExtensionPoint } from 'vs/workbench/contrib/customEditor/browser/extensionPoint'; -import { CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE, CONTEXT_CUSTOM_EDITORS, CustomEditorInfo, CustomEditorInfoCollection, CustomEditorPriority, CustomEditorSelector, ICustomEditor, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; +import { CONTEXT_CUSTOM_EDITORS, CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE, CustomEditorInfo, CustomEditorInfoCollection, CustomEditorPriority, CustomEditorSelector, ICustomEditor, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; import { CustomEditorModelManager } from 'vs/workbench/contrib/customEditor/common/customEditorModelManager'; import { IWebviewService, webviewHasOwnEditFunctionsContext } from 'vs/workbench/contrib/webview/browser/webview'; 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 { CustomEditorInput } from './customEditorInput'; export const defaultEditorId = 'default'; @@ -104,7 +102,6 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ constructor( @IContextKeyService contextKeyService: IContextKeyService, - @IWorkingCopyService workingCopyService: IWorkingCopyService, @IFileService fileService: IFileService, @IConfigurationService private readonly configurationService: IConfigurationService, @IEditorService private readonly editorService: IEditorService, @@ -112,11 +109,10 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ @IInstantiationService private readonly instantiationService: IInstantiationService, @IQuickInputService private readonly quickInputService: IQuickInputService, @IWebviewService private readonly webviewService: IWebviewService, - @ILabelService labelService: ILabelService ) { super(); - this._models = new CustomEditorModelManager(workingCopyService, labelService); + this._models = new CustomEditorModelManager(); this._customEditorContextKey = CONTEXT_CUSTOM_EDITORS.bindTo(contextKeyService); this._focusedCustomEditorIsEditable = CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE.bindTo(contextKeyService); diff --git a/src/vs/workbench/contrib/customEditor/common/customEditor.ts b/src/vs/workbench/contrib/customEditor/common/customEditor.ts index a407a4813bb6afc650728f85d056a699dd679887..ede7bc61a519f989902b532fea70554e1c1b8365 100644 --- a/src/vs/workbench/contrib/customEditor/common/customEditor.ts +++ b/src/vs/workbench/contrib/customEditor/common/customEditor.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { distinct, mergeSort } from 'vs/base/common/arrays'; -import { CancelablePromise } from 'vs/base/common/async'; import { Event } from 'vs/base/common/event'; import * as glob from 'vs/base/common/glob'; import { basename } from 'vs/base/common/resources'; @@ -12,9 +11,9 @@ 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 { IEditor, IRevertOptions, ISaveOptions, IEditorInput } from 'vs/workbench/common/editor'; +import { IEditor, IEditorInput, IRevertOptions, ISaveOptions } from 'vs/workbench/common/editor'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IDisposable, IReference } from 'vs/base/common/lifecycle'; export const ICustomEditorService = createDecorator('customEditorService'); @@ -44,39 +43,22 @@ export interface ICustomEditorService { } export interface ICustomEditorModelManager { - get(resource: URI, viewType: string): ICustomEditorModel | undefined; + get(resource: URI, viewType: string): Promise; - resolve(resource: URI, viewType: string): Promise; + tryRetain(resource: URI, viewType: string): Promise> | undefined; - disposeModel(model: ICustomEditorModel): void; + add(resource: URI, viewType: string, model: Promise): Promise>; disposeAllModelsForView(viewType: string): void; } -export interface CustomEditorSaveEvent { - readonly resource: URI; - readonly waitUntil: (until: Promise) => void; -} - -export interface CustomEditorSaveAsEvent { - readonly resource: URI; - readonly targetResource: URI; - readonly waitUntil: (until: Promise) => void; -} - -export interface ICustomEditorModel extends IWorkingCopy { +export interface ICustomEditorModel extends IDisposable { readonly viewType: string; + readonly resource: URI; - readonly onUndo: Event; - readonly onRedo: Event; - readonly onRevert: Event; - - readonly onWillSave: Event; - readonly onWillSaveAs: Event; - - onBackup(f: () => CancelablePromise): void; + isDirty(): boolean; + readonly onDidChangeDirty: Event; - setDirty(dirty: boolean): void; undo(): void; redo(): void; revert(options?: IRevertOptions): Promise; diff --git a/src/vs/workbench/contrib/customEditor/common/customEditorModel.ts b/src/vs/workbench/contrib/customEditor/common/customEditorModel.ts deleted file mode 100644 index d961dec639efeff67139b615e7bc82f0e7d65015..0000000000000000000000000000000000000000 --- a/src/vs/workbench/contrib/customEditor/common/customEditorModel.ts +++ /dev/null @@ -1,203 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { CancelablePromise } from 'vs/base/common/async'; -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 { 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'; - -namespace HotExitState { - export const enum Type { - NotSupported, - Allowed, - NotAllowed, - Pending, - } - - export const NotSupported = Object.freeze({ type: Type.NotSupported } as const); - export const Allowed = Object.freeze({ type: Type.Allowed } as const); - export const NotAllowed = Object.freeze({ type: Type.NotAllowed } as const); - - export class Pending { - readonly type = Type.Pending; - - constructor( - public readonly operation: CancelablePromise, - ) { } - } - - export type State = typeof NotSupported | typeof Allowed | typeof NotAllowed | Pending; -} - -export class CustomEditorModel extends Disposable implements ICustomEditorModel { - - private _hotExitState: HotExitState.State = HotExitState.NotSupported; - private _dirty = false; - - constructor( - public readonly viewType: string, - private readonly _resource: URI, - private readonly labelService: ILabelService, - ) { - super(); - } - - //#region IWorkingCopy - - public get resource() { - return this._resource; - } - - public get name() { - return basename(this.labelService.getUriLabel(this._resource)); - } - - public get capabilities(): WorkingCopyCapabilities { - return 0; - } - - public isDirty(): boolean { - return this._dirty; - } - - private readonly _onDidChangeDirty: Emitter = this._register(new Emitter()); - readonly onDidChangeDirty: Event = this._onDidChangeDirty.event; - - private readonly _onDidChangeContent: Emitter = this._register(new Emitter()); - readonly onDidChangeContent: Event = this._onDidChangeContent.event; - - //#endregion - - private readonly _onUndo = this._register(new Emitter()); - public readonly onUndo = this._onUndo.event; - - private readonly _onRedo = this._register(new Emitter()); - public readonly onRedo = this._onRedo.event; - - private readonly _onRevert = this._register(new Emitter()); - public readonly onRevert = this._onRevert.event; - - private readonly _onWillSave = this._register(new Emitter()); - public readonly onWillSave = this._onWillSave.event; - - private readonly _onWillSaveAs = this._register(new Emitter()); - public readonly onWillSaveAs = this._onWillSaveAs.event; - - private _onBackup: undefined | (() => CancelablePromise); - - public onBackup(f: () => CancelablePromise) { - if (this._onBackup) { - throw new Error('Backup already implemented'); - } - this._onBackup = f; - - if (this._hotExitState === HotExitState.NotSupported) { - this._hotExitState = this.isDirty() ? HotExitState.NotAllowed : HotExitState.Allowed; - } - } - - public setDirty(dirty: boolean): void { - this._onDidChangeContent.fire(); - - if (this._dirty !== dirty) { - this._dirty = dirty; - this._onDidChangeDirty.fire(); - } - } - - public async revert(_options?: IRevertOptions) { - if (this._dirty) { - this._onRevert.fire(); - } - } - - public undo() { - this._onUndo.fire(); - } - - public redo() { - this._onRedo.fire(); - } - - public async save(_options?: ISaveOptions): Promise { - const untils: Promise[] = []; - const handler: CustomEditorSaveEvent = { - resource: this._resource, - waitUntil: (until: Promise) => untils.push(until) - }; - - try { - this._onWillSave.fire(handler); - await Promise.all(untils); - } catch { - return false; - } - - this.setDirty(false); - - return true; - } - - public async saveAs(resource: URI, targetResource: URI, _options?: ISaveOptions): Promise { - const untils: Promise[] = []; - const handler: CustomEditorSaveAsEvent = { - resource, - targetResource, - waitUntil: (until: Promise) => untils.push(until) - }; - - try { - this._onWillSaveAs.fire(handler); - await Promise.all(untils); - } catch { - return false; - } - - this.setDirty(false); - - return true; - } - - public async backup(): Promise { - if (this._hotExitState === HotExitState.NotSupported) { - throw new Error('Not supported'); - } - - if (this._hotExitState.type === HotExitState.Type.Pending) { - this._hotExitState.operation.cancel(); - } - this._hotExitState = HotExitState.NotAllowed; - - const pendingState = new HotExitState.Pending(this._onBackup!()); - this._hotExitState = pendingState; - - try { - await pendingState.operation; - // Make sure state has not changed in the meantime - if (this._hotExitState === pendingState) { - this._hotExitState = HotExitState.Allowed; - } - } catch (e) { - // Make sure state has not changed in the meantime - if (this._hotExitState === pendingState) { - this._hotExitState = HotExitState.NotAllowed; - } - } - - if (this._hotExitState === HotExitState.Allowed) { - return { - meta: { - viewType: this.viewType, - } - }; - } - throw new Error('Cannot back up in this state'); - } -} diff --git a/src/vs/workbench/contrib/customEditor/common/customEditorModelManager.ts b/src/vs/workbench/contrib/customEditor/common/customEditorModelManager.ts index 0c1a5b9f10f5c5dffe802066c6042337fd2eca6c..b2b185ed874a588ff4473de57ab6300d9f856ba5 100644 --- a/src/vs/workbench/contrib/customEditor/common/customEditorModelManager.ts +++ b/src/vs/workbench/contrib/customEditor/common/customEditorModelManager.ts @@ -3,58 +3,64 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { IReference } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { ICustomEditorModel, ICustomEditorModelManager } from 'vs/workbench/contrib/customEditor/common/customEditor'; -import { CustomEditorModel } from 'vs/workbench/contrib/customEditor/common/customEditorModel'; -import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; -import { ILabelService } from 'vs/platform/label/common/label'; +import { once } from 'vs/base/common/functional'; export class CustomEditorModelManager implements ICustomEditorModelManager { - private readonly _models = new Map(); - constructor( - @IWorkingCopyService private readonly _workingCopyService: IWorkingCopyService, - @ILabelService private readonly _labelService: ILabelService - ) { } + private readonly _references = new Map, + counter: number + }>(); - - public get(resource: URI, viewType: string): ICustomEditorModel | undefined { - return this._models.get(this.key(resource, viewType))?.model; + public async get(resource: URI, viewType: string): Promise { + const key = this.key(resource, viewType); + const entry = this._references.get(key); + return entry?.model; } - public async resolve(resource: URI, viewType: string): Promise { - const existing = this.get(resource, viewType); - if (existing) { - return existing; + public tryRetain(resource: URI, viewType: string): Promise> | undefined { + const key = this.key(resource, viewType); + + const entry = this._references.get(key); + if (!entry) { + return undefined; } - const model = new CustomEditorModel(viewType, resource, this._labelService); - const disposables = new DisposableStore(); - disposables.add(this._workingCopyService.registerWorkingCopy(model)); - this._models.set(this.key(resource, viewType), { model, disposables }); - return model; + entry.counter++; + + return entry.model.then(model => { + return { + object: model, + dispose: once(() => { + if (--entry!.counter <= 0) { + entry.model.then(x => x.dispose()); + this._references.delete(key); + } + }), + }; + }); } - public disposeModel(model: ICustomEditorModel): void { - let foundKey: string | undefined; - for (const [key, value] of this._models) { - if (model === value.model) { - value.disposables.dispose(); - value.model.dispose(); - foundKey = key; - } - } - if (typeof foundKey === 'string') { - this._models.delete(foundKey); + public add(resource: URI, viewType: string, model: Promise): Promise> { + const key = this.key(resource, viewType); + const existing = this._references.get(key); + if (existing) { + throw new Error('Model already exists'); } - return; + + this._references.set(key, { viewType, model, counter: 0 }); + return this.tryRetain(resource, viewType)!; } public disposeAllModelsForView(viewType: string): void { - for (const [, value] of this._models) { - if (value.model.viewType === viewType) { - this.disposeModel(value.model); + for (const [key, value] of this._references) { + if (value.viewType === viewType) { + value.model.then(x => x.dispose()); + this._references.delete(key); } } } diff --git a/src/vs/workbench/contrib/customEditor/common/customTextEditorModel.ts b/src/vs/workbench/contrib/customEditor/common/customTextEditorModel.ts new file mode 100644 index 0000000000000000000000000000000000000000..1f0c33e633ffd640504b888dab5d458475778901 --- /dev/null +++ b/src/vs/workbench/contrib/customEditor/common/customTextEditorModel.ts @@ -0,0 +1,82 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable, IReference } from 'vs/base/common/lifecycle'; +import { isEqual } from 'vs/base/common/resources'; +import { URI } from 'vs/base/common/uri'; +import { IResolvedTextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; +import { IRevertOptions, ISaveOptions } from 'vs/workbench/common/editor'; +import { ICustomEditorModel } from 'vs/workbench/contrib/customEditor/common/customEditor'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; + +export class CustomTextEditorModel extends Disposable implements ICustomEditorModel { + + public static async create( + instantiationService: IInstantiationService, + viewType: string, + resource: URI + ): Promise { + return instantiationService.invokeFunction(async accessor => { + const textModelResolverService = accessor.get(ITextModelService); + const textFileService = accessor.get(ITextFileService); + const model = await textModelResolverService.createModelReference(resource); + return new CustomTextEditorModel(viewType, resource, model, textFileService); + }); + } + + private constructor( + public readonly viewType: string, + private readonly _resource: URI, + model: IReference, + @ITextFileService private readonly textFileService: ITextFileService, + ) { + super(); + + this._register(model); + + this._register(this.textFileService.files.onDidChangeDirty(e => { + if (isEqual(this.resource, e.resource)) { + this._onDidChangeDirty.fire(); + this._onDidChangeContent.fire(); + } + })); + } + + public get resource() { + return this._resource; + } + + public isDirty(): boolean { + return this.textFileService.isDirty(this.resource); + } + + private readonly _onDidChangeDirty: Emitter = this._register(new Emitter()); + readonly onDidChangeDirty: Event = this._onDidChangeDirty.event; + + private readonly _onDidChangeContent: Emitter = this._register(new Emitter()); + readonly onDidChangeContent: Event = this._onDidChangeContent.event; + + public async revert(options?: IRevertOptions) { + return this.textFileService.revert(this.resource, options); + } + + public undo() { + this.textFileService.files.get(this.resource)?.textEditorModel?.undo(); + } + + public redo() { + this.textFileService.files.get(this.resource)?.textEditorModel?.redo(); + } + + public async save(options?: ISaveOptions): Promise { + return !!await this.textFileService.save(this.resource, options); + } + + public async saveAs(resource: URI, targetResource: URI, options?: ISaveOptions): Promise { + return !!await this.textFileService.saveAs(resource, targetResource, options); + } +}