Path: blob/main/src/vs/workbench/api/common/extHostCustomEditors.ts
5239 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, resource: vscode.Uri) {107// Use the resource parameter directly instead of document.uri, because the document's108// URI may have changed (e.g., after SaveAs from untitled to a file path).109const key = this.key(viewType, resource);110this._documents.delete(key);111}112113private key(viewType: string, resource: vscode.Uri): string {114return `${viewType}@@@${resource}`;115}116}117118const enum CustomEditorType {119Text,120Custom121}122123type ProviderEntry = {124readonly extension: IExtensionDescription;125readonly type: CustomEditorType.Text;126readonly provider: vscode.CustomTextEditorProvider;127} | {128readonly extension: IExtensionDescription;129readonly type: CustomEditorType.Custom;130readonly provider: vscode.CustomReadonlyEditorProvider;131};132133class EditorProviderStore {134private readonly _providers = new Map<string, ProviderEntry>();135136public addTextProvider(viewType: string, extension: IExtensionDescription, provider: vscode.CustomTextEditorProvider): vscode.Disposable {137return this.add(viewType, { type: CustomEditorType.Text, extension, provider });138}139140public addCustomProvider(viewType: string, extension: IExtensionDescription, provider: vscode.CustomReadonlyEditorProvider): vscode.Disposable {141return this.add(viewType, { type: CustomEditorType.Custom, extension, provider });142}143144public get(viewType: string): ProviderEntry | undefined {145return this._providers.get(viewType);146}147148private add(viewType: string, entry: ProviderEntry): vscode.Disposable {149if (this._providers.has(viewType)) {150throw new Error(`Provider for viewType:${viewType} already registered`);151}152this._providers.set(viewType, entry);153return new extHostTypes.Disposable(() => this._providers.delete(viewType));154}155}156157export class ExtHostCustomEditors implements extHostProtocol.ExtHostCustomEditorsShape {158159private readonly _proxy: extHostProtocol.MainThreadCustomEditorsShape;160161private readonly _editorProviders = new EditorProviderStore();162163private readonly _documents = new CustomDocumentStore();164165constructor(166mainContext: extHostProtocol.IMainContext,167private readonly _extHostDocuments: ExtHostDocuments,168private readonly _extensionStoragePaths: IExtensionStoragePaths | undefined,169private readonly _extHostWebview: ExtHostWebviews,170private readonly _extHostWebviewPanels: ExtHostWebviewPanels,171) {172this._proxy = mainContext.getProxy(extHostProtocol.MainContext.MainThreadCustomEditors);173}174175public registerCustomEditorProvider(176extension: IExtensionDescription,177viewType: string,178provider: vscode.CustomReadonlyEditorProvider | vscode.CustomTextEditorProvider,179options: { webviewOptions?: vscode.WebviewPanelOptions; supportsMultipleEditorsPerDocument?: boolean },180): vscode.Disposable {181const disposables = new DisposableStore();182if (isCustomTextEditorProvider(provider)) {183disposables.add(this._editorProviders.addTextProvider(viewType, extension, provider));184this._proxy.$registerTextEditorProvider(toExtensionData(extension), viewType, options.webviewOptions || {}, {185supportsMove: !!provider.moveCustomTextEditor,186}, shouldSerializeBuffersForPostMessage(extension));187} else {188disposables.add(this._editorProviders.addCustomProvider(viewType, extension, provider));189190if (isCustomEditorProviderWithEditingCapability(provider)) {191disposables.add(provider.onDidChangeCustomDocument(e => {192const entry = this.getCustomDocumentEntry(viewType, e.document.uri);193if (isEditEvent(e)) {194const editId = entry.addEdit(e);195this._proxy.$onDidEdit(e.document.uri, viewType, editId, e.label);196} else {197this._proxy.$onContentChange(e.document.uri, viewType);198}199}));200}201202this._proxy.$registerCustomEditorProvider(toExtensionData(extension), viewType, options.webviewOptions || {}, !!options.supportsMultipleEditorsPerDocument, shouldSerializeBuffersForPostMessage(extension));203}204205return extHostTypes.Disposable.from(206disposables,207new extHostTypes.Disposable(() => {208this._proxy.$unregisterEditorProvider(viewType);209}));210}211212async $createCustomDocument(resource: UriComponents, viewType: string, backupId: string | undefined, untitledDocumentData: VSBuffer | undefined, cancellation: CancellationToken) {213const entry = this._editorProviders.get(viewType);214if (!entry) {215throw new Error(`No provider found for '${viewType}'`);216}217218if (entry.type !== CustomEditorType.Custom) {219throw new Error(`Invalid provide type for '${viewType}'`);220}221222const revivedResource = URI.revive(resource);223const document = await entry.provider.openCustomDocument(revivedResource, { backupId, untitledDocumentData: untitledDocumentData?.buffer }, cancellation);224225let storageRoot: URI | undefined;226if (isCustomEditorProviderWithEditingCapability(entry.provider) && this._extensionStoragePaths) {227storageRoot = this._extensionStoragePaths.workspaceValue(entry.extension) ?? this._extensionStoragePaths.globalValue(entry.extension);228}229this._documents.add(viewType, document, storageRoot);230231return { editable: isCustomEditorProviderWithEditingCapability(entry.provider) };232}233234async $disposeCustomDocument(resource: UriComponents, viewType: string): Promise<void> {235const entry = this._editorProviders.get(viewType);236if (!entry) {237throw new Error(`No provider found for '${viewType}'`);238}239240if (entry.type !== CustomEditorType.Custom) {241throw new Error(`Invalid provider type for '${viewType}'`);242}243244const revivedResource = URI.revive(resource);245const { document } = this.getCustomDocumentEntry(viewType, revivedResource);246// Pass the resource we used to look up the document, not document.uri,247// because the document's URI may have changed (e.g., after SaveAs).248this._documents.delete(viewType, revivedResource);249document.dispose();250}251252async $resolveCustomEditor(253resource: UriComponents,254handle: extHostProtocol.WebviewHandle,255viewType: string,256initData: {257title: string;258contentOptions: extHostProtocol.IWebviewContentOptions;259options: extHostProtocol.IWebviewPanelOptions;260active: boolean;261},262position: EditorGroupColumn,263cancellation: CancellationToken,264): Promise<void> {265const entry = this._editorProviders.get(viewType);266if (!entry) {267throw new Error(`No provider found for '${viewType}'`);268}269270const viewColumn = typeConverters.ViewColumn.to(position);271272const webview = this._extHostWebview.createNewWebview(handle, initData.contentOptions, entry.extension);273const panel = this._extHostWebviewPanels.createNewWebviewPanel(handle, viewType, initData.title, viewColumn, initData.options, webview, initData.active);274275const revivedResource = URI.revive(resource);276277switch (entry.type) {278case CustomEditorType.Custom: {279const { document } = this.getCustomDocumentEntry(viewType, revivedResource);280return entry.provider.resolveCustomEditor(document, panel, cancellation);281}282case CustomEditorType.Text: {283const document = this._extHostDocuments.getDocument(revivedResource);284return entry.provider.resolveCustomTextEditor(document, panel, cancellation);285}286default: {287throw new Error('Unknown webview provider type');288}289}290}291292$disposeEdits(resourceComponents: UriComponents, viewType: string, editIds: number[]): void {293const document = this.getCustomDocumentEntry(viewType, resourceComponents);294document.disposeEdits(editIds);295}296297async $onMoveCustomEditor(handle: string, newResourceComponents: UriComponents, viewType: string): Promise<void> {298const entry = this._editorProviders.get(viewType);299if (!entry) {300throw new Error(`No provider found for '${viewType}'`);301}302303if (!(entry.provider as vscode.CustomTextEditorProvider).moveCustomTextEditor) {304throw new Error(`Provider does not implement move '${viewType}'`);305}306307const webview = this._extHostWebviewPanels.getWebviewPanel(handle);308if (!webview) {309throw new Error(`No webview found`);310}311312const resource = URI.revive(newResourceComponents);313const document = this._extHostDocuments.getDocument(resource);314await (entry.provider as vscode.CustomTextEditorProvider).moveCustomTextEditor!(document, webview, CancellationToken.None);315}316317async $undo(resourceComponents: UriComponents, viewType: string, editId: number, isDirty: boolean): Promise<void> {318const entry = this.getCustomDocumentEntry(viewType, resourceComponents);319return entry.undo(editId, isDirty);320}321322async $redo(resourceComponents: UriComponents, viewType: string, editId: number, isDirty: boolean): Promise<void> {323const entry = this.getCustomDocumentEntry(viewType, resourceComponents);324return entry.redo(editId, isDirty);325}326327async $revert(resourceComponents: UriComponents, viewType: string, cancellation: CancellationToken): Promise<void> {328const entry = this.getCustomDocumentEntry(viewType, resourceComponents);329const provider = this.getCustomEditorProvider(viewType);330await provider.revertCustomDocument(entry.document, cancellation);331entry.disposeBackup();332}333334async $onSave(resourceComponents: UriComponents, viewType: string, cancellation: CancellationToken): Promise<void> {335const entry = this.getCustomDocumentEntry(viewType, resourceComponents);336const provider = this.getCustomEditorProvider(viewType);337await provider.saveCustomDocument(entry.document, cancellation);338entry.disposeBackup();339}340341async $onSaveAs(resourceComponents: UriComponents, viewType: string, targetResource: UriComponents, cancellation: CancellationToken): Promise<void> {342const entry = this.getCustomDocumentEntry(viewType, resourceComponents);343const provider = this.getCustomEditorProvider(viewType);344return provider.saveCustomDocumentAs(entry.document, URI.revive(targetResource), cancellation);345}346347async $backup(resourceComponents: UriComponents, viewType: string, cancellation: CancellationToken): Promise<string> {348const entry = this.getCustomDocumentEntry(viewType, resourceComponents);349const provider = this.getCustomEditorProvider(viewType);350351const backup = await provider.backupCustomDocument(entry.document, {352destination: entry.getNewBackupUri(),353}, cancellation);354entry.updateBackup(backup);355return backup.id;356}357358private getCustomDocumentEntry(viewType: string, resource: UriComponents): CustomDocumentStoreEntry {359const entry = this._documents.get(viewType, URI.revive(resource));360if (!entry) {361throw new Error('No custom document found');362}363return entry;364}365366private getCustomEditorProvider(viewType: string): vscode.CustomEditorProvider {367const entry = this._editorProviders.get(viewType);368const provider = entry?.provider;369if (!provider || !isCustomEditorProviderWithEditingCapability(provider)) {370throw new Error('Custom document is not editable');371}372return provider;373}374}375376function isCustomEditorProviderWithEditingCapability(provider: vscode.CustomTextEditorProvider | vscode.CustomEditorProvider | vscode.CustomReadonlyEditorProvider): provider is vscode.CustomEditorProvider {377return !!(provider as vscode.CustomEditorProvider).onDidChangeCustomDocument;378}379380function isCustomTextEditorProvider(provider: vscode.CustomReadonlyEditorProvider<vscode.CustomDocument> | vscode.CustomTextEditorProvider): provider is vscode.CustomTextEditorProvider {381return typeof (provider as vscode.CustomTextEditorProvider).resolveCustomTextEditor === 'function';382}383384function isEditEvent(e: vscode.CustomDocumentContentChangeEvent | vscode.CustomDocumentEditEvent): e is vscode.CustomDocumentEditEvent {385return typeof (e as vscode.CustomDocumentEditEvent).undo === 'function'386&& typeof (e as vscode.CustomDocumentEditEvent).redo === 'function';387}388389function hashPath(resource: URI): string {390const str = resource.scheme === Schemas.file || resource.scheme === Schemas.untitled ? resource.fsPath : resource.toString();391return hash(str) + '';392}393394395