From 3e58869c0b47347b4c61e11ac0cc2739d183fe66 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 --- CHANGELOG.md | 7 + .../application-manager/src/expose-loader.ts | 2 +- examples/api-tests/src/api-tests.d.ts | 21 + .../api-tests/src/launch-preferences.spec.js | 11 +- examples/api-tests/src/menus.spec.js | 5 +- examples/api-tests/src/monaco-api.spec.js | 50 +- examples/api-tests/src/saveable.spec.js | 29 +- examples/api-tests/src/shell.spec.js | 3 +- examples/api-tests/src/typescript.spec.js | 776 ++++++++++++++++++ examples/api-tests/src/views.spec.js | 1 + examples/browser/.eslintrc.js | 10 - examples/browser/package.json | 5 +- packages/core/src/browser/keybinding.ts | 23 + .../src/browser/progress-status-bar-item.ts | 18 +- packages/core/src/browser/saveable.ts | 3 + .../src/browser/shell/application-shell.ts | 16 +- .../src/browser/shell/side-panel-handler.ts | 1 + packages/core/src/browser/shell/tab-bars.ts | 19 +- .../core/src/browser/widget-open-handler.ts | 12 +- packages/core/src/common/reference.spec.ts | 21 + packages/core/src/common/reference.ts | 6 +- .../src/browser/language-client-services.ts | 2 +- packages/monaco/src/typings/monaco/index.d.ts | 43 + .../src/common/plugin-api-rpc-model.ts | 6 - .../plugin-ext/src/common/plugin-api-rpc.ts | 6 +- .../src/hosted/browser/hosted-plugin.ts | 8 + .../src/main/browser/debug/debug-main.ts | 4 +- .../src/main/browser/languages-main.ts | 5 +- .../src/main/browser/webview/webview.ts | 13 +- packages/plugin-ext/src/plugin/languages.ts | 79 +- .../plugin-ext/src/plugin/plugin-manager.ts | 72 +- tsconfig.json | 6 +- 32 files changed, 1101 insertions(+), 182 deletions(-) create mode 100644 examples/api-tests/src/api-tests.d.ts create mode 100644 examples/api-tests/src/typescript.spec.js delete mode 100644 examples/browser/.eslintrc.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 401b4d65cfcb7..bc446b0b9b947 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Change Log +## v1.1.0 + +Breaking changes: + +- [plugin] removed `configStorage` argument from `PluginManager.registerPlugin`. +Use `PluginManager.configStorage` property instead. [#7265](https://github.com/eclipse-theia/theia/pull/7265#discussion_r399956070) + ## v1.0.0 - [core] added functionality to ensure that nodes are refreshed properly on tree expansion [#7400](https://github.com/eclipse-theia/theia/pull/7400) 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/api-tests.d.ts b/examples/api-tests/src/api-tests.d.ts new file mode 100644 index 0000000000000..4e8bfa6aad80b --- /dev/null +++ b/examples/api-tests/src/api-tests.d.ts @@ -0,0 +1,21 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +interface Window { + theia: { + container: import('inversify').Container + } +} diff --git a/examples/api-tests/src/launch-preferences.spec.js b/examples/api-tests/src/launch-preferences.spec.js index 265cfaed9ff00..c47cfc2309ae9 100644 --- a/examples/api-tests/src/launch-preferences.spec.js +++ b/examples/api-tests/src/launch-preferences.spec.js @@ -15,8 +15,6 @@ ********************************************************************************/ // @ts-check -/// - /* eslint-disable no-unused-expressions, @typescript-eslint/no-explicit-any */ /** @@ -28,6 +26,7 @@ * See https://github.com/akosyakov/vscode-launch/blob/master/src/test/extension.test.ts */ describe('Launch Preferences', function () { + this.timeout(5000); const { assert } = chai; @@ -38,8 +37,7 @@ describe('Launch Preferences', function () { const { MonacoTextModelService } = require('@theia/monaco/lib/browser/monaco-text-model-service'); const { MonacoWorkspace } = require('@theia/monaco/lib/browser/monaco-workspace'); - /** @type {import('inversify').Container} */ - const container = window['theia'].container; + const container = window.theia.container; /** @type {import('@theia/core/lib/browser/preferences/preference-service').PreferenceService} */ const preferences = container.get(PreferenceService); const workspaceService = container.get(WorkspaceService); @@ -425,7 +423,7 @@ describe('Launch Preferences', function () { ]); } - const client = fileSystem.getClient(); + const client = /** @type {import('@theia/filesystem/lib/common/filesystem').FileSystemClient} */ (fileSystem.getClient()); const originalShouldOverwrite = client.shouldOverwrite; before(async () => { @@ -460,6 +458,7 @@ describe('Launch Preferences', function () { /** @typedef {monaco.editor.IReference} ConfigModelReference */ /** @type {ConfigModelReference[]} */ beforeEach(async () => { + /** @type {Promise[]} */ const promises = []; /** * @param {string} name @@ -507,11 +506,13 @@ describe('Launch Preferences', function () { }); testIt('get from undefined', () => { + /** @type {any} */ const config = preferences.get('launch', undefined, undefined); assert.deepStrictEqual(JSON.parse(JSON.stringify(config)), expectation); }); testIt('get from rootUri', () => { + /** @type {any} */ const config = preferences.get('launch', undefined, rootUri.toString()); assert.deepStrictEqual(JSON.parse(JSON.stringify(config)), expectation); }); diff --git a/examples/api-tests/src/menus.spec.js b/examples/api-tests/src/menus.spec.js index bbaa8d6547385..5c8ef3da4e45f 100644 --- a/examples/api-tests/src/menus.spec.js +++ b/examples/api-tests/src/menus.spec.js @@ -29,11 +29,10 @@ describe('Menus', function () { const { ProblemContribution } = require('@theia/markers/lib/browser/problem/problem-contribution'); const { SearchInWorkspaceFrontendContribution } = require('@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution'); - /** @type {import('inversify').Container} */ - const container = window['theia'].container; + const container = window.theia.container; const shell = container.get(ApplicationShell); const menuBarContribution = container.get(BrowserMenuBarContribution); - const menuBar = menuBarContribution.menuBar; + const menuBar = /** @type {import('@theia/core/lib/browser/menu/browser-menu-plugin').MenuBarWidget} */ (menuBarContribution.menuBar); for (const contribution of [ container.get(CallHierarchyContribution), diff --git a/examples/api-tests/src/monaco-api.spec.js b/examples/api-tests/src/monaco-api.spec.js index 958ec6ed960b7..6f9c71a2fc557 100644 --- a/examples/api-tests/src/monaco-api.spec.js +++ b/examples/api-tests/src/monaco-api.spec.js @@ -15,8 +15,8 @@ ********************************************************************************/ // @ts-check -/// describe('Monaco API', async function () { + this.timeout(5000); const { assert } = chai; @@ -27,8 +27,7 @@ describe('Monaco API', async function () { const { MonacoResolvedKeybinding } = require('@theia/monaco/lib/browser/monaco-resolved-keybinding'); const { MonacoTextmateService } = require('@theia/monaco/lib/browser/textmate/monaco-textmate-service'); - /** @type {import('inversify').Container} */ - const container = window['theia'].container; + const container = window.theia.container; const editorManager = container.get(EditorManager); const workspaceService = container.get(WorkspaceService); const textmateService = container.get(MonacoTextmateService); @@ -40,7 +39,7 @@ describe('Monaco API', async function () { const editor = await editorManager.open(new Uri.default(root.uri).resolve('package.json'), { mode: 'reveal' }); - monacoEditor = MonacoEditor.get(editor); + monacoEditor = /** @type {MonacoEditor} */ (MonacoEditor.get(editor)); }); it('KeybindingService.resolveKeybinding', () => { @@ -65,24 +64,25 @@ describe('Monaco API', async function () { assert.deepStrictEqual({ label, ariaLabel, electronAccelerator, userSettingsLabel, WYSIWYG, chord, parts, dispatchParts }, { - label: "Ctrl+Shift+Alt+K", - ariaLabel: "Ctrl+Shift+Alt+K", - electronAccelerator: null, - userSettingsLabel: "ctrl+shift+alt+K", - WYSIWYG: true, - chord: false, - parts: [{ - altKey: true, - ctrlKey: true, - keyAriaLabel: "K", - keyLabel: "K", - metaKey: false, - shiftKey: true - }], - dispatchParts: [ - "ctrl+shift+alt+K" - ] - }); + label: 'Ctrl+Shift+Alt+K', + ariaLabel: 'Ctrl+Shift+Alt+K', + // eslint-disable-next-line no-null/no-null + electronAccelerator: null, + userSettingsLabel: 'ctrl+shift+alt+K', + WYSIWYG: true, + chord: false, + parts: [{ + altKey: true, + ctrlKey: true, + keyAriaLabel: 'K', + keyLabel: 'K', + metaKey: false, + shiftKey: true + }], + dispatchParts: [ + 'ctrl+shift+alt+K' + ] + }); } else { assert.fail(`resolvedKeybinding must be of ${MonacoResolvedKeybinding.name} type`); } @@ -94,16 +94,16 @@ describe('Monaco API', async function () { const toDispose = monaco.modes.TokenizationRegistry.onDidChange(() => { toDispose.dispose(); resolve(); - }) + }); }); textmateService['themeService'].setCurrentTheme('light'); await didChangeColorMap; } const textMateColorMap = textmateService['grammarRegistry'].getColorMap(); - assert.notEqual(textMateColorMap.indexOf('#795E26'), -1, 'Expected custom toke colors for the ligth theme to be enabled.') + assert.notEqual(textMateColorMap.indexOf('#795E26'), -1, 'Expected custom toke colors for the ligth theme to be enabled.'); - const monacoColorMap = monaco.modes.TokenizationRegistry.getColorMap(). + const monacoColorMap = (monaco.modes.TokenizationRegistry.getColorMap() || []). splice(0, textMateColorMap.length).map(c => c.toString().toUpperCase()); assert.deepStrictEqual(monacoColorMap, textMateColorMap, 'Expected textmate colors to have the same index in the monaco color map.'); }); diff --git a/examples/api-tests/src/saveable.spec.js b/examples/api-tests/src/saveable.spec.js index d67e294935f6c..bdfc2ab704c7a 100644 --- a/examples/api-tests/src/saveable.spec.js +++ b/examples/api-tests/src/saveable.spec.js @@ -16,22 +16,21 @@ // @ts-check describe('Saveable', function () { + this.timeout(5000); const { assert } = chai; const { EditorManager } = require('@theia/editor/lib/browser/editor-manager'); const { EditorWidget } = require('@theia/editor/lib/browser/editor-widget'); - const { PreferenceService, PreferenceScope } = require('@theia/core/lib/browser/preferences/preference-service'); + const { PreferenceService } = require('@theia/core/lib/browser/preferences/preference-service'); const Uri = require('@theia/core/lib/common/uri'); const { Saveable, SaveableWidget } = require('@theia/core/lib/browser/saveable'); const { WorkspaceService } = require('@theia/workspace/lib/browser/workspace-service'); const { FileSystem } = require('@theia/filesystem/lib/common/filesystem'); const { MonacoEditor } = require('@theia/monaco/lib/browser/monaco-editor'); - const { DisposableCollection } = require('@theia/core/lib/common/disposable'); const { Deferred } = require('@theia/core/lib/common/promise-util'); - /** @type {import('inversify').Container} */ - const container = window['theia'].container; + const container = window.theia.container; const editorManager = container.get(EditorManager); const workspaceService = container.get(WorkspaceService); /** @type {import('@theia/filesystem/lib/common/filesystem').FileSystem} */ @@ -79,9 +78,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 +128,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 +150,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 +163,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 +180,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 +242,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 +253,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 +319,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/shell.spec.js b/examples/api-tests/src/shell.spec.js index d1bc9358eac14..499b2db496c1c 100644 --- a/examples/api-tests/src/shell.spec.js +++ b/examples/api-tests/src/shell.spec.js @@ -22,8 +22,7 @@ describe('Shell', function () { const { ApplicationShell } = require('@theia/core/lib/browser/shell/application-shell'); const { StatusBarImpl } = require('@theia/core/lib/browser/status-bar'); - /** @type {import('inversify').Container} */ - const container = window['theia'].container; + const container = window.theia.container; const shell = container.get(ApplicationShell); const statusBar = container.get(StatusBarImpl); diff --git a/examples/api-tests/src/typescript.spec.js b/examples/api-tests/src/typescript.spec.js new file mode 100644 index 0000000000000..2afe1ad68d970 --- /dev/null +++ b/examples/api-tests/src/typescript.spec.js @@ -0,0 +1,776 @@ +/******************************************************************************** + * 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(30000); + + const { assert } = chai; + + const Uri = require('@theia/core/lib/common/uri'); + 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'); + const { PreferenceService, PreferenceScope } = require('@theia/core/lib/browser/preferences/preference-service'); + const { ProgressStatusBarItem } = require('@theia/core/lib/browser/progress-status-bar-item'); + const { FileSystem } = require('@theia/filesystem/lib/common/filesystem'); + + 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); + /** @type {import('@theia/core/lib/browser/preferences/preference-service').PreferenceService} */ + const preferences = container.get(PreferenceService); + const progressStatusBarItem = container.get(ProgressStatusBarItem); + /** @type {import('@theia/filesystem/lib/common/filesystem').FileSystem} */ + const fileSystem = container.get(FileSystem); + + const pluginId = 'vscode.typescript-language-features'; + const rootUri = new Uri.default(workspaceService.tryGetRoots()[0].uri); + const serverUri = rootUri.resolve('src-gen/backend/test-server.js'); + const inversifyUri = rootUri.resolve('../../node_modules/inversify/dts/inversify.d.ts').normalizePath(); + const containerUri = rootUri.resolve('../../node_modules/inversify/dts/container/container.d.ts').normalizePath(); + + before(async function () { + await fileSystem.createFile(serverUri.toString(), { + content: `// @ts-check +require('reflect-metadata'); +const path = require('path'); +const express = require('express'); +const { Container } = require('inversify'); +const { BackendApplication, CliManager } = require('@theia/core/lib/node'); +const { backendApplicationModule } = require('@theia/core/lib/node/backend-application-module'); +const { messagingBackendModule } = require('@theia/core/lib/node/messaging/messaging-backend-module'); +const { loggerBackendModule } = require('@theia/core/lib/node/logger-backend-module'); + +const container = new Container(); +container.load(backendApplicationModule); +container.load(messagingBackendModule); +container.load(loggerBackendModule); + +function load(raw) { + return Promise.resolve(raw.default).then(module => + container.load(module) + ) +} + +function start(port, host, argv) { + if (argv === undefined) { + argv = process.argv; + } + + const cliManager = container.get(CliManager); + return cliManager.initializeCli(argv).then(function () { + const application = container.get(BackendApplication); + application.use(express.static(path.join(__dirname, '../../lib'))); + application.use(express.static(path.join(__dirname, '../../lib/index.html'))); + return application.start(port, host); + }); +} + +module.exports = (port, host, argv) => Promise.resolve() + .then(function () { return Promise.resolve(require('@theia/process/lib/node/process-backend-module')).then(load) }) + .then(function () { return Promise.resolve(require('@theia/filesystem/lib/node/filesystem-backend-module')).then(load) }) + .then(function () { return Promise.resolve(require('@theia/filesystem/lib/node/download/file-download-backend-module')).then(load) }) + .then(function () { return Promise.resolve(require('@theia/workspace/lib/node/workspace-backend-module')).then(load) }) + .then(function () { return Promise.resolve(require('@theia/languages/lib/node/languages-backend-module')).then(load) }) + .then(function () { return Promise.resolve(require('@theia/terminal/lib/node/terminal-backend-module')).then(load) }) + .then(function () { return Promise.resolve(require('@theia/task/lib/node/task-backend-module')).then(load) }) + .then(function () { return Promise.resolve(require('@theia/debug/lib/node/debug-backend-module')).then(load) }) + .then(function () { return Promise.resolve(require('@theia/file-search/lib/node/file-search-backend-module')).then(load) }) + .then(function () { return Promise.resolve(require('@theia/git/lib/node/git-backend-module')).then(load) }) + .then(function () { return Promise.resolve(require('@theia/git/lib/node/env/git-env-module')).then(load) }) + .then(function () { return Promise.resolve(require('@theia/json/lib/node/json-backend-module')).then(load) }) + .then(function () { return Promise.resolve(require('@theia/metrics/lib/node/metrics-backend-module')).then(load) }) + .then(function () { return Promise.resolve(require('@theia/mini-browser/lib/node/mini-browser-backend-module')).then(load) }) + .then(function () { return Promise.resolve(require('@theia/search-in-workspace/lib/node/search-in-workspace-backend-module')).then(load) }) + .then(function () { return Promise.resolve(require('@theia/plugin-ext/lib/plugin-ext-backend-module')).then(load) }) + .then(function () { return Promise.resolve(require('@theia/plugin-dev/lib/node/plugin-dev-backend-module')).then(load) }) + .then(function () { return Promise.resolve(require('@theia/plugin-ext-vscode/lib/node/plugin-vscode-backend-module')).then(load) }) + .then(function () { return Promise.resolve(require('@theia/plugin-metrics/lib/node/plugin-metrics-backend-module')).then(load) }) + .then(function () { return Promise.resolve(require('@theia/vsx-registry/lib/node/vsx-registry-backend-module')).then(load) }) + .then(() => start(port, host, argv)).catch(reason => { + console.error('Failed to start the backend application.'); + if (reason) { + console.error(reason); + } + throw reason; + }); + ` + }); + await pluginService.didStart; + if (!pluginService.getPlugin(pluginId)) { + throw new Error(pluginId + ' should be started'); + } + await pluginService.activatePlugin(pluginId); + }); + + after(async function () { + await fileSystem.delete(serverUri.toString()); + }); + + beforeEach(async function () { + await editorManager.closeAll({ save: false }); + }); + + const toTearDown = new DisposableCollection(); + afterEach(async () => { + toTearDown.dispose(); + 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); + assert.isDefined(editor); + + // wait till tsserver is running, see: + // https://github.com/microsoft/vscode/blob/93cbbc5cae50e9f5f5046343c751b6d010468200/extensions/typescript-language-features/src/extension.ts#L98-L103 + await waitForAnimation(() => contextKeyService.match('typescript.isManagedFile')); + + // wait till projects are loaded, see: + // https://github.com/microsoft/vscode/blob/4aac84268c6226d23828cc6a1fe45ee3982927f0/extensions/typescript-language-features/src/typescriptServiceClient.ts#L911 + await waitForAnimation(() => !progressStatusBarItem.currentProgress); + + return /** @type {MonacoEditor} */ (editor); + } + + /** + * @template T + * @param {() => Promise | T} condition + * @returns {Promise} + */ + function waitForAnimation(condition) { + return new Promise(async (resolve, dispose) => { + toTearDown.push({ dispose }); + do { + await animationFrame(); + } while (!condition()); + resolve(); + }); + } + + /** + * We ignore attributes on purprse since they are not stable. + * But structure is important for us to see whether the plain text is rendered or markdown. + * + * @param {Element} element + * @returns {string} + */ + function nodeAsString(element, indentation = '') { + const header = element.tagName; + let body = ''; + const childIndentation = indentation + ' '; + for (let i = 0; i < element.childNodes.length; i++) { + const childNode = element.childNodes.item(i); + if (childNode.nodeType === childNode.TEXT_NODE) { + body += childIndentation + `"${childNode.textContent}"` + '\n'; + } else if (childNode instanceof HTMLElement) { + body += childIndentation + nodeAsString(childNode, childIndentation) + '\n'; + } + } + const result = header + (body ? ' {\n' + body + indentation + '}' : ''); + if (indentation) { + return result; + } + return `\n${result}\n`; + } + + /** + * @param {MonacoEditor} editor + */ + async function assertPeekOpened(editor) { + const referencesController = editor.getControl()._contributions['editor.contrib.referencesController']; + await waitForAnimation(() => referencesController._widget && referencesController._widget._tree.getFocus().length); + + assert.isFalse(contextKeyService.match('editorTextFocus')); + assert.isTrue(contextKeyService.match('referenceSearchVisible')); + assert.isTrue(contextKeyService.match('listFocus')); + } + + /** + * @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'); + await assertPeekOpened(editor); + } + + async function openReference() { + keybindings.dispatchKeyDown('Enter'); + await waitForAnimation(() => contextKeyService.match('listFocus')); + assert.isFalse(contextKeyService.match('editorTextFocus')); + assert.isTrue(contextKeyService.match('referenceSearchVisible')); + assert.isTrue(contextKeyService.match('listFocus')); + } + + async function closePeek() { + keybindings.dispatchKeyDown('Escape'); + await waitForAnimation(() => !contextKeyService.match('listFocus')); + assert.isTrue(contextKeyService.match('editorTextFocus')); + assert.isFalse(contextKeyService.match('referenceSearchVisible')); + assert.isFalse(contextKeyService.match('listFocus')); + } + + 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'); + if (item) { + assert.isTrue(item.isVisible); + assert.isTrue(item.isEnabled); + } else { + assert.isDefined(item); + } + }); + + 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 }); + // @ts-ignore + assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'container'); + + await commands.executeCommand('editor.action.revealDefinition'); + + const activeEditor = /** @type {MonacoEditor} */ (MonacoEditor.get(editorManager.activeEditor)); + // @ts-ignore + assert.equal(editorManager.activeEditor.parent instanceof EditorPreviewWidget, preview); + assert.equal(activeEditor.uri.toString(), serverUri.toString()); + // const |container = new Container(); + // @ts-ignore + const { lineNumber, column } = activeEditor.getControl().getPosition(); + assert.deepEqual({ lineNumber, column }, { lineNumber: 11, column: 7 }); + // @ts-ignore + 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 }); + // @ts-ignore + assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'Container'); + + await commands.executeCommand('editor.action.revealDefinition'); + + const activeEditor = /** @type {MonacoEditor} */ (MonacoEditor.get(editorManager.activeEditor)); + // @ts-ignore + assert.isFalse(editorManager.activeEditor.parent instanceof EditorPreviewWidget); + assert.equal(activeEditor.uri.toString(), inversifyUri.toString()); + // export { |Container } from "./container/container"; + // @ts-ignore + const { lineNumber, column } = activeEditor.getControl().getPosition(); + assert.deepEqual({ lineNumber, column }, { lineNumber: 3, column: 10 }); + // @ts-ignore + 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 }); + // @ts-ignore + assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'Container'); + + await commands.executeCommand('editor.action.revealDefinition'); + + const activeEditor = /** @type {MonacoEditor} */ (MonacoEditor.get(editorManager.activeEditor)); + // @ts-ignore + assert.isTrue(editorManager.activeEditor.parent instanceof EditorPreviewWidget); + assert.equal(activeEditor.uri.toString(), inversifyUri.toString()); + // export { |Container } from "./container/container"; + // @ts-ignore + const { lineNumber, column } = activeEditor.getControl().getPosition(); + assert.deepEqual({ lineNumber, column }, { lineNumber: 3, column: 10 }); + // @ts-ignore + assert.equal(activeEditor.getControl().getModel().getWordAtPosition({ lineNumber, column }).word, 'Container'); + }); + } + }); + + describe('editor.action.peekDefinition', 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 }); + // @ts-ignore + assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'container'); + + await openPeek(editor); + await openReference(); + + const activeEditor = /** @type {MonacoEditor} */ (MonacoEditor.get(editorManager.activeEditor)); + // @ts-ignore + assert.equal(editorManager.activeEditor.parent instanceof EditorPreviewWidget, preview); + assert.equal(activeEditor.uri.toString(), serverUri.toString()); + // const |container = new Container(); + // @ts-ignore + const { lineNumber, column } = activeEditor.getControl().getPosition(); + assert.deepEqual({ lineNumber, column }, { lineNumber: 11, column: 7 }); + // @ts-ignore + 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 }); + // @ts-ignore + assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'Container'); + + await openPeek(editor); + await openReference(); + + const activeEditor = /** @type {MonacoEditor} */ (MonacoEditor.get(editorManager.activeEditor)); + // @ts-ignore + assert.isFalse(editorManager.activeEditor.parent instanceof EditorPreviewWidget); + assert.equal(activeEditor.uri.toString(), inversifyUri.toString()); + // export { |Container } from "./container/container"; + // @ts-ignore + const { lineNumber, column } = activeEditor.getControl().getPosition(); + assert.deepEqual({ lineNumber, column }, { lineNumber: 3, column: 10 }); + // @ts-ignore + 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 }); + // @ts-ignore + assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'Container'); + + await openPeek(editor); + await openReference(); + + const activeEditor = /** @type {MonacoEditor} */ (MonacoEditor.get(editorManager.activeEditor)); + // @ts-ignore + assert.isTrue(editorManager.activeEditor.parent instanceof EditorPreviewWidget); + assert.equal(activeEditor.uri.toString(), inversifyUri.toString()); + // export { |Container } from "./container/container"; + // @ts-ignore + const { lineNumber, column } = activeEditor.getControl().getPosition(); + assert.deepEqual({ lineNumber, column }, { lineNumber: 3, column: 10 }); + // @ts-ignore + 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 }); + // @ts-ignore + 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 waitForAnimation(() => contextKeyService.match('suggestWidgetVisible')); + + assert.isTrue(contextKeyService.match('editorTextFocus')); + assert.isTrue(contextKeyService.match('suggestWidgetVisible')); + + keybindings.dispatchKeyDown('Enter'); + await waitForAnimation(() => !contextKeyService.match('suggestWidgetVisible')); + + assert.isTrue(contextKeyService.match('editorTextFocus')); + assert.isFalse(contextKeyService.match('suggestWidgetVisible')); + + const activeEditor = /** @type {MonacoEditor} */ (MonacoEditor.get(editorManager.activeEditor)); + assert.equal(activeEditor.uri.toString(), serverUri.toString()); + // const { Container| } = require('inversify'); + // @ts-ignore + const { lineNumber, column } = activeEditor.getControl().getPosition(); + assert.deepEqual({ lineNumber, column }, { lineNumber: 5, column: 18 }); + // @ts-ignore + assert.equal(activeEditor.getControl().getModel().getWordAtPosition({ lineNumber, column }).word, 'Container'); + }); + + it('editor.action.rename', async function () { + const editor = await openEditor(serverUri); + // const |container = new Container(); + editor.getControl().setPosition({ lineNumber: 11, column: 7 }); + // @ts-ignore + 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 waitForAnimation(() => contextKeyService.match('renameInputVisible') + && document.activeElement instanceof HTMLInputElement + && document.activeElement.selectionEnd === 'container'.length); + assert.isFalse(contextKeyService.match('editorTextFocus')); + assert.isTrue(contextKeyService.match('renameInputVisible')); + + const input = document.activeElement; + if (!(input instanceof HTMLInputElement)) { + assert.fail('expected 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 = /** @type {MonacoEditor} */ (MonacoEditor.get(editorManager.activeEditor)); + assert.equal(activeEditor.uri.toString(), serverUri.toString()); + // const |foo = new Container(); + // @ts-ignore + const { lineNumber, column } = activeEditor.getControl().getPosition(); + assert.deepEqual({ lineNumber, column }, { lineNumber: 11, column: 7 }); + // @ts-ignore + assert.equal(activeEditor.getControl().getModel().getWordAtPosition({ lineNumber, column }).word, 'foo'); + }); + + it('editor.action.triggerParameterHints', async function () { + const editor = await openEditor(serverUri); + // container.load(|backendApplicationModule); + editor.getControl().setPosition({ lineNumber: 12, column: 16 }); + // @ts-ignore + assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'backendApplicationModule'); + + assert.isTrue(contextKeyService.match('editorTextFocus')); + assert.isFalse(contextKeyService.match('parameterHintsVisible')); + + await commands.executeCommand('editor.action.triggerParameterHints'); + await waitForAnimation(() => contextKeyService.match('parameterHintsVisible')); + + assert.isTrue(contextKeyService.match('editorTextFocus')); + assert.isTrue(contextKeyService.match('parameterHintsVisible')); + + keybindings.dispatchKeyDown('Escape'); + await waitForAnimation(() => !contextKeyService.match('parameterHintsVisible')); + + assert.isTrue(contextKeyService.match('editorTextFocus')); + assert.isFalse(contextKeyService.match('parameterHintsVisible')); + }); + + it('editor.action.showHover', async function () { + const editor = await openEditor(serverUri); + // container.load(|backendApplicationModule); + editor.getControl().setPosition({ lineNumber: 12, column: 16 }); + // @ts-ignore + assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'backendApplicationModule'); + + const hover = editor.getControl()._contributions['editor.contrib.hover']; + + assert.isTrue(contextKeyService.match('editorTextFocus')); + assert.isFalse(hover.contentWidget.isVisible); + + await commands.executeCommand('editor.action.showHover'); + await waitForAnimation(() => hover.contentWidget.isVisible); + + assert.isTrue(contextKeyService.match('editorTextFocus')); + assert.isTrue(hover.contentWidget.isVisible); + + assert.deepEqual(nodeAsString(hover.contentWidget._domNode), ` +DIV { + DIV { + DIV { + DIV { + DIV { + SPAN { + DIV { + SPAN { + "const" + } + SPAN { + " " + } + SPAN { + "backendApplicationModule" + } + SPAN { + ": " + } + SPAN { + "ContainerModule" + } + } + } + } + } + } + } +} +`); + + keybindings.dispatchKeyDown('Escape'); + await waitForAnimation(() => !hover.contentWidget.isVisible); + + assert.isTrue(contextKeyService.match('editorTextFocus')); + assert.isFalse(hover.contentWidget.isVisible); + }); + + it('highligh semantic (write) occurences', async function () { + const editor = await openEditor(serverUri); + // const |container = new Container(); + const lineNumber = 11; + const column = 7; + const endColumn = column + 'container'.length; + + const hasWriteDecoration = () => { + // @ts-ignore + for (const decoration of editor.getControl().getModel().getLineDecorations(lineNumber)) { + if (decoration.range.startColumn === column && decoration.range.endColumn === endColumn && decoration.options.className === 'wordHighlightStrong') { + return true; + } + } + return false; + }; + assert.isFalse(hasWriteDecoration()); + + editor.getControl().setPosition({ lineNumber, column }); + // @ts-ignore + assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'container'); + // highlight occurences is not trigged on the explicit position change, so move a cursor as a user + keybindings.dispatchKeyDown('ArrowRight'); + await waitForAnimation(() => hasWriteDecoration()); + + assert.isTrue(hasWriteDecoration()); + }); + + it('editor.action.goToImplementation', async function () { + const editor = await openEditor(serverUri); + // con|tainer.load(backendApplicationModule); + editor.getControl().setPosition({ lineNumber: 12, column: 4 }); + // @ts-ignore + assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'container'); + + await commands.executeCommand('editor.action.goToImplementation'); + + const activeEditor = /** @type {MonacoEditor} */ (MonacoEditor.get(editorManager.activeEditor)); + assert.equal(activeEditor.uri.toString(), serverUri.toString()); + // const |container = new Container(); + // @ts-ignore + const { lineNumber, column } = activeEditor.getControl().getPosition(); + assert.deepEqual({ lineNumber, column }, { lineNumber: 11, column: 7 }); + // @ts-ignore + assert.equal(activeEditor.getControl().getModel().getWordAtPosition({ lineNumber, column }).word, 'container'); + }); + + it('editor.action.goToTypeDefinition', async function () { + const editor = await openEditor(serverUri); + // con|tainer.load(backendApplicationModule); + editor.getControl().setPosition({ lineNumber: 12, column: 4 }); + // @ts-ignore + assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'container'); + + await commands.executeCommand('editor.action.goToTypeDefinition'); + + const activeEditor = /** @type {MonacoEditor} */ (MonacoEditor.get(editorManager.activeEditor)); + assert.equal(activeEditor.uri.toString(), containerUri.toString()); + // declare class |Container implements interfaces.Container { + // @ts-ignore + const { lineNumber, column } = activeEditor.getControl().getPosition(); + assert.deepEqual({ lineNumber, column }, { lineNumber: 2, column: 15 }); + // @ts-ignore + assert.equal(activeEditor.getControl().getModel().getWordAtPosition({ lineNumber, column }).word, 'Container'); + }); + + it('run reference code lens', async function () { + // @ts-ignore + const globalValue = preferences.inspect('javascript.referencesCodeLens.enabled').globalValue; + toTearDown.push({ dispose: () => preferences.set('javascript.referencesCodeLens.enabled', globalValue, PreferenceScope.User) }); + + const editor = await openEditor(serverUri); + + const codeLens = editor.getControl()._contributions['css.editor.codeLens']; + const codeLensNode = () => codeLens._lenses[0] && codeLens._lenses[0]._contentWidget && codeLens._lenses[0]._contentWidget._domNode; + const codeLensNodeVisible = () => { + const n = codeLensNode(); + return !!n && n.style.visibility !== 'hidden'; + }; + + assert.isFalse(codeLensNodeVisible()); + + // [export ]function load(raw) { + const position = { lineNumber: 16, column: 1 }; + // @ts-ignore + editor.getControl().getModel().applyEdits([{ + range: monaco.Range.fromPositions(position, position), + forceMoveMarkers: false, + text: 'export ' + }]); + editor.getControl().revealPosition(position); + await preferences.set('javascript.referencesCodeLens.enabled', true, PreferenceScope.User); + await waitForAnimation(() => codeLensNodeVisible()); + + assert.isTrue(codeLensNodeVisible()); + const node = codeLensNode(); + if (node) { + assert.equal(nodeAsString(node), ` +SPAN { + A { + "20 references" + } +} +`); + const link = node.getElementsByTagName('a').item(0); + if (link) { + link.dispatchEvent(new MouseEvent('mouseup', { bubbles: true })); + await assertPeekOpened(editor); + await closePeek(); + } else { + assert.isDefined(link); + } + } else { + assert.isDefined(node); + } + }); + + it('editor.action.quickFix', async function () { + const column = 6; + const lineNumber = 19; + const editor = await openEditor(serverUri); + // @ts-ignore + const currentChar = () => editor.getControl().getModel().getLineContent(lineNumber).charAt(column - 1); + + // missing semicolon at + // )| + editor.getControl().setPosition({ lineNumber, column }); + editor.getControl().revealPosition({ lineNumber, column }); + assert.equal(currentChar(), ''); + + const quickFixController = editor.getControl()._contributions['editor.contrib.quickFixController']; + const lightBulbNode = () => { + const ui = quickFixController._ui.rawValue; + const lightBulb = ui && ui._lightBulbWidget.rawValue; + return lightBulb && lightBulb._domNode; + }; + const lightBulbVisible = () => { + const node = lightBulbNode(); + return !!node && node.style.visibility !== 'hidden'; + }; + + assert.isFalse(lightBulbVisible()); + await waitForAnimation(() => lightBulbVisible()); + + await commands.executeCommand('editor.action.quickFix'); + await waitForAnimation(() => !!document.querySelector('.p-Widget.p-Menu')); + await animationFrame(); + + keybindings.dispatchKeyDown('ArrowDown'); + keybindings.dispatchKeyDown('Enter'); + + await waitForAnimation(() => currentChar() === ';'); + assert.equal(currentChar(), ';'); + + await waitForAnimation(() => !lightBulbVisible()); + assert.isFalse(lightBulbVisible()); + }); + + it('editor.action.formatDocument', async function () { + const lineNumber = 5; + const editor = await openEditor(serverUri); + // @ts-ignore + const originalLenght = editor.getControl().getModel().getLineLength(lineNumber); + + // const { Container[ ] } = require('inversify'); + // @ts-ignore + editor.getControl().getModel().applyEdits([{ + range: monaco.Range.fromPositions({ lineNumber, column: 18 }, { lineNumber, column: 18 }), + forceMoveMarkers: false, + text: ' ' + }]); + + // @ts-ignore + assert.equal(editor.getControl().getModel().getLineLength(lineNumber), originalLenght + 1); + + await commands.executeCommand('editor.action.formatDocument'); + + // @ts-ignore + assert.equal(editor.getControl().getModel().getLineLength(lineNumber), originalLenght); + }); + + it('editor.action.formatSelection', async function () { + const lineNumber = 5; + const editor = await openEditor(serverUri); + // @ts-ignore + const originalLenght = editor.getControl().getModel().getLineLength(lineNumber); + + // const { Container[ } ]= require('inversify'); + // @ts-ignore + editor.getControl().getModel().applyEdits([{ + range: monaco.Range.fromPositions({ lineNumber, column: 18 }, { lineNumber, column: 21 }), + forceMoveMarkers: false, + text: ' } ' + }]); + + // @ts-ignore + assert.equal(editor.getControl().getModel().getLineLength(lineNumber), originalLenght + 2); + + // [const { Container }] = require('inversify'); + editor.getControl().setSelection({ startLineNumber: lineNumber, startColumn: 1, endLineNumber: lineNumber, endColumn: 21 }); + + await commands.executeCommand('editor.action.formatSelection'); + + // [const { Container }] = require('inversify'); + // @ts-ignore + assert.equal(editor.getControl().getModel().getLineLength(lineNumber), originalLenght + 1); + }); + +}); diff --git a/examples/api-tests/src/views.spec.js b/examples/api-tests/src/views.spec.js index dc982997e4985..d3059ad2435ce 100644 --- a/examples/api-tests/src/views.spec.js +++ b/examples/api-tests/src/views.spec.js @@ -50,6 +50,7 @@ describe('Views', function () { assert.notEqual(view, undefined); assert.equal(shell.getAreaFor(view), contribution.defaultViewOptions.area); assert.isDefined(shell.getTabBarFor(view)); + // @ts-ignore assert.equal(shell.getAreaFor(shell.getTabBarFor(view)), contribution.defaultViewOptions.area); assert.isTrue(view.isVisible); assert.equal(view, shell.activeWidget); diff --git a/examples/browser/.eslintrc.js b/examples/browser/.eslintrc.js deleted file mode 100644 index be9cf1a1b3dff..0000000000000 --- a/examples/browser/.eslintrc.js +++ /dev/null @@ -1,10 +0,0 @@ -/** @type {import('eslint').Linter.Config} */ -module.exports = { - extends: [ - '../../configs/build.eslintrc.json' - ], - parserOptions: { - tsconfigRootDir: __dirname, - project: 'compile.tsconfig.json' - } -}; diff --git a/examples/browser/package.json b/examples/browser/package.json index e97b0e99e3a88..b583fa6a2629c 100644 --- a/examples/browser/package.json +++ b/examples/browser/package.json @@ -56,13 +56,12 @@ }, "scripts": { "prepare": "yarn run clean && yarn build", - "lint": "theiaext lint", "clean": "theia clean", "build": "theia build --mode development", "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", @@ -71,4 +70,4 @@ "devDependencies": { "@theia/cli": "^1.0.0" } -} +} \ No newline at end of file diff --git a/packages/core/src/browser/keybinding.ts b/packages/core/src/browser/keybinding.ts index 3b2d6a5f69136..429a1f50ac34c 100644 --- a/packages/core/src/browser/keybinding.ts +++ b/packages/core/src/browser/keybinding.ts @@ -571,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/core/src/browser/progress-status-bar-item.ts b/packages/core/src/browser/progress-status-bar-item.ts index e1d18461a07c5..81970c6a77cd1 100644 --- a/packages/core/src/browser/progress-status-bar-item.ts +++ b/packages/core/src/browser/progress-status-bar-item.ts @@ -30,6 +30,14 @@ export class ProgressStatusBarItem implements ProgressClient { @inject(StatusBar) protected readonly statusBar: StatusBar; + protected messagesByProgress = new Map(); + + protected incomingQueue = new Array(); + + get currentProgress(): string | undefined { + return this.incomingQueue.slice(-1)[0]; + } + showProgress(progressId: string, message: ProgressMessage, cancellationToken: CancellationToken): Promise { const result = new Deferred(); cancellationToken.onCancellationRequested(() => { @@ -39,21 +47,19 @@ export class ProgressStatusBarItem implements ProgressClient { this.processEvent(progressId, 'start', message.text); return result.promise; } - protected messagesByProgress = new Map(); - protected incomingQueue = new Array(); + protected processEvent(progressId: string, event: 'start' | 'done', message?: string): void { if (event === 'start') { this.incomingQueue.push(progressId); this.messagesByProgress.set(progressId, message); } else { this.incomingQueue = this.incomingQueue.filter(id => id !== progressId); + this.messagesByProgress.delete(progressId); } this.triggerUpdate(); } - protected readonly triggerUpdate = throttle(() => { - const pick: string | undefined = this.incomingQueue.slice(-1)[0]; - this.update(pick); - }, 250, { leading: true, trailing: true }); + + protected readonly triggerUpdate = throttle(() => this.update(this.currentProgress), 250, { leading: true, trailing: true }); async reportProgress(progressId: string, update: ProgressUpdate, originalMessage: ProgressMessage, _cancellationToken: CancellationToken): Promise { const newMessage = update.message ? `${originalMessage.text}: ${update.message}` : originalMessage.text; diff --git a/packages/core/src/browser/saveable.ts b/packages/core/src/browser/saveable.ts index e97783ef6733a..e6e09221693b6 100644 --- a/packages/core/src/browser/saveable.ts +++ b/packages/core/src/browser/saveable.ts @@ -105,6 +105,9 @@ export namespace Saveable { saveable.onDirtyChanged(() => setDirty(widget, saveable.dirty)); const closeWidget = widget.close.bind(widget); const closeWithoutSaving: SaveableWidget['closeWithoutSaving'] = async () => { + if (saveable.dirty && saveable.revert) { + await saveable.revert(); + } closeWidget(); return waitForClosed(widget); }; diff --git a/packages/core/src/browser/shell/application-shell.ts b/packages/core/src/browser/shell/application-shell.ts index 07bd73a2c6af6..83339b3ee8bc8 100644 --- a/packages/core/src/browser/shell/application-shell.ts +++ b/packages/core/src/browser/shell/application-shell.ts @@ -93,6 +93,7 @@ export class DockPanelRenderer implements DockLayout.IRenderer { }); this.tabBarClasses.forEach(c => tabBar.addClass(c)); renderer.tabBar = tabBar; + tabBar.disposed.connect(() => renderer.dispose()); renderer.contextMenuPath = SHELL_TABBAR_CONTEXT_MENU; tabBar.currentChanged.connect(this.onCurrentTabChanged, this); return tabBar; @@ -1325,27 +1326,28 @@ export class ApplicationShell extends Widget { } async closeWidget(id: string, options?: ApplicationShell.CloseOptions): Promise { + // TODO handle save for composite widgets, i.e. the preference widget has 2 editors const stack = this.toTrackedStack(id); - const widget = this.toTrackedStack(id).pop(); - if (!widget) { + const current = stack.pop(); + if (!current) { return undefined; } let pendingClose; - if (SaveableWidget.is(widget)) { + if (SaveableWidget.is(current)) { let shouldSave; if (options && 'save' in options) { shouldSave = () => options.save; } - pendingClose = widget.closeWithSaving({ shouldSave }); + pendingClose = current.closeWithSaving({ shouldSave }); } else { - widget.close(); - pendingClose = waitForClosed(widget); + current.close(); + pendingClose = waitForClosed(current); }; await Promise.all([ pendingClose, this.pendingUpdates ]); - return stack[0] || widget; + return stack[0] || current; } /** diff --git a/packages/core/src/browser/shell/side-panel-handler.ts b/packages/core/src/browser/shell/side-panel-handler.ts index ff72e3c1f24a6..6667c1516d00e 100644 --- a/packages/core/src/browser/shell/side-panel-handler.ts +++ b/packages/core/src/browser/shell/side-panel-handler.ts @@ -138,6 +138,7 @@ export class SidePanelHandler { suppressScrollX: true }); tabBarRenderer.tabBar = sideBar; + sideBar.disposed.connect(() => tabBarRenderer.dispose()); tabBarRenderer.contextMenuPath = SHELL_TABBAR_CONTEXT_MENU; sideBar.addClass('theia-app-' + side); sideBar.addClass(LEFT_RIGHT_AREA_CLASS); diff --git a/packages/core/src/browser/shell/tab-bars.ts b/packages/core/src/browser/shell/tab-bars.ts index 3189561a3111d..58404dd03308c 100644 --- a/packages/core/src/browser/shell/tab-bars.ts +++ b/packages/core/src/browser/shell/tab-bars.ts @@ -94,6 +94,10 @@ export class TabBarRenderer extends TabBar.Renderer { } } + dispose(): void { + this.toDispose.dispose(); + } + protected _tabBar?: TabBar; protected readonly toDisposeOnTabBar = new DisposableCollection(); /** @@ -101,6 +105,9 @@ export class TabBarRenderer extends TabBar.Renderer { * is requested. */ set tabBar(tabBar: TabBar | undefined) { + if (this.toDispose.disposed) { + throw new Error('disposed'); + } if (this._tabBar === tabBar) { return; } @@ -453,11 +460,21 @@ export class ScrollableTabBar extends TabBar { private scrollBarFactory: () => PerfectScrollbar; private pendingReveal?: Promise; + protected readonly toDispose = new DisposableCollection(); + constructor(options?: TabBar.IOptions & PerfectScrollbar.Options) { super(options); this.scrollBarFactory = () => new PerfectScrollbar(this.scrollbarHost, options); } + dispose(): void { + if (this.isDisposed) { + return; + } + super.dispose(); + this.toDispose.dispose(); + } + protected onAfterAttach(msg: Message): void { if (!this.scrollBar) { this.scrollBar = this.scrollBarFactory(); @@ -571,7 +588,7 @@ export class ToolbarAwareTabBar extends ScrollableTabBar { super(options); this.rewireDOM(); - this.tabBarToolbarRegistry.onDidChange(() => this.update()); + this.toDispose.push(this.tabBarToolbarRegistry.onDidChange(() => this.update())); } /** diff --git a/packages/core/src/browser/widget-open-handler.ts b/packages/core/src/browser/widget-open-handler.ts index 39c3015798a2c..1c8ddfbb4d731 100644 --- a/packages/core/src/browser/widget-open-handler.ts +++ b/packages/core/src/browser/widget-open-handler.ts @@ -132,16 +132,8 @@ export abstract class WidgetOpenHandler implements OpenHan protected abstract createWidgetOptions(uri: URI, options?: WidgetOpenerOptions): Object; async closeAll(options?: ApplicationShell.CloseOptions): Promise { - const allClosed: W[] = []; - await Promise.all( - [this.all.map(async widget => { - const closed = await this.shell.closeWidget(widget.id, options); - if (closed) { - allClosed.push(closed as W); - } - })] - ); - return allClosed; + const closed = await Promise.all(this.all.map(widget => this.shell.closeWidget(widget.id, options))); + return closed.filter(widget => !!widget) as W[]; } } diff --git a/packages/core/src/common/reference.spec.ts b/packages/core/src/common/reference.spec.ts index 4ea946508c75f..b80525344f68f 100644 --- a/packages/core/src/common/reference.spec.ts +++ b/packages/core/src/common/reference.spec.ts @@ -121,4 +121,25 @@ describe('reference', () => { result.forEach(v => assert.ok(result[0].object === v.object)); }); + it('should not dispose an object if a reference is pending', async () => { + let disposed = false; + const references = new ReferenceCollection(async key => ({ + key, dispose: () => { + disposed = true; + } + })); + assert.ok(!disposed); + + let reference = await references.acquire('a'); + + const pendingReference = references.acquire('a'); + reference.dispose(); + + assert.ok(!disposed); + + reference = await pendingReference; + reference.dispose(); + assert.ok(disposed); + }); + }); diff --git a/packages/core/src/common/reference.ts b/packages/core/src/common/reference.ts index be03890d7e486..0386d73971f19 100644 --- a/packages/core/src/common/reference.ts +++ b/packages/core/src/common/reference.ts @@ -116,13 +116,17 @@ export class ReferenceCollection extends AbstractRefere async acquire(args: K): Promise> { const key = this.toKey(args); + const existing = this._values.get(key); + if (existing) { + return this.doAcquire(key, existing); + } const object = await this.getOrCreateValue(key, args); return this.doAcquire(key, object); } protected readonly pendingValues = new Map>(); protected async getOrCreateValue(key: string, args: K): Promise { - const existing = this._values.get(key) || this.pendingValues.get(key); + const existing = this.pendingValues.get(key); if (existing) { return existing; } diff --git a/packages/languages/src/browser/language-client-services.ts b/packages/languages/src/browser/language-client-services.ts index 6205afd9ad9c9..88160abe49034 100644 --- a/packages/languages/src/browser/language-client-services.ts +++ b/packages/languages/src/browser/language-client-services.ts @@ -28,7 +28,7 @@ export interface Language { } export interface WorkspaceSymbolProvider extends services.WorkspaceSymbolProvider { - resolveWorkspaceSymbol?(symbol: services.SymbolInformation, token: services.CancellationToken): Thenable + resolveWorkspaceSymbol?(symbol: services.SymbolInformation, token: services.CancellationToken): Thenable } export const Languages = Symbol('Languages'); diff --git a/packages/monaco/src/typings/monaco/index.d.ts b/packages/monaco/src/typings/monaco/index.d.ts index 1a33f412b66be..420106caf6e49 100644 --- a/packages/monaco/src/typings/monaco/index.d.ts +++ b/packages/monaco/src/typings/monaco/index.d.ts @@ -68,12 +68,50 @@ declare module monaco.editor { readonly _contributions: { 'editor.controller.quickOpenController': monaco.quickOpen.QuickOpenController 'editor.contrib.referencesController': monaco.referenceSearch.ReferencesController + 'editor.contrib.hover': ModesHoverController + 'css.editor.codeLens': CodeLensContribution + 'editor.contrib.quickFixController': QuickFixController } readonly _modelData: { cursor: ICursor } | null; } + // https://github.com/theia-ide/vscode/blob/d24b5f70c69b3e75cd10c6b5247a071265ccdd38/src/vs/editor/contrib/codeAction/codeActionCommands.ts#L69 + export interface QuickFixController { + readonly _ui: { + rawValue?: CodeActionUi + } + } + export interface CodeActionUi { + readonly _lightBulbWidget: { + rawValue?: LightBulbWidget + } + } + export interface LightBulbWidget { + readonly _domNode: HTMLDivElement; + } + + // https://github.com/theia-ide/vscode/blob/d24b5f70c69b3e75cd10c6b5247a071265ccdd38/src/vs/editor/contrib/codelens/codelensController.ts#L24 + export interface CodeLensContribution { + readonly _lenses: CodeLensWidget[]; + } + export interface CodeLensWidget { + readonly _contentWidget?: CodeLensContentWidget; + } + export interface CodeLensContentWidget { + readonly _domNode: HTMLElement; + } + + // https://github.com/theia-ide/vscode/blob/standalone/0.19.x/src/vs/editor/contrib/hover/hover.ts#L31 + export interface ModesHoverController { + readonly contentWidget: ModesContentHoverWidget + } + export interface ModesContentHoverWidget { + readonly isVisible: boolean; + readonly _domNode: HTMLElement; + } + // https://github.com/theia-ide/vscode/blob/standalone/0.19.x/src/vs/editor/common/controller/cursor.ts#L169 export interface ICursor { trigger(source: string, handlerId: string, payload: any): void; @@ -733,7 +771,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-model.ts b/packages/plugin-ext/src/common/plugin-api-rpc-model.ts index 9e359bb8e21ef..1a2772fbb9fe7 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc-model.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc-model.ts @@ -17,7 +17,6 @@ import * as theia from '@theia/plugin'; import { UriComponents } from './uri-components'; import { FileStat } from '@theia/filesystem/lib/common'; -import { SymbolInformation } from 'vscode-languageserver-types'; // Should contains internal Plugin API types @@ -452,11 +451,6 @@ export interface Breakpoint { readonly functionName?: string; } -export interface WorkspaceSymbolProvider { - provideWorkspaceSymbols(params: WorkspaceSymbolParams, token: monaco.CancellationToken): Thenable; - resolveWorkspaceSymbol(symbol: SymbolInformation, token: monaco.CancellationToken): Thenable -} - export interface WorkspaceSymbolParams { query: string } diff --git a/packages/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts index 4883881957355..c090dcf57dfab 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 { @@ -1147,7 +1149,7 @@ export interface PluginInfo { export interface LanguagesExt { $provideCompletionItems(handle: number, resource: UriComponents, position: Position, context: CompletionContext, token: CancellationToken): Promise; - $resolveCompletionItem(handle: number, resource: UriComponents, position: Position, completion: Completion, token: CancellationToken): Promise; + $resolveCompletionItem(handle: number, resource: UriComponents, position: Position, completion: Completion, token: CancellationToken): Promise; $releaseCompletionItems(handle: number, id: number): void; $provideImplementation(handle: number, resource: UriComponents, position: Position, token: CancellationToken): Promise; $provideTypeDefinition(handle: number, resource: UriComponents, position: Position, token: CancellationToken): Promise; @@ -1187,7 +1189,7 @@ export interface LanguagesExt { ): Promise; $provideDocumentSymbols(handle: number, resource: UriComponents, token: CancellationToken): Promise; $provideWorkspaceSymbols(handle: number, query: string, token: CancellationToken): PromiseLike; - $resolveWorkspaceSymbol(handle: number, symbol: SymbolInformation, token: CancellationToken): PromiseLike; + $resolveWorkspaceSymbol(handle: number, symbol: SymbolInformation, token: CancellationToken): PromiseLike; $provideFoldingRange( handle: number, resource: UriComponents, diff --git a/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts b/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts index 94eefa791ac02..a3bac3f18dfc8 100644 --- a/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts +++ b/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts @@ -631,6 +631,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/debug/debug-main.ts b/packages/plugin-ext/src/main/browser/debug/debug-main.ts index cab3e34ad054b..a3183978fcadd 100644 --- a/packages/plugin-ext/src/main/browser/debug/debug-main.ts +++ b/packages/plugin-ext/src/main/browser/debug/debug-main.ts @@ -97,11 +97,11 @@ export class DebugMainImpl implements DebugMain, Disposable { ); }; this.debugExt.$breakpointsDidChange(this.toTheiaPluginApiBreakpoints(this.breakpointsManager.getBreakpoints()), [], []); - this.breakpointsManager.onDidChangeBreakpoints(fireDidChangeBreakpoints); this.debugExt.$breakpointsDidChange(this.toTheiaPluginApiBreakpoints(this.breakpointsManager.getFunctionBreakpoints()), [], []); - this.breakpointsManager.onDidChangeFunctionBreakpoints(fireDidChangeBreakpoints); this.toDispose.pushAll([ + this.breakpointsManager.onDidChangeBreakpoints(fireDidChangeBreakpoints), + this.breakpointsManager.onDidChangeFunctionBreakpoints(fireDidChangeBreakpoints), this.sessionManager.onDidCreateDebugSession(debugSession => this.debugExt.$sessionDidCreate(debugSession.id)), this.sessionManager.onDidDestroyDebugSession(debugSession => this.debugExt.$sessionDidDestroy(debugSession.id)), this.sessionManager.onDidChangeActiveDebugSession(event => this.debugExt.$sessionDidChange(event.current && event.current.id)), diff --git a/packages/plugin-ext/src/main/browser/languages-main.ts b/packages/plugin-ext/src/main/browser/languages-main.ts index 8eec2f4d6e267..697abb88a3152 100644 --- a/packages/plugin-ext/src/main/browser/languages-main.ts +++ b/packages/plugin-ext/src/main/browser/languages-main.ts @@ -36,7 +36,7 @@ import { } from '../../common/plugin-api-rpc'; import { injectable, inject } from 'inversify'; import { - SerializedDocumentFilter, MarkerData, Range, WorkspaceSymbolProvider, RelatedInformation, + SerializedDocumentFilter, MarkerData, Range, RelatedInformation, MarkerSeverity, DocumentLink, WorkspaceSymbolParams, CodeAction } from '../../common/plugin-api-rpc-model'; import { RPCProtocol } from '../../common/rpc-protocol'; @@ -56,6 +56,7 @@ import { CallHierarchyService, CallHierarchyServiceProvider, Caller, Definition import { toDefinition, toUriComponents, fromDefinition, fromPosition, toCaller } from './callhierarchy/callhierarchy-type-converters'; import { Position, DocumentUri } from 'vscode-languageserver-types'; import { ObjectIdentifier } from '../../common/object-identifier'; +import { WorkspaceSymbolProvider } from '@theia/languages/lib/browser/language-client-services'; @injectable() export class LanguagesMainImpl implements LanguagesMain, Disposable { @@ -355,7 +356,7 @@ export class LanguagesMainImpl implements LanguagesMain, Disposable { return this.proxy.$provideWorkspaceSymbols(handle, params.query, token); } - protected resolveWorkspaceSymbol(handle: number, symbol: vst.SymbolInformation, token: monaco.CancellationToken): Thenable { + protected resolveWorkspaceSymbol(handle: number, symbol: vst.SymbolInformation, token: monaco.CancellationToken): Thenable { return this.proxy.$resolveWorkspaceSymbol(handle, symbol, token); } diff --git a/packages/plugin-ext/src/main/browser/webview/webview.ts b/packages/plugin-ext/src/main/browser/webview/webview.ts index bc2f348a8fafd..96254daaf5611 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/languages.ts b/packages/plugin-ext/src/plugin/languages.ts index 494fcfc1e6e2d..5fb27fbab1382 100644 --- a/packages/plugin-ext/src/plugin/languages.ts +++ b/packages/plugin-ext/src/plugin/languages.ts @@ -187,12 +187,15 @@ export class LanguagesExtImpl implements LanguagesExt { } // eslint-disable-next-line @typescript-eslint/no-explicit-any - private withAdapter(handle: number, ctor: { new(...args: any[]): A }, callback: (adapter: A) => Promise): Promise { + private async withAdapter(handle: number, ctor: { new(...args: any[]): A }, callback: (adapter: A) => Promise, fallbackValue: R): Promise { const adapter = this.adaptersMap.get(handle); - if (!(adapter instanceof ctor)) { - return Promise.reject(new Error('no adapter found')); + if (!adapter) { + return fallbackValue; } - return callback(adapter); + if (adapter instanceof ctor) { + return callback(adapter); + } + throw new Error('no adapter found'); } private transformDocumentSelector(selector: theia.DocumentSelector): SerializedDocumentFilter[] { @@ -226,15 +229,15 @@ export class LanguagesExtImpl implements LanguagesExt { // ### Completion begin $provideCompletionItems(handle: number, resource: UriComponents, position: Position, context: CompletionContext, token: theia.CancellationToken): Promise { - return this.withAdapter(handle, CompletionAdapter, adapter => adapter.provideCompletionItems(URI.revive(resource), position, context, token)); + return this.withAdapter(handle, CompletionAdapter, adapter => adapter.provideCompletionItems(URI.revive(resource), position, context, token), undefined); } - $resolveCompletionItem(handle: number, resource: UriComponents, position: Position, completion: Completion, token: theia.CancellationToken): Promise { - return this.withAdapter(handle, CompletionAdapter, adapter => adapter.resolveCompletionItem(URI.revive(resource), position, completion, token)); + $resolveCompletionItem(handle: number, resource: UriComponents, position: Position, completion: Completion, token: theia.CancellationToken): Promise { + return this.withAdapter(handle, CompletionAdapter, adapter => adapter.resolveCompletionItem(URI.revive(resource), position, completion, token), undefined); } $releaseCompletionItems(handle: number, id: number): void { - this.withAdapter(handle, CompletionAdapter, async adapter => adapter.releaseCompletionItems(id)); + this.withAdapter(handle, CompletionAdapter, async adapter => adapter.releaseCompletionItems(id), undefined); } registerCompletionItemProvider(selector: theia.DocumentSelector, provider: theia.CompletionItemProvider, triggerCharacters: string[], @@ -247,7 +250,7 @@ export class LanguagesExtImpl implements LanguagesExt { // ### Definition provider begin $provideDefinition(handle: number, resource: UriComponents, position: Position, token: theia.CancellationToken): Promise { - return this.withAdapter(handle, DefinitionAdapter, adapter => adapter.provideDefinition(URI.revive(resource), position, token)); + return this.withAdapter(handle, DefinitionAdapter, adapter => adapter.provideDefinition(URI.revive(resource), position, token), undefined); } registerDefinitionProvider(selector: theia.DocumentSelector, provider: theia.DefinitionProvider, pluginInfo: PluginInfo): theia.Disposable { @@ -259,7 +262,7 @@ export class LanguagesExtImpl implements LanguagesExt { // ### Declaration provider begin $provideDeclaration(handle: number, resource: UriComponents, position: Position, token: theia.CancellationToken): Promise { - return this.withAdapter(handle, DeclarationAdapter, adapter => adapter.provideDeclaration(URI.revive(resource), position, token)); + return this.withAdapter(handle, DeclarationAdapter, adapter => adapter.provideDeclaration(URI.revive(resource), position, token), undefined); } registerDeclarationProvider(selector: theia.DocumentSelector, provider: theia.DeclarationProvider, pluginInfo: PluginInfo): theia.Disposable { @@ -273,11 +276,11 @@ export class LanguagesExtImpl implements LanguagesExt { $provideSignatureHelp( handle: number, resource: UriComponents, position: Position, context: SignatureHelpContext, token: theia.CancellationToken ): Promise { - return this.withAdapter(handle, SignatureHelpAdapter, adapter => adapter.provideSignatureHelp(URI.revive(resource), position, token, context)); + return this.withAdapter(handle, SignatureHelpAdapter, adapter => adapter.provideSignatureHelp(URI.revive(resource), position, token, context), undefined); } $releaseSignatureHelp(handle: number, id: number): void { - this.withAdapter(handle, SignatureHelpAdapter, async adapter => adapter.releaseSignatureHelp(id)); + this.withAdapter(handle, SignatureHelpAdapter, async adapter => adapter.releaseSignatureHelp(id), undefined); } registerSignatureHelpProvider(selector: theia.DocumentSelector, provider: theia.SignatureHelpProvider, metadata: theia.SignatureHelpProviderMetadata, @@ -300,7 +303,7 @@ export class LanguagesExtImpl implements LanguagesExt { // ### Implementation provider begin $provideImplementation(handle: number, resource: UriComponents, position: Position, token: theia.CancellationToken): Promise { - return this.withAdapter(handle, ImplementationAdapter, adapter => adapter.provideImplementation(URI.revive(resource), position, token)); + return this.withAdapter(handle, ImplementationAdapter, adapter => adapter.provideImplementation(URI.revive(resource), position, token), undefined); } registerImplementationProvider(selector: theia.DocumentSelector, provider: theia.ImplementationProvider, pluginInfo: PluginInfo): theia.Disposable { @@ -312,7 +315,7 @@ export class LanguagesExtImpl implements LanguagesExt { // ### Type Definition provider begin $provideTypeDefinition(handle: number, resource: UriComponents, position: Position, token: theia.CancellationToken): Promise { - return this.withAdapter(handle, TypeDefinitionAdapter, adapter => adapter.provideTypeDefinition(URI.revive(resource), position, token)); + return this.withAdapter(handle, TypeDefinitionAdapter, adapter => adapter.provideTypeDefinition(URI.revive(resource), position, token), undefined); } registerTypeDefinitionProvider(selector: theia.DocumentSelector, provider: theia.TypeDefinitionProvider, pluginInfo: PluginInfo): theia.Disposable { @@ -330,7 +333,7 @@ export class LanguagesExtImpl implements LanguagesExt { } $provideHover(handle: number, resource: UriComponents, position: Position, token: theia.CancellationToken): Promise { - return this.withAdapter(handle, HoverAdapter, adapter => adapter.provideHover(URI.revive(resource), position, token)); + return this.withAdapter(handle, HoverAdapter, adapter => adapter.provideHover(URI.revive(resource), position, token), undefined); } // ### Hover Provider end @@ -342,7 +345,7 @@ export class LanguagesExtImpl implements LanguagesExt { } $provideDocumentHighlights(handle: number, resource: UriComponents, position: Position, token: theia.CancellationToken): Promise { - return this.withAdapter(handle, DocumentHighlightAdapter, adapter => adapter.provideDocumentHighlights(URI.revive(resource), position, token)); + return this.withAdapter(handle, DocumentHighlightAdapter, adapter => adapter.provideDocumentHighlights(URI.revive(resource), position, token), undefined); } // ### Document Highlight Provider end @@ -354,11 +357,11 @@ export class LanguagesExtImpl implements LanguagesExt { } $provideWorkspaceSymbols(handle: number, query: string, token: theia.CancellationToken): PromiseLike { - return this.withAdapter(handle, WorkspaceSymbolAdapter, adapter => adapter.provideWorkspaceSymbols(query, token)); + return this.withAdapter(handle, WorkspaceSymbolAdapter, adapter => adapter.provideWorkspaceSymbols(query, token), []); } - $resolveWorkspaceSymbol(handle: number, symbol: SymbolInformation, token: theia.CancellationToken): PromiseLike { - return this.withAdapter(handle, WorkspaceSymbolAdapter, adapter => adapter.resolveWorkspaceSymbol(symbol, token)); + $resolveWorkspaceSymbol(handle: number, symbol: SymbolInformation, token: theia.CancellationToken): PromiseLike { + return this.withAdapter(handle, WorkspaceSymbolAdapter, adapter => adapter.resolveWorkspaceSymbol(symbol, token), undefined); } // ### WorkspaceSymbol Provider end @@ -371,7 +374,7 @@ export class LanguagesExtImpl implements LanguagesExt { $provideDocumentFormattingEdits(handle: number, resource: UriComponents, options: FormattingOptions, token: theia.CancellationToken): Promise { - return this.withAdapter(handle, DocumentFormattingAdapter, adapter => adapter.provideDocumentFormattingEdits(URI.revive(resource), options, token)); + return this.withAdapter(handle, DocumentFormattingAdapter, adapter => adapter.provideDocumentFormattingEdits(URI.revive(resource), options, token), undefined); } // ### Document Formatting Edit end @@ -385,7 +388,7 @@ export class LanguagesExtImpl implements LanguagesExt { $provideDocumentRangeFormattingEdits(handle: number, resource: UriComponents, range: Range, options: FormattingOptions, token: theia.CancellationToken): Promise { - return this.withAdapter(handle, RangeFormattingAdapter, adapter => adapter.provideDocumentRangeFormattingEdits(URI.revive(resource), range, options, token)); + return this.withAdapter(handle, RangeFormattingAdapter, adapter => adapter.provideDocumentRangeFormattingEdits(URI.revive(resource), range, options, token), undefined); } // ### Document Range Formatting Edit end @@ -403,17 +406,17 @@ export class LanguagesExtImpl implements LanguagesExt { $provideOnTypeFormattingEdits(handle: number, resource: UriComponents, position: Position, ch: string, options: FormattingOptions, token: theia.CancellationToken): Promise { - return this.withAdapter(handle, OnTypeFormattingAdapter, adapter => adapter.provideOnTypeFormattingEdits(URI.revive(resource), position, ch, options, token)); + return this.withAdapter(handle, OnTypeFormattingAdapter, adapter => adapter.provideOnTypeFormattingEdits(URI.revive(resource), position, ch, options, token), undefined); } // ### On Type Formatting Edit end // ### Document Link Provider begin $provideDocumentLinks(handle: number, resource: UriComponents, token: theia.CancellationToken): Promise { - return this.withAdapter(handle, LinkProviderAdapter, adapter => adapter.provideLinks(URI.revive(resource), token)); + return this.withAdapter(handle, LinkProviderAdapter, adapter => adapter.provideLinks(URI.revive(resource), token), undefined); } $resolveDocumentLink(handle: number, link: DocumentLink, token: theia.CancellationToken): Promise { - return this.withAdapter(handle, LinkProviderAdapter, adapter => adapter.resolveLink(link, token)); + return this.withAdapter(handle, LinkProviderAdapter, adapter => adapter.resolveLink(link, token), undefined); } registerLinkProvider(selector: theia.DocumentSelector, provider: theia.DocumentLinkProvider, pluginInfo: PluginInfo): theia.Disposable { @@ -423,7 +426,7 @@ export class LanguagesExtImpl implements LanguagesExt { } $releaseDocumentLinks(handle: number, ids: number[]): void { - this.withAdapter(handle, LinkProviderAdapter, async adapter => adapter.releaseDocumentLinks(ids)); + this.withAdapter(handle, LinkProviderAdapter, async adapter => adapter.releaseDocumentLinks(ids), undefined); } // ### Document Link Provider end @@ -452,7 +455,7 @@ export class LanguagesExtImpl implements LanguagesExt { context: CodeActionContext, token: theia.CancellationToken ): Promise { - return this.withAdapter(handle, CodeActionAdapter, adapter => adapter.provideCodeAction(URI.revive(resource), rangeOrSelection, context, token)); + return this.withAdapter(handle, CodeActionAdapter, adapter => adapter.provideCodeAction(URI.revive(resource), rangeOrSelection, context, token), undefined); } // ### Code Actions Provider end @@ -472,21 +475,21 @@ export class LanguagesExtImpl implements LanguagesExt { } $provideCodeLenses(handle: number, resource: UriComponents, token: theia.CancellationToken): Promise { - return this.withAdapter(handle, CodeLensAdapter, adapter => adapter.provideCodeLenses(URI.revive(resource), token)); + return this.withAdapter(handle, CodeLensAdapter, adapter => adapter.provideCodeLenses(URI.revive(resource), token), undefined); } $resolveCodeLens(handle: number, resource: UriComponents, symbol: CodeLensSymbol, token: theia.CancellationToken): Promise { - return this.withAdapter(handle, CodeLensAdapter, adapter => adapter.resolveCodeLens(URI.revive(resource), symbol, token)); + return this.withAdapter(handle, CodeLensAdapter, adapter => adapter.resolveCodeLens(URI.revive(resource), symbol, token), undefined); } $releaseCodeLenses(handle: number, ids: number[]): void { - this.withAdapter(handle, CodeLensAdapter, async adapter => adapter.releaseCodeLenses(ids)); + this.withAdapter(handle, CodeLensAdapter, async adapter => adapter.releaseCodeLenses(ids), undefined); } // ### Code Lens Provider end // ### Code Reference Provider begin $provideReferences(handle: number, resource: UriComponents, position: Position, context: ReferenceContext, token: theia.CancellationToken): Promise { - return this.withAdapter(handle, ReferenceAdapter, adapter => adapter.provideReferences(URI.revive(resource), position, context, token)); + return this.withAdapter(handle, ReferenceAdapter, adapter => adapter.provideReferences(URI.revive(resource), position, context, token), undefined); } registerReferenceProvider(selector: theia.DocumentSelector, provider: theia.ReferenceProvider, pluginInfo: PluginInfo): theia.Disposable { @@ -504,7 +507,7 @@ export class LanguagesExtImpl implements LanguagesExt { } $provideDocumentSymbols(handle: number, resource: UriComponents, token: theia.CancellationToken): Promise { - return this.withAdapter(handle, OutlineAdapter, adapter => adapter.provideDocumentSymbols(URI.revive(resource), token)); + return this.withAdapter(handle, OutlineAdapter, adapter => adapter.provideDocumentSymbols(URI.revive(resource), token), undefined); } // ### Document Symbol Provider end @@ -516,11 +519,11 @@ export class LanguagesExtImpl implements LanguagesExt { } $provideDocumentColors(handle: number, resource: UriComponents, token: theia.CancellationToken): Promise { - return this.withAdapter(handle, ColorProviderAdapter, adapter => adapter.provideColors(URI.revive(resource), token)); + return this.withAdapter(handle, ColorProviderAdapter, adapter => adapter.provideColors(URI.revive(resource), token), []); } $provideColorPresentations(handle: number, resource: UriComponents, colorInfo: RawColorInfo, token: theia.CancellationToken): Promise { - return this.withAdapter(handle, ColorProviderAdapter, adapter => adapter.provideColorPresentations(URI.revive(resource), colorInfo, token)); + return this.withAdapter(handle, ColorProviderAdapter, adapter => adapter.provideColorPresentations(URI.revive(resource), colorInfo, token), []); } // ### Color Provider end @@ -537,7 +540,7 @@ export class LanguagesExtImpl implements LanguagesExt { context: theia.FoldingContext, token: theia.CancellationToken ): Promise { - return this.withAdapter(callId, FoldingProviderAdapter, adapter => adapter.provideFoldingRanges(URI.revive(resource), context, token)); + return this.withAdapter(callId, FoldingProviderAdapter, adapter => adapter.provideFoldingRanges(URI.revive(resource), context, token), undefined); } // ### Folding Range Provider end @@ -549,11 +552,11 @@ export class LanguagesExtImpl implements LanguagesExt { } $provideRenameEdits(handle: number, resource: UriComponents, position: Position, newName: string, token: theia.CancellationToken): Promise { - return this.withAdapter(handle, RenameAdapter, adapter => adapter.provideRenameEdits(URI.revive(resource), position, newName, token)); + return this.withAdapter(handle, RenameAdapter, adapter => adapter.provideRenameEdits(URI.revive(resource), position, newName, token), undefined); } $resolveRenameLocation(handle: number, resource: UriComponents, position: Position, token: theia.CancellationToken): Promise { - return this.withAdapter(handle, RenameAdapter, adapter => adapter.resolveRenameLocation(URI.revive(resource), position, token)); + return this.withAdapter(handle, RenameAdapter, adapter => adapter.resolveRenameLocation(URI.revive(resource), position, token), undefined); } // ### Rename Provider end @@ -565,11 +568,11 @@ export class LanguagesExtImpl implements LanguagesExt { } $provideRootDefinition(handle: number, resource: UriComponents, location: Position, token: theia.CancellationToken): Promise { - return this.withAdapter(handle, CallHierarchyAdapter, adapter => adapter.provideRootDefinition(URI.revive(resource), location, token)); + return this.withAdapter(handle, CallHierarchyAdapter, adapter => adapter.provideRootDefinition(URI.revive(resource), location, token), undefined); } $provideCallers(handle: number, definition: CallHierarchyDefinition, token: theia.CancellationToken): Promise { - return this.withAdapter(handle, CallHierarchyAdapter, adapter => adapter.provideCallers(definition, token)); + return this.withAdapter(handle, CallHierarchyAdapter, adapter => adapter.provideCallers(definition, token), undefined); } // ### Call Hierarchy Provider end } diff --git a/packages/plugin-ext/src/plugin/plugin-manager.ts b/packages/plugin-ext/src/plugin/plugin-manager.ts index 0a33047a13887..a303d68464397 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[] = []; diff --git a/tsconfig.json b/tsconfig.json index e62e4145a55a9..4445c3b8712ac 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,9 +8,11 @@ "include": [ "dev-packages/*/src", "packages/*/src", - "examples/*/src" + "examples/*/src", + "examples/browser/src-gen" ], "compilerOptions": { + "allowJs": true, "baseUrl": ".", "paths": { "mv": [ @@ -165,4 +167,4 @@ ] } } -} +} \ No newline at end of file