From a14ac9e2ef6cf2a8a4da106d751ab0d00299f641 Mon Sep 17 00:00:00 2001 From: Anton Kosyakov Date: Mon, 16 Mar 2020 11:10:51 +0000 Subject: [PATCH] add API tests for typescript language Signed-off-by: Anton Kosyakov --- .../application-manager/src/expose-loader.ts | 2 +- examples/api-tests/src/saveable.spec.js | 22 +- examples/api-tests/src/typescript.spec.js | 369 ++++++++++++++++++ examples/browser/package.json | 2 +- packages/core/src/browser/keybinding.ts | 30 +- packages/monaco/src/typings/monaco/index.d.ts | 5 + .../plugin-ext/src/common/plugin-api-rpc.ts | 2 + .../src/hosted/browser/hosted-plugin.ts | 8 + .../src/main/browser/webview/webview.ts | 13 +- .../plugin-ext/src/plugin/plugin-manager.ts | 72 ++-- 10 files changed, 467 insertions(+), 58 deletions(-) create mode 100644 examples/api-tests/src/typescript.spec.js diff --git a/dev-packages/application-manager/src/expose-loader.ts b/dev-packages/application-manager/src/expose-loader.ts index 43ecf4dfe50ec..3a23ce118ef9d 100644 --- a/dev-packages/application-manager/src/expose-loader.ts +++ b/dev-packages/application-manager/src/expose-loader.ts @@ -53,7 +53,7 @@ export = function (this: webpack.loader.LoaderContext, source: string, sourceMap this.cacheable(); } - let modulePackage = modulePackages.find(({ dir }) => this.resourcePath.startsWith(dir)); + let modulePackage = modulePackages.find(({ dir }) => this.resourcePath.startsWith(dir + '/')); if (modulePackage) { this.callback(undefined, exposeModule(modulePackage, this.resourcePath, source), sourceMap); return; diff --git a/examples/api-tests/src/saveable.spec.js b/examples/api-tests/src/saveable.spec.js index d67e294935f6c..0cd7e3db39328 100644 --- a/examples/api-tests/src/saveable.spec.js +++ b/examples/api-tests/src/saveable.spec.js @@ -79,9 +79,9 @@ describe('Saveable', function () { assert.isTrue(Saveable.isDirty(widget), `should be dirty before '${edit}' save`); await Saveable.save(widget); assert.isFalse(Saveable.isDirty(widget), `should NOT be dirty after '${edit}' save`); - assert.equal(editor.getControl().getValue(), edit, `model should be updated with '${edit}'`); + assert.equal(editor.getControl().getValue().trimRight(), edit, `model should be updated with '${edit}'`); const state = await fileSystem.resolveContent(fileUri.toString()); - assert.equal(state.content, edit, `fs should be updated with '${edit}'`); + assert.equal(state.content.trimRight(), edit, `fs should be updated with '${edit}'`); } }); @@ -129,7 +129,7 @@ describe('Saveable', function () { assert.isTrue(outOfSync, 'file should be out of sync'); assert.equal(outOfSyncCount, 1, 'user should be prompted only once with out of sync dialog'); assert.isTrue(Saveable.isDirty(widget), 'should be dirty after rejected save'); - assert.equal(editor.getControl().getValue(), longContent.substring(3), 'model should be updated'); + assert.equal(editor.getControl().getValue().trimRight(), longContent.substring(3), 'model should be updated'); const state = await fileSystem.resolveContent(fileUri.toString()); assert.equal(state.content, 'baz', 'fs should NOT be updated'); }); @@ -151,7 +151,7 @@ describe('Saveable', function () { await Saveable.save(widget); assert.isTrue(outOfSync, 'file should be out of sync'); assert.isTrue(Saveable.isDirty(widget), 'should be dirty after rejected save'); - assert.equal(editor.getControl().getValue(), 'bar', 'model should be updated'); + assert.equal(editor.getControl().getValue().trimRight(), 'bar', 'model should be updated'); let state = await fileSystem.resolveContent(fileUri.toString()); assert.equal(state.content, 'baz', 'fs should NOT be updated'); @@ -164,9 +164,9 @@ describe('Saveable', function () { await Saveable.save(widget); assert.isTrue(outOfSync, 'file should be out of sync'); assert.isFalse(Saveable.isDirty(widget), 'should NOT be dirty after save'); - assert.equal(editor.getControl().getValue(), 'bar', 'model should be updated'); + assert.equal(editor.getControl().getValue().trimRight(), 'bar', 'model should be updated'); state = await fileSystem.resolveContent(fileUri.toString()); - assert.equal(state.content, 'bar', 'fs should be updated'); + assert.equal(state.content.trimRight(), 'bar', 'fs should be updated'); }); it('accept new save', async () => { @@ -181,9 +181,9 @@ describe('Saveable', function () { await Saveable.save(widget); assert.isTrue(outOfSync, 'file should be out of sync'); assert.isFalse(Saveable.isDirty(widget), 'should NOT be dirty after save'); - assert.equal(editor.getControl().getValue(), 'bar', 'model should be updated'); + assert.equal(editor.getControl().getValue().trimRight(), 'bar', 'model should be updated'); const state = await fileSystem.resolveContent(fileUri.toString()); - assert.equal(state.content, 'bar', 'fs should be updated'); + assert.equal(state.content.trimRight(), 'bar', 'fs should be updated'); }); it('cancel save on close', async () => { @@ -243,7 +243,7 @@ describe('Saveable', function () { assert.isTrue(outOfSync, 'file should be out of sync'); assert.isTrue(widget.isDisposed, 'model should be disposed after close'); const state = await fileSystem.resolveContent(fileUri.toString()); - assert.equal(state.content, 'bar', 'fs should be updated'); + assert.equal(state.content.trimRight(), 'bar', 'fs should be updated'); }); it('normal close', async () => { @@ -254,7 +254,7 @@ describe('Saveable', function () { }); assert.isTrue(widget.isDisposed, 'model should be disposed after close'); const state = await fileSystem.resolveContent(fileUri.toString()); - assert.equal(state.content, 'bar', 'fs should be updated'); + assert.equal(state.content.trimRight(), 'bar', 'fs should be updated'); }); it('delete file for saved', async () => { @@ -320,7 +320,7 @@ describe('Saveable', function () { assert.isFalse(Saveable.isDirty(widget), 'should NOT be dirty after save'); assert.isTrue(editor.document.valid, 'should be valid after save'); const state = await fileSystem.resolveContent(fileUri.toString()); - assert.equal(state.content, 'bar', 'fs should be updated'); + assert.equal(state.content.trimRight(), 'bar', 'fs should be updated'); }); it('move file for saved', async function () { diff --git a/examples/api-tests/src/typescript.spec.js b/examples/api-tests/src/typescript.spec.js new file mode 100644 index 0000000000000..89e280dd6dfd1 --- /dev/null +++ b/examples/api-tests/src/typescript.spec.js @@ -0,0 +1,369 @@ +/******************************************************************************** + * Copyright (C) 2020 TypeFox 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 WITH Classpath-exception-2.0 + ********************************************************************************/ + +// @ts-check +/// +describe('TypeScript', function () { + this.timeout(15000); + + const { assert } = chai; + + const Uri = require('@theia/core/lib/common/uri'); + const { Key } = require('@theia/core/lib/browser/keys'); + const { Deferred } = require('@theia/core/lib/common/promise-util'); + const { DisposableCollection } = require('@theia/core/lib/common/disposable'); + const { BrowserMainMenuFactory } = require('@theia/core/lib/browser/menu/browser-menu-plugin'); + const { EditorManager } = require('@theia/editor/lib/browser/editor-manager'); + const { EditorWidget } = require('@theia/editor/lib/browser/editor-widget'); + const { EDITOR_CONTEXT_MENU } = require('@theia/editor/lib/browser/editor-menu'); + const { WorkspaceService } = require('@theia/workspace/lib/browser/workspace-service'); + const { MonacoEditor } = require('@theia/monaco/lib/browser/monaco-editor'); + const { HostedPluginSupport } = require('@theia/plugin-ext/lib/hosted/browser/hosted-plugin'); + const { ContextKeyService } = require('@theia/core/lib/browser/context-key-service'); + const { CommandRegistry } = require('@theia/core/lib/common/command'); + const { KeybindingRegistry } = require('@theia/core/lib/browser/keybinding'); + const { OpenerService, open } = require('@theia/core/lib/browser/opener-service'); + const { EditorPreviewWidget } = require('@theia/editor-preview/lib/browser/editor-preview-widget'); + const { animationFrame } = require('@theia/core/lib/browser/browser'); + + /** @type {import('inversify').Container} */ + const container = window['theia'].container; + const editorManager = container.get(EditorManager); + const workspaceService = container.get(WorkspaceService); + const menuFactory = container.get(BrowserMainMenuFactory); + const pluginService = container.get(HostedPluginSupport); + const contextKeyService = container.get(ContextKeyService); + const commands = container.get(CommandRegistry); + const openerService = container.get(OpenerService); + const keybindings = container.get(KeybindingRegistry); + + const rootUri = new Uri.default(workspaceService.tryGetRoots()[0].uri); + const serverUri = rootUri.resolve('src-gen/backend/server.js'); + const inversifyUri = rootUri.resolve('../../node_modules/inversify/dts/inversify.d.ts').normalizePath(); + + before(async function () { + await pluginService.load(); + const plugin = pluginService.plugins.find(p => p.model.id === 'vscode.typescript-language-features'); + await pluginService.activatePlugin(plugin.model.id); + }); + + beforeEach(async function () { + await editorManager.closeAll({ save: false }); + }); + + /** + * @param {Uri.default} uri + * @param {boolean} preview + */ + async function openEditor(uri, preview = false) { + const widget = await open(openerService, uri, { mode: 'activate', preview }); + const editorWidget = widget instanceof EditorPreviewWidget ? widget.editorWidget : widget instanceof EditorWidget ? widget : undefined; + const editor = MonacoEditor.get(editorWidget); + // wait till tsserver is running, see: + // https://github.com/microsoft/vscode/blob/93cbbc5cae50e9f5f5046343c751b6d010468200/extensions/typescript-language-features/src/extension.ts#L98-L103 + await new Promise(resolve => { + if (contextKeyService.match('typescript.isManagedFile')) { + resolve(); + return; + } + contextKeyService.onDidChange(() => { + if (contextKeyService.match('typescript.isManagedFile')) { + resolve(); + } + }); + }); + return editor; + } + + afterEach(async () => { + await editorManager.closeAll({ save: false }); + }); + + const toTearDown = new DisposableCollection(); + afterEach(() => toTearDown.dispose()); + + it('document formating should be visible and enabled', async () => { + await openEditor(serverUri); + const menu = menuFactory.createContextMenu(EDITOR_CONTEXT_MENU); + const item = menu.items.find(i => i.command === 'editor.action.formatDocument'); + assert.isDefined(item); + assert.isTrue(item.isVisible); + assert.isTrue(item.isEnabled); + }); + + describe('editor.action.revealDefinition', function () { + for (const preview of [false, true]) { + const from = 'an editor' + (preview ? ' preview' : ''); + it('within ' + from, async function () { + const editor = await openEditor(serverUri, preview); + // con|tainer.load(backendApplicationModule); + editor.getControl().setPosition({ lineNumber: 12, column: 4 }); + assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'container'); + + await commands.executeCommand('editor.action.revealDefinition'); + + const activeEditor = MonacoEditor.get(editorManager.activeEditor); + assert.equal(editorManager.activeEditor.parent instanceof EditorPreviewWidget, preview); + assert.equal(activeEditor.uri.toString(), serverUri.toString()); + // const |container = new Container(); + const { lineNumber, column } = activeEditor.getControl().getPosition(); + assert.deepEqual({ lineNumber, column }, { lineNumber: 11, column: 7 }); + assert.equal(activeEditor.getControl().getModel().getWordAtPosition({ lineNumber, column }).word, 'container'); + }); + + it(`from ${from} to another editor`, async function () { + await editorManager.open(inversifyUri, { mode: 'open' }); + + const editor = await openEditor(serverUri, preview); + // const { Cont|ainer } = require('inversify'); + editor.getControl().setPosition({ lineNumber: 5, column: 13 }); + assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'Container'); + + await commands.executeCommand('editor.action.revealDefinition'); + + const activeEditor = MonacoEditor.getActive(editorManager); + assert.isFalse(editorManager.activeEditor.parent instanceof EditorPreviewWidget); + assert.equal(activeEditor.uri.toString(), inversifyUri.toString()); + // export { |Container } from "./container/container"; + const { lineNumber, column } = activeEditor.getControl().getPosition(); + assert.deepEqual({ lineNumber, column }, { lineNumber: 3, column: 10 }); + assert.equal(activeEditor.getControl().getModel().getWordAtPosition({ lineNumber, column }).word, 'Container'); + }); + + it(`from ${from} to an editor preview`, async function () { + const editor = await openEditor(serverUri); + // const { Cont|ainer } = require('inversify'); + editor.getControl().setPosition({ lineNumber: 5, column: 13 }); + assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'Container'); + + await commands.executeCommand('editor.action.revealDefinition'); + + const activeEditor = MonacoEditor.getActive(editorManager); + assert.isTrue(editorManager.activeEditor.parent instanceof EditorPreviewWidget); + assert.equal(activeEditor.uri.toString(), inversifyUri.toString()); + // export { |Container } from "./container/container"; + const { lineNumber, column } = activeEditor.getControl().getPosition(); + assert.deepEqual({ lineNumber, column }, { lineNumber: 3, column: 10 }); + assert.equal(activeEditor.getControl().getModel().getWordAtPosition({ lineNumber, column }).word, 'Container'); + }); + } + }); + + describe('editor.action.peekDefinition', function () { + + /** + * @param {MonacoEditor} editor + */ + async function openPeek(editor) { + assert.isTrue(contextKeyService.match('editorTextFocus')); + assert.isFalse(contextKeyService.match('referenceSearchVisible')); + assert.isFalse(contextKeyService.match('listFocus')); + + await commands.executeCommand('editor.action.peekDefinition'); + const referencesController = editor.getControl()._contributions['editor.contrib.referencesController']; + await new Promise(async (resolve, dispose) => { + toTearDown.push({ dispose }); + do { + await animationFrame(); + } while (!(referencesController._widget && referencesController._widget._tree.getFocus().length)); + resolve(); + }); + assert.isFalse(contextKeyService.match('editorTextFocus')); + assert.isTrue(contextKeyService.match('referenceSearchVisible')); + assert.isTrue(contextKeyService.match('listFocus')); + } + + async function openReference() { + keybindings.dispatchKeyDown('Enter'); + await new Promise(async (resolve, dispose) => { + toTearDown.push({ dispose }); + do { + await animationFrame(); + } while (!contextKeyService.match('listFocus')); + resolve(); + }); + assert.isFalse(contextKeyService.match('editorTextFocus')); + assert.isTrue(contextKeyService.match('referenceSearchVisible')); + assert.isTrue(contextKeyService.match('listFocus')); + } + + async function closePeek() { + keybindings.dispatchKeyDown('Escape'); + await new Promise(async (resolve, dispose) => { + toTearDown.push({ dispose }); + do { + await animationFrame(); + } while (contextKeyService.match('listFocus')); + resolve(); + }); + assert.isTrue(contextKeyService.match('editorTextFocus')); + assert.isFalse(contextKeyService.match('referenceSearchVisible')); + assert.isFalse(contextKeyService.match('listFocus')); + } + + for (const preview of [false, true]) { + const from = 'an editor' + (preview ? ' preview' : ''); + it('within ' + from, async function () { + const editor = await openEditor(serverUri, preview); + // con|tainer.load(backendApplicationModule); + editor.getControl().setPosition({ lineNumber: 12, column: 4 }); + assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'container'); + + await openPeek(editor); + await openReference(); + + const activeEditor = MonacoEditor.get(editorManager.activeEditor); + assert.equal(editorManager.activeEditor.parent instanceof EditorPreviewWidget, preview); + assert.equal(activeEditor.uri.toString(), serverUri.toString()); + // const |container = new Container(); + const { lineNumber, column } = activeEditor.getControl().getPosition(); + assert.deepEqual({ lineNumber, column }, { lineNumber: 11, column: 7 }); + assert.equal(activeEditor.getControl().getModel().getWordAtPosition({ lineNumber, column }).word, 'container'); + + await closePeek(); + }); + + it(`from ${from} to another editor`, async function () { + await editorManager.open(inversifyUri, { mode: 'open' }); + + const editor = await openEditor(serverUri, preview); + // const { Cont|ainer } = require('inversify'); + editor.getControl().setPosition({ lineNumber: 5, column: 13 }); + assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'Container'); + + await openPeek(editor); + await openReference(); + + const activeEditor = MonacoEditor.getActive(editorManager); + assert.isFalse(editorManager.activeEditor.parent instanceof EditorPreviewWidget); + assert.equal(activeEditor.uri.toString(), inversifyUri.toString()); + // export { |Container } from "./container/container"; + const { lineNumber, column } = activeEditor.getControl().getPosition(); + assert.deepEqual({ lineNumber, column }, { lineNumber: 3, column: 10 }); + assert.equal(activeEditor.getControl().getModel().getWordAtPosition({ lineNumber, column }).word, 'Container'); + + await closePeek(); + }); + + it(`from ${from} to an editor preview`, async function () { + const editor = await openEditor(serverUri); + // const { Cont|ainer } = require('inversify'); + editor.getControl().setPosition({ lineNumber: 5, column: 13 }); + assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'Container'); + + await openPeek(editor); + await openReference(); + + const activeEditor = MonacoEditor.getActive(editorManager); + assert.isTrue(editorManager.activeEditor.parent instanceof EditorPreviewWidget); + assert.equal(activeEditor.uri.toString(), inversifyUri.toString()); + // export { |Container } from "./container/container"; + const { lineNumber, column } = activeEditor.getControl().getPosition(); + assert.deepEqual({ lineNumber, column }, { lineNumber: 3, column: 10 }); + assert.equal(activeEditor.getControl().getModel().getWordAtPosition({ lineNumber, column }).word, 'Container'); + + await closePeek(); + }); + } + }); + + it('editor.action.triggerSuggest', async function () { + const editor = await openEditor(serverUri); + // const { [|Container] } = require('inversify'); + editor.getControl().setPosition({ lineNumber: 5, column: 9 }); + editor.getControl().setSelection({ startLineNumber: 5, startColumn: 9, endLineNumber: 5, endColumn: 18 }); + assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'Container'); + + assert.isTrue(contextKeyService.match('editorTextFocus')); + assert.isFalse(contextKeyService.match('suggestWidgetVisible')); + + await commands.executeCommand('editor.action.triggerSuggest'); + await new Promise(async (resolve, dispose) => { + toTearDown.push({ dispose }); + do { + await animationFrame(); + } while (!contextKeyService.match('suggestWidgetVisible')); + resolve(); + }); + assert.isTrue(contextKeyService.match('editorTextFocus')); + assert.isTrue(contextKeyService.match('suggestWidgetVisible')); + + keybindings.dispatchKeyDown('Enter'); + await new Promise(async (resolve, dispose) => { + toTearDown.push({ dispose }); + do { + await animationFrame(); + } while (contextKeyService.match('suggestWidgetVisible')); + resolve(); + }); + + assert.isTrue(contextKeyService.match('editorTextFocus')); + assert.isFalse(contextKeyService.match('suggestWidgetVisible')); + + const activeEditor = MonacoEditor.getActive(editorManager); + assert.equal(activeEditor.uri.toString(), serverUri.toString()); + // const { Container| } = require('inversify'); + const { lineNumber, column } = activeEditor.getControl().getPosition(); + assert.deepEqual({ lineNumber, column }, { lineNumber: 5, column: 18 }); + assert.equal(activeEditor.getControl().getModel().getWordAtPosition({ lineNumber, column }).word, 'Container'); + }); + + it('editor.action.rename', async function () { + this.timeout(0); + + const editor = await openEditor(serverUri); + // const |container = new Container(); + editor.getControl().setPosition({ lineNumber: 11, column: 7 }); + assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'container'); + + assert.isTrue(contextKeyService.match('editorTextFocus')); + assert.isFalse(contextKeyService.match('renameInputVisible')); + + const renaming = commands.executeCommand('editor.action.rename'); + await new Promise(async (resolve, dispose) => { + toTearDown.push({ dispose }); + do { + await animationFrame(); + } while (!(contextKeyService.match('renameInputVisible') + && document.activeElement instanceof HTMLInputElement + && document.activeElement.selectionEnd === 'container'.length)); + resolve(); + }); + assert.isFalse(contextKeyService.match('editorTextFocus')); + assert.isTrue(contextKeyService.match('renameInputVisible')); + + const input = document.activeElement; + if (!(input instanceof HTMLInputElement)) { + assert.fail('expecte focused input, but: ' + input); + return; + } + + input.value = 'foo'; + keybindings.dispatchKeyDown('Enter', input); + + await renaming; + assert.isTrue(contextKeyService.match('editorTextFocus')); + assert.isFalse(contextKeyService.match('renameInputVisible')); + + const activeEditor = MonacoEditor.getActive(editorManager); + assert.equal(activeEditor.uri.toString(), serverUri.toString()); + // const |foo = new Container(); + const { lineNumber, column } = activeEditor.getControl().getPosition(); + assert.deepEqual({ lineNumber, column }, { lineNumber: 11, column: 7 }); + assert.equal(activeEditor.getControl().getModel().getWordAtPosition({ lineNumber, column }).word, 'foo'); + }); + +}); diff --git a/examples/browser/package.json b/examples/browser/package.json index bb8becec87883..e72abbc7d556e 100644 --- a/examples/browser/package.json +++ b/examples/browser/package.json @@ -61,7 +61,7 @@ "watch": "yarn build --watch", "start": "theia start --plugins=local-dir:../../plugins", "start:debug": "yarn start --log-level=debug", - "test": "theia test . --test-spec=../api-tests/**/*.spec.js", + "test": "theia test . --plugins=local-dir:../../plugins --test-spec=../api-tests/**/*.spec.js", "test:debug": "yarn test --test-inspect", "coverage": "yarn test --test-coverage && yarn coverage:report", "coverage:report": "nyc report --reporter=html", diff --git a/packages/core/src/browser/keybinding.ts b/packages/core/src/browser/keybinding.ts index 463cd1e0263da..429a1f50ac34c 100644 --- a/packages/core/src/browser/keybinding.ts +++ b/packages/core/src/browser/keybinding.ts @@ -546,11 +546,8 @@ export class KeybindingRegistry { } else { const command = this.commandRegistry.getCommand(binding.command); if (command) { - const commandHandler = this.commandRegistry.getActiveHandler(command.id, binding.args); - - if (commandHandler) { - commandHandler.execute(binding.args); - } + this.commandRegistry.executeCommand(binding.command, binding.args) + .catch(e => console.error('Failed to execute command:', e)); /* Note that if a keybinding is in context but the command is not active we still stop the processing here. */ @@ -574,6 +571,29 @@ export class KeybindingRegistry { return true; } + dispatchKeyDown(input: KeyboardEventInit | KeyCode | string, target: EventTarget = document.activeElement || window): void { + const eventInit = this.asKeyboardEventInit(input); + const emulatedKeyboardEvent = new KeyboardEvent('keydown', eventInit); + target.dispatchEvent(emulatedKeyboardEvent); + } + protected asKeyboardEventInit(input: KeyboardEventInit | KeyCode | string): KeyboardEventInit & Partial<{ keyCode: number }> { + if (typeof input === 'string') { + return this.asKeyboardEventInit(KeyCode.createKeyCode(input)); + } + if (input instanceof KeyCode) { + return { + metaKey: input.meta, + shiftKey: input.shift, + altKey: input.alt, + ctrlKey: input.ctrl, + code: input.key && input.key.code, + key: (input && input.character) || (input.key && input.key.code), + keyCode: input.key && input.key.keyCode + }; + } + return input; + } + /** * Run the command matching to the given keyboard event. */ diff --git a/packages/monaco/src/typings/monaco/index.d.ts b/packages/monaco/src/typings/monaco/index.d.ts index 3e0f982e85210..ddc2f56b7bdd0 100644 --- a/packages/monaco/src/typings/monaco/index.d.ts +++ b/packages/monaco/src/typings/monaco/index.d.ts @@ -728,7 +728,12 @@ declare module monaco.referenceSearch { show(range: IRange): void; hide(): void; focus(): void; + _tree: ReferenceTree } + export interface ReferenceTree { + getFocus(): ReferenceTreeElement[] + } + export interface ReferenceTreeElement { } // https://github.com/theia-ide/vscode/blob/standalone/0.19.x/src/vs/editor/contrib/gotoSymbol/peek/referencesController.ts#L30 export interface ReferencesController extends IDisposable { diff --git a/packages/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts index 676cfb4278044..2b48fc65bbd91 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts @@ -190,6 +190,8 @@ export interface PluginManagerExt { $updateStoragePath(path: string | undefined): Promise; $activateByEvent(event: string): Promise; + + $activatePlugin(id: string): Promise; } export interface CommandRegistryMain { diff --git a/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts b/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts index 645340e7fecbf..4d5f15621854a 100644 --- a/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts +++ b/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts @@ -603,6 +603,14 @@ export class HostedPluginSupport { } } + async activatePlugin(id: string): Promise { + const activation = []; + for (const manager of this.managers.values()) { + activation.push(manager.$activatePlugin(id)); + } + await Promise.all(activation); + } + protected createMeasurement(name: string): () => number { const startMarker = `${name}-start`; const endMarker = `${name}-end`; diff --git a/packages/plugin-ext/src/main/browser/webview/webview.ts b/packages/plugin-ext/src/main/browser/webview/webview.ts index 93162847c1e49..aa1f3003c8f83 100644 --- a/packages/plugin-ext/src/main/browser/webview/webview.ts +++ b/packages/plugin-ext/src/main/browser/webview/webview.ts @@ -280,7 +280,7 @@ export class WebviewWidget extends BaseWidget implements StatefulWidget { // Electron: workaround for https://github.com/electron/electron/issues/14258 // We have to detect keyboard events in the and dispatch them to our // keybinding service because these events do not bubble to the parent window anymore. - this.dispatchKeyDown(data); + this.keybindings.dispatchKeyDown(data, this.element); })); this.style(); @@ -395,17 +395,6 @@ export class WebviewWidget extends BaseWidget implements StatefulWidget { this.doSend('styles', { styles, activeTheme }); } - protected dispatchKeyDown(event: KeyboardEventInit): void { - // Create a fake KeyboardEvent from the data provided - const emulatedKeyboardEvent = new KeyboardEvent('keydown', event); - // Force override the target - Object.defineProperty(emulatedKeyboardEvent, 'target', { - get: () => this.element, - }); - // And re-dispatch - this.keybindings.run(emulatedKeyboardEvent); - } - protected openLink(link: URI): void { const supported = this.toSupportedLink(link); if (supported) { diff --git a/packages/plugin-ext/src/plugin/plugin-manager.ts b/packages/plugin-ext/src/plugin/plugin-manager.ts index 2916b0d9d6dcc..d9834626802dd 100644 --- a/packages/plugin-ext/src/plugin/plugin-manager.ts +++ b/packages/plugin-ext/src/plugin/plugin-manager.ts @@ -84,6 +84,7 @@ export class PluginManagerExtImpl implements PluginManagerExt, PluginManager { 'onWebviewPanel' ]); + private configStorage: ConfigStorage | undefined; private readonly registry = new Map(); private readonly activations = new Map Promise)[] | undefined>(); /** promises to whether loading each plugin has been successful */ @@ -196,14 +197,16 @@ export class PluginManagerExtImpl implements PluginManagerExt, PluginManager { } async $start(params: PluginManagerStartParams): Promise { + this.configStorage = params.configStorage; + const [plugins, foreignPlugins] = await this.host.init(params.plugins); // add foreign plugins for (const plugin of foreignPlugins) { - this.registerPlugin(plugin, params.configStorage); + this.registerPlugin(plugin); } // add own plugins, before initialization for (const plugin of plugins) { - this.registerPlugin(plugin, params.configStorage); + this.registerPlugin(plugin); } // run eager plugins @@ -219,15 +222,10 @@ export class PluginManagerExtImpl implements PluginManagerExt, PluginManager { this.fireOnDidChange(); } - protected registerPlugin(plugin: Plugin, configStorage: ConfigStorage): void { + protected registerPlugin(plugin: Plugin): void { this.registry.set(plugin.model.id, plugin); if (plugin.pluginPath && Array.isArray(plugin.rawModel.activationEvents)) { - const activation = async () => { - const title = `Activating ${plugin.model.displayName || plugin.model.name}`; - const id = await this.notificationMain.$startProgress({ title, location: 'window' }); - await this.loadPlugin(plugin, configStorage); - this.notificationMain.$stopProgress(id); - }; + const activation = () => this.$activatePlugin(plugin.model.id); // an internal activation event is a subject to change this.setActivation(`onPlugin:${plugin.model.id}`, activation); const unsupportedActivationEvents = plugin.rawModel.activationEvents.filter(e => !PluginManagerExtImpl.SUPPORTED_ACTIVATION_EVENTS.has(e.split(':')[0])); @@ -261,31 +259,39 @@ export class PluginManagerExtImpl implements PluginManagerExt, PluginManager { let loading = this.loadedPlugins.get(plugin.model.id); if (!loading) { loading = (async () => { - if (plugin.rawModel.extensionDependencies) { - for (const dependencyId of plugin.rawModel.extensionDependencies) { - const dependency = this.registry.get(dependencyId.toLowerCase()); - const id = plugin.model.displayName || plugin.model.id; - if (dependency) { - const depId = dependency.model.displayName || dependency.model.id; - const loadedSuccessfully = await this.loadPlugin(dependency, configStorage, visited); - if (!loadedSuccessfully) { - const message = `Cannot activate extension '${id}' because it depends on extension '${depId}', which failed to activate.`; + const progressId = await this.notificationMain.$startProgress({ + title: `Activating ${plugin.model.displayName || plugin.model.name}`, + location: 'window' + }); + try { + if (plugin.rawModel.extensionDependencies) { + for (const dependencyId of plugin.rawModel.extensionDependencies) { + const dependency = this.registry.get(dependencyId.toLowerCase()); + const id = plugin.model.displayName || plugin.model.id; + if (dependency) { + const depId = dependency.model.displayName || dependency.model.id; + const loadedSuccessfully = await this.loadPlugin(dependency, configStorage, visited); + if (!loadedSuccessfully) { + const message = `Cannot activate extension '${id}' because it depends on extension '${depId}', which failed to activate.`; + this.messageRegistryProxy.$showMessage(MainMessageType.Error, message, {}, []); + return false; + } + } else { + const message = `Cannot activate the '${id}' extension because it depends on the '${dependencyId}' extension, which is not installed.`; this.messageRegistryProxy.$showMessage(MainMessageType.Error, message, {}, []); + console.warn(message); return false; } - } else { - const message = `Cannot activate the '${id}' extension because it depends on the '${dependencyId}' extension, which is not installed.`; - this.messageRegistryProxy.$showMessage(MainMessageType.Error, message, {}, []); - console.warn(message); - return false; } } - } - let pluginMain = this.host.loadPlugin(plugin); - // see https://github.com/TypeFox/vscode/blob/70b8db24a37fafc77247de7f7cb5bb0195120ed0/src/vs/workbench/api/common/extHostExtensionService.ts#L372-L376 - pluginMain = pluginMain || {}; - return this.startPlugin(plugin, configStorage, pluginMain); + let pluginMain = this.host.loadPlugin(plugin); + // see https://github.com/TypeFox/vscode/blob/70b8db24a37fafc77247de7f7cb5bb0195120ed0/src/vs/workbench/api/common/extHostExtensionService.ts#L372-L376 + pluginMain = pluginMain || {}; + return await this.startPlugin(plugin, configStorage, pluginMain); + } finally { + this.notificationMain.$stopProgress(progressId); + } })(); } this.loadedPlugins.set(plugin.model.id, loading); @@ -293,6 +299,9 @@ export class PluginManagerExtImpl implements PluginManagerExt, PluginManager { } async $updateStoragePath(path: string | undefined): Promise { + if (this.configStorage) { + this.configStorage.hostStoragePath = path; + } this.pluginContextsMap.forEach((pluginContext: theia.PluginContext, pluginId: string) => { pluginContext.storagePath = path ? join(path, pluginId) : undefined; }); @@ -309,6 +318,13 @@ export class PluginManagerExtImpl implements PluginManagerExt, PluginManager { } } + async $activatePlugin(id: string): Promise { + const plugin = this.registry.get(id); + if (plugin && this.configStorage) { + await this.loadPlugin(plugin, this.configStorage); + } + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any private async startPlugin(plugin: Plugin, configStorage: ConfigStorage, pluginMain: any): Promise { const subscriptions: theia.Disposable[] = [];