Path: blob/main/src/vs/workbench/api/common/extHostCustomEditors.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { VSBuffer } from '../../../base/common/buffer.js';6import { CancellationToken } from '../../../base/common/cancellation.js';7import { hash } from '../../../base/common/hash.js';8import { DisposableStore } from '../../../base/common/lifecycle.js';9import { Schemas } from '../../../base/common/network.js';10import { joinPath } from '../../../base/common/resources.js';11import { URI, UriComponents } from '../../../base/common/uri.js';12import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js';13import { ExtHostDocuments } from './extHostDocuments.js';14import { IExtensionStoragePaths } from './extHostStoragePaths.js';15import * as typeConverters from './extHostTypeConverters.js';16import { ExtHostWebviews, shouldSerializeBuffersForPostMessage, toExtensionData } from './extHostWebview.js';17import { ExtHostWebviewPanels } from './extHostWebviewPanels.js';18import { EditorGroupColumn } from '../../services/editor/common/editorGroupColumn.js';19import type * as vscode from 'vscode';20import { Cache } from './cache.js';21import * as extHostProtocol from './extHost.protocol.js';22import * as extHostTypes from './extHostTypes.js';232425class CustomDocumentStoreEntry {2627private _backupCounter = 1;2829constructor(30public readonly document: vscode.CustomDocument,31private readonly _storagePath: URI | undefined,32) { }3334private readonly _edits = new Cache<vscode.CustomDocumentEditEvent>('custom documents');3536private _backup?: vscode.CustomDocumentBackup;3738addEdit(item: vscode.CustomDocumentEditEvent): number {39return this._edits.add([item]);40}4142async undo(editId: number, isDirty: boolean): Promise<void> {43await this.getEdit(editId).undo();44if (!isDirty) {45this.disposeBackup();46}47}4849async redo(editId: number, isDirty: boolean): Promise<void> {50await this.getEdit(editId).redo();51if (!isDirty) {52this.disposeBackup();53}54}5556disposeEdits(editIds: number[]): void {57for (const id of editIds) {58this._edits.delete(id);59}60}6162getNewBackupUri(): URI {63if (!this._storagePath) {64throw new Error('Backup requires a valid storage path');65}66const fileName = hashPath(this.document.uri) + (this._backupCounter++);67return joinPath(this._storagePath, fileName);68}6970updateBackup(backup: vscode.CustomDocumentBackup): void {71this._backup?.delete();72this._backup = backup;73}7475disposeBackup(): void {76this._backup?.delete();77this._backup = undefined;78}7980private getEdit(editId: number): vscode.CustomDocumentEditEvent {81const edit = this._edits.get(editId, 0);82if (!edit) {83throw new Error('No edit found');84}85return edit;86}87}8889class CustomDocumentStore {90private readonly _documents = new Map<string, CustomDocumentStoreEntry>();9192public get(viewType: string, resource: vscode.Uri): CustomDocumentStoreEntry | undefined {93return this._documents.get(this.key(viewType, resource));94}9596public add(viewType: string, document: vscode.CustomDocument, storagePath: URI | undefined): CustomDocumentStoreEntry {97const key = this.key(viewType, document.uri);98if (this._documents.has(key)) {99throw new Error(`Document already exists for viewType:${viewType} resource:${document.uri}`);100}101const entry = new CustomDocumentStoreEntry(document, storagePath);102this._documents.set(key, entry);103return entry;104}105106public delete(viewType: string, document: vscode.CustomDocument) {107const key = this.key(viewType, document.uri);108this._documents.delete(key);109}110111private key(viewType: string, resource: vscode.Uri): string {112return `${viewType}@@@${resource}`;113}114}115116const enum CustomEditorType {117Text,118Custom119}120121type ProviderEntry = {122readonly extension: IExtensionDescription;123readonly type: CustomEditorType.Text;124readonly provider: vscode.CustomTextEditorProvider;125} | {126readonly extension: IExtensionDescription;127readonly type: CustomEditorType.Custom;128readonly provider: vscode.CustomReadonlyEditorProvider;129};130131class EditorProviderStore {132private readonly _providers = new Map<string, ProviderEntry>();133134public addTextProvider(viewType: string, extension: IExtensionDescription, provider: vscode.CustomTextEditorProvider): vscode.Disposable {135return this.add(viewType, { type: CustomEditorType.Text, extension, provider });136}137138public addCustomProvider(viewType: string, extension: IExtensionDescription, provider: vscode.CustomReadonlyEditorProvider): vscode.Disposable {139return this.add(viewType, { type: CustomEditorType.Custom, extension, provider });140}141142public get(viewType: string): ProviderEntry | undefined {143return this._providers.get(viewType);144}145146private add(viewType: string, entry: ProviderEntry): vscode.Disposable {147if (this._providers.has(viewType)) {148throw new Error(`Provider for viewType:${viewType} already registered`);149}150this._providers.set(viewType, entry);151return new extHostTypes.Disposable(() => this._providers.delete(viewType));152}153}154155export class ExtHostCustomEditors implements extHostProtocol.ExtHostCustomEditorsShape {156157private readonly _proxy: extHostProtocol.MainThreadCustomEditorsShape;158159private readonly _editorProviders = new EditorProviderStore();160161private readonly _documents = new CustomDocumentStore();162163constructor(164mainContext: extHostProtocol.IMainContext,165private readonly _extHostDocuments: ExtHostDocuments,166private readonly _extensionStoragePaths: IExtensionStoragePaths | undefined,167private readonly _extHostWebview: ExtHostWebviews,168private readonly _extHostWebviewPanels: ExtHostWebviewPanels,169) {170this._proxy = mainContext.getProxy(extHostProtocol.MainContext.MainThreadCustomEditors);171}172173public registerCustomEditorProvider(174extension: IExtensionDescription,175viewType: string,176provider: vscode.CustomReadonlyEditorProvider | vscode.CustomTextEditorProvider,177options: { webviewOptions?: vscode.WebviewPanelOptions; supportsMultipleEditorsPerDocument?: boolean },178): vscode.Disposable {179const disposables = new DisposableStore();180if (isCustomTextEditorProvider(provider)) {181disposables.add(this._editorProviders.addTextProvider(viewType, extension, provider));182this._proxy.$registerTextEditorProvider(toExtensionData(extension), viewType, options.webviewOptions || {}, {183supportsMove: !!provider.moveCustomTextEditor,184}, shouldSerializeBuffersForPostMessage(extension));185} else {186disposables.add(this._editorProviders.addCustomProvider(viewType, extension, provider));187188if (isCustomEditorProviderWithEditingCapability(provider)) {189disposables.add(provider.onDidChangeCustomDocument(e => {190const entry = this.getCustomDocumentEntry(viewType, e.document.uri);191if (isEditEvent(e)) {192const editId = entry.addEdit(e);193this._proxy.$onDidEdit(e.document.uri, viewType, editId, e.label);194} else {195this._proxy.$onContentChange(e.document.uri, viewType);196}197}));198}199200this._proxy.$registerCustomEditorProvider(toExtensionData(extension), viewType, options.webviewOptions || {}, !!options.supportsMultipleEditorsPerDocument, shouldSerializeBuffersForPostMessage(extension));201}202203return extHostTypes.Disposable.from(204disposables,205new extHostTypes.Disposable(() => {206this._proxy.$unregisterEditorProvider(viewType);207}));208}209210async $createCustomDocument(resource: UriComponents, viewType: string, backupId: string | undefined, untitledDocumentData: VSBuffer | undefined, cancellation: CancellationToken) {211const entry = this._editorProviders.get(viewType);212if (!entry) {213throw new Error(`No provider found for '${viewType}'`);214}215216if (entry.type !== CustomEditorType.Custom) {217throw new Error(`Invalid provide type for '${viewType}'`);218}219220const revivedResource = URI.revive(resource);221const document = await entry.provider.openCustomDocument(revivedResource, { backupId, untitledDocumentData: untitledDocumentData?.buffer }, cancellation);222223let storageRoot: URI | undefined;224if (isCustomEditorProviderWithEditingCapability(entry.provider) && this._extensionStoragePaths) {225storageRoot = this._extensionStoragePaths.workspaceValue(entry.extension) ?? this._extensionStoragePaths.globalValue(entry.extension);226}227this._documents.add(viewType, document, storageRoot);228229return { editable: isCustomEditorProviderWithEditingCapability(entry.provider) };230}231232async $disposeCustomDocument(resource: UriComponents, viewType: string): Promise<void> {233const entry = this._editorProviders.get(viewType);234if (!entry) {235throw new Error(`No provider found for '${viewType}'`);236}237238if (entry.type !== CustomEditorType.Custom) {239throw new Error(`Invalid provider type for '${viewType}'`);240}241242const revivedResource = URI.revive(resource);243const { document } = this.getCustomDocumentEntry(viewType, revivedResource);244this._documents.delete(viewType, document);245document.dispose();246}247248async $resolveCustomEditor(249resource: UriComponents,250handle: extHostProtocol.WebviewHandle,251viewType: string,252initData: {253title: string;254contentOptions: extHostProtocol.IWebviewContentOptions;255options: extHostProtocol.IWebviewPanelOptions;256active: boolean;257},258position: EditorGroupColumn,259cancellation: CancellationToken,260): Promise<void> {261const entry = this._editorProviders.get(viewType);262if (!entry) {263throw new Error(`No provider found for '${viewType}'`);264}265266const viewColumn = typeConverters.ViewColumn.to(position);267268const webview = this._extHostWebview.createNewWebview(handle, initData.contentOptions, entry.extension);269const panel = this._extHostWebviewPanels.createNewWebviewPanel(handle, viewType, initData.title, viewColumn, initData.options, webview, initData.active);270271const revivedResource = URI.revive(resource);272273switch (entry.type) {274case CustomEditorType.Custom: {275const { document } = this.getCustomDocumentEntry(viewType, revivedResource);276return entry.provider.resolveCustomEditor(document, panel, cancellation);277}278case CustomEditorType.Text: {279const document = this._extHostDocuments.getDocument(revivedResource);280return entry.provider.resolveCustomTextEditor(document, panel, cancellation);281}282default: {283throw new Error('Unknown webview provider type');284}285}286}287288$disposeEdits(resourceComponents: UriComponents, viewType: string, editIds: number[]): void {289const document = this.getCustomDocumentEntry(viewType, resourceComponents);290document.disposeEdits(editIds);291}292293async $onMoveCustomEditor(handle: string, newResourceComponents: UriComponents, viewType: string): Promise<void> {294const entry = this._editorProviders.get(viewType);295if (!entry) {296throw new Error(`No provider found for '${viewType}'`);297}298299if (!(entry.provider as vscode.CustomTextEditorProvider).moveCustomTextEditor) {300throw new Error(`Provider does not implement move '${viewType}'`);301}302303const webview = this._extHostWebviewPanels.getWebviewPanel(handle);304if (!webview) {305throw new Error(`No webview found`);306}307308const resource = URI.revive(newResourceComponents);309const document = this._extHostDocuments.getDocument(resource);310await (entry.provider as vscode.CustomTextEditorProvider).moveCustomTextEditor!(document, webview, CancellationToken.None);311}312313async $undo(resourceComponents: UriComponents, viewType: string, editId: number, isDirty: boolean): Promise<void> {314const entry = this.getCustomDocumentEntry(viewType, resourceComponents);315return entry.undo(editId, isDirty);316}317318async $redo(resourceComponents: UriComponents, viewType: string, editId: number, isDirty: boolean): Promise<void> {319const entry = this.getCustomDocumentEntry(viewType, resourceComponents);320return entry.redo(editId, isDirty);321}322323async $revert(resourceComponents: UriComponents, viewType: string, cancellation: CancellationToken): Promise<void> {324const entry = this.getCustomDocumentEntry(viewType, resourceComponents);325const provider = this.getCustomEditorProvider(viewType);326await provider.revertCustomDocument(entry.document, cancellation);327entry.disposeBackup();328}329330async $onSave(resourceComponents: UriComponents, viewType: string, cancellation: CancellationToken): Promise<void> {331const entry = this.getCustomDocumentEntry(viewType, resourceComponents);332const provider = this.getCustomEditorProvider(viewType);333await provider.saveCustomDocument(entry.document, cancellation);334entry.disposeBackup();335}336337async $onSaveAs(resourceComponents: UriComponents, viewType: string, targetResource: UriComponents, cancellation: CancellationToken): Promise<void> {338const entry = this.getCustomDocumentEntry(viewType, resourceComponents);339const provider = this.getCustomEditorProvider(viewType);340return provider.saveCustomDocumentAs(entry.document, URI.revive(targetResource), cancellation);341}342343async $backup(resourceComponents: UriComponents, viewType: string, cancellation: CancellationToken): Promise<string> {344const entry = this.getCustomDocumentEntry(viewType, resourceComponents);345const provider = this.getCustomEditorProvider(viewType);346347const backup = await provider.backupCustomDocument(entry.document, {348destination: entry.getNewBackupUri(),349}, cancellation);350entry.updateBackup(backup);351return backup.id;352}353354private getCustomDocumentEntry(viewType: string, resource: UriComponents): CustomDocumentStoreEntry {355const entry = this._documents.get(viewType, URI.revive(resource));356if (!entry) {357throw new Error('No custom document found');358}359return entry;360}361362private getCustomEditorProvider(viewType: string): vscode.CustomEditorProvider {363const entry = this._editorProviders.get(viewType);364const provider = entry?.provider;365if (!provider || !isCustomEditorProviderWithEditingCapability(provider)) {366throw new Error('Custom document is not editable');367}368return provider;369}370}371372function isCustomEditorProviderWithEditingCapability(provider: vscode.CustomTextEditorProvider | vscode.CustomEditorProvider | vscode.CustomReadonlyEditorProvider): provider is vscode.CustomEditorProvider {373return !!(provider as vscode.CustomEditorProvider).onDidChangeCustomDocument;374}375376function isCustomTextEditorProvider(provider: vscode.CustomReadonlyEditorProvider<vscode.CustomDocument> | vscode.CustomTextEditorProvider): provider is vscode.CustomTextEditorProvider {377return typeof (provider as vscode.CustomTextEditorProvider).resolveCustomTextEditor === 'function';378}379380function isEditEvent(e: vscode.CustomDocumentContentChangeEvent | vscode.CustomDocumentEditEvent): e is vscode.CustomDocumentEditEvent {381return typeof (e as vscode.CustomDocumentEditEvent).undo === 'function'382&& typeof (e as vscode.CustomDocumentEditEvent).redo === 'function';383}384385function hashPath(resource: URI): string {386const str = resource.scheme === Schemas.file || resource.scheme === Schemas.untitled ? resource.fsPath : resource.toString();387return hash(str) + '';388}389390391