mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-23 01:58:53 +01:00
* editor commands - move API commands to workbench core * rename EditorViewColumn => EditorGroupColumn * mixin context to open commands * address some feedback * add comment
389 lines
14 KiB
TypeScript
389 lines
14 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import { CancellationToken } from 'vs/base/common/cancellation';
|
|
import { hash } from 'vs/base/common/hash';
|
|
import { DisposableStore } from 'vs/base/common/lifecycle';
|
|
import { Schemas } from 'vs/base/common/network';
|
|
import { joinPath } from 'vs/base/common/resources';
|
|
import { URI, UriComponents } from 'vs/base/common/uri';
|
|
import * as modes from 'vs/editor/common/modes';
|
|
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
|
|
import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments';
|
|
import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths';
|
|
import { ExtHostWebviews, toExtensionData } from 'vs/workbench/api/common/extHostWebview';
|
|
import { ExtHostWebviewPanels } from 'vs/workbench/api/common/extHostWebviewPanels';
|
|
import { EditorGroupColumn } from 'vs/workbench/common/editor';
|
|
import type * as vscode from 'vscode';
|
|
import { Cache } from './cache';
|
|
import * as extHostProtocol from './extHost.protocol';
|
|
import * as extHostTypes from './extHostTypes';
|
|
|
|
|
|
class CustomDocumentStoreEntry {
|
|
|
|
private _backupCounter = 1;
|
|
|
|
constructor(
|
|
public readonly document: vscode.CustomDocument,
|
|
private readonly _storagePath: URI | undefined,
|
|
) { }
|
|
|
|
private readonly _edits = new Cache<vscode.CustomDocumentEditEvent>('custom documents');
|
|
|
|
private _backup?: vscode.CustomDocumentBackup;
|
|
|
|
addEdit(item: vscode.CustomDocumentEditEvent): number {
|
|
return this._edits.add([item]);
|
|
}
|
|
|
|
async undo(editId: number, isDirty: boolean): Promise<void> {
|
|
await this.getEdit(editId).undo();
|
|
if (!isDirty) {
|
|
this.disposeBackup();
|
|
}
|
|
}
|
|
|
|
async redo(editId: number, isDirty: boolean): Promise<void> {
|
|
await this.getEdit(editId).redo();
|
|
if (!isDirty) {
|
|
this.disposeBackup();
|
|
}
|
|
}
|
|
|
|
disposeEdits(editIds: number[]): void {
|
|
for (const id of editIds) {
|
|
this._edits.delete(id);
|
|
}
|
|
}
|
|
|
|
getNewBackupUri(): URI {
|
|
if (!this._storagePath) {
|
|
throw new Error('Backup requires a valid storage path');
|
|
}
|
|
const fileName = hashPath(this.document.uri) + (this._backupCounter++);
|
|
return joinPath(this._storagePath, fileName);
|
|
}
|
|
|
|
updateBackup(backup: vscode.CustomDocumentBackup): void {
|
|
this._backup?.delete();
|
|
this._backup = backup;
|
|
}
|
|
|
|
disposeBackup(): void {
|
|
this._backup?.delete();
|
|
this._backup = undefined;
|
|
}
|
|
|
|
private getEdit(editId: number): vscode.CustomDocumentEditEvent {
|
|
const edit = this._edits.get(editId, 0);
|
|
if (!edit) {
|
|
throw new Error('No edit found');
|
|
}
|
|
return edit;
|
|
}
|
|
}
|
|
|
|
class CustomDocumentStore {
|
|
private readonly _documents = new Map<string, CustomDocumentStoreEntry>();
|
|
|
|
public get(viewType: string, resource: vscode.Uri): CustomDocumentStoreEntry | undefined {
|
|
return this._documents.get(this.key(viewType, resource));
|
|
}
|
|
|
|
public add(viewType: string, document: vscode.CustomDocument, storagePath: URI | undefined): CustomDocumentStoreEntry {
|
|
const key = this.key(viewType, document.uri);
|
|
if (this._documents.has(key)) {
|
|
throw new Error(`Document already exists for viewType:${viewType} resource:${document.uri}`);
|
|
}
|
|
const entry = new CustomDocumentStoreEntry(document, storagePath);
|
|
this._documents.set(key, entry);
|
|
return entry;
|
|
}
|
|
|
|
public delete(viewType: string, document: vscode.CustomDocument) {
|
|
const key = this.key(viewType, document.uri);
|
|
this._documents.delete(key);
|
|
}
|
|
|
|
private key(viewType: string, resource: vscode.Uri): string {
|
|
return `${viewType}@@@${resource}`;
|
|
}
|
|
|
|
}
|
|
|
|
const enum WebviewEditorType {
|
|
Text,
|
|
Custom
|
|
}
|
|
|
|
type ProviderEntry = {
|
|
readonly extension: IExtensionDescription;
|
|
readonly type: WebviewEditorType.Text;
|
|
readonly provider: vscode.CustomTextEditorProvider;
|
|
} | {
|
|
readonly extension: IExtensionDescription;
|
|
readonly type: WebviewEditorType.Custom;
|
|
readonly provider: vscode.CustomReadonlyEditorProvider;
|
|
};
|
|
|
|
class EditorProviderStore {
|
|
private readonly _providers = new Map<string, ProviderEntry>();
|
|
|
|
public addTextProvider(viewType: string, extension: IExtensionDescription, provider: vscode.CustomTextEditorProvider): vscode.Disposable {
|
|
return this.add(WebviewEditorType.Text, viewType, extension, provider);
|
|
}
|
|
|
|
public addCustomProvider(viewType: string, extension: IExtensionDescription, provider: vscode.CustomReadonlyEditorProvider): vscode.Disposable {
|
|
return this.add(WebviewEditorType.Custom, viewType, extension, provider);
|
|
}
|
|
|
|
public get(viewType: string): ProviderEntry | undefined {
|
|
return this._providers.get(viewType);
|
|
}
|
|
|
|
private add(type: WebviewEditorType, viewType: string, extension: IExtensionDescription, provider: vscode.CustomTextEditorProvider | vscode.CustomReadonlyEditorProvider): vscode.Disposable {
|
|
if (this._providers.has(viewType)) {
|
|
throw new Error(`Provider for viewType:${viewType} already registered`);
|
|
}
|
|
this._providers.set(viewType, { type, extension, provider } as ProviderEntry);
|
|
return new extHostTypes.Disposable(() => this._providers.delete(viewType));
|
|
}
|
|
}
|
|
|
|
export class ExtHostCustomEditors implements extHostProtocol.ExtHostCustomEditorsShape {
|
|
|
|
private readonly _proxy: extHostProtocol.MainThreadCustomEditorsShape;
|
|
|
|
private readonly _editorProviders = new EditorProviderStore();
|
|
|
|
private readonly _documents = new CustomDocumentStore();
|
|
|
|
constructor(
|
|
mainContext: extHostProtocol.IMainContext,
|
|
private readonly _extHostDocuments: ExtHostDocuments,
|
|
private readonly _extensionStoragePaths: IExtensionStoragePaths | undefined,
|
|
private readonly _extHostWebview: ExtHostWebviews,
|
|
private readonly _extHostWebviewPanels: ExtHostWebviewPanels,
|
|
) {
|
|
this._proxy = mainContext.getProxy(extHostProtocol.MainContext.MainThreadCustomEditors);
|
|
}
|
|
|
|
public registerCustomEditorProvider(
|
|
extension: IExtensionDescription,
|
|
viewType: string,
|
|
provider: vscode.CustomReadonlyEditorProvider | vscode.CustomTextEditorProvider,
|
|
options: { webviewOptions?: vscode.WebviewPanelOptions, supportsMultipleEditorsPerDocument?: boolean },
|
|
): vscode.Disposable {
|
|
const disposables = new DisposableStore();
|
|
if ('resolveCustomTextEditor' in provider) {
|
|
disposables.add(this._editorProviders.addTextProvider(viewType, extension, provider));
|
|
this._proxy.$registerTextEditorProvider(toExtensionData(extension), viewType, options.webviewOptions || {}, {
|
|
supportsMove: !!provider.moveCustomTextEditor,
|
|
});
|
|
} else {
|
|
disposables.add(this._editorProviders.addCustomProvider(viewType, extension, provider));
|
|
|
|
if (this.supportEditing(provider)) {
|
|
disposables.add(provider.onDidChangeCustomDocument(e => {
|
|
const entry = this.getCustomDocumentEntry(viewType, e.document.uri);
|
|
if (isEditEvent(e)) {
|
|
const editId = entry.addEdit(e);
|
|
this._proxy.$onDidEdit(e.document.uri, viewType, editId, e.label);
|
|
} else {
|
|
this._proxy.$onContentChange(e.document.uri, viewType);
|
|
}
|
|
}));
|
|
}
|
|
|
|
this._proxy.$registerCustomEditorProvider(toExtensionData(extension), viewType, options.webviewOptions || {}, !!options.supportsMultipleEditorsPerDocument);
|
|
}
|
|
|
|
return extHostTypes.Disposable.from(
|
|
disposables,
|
|
new extHostTypes.Disposable(() => {
|
|
this._proxy.$unregisterEditorProvider(viewType);
|
|
}));
|
|
}
|
|
|
|
|
|
async $createCustomDocument(resource: UriComponents, viewType: string, backupId: string | undefined, cancellation: CancellationToken) {
|
|
const entry = this._editorProviders.get(viewType);
|
|
if (!entry) {
|
|
throw new Error(`No provider found for '${viewType}'`);
|
|
}
|
|
|
|
if (entry.type !== WebviewEditorType.Custom) {
|
|
throw new Error(`Invalid provide type for '${viewType}'`);
|
|
}
|
|
|
|
const revivedResource = URI.revive(resource);
|
|
const document = await entry.provider.openCustomDocument(revivedResource, { backupId }, cancellation);
|
|
|
|
let storageRoot: URI | undefined;
|
|
if (this.supportEditing(entry.provider) && this._extensionStoragePaths) {
|
|
storageRoot = this._extensionStoragePaths.workspaceValue(entry.extension) ?? this._extensionStoragePaths.globalValue(entry.extension);
|
|
}
|
|
this._documents.add(viewType, document, storageRoot);
|
|
|
|
return { editable: this.supportEditing(entry.provider) };
|
|
}
|
|
|
|
async $disposeCustomDocument(resource: UriComponents, viewType: string): Promise<void> {
|
|
const entry = this._editorProviders.get(viewType);
|
|
if (!entry) {
|
|
throw new Error(`No provider found for '${viewType}'`);
|
|
}
|
|
|
|
if (entry.type !== WebviewEditorType.Custom) {
|
|
throw new Error(`Invalid provider type for '${viewType}'`);
|
|
}
|
|
|
|
const revivedResource = URI.revive(resource);
|
|
const { document } = this.getCustomDocumentEntry(viewType, revivedResource);
|
|
this._documents.delete(viewType, document);
|
|
document.dispose();
|
|
}
|
|
|
|
async $resolveWebviewEditor(
|
|
resource: UriComponents,
|
|
handle: extHostProtocol.WebviewHandle,
|
|
viewType: string,
|
|
title: string,
|
|
position: EditorGroupColumn,
|
|
options: modes.IWebviewOptions & modes.IWebviewPanelOptions,
|
|
cancellation: CancellationToken,
|
|
): Promise<void> {
|
|
const entry = this._editorProviders.get(viewType);
|
|
if (!entry) {
|
|
throw new Error(`No provider found for '${viewType}'`);
|
|
}
|
|
|
|
const webview = this._extHostWebview.createNewWebview(handle, options, entry.extension);
|
|
const panel = this._extHostWebviewPanels.createNewWebviewPanel(handle, viewType, title, position, options, webview);
|
|
|
|
const revivedResource = URI.revive(resource);
|
|
|
|
switch (entry.type) {
|
|
case WebviewEditorType.Custom:
|
|
{
|
|
const { document } = this.getCustomDocumentEntry(viewType, revivedResource);
|
|
return entry.provider.resolveCustomEditor(document, panel, cancellation);
|
|
}
|
|
case WebviewEditorType.Text:
|
|
{
|
|
const document = this._extHostDocuments.getDocument(revivedResource);
|
|
return entry.provider.resolveCustomTextEditor(document, panel, cancellation);
|
|
}
|
|
default:
|
|
{
|
|
throw new Error('Unknown webview provider type');
|
|
}
|
|
}
|
|
}
|
|
|
|
$disposeEdits(resourceComponents: UriComponents, viewType: string, editIds: number[]): void {
|
|
const document = this.getCustomDocumentEntry(viewType, resourceComponents);
|
|
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._extHostWebviewPanels.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, CancellationToken.None);
|
|
}
|
|
|
|
async $undo(resourceComponents: UriComponents, viewType: string, editId: number, isDirty: boolean): Promise<void> {
|
|
const entry = this.getCustomDocumentEntry(viewType, resourceComponents);
|
|
return entry.undo(editId, isDirty);
|
|
}
|
|
|
|
async $redo(resourceComponents: UriComponents, viewType: string, editId: number, isDirty: boolean): Promise<void> {
|
|
const entry = this.getCustomDocumentEntry(viewType, resourceComponents);
|
|
return entry.redo(editId, isDirty);
|
|
}
|
|
|
|
async $revert(resourceComponents: UriComponents, viewType: string, cancellation: CancellationToken): Promise<void> {
|
|
const entry = this.getCustomDocumentEntry(viewType, resourceComponents);
|
|
const provider = this.getCustomEditorProvider(viewType);
|
|
await provider.revertCustomDocument(entry.document, cancellation);
|
|
entry.disposeBackup();
|
|
}
|
|
|
|
async $onSave(resourceComponents: UriComponents, viewType: string, cancellation: CancellationToken): Promise<void> {
|
|
const entry = this.getCustomDocumentEntry(viewType, resourceComponents);
|
|
const provider = this.getCustomEditorProvider(viewType);
|
|
await provider.saveCustomDocument(entry.document, cancellation);
|
|
entry.disposeBackup();
|
|
}
|
|
|
|
async $onSaveAs(resourceComponents: UriComponents, viewType: string, targetResource: UriComponents, cancellation: CancellationToken): Promise<void> {
|
|
const entry = this.getCustomDocumentEntry(viewType, resourceComponents);
|
|
const provider = this.getCustomEditorProvider(viewType);
|
|
return provider.saveCustomDocumentAs(entry.document, URI.revive(targetResource), cancellation);
|
|
}
|
|
|
|
async $backup(resourceComponents: UriComponents, viewType: string, cancellation: CancellationToken): Promise<string> {
|
|
const entry = this.getCustomDocumentEntry(viewType, resourceComponents);
|
|
const provider = this.getCustomEditorProvider(viewType);
|
|
|
|
const backup = await provider.backupCustomDocument(entry.document, {
|
|
destination: entry.getNewBackupUri(),
|
|
}, cancellation);
|
|
entry.updateBackup(backup);
|
|
return backup.id;
|
|
}
|
|
|
|
|
|
private getCustomDocumentEntry(viewType: string, resource: UriComponents): CustomDocumentStoreEntry {
|
|
const entry = this._documents.get(viewType, URI.revive(resource));
|
|
if (!entry) {
|
|
throw new Error('No custom document found');
|
|
}
|
|
return entry;
|
|
}
|
|
|
|
private getCustomEditorProvider(viewType: string): vscode.CustomEditorProvider {
|
|
const entry = this._editorProviders.get(viewType);
|
|
const provider = entry?.provider;
|
|
if (!provider || !this.supportEditing(provider)) {
|
|
throw new Error('Custom document is not editable');
|
|
}
|
|
return provider;
|
|
}
|
|
|
|
private supportEditing(
|
|
provider: vscode.CustomTextEditorProvider | vscode.CustomEditorProvider | vscode.CustomReadonlyEditorProvider
|
|
): provider is vscode.CustomEditorProvider {
|
|
return !!(provider as vscode.CustomEditorProvider).onDidChangeCustomDocument;
|
|
}
|
|
}
|
|
|
|
|
|
function isEditEvent(e: vscode.CustomDocumentContentChangeEvent | vscode.CustomDocumentEditEvent): e is vscode.CustomDocumentEditEvent {
|
|
return typeof (e as vscode.CustomDocumentEditEvent).undo === 'function'
|
|
&& typeof (e as vscode.CustomDocumentEditEvent).redo === 'function';
|
|
}
|
|
|
|
function hashPath(resource: URI): string {
|
|
const str = resource.scheme === Schemas.file || resource.scheme === Schemas.untitled ? resource.fsPath : resource.toString();
|
|
return hash(str) + '';
|
|
}
|
|
|