From 9af5ee3eb46bb54e0a8e2c822eca6699d0b3df3f Mon Sep 17 00:00:00 2001 From: Anton Kosyakov Date: Tue, 21 Jan 2020 06:25:55 +0000 Subject: [PATCH] =?UTF-8?q?[monaco]=C2=A0fix=20#6920:=20handle=20internal?= =?UTF-8?q?=20open=20calls=20with=20OpenerService?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anton Kosyakov --- examples/api-tests/src/monaco-api.spec.js | 24 ++++++++++ .../src/browser/command-open-handler.ts} | 25 ++++++----- .../browser/frontend-application-module.ts | 4 ++ .../core/src/browser/http-open-handler.ts | 8 +++- packages/editor/src/browser/editor-manager.ts | 22 ++++++++-- .../src/browser/monaco-editor-provider.ts | 44 +++++++++++++++++-- packages/monaco/src/browser/monaco-loader.ts | 5 ++- packages/monaco/src/typings/monaco/index.d.ts | 19 ++++++++ .../browser/plugin-ext-frontend-module.ts | 10 ++--- .../src/main/browser/text-editor-main.ts | 1 + 10 files changed, 132 insertions(+), 30 deletions(-) rename packages/{plugin-ext/src/main/browser/plugin-command-open-handler.ts => core/src/browser/command-open-handler.ts} (69%) diff --git a/examples/api-tests/src/monaco-api.spec.js b/examples/api-tests/src/monaco-api.spec.js index ab74e94655faa..b7b990e6f5cee 100644 --- a/examples/api-tests/src/monaco-api.spec.js +++ b/examples/api-tests/src/monaco-api.spec.js @@ -25,11 +25,13 @@ describe('Monaco API', async function () { const { WorkspaceService } = require('@theia/workspace/lib/browser/workspace-service'); const { MonacoEditor } = require('@theia/monaco/lib/browser/monaco-editor'); const { MonacoResolvedKeybinding } = require('@theia/monaco/lib/browser/monaco-resolved-keybinding'); + const { CommandRegistry } = require('@theia/core/lib/common/command'); /** @type {import('inversify').Container} */ const container = window['theia'].container; const editorManager = container.get(EditorManager); const workspaceService = container.get(WorkspaceService); + const commands = container.get(CommandRegistry); /** @type {MonacoEditor} */ let monacoEditor; @@ -87,4 +89,26 @@ describe('Monaco API', async function () { } }); + it('OpenerService.open', async () => { + const hoverContribution = monacoEditor.getControl().getContribution('editor.contrib.hover'); + assert.isDefined(hoverContribution); + if (!('_openerService' in hoverContribution)) { + assert.fail(`hoverContribution does not have OpenerService`); + } + /** @type {monaco.services.OpenerService} */ + const openerService = hoverContribution['_openerService']; + + let opened = false; + const id = '__test:OpenerService.open' + const unregisterCommand = commands.registerCommand({ id }, { + execute: () => opened = true + }); + try { + await openerService.open(monaco.Uri.parse('command:' + id)); + assert.isTrue(opened); + } finally { + unregisterCommand.dispose(); + } + }); + }); diff --git a/packages/plugin-ext/src/main/browser/plugin-command-open-handler.ts b/packages/core/src/browser/command-open-handler.ts similarity index 69% rename from packages/plugin-ext/src/main/browser/plugin-command-open-handler.ts rename to packages/core/src/browser/command-open-handler.ts index 4d685ab30a446..9c20260bf089a 100644 --- a/packages/plugin-ext/src/main/browser/plugin-command-open-handler.ts +++ b/packages/core/src/browser/command-open-handler.ts @@ -15,33 +15,34 @@ ********************************************************************************/ import { injectable, inject } from 'inversify'; -import URI from '@theia/core/lib/common/uri'; -import { OpenHandler } from '@theia/core/lib/browser/opener-service'; -import { Schemes } from '../../common/uri-components'; -import { CommandService } from '@theia/core/lib/common/command'; +import { CommandService } from '../common/command'; +import URI from '../common/uri'; +import { OpenHandler } from './opener-service'; @injectable() -export class PluginCommandOpenHandler implements OpenHandler { +export class CommandOpenHandler implements OpenHandler { - readonly id = 'plugin-command'; + readonly id = 'command'; @inject(CommandService) protected readonly commands: CommandService; canHandle(uri: URI): number { - return uri.scheme === Schemes.COMMAND ? 500 : -1; + return uri.scheme === 'command' ? 500 : -1; } async open(uri: URI): Promise { // tslint:disable-next-line:no-any let args: any = []; try { - args = JSON.parse(uri.query); - if (!Array.isArray(args)) { - args = [args]; + args = JSON.parse(decodeURIComponent(uri.query)); + } catch { + // ignore and retry + try { + args = JSON.parse(uri.query); + } catch { + // ignore error } - } catch (e) { - // ignore error } await this.commands.executeCommand(uri.path.toString(), ...args); return true; diff --git a/packages/core/src/browser/frontend-application-module.ts b/packages/core/src/browser/frontend-application-module.ts index 18b0c189a507b..eb7390cfdd449 100644 --- a/packages/core/src/browser/frontend-application-module.ts +++ b/packages/core/src/browser/frontend-application-module.ts @@ -89,6 +89,7 @@ import { ExternalUriService } from './external-uri-service'; import { IconThemeService, NoneIconTheme } from './icon-theme-service'; import { IconThemeApplicationContribution, IconThemeContribution, DefaultFileIconThemeContribution } from './icon-theme-contribution'; import { TreeLabelProvider } from './tree/tree-label-provider'; +import { CommandOpenHandler } from './command-open-handler'; export { bindResourceProvider, bindMessageService, bindPreferenceService }; @@ -159,6 +160,9 @@ export const frontendApplicationModule = new ContainerModule((bind, unbind, isBo bind(HttpOpenHandler).toSelf().inSingletonScope(); bind(OpenHandler).toService(HttpOpenHandler); + bind(CommandOpenHandler).toSelf().inSingletonScope(); + bind(OpenHandler).toService(CommandOpenHandler); + bindContributionProvider(bind, ApplicationShellLayoutMigration); bind(ApplicationShellLayoutMigration).toConstantValue({ layoutVersion: 2.0, diff --git a/packages/core/src/browser/http-open-handler.ts b/packages/core/src/browser/http-open-handler.ts index d21ec019e308c..dfd2d6cfe78d7 100644 --- a/packages/core/src/browser/http-open-handler.ts +++ b/packages/core/src/browser/http-open-handler.ts @@ -20,6 +20,10 @@ import { OpenHandler } from './opener-service'; import { WindowService } from './window/window-service'; import { ExternalUriService } from './external-uri-service'; +export interface HttpOpenHandlerOptions { + openExternal?: boolean +} + @injectable() export class HttpOpenHandler implements OpenHandler { @@ -31,8 +35,8 @@ export class HttpOpenHandler implements OpenHandler { @inject(ExternalUriService) protected readonly externalUriService: ExternalUriService; - canHandle(uri: URI): number { - return (uri.scheme.startsWith('http') || uri.scheme.startsWith('mailto')) ? 500 : 0; + canHandle(uri: URI, options?: HttpOpenHandlerOptions): number { + return ((options && options.openExternal) || uri.scheme.startsWith('http') || uri.scheme.startsWith('mailto')) ? 500 : 0; } async open(uri: URI): Promise { diff --git a/packages/editor/src/browser/editor-manager.ts b/packages/editor/src/browser/editor-manager.ts index 29b549d8f8dba..090627271e67e 100644 --- a/packages/editor/src/browser/editor-manager.ts +++ b/packages/editor/src/browser/editor-manager.ts @@ -103,14 +103,28 @@ export class EditorManager extends NavigatableWidgetOpenHandler { async open(uri: URI, options?: EditorOpenerOptions): Promise { const editor = await super.open(uri, options); - this.revealSelection(editor, options); + this.revealSelection(editor, options, uri); return editor; } - protected revealSelection(widget: EditorWidget, input?: EditorOpenerOptions): void { - if (input && input.selection) { + protected revealSelection(widget: EditorWidget, input?: EditorOpenerOptions, uri?: URI): void { + let inputSelection = input && input.selection; + if (!inputSelection && uri) { + const match = /^L?(\d+)(?:,(\d+))?/.exec(uri.fragment); + if (match) { + // support file:///some/file.js#73,84 + // support file:///some/file.js#L73 + inputSelection = { + start: { + line: parseInt(match[1]) - 1, + character: match[2] ? parseInt(match[2]) - 1 : 0 + } + }; + } + } + if (inputSelection) { const editor = widget.editor; - const selection = this.getSelection(widget, input.selection); + const selection = this.getSelection(widget, inputSelection); if (Position.is(selection)) { editor.cursor = selection; editor.revealPosition(selection); diff --git a/packages/monaco/src/browser/monaco-editor-provider.ts b/packages/monaco/src/browser/monaco-editor-provider.ts index 64d965f56f2c4..00e5c33f94a4b 100644 --- a/packages/monaco/src/browser/monaco-editor-provider.ts +++ b/packages/monaco/src/browser/monaco-editor-provider.ts @@ -36,8 +36,9 @@ import { MonacoBulkEditService } from './monaco-bulk-edit-service'; import IEditorOverrideServices = monaco.editor.IEditorOverrideServices; import { ApplicationServer } from '@theia/core/lib/common/application-protocol'; import { OS } from '@theia/core'; -import { KeybindingRegistry } from '@theia/core/lib/browser'; +import { KeybindingRegistry, OpenerService, open, WidgetOpenerOptions } from '@theia/core/lib/browser'; import { MonacoResolvedKeybinding } from './monaco-resolved-keybinding'; +import { HttpOpenHandlerOptions } from '@theia/core/lib/browser/http-open-handler'; @injectable() export class MonacoEditorProvider { @@ -49,7 +50,10 @@ export class MonacoEditorProvider { protected readonly services: MonacoEditorServices; @inject(KeybindingRegistry) - protected keybindingRegistry: KeybindingRegistry; + protected readonly keybindingRegistry: KeybindingRegistry; + + @inject(OpenerService) + protected readonly openerService: OpenerService; private isWindowsBackend: boolean = false; @@ -119,6 +123,10 @@ export class MonacoEditorProvider { const contextKeyService = this.contextKeyService.createScoped(); const { codeEditorService, textModelService, contextMenuService } = this; const IWorkspaceEditService = this.bulkEditService; + const openerService = new monaco.services.OpenerService(codeEditorService, commandService); + openerService.registerOpener({ + open: (uri, options) => this.interceptOpen(uri, options) + }); const toDispose = new DisposableCollection(); const editor = await factory({ codeEditorService, @@ -126,7 +134,8 @@ export class MonacoEditorProvider { contextMenuService, commandService, IWorkspaceEditService, - contextKeyService + contextKeyService, + openerService }, toDispose); editor.onDispose(() => toDispose.dispose()); @@ -152,6 +161,35 @@ export class MonacoEditorProvider { return editor; } + /** + * Intercept internal Monaco open calls and delegate to OpenerService. + */ + protected async interceptOpen(monacoUri: monaco.Uri | string, monacoOptions?: monaco.services.OpenInternalOptions | monaco.services.OpenExternalOptions): Promise { + let options = undefined; + if (monacoOptions) { + if ('openToSide' in monacoOptions && monacoOptions.openToSide) { + options = Object.assign(options || {}, { + widgetOptions: { + mode: 'split-right' + } + }); + } + if ('openExternal' in monacoOptions && monacoOptions.openExternal) { + options = Object.assign(options || {}, { + openExternal: true + }); + } + } + const uri = new URI(monacoUri.toString()); + try { + await open(this.openerService, uri, options); + return true; + } catch (e) { + console.error(`Fail to open '${uri.toString()}':`, e); + return false; + } + } + /** * Suppresses Monaco keydown listener to avoid triggering default Monaco keybindings * if they are overriden by a user. Monaco keybindings should be registered as Theia keybindings diff --git a/packages/monaco/src/browser/monaco-loader.ts b/packages/monaco/src/browser/monaco-loader.ts index cf97c381a36f1..29045105a0838 100644 --- a/packages/monaco/src/browser/monaco-loader.ts +++ b/packages/monaco/src/browser/monaco-loader.ts @@ -71,6 +71,7 @@ export function loadMonaco(vsRequire: any): Promise { 'vs/platform/configuration/common/configurationModels', 'vs/editor/browser/services/codeEditorService', 'vs/editor/browser/services/codeEditorServiceImpl', + 'vs/editor/browser/services/openerService', 'vs/platform/markers/common/markerService', 'vs/platform/contextkey/common/contextkey', 'vs/platform/contextkey/browser/contextKeyService', @@ -81,7 +82,7 @@ export function loadMonaco(vsRequire: any): Promise { filters: any, styler: any, colorRegistry: any, color: any, platform: any, modes: any, suggest: any, snippetParser: any, configuration: any, configurationModels: any, - codeEditorService: any, codeEditorServiceImpl: any, + codeEditorService: any, codeEditorServiceImpl: any, openerService: any, markerService: any, contextKey: any, contextKeyService: any, error: any) => { @@ -90,7 +91,7 @@ export function loadMonaco(vsRequire: any): Promise { global.monaco.actions = actions; global.monaco.keybindings = Object.assign({}, keybindingsRegistry, keybindingResolver, resolvedKeybinding, keybindingLabels, keyCodes); global.monaco.services = Object.assign({}, simpleServices, standaloneServices, configuration, configurationModels, - codeEditorService, codeEditorServiceImpl, markerService); + codeEditorService, codeEditorServiceImpl, markerService, openerService); global.monaco.quickOpen = Object.assign({}, quickOpenWidget, quickOpenModel); global.monaco.filters = filters; global.monaco.theme = styler; diff --git a/packages/monaco/src/typings/monaco/index.d.ts b/packages/monaco/src/typings/monaco/index.d.ts index 6d558fc055410..e9a33b841c85a 100644 --- a/packages/monaco/src/typings/monaco/index.d.ts +++ b/packages/monaco/src/typings/monaco/index.d.ts @@ -438,6 +438,25 @@ declare module monaco.keybindings { declare module monaco.services { + export interface OpenInternalOptions { + readonly openToSide?: boolean; + } + + export interface OpenExternalOptions { + readonly openExternal?: boolean + } + + export interface IOpener { + open(resource: monaco.Uri | string, options?: OpenInternalOptions | OpenExternalOptions): Promise; + } + + // https://github.com/TypeFox/vscode/blob/70b8db24a37fafc77247de7f7cb5bb0195120ed0/src/vs/editor/browser/services/openerService.ts#L18 + export class OpenerService { + constructor(editorService: monaco.editor.ICodeEditorService, commandService: monaco.commands.ICommandService); + registerOpener(opener: IOpener): monaco.IDisposable; + open(resource: monaco.Uri, options?: { openToSide?: boolean }): Promise; + } + export const ICodeEditorService: any; export const IConfigurationService: any; diff --git a/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts b/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts index 499866465a556..ec22c6c4b1186 100644 --- a/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts +++ b/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts @@ -19,8 +19,8 @@ import '../../../src/main/browser/style/index.css'; import { ContainerModule } from 'inversify'; import { - FrontendApplicationContribution, FrontendApplication, WidgetFactory, bindViewContribution, - ViewContainerIdentifier, ViewContainer, createTreeContainer, TreeImpl, TreeWidget, TreeModelImpl, OpenHandler, LabelProviderContribution + FrontendApplicationContribution, WidgetFactory, bindViewContribution, + ViewContainerIdentifier, ViewContainer, createTreeContainer, TreeImpl, TreeWidget, TreeModelImpl, LabelProviderContribution } from '@theia/core/lib/browser'; import { MaybePromise, CommandContribution, ResourceResolver, bindContributionProvider } from '@theia/core/lib/common'; import { WebSocketConnectionProvider } from '@theia/core/lib/browser/messaging'; @@ -66,7 +66,6 @@ import { InPluginFileSystemWatcherManager } from './in-plugin-filesystem-watcher import { WebviewWidget, WebviewWidgetIdentifier, WebviewWidgetExternalEndpoint } from './webview/webview'; import { WebviewEnvironment } from './webview/webview-environment'; import { WebviewThemeDataProvider } from './webview/webview-theme-data-provider'; -import { PluginCommandOpenHandler } from './plugin-command-open-handler'; import { bindWebviewPreferences } from './webview/webview-preferences'; import { WebviewResourceLoader, WebviewResourceLoaderPath } from '../common/webview-protocol'; import { WebviewResourceCache } from './webview/webview-resource-cache'; @@ -106,7 +105,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(ResourceResolver).toService(UntitledResourceResolver); bind(FrontendApplicationContribution).toDynamicValue(ctx => ({ - onStart(app: FrontendApplication): MaybePromise { + onStart(): MaybePromise { ctx.container.get(HostedPluginSupport).onStart(ctx.container); } })); @@ -157,9 +156,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { } })).inSingletonScope(); - bind(PluginCommandOpenHandler).toSelf().inSingletonScope(); - bind(OpenHandler).toService(PluginCommandOpenHandler); - bindWebviewPreferences(bind); bind(WebviewEnvironment).toSelf().inSingletonScope(); bind(WebviewThemeDataProvider).toSelf().inSingletonScope(); diff --git a/packages/plugin-ext/src/main/browser/text-editor-main.ts b/packages/plugin-ext/src/main/browser/text-editor-main.ts index 07aa30e35f58d..8e94c8c0a2e0b 100644 --- a/packages/plugin-ext/src/main/browser/text-editor-main.ts +++ b/packages/plugin-ext/src/main/browser/text-editor-main.ts @@ -293,6 +293,7 @@ export class TextEditorMain implements Disposable { } } +// TODO move to monaco typings! interface SnippetController2 extends monaco.editor.IEditorContribution { insert(template: string, overwriteBefore: number, overwriteAfter: number,