// ***************************************************************************** // Copyright (C) 2021 SAP SE or an SAP affiliate company and others. // // This program and the accompanying materials are made available under the // terms of the Eclipse Public License v. 2.0 which is available at // http://www.eclipse.org/legal/epl-2.0. // // This Source Code may also be made available under the following Secondary // Licenses when the conditions for such availability set forth in the Eclipse // Public License v. 2.0 are satisfied: GNU General Public License, version 2 // with the GNU Classpath Exception which is available at // https://www.gnu.org/software/classpath/license.html. // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** 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, PreferenceService } from '@theia/core'; 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 Map, disposable: Disposable }>(); private readonly resolvers = new Map Promise>(); private readonly onWillOpenCustomEditorEmitter = new Emitter(); readonly onWillOpenCustomEditor = this.onWillOpenCustomEditorEmitter.event; @inject(DefaultOpenerService) protected readonly defaultOpenerService: DefaultOpenerService; @inject(WidgetManager) protected readonly widgetManager: WidgetManager; @inject(ApplicationShell) protected readonly shell: ApplicationShell; @inject(OpenWithService) protected readonly openWithService: OpenWithService; @inject(PreferenceService) protected readonly preferenceService: PreferenceService; @postConstruct() protected init(): void { this.widgetManager.onDidCreateWidget(({ factoryId, widget }) => { if (factoryId === CustomEditorWidget.FACTORY_ID && widget instanceof CustomEditorWidget) { const restoreState = widget.restoreState.bind(widget); widget.restoreState = state => { if (state.viewType && state.strResource) { restoreState(state); this.resolveWidget(widget); } else { widget.dispose(); } }; } }); } registerCustomEditor(editor: CustomEditor, plugin: DeployedPlugin): Disposable { if (this.editors.has(editor.viewType)) { console.warn('editor with such id already registered: ', JSON.stringify(editor)); return Disposable.NULL; } this.editors.set(editor.viewType, editor); const toDispose = new DisposableCollection(); toDispose.push(Disposable.create(() => this.editors.delete(editor.viewType))); const editorOpenHandler = new CustomEditorOpener( editor, this.shell, this.widgetManager, this, this.preferenceService ); toDispose.push(this.defaultOpenerService.addHandler(editorOpenHandler)); toDispose.push( this.openWithService.registerHandler({ id: editor.viewType, label: editorOpenHandler.label, providerName: plugin.metadata.model.displayName, canHandle: uri => editorOpenHandler.canOpenWith(uri), open: uri => editorOpenHandler.open(uri) }) ); return toDispose; } async resolveWidget(widget: CustomEditorWidget): Promise { const resolver = this.resolvers.get(widget.viewType); if (resolver) { await resolver(widget); } else { 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) => Promise): Disposable { if (this.resolvers.has(viewType)) { throw new Error(`Resolver for ${viewType} already registered`); } for (const [editorWidget, { deferred, disposable }] of this.pendingEditors.entries()) { if (editorWidget.viewType === viewType) { resolver(editorWidget).then(() => deferred.resolve(), err => deferred.reject(err)).finally(() => disposable.dispose()); this.pendingEditors.delete(editorWidget); } } this.resolvers.set(viewType, resolver); return Disposable.create(() => this.resolvers.delete(viewType)); } }