diff --git a/packages/core/src/browser/widget-open-handler.ts b/packages/core/src/browser/widget-open-handler.ts index 7e3e4c37fab07..25802c6359869 100644 --- a/packages/core/src/browser/widget-open-handler.ts +++ b/packages/core/src/browser/widget-open-handler.ts @@ -24,7 +24,10 @@ import { WidgetManager } from './widget-manager'; export type WidgetOpenMode = 'open' | 'reveal' | 'activate'; /** - * `WidgetOpenerOptions` define serializable generic options used by the {@link WidgetOpenHandler}. + * `WidgetOpenerOptions` define generic options used by the {@link WidgetOpenHandler}. + * + * _Note:_ This object may contain references to widgets (e.g. `widgetOptions.ref`); + * these need to be transformed before it can be serialized. */ export interface WidgetOpenerOptions extends OpenerOptions { /** diff --git a/packages/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts index 0b081d3904639..0a3070cb5b377 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts @@ -1880,7 +1880,7 @@ export interface CustomEditorsExt { newWebviewHandle: string, viewType: string, title: string, - widgetOpenerOptions: object | undefined, + position: number, options: theia.WebviewPanelOptions, cancellation: CancellationToken): Promise; $createCustomDocument(resource: UriComponents, viewType: string, openContext: theia.CustomDocumentOpenContext, cancellation: CancellationToken): Promise<{ editable: boolean }>; @@ -1903,7 +1903,6 @@ export interface CustomEditorsMain { $registerTextEditorProvider(viewType: string, options: theia.WebviewPanelOptions, capabilities: CustomTextEditorCapabilities): void; $registerCustomEditorProvider(viewType: string, options: theia.WebviewPanelOptions, supportsMultipleEditorsPerDocument: boolean): void; $unregisterEditorProvider(viewType: string): void; - $createCustomEditorPanel(handle: string, title: string, widgetOpenerOptions: object | undefined, options: theia.WebviewPanelOptions & theia.WebviewOptions): Promise; $onDidEdit(resource: UriComponents, viewType: string, editId: number, label: string | undefined): void; $onContentChange(resource: UriComponents, viewType: string): void; } diff --git a/packages/plugin-ext/src/main/browser/custom-editors/custom-editor-opener.tsx b/packages/plugin-ext/src/main/browser/custom-editors/custom-editor-opener.tsx index 838c3ad08b162..00532f7a2bc61 100644 --- a/packages/plugin-ext/src/main/browser/custom-editors/custom-editor-opener.tsx +++ b/packages/plugin-ext/src/main/browser/custom-editors/custom-editor-opener.tsx @@ -14,11 +14,11 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { inject } from '@theia/core/shared/inversify'; import URI from '@theia/core/lib/common/uri'; -import { ApplicationShell, OpenHandler, Widget, WidgetManager, WidgetOpenerOptions } from '@theia/core/lib/browser'; +import { ApplicationShell, OpenHandler, WidgetManager, WidgetOpenerOptions } from '@theia/core/lib/browser'; import { CustomEditor, CustomEditorPriority, CustomEditorSelector } from '../../../common'; import { CustomEditorWidget } from './custom-editor-widget'; +import { PluginCustomEditorRegistry } from './plugin-custom-editor-registry'; import { generateUuid } from '@theia/core/lib/common/uuid'; import { Emitter } from '@theia/core'; import { match } from '@theia/core/lib/common/glob'; @@ -33,8 +33,9 @@ export class CustomEditorOpener implements OpenHandler { constructor( private readonly editor: CustomEditor, - @inject(ApplicationShell) protected readonly shell: ApplicationShell, - @inject(WidgetManager) protected readonly widgetManager: WidgetManager + protected readonly shell: ApplicationShell, + protected readonly widgetManager: WidgetManager, + protected readonly editorRegistry: PluginCustomEditorRegistry ) { this.id = CustomEditorOpener.toCustomEditorId(this.editor.viewType); this.label = this.editor.displayName; @@ -62,31 +63,44 @@ export class CustomEditorOpener implements OpenHandler { } protected readonly pendingWidgetPromises = new Map>(); - async open(uri: URI, options?: WidgetOpenerOptions): Promise { + async open(uri: URI, options?: WidgetOpenerOptions): Promise { let widget: CustomEditorWidget | undefined; - const widgets = this.widgetManager.getWidgets(CustomEditorWidget.FACTORY_ID) as CustomEditorWidget[]; - widget = widgets.find(w => w.viewType === this.editor.viewType && w.resource.toString() === uri.toString()); - - if (widget?.isVisible) { - return this.shell.revealWidget(widget.id); - } - if (widget?.isAttached) { - return this.shell.activateWidget(widget.id); - } - if (!widget) { - const uriString = uri.toString(); - let widgetPromise = this.pendingWidgetPromises.get(uriString); - if (!widgetPromise) { + let shouldNotify = false; + const uriString = uri.toString(); + let widgetPromise = this.pendingWidgetPromises.get(uriString); + if (widgetPromise) { + widget = await widgetPromise; + } else { + const widgets = this.widgetManager.getWidgets(CustomEditorWidget.FACTORY_ID) as CustomEditorWidget[]; + widget = widgets.find(w => w.viewType === this.editor.viewType && w.resource.toString() === uriString); + if (!widget) { + shouldNotify = true; const id = generateUuid(); - widgetPromise = this.widgetManager.getOrCreateWidget(CustomEditorWidget.FACTORY_ID, { id }); + widgetPromise = this.widgetManager.getOrCreateWidget(CustomEditorWidget.FACTORY_ID, { id }).then(async w => { + try { + w.viewType = this.editor.viewType; + w.resource = uri; + await this.editorRegistry.resolveWidget(w); + await this.shell.addWidget(w, options?.widgetOptions); + return w; + } catch (e) { + w.dispose(); + throw e; + } + }).finally(() => this.pendingWidgetPromises.delete(uriString)); this.pendingWidgetPromises.set(uriString, widgetPromise); widget = await widgetPromise; - this.pendingWidgetPromises.delete(uriString); - widget.viewType = this.editor.viewType; - widget.resource = uri; - this.onDidOpenCustomEditorEmitter.fire([widget, options]); } } + const mode = options?.mode ?? 'activate'; + if (mode === 'activate') { + await this.shell.activateWidget(widget.id); + } else if (mode === 'reveal') { + await this.shell.revealWidget(widget.id); + } + if (shouldNotify) { + this.onDidOpenCustomEditorEmitter.fire([widget, options]); + } return widget; } diff --git a/packages/plugin-ext/src/main/browser/custom-editors/custom-editors-main.ts b/packages/plugin-ext/src/main/browser/custom-editors/custom-editors-main.ts index 4368a500f1e69..a7b9181e7e24d 100644 --- a/packages/plugin-ext/src/main/browser/custom-editors/custom-editors-main.ts +++ b/packages/plugin-ext/src/main/browser/custom-editors/custom-editors-main.ts @@ -24,7 +24,6 @@ import { MAIN_RPC_CONTEXT, CustomEditorsMain, CustomEditorsExt, CustomTextEditor import { RPCProtocol } from '../../../common/rpc-protocol'; import { HostedPluginSupport } from '../../../hosted/browser/hosted-plugin'; import { PluginCustomEditorRegistry } from './plugin-custom-editor-registry'; -import { CustomEditorWidget } from './custom-editor-widget'; import { Emitter } from '@theia/core'; import { UriComponents } from '../../../common/uri-components'; import { URI } from '@theia/core/shared/vscode-uri'; @@ -39,11 +38,9 @@ import { FileService } from '@theia/filesystem/lib/browser/file-service'; import { UndoRedoService } from '@theia/editor/lib/browser/undo-redo-service'; import { WebviewsMainImpl } from '../webviews-main'; import { WidgetManager } from '@theia/core/lib/browser/widget-manager'; -import { ApplicationShell, DefaultUriLabelProviderContribution, Saveable, SaveOptions, WidgetOpenerOptions } from '@theia/core/lib/browser'; -import { WebviewOptions, WebviewPanelOptions, WebviewPanelShowOptions } from '@theia/plugin'; -import { WebviewWidgetIdentifier } from '../webview/webview'; +import { ApplicationShell, LabelProvider, Saveable, SaveOptions } from '@theia/core/lib/browser'; +import { WebviewPanelOptions } from '@theia/plugin'; import { EditorPreferences } from '@theia/editor/lib/browser'; -import { ViewColumn, WebviewPanelTargetArea } from '../../../plugin/types-impl'; const enum CustomEditorModelType { Custom, @@ -58,7 +55,7 @@ export class CustomEditorsMainImpl implements CustomEditorsMain, Disposable { protected readonly customEditorService: CustomEditorService; protected readonly undoRedoService: UndoRedoService; protected readonly customEditorRegistry: PluginCustomEditorRegistry; - protected readonly labelProvider: DefaultUriLabelProviderContribution; + protected readonly labelProvider: LabelProvider; protected readonly widgetManager: WidgetManager; protected readonly editorPreferences: EditorPreferences; private readonly proxy: CustomEditorsExt; @@ -75,7 +72,7 @@ export class CustomEditorsMainImpl implements CustomEditorsMain, Disposable { this.customEditorService = container.get(CustomEditorService); this.undoRedoService = container.get(UndoRedoService); this.customEditorRegistry = container.get(PluginCustomEditorRegistry); - this.labelProvider = container.get(DefaultUriLabelProviderContribution); + this.labelProvider = container.get(LabelProvider); this.editorPreferences = container.get(EditorPreferences); this.widgetManager = container.get(WidgetManager); this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.CUSTOM_EDITORS_EXT); @@ -111,7 +108,8 @@ export class CustomEditorsMainImpl implements CustomEditorsMain, Disposable { const disposables = new DisposableCollection(); disposables.push( - this.customEditorRegistry.registerResolver(viewType, async (widget, widgetOpenerOptions) => { + this.customEditorRegistry.registerResolver(viewType, async widget => { + const { resource, identifier } = widget; widget.options = options; @@ -144,13 +142,16 @@ export class CustomEditorsMainImpl implements CustomEditorsMain, Disposable { }); } + this.webviewsMain.hookWebview(widget); + widget.title.label = this.labelProvider.getName(resource); + const _cancellationSource = new CancellationTokenSource(); await this.proxy.$resolveWebviewEditor( resource.toComponents(), identifier.id, viewType, - this.labelProvider.getName(resource)!, - widgetOpenerOptions, + widget.title.label, + widget.viewState.position, options, _cancellationSource.token ); @@ -213,66 +214,6 @@ export class CustomEditorsMainImpl implements CustomEditorsMain, Disposable { const model = await this.getCustomEditorModel(resourceComponents, viewType); model.changeContent(); } - - async $createCustomEditorPanel( - panelId: string, - title: string, - widgetOpenerOptions: WidgetOpenerOptions | undefined, - options: WebviewPanelOptions & WebviewOptions - ): Promise { - const view = await this.widgetManager.getOrCreateWidget(CustomEditorWidget.FACTORY_ID, { id: panelId }); - this.webviewsMain.hookWebview(view); - view.title.label = title; - const { enableFindWidget, retainContextWhenHidden, enableScripts, enableForms, localResourceRoots, ...contentOptions } = options; - view.viewColumn = ViewColumn.One; // behaviour might be overridden later using widgetOpenerOptions (if available) - view.options = { enableFindWidget, retainContextWhenHidden }; - view.setContentOptions({ - allowScripts: enableScripts, - allowForms: enableForms, - localResourceRoots: localResourceRoots && localResourceRoots.map(root => root.toString()), - ...contentOptions, - ...view.contentOptions - }); - if (view.isAttached) { - if (view.isVisible) { - this.shell.revealWidget(view.id); - } - return; - } - const showOptions: WebviewPanelShowOptions = { - preserveFocus: true - }; - - if (widgetOpenerOptions) { - if (widgetOpenerOptions.mode === 'reveal') { - showOptions.preserveFocus = false; - } - - if (widgetOpenerOptions.widgetOptions) { - let area: WebviewPanelTargetArea; - switch (widgetOpenerOptions.widgetOptions.area) { - case 'main': - area = WebviewPanelTargetArea.Main; - case 'left': - area = WebviewPanelTargetArea.Left; - case 'right': - area = WebviewPanelTargetArea.Right; - case 'bottom': - area = WebviewPanelTargetArea.Bottom; - default: // includes 'top' and 'secondaryWindow' - area = WebviewPanelTargetArea.Main; - } - showOptions.area = area; - - if (widgetOpenerOptions.widgetOptions.mode === 'split-right' || - widgetOpenerOptions.widgetOptions.mode === 'open-to-right') { - showOptions.viewColumn = ViewColumn.Beside; - } - } - } - - this.webviewsMain.addOrReattachWidget(view, showOptions); - } } export interface CustomEditorModel extends Saveable, Disposable { diff --git a/packages/plugin-ext/src/main/browser/custom-editors/plugin-custom-editor-registry.ts b/packages/plugin-ext/src/main/browser/custom-editors/plugin-custom-editor-registry.ts index 50bfdb287f8a0..7250e3f4c0908 100644 --- a/packages/plugin-ext/src/main/browser/custom-editors/plugin-custom-editor-registry.ts +++ b/packages/plugin-ext/src/main/browser/custom-editors/plugin-custom-editor-registry.ts @@ -17,16 +17,17 @@ import { injectable, inject, postConstruct } from '@theia/core/shared/inversify'; import { CustomEditor, DeployedPlugin } from '../../../common'; import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; +import { Deferred } from '@theia/core/lib/common/promise-util'; import { CustomEditorOpener } from './custom-editor-opener'; import { Emitter } from '@theia/core'; -import { ApplicationShell, DefaultOpenerService, OpenWithService, WidgetManager, WidgetOpenerOptions } from '@theia/core/lib/browser'; +import { ApplicationShell, DefaultOpenerService, OpenWithService, WidgetManager } from '@theia/core/lib/browser'; import { CustomEditorWidget } from './custom-editor-widget'; @injectable() export class PluginCustomEditorRegistry { private readonly editors = new Map(); - private readonly pendingEditors = new Set(); - private readonly resolvers = new Map void>(); + private readonly pendingEditors = new Map, disposable: Disposable }>(); + private readonly resolvers = new Map Promise>(); private readonly onWillOpenCustomEditorEmitter = new Emitter(); readonly onWillOpenCustomEditor = this.onWillOpenCustomEditorEmitter.event; @@ -74,7 +75,8 @@ export class PluginCustomEditorRegistry { const editorOpenHandler = new CustomEditorOpener( editor, this.shell, - this.widgetManager + this.widgetManager, + this ); toDispose.push(this.defaultOpenerService.addHandler(editorOpenHandler)); toDispose.push( @@ -86,30 +88,30 @@ export class PluginCustomEditorRegistry { open: uri => editorOpenHandler.open(uri) }) ); - toDispose.push( - editorOpenHandler.onDidOpenCustomEditor(event => this.resolveWidget(event[0], event[1])) - ); return toDispose; } - resolveWidget = (widget: CustomEditorWidget, options?: WidgetOpenerOptions) => { + async resolveWidget(widget: CustomEditorWidget): Promise { const resolver = this.resolvers.get(widget.viewType); if (resolver) { - resolver(widget, options); + await resolver(widget); } else { - this.pendingEditors.add(widget); + const deferred = new Deferred(); + const disposable = widget.onDidDispose(() => this.pendingEditors.delete(widget)); + this.pendingEditors.set(widget, { deferred, disposable }); this.onWillOpenCustomEditorEmitter.fire(widget.viewType); + return deferred.promise; } }; - registerResolver(viewType: string, resolver: (widget: CustomEditorWidget, options?: WidgetOpenerOptions) => void): Disposable { + registerResolver(viewType: string, resolver: (widget: CustomEditorWidget) => Promise): Disposable { if (this.resolvers.has(viewType)) { throw new Error(`Resolver for ${viewType} already registered`); } - for (const editorWidget of this.pendingEditors) { + for (const [editorWidget, { deferred, disposable }] of this.pendingEditors.entries()) { if (editorWidget.viewType === viewType) { - resolver(editorWidget); + resolver(editorWidget).then(() => deferred.resolve(), err => deferred.reject(err)).finally(() => disposable.dispose()); this.pendingEditors.delete(editorWidget); } } diff --git a/packages/plugin-ext/src/main/browser/webview/webview.ts b/packages/plugin-ext/src/main/browser/webview/webview.ts index 543ad934b88ee..9ed8585bfc91d 100644 --- a/packages/plugin-ext/src/main/browser/webview/webview.ts +++ b/packages/plugin-ext/src/main/browser/webview/webview.ts @@ -48,7 +48,6 @@ import { isFirefox } from '@theia/core/lib/browser/browser'; import { FileService } from '@theia/filesystem/lib/browser/file-service'; import { FileOperationError, FileOperationResult } from '@theia/filesystem/lib/common/files'; import { BinaryBufferReadableStream } from '@theia/core/lib/common/buffer'; -import { ViewColumn } from '../../../plugin/types-impl'; import { ExtractableWidget } from '@theia/core/lib/browser/widgets/extractable-widget'; import { BadgeWidget } from '@theia/core/lib/browser/view-container'; import { MenuPath } from '@theia/core'; @@ -185,7 +184,6 @@ export class WebviewWidget extends BaseWidget implements StatefulWidget, Extract } viewType: string; - viewColumn: ViewColumn; options: WebviewPanelOptions = {}; protected ready = new Deferred(); diff --git a/packages/plugin-ext/src/plugin/custom-editors.ts b/packages/plugin-ext/src/plugin/custom-editors.ts index 4ec88ce886fe9..700151a37303d 100644 --- a/packages/plugin-ext/src/plugin/custom-editors.ts +++ b/packages/plugin-ext/src/plugin/custom-editors.ts @@ -25,11 +25,11 @@ import { RPCProtocol } from '../common/rpc-protocol'; import { Disposable, URI } from './types-impl'; import { UriComponents } from '../common/uri-components'; import { DocumentsExtImpl } from './documents'; -import { WebviewImpl, WebviewsExtImpl } from './webviews'; +import { WebviewsExtImpl } from './webviews'; import { CancellationToken, CancellationTokenSource } from '@theia/core/lib/common/cancellation'; import { DisposableCollection } from '@theia/core/lib/common/disposable'; -import { WorkspaceExtImpl } from './workspace'; import { Cache } from '../common/cache'; +import * as Converters from './type-converters'; export class CustomEditorsExtImpl implements CustomEditorsExt { private readonly proxy: CustomEditorsMain; @@ -38,8 +38,7 @@ export class CustomEditorsExtImpl implements CustomEditorsExt { constructor(rpc: RPCProtocol, private readonly documentExt: DocumentsExtImpl, - private readonly webviewExt: WebviewsExtImpl, - private readonly workspace: WorkspaceExtImpl) { + private readonly webviewExt: WebviewsExtImpl) { this.proxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.CUSTOM_EDITORS_MAIN); } @@ -116,22 +115,21 @@ export class CustomEditorsExtImpl implements CustomEditorsExt { document.dispose(); } - async $resolveWebviewEditor( + async $resolveWebviewEditor( resource: UriComponents, handler: string, viewType: string, title: string, - widgetOpenerOptions: object | undefined, - options: theia.WebviewPanelOptions & theia.WebviewOptions, + position: number, + options: theia.WebviewPanelOptions, cancellation: CancellationToken ): Promise { const entry = this.editorProviders.get(viewType); if (!entry) { throw new Error(`No provider found for '${viewType}'`); } - const panel = this.webviewExt.createWebviewPanel(viewType, title, {}, options, entry.plugin, handler, false); - const webviewOptions = WebviewImpl.toWebviewOptions(options, this.workspace, entry.plugin); - await this.proxy.$createCustomEditorPanel(handler, title, widgetOpenerOptions, webviewOptions); + const viewColumn = Converters.toViewColumn(position); + const panel = this.webviewExt.createWebviewPanel(viewType, title, { viewColumn }, options, entry.plugin, handler, false); const revivedResource = URI.revive(resource); diff --git a/packages/plugin-ext/src/plugin/plugin-context.ts b/packages/plugin-ext/src/plugin/plugin-context.ts index 2dc28f72b501b..c45de8ac715bc 100644 --- a/packages/plugin-ext/src/plugin/plugin-context.ts +++ b/packages/plugin-ext/src/plugin/plugin-context.ts @@ -316,7 +316,7 @@ export function createAPIFactory( const themingExt = rpc.set(MAIN_RPC_CONTEXT.THEMING_EXT, new ThemingExtImpl(rpc)); const commentsExt = rpc.set(MAIN_RPC_CONTEXT.COMMENTS_EXT, new CommentsExtImpl(rpc, commandRegistry, documents)); const tabsExt = rpc.set(MAIN_RPC_CONTEXT.TABS_EXT, new TabsExtImpl(rpc)); - const customEditorExt = rpc.set(MAIN_RPC_CONTEXT.CUSTOM_EDITORS_EXT, new CustomEditorsExtImpl(rpc, documents, webviewExt, workspaceExt)); + const customEditorExt = rpc.set(MAIN_RPC_CONTEXT.CUSTOM_EDITORS_EXT, new CustomEditorsExtImpl(rpc, documents, webviewExt)); const webviewViewsExt = rpc.set(MAIN_RPC_CONTEXT.WEBVIEW_VIEWS_EXT, new WebviewViewsExtImpl(rpc, webviewExt)); const telemetryExt = rpc.set(MAIN_RPC_CONTEXT.TELEMETRY_EXT, new TelemetryExtImpl()); const testingExt = rpc.set(MAIN_RPC_CONTEXT.TESTING_EXT, new TestingExtImpl(rpc, commandRegistry));