From 3ce66db82ef905917d9643bcdcad1f6a3e53d019 Mon Sep 17 00:00:00 2001 From: William Gonzalez Date: Wed, 12 Sep 2018 17:49:12 +0100 Subject: [PATCH] Use filesystem specific path for search path Signed-off-by: William Gonzalez --- .github/PULL_REQUEST_TEMPLATE.md | 1 + README.md | 9 +- Untitled.txt | 0 dev-packages/application-manager/package.json | 6 +- .../src/generator/webpack-generator.ts | 2 +- dev-packages/application-package/package.json | 4 +- dev-packages/cli/package.json | 4 +- dev-packages/ext-scripts/package.json | 2 +- doc/Developing.md | 12 + examples/browser/package.json | 81 +-- .../test/top-panel/top-panel.ui-spec.ts | 4 +- examples/browser/wdio.conf.js | 32 +- examples/electron/package.json | 81 +-- lerna.json | 2 +- package.json | 1 - packages/bunyan/package.json | 6 +- .../bunyan/src/node/bunyan-logger-server.ts | 3 +- packages/callhierarchy/package.json | 12 +- packages/core/package.json | 6 +- .../browser/common-frontend-contribution.ts | 5 + packages/core/src/browser/dialogs.ts | 87 ++- .../browser/frontend-application-module.ts | 29 +- .../core/src/browser/http-open-handler.ts | 2 +- .../src/browser/logger-frontend-module.ts | 14 +- .../src/browser/menu/browser-menu-plugin.ts | 2 +- .../preferences/preference-contribution.ts | 48 +- .../preferences/preference-provider.ts | 2 + .../browser/preferences/preference-proxy.ts | 63 +-- .../browser/preferences/preference-service.ts | 159 +++--- packages/core/src/browser/quick-open/index.ts | 2 + .../quick-open/prefix-quick-open-service.ts | 264 +++++++++ .../quick-open/quick-command-contribution.ts | 11 +- .../quick-open/quick-command-service.ts | 46 +- .../quick-open-frontend-contribution.ts | 36 ++ .../browser/quick-open/quick-open-service.ts | 5 + .../browser/shell/shell-layout-restorer.ts | 4 +- .../core/src/browser/storage-service.spec.ts | 4 + packages/core/src/browser/storage-service.ts | 50 +- packages/core/src/browser/style/index.css | 3 +- packages/core/src/browser/style/menus.css | 1 - .../core/src/browser/style/scrollbars.css | 51 +- .../src/browser/style/search-box.css | 1 - .../src/browser/tree}/fuzzy-search.spec.ts | 0 .../src/browser/tree}/fuzzy-search.ts | 0 packages/core/src/browser/tree/index.ts | 1 + .../src/browser/tree}/search-box-debounce.ts | 4 +- .../src/browser/tree}/search-box.ts | 12 +- .../core/src/browser/tree/tree-container.ts | 13 + .../src/browser/tree/tree-iterator.spec.ts | 4 + packages/core/src/browser/tree/tree-model.ts | 23 +- .../src/browser/tree/tree-search.ts} | 41 +- .../browser/tree/tree-selection-state.spec.ts | 4 + .../core/src/browser/tree/tree-widget.tsx | 129 ++++- .../core/src/browser/widgets/react-widget.tsx | 6 +- packages/core/src/browser/widgets/widget.ts | 5 +- packages/core/src/common/event.ts | 2 +- packages/core/src/common/logger-protocol.ts | 62 +++ packages/core/src/common/logger-watcher.ts | 1 + packages/core/src/common/logger.ts | 94 +--- packages/core/src/node/cluster.spec.ts | 2 +- packages/core/src/node/cluster/main.ts | 12 +- .../core/src/node/cluster/master-process.ts | 30 +- .../core/src/node/cluster/server-worker.ts | 9 +- .../core/src/node/console-logger-server.ts | 21 +- packages/cpp/README.md | 8 +- packages/cpp/package.json | 18 +- ...-build-configurations-statusbar-element.ts | 6 +- .../browser/cpp-build-configurations-ui.ts | 158 ++++++ .../src/browser/cpp-build-configurations.ts | 105 +--- .../cpp/src/browser/cpp-frontend-module.ts | 3 +- .../src/browser/cpp-grammar-contribution.ts | 6 +- packages/cpp/src/browser/cpp-preferences.ts | 9 +- packages/debug-nodejs/package.json | 10 +- packages/debug/package.json | 16 +- .../browser/breakpoint/breakpoint-manager.ts | 2 +- packages/debug/src/browser/debug-command.ts | 127 +++-- packages/debug/src/node/debug-adapter.ts | 10 + packages/debug/src/node/debug-model.ts | 1 + packages/debug/src/node/debug-service.ts | 9 +- packages/editor/package.json | 12 +- .../editor/src/browser/editor-preferences.ts | 7 +- packages/editor/src/browser/editor.ts | 4 +- packages/editorconfig/package.json | 10 +- .../browser/editorconfig-document-manager.ts | 15 +- packages/extension-manager/package.json | 10 +- .../src/browser/extension-widget.tsx | 6 +- packages/file-search/package.json | 12 +- .../browser/file-search-frontend-module.ts | 9 +- .../browser/quick-file-open-contribution.ts | 10 +- .../src/browser/quick-file-open.ts | 52 +- packages/filesystem/package.json | 6 +- .../src/browser/file-dialog-service.ts | 43 +- .../file-dialog/file-dialog-container.ts | 11 +- .../src/browser/file-dialog/file-dialog.ts | 24 +- .../browser/file-tree/file-tree-widget.tsx | 9 +- packages/git/package.json | 18 +- .../browser/history/git-history-widget.tsx | 359 ++++++++---- packages/git/src/browser/style/history.css | 33 +- packages/git/src/browser/style/index.css | 3 +- packages/git/src/node/dugite-git.spec.ts | 35 ++ packages/git/src/node/dugite-git.ts | 2 +- packages/java/package.json | 14 +- .../java-textmate-contribution.ts | 4 +- packages/json/README.md | 109 ++++ packages/json/compile.tsconfig.json | 10 + packages/json/data/json.tmLanguage.json | 213 ++++++++ packages/json/data/jsonc.tmLanguage.json | 213 ++++++++ packages/json/package.json | 51 ++ .../src/browser/json-client-contribution.ts | 96 ++++ .../json/src/browser/json-frontend-module.ts | 29 + .../src/browser/json-grammar-contribution.ts | 110 ++++ packages/json/src/browser/json-preferences.ts | 103 ++++ packages/json/src/browser/monaco.d.ts | 1 + packages/json/src/common/index.ts | 21 + packages/json/src/node/json-backend-module.ts | 23 + packages/json/src/node/json-contribution.ts | 51 ++ packages/json/src/node/json-starter.ts | 17 + packages/json/src/package.spec.ts | 28 + packages/keymaps/package.json | 11 +- .../src/browser/keymaps-frontend-module.ts | 2 + .../src/browser/monaco-contribution.ts | 25 + packages/keymaps/src/browser/monaco.d.ts | 1 + packages/languages/package.json | 17 +- .../browser/language-client-contribution.ts | 31 +- .../src/browser/language-client-factory.ts | 1 + .../src/browser/languages-frontend-module.ts | 7 +- .../src/browser/workspace-symbols.ts | 39 +- .../src/node/language-server-contribution.ts | 9 +- .../src/node/languages-backend-module.ts | 1 - packages/markers/package.json | 12 +- packages/markers/src/browser/style/index.css | 2 +- packages/merge-conflicts/package.json | 10 +- packages/messages/package.json | 6 +- .../browser/notifications-message-client.ts | 15 +- packages/metrics/package.json | 8 +- packages/mini-browser/package.json | 8 +- packages/monaco/package.json | 20 +- .../src/browser/monaco-bulk-edit-service.ts | 30 + .../src/browser/monaco-command-service.ts | 10 +- .../src/browser/monaco-configurations.ts | 131 +++++ .../src/browser/monaco-editor-provider.ts | 36 +- .../src/browser/monaco-editor-service.ts | 54 +- packages/monaco/src/browser/monaco-editor.ts | 32 +- .../src/browser/monaco-frontend-module.ts | 8 +- packages/monaco/src/browser/monaco-loader.ts | 8 +- .../browser/monaco-outline-contribution.ts | 164 +++--- .../src/browser/monaco-quick-input-service.ts | 2 +- .../src/browser/monaco-quick-open-service.ts | 40 +- .../monaco/src/browser/monaco-workspace.ts | 31 +- .../textmate/monaco-builtin-theme-provider.ts | 105 ---- .../monaco-textmate-builtin-theme-provider.ts | 5 +- .../monaco-textmate-frontend-bindings.ts | 4 +- .../textmate/monaco-textmate-service.ts | 36 +- .../browser/textmate/monaco-theme-registry.ts | 124 +++++ .../browser/textmate/textmate-contribution.ts | 3 +- .../src/browser/textmate/textmate-registry.ts | 23 +- .../textmate-snippet-completion-provider.ts | 62 +++ .../browser/textmate/textmate-tokenizer.ts | 81 +-- packages/monaco/src/typings/monaco/index.d.ts | 99 ++-- packages/navigator/package.json | 10 +- .../src/browser/navigator-container.ts | 7 +- .../browser/navigator-decorator-service.ts | 2 +- .../src/browser/navigator-frontend-module.ts | 11 - .../navigator/src/browser/navigator-model.ts | 31 -- .../src/browser/navigator-widget.tsx | 28 +- .../navigator/src/browser/style/index.css | 2 - packages/outline-view/package.json | 6 +- .../browser/outline-view-frontend-module.ts | 11 +- packages/output/package.json | 6 +- packages/plugin-ext-vscode/package.json | 6 +- .../src/node/plugin-vscode-file-handler.ts | 4 +- .../src/node/plugin-vscode-init.ts | 90 ++- .../src/node/scanner-vscode.ts | 1 + packages/plugin-ext/package.json | 16 +- packages/plugin-ext/src/api/model.ts | 35 ++ packages/plugin-ext/src/api/plugin-api.ts | 76 ++- packages/plugin-ext/src/common/index.ts | 1 + .../plugin-ext/src/common/plugin-protocol.ts | 37 +- packages/plugin-ext/src/common/types.ts | 38 +- .../src/hosted/browser/hosted-plugin.ts | 103 ++-- .../src/hosted/browser/worker/worker-main.ts | 96 +++- .../src/hosted/node/hosted-plugin.ts | 5 + .../plugin-ext/src/hosted/node/plugin-host.ts | 92 ++-- .../node/scanners/backend-init-theia.ts | 70 ++- .../src/hosted/node/scanners/scanner-theia.ts | 67 ++- .../hosted/plugin/hosted-plugin-manager.ts | 50 -- .../src/main/browser/languages-main.ts | 54 +- .../browser/plugin-contribution-handler.ts | 25 +- .../browser/plugin-ext-frontend-module.ts | 10 +- .../src/main/browser/style/index.css | 1 + .../src/main/browser/style/view-registry.css | 106 ++++ .../src/main/browser/view/view-registry.ts | 100 ++++ .../browser/view/views-container-widget.tsx | 78 +++ .../handlers/plugin-theia-file-handler.ts | 4 +- .../plugin-ext/src/main/node/temp-dir-util.ts | 27 + packages/plugin-ext/src/plugin/languages.ts | 167 +----- .../src/plugin/languages/completion.ts | 166 ++++++ .../src/plugin/languages/diagnostics.ts | 324 +++++++++++ .../plugin-ext/src/plugin/plugin-context.ts | 514 ++++++++++-------- .../plugin-ext/src/plugin/plugin-manager.ts | 148 +++++ .../plugin-ext/src/plugin/type-converters.ts | 67 ++- packages/plugin-ext/src/plugin/types-impl.ts | 62 +++ .../plugin-ext/src/typings/monaco/index.d.ts | 2 +- packages/plugin/API.md | 125 ++++- packages/plugin/package.json | 4 +- packages/plugin/src/theia.d.ts | 414 +++++++++++++- packages/preferences/package.json | 15 +- .../abstract-resource-preference-provider.ts | 5 +- .../src/browser/monaco-contribution.ts | 25 + packages/preferences/src/browser/monaco.d.ts | 1 + .../src/browser/preference-frontend-module.ts | 2 + .../src/browser/preference-service.spec.ts | 11 +- .../src/browser/preference-tree-container.ts | 9 +- .../src/browser/preferences-contribution.ts | 47 +- .../src/browser/preferences-decorator.ts | 5 +- .../src/browser/preferences-menu-factory.ts | 8 +- .../src/browser/preferences-tree-widget.ts | 69 ++- packages/preview/package.json | 12 +- .../markdown/markdown-preview-handler.spec.ts | 71 ++- .../markdown/markdown-preview-handler.ts | 40 +- packages/process/package.json | 6 +- .../src/node/multi-ring-buffer.spec.ts | 8 + .../process/src/node/multi-ring-buffer.ts | 8 +- packages/process/src/node/process-manager.ts | 4 +- packages/process/src/node/process.ts | 4 +- packages/process/src/node/raw-process.ts | 3 +- packages/process/src/node/terminal-process.ts | 3 +- packages/python/package.json | 10 +- .../browser/python-grammar-contribution.ts | 4 +- packages/search-in-workspace/package.json | 12 +- ...arch-in-workspace-frontend-contribution.ts | 8 +- .../search-in-workspace-frontend-module.ts | 3 +- ...search-in-workspace-result-tree-widget.tsx | 46 +- .../browser/search-in-workspace-service.ts | 4 +- .../browser/search-in-workspace-widget.tsx | 6 + .../src/browser/styles/index.css | 2 + .../common/search-in-workspace-interface.ts | 2 +- ...ep-search-in-workspace-server.slow-spec.ts | 3 +- .../ripgrep-search-in-workspace-server.ts | 5 +- packages/task/package.json | 16 +- packages/task/src/browser/quick-open-task.ts | 43 +- .../task/src/browser/task-configurations.ts | 4 +- .../src/browser/task-frontend-contribution.ts | 8 +- .../task/src/browser/task-frontend-module.ts | 4 +- packages/task/src/browser/task-service.ts | 3 +- .../src/node/process/process-task-runner.ts | 71 ++- packages/terminal/package.json | 12 +- .../src/browser/terminal-widget-impl.ts | 2 +- .../terminal/src/node/base-terminal-server.ts | 40 +- packages/terminal/src/node/terminal-server.ts | 9 +- packages/textmate-grammars/package.json | 6 +- packages/textmate-grammars/src/browser/bat.ts | 2 +- .../textmate-grammars/src/browser/clojure.ts | 18 +- .../src/browser/coffeescript.ts | 2 +- .../textmate-grammars/src/browser/csharp.ts | 2 +- packages/textmate-grammars/src/browser/css.ts | 2 +- .../textmate-grammars/src/browser/fsharp.ts | 2 +- .../textmate-grammars/src/browser/groovy.ts | 2 +- .../src/browser/handlebars.ts | 4 +- .../textmate-grammars/src/browser/hlsl.ts | 2 +- .../textmate-grammars/src/browser/html.ts | 2 +- packages/textmate-grammars/src/browser/ini.ts | 2 +- .../textmate-grammars/src/browser/less.ts | 2 +- packages/textmate-grammars/src/browser/log.ts | 2 +- packages/textmate-grammars/src/browser/lua.ts | 2 +- .../textmate-grammars/src/browser/make.ts | 2 +- .../textmate-grammars/src/browser/markdown.ts | 2 +- .../src/browser/objective-c.ts | 2 +- .../textmate-grammars/src/browser/perl.ts | 2 +- .../src/browser/powershell.ts | 2 +- packages/textmate-grammars/src/browser/pug.ts | 2 +- packages/textmate-grammars/src/browser/r.ts | 2 +- .../textmate-grammars/src/browser/razor.ts | 2 +- .../src/browser/shaderlab.ts | 2 +- .../textmate-grammars/src/browser/shell.ts | 2 +- packages/textmate-grammars/src/browser/sql.ts | 2 +- .../textmate-grammars/src/browser/swift.ts | 2 +- packages/textmate-grammars/src/browser/vb.ts | 2 +- packages/textmate-grammars/src/browser/xml.ts | 2 +- packages/textmate-grammars/src/browser/xsl.ts | 2 +- .../textmate-grammars/src/browser/yaml.ts | 4 +- .../typescript/data/snippets/typescript.json | 273 ++++++++++ packages/typescript/package.json | 16 +- .../src/browser/javascript-language-config.ts | 6 +- .../browser/typescript-client-contribution.ts | 50 +- .../typescript-frontend-contribution.ts | 47 +- .../src/browser/typescript-frontend-module.ts | 3 + .../src/browser/typescript-language-config.ts | 14 +- .../src/browser/typescript-preferences.ts | 74 +++ .../src/node/typescript-contribution.ts | 1 - packages/userstorage/package.json | 8 +- packages/variable-resolver/package.json | 6 +- packages/workspace/package.json | 12 +- .../src/browser/workspace-commands.ts | 16 +- .../workspace-frontend-contribution.ts | 21 +- .../src/browser/workspace-frontend-module.ts | 14 +- .../src/browser/workspace-preferences.ts | 8 +- yarn.lock | 292 +++------- 298 files changed, 7499 insertions(+), 2636 deletions(-) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 Untitled.txt create mode 100644 packages/core/src/browser/quick-open/prefix-quick-open-service.ts create mode 100644 packages/core/src/browser/quick-open/quick-open-frontend-contribution.ts rename packages/{navigator => core}/src/browser/style/search-box.css (99%) rename packages/{navigator/src/browser => core/src/browser/tree}/fuzzy-search.spec.ts (100%) rename packages/{navigator/src/browser => core/src/browser/tree}/fuzzy-search.ts (100%) rename packages/{navigator/src/browser => core/src/browser/tree}/search-box-debounce.ts (94%) rename packages/{navigator/src/browser => core/src/browser/tree}/search-box.ts (95%) rename packages/{navigator/src/browser/navigator-search.ts => core/src/browser/tree/tree-search.ts} (66%) create mode 100644 packages/cpp/src/browser/cpp-build-configurations-ui.ts create mode 100644 packages/json/README.md create mode 100644 packages/json/compile.tsconfig.json create mode 100644 packages/json/data/json.tmLanguage.json create mode 100644 packages/json/data/jsonc.tmLanguage.json create mode 100644 packages/json/package.json create mode 100644 packages/json/src/browser/json-client-contribution.ts create mode 100644 packages/json/src/browser/json-frontend-module.ts create mode 100644 packages/json/src/browser/json-grammar-contribution.ts create mode 100644 packages/json/src/browser/json-preferences.ts create mode 100644 packages/json/src/browser/monaco.d.ts create mode 100644 packages/json/src/common/index.ts create mode 100644 packages/json/src/node/json-backend-module.ts create mode 100644 packages/json/src/node/json-contribution.ts create mode 100644 packages/json/src/node/json-starter.ts create mode 100644 packages/json/src/package.spec.ts create mode 100644 packages/keymaps/src/browser/monaco-contribution.ts create mode 100644 packages/keymaps/src/browser/monaco.d.ts create mode 100644 packages/monaco/src/browser/monaco-bulk-edit-service.ts create mode 100644 packages/monaco/src/browser/monaco-configurations.ts delete mode 100644 packages/monaco/src/browser/textmate/monaco-builtin-theme-provider.ts create mode 100644 packages/monaco/src/browser/textmate/monaco-theme-registry.ts create mode 100644 packages/monaco/src/browser/textmate/textmate-snippet-completion-provider.ts delete mode 100644 packages/plugin-ext/src/hosted/plugin/hosted-plugin-manager.ts create mode 100644 packages/plugin-ext/src/main/browser/style/view-registry.css create mode 100644 packages/plugin-ext/src/main/browser/view/view-registry.ts create mode 100644 packages/plugin-ext/src/main/browser/view/views-container-widget.tsx create mode 100644 packages/plugin-ext/src/main/node/temp-dir-util.ts create mode 100644 packages/plugin-ext/src/plugin/languages/completion.ts create mode 100644 packages/plugin-ext/src/plugin/languages/diagnostics.ts create mode 100644 packages/plugin-ext/src/plugin/plugin-manager.ts create mode 100644 packages/preferences/src/browser/monaco-contribution.ts create mode 100644 packages/preferences/src/browser/monaco.d.ts create mode 100644 packages/typescript/data/snippets/typescript.json create mode 100644 packages/typescript/src/browser/typescript-preferences.ts diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000000..a267c124a38e3 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1 @@ + diff --git a/README.md b/README.md index 57dcecf40a2ea..7e7ea0f98a022 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Theia - Cloud & Desktop IDE +[![Gitpod - Code Now](https://img.shields.io/badge/Gitpod-code%20now-blue.svg?longCache=true)](https://gitpod.io#https://github.com/theia-ide/theia) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://github.com/theia-ide/theia/labels/help%20wanted) [![Gitter](https://img.shields.io/badge/chat-on%20gitter-blue.svg)](https://gitter.im/theia-ide/theia) [![Build Status](https://travis-ci.org/theia-ide/theia.svg?branch=master)](https://travis-ci.org/theia-ide/theia) @@ -31,9 +32,11 @@ Theia is an extensible platform to develop full-fledged multi-language Cloud & D ## Getting Started Here you can find guides and examples for common scenarios: -- [Develop a new Theia extension](http://www.theia-ide.org/doc/Authoring_Extensions.html) -- Develop a new Theia application for [Cloud](examples/browser/package.json) or [Desktop](examples/electron/package.json) with [Theia CLI](dev-packages/cli/README.md) -- [Run Theia IDE for Web Developers with Docker](https://github.com/theia-ide/theia-apps#theia-docker) +- [Run Theia in Docker](https://github.com/theia-ide/theia-apps#theia-docker) +- [Run Theia in Gitpod - a Theia-based IDE for GitHub](doc/Developing.md#run-the-browser-example-with-gitpod) +- [Develop a Theia application - your own IDE](https://www.theia-ide.org/doc/Composing_Applications.html) +- [Develop a Theia plugin - a VS Code like extension](https://www.theia-ide.org/doc/Authoring_Plugins.html) +- [Develop a Theia extension](http://www.theia-ide.org/doc/Authoring_Extensions.html) - [Package a desktop Theia application with Electron](https://github.com/theia-ide/yangster-electron) ## Contributing diff --git a/Untitled.txt b/Untitled.txt new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/dev-packages/application-manager/package.json b/dev-packages/application-manager/package.json index adaaa870af547..026366d758ca4 100644 --- a/dev-packages/application-manager/package.json +++ b/dev-packages/application-manager/package.json @@ -1,6 +1,6 @@ { "name": "@theia/application-manager", - "version": "0.3.13", + "version": "0.3.14", "description": "Theia application manager API.", "publishConfig": { "access": "public" @@ -29,7 +29,7 @@ "docs": "theiaext docs" }, "dependencies": { - "@theia/application-package": "^0.3.13", + "@theia/application-package": "^0.3.14", "bunyan": "^1.8.10", "circular-dependency-plugin": "^5.0.0", "copy-webpack-plugin": "^4.5.0", @@ -49,7 +49,7 @@ "worker-loader": "^1.1.1" }, "devDependencies": { - "@theia/ext-scripts": "^0.3.13" + "@theia/ext-scripts": "^0.3.14" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/dev-packages/application-manager/src/generator/webpack-generator.ts b/dev-packages/application-manager/src/generator/webpack-generator.ts index 6ef7c564e2ec8..902f31d3663dc 100644 --- a/dev-packages/application-manager/src/generator/webpack-generator.ts +++ b/dev-packages/application-manager/src/generator/webpack-generator.ts @@ -47,7 +47,7 @@ const { mode } = yargs.option('mode', { }).argv; const development = mode === 'development';${this.ifMonaco(() => ` -const monacoEditorCorePath = development ? '${this.resolve('monaco-editor-core', 'dev/vs')}' : '${this.resolve('monaco-editor-core', 'min/vs')}'; +const monacoEditorCorePath = development ? '${this.resolve('@typefox/monaco-editor-core', 'dev/vs')}' : '${this.resolve('@typefox/monaco-editor-core', 'min/vs')}'; const monacoCssLanguagePath = '${this.resolve('monaco-css', 'release/min')}'; const monacoHtmlLanguagePath = '${this.resolve('monaco-html', 'release/min')}';`)} diff --git a/dev-packages/application-package/package.json b/dev-packages/application-package/package.json index 0dca7db97313e..25ca1ff9dd0d9 100644 --- a/dev-packages/application-package/package.json +++ b/dev-packages/application-package/package.json @@ -1,6 +1,6 @@ { "name": "@theia/application-package", - "version": "0.3.13", + "version": "0.3.14", "description": "Theia application package API.", "publishConfig": { "access": "public" @@ -40,7 +40,7 @@ "write-json-file": "^2.2.0" }, "devDependencies": { - "@theia/ext-scripts": "^0.3.13" + "@theia/ext-scripts": "^0.3.14" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/dev-packages/cli/package.json b/dev-packages/cli/package.json index 9b2f56c64f593..2079581cabd5b 100644 --- a/dev-packages/cli/package.json +++ b/dev-packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@theia/cli", - "version": "0.3.13", + "version": "0.3.14", "description": "Theia CLI.", "publishConfig": { "access": "public" @@ -31,6 +31,6 @@ "docs": "echo 'skip'" }, "dependencies": { - "@theia/application-manager": "^0.3.13" + "@theia/application-manager": "^0.3.14" } } diff --git a/dev-packages/ext-scripts/package.json b/dev-packages/ext-scripts/package.json index 03412b47e2d5c..a509f11de9099 100644 --- a/dev-packages/ext-scripts/package.json +++ b/dev-packages/ext-scripts/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "@theia/ext-scripts", - "version": "0.3.13", + "version": "0.3.14", "description": "NPM scripts for Theia packages.", "files": [ "theiaext" diff --git a/doc/Developing.md b/doc/Developing.md index df918dfe82b57..315dcc048f22f 100644 --- a/doc/Developing.md +++ b/doc/Developing.md @@ -14,6 +14,7 @@ For Windows instructions [click here](#building-on-windows). - [**Prerequisites**](#prerequisites) - [**Quick Start**](#quick-start) - [Run with SSL](#run-the-browser-example-with-ssl) + - [Run with Gitpod](#run-the-browser-example-with-gitpod) - [**Clone the repository**](#clone-the-repository) - [**The repository structure**](#the-repository-structure) - [**Build core, extensions and examples packages**](#build-core-extensions-and-examples-packages) @@ -83,6 +84,17 @@ To run the browser example using SSL use: Start your browser on https://localhost:3000. +### Run the browser example with Gitpod + +[Gitpod](http://gitpod.io/) is a Theia-based IDE for GitHub. +You can start by prefixing any GitHub URL in the Theia repository with `gitpod.io#`: +- Open http://gitpod.io#https://github.com/theia-ide/theia to start development with the master branch. +- Gitpod will start a properly configured for Theia development workspace, clone and build the Theia repository. +- After the build is finished, run from the terminal in Gitpod: + + cd examples/browser \ + && yarn run start ../.. --hostname 0.0.0.0 + ## Clone the repository git clone https://github.com/theia-ide/theia diff --git a/examples/browser/package.json b/examples/browser/package.json index 4fbe0603da852..b17c8c485d8cf 100644 --- a/examples/browser/package.json +++ b/examples/browser/package.json @@ -1,45 +1,46 @@ { "private": true, "name": "@theia/example-browser", - "version": "0.3.13", + "version": "0.3.14", "dependencies": { - "@theia/callhierarchy": "^0.3.13", - "@theia/core": "^0.3.13", - "@theia/cpp": "^0.3.13", - "@theia/debug": "^0.3.13", - "@theia/debug-nodejs": "^0.3.13", - "@theia/editor": "^0.3.13", - "@theia/editorconfig": "^0.3.13", - "@theia/extension-manager": "^0.3.13", - "@theia/file-search": "^0.3.13", - "@theia/filesystem": "^0.3.13", - "@theia/git": "^0.3.13", - "@theia/java": "^0.3.13", - "@theia/keymaps": "^0.3.13", - "@theia/languages": "^0.3.13", - "@theia/markers": "^0.3.13", - "@theia/merge-conflicts": "^0.3.13", - "@theia/messages": "^0.3.13", - "@theia/metrics": "^0.3.13", - "@theia/mini-browser": "^0.3.13", - "@theia/monaco": "^0.3.13", - "@theia/navigator": "^0.3.13", - "@theia/outline-view": "^0.3.13", - "@theia/output": "^0.3.13", - "@theia/plugin-ext": "^0.3.13", - "@theia/plugin-ext-vscode": "^0.3.13", - "@theia/preferences": "^0.3.13", - "@theia/preview": "^0.3.13", - "@theia/process": "^0.3.13", - "@theia/python": "^0.3.13", - "@theia/search-in-workspace": "^0.3.13", - "@theia/task": "^0.3.13", - "@theia/terminal": "^0.3.13", - "@theia/textmate-grammars": "^0.3.13", - "@theia/typescript": "^0.3.13", - "@theia/userstorage": "^0.3.13", - "@theia/variable-resolver": "^0.3.13", - "@theia/workspace": "^0.3.13" + "@theia/callhierarchy": "^0.3.14", + "@theia/core": "^0.3.14", + "@theia/cpp": "^0.3.14", + "@theia/debug": "^0.3.14", + "@theia/debug-nodejs": "^0.3.14", + "@theia/editor": "^0.3.14", + "@theia/editorconfig": "^0.3.14", + "@theia/extension-manager": "^0.3.14", + "@theia/file-search": "^0.3.14", + "@theia/filesystem": "^0.3.14", + "@theia/git": "^0.3.14", + "@theia/java": "^0.3.14", + "@theia/json": "^0.3.14", + "@theia/keymaps": "^0.3.14", + "@theia/languages": "^0.3.14", + "@theia/markers": "^0.3.14", + "@theia/merge-conflicts": "^0.3.14", + "@theia/messages": "^0.3.14", + "@theia/metrics": "^0.3.14", + "@theia/mini-browser": "^0.3.14", + "@theia/monaco": "^0.3.14", + "@theia/navigator": "^0.3.14", + "@theia/outline-view": "^0.3.14", + "@theia/output": "^0.3.14", + "@theia/plugin-ext": "^0.3.14", + "@theia/plugin-ext-vscode": "^0.3.14", + "@theia/preferences": "^0.3.14", + "@theia/preview": "^0.3.14", + "@theia/process": "^0.3.14", + "@theia/python": "^0.3.14", + "@theia/search-in-workspace": "^0.3.14", + "@theia/task": "^0.3.14", + "@theia/terminal": "^0.3.14", + "@theia/textmate-grammars": "^0.3.14", + "@theia/typescript": "^0.3.14", + "@theia/userstorage": "^0.3.14", + "@theia/variable-resolver": "^0.3.14", + "@theia/workspace": "^0.3.14" }, "scripts": { "prepare": "yarn run clean && yarn build", @@ -56,6 +57,6 @@ "coverage": "yarn coverage:compile && yarn test && yarn coverage:remap && yarn coverage:report:lcov && yarn coverage:report:html" }, "devDependencies": { - "@theia/cli": "^0.3.13" + "@theia/cli": "^0.3.14" } -} \ No newline at end of file +} diff --git a/examples/browser/test/top-panel/top-panel.ui-spec.ts b/examples/browser/test/top-panel/top-panel.ui-spec.ts index 47c9fefc8e199..d320f4ff0510a 100644 --- a/examples/browser/test/top-panel/top-panel.ui-spec.ts +++ b/examples/browser/test/top-panel/top-panel.ui-spec.ts @@ -245,8 +245,8 @@ describe('theia top panel (menubar)', () => { }); describe('search view UI', () => { - it('should start with search view not visible', () => { - expect(leftPanel.isSearchViewVisible()).to.be.false; + it('should start with search view visible', () => { + expect(leftPanel.isSearchViewVisible()).to.be.true; }); it('search view should toggle-on then toggle-off', () => { if (!leftPanel.isSearchViewVisible()) { diff --git a/examples/browser/wdio.conf.js b/examples/browser/wdio.conf.js index e309a0f56b5c4..77bb5f85016b5 100644 --- a/examples/browser/wdio.conf.js +++ b/examples/browser/wdio.conf.js @@ -2,10 +2,27 @@ const http = require('http'); const path = require('path'); -const port = 3000; -const host = 'localhost'; +const wdioRunnerScript = require.resolve('webdriverio/build/lib/runner.js'); + +/** + * WebdriverIO will execute this current script first to setup the tests, + * and it will re-execute it for every test workers (subprocesses). + * This means that if we are to set a random port for Theia's backend in the master process, + * we have to pass the port value to the child processes. + * This is done via command line arguments: the following lines fetch the port passed + * to the script, it should be set by the master process for the children, + * but you can also specify it manually by doing `yarn test --theia-port 4000` from + * `examples/browser`. + */ +const cliPortKey = '--theia-port'; +const cliPortIndex = process.argv.indexOf(cliPortKey); +const masterPort = cliPortIndex > -1 ? process.argv[cliPortIndex + 1] : 0; // 0 if master +if (typeof masterPort === 'undefined') { + throw new Error(`${cliPortKey} expects a number as following argument`); +} -let server; +const port = masterPort; +const host = 'localhost'; exports.config = { @@ -167,15 +184,16 @@ exports.config = { // // Gets executed once before all workers get launched. onPrepare: function (config, capabilities) { - return require('./src-gen/backend/server')(port, host).then(s => { - server = s; + return require('./src-gen/backend/server')(port, host).then(created => { + this.execArgv = [wdioRunnerScript, cliPortKey, created.address().port]; + this.server = created; }); }, // Gets executed after all workers got shut down and the process is about to exit. It is not // possible to defer the end of the process using a promise. onComplete: function (exitCode) { - if (server) { - server.close(); + if (this.server) { + this.server.close(); } }, // diff --git a/examples/electron/package.json b/examples/electron/package.json index f0c89f2217d26..ba107d0077875 100644 --- a/examples/electron/package.json +++ b/examples/electron/package.json @@ -1,48 +1,49 @@ { "private": true, "name": "@theia/example-electron", - "version": "0.3.13", + "version": "0.3.14", "theia": { "target": "electron" }, "dependencies": { - "@theia/callhierarchy": "^0.3.13", - "@theia/core": "^0.3.13", - "@theia/cpp": "^0.3.13", - "@theia/debug": "^0.3.13", - "@theia/debug-nodejs": "^0.3.13", - "@theia/editor": "^0.3.13", - "@theia/editorconfig": "^0.3.13", - "@theia/extension-manager": "^0.3.13", - "@theia/file-search": "^0.3.13", - "@theia/filesystem": "^0.3.13", - "@theia/git": "^0.3.13", - "@theia/java": "^0.3.13", - "@theia/keymaps": "^0.3.13", - "@theia/languages": "^0.3.13", - "@theia/markers": "^0.3.13", - "@theia/merge-conflicts": "^0.3.13", - "@theia/messages": "^0.3.13", - "@theia/metrics": "^0.3.13", - "@theia/mini-browser": "^0.3.13", - "@theia/monaco": "^0.3.13", - "@theia/navigator": "^0.3.13", - "@theia/outline-view": "^0.3.13", - "@theia/output": "^0.3.13", - "@theia/plugin-ext": "^0.3.13", - "@theia/plugin-ext-vscode": "^0.3.13", - "@theia/preferences": "^0.3.13", - "@theia/preview": "^0.3.13", - "@theia/process": "^0.3.13", - "@theia/python": "^0.3.13", - "@theia/search-in-workspace": "^0.3.13", - "@theia/task": "^0.3.13", - "@theia/terminal": "^0.3.13", - "@theia/textmate-grammars": "^0.3.13", - "@theia/typescript": "^0.3.13", - "@theia/userstorage": "^0.3.13", - "@theia/variable-resolver": "^0.3.13", - "@theia/workspace": "^0.3.13" + "@theia/callhierarchy": "^0.3.14", + "@theia/core": "^0.3.14", + "@theia/cpp": "^0.3.14", + "@theia/debug": "^0.3.14", + "@theia/debug-nodejs": "^0.3.14", + "@theia/editor": "^0.3.14", + "@theia/editorconfig": "^0.3.14", + "@theia/extension-manager": "^0.3.14", + "@theia/file-search": "^0.3.14", + "@theia/filesystem": "^0.3.14", + "@theia/git": "^0.3.14", + "@theia/java": "^0.3.14", + "@theia/json": "^0.3.14", + "@theia/keymaps": "^0.3.14", + "@theia/languages": "^0.3.14", + "@theia/markers": "^0.3.14", + "@theia/merge-conflicts": "^0.3.14", + "@theia/messages": "^0.3.14", + "@theia/metrics": "^0.3.14", + "@theia/mini-browser": "^0.3.14", + "@theia/monaco": "^0.3.14", + "@theia/navigator": "^0.3.14", + "@theia/outline-view": "^0.3.14", + "@theia/output": "^0.3.14", + "@theia/plugin-ext": "^0.3.14", + "@theia/plugin-ext-vscode": "^0.3.14", + "@theia/preferences": "^0.3.14", + "@theia/preview": "^0.3.14", + "@theia/process": "^0.3.14", + "@theia/python": "^0.3.14", + "@theia/search-in-workspace": "^0.3.14", + "@theia/task": "^0.3.14", + "@theia/terminal": "^0.3.14", + "@theia/textmate-grammars": "^0.3.14", + "@theia/typescript": "^0.3.14", + "@theia/userstorage": "^0.3.14", + "@theia/variable-resolver": "^0.3.14", + "@theia/workspace": "^0.3.14" }, "scripts": { "prepare": "yarn run clean && yarn build", @@ -55,6 +56,6 @@ "test:ui": "wdio wdio.conf.js" }, "devDependencies": { - "@theia/cli": "^0.3.13" + "@theia/cli": "^0.3.14" } -} \ No newline at end of file +} diff --git a/lerna.json b/lerna.json index 39887a34dd36f..de262f3361a5c 100644 --- a/lerna.json +++ b/lerna.json @@ -2,7 +2,7 @@ "lerna": "2.2.0", "npmClient": "yarn", "useWorkspaces": true, - "version": "0.3.13", + "version": "0.3.14", "command": { "run": { "stream": true diff --git a/package.json b/package.json index 85905d3be2f92..84b00afdc08cd 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,6 @@ "typescript": "^2.7.2", "uuid": "^3.1.0", "wdio-mocha-framework": "0.5.9", - "wdio-phantomjs-service": "0.2.2", "wdio-selenium-standalone-service": "0.0.8", "wdio-spec-reporter": "0.1.0", "webdriverio": "4.9.2" diff --git a/packages/bunyan/package.json b/packages/bunyan/package.json index 36070ff248abf..ad92f9fccc2a1 100644 --- a/packages/bunyan/package.json +++ b/packages/bunyan/package.json @@ -1,9 +1,9 @@ { "name": "@theia/bunyan", - "version": "0.3.13", + "version": "0.3.14", "description": "Theia - bunyan Logger Extension", "dependencies": { - "@theia/core": "^0.3.13", + "@theia/core": "^0.3.14", "@types/bunyan": "^1.8.0", "bunyan": "^1.8.10" }, @@ -40,7 +40,7 @@ "docs": "theiaext docs" }, "devDependencies": { - "@theia/ext-scripts": "^0.3.13" + "@theia/ext-scripts": "^0.3.14" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/bunyan/src/node/bunyan-logger-server.ts b/packages/bunyan/src/node/bunyan-logger-server.ts index 637185c6a518e..bb078bacaec7a 100644 --- a/packages/bunyan/src/node/bunyan-logger-server.ts +++ b/packages/bunyan/src/node/bunyan-logger-server.ts @@ -17,9 +17,8 @@ import * as bunyan from 'bunyan'; import { inject, injectable, postConstruct } from 'inversify'; import { LoggerWatcher } from '@theia/core/lib/common/logger-watcher'; -import { LogLevel, rootLoggerName } from '@theia/core/lib/common/logger'; import { LogLevelCliContribution } from '@theia/core/lib/node/logger-cli-contribution'; -import { ILoggerServer, ILoggerClient, ILogLevelChangedEvent } from '@theia/core/lib/common/logger-protocol'; +import { ILoggerServer, ILoggerClient, ILogLevelChangedEvent, LogLevel, rootLoggerName } from '@theia/core/lib/common/logger-protocol'; @injectable() export class BunyanLoggerServer implements ILoggerServer { diff --git a/packages/callhierarchy/package.json b/packages/callhierarchy/package.json index 14b6c39e6dd30..9090f60949848 100644 --- a/packages/callhierarchy/package.json +++ b/packages/callhierarchy/package.json @@ -1,12 +1,12 @@ { "name": "@theia/callhierarchy", - "version": "0.3.13", + "version": "0.3.14", "description": "Theia - Call Hierarchy Extension", "dependencies": { - "@theia/core": "^0.3.13", - "@theia/editor": "^0.3.13", - "@theia/languages": "^0.3.13", - "@theia/monaco": "^0.3.13", + "@theia/core": "^0.3.14", + "@theia/editor": "^0.3.14", + "@theia/languages": "^0.3.14", + "@theia/monaco": "^0.3.14", "ts-md5": "^1.2.2" }, "publishConfig": { @@ -42,7 +42,7 @@ "docs": "theiaext docs" }, "devDependencies": { - "@theia/ext-scripts": "^0.3.13" + "@theia/ext-scripts": "^0.3.14" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/core/package.json b/packages/core/package.json index e93dfa37b10fd..c3e87afcdd253 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,12 +1,12 @@ { "name": "@theia/core", - "version": "0.3.13", + "version": "0.3.14", "description": "Theia is a cloud & desktop IDE framework implemented in TypeScript.", "main": "lib/common/index.js", "typings": "lib/common/index.d.ts", "dependencies": { "@phosphor/widgets": "^1.5.0", - "@theia/application-package": "^0.3.13", + "@theia/application-package": "^0.3.14", "@types/body-parser": "^1.16.4", "@types/bunyan": "^1.8.0", "@types/express": "^4.16.0", @@ -80,7 +80,7 @@ "docs": "theiaext docs" }, "devDependencies": { - "@theia/ext-scripts": "^0.3.13" + "@theia/ext-scripts": "^0.3.14" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/core/src/browser/common-frontend-contribution.ts b/packages/core/src/browser/common-frontend-contribution.ts index 2908714e73bd0..7dd662c8da9b2 100644 --- a/packages/core/src/browser/common-frontend-contribution.ts +++ b/packages/core/src/browser/common-frontend-contribution.ts @@ -153,6 +153,11 @@ export namespace CommonCommands { label: 'About' }; + export const OPEN_PREFERENCES: Command = { + id: 'preferences:open', + label: 'Open Preferences' + }; + } export const supportCut = browser.isNative || document.queryCommandSupported('cut'); diff --git a/packages/core/src/browser/dialogs.ts b/packages/core/src/browser/dialogs.ts index 9568167dda83e..72e9c025b82c6 100644 --- a/packages/core/src/browser/dialogs.ts +++ b/packages/core/src/browser/dialogs.ts @@ -24,6 +24,33 @@ export class DialogProps { readonly title: string; } +export type DialogMode = 'open' | 'preview'; + +export type DialogError = string | boolean | { + message: string + result: boolean +}; +export namespace DialogError { + export function getResult(error: DialogError): boolean { + if (typeof error === 'string') { + return !error.length; + } + if (typeof error === 'boolean') { + return error; + } + return error.result; + } + export function getMessage(error: DialogError): string { + if (typeof error === 'string') { + return error; + } + if (typeof error === 'boolean') { + return ''; + } + return error.message; + } +} + @injectable() export abstract class AbstractDialog extends BaseWidget { @@ -39,6 +66,8 @@ export abstract class AbstractDialog extends BaseWidget { protected closeButton: HTMLButtonElement | undefined; protected acceptButton: HTMLButtonElement | undefined; + protected activeElement: HTMLElement | undefined; + constructor( @inject(DialogProps) protected readonly props: DialogProps ) { @@ -139,7 +168,7 @@ export abstract class AbstractDialog extends BaseWidget { if (this.resolve) { return Promise.reject('The dialog is already opened.'); } - + this.activeElement = window.document.activeElement as HTMLElement; return new Promise((resolve, reject) => { this.resolve = resolve; this.reject = reject; @@ -155,26 +184,34 @@ export abstract class AbstractDialog extends BaseWidget { close(): void { if (this.resolve) { + if (this.activeElement) { + this.activeElement.focus(); + } this.resolve(undefined); } - + this.activeElement = undefined; super.close(); } protected onUpdateRequest(msg: Message): void { super.onUpdateRequest(msg); - if (this.resolve) { - const value = this.value; - const error = this.isValid(value); - this.setErrorMessage(error); + this.validate(); + } + + protected validate(): void { + if (!this.resolve) { + return; } + const value = this.value; + const error = this.isValid(value, 'preview'); + this.setErrorMessage(error); } protected accept(): void { if (this.resolve) { const value = this.value; - const error = this.isValid(value); - if (error) { + const error = this.isValid(value, 'open'); + if (!DialogError.getResult(error)) { this.setErrorMessage(error); } else { this.resolve(value); @@ -185,15 +222,18 @@ export abstract class AbstractDialog extends BaseWidget { abstract get value(): T; - isValid(value: T): string { + /** + * Return a string of zero-length or true if valid. + */ + protected isValid(value: T, mode: DialogMode): DialogError { return ''; } - protected setErrorMessage(error: string) { + protected setErrorMessage(error: DialogError): void { if (this.acceptButton) { - this.acceptButton.disabled = !!error; + this.acceptButton.disabled = !DialogError.getResult(error); } - this.errorMessageNode.innerHTML = error; + this.errorMessageNode.innerHTML = DialogError.getMessage(error); } protected addCloseAction(element: HTMLElement, ...additionalEventTypes: K[]): void { @@ -252,7 +292,12 @@ export class ConfirmDialog extends AbstractDialog { export class SingleTextInputDialogProps extends DialogProps { readonly confirmButtonLabel?: string; readonly initialValue?: string; - readonly validate?: (input: string) => string; + readonly initialSelectionRange?: { + start: number + end: number + direction?: 'forward' | 'backward' | 'none' + }; + readonly validate?: (input: string, mode: DialogMode) => DialogError; } export class SingleTextInputDialog extends AbstractDialog { @@ -268,6 +313,15 @@ export class SingleTextInputDialog extends AbstractDialog { this.inputField.type = 'text'; this.inputField.setAttribute('style', 'flex: 0;'); this.inputField.value = props.initialValue || ''; + if (props.initialSelectionRange) { + this.inputField.setSelectionRange( + props.initialSelectionRange.start, + props.initialSelectionRange.end, + props.initialSelectionRange.direction + ); + } else { + this.inputField.select(); + } this.contentNode.appendChild(this.inputField); this.appendAcceptButton(props.confirmButtonLabel); @@ -277,11 +331,11 @@ export class SingleTextInputDialog extends AbstractDialog { return this.inputField.value; } - isValid(value: string): string { + protected isValid(value: string, mode: DialogMode): DialogError { if (this.props.validate) { - return this.props.validate(value); + return this.props.validate(value, mode); } - return super.isValid(value); + return super.isValid(value, mode); } protected onAfterAttach(msg: Message): void { @@ -291,7 +345,6 @@ export class SingleTextInputDialog extends AbstractDialog { protected onActivateRequest(msg: Message): void { this.inputField.focus(); - this.inputField.select(); } } diff --git a/packages/core/src/browser/frontend-application-module.ts b/packages/core/src/browser/frontend-application-module.ts index 5d4f44066d320..4afc48d4e4d49 100644 --- a/packages/core/src/browser/frontend-application-module.ts +++ b/packages/core/src/browser/frontend-application-module.ts @@ -33,7 +33,10 @@ import { FrontendApplication, FrontendApplicationContribution, DefaultFrontendAp import { DefaultOpenerService, OpenerService, OpenHandler } from './opener-service'; import { HttpOpenHandler } from './http-open-handler'; import { CommonFrontendContribution } from './common-frontend-contribution'; -import { QuickOpenService, QuickCommandService, QuickCommandFrontendContribution, QuickPickService } from './quick-open'; +import { + QuickOpenService, QuickCommandService, QuickCommandFrontendContribution, QuickPickService, QuickOpenContribution, + QuickOpenHandlerRegistry, CommandQuickOpenContribution, HelpQuickOpenHandler, QuickOpenFrontendContribution, PrefixQuickOpenService +} from './quick-open'; import { LocalStorageService, StorageService } from './storage-service'; import { WidgetFactory, WidgetManager } from './widget-manager'; import { @@ -44,8 +47,8 @@ import { StatusBar, StatusBarImpl } from './status-bar/status-bar'; import { LabelParser } from './label-parser'; import { LabelProvider, LabelProviderContribution, DefaultUriLabelProviderContribution } from './label-provider'; import { - PreferenceProviders, PreferenceProvider, PreferenceScope, PreferenceService, - PreferenceServiceImpl, PreferenceSchemaProvider, PreferenceContribution + PreferenceProviderProvider, PreferenceProvider, PreferenceScope, PreferenceService, + PreferenceServiceImpl, bindPreferenceSchemaProvider } from './preferences'; import { ContextMenuRenderer } from './context-menu-renderer'; import { ThemingCommandContribution, ThemeService, BuiltinThemeProvider } from './theming'; @@ -101,6 +104,7 @@ export const frontendApplicationModule = new ContainerModule((bind, unbind, isBo bind(CommandRegistry).toSelf().inSingletonScope(); bind(CommandService).toDynamicValue(context => context.container.get(CommandRegistry)); bindContributionProvider(bind, CommandContribution); + bind(QuickOpenContribution).to(CommandQuickOpenContribution); bind(MenuModelRegistry).toSelf().inSingletonScope(); bindContributionProvider(bind, MenuContribution); @@ -125,6 +129,15 @@ export const frontendApplicationModule = new ContainerModule((bind, unbind, isBo bind(serviceIdentifier).toDynamicValue(ctx => ctx.container.get(QuickCommandFrontendContribution)).inSingletonScope() ); + bind(PrefixQuickOpenService).toSelf().inSingletonScope(); + bindContributionProvider(bind, QuickOpenContribution); + bind(QuickOpenHandlerRegistry).toSelf().inSingletonScope(); + bind(QuickOpenFrontendContribution).toSelf().inSingletonScope(); + bind(FrontendApplicationContribution).toService(QuickOpenFrontendContribution); + + bind(HelpQuickOpenHandler).toSelf().inSingletonScope(); + bind(QuickOpenContribution).toService(HelpQuickOpenHandler); + bind(LocalStorageService).toSelf().inSingletonScope(); bind(StorageService).toService(LocalStorageService); @@ -139,13 +152,11 @@ export const frontendApplicationModule = new ContainerModule((bind, unbind, isBo bind(PreferenceProvider).toSelf().inSingletonScope().whenTargetNamed(PreferenceScope.User); bind(PreferenceProvider).toSelf().inSingletonScope().whenTargetNamed(PreferenceScope.Workspace); - bind(PreferenceProviders).toFactory(ctx => (scope: PreferenceScope) => ctx.container.getNamed(PreferenceProvider, scope)); + bind(PreferenceProviderProvider).toFactory(ctx => (scope: PreferenceScope) => ctx.container.getNamed(PreferenceProvider, scope)); bind(PreferenceServiceImpl).toSelf().inSingletonScope(); - for (const serviceIdentifier of [PreferenceService, FrontendApplicationContribution]) { - bind(serviceIdentifier).toDynamicValue(ctx => ctx.container.get(PreferenceServiceImpl)).inSingletonScope(); - } - bind(PreferenceSchemaProvider).toSelf().inSingletonScope(); - bindContributionProvider(bind, PreferenceContribution); + bind(PreferenceService).toService(PreferenceServiceImpl); + bind(FrontendApplicationContribution).toService(PreferenceServiceImpl); + bindPreferenceSchemaProvider(bind); bind(PingService).toDynamicValue(ctx => { // let's reuse a simple and cheap service from this package diff --git a/packages/core/src/browser/http-open-handler.ts b/packages/core/src/browser/http-open-handler.ts index 619f40c64544c..4f753ec4309d8 100644 --- a/packages/core/src/browser/http-open-handler.ts +++ b/packages/core/src/browser/http-open-handler.ts @@ -28,7 +28,7 @@ export class HttpOpenHandler implements OpenHandler { } open(uri: URI): Window | undefined { - return window.open(uri.toString()) || undefined; + return window.open(uri.toString(true)) || undefined; } } diff --git a/packages/core/src/browser/logger-frontend-module.ts b/packages/core/src/browser/logger-frontend-module.ts index a9e6ffb960908..314d2dbfa5a37 100644 --- a/packages/core/src/browser/logger-frontend-module.ts +++ b/packages/core/src/browser/logger-frontend-module.ts @@ -15,7 +15,7 @@ ********************************************************************************/ import { ContainerModule, Container } from 'inversify'; -import { ILoggerServer, loggerPath } from '../common/logger-protocol'; +import { ILoggerServer, loggerPath, ConsoleLogger } from '../common/logger-protocol'; import { ILogger, Logger, LoggerFactory, setRootLogger, LoggerName, rootLoggerName } from '../common/logger'; import { LoggerWatcher } from '../common/logger-watcher'; import { WebSocketConnectionProvider } from './messaging'; @@ -34,7 +34,17 @@ export const loggerFrontendModule = new ContainerModule(bind => { bind(ILoggerServer).toDynamicValue(ctx => { const loggerWatcher = ctx.container.get(LoggerWatcher); const connection = ctx.container.get(WebSocketConnectionProvider); - return connection.createProxy(loggerPath, loggerWatcher.getLoggerClient()); + const target = connection.createProxy(loggerPath, loggerWatcher.getLoggerClient()); + function get(_: ILoggerServer, property: K): ILoggerServer[K] { + if (property === 'log') { + return (name, logLevel, message, params) => { + ConsoleLogger.log(name, logLevel, message, params); + return target.log(name, logLevel, message, params); + }; + } + return target[property]; + } + return new Proxy(target, { get }); }).inSingletonScope(); bind(LoggerFactory).toFactory(ctx => (name: string) => { diff --git a/packages/core/src/browser/menu/browser-menu-plugin.ts b/packages/core/src/browser/menu/browser-menu-plugin.ts index 35f6df363538c..4b445bf410f1f 100644 --- a/packages/core/src/browser/menu/browser-menu-plugin.ts +++ b/packages/core/src/browser/menu/browser-menu-plugin.ts @@ -149,7 +149,7 @@ class DynamicMenuWidget extends MenuWidget { private updateSubMenus(parent: MenuWidget, menu: CompositeMenuNode, commands: PhosphorCommandRegistry): void { for (const item of menu.children) { if (item instanceof CompositeMenuNode) { - if (item.label) { + if (item.label && item.children.length > 0) { parent.addItem({ type: 'submenu', submenu: new DynamicMenuWidget(item, this.options) diff --git a/packages/core/src/browser/preferences/preference-contribution.ts b/packages/core/src/browser/preferences/preference-contribution.ts index fb7aef0f85f57..97d8c5461fa0b 100644 --- a/packages/core/src/browser/preferences/preference-contribution.ts +++ b/packages/core/src/browser/preferences/preference-contribution.ts @@ -14,8 +14,12 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { inject, injectable, named } from 'inversify'; -import { ContributionProvider, ILogger } from '../../common'; +import * as Ajv from 'ajv'; +import { inject, injectable, named, interfaces } from 'inversify'; +import { ContributionProvider, bindContributionProvider } from '../../common'; +import { PreferenceProvider } from './preference-provider'; + +// tslint:disable:no-any export const PreferenceContribution = Symbol('PreferenceContribution'); export interface PreferenceContribution { @@ -45,27 +49,55 @@ export interface PreferenceProperty { export type JsonType = 'string' | 'array' | 'number' | 'integer' | 'object' | 'boolean' | 'null'; +export function bindPreferenceSchemaProvider(bind: interfaces.Bind): void { + bind(PreferenceSchemaProvider).toSelf().inSingletonScope(); + bindContributionProvider(bind, PreferenceContribution); +} + @injectable() -export class PreferenceSchemaProvider { - protected readonly combinedSchema: PreferenceSchema = {properties: {}}; +export class PreferenceSchemaProvider extends PreferenceProvider { + + protected readonly combinedSchema: PreferenceSchema = { properties: {} }; + protected readonly preferences: { [name: string]: any } = {}; + protected readonly validateFunction: Ajv.ValidateFunction; constructor( - @inject(ILogger) protected readonly logger: ILogger, @inject(ContributionProvider) @named(PreferenceContribution) protected readonly preferenceContributions: ContributionProvider ) { + super(); + const schema = this.combinedSchema; this.preferenceContributions.getContributions().forEach(contrib => { for (const property in contrib.schema.properties) { - if (this.combinedSchema.properties[property]) { - this.logger.error('Preference name collision detected in the schema for property: ' + property); + if (schema.properties[property]) { + console.error('Preference name collision detected in the schema for property: ' + property); } else { - this.combinedSchema.properties[property] = contrib.schema.properties[property]; + schema.properties[property] = contrib.schema.properties[property]; } } }); + this.validateFunction = new Ajv().compile(schema); + // tslint:disable-next-line:forin + for (const property in schema.properties) { + this.preferences[property] = schema.properties[property].default; + } + this._ready.resolve(); + } + + validate(name: string, value: any): boolean { + return this.validateFunction({ [name]: value }) as boolean; } getCombinedSchema(): PreferenceSchema { return this.combinedSchema; } + + getPreferences(): { [name: string]: any } { + return this.preferences; + } + + async setPreference(): Promise { + throw new Error('Unsupported'); + } + } diff --git a/packages/core/src/browser/preferences/preference-provider.ts b/packages/core/src/browser/preferences/preference-provider.ts index b9d3a246e1e47..d29d0e7473fbf 100644 --- a/packages/core/src/browser/preferences/preference-provider.ts +++ b/packages/core/src/browser/preferences/preference-provider.ts @@ -14,6 +14,8 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ +// tslint:disable:no-any + import { injectable } from 'inversify'; import { Disposable, DisposableCollection, Emitter, Event } from '../../common'; import { Deferred } from '../../common/promise-util'; diff --git a/packages/core/src/browser/preferences/preference-proxy.ts b/packages/core/src/browser/preferences/preference-proxy.ts index badb598597ca4..6986bcae35a14 100644 --- a/packages/core/src/browser/preferences/preference-proxy.ts +++ b/packages/core/src/browser/preferences/preference-proxy.ts @@ -16,14 +16,10 @@ // tslint:disable:no-any -import { Disposable, DisposableCollection, Event, Emitter, deepFreeze } from '../../common'; +import { Disposable, DisposableCollection, Event, Emitter } from '../../common'; import { PreferenceService, PreferenceChange } from './preference-service'; import { PreferenceSchema } from './preference-contribution'; -import * as Ajv from 'ajv'; -export interface Configuration { - [preferenceName: string]: any; -} export interface PreferenceChangeEvent { readonly preferenceName: keyof T readonly newValue?: T[keyof T] @@ -35,46 +31,22 @@ export interface PreferenceEventEmitter { } export type PreferenceProxy = Readonly & Disposable & PreferenceEventEmitter; -export function createPreferenceProxy(preferences: PreferenceService, schema: PreferenceSchema): PreferenceProxy { - const configuration = createConfiguration(schema); - const ajv = new Ajv(); - const validateFunction = ajv.compile(schema); - const validate = (name: string, value: any) => validateFunction({ [name]: value }); +export function createPreferenceProxy(preferences: PreferenceService, schema: PreferenceSchema): PreferenceProxy { const toDispose = new DisposableCollection(); const onPreferenceChangedEmitter = new Emitter(); toDispose.push(onPreferenceChangedEmitter); toDispose.push(preferences.onPreferenceChanged(e => { - if (e.preferenceName in configuration) { - if (e.newValue !== undefined) { - if (validate(e.preferenceName, e.newValue)) { - onPreferenceChangedEmitter.fire(e); - } else { - onPreferenceChangedEmitter.fire({ - preferenceName: e.preferenceName, - newValue: configuration[e.preferenceName] - }); - } - } else { - onPreferenceChangedEmitter.fire({ - preferenceName: e.preferenceName, - newValue: configuration[e.preferenceName], - oldValue: e.oldValue - }); - } + if (schema.properties[e.preferenceName]) { + onPreferenceChangedEmitter.fire(e); } })); - const unsupportedOperation = (_: any, property: string) => { + const unsupportedOperation = (_: any, __: string) => { throw new Error('Unsupported operation'); }; - return new Proxy(configuration as any, { + return new Proxy({}, { get: (_, property: string) => { - if (property in configuration) { - const preference = preferences.get(property, configuration[property]); - if (validate(property, preference)) { - return preference; - } else { - return configuration[property]; - } + if (schema.properties[property]) { + return preferences.get(property); } if (property === 'onPreferenceChanged') { return onPreferenceChangedEmitter.event; @@ -87,17 +59,18 @@ export function createPreferenceProxy(preferences: Pref } throw new Error('unexpected property: ' + property); }, + ownKeys: () => Object.keys(schema.properties), + getOwnPropertyDescriptor: (_, property: string) => { + if (schema.properties[property]) { + return { + enumerable: true, + configurable: true + }; + } + return {}; + }, set: unsupportedOperation, deleteProperty: unsupportedOperation, defineProperty: unsupportedOperation }); } - -function createConfiguration(schema: PreferenceSchema): T { - const configuration = {} as T; - // tslint:disable-next-line:forin - for (const property in schema.properties) { - configuration[property] = deepFreeze(schema.properties[property].default); - } - return configuration; -} diff --git a/packages/core/src/browser/preferences/preference-service.ts b/packages/core/src/browser/preferences/preference-service.ts index ee611078f9532..858dc360c2c54 100644 --- a/packages/core/src/browser/preferences/preference-service.ts +++ b/packages/core/src/browser/preferences/preference-service.ts @@ -14,11 +14,15 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ +// tslint:disable:no-any + import { JSONExt } from '@phosphor/coreutils'; -import { injectable, inject } from 'inversify'; +import { injectable, inject, postConstruct } from 'inversify'; import { FrontendApplicationContribution } from '../../browser'; import { Event, Emitter, DisposableCollection, Disposable, deepFreeze } from '../../common'; +import { Deferred } from '../../common/promise-util'; import { PreferenceProvider } from './preference-provider'; +import { PreferenceSchemaProvider } from './preference-contribution'; export enum PreferenceScope { User, @@ -35,6 +39,10 @@ export interface PreferenceChange { readonly oldValue?: any; } +export interface PreferenceChanges { + [preferenceName: string]: PreferenceChange +} + export const PreferenceService = Symbol('PreferenceService'); export interface PreferenceService extends Disposable { readonly ready: Promise; @@ -45,104 +53,123 @@ export interface PreferenceService extends Disposable { onPreferenceChanged: Event; } -export const PreferenceProviders = Symbol('PreferenceProviders'); -export type PreferenceProviders = (scope: PreferenceScope) => PreferenceProvider; +/** + * We cannot load providers directly in the case if they depend on `PreferenceService` somehow. + * It allows to load them lazilly after DI is configured. + */ +export const PreferenceProviderProvider = Symbol('PreferenceProviderProvider'); +export type PreferenceProviderProvider = (scope: PreferenceScope) => PreferenceProvider; @injectable() export class PreferenceServiceImpl implements PreferenceService, FrontendApplicationContribution { protected preferences: { [key: string]: any } = {}; - protected readonly toDispose = new DisposableCollection(); protected readonly onPreferenceChangedEmitter = new Emitter(); readonly onPreferenceChanged = this.onPreferenceChangedEmitter.event; - @inject(PreferenceProviders) - protected readonly getPreferenceProvider: PreferenceProviders; + protected readonly onPreferencesChangedEmitter = new Emitter(); + readonly onPreferencesChanged = this.onPreferencesChangedEmitter.event; - constructor() { - this.toDispose.push(this.onPreferenceChangedEmitter); - } + protected readonly toDispose = new DisposableCollection(this.onPreferenceChangedEmitter, this.onPreferencesChangedEmitter); - protected _preferenceProviders: PreferenceProvider[] | undefined; - protected get preferenceProviders(): PreferenceProvider[] { - if (!this._preferenceProviders) { - this._preferenceProviders = [ - this.getPreferenceProvider(PreferenceScope.User), - this.getPreferenceProvider(PreferenceScope.Workspace) - ]; - } - return this._preferenceProviders; + @inject(PreferenceSchemaProvider) + protected readonly schema: PreferenceSchemaProvider; + + @inject(PreferenceProviderProvider) + protected readonly providerProvider: PreferenceProviderProvider; + + protected readonly providers: PreferenceProvider[] = []; + + @postConstruct() + protected init(): void { + this.toDispose.push(Disposable.create(() => this._ready.reject())); + this.providers.push(this.schema); + this.preferences = this.parsePreferences(); } - onStart() { - // tslint:disable-next-line:no-unused-expression - this.ready; + dispose(): void { + this.toDispose.dispose(); } - protected _ready: Promise | undefined; + protected readonly _ready = new Deferred(); get ready(): Promise { - if (!this._ready) { - this._ready = new Promise(async (resolve, reject) => { - this.toDispose.push(Disposable.create(() => reject())); - for (const preferenceProvider of this.preferenceProviders) { - this.toDispose.push(preferenceProvider); - preferenceProvider.onDidPreferencesChanged(event => this.reconcilePreferences()); - } - - // Wait until all the providers are ready to provide preferences. - await Promise.all(this.preferenceProviders.map(p => p.ready)); + return this._ready.promise; + } - this.reconcilePreferences(); - resolve(); - }); + initialize(): void { + this.initializeProviders(); + } + protected async initializeProviders(): Promise { + try { + const providers = this.createProviders(); + this.toDispose.pushAll(providers); + await Promise.all(providers.map(p => p.ready)); + if (this.toDispose.disposed) { + return; + } + this.providers.push(...providers); + for (const provider of providers) { + provider.onDidPreferencesChanged(_ => this.reconcilePreferences()); + } + this.reconcilePreferences(); + this._ready.resolve(); + } catch (e) { + this._ready.reject(e); } - return this._ready; } - - dispose(): void { - this.toDispose.dispose(); + protected createProviders(): PreferenceProvider[] { + return [ + this.providerProvider(PreferenceScope.User), + this.providerProvider(PreferenceScope.Workspace) + ]; } protected reconcilePreferences(): void { - const preferenceChanges: { [preferenceName: string]: PreferenceChange } = {}; + const changes: PreferenceChanges = {}; const deleted = new Set(Object.keys(this.preferences)); - - for (const preferenceProvider of this.preferenceProviders) { - const preferences = preferenceProvider.getPreferences(); - // tslint:disable-next-line:forin - for (const preferenceName in preferences) { - deleted.delete(preferenceName); - const oldValue = this.preferences[preferenceName]; - const newValue = deepFreeze(preferences[preferenceName]); - if (oldValue !== undefined) { - /* Value changed */ - if (!JSONExt.deepEqual(oldValue, newValue)) { - preferenceChanges[preferenceName] = { preferenceName, newValue, oldValue }; - this.preferences[preferenceName] = newValue; - } - /* Value didn't change - Do nothing */ - } else { - /* New value without old value */ - preferenceChanges[preferenceName] = { preferenceName, newValue }; - this.preferences[preferenceName] = newValue; + const preferences = this.parsePreferences(); + // tslint:disable-next-line:forin + for (const preferenceName in preferences) { + deleted.delete(preferenceName); + const oldValue = this.preferences[preferenceName]; + const newValue = preferences[preferenceName]; + if (oldValue !== undefined) { + if (!JSONExt.deepEqual(oldValue, newValue)) { + changes[preferenceName] = { preferenceName, newValue, oldValue }; + this.preferences[preferenceName] = deepFreeze(newValue); } + } else { + changes[preferenceName] = { preferenceName, newValue }; + this.preferences[preferenceName] = deepFreeze(newValue); } } - - /* Deleted values */ for (const preferenceName of deleted) { const oldValue = this.preferences[preferenceName]; - preferenceChanges[preferenceName] = { preferenceName, oldValue }; + changes[preferenceName] = { preferenceName, oldValue }; this.preferences[preferenceName] = undefined; } + this.onPreferencesChangedEmitter.fire(changes); // tslint:disable-next-line:forin - for (const preferenceName in preferenceChanges) { - this.onPreferenceChangedEmitter.fire(preferenceChanges[preferenceName]); + for (const preferenceName in changes) { + this.onPreferenceChangedEmitter.fire(changes[preferenceName]); + } + } + protected parsePreferences(): { [name: string]: any } { + const result: { [name: string]: any } = {}; + for (const provider of this.providers) { + const preferences = provider.getPreferences(); + // tslint:disable-next-line:forin + for (const preferenceName in preferences) { + if (this.schema.validate(preferenceName, preferences[preferenceName])) { + result[preferenceName] = preferences[preferenceName]; + } + } } + return result; } - getPreferences(): { [key: string]: any } { + getPreferences(): { [key: string]: Object | undefined } { return this.preferences; } @@ -158,7 +185,7 @@ export class PreferenceServiceImpl implements PreferenceService, FrontendApplica } set(preferenceName: string, value: any, scope: PreferenceScope = PreferenceScope.User): Promise { - return this.getPreferenceProvider(scope).setPreference(preferenceName, value); + return this.providerProvider(scope).setPreference(preferenceName, value); } getBoolean(preferenceName: string): boolean | undefined; diff --git a/packages/core/src/browser/quick-open/index.ts b/packages/core/src/browser/quick-open/index.ts index 618473e53a316..857e63e35dd6b 100644 --- a/packages/core/src/browser/quick-open/index.ts +++ b/packages/core/src/browser/quick-open/index.ts @@ -19,3 +19,5 @@ export * from './quick-open-service'; export * from './quick-pick-service'; export * from './quick-command-service'; export * from './quick-command-contribution'; +export * from './quick-open-frontend-contribution'; +export * from './prefix-quick-open-service'; diff --git a/packages/core/src/browser/quick-open/prefix-quick-open-service.ts b/packages/core/src/browser/quick-open/prefix-quick-open-service.ts new file mode 100644 index 0000000000000..a51f1d392f8c1 --- /dev/null +++ b/packages/core/src/browser/quick-open/prefix-quick-open-service.ts @@ -0,0 +1,264 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. 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 + ********************************************************************************/ + +import { injectable, inject } from 'inversify'; +import { QuickOpenModel, QuickOpenItem, QuickOpenMode } from './quick-open-model'; +import { QuickOpenService, QuickOpenOptions } from './quick-open-service'; +import { Disposable, DisposableCollection } from '../../common/disposable'; +import { ILogger } from '../../common/logger'; + +export const QuickOpenContribution = Symbol('QuickOpenContribution'); +/** + * The quick open contribution should be implemented to register custom quick open handler. + */ +export interface QuickOpenContribution { + registerQuickOpenHandlers(handlers: QuickOpenHandlerRegistry): void; +} + +/** + * A handler allows to call it's quick open model when + * the handler's prefix is typed in the quick open widget. + */ +export interface QuickOpenHandler { + + /** + * Prefix to trigger this handler's model. + */ + readonly prefix: string; + + /** + * A human-readable description of this handler. + */ + readonly description: string; + + /** + * Called immediately when the user's input in + * the quick open widget matches this handler's prefix. + * Allows to initialize the model with some initial data. + */ + init?(): void; + + /** + * A model that should be used by the quick open widget when this handler's prefix is used. + */ + getModel(): QuickOpenModel; + + /** + * Returns the options which should be used for the quick open widget. + * Note, that the `prefix` and `skipPrefix` options are ignored and will be overridden. + * The `placeholder` option makes sense for a default handler only since it's used without a prefix in quick open widget. + */ + getOptions(): QuickOpenOptions; +} + +@injectable() +export class QuickOpenHandlerRegistry implements Disposable { + + protected readonly handlers: Map = new Map(); + protected readonly toDispose = new DisposableCollection(); + protected defaultHandler: QuickOpenHandler | undefined; + + @inject(ILogger) + protected readonly logger: ILogger; + + /** + * Register the given handler. + * Do nothing if a handler is already registered. + * @param handler the handler to register + * @param defaultHandler default means that a handler is used when the user's + * input in the quick open widget doesn't match any of known prefixes + */ + registerHandler(handler: QuickOpenHandler, defaultHandler: boolean = false): Disposable { + if (this.handlers.has(handler.prefix)) { + this.logger.warn(`A handler with prefix ${handler.prefix} is already registered.`); + return Disposable.NULL; + } + this.handlers.set(handler.prefix, handler); + const disposable = { + dispose: () => this.handlers.delete(handler.prefix) + }; + this.toDispose.push(disposable); + if (defaultHandler) { + this.defaultHandler = handler; + } + return disposable; + } + + getDefaultHandler(): QuickOpenHandler | undefined { + return this.defaultHandler; + } + + isDefaultHandler(handler: QuickOpenHandler): boolean { + return handler === this.getDefaultHandler(); + } + + /** + * Return all registered handlers. + */ + getHandlers(): QuickOpenHandler[] { + return [...this.handlers.values()]; + } + + /** + * Return a handler that matches the given text or the default handler if none. + */ + getHandlerOrDefault(text: string): QuickOpenHandler | undefined { + for (const handler of this.handlers.values()) { + if (text.startsWith(handler.prefix)) { + return handler; + } + } + return this.getDefaultHandler(); + } + + dispose(): void { + this.toDispose.dispose(); + } +} + +/** Prefix-based quick open service. */ +@injectable() +export class PrefixQuickOpenService { + + @inject(QuickOpenHandlerRegistry) + protected readonly handlers: QuickOpenHandlerRegistry; + + @inject(QuickOpenService) + protected readonly quickOpenService: QuickOpenService; + + /** + * Opens a quick open widget with the model that handles the known prefixes. + * @param prefix string that may contain a prefix of some of the known quick open handlers. + * A default quick open handler will be called if the provided string doesn't match any. + * An empty quick open will be opened if there's no default handler registered. + */ + open(prefix: string): void { + const handler = this.handlers.getHandlerOrDefault(prefix); + this.setCurrentHandler(prefix, handler); + } + + protected toDisposeCurrent = new DisposableCollection(); + protected currentHandler: QuickOpenHandler | undefined; + + protected async setCurrentHandler(prefix: string, handler: QuickOpenHandler | undefined): Promise { + if (handler !== this.currentHandler) { + this.toDisposeCurrent.dispose(); + this.currentHandler = handler; + this.toDisposeCurrent.push(Disposable.create(() => { + const closingHandler = handler && handler.getOptions().onClose; + if (closingHandler) { + closingHandler(true); + } + })); + } + if (!handler) { + this.doOpen(); + return; + } + if (handler.init) { + await handler.init(); + } + let optionsPrefix = prefix; + if (this.handlers.isDefaultHandler(handler) && prefix.startsWith(handler.prefix)) { + optionsPrefix = prefix.substr(handler.prefix.length); + } + const skipPrefix = this.handlers.isDefaultHandler(handler) ? 0 : handler.prefix.length; + const handlerOptions = handler.getOptions(); + this.doOpen({ + prefix: optionsPrefix, + placeholder: "Type '?' to get help on the actions you can take from here", + skipPrefix, + ...handlerOptions + }); + } + + protected doOpen(options?: QuickOpenOptions): void { + this.quickOpenService.open({ + onType: (lookFor, acceptor) => this.onType(lookFor, acceptor) + }, options); + } + + protected onType(lookFor: string, acceptor: (items: QuickOpenItem[]) => void): void { + const handler = this.handlers.getHandlerOrDefault(lookFor); + if (handler === undefined) { + const items: QuickOpenItem[] = []; + items.push(new QuickOpenItem({ + label: lookFor.length === 0 ? 'No default handler is registered' : `No handlers matches the prefix ${lookFor} and no default handler is registered.` + })); + acceptor(items); + } else if (handler !== this.currentHandler) { + this.setCurrentHandler(lookFor, handler); + } else { + const handlerModel = handler.getModel(); + const searchValue = this.handlers.isDefaultHandler(handler) ? lookFor : lookFor.substr(handler.prefix.length); + handlerModel.onType(searchValue, items => acceptor(items)); + } + } + +} + +@injectable() +export class HelpQuickOpenHandler implements QuickOpenHandler, QuickOpenContribution { + + readonly prefix: string = '?'; + readonly description: string = ''; + protected items: QuickOpenItem[]; + + @inject(QuickOpenHandlerRegistry) + protected readonly handlers: QuickOpenHandlerRegistry; + + @inject(PrefixQuickOpenService) + protected readonly quickOpenService: PrefixQuickOpenService; + + init(): void { + this.items = this.handlers.getHandlers() + .filter(handler => handler.prefix !== this.prefix) + .map(handler => new QuickOpenItem({ + label: handler.prefix, + description: handler.description, + run: (mode: QuickOpenMode) => { + if (mode !== QuickOpenMode.OPEN) { + return false; + } + this.quickOpenService.open(handler.prefix); + return false; + } + })); + + if (this.items.length === 0) { + this.items.push(new QuickOpenItem({ + label: 'No handlers registered', + run: () => false + })); + } + } + + getModel(): QuickOpenModel { + return { + onType: (lookFor: string, acceptor: (items: QuickOpenItem[]) => void) => { + acceptor(this.items); + } + }; + } + + getOptions(): QuickOpenOptions { + return {}; + } + + registerQuickOpenHandlers(handlers: QuickOpenHandlerRegistry): void { + handlers.registerHandler(this); + } +} diff --git a/packages/core/src/browser/quick-open/quick-command-contribution.ts b/packages/core/src/browser/quick-open/quick-command-contribution.ts index d120767082c90..35f568bb8e6f9 100644 --- a/packages/core/src/browser/quick-open/quick-command-contribution.ts +++ b/packages/core/src/browser/quick-open/quick-command-contribution.ts @@ -15,9 +15,9 @@ ********************************************************************************/ import { injectable, inject } from 'inversify'; -import { QuickCommandService } from './quick-command-service'; import { Command, CommandRegistry, CommandContribution } from '../../common'; import { KeybindingRegistry, KeybindingContribution } from '../keybinding'; +import { PrefixQuickOpenService, QuickOpenHandlerRegistry } from './prefix-quick-open-service'; export const quickCommand: Command = { id: 'quickCommand', @@ -27,12 +27,14 @@ export const quickCommand: Command = { @injectable() export class QuickCommandFrontendContribution implements CommandContribution, KeybindingContribution { - @inject(QuickCommandService) - protected readonly quickCommandService: QuickCommandService; + @inject(PrefixQuickOpenService) + protected readonly quickOpenService: PrefixQuickOpenService; + + @inject(QuickOpenHandlerRegistry) protected readonly quickOpenHandlerRegistry: QuickOpenHandlerRegistry; registerCommands(commands: CommandRegistry): void { commands.registerCommand(quickCommand, { - execute: () => this.quickCommandService.open() + execute: () => this.quickOpenService.open('>') }); } @@ -46,5 +48,4 @@ export class QuickCommandFrontendContribution implements CommandContribution, Ke keybinding: 'ctrlcmd+shift+p' }); } - } diff --git a/packages/core/src/browser/quick-open/quick-command-service.ts b/packages/core/src/browser/quick-open/quick-command-service.ts index 99ab42e6bd5ee..e14c4573564a3 100644 --- a/packages/core/src/browser/quick-open/quick-command-service.ts +++ b/packages/core/src/browser/quick-open/quick-command-service.ts @@ -18,20 +18,26 @@ import { inject, injectable } from 'inversify'; import { Command, CommandRegistry } from '../../common'; import { Keybinding, KeybindingRegistry } from '../keybinding'; import { QuickOpenModel, QuickOpenItem, QuickOpenMode } from './quick-open-model'; -import { QuickOpenService } from './quick-open-service'; +import { QuickOpenOptions } from './quick-open-service'; +import { QuickOpenContribution, QuickOpenHandlerRegistry, QuickOpenHandler } from './prefix-quick-open-service'; @injectable() -export class QuickCommandService implements QuickOpenModel { +export class QuickCommandService implements QuickOpenModel, QuickOpenHandler { private items: QuickOpenItem[]; - constructor( - @inject(CommandRegistry) protected readonly commands: CommandRegistry, - @inject(KeybindingRegistry) protected readonly keybindings: KeybindingRegistry, - @inject(QuickOpenService) protected readonly quickOpenService: QuickOpenService - ) { } + readonly prefix: string = '>'; + + readonly description: string = 'Quick Command'; + + @inject(CommandRegistry) + protected readonly commands: CommandRegistry; + + @inject(KeybindingRegistry) + protected readonly keybindings: KeybindingRegistry; - open(): void { + /** Initialize this quick open model with the commands. */ + init(): void { // let's compute the items here to do it in the context of the currently activeElement this.items = []; const filteredAndSortedCommands = this.commands.commands.filter(a => a.label).sort((a, b) => a.label!.localeCompare(b.label!)); @@ -40,18 +46,19 @@ export class QuickCommandService implements QuickOpenModel { this.items.push(new CommandQuickOpenItem(command, this.commands, this.keybindings)); } } - - this.quickOpenService.open(this, { - placeholder: 'Type the name of a command you want to execute', - fuzzyMatchLabel: true, - fuzzySort: false - }); } public onType(lookFor: string, acceptor: (items: QuickOpenItem[]) => void): void { acceptor(this.items); } + getModel(): QuickOpenModel { + return this; + } + + getOptions(): QuickOpenOptions { + return { fuzzyMatchLabel: true }; + } } export class CommandQuickOpenItem extends QuickOpenItem { @@ -103,3 +110,14 @@ export class CommandQuickOpenItem extends QuickOpenItem { return true; } } + +@injectable() +export class CommandQuickOpenContribution implements QuickOpenContribution { + + @inject(QuickCommandService) + protected readonly commandQuickOpenHandler: QuickCommandService; + + registerQuickOpenHandlers(handlers: QuickOpenHandlerRegistry): void { + handlers.registerHandler(this.commandQuickOpenHandler); + } +} diff --git a/packages/core/src/browser/quick-open/quick-open-frontend-contribution.ts b/packages/core/src/browser/quick-open/quick-open-frontend-contribution.ts new file mode 100644 index 0000000000000..4b27be3aacf3d --- /dev/null +++ b/packages/core/src/browser/quick-open/quick-open-frontend-contribution.ts @@ -0,0 +1,36 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. 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 + ********************************************************************************/ + +import { injectable, inject, named } from 'inversify'; +import { ContributionProvider } from '../../common'; +import { FrontendApplicationContribution } from '../frontend-application'; +import { QuickOpenContribution, QuickOpenHandlerRegistry } from './prefix-quick-open-service'; + +@injectable() +export class QuickOpenFrontendContribution implements FrontendApplicationContribution { + + @inject(QuickOpenHandlerRegistry) + protected readonly quickOpenHandlerRegistry: QuickOpenHandlerRegistry; + + @inject(ContributionProvider) @named(QuickOpenContribution) + protected readonly contributionProvider: ContributionProvider; + + onStart(): void { + this.contributionProvider.getContributions().forEach(contrib => + contrib.registerQuickOpenHandlers(this.quickOpenHandlerRegistry) + ); + } +} diff --git a/packages/core/src/browser/quick-open/quick-open-service.ts b/packages/core/src/browser/quick-open/quick-open-service.ts index 3ac1dccffbc8f..e58e7831695e8 100644 --- a/packages/core/src/browser/quick-open/quick-open-service.ts +++ b/packages/core/src/browser/quick-open/quick-open-service.ts @@ -28,6 +28,9 @@ export namespace QuickOpenOptions { readonly fuzzyMatchDescription: boolean; readonly fuzzySort: boolean; + /** The amount of first symbols to be ignored by quick open widget (e.g. don't affect matching). */ + readonly skipPrefix: number; + /** * Whether to display the items that don't have any highlight. */ @@ -46,6 +49,8 @@ export namespace QuickOpenOptions { fuzzyMatchDescription: false, fuzzySort: false, + skipPrefix: 0, + showItemsWithoutHighlight: false, onClose: () => { /* no-op*/ }, diff --git a/packages/core/src/browser/shell/shell-layout-restorer.ts b/packages/core/src/browser/shell/shell-layout-restorer.ts index e6ab56931465f..f94a23db23b11 100644 --- a/packages/core/src/browser/shell/shell-layout-restorer.ts +++ b/packages/core/src/browser/shell/shell-layout-restorer.ts @@ -114,9 +114,7 @@ export class ShellLayoutRestorer implements CommandContribution { return JSON.stringify(data, (property: string, value) => { if (this.isWidgetProperty(property)) { const description = this.convertToDescription(value as Widget); - if (description) { - return description; - } + return description; } else if (this.isWidgetsProperty(property)) { const descriptions: WidgetDescription[] = []; for (const widget of (value as Widget[])) { diff --git a/packages/core/src/browser/storage-service.spec.ts b/packages/core/src/browser/storage-service.spec.ts index 42cf81c5981f1..cdeb8e4cc93bb 100644 --- a/packages/core/src/browser/storage-service.spec.ts +++ b/packages/core/src/browser/storage-service.spec.ts @@ -20,6 +20,7 @@ import { expect } from 'chai'; import { ILogger } from '../common/logger'; import { MockLogger } from '../common/test/mock-logger'; import * as sinon from 'sinon'; +import { MessageService, MessageClient } from '../common/'; let storageService: StorageService; @@ -37,6 +38,9 @@ before(() => { testContainer.bind(StorageService).to(LocalStorageService).inSingletonScope(); testContainer.bind(LocalStorageService).toSelf().inSingletonScope(); + testContainer.bind(MessageClient).toSelf().inSingletonScope(); + testContainer.bind(MessageService).toSelf().inSingletonScope(); + storageService = testContainer.get(StorageService); }); diff --git a/packages/core/src/browser/storage-service.ts b/packages/core/src/browser/storage-service.ts index e26ec91d6a02d..9bff2d75215b3 100644 --- a/packages/core/src/browser/storage-service.ts +++ b/packages/core/src/browser/storage-service.ts @@ -14,8 +14,9 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { inject, injectable } from 'inversify'; +import { inject, injectable, postConstruct } from 'inversify'; import { ILogger } from '../common/logger'; +import { MessageService } from '../common/message-service'; export const StorageService = Symbol('IStorageService'); /** @@ -43,21 +44,27 @@ interface LocalStorage { @injectable() export class LocalStorageService implements StorageService { private storage: LocalStorage; + @inject(ILogger) protected logger: ILogger; + @inject(MessageService) protected readonly messageService: MessageService; - constructor( - @inject(ILogger) protected logger: ILogger - ) { + @postConstruct() + protected init() { if (typeof window !== 'undefined' && window.localStorage) { this.storage = window.localStorage; + this.testLocalStorage(); } else { - logger.warn(log => log("The browser doesn't support localStorage. state will not be persisted across sessions.")); + this.logger.warn(log => log("The browser doesn't support localStorage. state will not be persisted across sessions.")); this.storage = {}; } } setData(key: string, data?: T): Promise { if (data !== undefined) { - this.storage[this.prefix(key)] = JSON.stringify(data); + try { + this.storage[this.prefix(key)] = JSON.stringify(data); + } catch (e) { + this.showDiskQuotaExceededMessage(); + } } else { delete this.storage[this.prefix(key)]; } @@ -76,4 +83,35 @@ export class LocalStorageService implements StorageService { const pathname = typeof window === 'undefined' ? '' : window.location.pathname; return `theia:${pathname}:${key}`; } + + private showDiskQuotaExceededMessage() { + const READ_INSTRUCTIONS_ACTION = 'Read Instructions'; + const ERROR_MESSAGE = `Your preferred browser's local storage is almost full. + To be able to save your current workspace layout or data, you may need to free up some space. + You can refer to Theia's documentation page for instructions on how to manually clean + your browser's local storage.`; + this.messageService.warn(ERROR_MESSAGE, READ_INSTRUCTIONS_ACTION).then(selected => { + if (READ_INSTRUCTIONS_ACTION === selected) { + window.open('https://github.com/theia-ide/theia/wiki/Cleaning-Local-Storage'); + } + }); + } + + /** + * Verify if there is still some spaces left to save another workspace configuration into the local storage of your browser. + * If we are close to the limit, use a dialog to notify the user. + */ + private testLocalStorage(): void { + const array = new Array(60000); // size: * 5 = ~ 300K + const keyTest = this.prefix('Test'); + + try { + this.storage[keyTest] = JSON.stringify(array); + } catch (error) { + this.showDiskQuotaExceededMessage(); + } finally { + this.storage.removeItem(keyTest); + } + } + } diff --git a/packages/core/src/browser/style/index.css b/packages/core/src/browser/style/index.css index b7faae98538e0..8d571712b931c 100644 --- a/packages/core/src/browser/style/index.css +++ b/packages/core/src/browser/style/index.css @@ -157,4 +157,5 @@ button[disabled], .theia-button[disabled] { @import './tree.css'; @import './status-bar.css'; @import './tree-decorators.css'; -@import './about.css'; \ No newline at end of file +@import './about.css'; +@import './search-box.css'; diff --git a/packages/core/src/browser/style/menus.css b/packages/core/src/browser/style/menus.css index 19f9d1b649d0d..746b1586fe075 100644 --- a/packages/core/src/browser/style/menus.css +++ b/packages/core/src/browser/style/menus.css @@ -101,7 +101,6 @@ padding: 4px 0px; background: var(--theia-menu-color1); color: var(--theia-ui-font-color1); - border: var(--theia-border-width) solid var(--theia-border-color1); font-size: var(--theia-ui-font-size1); box-shadow: 0px 1px 6px rgba(0, 0, 0, 0.2); } diff --git a/packages/core/src/browser/style/scrollbars.css b/packages/core/src/browser/style/scrollbars.css index f2263dc4152c2..18ca12f222b0b 100644 --- a/packages/core/src/browser/style/scrollbars.css +++ b/packages/core/src/browser/style/scrollbars.css @@ -43,62 +43,67 @@ | Perfect scrollbar |----------------------------------------------------------------------------*/ -.ps__rail-x { +#theia-app-shell .ps__rail-x { height: var(--theia-scrollbar-rail-width); background: var(--theia-scrollbar-rail-color); } -.ps__rail-x > .ps__thumb-x { +#theia-app-shell .ps__rail-x > .ps__thumb-x { height: var(--theia-scrollbar-width); bottom: calc((var(--theia-scrollbar-rail-width) - var(--theia-scrollbar-width)) / 2); background: var(--theia-scrollbar-thumb-color); border-radius: 0px; } -.ps__rail-x:hover, -.ps__rail-x:focus { +#theia-app-shell .ps__rail-x:hover, +#theia-app-shell .ps__rail-x:focus { height: var(--theia-scrollbar-rail-width); - background: var(--theia-scrollbar-active-rail-color); } -.ps__rail-x:hover > .ps__thumb-x, -.ps__rail-x:focus > .ps__thumb-x { +#theia-app-shell .ps__rail-x:hover > .ps__thumb-x, +#theia-app-shell .ps__rail-x:focus > .ps__thumb-x, +#theia-app-shell .ps__rail-x.ps--clicking .ps__thumb-x { height: var(--theia-scrollbar-width); - bottom: calc((var(--theia-scrollbar-rail-width) - var(--theia-scrollbar-width)) / 2); - background: var(--theia-scrollbar-active-thumb-color); } -.ps__rail-y { +#theia-app-shell .ps__rail-y { width: var(--theia-scrollbar-rail-width); background: var(--theia-scrollbar-rail-color); } -.ps__rail-y > .ps__thumb-y { +#theia-app-shell .ps__rail-y > .ps__thumb-y { width: var(--theia-scrollbar-width); right: calc((var(--theia-scrollbar-rail-width) - var(--theia-scrollbar-width)) / 2); background: var(--theia-scrollbar-thumb-color); border-radius: 0px; } -.ps__rail-y:hover, -.ps__rail-y:focus { +#theia-app-shell .ps__rail-y:hover, +#theia-app-shell .ps__rail-y:focus { width: var(--theia-scrollbar-rail-width); - background: var(--theia-scrollbar-active-rail-color); } -.ps__rail-y:hover > .ps__thumb-y, -.ps__rail-y:focus > .ps__thumb-y { - width: var(--theia-scrollbar-width); +#theia-app-shell .ps__rail-y:hover > .ps__thumb-y, +#theia-app-shell .ps__rail-y:focus > .ps__thumb-y, +#theia-app-shell .ps__rail-y.ps--clicking .ps__thumb-y { right: calc((var(--theia-scrollbar-rail-width) - var(--theia-scrollbar-width)) / 2); + width: var(--theia-scrollbar-width); +} + +#theia-app-shell .ps [class^='ps__rail']:hover, +#theia-app-shell .ps [class^='ps__rail']:focus, +#theia-app-shell .ps [class^='ps__rail'].ps--clicking { + background-color: var(--theia-scrollbar-active-rail-color); +} + +#theia-app-shell .ps [class^='ps__rail']:hover > [class^='ps__thumb'], +#theia-app-shell .ps [class^='ps__rail']:focus > [class^='ps__thumb'] { background: var(--theia-scrollbar-active-thumb-color); } -.ps:hover > .ps__rail-x, -.ps:hover > .ps__rail-y, -.ps--focus > .ps__rail-x, -.ps--focus > .ps__rail-y, -.ps--scrolling-x > .ps__rail-x, -.ps--scrolling-y > .ps__rail-y { +#theia-app-shell .ps:hover > [class^='ps__rail'], +#theia-app-shell .ps--focus > [class^='ps__rail'], +#theia-app-shell .ps--scrolling-y > [class^='ps__rail'] { opacity: 1; } diff --git a/packages/navigator/src/browser/style/search-box.css b/packages/core/src/browser/style/search-box.css similarity index 99% rename from packages/navigator/src/browser/style/search-box.css rename to packages/core/src/browser/style/search-box.css index cfe8544b06b37..763d4d3d05cd3 100644 --- a/packages/navigator/src/browser/style/search-box.css +++ b/packages/core/src/browser/style/search-box.css @@ -78,4 +78,3 @@ user-select: none; cursor: default; } - \ No newline at end of file diff --git a/packages/navigator/src/browser/fuzzy-search.spec.ts b/packages/core/src/browser/tree/fuzzy-search.spec.ts similarity index 100% rename from packages/navigator/src/browser/fuzzy-search.spec.ts rename to packages/core/src/browser/tree/fuzzy-search.spec.ts diff --git a/packages/navigator/src/browser/fuzzy-search.ts b/packages/core/src/browser/tree/fuzzy-search.ts similarity index 100% rename from packages/navigator/src/browser/fuzzy-search.ts rename to packages/core/src/browser/tree/fuzzy-search.ts diff --git a/packages/core/src/browser/tree/index.ts b/packages/core/src/browser/tree/index.ts index 6e6e678ada596..603e7ee7db153 100644 --- a/packages/core/src/browser/tree/index.ts +++ b/packages/core/src/browser/tree/index.ts @@ -23,3 +23,4 @@ export * from './tree-model'; export * from './tree-widget'; export * from './tree-container'; export * from './tree-decorator'; +export * from './tree-search'; diff --git a/packages/navigator/src/browser/search-box-debounce.ts b/packages/core/src/browser/tree/search-box-debounce.ts similarity index 94% rename from packages/navigator/src/browser/search-box-debounce.ts rename to packages/core/src/browser/tree/search-box-debounce.ts index 3e1b31f3cec5f..4012cd09166e7 100644 --- a/packages/navigator/src/browser/search-box-debounce.ts +++ b/packages/core/src/browser/tree/search-box-debounce.ts @@ -14,8 +14,8 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { Event, Emitter } from '@theia/core/lib/common/event'; -import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; +import { Event, Emitter } from '../../common/event'; +import { Disposable, DisposableCollection } from '../../common/disposable'; import debounce = require('lodash.debounce'); diff --git a/packages/navigator/src/browser/search-box.ts b/packages/core/src/browser/tree/search-box.ts similarity index 95% rename from packages/navigator/src/browser/search-box.ts rename to packages/core/src/browser/tree/search-box.ts index df2643693ce12..a67a381ccbb45 100644 --- a/packages/navigator/src/browser/search-box.ts +++ b/packages/core/src/browser/tree/search-box.ts @@ -14,10 +14,10 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { KeyCode, Key } from '@theia/core/lib/browser'; -import { BaseWidget } from '@theia/core/lib/browser/widgets/widget'; -import { Event, Emitter } from '@theia/core/lib/common/event'; -import { SearchBoxDebounce, SearchBoxDebounceOptions } from './search-box-debounce'; +import { SearchBoxDebounce, SearchBoxDebounceOptions } from '../tree/search-box-debounce'; +import { BaseWidget } from '../widgets/widget'; +import { Emitter, Event } from '../../common/event'; +import { KeyCode, Key } from '../keys'; /** * Initializer properties for the search box widget. @@ -25,7 +25,7 @@ import { SearchBoxDebounce, SearchBoxDebounceOptions } from './search-box-deboun export interface SearchBoxProps extends SearchBoxDebounceOptions { /** - * If `true`, the `Previous`, `Next`, and `Clone` buttons will be visible. Otherwise, `false`. Defaults to `false`. + * If `true`, the `Previous`, `Next`, and `Close` buttons will be visible. Otherwise, `false`. Defaults to `false`. */ readonly showButtons?: boolean; @@ -45,7 +45,7 @@ export namespace SearchBoxProps { */ export class SearchBox extends BaseWidget { - private static SPECIAL_KEYS = [ + protected static SPECIAL_KEYS = [ Key.ESCAPE, Key.BACKSPACE ]; diff --git a/packages/core/src/browser/tree/tree-container.ts b/packages/core/src/browser/tree/tree-container.ts index 3812959661555..98380a1f0e328 100644 --- a/packages/core/src/browser/tree/tree-container.ts +++ b/packages/core/src/browser/tree/tree-container.ts @@ -23,6 +23,10 @@ import { TreeSelectionServiceImpl } from './tree-selection-impl'; import { TreeExpansionService, TreeExpansionServiceImpl } from './tree-expansion'; import { TreeNavigationService } from './tree-navigation'; import { TreeDecoratorService, NoopTreeDecoratorService } from './tree-decorator'; +import { TreeSearch } from './tree-search'; +import { FuzzySearch } from './fuzzy-search'; +import { SearchBox, SearchBoxFactory, SearchBoxProps } from './search-box'; +import { SearchBoxDebounce } from './search-box-debounce'; export function createTreeContainer(parent: interfaces.Container): Container { const child = new Container({ defaultScope: 'Singleton' }); @@ -45,6 +49,15 @@ export function createTreeContainer(parent: interfaces.Container): Container { child.bind(TreeWidget).toSelf(); child.bind(TreeProps).toConstantValue(defaultTreeProps); + child.bind(TreeSearch).toSelf().inSingletonScope(); + child.bind(FuzzySearch).toSelf().inSingletonScope(); + child.bind(SearchBoxFactory).toFactory(context => + (options: SearchBoxProps) => { + const debounce = new SearchBoxDebounce(options); + return new SearchBox(options, debounce); + } + ); + child.bind(TreeDecoratorService).to(NoopTreeDecoratorService).inSingletonScope(); return child; } diff --git a/packages/core/src/browser/tree/tree-iterator.spec.ts b/packages/core/src/browser/tree/tree-iterator.spec.ts index a2113a73bc9f7..520e5da75dcf1 100644 --- a/packages/core/src/browser/tree/tree-iterator.spec.ts +++ b/packages/core/src/browser/tree/tree-iterator.spec.ts @@ -17,6 +17,8 @@ import { expect } from 'chai'; import { Container } from 'inversify'; import { notEmpty } from '../../common/objects'; +import { TreeSearch } from './tree-search'; +import { FuzzySearch } from './fuzzy-search'; import { Tree, TreeImpl } from './tree'; import { MockTreeModel } from './test/mock-tree-model'; import { TreeNavigationService } from './tree-navigation'; @@ -125,6 +127,8 @@ describe('tree-iterator', () => { container.bind(TreeNavigationService).toSelf(); container.bind(TreeModelImpl).toSelf(); container.bind(TreeModel).toService(TreeModelImpl); + container.bind(TreeSearch).toSelf(); + container.bind(FuzzySearch).toSelf(); return container.get(TreeModel); } diff --git a/packages/core/src/browser/tree/tree-model.ts b/packages/core/src/browser/tree/tree-model.ts index 6565312ef7225..65a53f3ea0a6c 100644 --- a/packages/core/src/browser/tree/tree-model.ts +++ b/packages/core/src/browser/tree/tree-model.ts @@ -20,7 +20,8 @@ import { Tree, TreeNode, CompositeTreeNode } from './tree'; import { TreeSelectionService, SelectableTreeNode, TreeSelection } from './tree-selection'; import { TreeExpansionService, ExpandableTreeNode } from './tree-expansion'; import { TreeNavigationService } from './tree-navigation'; -import { TreeIterator, BottomUpTreeIterator, TopDownTreeIterator } from './tree-iterator'; +import { TreeIterator, BottomUpTreeIterator, TopDownTreeIterator, Iterators } from './tree-iterator'; +import { TreeSearch } from './tree-search'; /** * The tree model. @@ -132,6 +133,7 @@ export class TreeModelImpl implements TreeModel, SelectionProvider(); protected readonly onOpenNodeEmitter = new Emitter(); @@ -156,6 +158,7 @@ export class TreeModelImpl implements TreeModel, SelectionProvider Map>(); protected readonly filteredNodesEmitter = new Emitter>>(); protected _filterResult: FuzzySearch.Match[] = []; @@ -42,15 +39,11 @@ export class FileNavigatorSearch implements Disposable, TreeDecorator { @postConstruct() init() { - this.disposables.pushAll([ - this.decorationEmitter, - this.filteredNodesEmitter, - this.tree.onChanged(() => this.filter(undefined)) - ]); + this.disposables.push(this.filteredNodesEmitter); } - async decorations(): Promise> { - return new Map(this._filterResult.map(m => [m.item.id, this.toDecorator(m)] as [string, TreeDecoration.Data])); + getHighlights(): Map { + return new Map(this._filterResult.map(m => [m.item.id, this.toCaptionHighlight(m)] as [string, TreeDecoration.CaptionHighlight])); } /** @@ -61,7 +54,6 @@ export class FileNavigatorSearch implements Disposable, TreeDecorator { if (!pattern || !root) { this._filterResult = []; this._filteredNodes = []; - this.fireDidChangeDecorations(() => new Map()); this.fireFilteredNodesChanged(this._filteredNodes); return []; } @@ -73,15 +65,10 @@ export class FileNavigatorSearch implements Disposable, TreeDecorator { transform }); this._filteredNodes = this._filterResult.map(match => match.item); - this.fireDidChangeDecorations(() => new Map(this._filterResult.map(m => [m.item.id, this.toDecorator(m)] as [string, TreeDecoration.Data]))); this.fireFilteredNodesChanged(this._filteredNodes); return this._filteredNodes!.slice(); } - get onDidChangeDecorations(): Event<(tree: Tree) => Map> { - return this.decorationEmitter.event; - } - /** * Returns with the filtered nodes after invoking the `filter` method. */ @@ -100,19 +87,13 @@ export class FileNavigatorSearch implements Disposable, TreeDecorator { this.disposables.dispose(); } - protected fireDidChangeDecorations(event: (tree: Tree) => Map): void { - this.decorationEmitter.fire(event); - } - protected fireFilteredNodesChanged(nodes: ReadonlyArray>): void { this.filteredNodesEmitter.fire(nodes); } - protected toDecorator(match: FuzzySearch.Match): TreeDecoration.Data { + protected toCaptionHighlight(match: FuzzySearch.Match): TreeDecoration.CaptionHighlight { return { - highlight: { - ranges: match.ranges.map(this.mapRange.bind(this)) - } + ranges: match.ranges.map(this.mapRange.bind(this)) }; } diff --git a/packages/core/src/browser/tree/tree-selection-state.spec.ts b/packages/core/src/browser/tree/tree-selection-state.spec.ts index 11ef5690d9833..8b6a91b0da118 100644 --- a/packages/core/src/browser/tree/tree-selection-state.spec.ts +++ b/packages/core/src/browser/tree/tree-selection-state.spec.ts @@ -16,6 +16,8 @@ import { expect } from 'chai'; import { Container } from 'inversify'; +import { TreeSearch } from './tree-search'; +import { FuzzySearch } from './fuzzy-search'; import { Tree, TreeImpl } from './tree'; import { MockTreeModel } from './test/mock-tree-model'; import { TreeSelectionState } from './tree-selection-state'; @@ -445,6 +447,8 @@ describe('tree-selection-state', () => { container.bind(TreeNavigationService).toSelf(); container.bind(TreeModelImpl).toSelf(); container.bind(TreeModel).toService(TreeModelImpl); + container.bind(TreeSearch).toSelf(); + container.bind(FuzzySearch).toSelf(); return container.get(TreeModel); } diff --git a/packages/core/src/browser/tree/tree-widget.tsx b/packages/core/src/browser/tree/tree-widget.tsx index 8c530a4b0dbc7..cea531852a3ef 100644 --- a/packages/core/src/browser/tree/tree-widget.tsx +++ b/packages/core/src/browser/tree/tree-widget.tsx @@ -30,8 +30,10 @@ import { notEmpty } from '../../common/objects'; import { isOSX } from '../../common/os'; import { ReactWidget } from '../widgets/react-widget'; import * as React from 'react'; -import { List, ListRowRenderer } from 'react-virtualized'; +import { List, ListRowRenderer, ScrollParams } from 'react-virtualized'; import { TopDownTreeIterator } from './tree-iterator'; +import { SearchBox, SearchBoxFactory, SearchBoxProps } from './search-box'; +import { TreeSearch } from './tree-search'; const debounce = require('lodash.debounce'); @@ -66,6 +68,11 @@ export interface TreeProps { * `true` if the tree widget support multi-selection. Otherwise, `false`. Defaults to `false`. */ readonly multiSelect?: boolean; + + /** + * 'true' if the tree widget support searching. Otherwise, `false`. Defaults to `false`. + */ + readonly search?: boolean } export interface NodeProps { @@ -97,8 +104,15 @@ export namespace TreeWidget { @injectable() export class TreeWidget extends ReactWidget implements StatefulWidget { + protected searchBox: SearchBox; + protected searchHighlights: Map; + @inject(TreeDecoratorService) protected readonly decoratorService: TreeDecoratorService; + @inject(TreeSearch) + protected readonly treeSearch: TreeSearch; + @inject(SearchBoxFactory) + protected readonly searchBoxFactory: SearchBoxFactory; protected decorations: Map = new Map(); @@ -117,6 +131,30 @@ export class TreeWidget extends ReactWidget implements StatefulWidget { @postConstruct() protected init(): void { + if (this.props.search) { + this.searchBox = this.searchBoxFactory(SearchBoxProps.DEFAULT); + this.toDispose.pushAll([ + this.searchBox, + this.searchBox.onTextChange(async data => { + await this.treeSearch.filter(data); + this.searchHighlights = this.treeSearch.getHighlights(); + this.update(); + }), + this.searchBox.onClose(data => this.treeSearch.filter(undefined)), + this.searchBox.onNext(() => this.model.selectNextNode()), + this.searchBox.onPrevious(() => this.model.selectPrevNode()), + this.treeSearch, + this.treeSearch.onFilteredNodesChanged(nodes => { + const node = nodes.find(SelectableTreeNode.is); + if (node) { + this.model.selectNode(node); + } + }), + this.model.onExpansionChanged(() => { + this.searchBox.hide(); + }) + ]); + } this.toDispose.pushAll([ this.model, this.model.onChanged(() => this.updateRows()), @@ -241,12 +279,17 @@ export class TreeWidget extends ReactWidget implements StatefulWidget { getNodeRowHeight={this.getNodeRowHeight} renderNodeRow={this.renderNodeRow} scrollToRow={this.scrollToRow} + handleScroll={this.handleScroll} />; } // tslint:disable-next-line:no-null-keyword return null; } + protected readonly handleScroll = (info: ScrollParams) => { + this.node.scrollTop = info.scrollTop; + } + protected readonly renderNodeRow = (row: TreeWidget.NodeRow) => this.doRenderNodeRow(row); protected doRenderNodeRow({ index, node, depth }: TreeWidget.NodeRow): React.ReactNode { return this.renderNode(node, { depth }); @@ -312,38 +355,46 @@ export class TreeWidget extends ReactWidget implements StatefulWidget { title: tooltip }; } - const highlight = this.getDecorationData(node, 'highlight')[0]; const children: React.ReactNode[] = []; const caption = node.name; + const highlight = this.getDecorationData(node, 'highlight')[0]; if (highlight) { - let style: React.CSSProperties = {}; - if (highlight.color) { - style = { - ...style, - color: highlight.color - }; - } - if (highlight.backgroundColor) { - style = { - ...style, - backgroundColor: highlight.backgroundColor - }; - } - const createChildren = (fragment: TreeDecoration.CaptionHighlight.Fragment) => { - const { data } = fragment; - if (fragment.highligh) { - return {data}; - } else { - return data; - } - }; - children.push(...TreeDecoration.CaptionHighlight.split(caption, highlight).map(createChildren)); - } else { + children.push(this.toReactNode(caption, highlight)); + } + const searchHighlight = this.searchHighlights ? this.searchHighlights.get(node.id) : undefined; + if (searchHighlight) { + children.push(...this.toReactNode(caption, searchHighlight)); + } else if (!highlight) { children.push(caption); } return React.createElement('div', attrs, ...children); } + protected toReactNode(caption: string, highlight: TreeDecoration.CaptionHighlight): React.ReactNode[] { + let style: React.CSSProperties = {}; + if (highlight.color) { + style = { + ...style, + color: highlight.color + }; + } + if (highlight.backgroundColor) { + style = { + ...style, + backgroundColor: highlight.backgroundColor + }; + } + const createChildren = (fragment: TreeDecoration.CaptionHighlight.Fragment) => { + const {data} = fragment; + if (fragment.highligh) { + return {data}; + } else { + return data; + } + }; + return TreeDecoration.CaptionHighlight.split(caption, highlight).map(createChildren); + } + protected decorateCaption(node: TreeNode, attrs: React.HTMLAttributes): React.Attributes & React.HTMLAttributes { const style = this.getDecorationData(node, 'fontData').filter(notEmpty).reverse().map(fontData => this.applyFontStyles({}, fontData)).reduce((acc, current) => ({ @@ -418,9 +469,11 @@ export class TreeWidget extends ReactWidget implements StatefulWidget { } const style = this.applyFontStyles({}, affix.fontData); const className = classes.join(' '); + const key = node.id + '_' + i; const attrs = { className, - style + style, + key }; children.push(React.createElement('div', attrs, affix.data)); } @@ -559,12 +612,28 @@ export class TreeWidget extends ReactWidget implements StatefulWidget { Key.ARROW_DOWN, KeyCode.createKeyCode({ first: Key.ARROW_DOWN, modifiers: [KeyModifier.Shift] }) ]; + if (this.props.search) { + if (this.searchBox.isAttached) { + Widget.detach(this.searchBox); + } + Widget.attach(this.searchBox, this.node.parentElement!); + this.addKeyListener(this.node, this.searchBox.keyCodePredicate.bind(this.searchBox), this.searchBox.handle.bind(this.searchBox)); + this.toDisposeOnDetach.push(Disposable.create(() => { + Widget.detach(this.searchBox); + })); + } super.onAfterAttach(msg); this.addKeyListener(this.node, Key.ARROW_LEFT, event => this.handleLeft(event)); this.addKeyListener(this.node, Key.ARROW_RIGHT, event => this.handleRight(event)); this.addKeyListener(this.node, up, event => this.handleUp(event)); this.addKeyListener(this.node, down, event => this.handleDown(event)); this.addKeyListener(this.node, Key.ENTER, event => this.handleEnter(event)); + this.addEventListener(this.node, 'ps-scroll-y', (e: Event & { target: { scrollTop: number } }) => { + if (this.view && this.view.list && this.view.list.Grid) { + const { scrollTop } = e.target; + this.view.list.Grid.handleScrollEvent({ scrollTop }); + } + }); } protected async handleLeft(event: KeyboardEvent): Promise { @@ -751,13 +820,14 @@ export namespace TreeWidget { height: number scrollToRow?: number rows: NodeRow[] + handleScroll: (info: ScrollParams) => void getNodeRowHeight: (row: NodeRow) => number renderNodeRow: (row: NodeRow) => React.ReactNode } export class View extends React.Component { list: List | undefined; render(): React.ReactNode { - const { rows, width, height, scrollToRow } = this.props; + const { rows, width, height, scrollToRow, handleScroll } = this.props; return this.list = (list || undefined)} width={width} @@ -766,7 +836,12 @@ export namespace TreeWidget { rowHeight={this.getNodeRowHeight} rowRenderer={this.renderTreeRow} scrollToIndex={scrollToRow} + onScroll={handleScroll} tabIndex={-1} + style={{ + overflowY: 'visible', + overflowX: 'visible' + }} />; } protected renderTreeRow: ListRowRenderer = ({ key, index, style }) => { diff --git a/packages/core/src/browser/widgets/react-widget.tsx b/packages/core/src/browser/widgets/react-widget.tsx index bf0632e0b4346..9d0268cc725a9 100644 --- a/packages/core/src/browser/widgets/react-widget.tsx +++ b/packages/core/src/browser/widgets/react-widget.tsx @@ -24,12 +24,12 @@ import { BaseWidget, Message } from './widget'; export abstract class ReactWidget extends BaseWidget { protected readonly onRender = new DisposableCollection(); - protected scrollOptions = { - suppressScrollX: true - }; constructor() { super(); + this.scrollOptions = { + suppressScrollX: true + }; this.toDispose.push(Disposable.create(() => { ReactDOM.unmountComponentAtNode(this.node); })); diff --git a/packages/core/src/browser/widgets/widget.ts b/packages/core/src/browser/widgets/widget.ts index 57f0a261cdcd7..8820da36d8354 100644 --- a/packages/core/src/browser/widgets/widget.ts +++ b/packages/core/src/browser/widgets/widget.ts @@ -79,10 +79,7 @@ export class BaseWidget extends Widget { (async () => { const container = await this.getScrollContainer(); container.style.overflow = 'hidden'; - this.scrollBar = new PerfectScrollbar(container, { - suppressScrollX: true - }); - + this.scrollBar = new PerfectScrollbar(container, this.scrollOptions); this.toDispose.push(Disposable.create(async () => { if (this.scrollBar) { this.scrollBar.destroy(); diff --git a/packages/core/src/common/event.ts b/packages/core/src/common/event.ts index 779447366fc71..3ed4e061b08d3 100644 --- a/packages/core/src/common/event.ts +++ b/packages/core/src/common/event.ts @@ -106,7 +106,7 @@ class CallbackList { try { ret.push(callbacks[i].apply(contexts[i], args)); } catch (e) { - // FIXME: log error + console.error(e); } } return ret; diff --git a/packages/core/src/common/logger-protocol.ts b/packages/core/src/common/logger-protocol.ts index bd569d52830a1..6d0b796ffb218 100644 --- a/packages/core/src/common/logger-protocol.ts +++ b/packages/core/src/common/logger-protocol.ts @@ -37,3 +37,65 @@ export interface ILogLevelChangedEvent { export interface ILoggerClient { onLogLevelChanged(event: ILogLevelChangedEvent): void; } + +export const rootLoggerName = 'root'; + +export enum LogLevel { + FATAL = 60, + ERROR = 50, + WARN = 40, + INFO = 30, + DEBUG = 20, + TRACE = 10 +} +export namespace LogLevel { + export const strings = new Map([ + [LogLevel.FATAL, 'fatal'], + [LogLevel.ERROR, 'error'], + [LogLevel.WARN, 'warn'], + [LogLevel.INFO, 'info'], + [LogLevel.DEBUG, 'debug'], + [LogLevel.TRACE, 'trace'] + ]); + + export function toString(level: LogLevel): string | undefined { + return strings.get(level); + } + + export function fromString(levelStr: string): LogLevel | undefined { + for (const pair of strings) { + if (pair[1] === levelStr) { + return pair[0]; + } + } + + return undefined; + } +} + +// tslint:disable:no-any +export namespace ConsoleLogger { + type Console = (message?: any, ...optionalParams: any[]) => void; + const originalConsoleLog = console.log; + const consoles = new Map([ + [LogLevel.FATAL, console.error], + [LogLevel.ERROR, console.error], + [LogLevel.WARN, console.warn], + [LogLevel.INFO, console.info], + [LogLevel.DEBUG, console.debug], + [LogLevel.TRACE, console.trace] + ]); + export function reset(): void { + console.error = consoles.get(LogLevel.ERROR)!; + console.warn = consoles.get(LogLevel.WARN)!; + console.info = consoles.get(LogLevel.INFO)!; + console.debug = consoles.get(LogLevel.DEBUG)!; + console.trace = consoles.get(LogLevel.TRACE)!; + console.log = originalConsoleLog; + } + export function log(name: string, logLevel: number, message: string, params: any[]): void { + const console = consoles.get(logLevel) || originalConsoleLog; + const severity = (LogLevel.strings.get(logLevel) || 'unknown').toUpperCase(); + console(`${name} ${severity}`, message, ...params); + } +} diff --git a/packages/core/src/common/logger-watcher.ts b/packages/core/src/common/logger-watcher.ts index 742cc05019314..695e1a9613f03 100644 --- a/packages/core/src/common/logger-watcher.ts +++ b/packages/core/src/common/logger-watcher.ts @@ -36,6 +36,7 @@ export class LoggerWatcher { return this.onLogLevelChangedEmitter.event; } + // FIXME: get rid of it, backend services should as well set a client on the server fireLogLevelChanged(event: ILogLevelChangedEvent) { this.onLogLevelChangedEmitter.fire(event); } diff --git a/packages/core/src/common/logger.ts b/packages/core/src/common/logger.ts index f706929651e72..a9901004bec51 100644 --- a/packages/core/src/common/logger.ts +++ b/packages/core/src/common/logger.ts @@ -16,63 +16,17 @@ import { inject, injectable } from 'inversify'; import { LoggerWatcher } from './logger-watcher'; -import { ILoggerServer } from './logger-protocol'; +import { ILoggerServer, LogLevel, ConsoleLogger, rootLoggerName } from './logger-protocol'; // tslint:disable:no-any -export enum LogLevel { - FATAL = 60, - ERROR = 50, - WARN = 40, - INFO = 30, - DEBUG = 20, - TRACE = 10 -} - -export namespace LogLevel { - export const strings = new Map([ - [LogLevel.FATAL, 'fatal'], - [LogLevel.ERROR, 'error'], - [LogLevel.WARN, 'warn'], - [LogLevel.INFO, 'info'], - [LogLevel.DEBUG, 'debug'], - [LogLevel.TRACE, 'trace'] - ]); - - export function toString(level: LogLevel): string | undefined { - return strings.get(level); - } - - export function fromString(levelStr: string): LogLevel | undefined { - for (const pair of strings) { - if (pair[1] === levelStr) { - return pair[0]; - } - } - - return undefined; - } -} - -type ConsoleError = typeof console.error; -type ConsoleWarn = typeof console.warn; -type ConsoleInfo = typeof console.info; -type ConsoleDebug = typeof console.debug; -type ConsoleTrace = typeof console.trace; -type ConsoleLog = typeof console.log; - -let originalConsoleError: ConsoleError; -let originalConsoleWarn: ConsoleWarn; -let originalConsoleInfo: ConsoleInfo; -let originalConsoleDebug: ConsoleDebug; -let originalConsoleTrace: ConsoleTrace; -let originalConsoleLog: ConsoleLog; +export { + LogLevel, rootLoggerName +}; /* This is to be initialized from container composition root. It can be used outside of the inversify context. */ export let logger: ILogger; -export const rootLoggerName: string = 'root'; - /** * Counterpart of the `#setRootLogger(ILogger)`. Restores the `console.xxx` bindings to the original one. * Invoking has no side-effect if `setRootLogger` was not called before. Multiple function invocation has @@ -80,40 +34,22 @@ export const rootLoggerName: string = 'root'; */ export function unsetRootLogger() { if (logger !== undefined) { - console.error = originalConsoleError; - console.warn = originalConsoleWarn; - console.info = originalConsoleInfo; - console.debug = originalConsoleDebug; - console.trace = originalConsoleTrace; - console.log = originalConsoleLog; + ConsoleLogger.reset(); (logger) = undefined; } } -export function setRootLogger(aLogger: ILogger) { - if (logger === undefined) { - originalConsoleError = console.error; - originalConsoleWarn = console.warn; - originalConsoleInfo = console.info; - originalConsoleDebug = console.debug; - originalConsoleTrace = console.trace; - originalConsoleLog = console.log; - } +export function setRootLogger(aLogger: ILogger): void { logger = aLogger; - const frontend = typeof window !== 'undefined' && typeof (window as any).process === 'undefined'; - const log = (logLevel: number, consoleLog: ConsoleLog, message?: any, ...optionalParams: any[]) => { - aLogger.log(logLevel, message, ...optionalParams); - if (frontend) { - consoleLog(message, ...optionalParams); - } - }; - - console.error = log.bind(undefined, LogLevel.ERROR, console.error); - console.warn = log.bind(undefined, LogLevel.WARN, console.warn); - console.info = log.bind(undefined, LogLevel.INFO, console.info); - console.debug = log.bind(undefined, LogLevel.DEBUG, console.debug); - console.trace = log.bind(undefined, LogLevel.TRACE, console.trace); - console.log = log.bind(undefined, LogLevel.INFO, console.log); + const log = (logLevel: number, message?: any, ...optionalParams: any[]) => + logger.log(logLevel, message, ...optionalParams); + + console.error = log.bind(undefined, LogLevel.ERROR); + console.warn = log.bind(undefined, LogLevel.WARN); + console.info = log.bind(undefined, LogLevel.INFO); + console.debug = log.bind(undefined, LogLevel.DEBUG); + console.trace = log.bind(undefined, LogLevel.TRACE); + console.log = log.bind(undefined, LogLevel.INFO); } export type Log = (message: any, ...params: any[]) => void; diff --git a/packages/core/src/node/cluster.spec.ts b/packages/core/src/node/cluster.spec.ts index aa0dc0e7396f4..417025c57f687 100644 --- a/packages/core/src/node/cluster.spec.ts +++ b/packages/core/src/node/cluster.spec.ts @@ -56,7 +56,7 @@ describe('master-process', () => { */ it('start', async function () { this.timeout(10000); - const master = new MasterProcess(); + const master = new MasterProcess(5000); prepareTestWorker('restart'); const restartWorker = await master.start(); diff --git a/packages/core/src/node/cluster/main.ts b/packages/core/src/node/cluster/main.ts index 18039e42ff3a7..9d1bc8e499340 100644 --- a/packages/core/src/node/cluster/main.ts +++ b/packages/core/src/node/cluster/main.ts @@ -24,11 +24,18 @@ process.on('unhandledRejection', (reason, promise) => { throw reason; }); -const args = require('yargs').help(false).argv; +import yargs = require('yargs'); +const args = yargs.option(MasterProcess.startupTimeoutOption, { + description: 'The number of milliseconds to wait for the server to start up. Pass a negative number to disable the timeout.', + type: 'number', + default: 5000 +}).help(false).argv; const noCluster = args['cluster'] === false; const isMaster = !noCluster && cluster.isMaster; const development = process.env.NODE_ENV === 'development'; +const startupTimeout = args[MasterProcess.startupTimeoutOption] as number; + if (isMaster && development) { // https://github.com/Microsoft/vscode/issues/3201 process.execArgv = process.execArgv.reduce((result, arg) => { @@ -44,12 +51,13 @@ export interface Address { export async function start(serverPath: string): Promise
{ if (isMaster) { - const master = new MasterProcess(); + const master = new MasterProcess(startupTimeout); master.onexit(process.exit); try { const worker = await master.start(); return worker.listening; } catch (error) { + console.error(error.message); process.exit(error.returnCode); } } diff --git a/packages/core/src/node/cluster/master-process.ts b/packages/core/src/node/cluster/master-process.ts index 7df0b5a61ce3e..507bc8ccda4e1 100644 --- a/packages/core/src/node/cluster/master-process.ts +++ b/packages/core/src/node/cluster/master-process.ts @@ -24,25 +24,38 @@ class ProcessError extends Error { export type MasterProcessEvent = 'started' | 'restarted' | 'restarting'; export class MasterProcess extends EventEmitter { + static startupTimeoutOption = 'startup-timeout'; + protected serverWorker: ServerWorker | undefined; protected workerCount: number = 0; + constructor( + protected readonly startupTimeout: number + ) { + super(); + } + protected async fork(): Promise { const worker = new ServerWorker(() => this.restart()); const success = worker.initialized.then(() => true); + // tslint:disable-next-line:no-any const failure = Promise.race( - [worker.failed, worker.disconnect, worker.exit, this.timeout(5000)] + [worker.failed, worker.disconnect, worker.exit, this.timeout(this.startupTimeout)] ).then(() => false); const started = await Promise.race([success, failure]); // Failure if (!started) { - const error = new ProcessError('Server worker failed to start.'); - console.error(error.message); - - worker.stop(); - error.returnCode = await worker.exit; + let message = 'Server worker failed to start'; + if (this.startupTimeout >= 0) { + message += ` within ${this.startupTimeout} milliseconds. +Pass a greater value as '--${MasterProcess.startupTimeoutOption}' option to increase the timeout or a negative to disable.`; + } else { + message += '.'; + } + const error = new ProcessError(message); + error.returnCode = await worker.stop(); throw error; } @@ -90,10 +103,7 @@ export class MasterProcess extends EventEmitter { } protected timeout(delay: number): Promise { - let resolveTimeout: () => void; - const timeout = new Promise(resolve => resolveTimeout = resolve); - setTimeout(() => resolveTimeout(), delay); - return timeout; + return new Promise(resolve => delay >= 0 && setTimeout(resolve, delay)); } onexit(listener: (code: number) => void): this { diff --git a/packages/core/src/node/cluster/server-worker.ts b/packages/core/src/node/cluster/server-worker.ts index 1418662e7d689..4ec4b8ec42dd5 100644 --- a/packages/core/src/node/cluster/server-worker.ts +++ b/packages/core/src/node/cluster/server-worker.ts @@ -54,15 +54,16 @@ export class ServerWorker { this.exit.then(() => console.log(`Server worker has been stopped. ${workerIdentifier}`)); } - async stop(): Promise { + async stop(): Promise { if (this.worker.isConnected) { this.worker.disconnect(); await this.disconnect; } - if (!this.worker.isDead) { - this.worker.kill(); - await this.exit; + if (this.worker.isDead) { + return 1; } + this.worker.kill(); + return await this.exit; } } diff --git a/packages/core/src/node/console-logger-server.ts b/packages/core/src/node/console-logger-server.ts index a8b95706244ff..053cbf13ccba0 100644 --- a/packages/core/src/node/console-logger-server.ts +++ b/packages/core/src/node/console-logger-server.ts @@ -15,22 +15,9 @@ ********************************************************************************/ import { inject, injectable, postConstruct } from 'inversify'; -import { LogLevel } from '../common/logger'; import { LoggerWatcher } from '../common/logger-watcher'; import { LogLevelCliContribution } from './logger-cli-contribution'; -import { ILoggerServer, ILoggerClient } from '../common/logger-protocol'; - -// tslint:disable-next-line:no-any -type Console = (message?: any, ...optionalParams: any[]) => void; -const originalConsoleLog = console.log; -const Consoles = new Map([ - [LogLevel.FATAL, console.error], - [LogLevel.ERROR, console.error], - [LogLevel.WARN, console.warn], - [LogLevel.INFO, console.info], - [LogLevel.DEBUG, console.debug], - [LogLevel.TRACE, console.trace] -]); +import { ILoggerServer, ILoggerClient, LogLevel, ConsoleLogger } from '../common/logger-protocol'; @injectable() export class ConsoleLoggerServer implements ILoggerServer { @@ -68,13 +55,11 @@ export class ConsoleLoggerServer implements ILoggerServer { return this.loggers.get(name) || this.cli.defaultLogLevel; } - // tslint:disable-next-line:no-any + // tslint:disable:no-any async log(name: string, logLevel: number, message: string, params: any[]): Promise { const configuredLogLevel = await this.getLogLevel(name); if (logLevel >= configuredLogLevel) { - const console = Consoles.get(logLevel) || originalConsoleLog; - const severity = `${(LogLevel.strings.get(logLevel) || 'unknown').toUpperCase()}`; - console(`${name} ${severity}`, message, ...params); + ConsoleLogger.log(name, logLevel, message, params); } } diff --git a/packages/cpp/README.md b/packages/cpp/README.md index 0cda389b190b7..14d685955b15c 100644 --- a/packages/cpp/README.md +++ b/packages/cpp/README.md @@ -3,12 +3,12 @@ This extension uses [Clangd](https://clang.llvm.org/extra/clangd.html) to provide LSP features. -To install Clangd on Ubuntu 16.04: +To install Clangd on Ubuntu 18.04: $ wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add - - $ echo "deb http://apt.llvm.org/xenial/ llvm-toolchain-xenial main" | sudo tee /etc/apt/sources.list.d/llvm.list - $ sudo apt-get update && sudo apt-get install -y clang-tools-7 - $ sudo ln -s /usr/bin/clangd-7 /usr/bin/clangd + $ echo "deb http://apt.llvm.org/bionic/ llvm-toolchain-bionic main" | sudo tee /etc/apt/sources.list.d/llvm.list + $ sudo apt-get update && sudo apt-get install -y clang-tools-8 + $ sudo ln -s /usr/bin/clangd-8 /usr/bin/clangd See [here](https://clang.llvm.org/extra/clangd.html#id4) for detailed installation instructions. diff --git a/packages/cpp/package.json b/packages/cpp/package.json index 367f6f375290f..7c03f3eecb1d1 100644 --- a/packages/cpp/package.json +++ b/packages/cpp/package.json @@ -1,15 +1,15 @@ { "name": "@theia/cpp", - "version": "0.3.13", + "version": "0.3.14", "description": "Theia - Cpp Extension", "dependencies": { - "@theia/core": "^0.3.13", - "@theia/editor": "^0.3.13", - "@theia/filesystem": "^0.3.13", - "@theia/languages": "^0.3.13", - "@theia/monaco": "^0.3.13", - "@theia/preferences": "^0.3.13", - "@theia/process": "^0.3.13" + "@theia/core": "^0.3.14", + "@theia/editor": "^0.3.14", + "@theia/filesystem": "^0.3.14", + "@theia/languages": "^0.3.14", + "@theia/monaco": "^0.3.14", + "@theia/preferences": "^0.3.14", + "@theia/process": "^0.3.14" }, "publishConfig": { "access": "public" @@ -46,7 +46,7 @@ "docs": "theiaext docs" }, "devDependencies": { - "@theia/ext-scripts": "^0.3.13" + "@theia/ext-scripts": "^0.3.14" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/cpp/src/browser/cpp-build-configurations-statusbar-element.ts b/packages/cpp/src/browser/cpp-build-configurations-statusbar-element.ts index d15df92825bd4..3764ffa6c98a0 100644 --- a/packages/cpp/src/browser/cpp-build-configurations-statusbar-element.ts +++ b/packages/cpp/src/browser/cpp-build-configurations-statusbar-element.ts @@ -16,7 +16,8 @@ import { injectable, inject } from 'inversify'; import { StatusBar, StatusBarAlignment } from '@theia/core/lib/browser'; -import { CPP_CHANGE_BUILD_CONFIGURATION, CppBuildConfigurationManager } from './cpp-build-configurations'; +import { CppBuildConfigurationManager } from './cpp-build-configurations'; +import { CPP_CHANGE_BUILD_CONFIGURATION } from './cpp-build-configurations-ui'; @injectable() export class CppBuildConfigurationsStatusBarElement { @@ -47,7 +48,8 @@ export class CppBuildConfigurationsStatusBarElement { this.statusBar.setElement(this.cppIdentifier, { text: `$(wrench) C/C++ Build Config ${(activeConfig) ? activeConfig.name : ''}`, alignment: StatusBarAlignment.RIGHT, - command: CPP_CHANGE_BUILD_CONFIGURATION.id + command: CPP_CHANGE_BUILD_CONFIGURATION.id, + priority: 0.5, }); } diff --git a/packages/cpp/src/browser/cpp-build-configurations-ui.ts b/packages/cpp/src/browser/cpp-build-configurations-ui.ts new file mode 100644 index 0000000000000..07c05881252b0 --- /dev/null +++ b/packages/cpp/src/browser/cpp-build-configurations-ui.ts @@ -0,0 +1,158 @@ +/******************************************************************************** + * Copyright (C) 2018 Ericsson 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 + ********************************************************************************/ + +import { Command, CommandContribution, CommandRegistry, CommandService } from '@theia/core'; +import { injectable, inject } from 'inversify'; +import { QuickOpenService } from '@theia/core/lib/browser/quick-open/quick-open-service'; +import { QuickOpenModel, QuickOpenItem, QuickOpenMode, } from '@theia/core/lib/browser/quick-open/quick-open-model'; +import { FileSystem, FileSystemUtils } from '@theia/filesystem/lib/common'; +import URI from '@theia/core/lib/common/uri'; +import { PreferenceScope, PreferenceService } from '@theia/preferences/lib/browser'; +import { CppBuildConfigurationManager, CppBuildConfiguration } from './cpp-build-configurations'; +import { EditorManager } from '@theia/editor/lib/browser'; +import { CommonCommands } from '@theia/core/lib/browser'; + +@injectable() +export class CppBuildConfigurationChanger implements QuickOpenModel { + + @inject(CommandService) + protected readonly commandService: CommandService; + + @inject(CppBuildConfigurationManager) + protected readonly cppBuildConfigurations: CppBuildConfigurationManager; + + @inject(EditorManager) + protected readonly editorManager: EditorManager; + + @inject(FileSystem) + protected readonly fileSystem: FileSystem; + + @inject(QuickOpenService) + protected readonly quickOpenService: QuickOpenService; + + @inject(PreferenceService) + protected readonly preferenceService: PreferenceService; + + readonly createItem: QuickOpenItem = new QuickOpenItem({ + label: 'Create New', + iconClass: 'fa fa-plus', + description: 'Create a new build configuration', + run: (mode: QuickOpenMode): boolean => { + if (mode !== QuickOpenMode.OPEN) { + return false; + } + this.createConfig(); + return true; + }, + }); + + readonly resetItem: QuickOpenItem = new QuickOpenItem({ + label: 'None', + iconClass: 'fa fa-times', + description: 'Reset active build configuration', + run: (mode: QuickOpenMode): boolean => { + if (mode !== QuickOpenMode.OPEN) { + return false; + } + this.cppBuildConfigurations.setActiveConfig(undefined); + return true; + }, + }); + + async onType(lookFor: string, acceptor: (items: QuickOpenItem[]) => void): Promise { + const items: QuickOpenItem[] = []; + const active: CppBuildConfiguration | undefined = this.cppBuildConfigurations.getActiveConfig(); + const configurations = this.cppBuildConfigurations.getValidConfigs(); + + const homeStat = await this.fileSystem.getCurrentUserHome(); + const home = (homeStat) ? new URI(homeStat.uri).withoutScheme().toString() : undefined; + + // Item to create a new build configuration + items.push(this.createItem); + + // Only return 'Create New' when no build configurations present + if (!configurations.length) { + return acceptor(items); + } + + // Item to de-select any active build config + if (active) { + items.push(this.resetItem); + } + + // Add one item per build config + configurations.forEach(config => { + const uri = new URI(config.directory); + items.push(new QuickOpenItem({ + label: config.name, + // add an icon for active build config, and an empty placeholder for all others + iconClass: (config === active) ? 'fa fa-check' : 'fa fa-empty-item', + description: (home) ? FileSystemUtils.tildifyPath(uri.path.toString(), home) : uri.path.toString(), + run: (mode: QuickOpenMode): boolean => { + if (mode !== QuickOpenMode.OPEN) { + return false; + } + + this.cppBuildConfigurations.setActiveConfig(config); + return true; + }, + })); + }); + + acceptor(items); + } + + open() { + const configs = this.cppBuildConfigurations.getValidConfigs(); + this.quickOpenService.open(this, { + placeholder: (configs.length) ? 'Choose a build configuration...' : 'No build configurations present', + fuzzyMatchLabel: true, + fuzzyMatchDescription: true, + }); + } + + /** Create a new build configuration with placeholder values. */ + async createConfig(): Promise { + this.commandService.executeCommand(CommonCommands.OPEN_PREFERENCES.id, PreferenceScope.Workspace); + const configs = this.cppBuildConfigurations.getConfigs(); + const newConfigs = configs.slice(0); + newConfigs.push({ name: '', directory: '' }); + await this.preferenceService.set(this.cppBuildConfigurations.BUILD_CONFIGURATIONS_PREFERENCE_KEY, newConfigs, PreferenceScope.Workspace); + } + +} + +/** + * Open the quick open menu to let the user change the active build + * configuration. + */ +export const CPP_CHANGE_BUILD_CONFIGURATION: Command = { + id: 'cpp.change-build-configuration', + label: 'C/C++: Change Build Configuration' +}; + +@injectable() +export class CppBuildConfigurationsContributions implements CommandContribution { + + @inject(CppBuildConfigurationChanger) + protected readonly cppChangeBuildConfiguration: CppBuildConfigurationChanger; + + registerCommands(commands: CommandRegistry): void { + commands.registerCommand(CPP_CHANGE_BUILD_CONFIGURATION, { + execute: () => this.cppChangeBuildConfiguration.open() + }); + } +} diff --git a/packages/cpp/src/browser/cpp-build-configurations.ts b/packages/cpp/src/browser/cpp-build-configurations.ts index 67960812cb755..6606093cb306f 100644 --- a/packages/cpp/src/browser/cpp-build-configurations.ts +++ b/packages/cpp/src/browser/cpp-build-configurations.ts @@ -14,15 +14,16 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { Command, CommandContribution, CommandRegistry, Event, Emitter } from '@theia/core'; import { injectable, inject, postConstruct } from 'inversify'; -import { QuickOpenService } from '@theia/core/lib/browser/quick-open/quick-open-service'; -import { QuickOpenModel, QuickOpenItem, QuickOpenMode, } from '@theia/core/lib/browser/quick-open/quick-open-model'; -import { StorageService } from '@theia/core/lib/browser/storage-service'; +import { Emitter, Event } from '@theia/core'; import { CppPreferences } from './cpp-preferences'; +import { StorageService } from '@theia/core/lib/browser/storage-service'; export interface CppBuildConfiguration { + /** Human-readable configuration name. */ name: string; + + /** Base directory of this build. */ directory: string; } @@ -37,12 +38,13 @@ class SavedActiveBuildConfiguration { */ @injectable() export class CppBuildConfigurationManager { - @inject(StorageService) - protected readonly storageService: StorageService; @inject(CppPreferences) protected readonly cppPreferences: CppPreferences; + @inject(StorageService) + protected readonly storageService: StorageService; + /** * The current active build configuration. undefined means there's not * current active configuration. @@ -117,92 +119,11 @@ export class CppBuildConfigurationManager { getConfigs(): CppBuildConfiguration[] { return this.cppPreferences[this.BUILD_CONFIGURATIONS_PREFERENCE_KEY] || []; } -} - -@injectable() -export class CppBuildConfigurationChanger implements QuickOpenModel { - - @inject(QuickOpenService) - protected readonly quickOpenService: QuickOpenService; - - @inject(CppBuildConfigurationManager) - protected readonly cppBuildConfigurations: CppBuildConfigurationManager; - - async onType(lookFor: string, acceptor: (items: QuickOpenItem[]) => void): Promise { - const items: QuickOpenItem[] = []; - const active: CppBuildConfiguration | undefined = this.cppBuildConfigurations.getActiveConfig(); - const configurations = Array.from(this.cppBuildConfigurations.getConfigs()).sort(); - - // Add feedback item when no configurations are present - if (!configurations.length) { - items.push(new QuickOpenItem({ - label: 'No build configurations available', - run: () => false, - })); - return acceptor(items); - } - // Item to de-select any active build config - if (active) { - items.push(new QuickOpenItem({ - label: 'None', - detail: 'Reset active build configuration', - run: (mode: QuickOpenMode): boolean => { - if (mode !== QuickOpenMode.OPEN) { - return false; - } - this.cppBuildConfigurations.setActiveConfig(undefined); - return true; - }, - })); - } - - // Add one item per build config. - configurations.forEach(config => { - items.push(new QuickOpenItem({ - label: config.name + (config === active ? ' ✔' : ''), - detail: config.directory, - run: (mode: QuickOpenMode): boolean => { - if (mode !== QuickOpenMode.OPEN) { - return false; - } - - this.cppBuildConfigurations.setActiveConfig(config); - return true; - }, - })); - }); - - acceptor(items); - } - - open() { - this.quickOpenService.open(this, { - placeholder: 'Choose a build configuration...', - fuzzyMatchLabel: true, - fuzzyMatchDescription: true, - }); - } -} - -/** - * Open the quick open menu to let the user change the active build - * configuration. - */ -export const CPP_CHANGE_BUILD_CONFIGURATION: Command = { - id: 'cpp.change-build-configuration', - label: 'C/C++: Change Build Configuration' -}; - -@injectable() -export class CppBuildConfigurationsContributions implements CommandContribution { - - @inject(CppBuildConfigurationChanger) - protected readonly cppChangeBuildConfiguration: CppBuildConfigurationChanger; - - registerCommands(commands: CommandRegistry): void { - commands.registerCommand(CPP_CHANGE_BUILD_CONFIGURATION, { - execute: () => this.cppChangeBuildConfiguration.open() - }); + /** Get the list of valid defined build configurations. */ + getValidConfigs(): CppBuildConfiguration[] { + return Array.from(this.getConfigs()) + .filter(a => a.name !== '' && a.directory !== '') + .sort((a, b) => (a.name.localeCompare(b.name))); } } diff --git a/packages/cpp/src/browser/cpp-frontend-module.ts b/packages/cpp/src/browser/cpp-frontend-module.ts index cb2e5a543a650..eac6aa53a5427 100644 --- a/packages/cpp/src/browser/cpp-frontend-module.ts +++ b/packages/cpp/src/browser/cpp-frontend-module.ts @@ -23,7 +23,8 @@ import { LanguageClientContribution } from '@theia/languages/lib/browser'; import { CppLanguageClientContribution } from './cpp-language-client-contribution'; import { CppKeybindingContribution, CppKeybindingContext } from './cpp-keybinding'; import { bindCppPreferences } from './cpp-preferences'; -import { CppBuildConfigurationsContributions, CppBuildConfigurationChanger, CppBuildConfigurationManager } from './cpp-build-configurations'; +import { CppBuildConfigurationsContributions, CppBuildConfigurationChanger } from './cpp-build-configurations-ui'; +import { CppBuildConfigurationManager } from './cpp-build-configurations'; import { LanguageGrammarDefinitionContribution } from '@theia/monaco/lib/browser/textmate'; import { CppGrammarContribution } from './cpp-grammar-contribution'; import { CppBuildConfigurationsStatusBarElement } from './cpp-build-configurations-statusbar-element'; diff --git a/packages/cpp/src/browser/cpp-grammar-contribution.ts b/packages/cpp/src/browser/cpp-grammar-contribution.ts index 3e61c9d358379..1e24bdc934dd4 100644 --- a/packages/cpp/src/browser/cpp-grammar-contribution.ts +++ b/packages/cpp/src/browser/cpp-grammar-contribution.ts @@ -63,7 +63,7 @@ export class CppGrammarContribution implements LanguageGrammarDefinitionContribu monaco.languages.setLanguageConfiguration(C_LANGUAGE_ID, this.config); const platformGrammar = require('../../data/platform.tmLanguage.json'); - registry.registerTextMateGrammarScope('source.c.platform', { + registry.registerTextmateGrammarScope('source.c.platform', { async getGrammarDefinition() { return { format: 'json', @@ -73,7 +73,7 @@ export class CppGrammarContribution implements LanguageGrammarDefinitionContribu }); const cGrammar = require('../../data/c.tmLanguage.json'); - registry.registerTextMateGrammarScope('source.c', { + registry.registerTextmateGrammarScope('source.c', { async getGrammarDefinition() { return { format: 'json', @@ -93,7 +93,7 @@ export class CppGrammarContribution implements LanguageGrammarDefinitionContribu monaco.languages.setLanguageConfiguration(CPP_LANGUAGE_ID, this.config); const cppGrammar = require('../../data/cpp.tmLanguage.json'); - registry.registerTextMateGrammarScope('source.cpp', { + registry.registerTextmateGrammarScope('source.cpp', { async getGrammarDefinition() { return { format: 'json', diff --git a/packages/cpp/src/browser/cpp-preferences.ts b/packages/cpp/src/browser/cpp-preferences.ts index 967319e0544db..4bb39d20696a7 100644 --- a/packages/cpp/src/browser/cpp-preferences.ts +++ b/packages/cpp/src/browser/cpp-preferences.ts @@ -35,10 +35,17 @@ export const cppPreferencesSchema: PreferenceSchema = { } }, required: ['name', 'directory'], - } + }, + default: [ + { + name: '', + directory: '' + } + ], }, 'cpp.experimentalCommands': { description: 'Enable experimental commands mostly intended for Clangd developers.', + default: false, type: 'boolean' } } diff --git a/packages/debug-nodejs/package.json b/packages/debug-nodejs/package.json index 98cbefd0f7e8d..111af3198ab54 100644 --- a/packages/debug-nodejs/package.json +++ b/packages/debug-nodejs/package.json @@ -1,9 +1,9 @@ { "name": "@theia/debug-nodejs", - "version": "0.3.13", + "version": "0.3.14", "description": "Theia - NodeJS Debug Extension", "dependencies": { - "@theia/debug": "^0.3.13", + "@theia/debug": "^0.3.14", "vscode-debugprotocol": "^1.26.0" }, "publishConfig": { @@ -17,7 +17,7 @@ "keywords": [ "theia-extension, debug, nodejs" ], - "license": "Apache-2.0", + "license": "EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0", "repository": { "type": "git", "url": "https://github.com/theia-ide/theia.git" @@ -40,7 +40,7 @@ "docs": "theiaext docs" }, "devDependencies": { - "@theia/ext-scripts": "^0.3.13" + "@theia/ext-scripts": "^0.3.14" }, "nyc": { "extends": "../../configs/nyc.json" @@ -49,4 +49,4 @@ "downloadUrl": "https://github.com/tolusha/node-debug/releases/download/v1.23.5/vscode-node-debug.tar.gz", "dir": "lib/adapter" } -} \ No newline at end of file +} diff --git a/packages/debug/package.json b/packages/debug/package.json index 9d0c1d576d0d6..f5bcf8e728d0d 100644 --- a/packages/debug/package.json +++ b/packages/debug/package.json @@ -1,12 +1,12 @@ { "name": "@theia/debug", - "version": "0.3.13", + "version": "0.3.14", "description": "Theia - Debug Extension", "dependencies": { - "@theia/core": "^0.3.13", - "@theia/editor": "^0.3.13", - "@theia/monaco": "^0.3.13", - "@theia/process": "^0.3.13", + "@theia/core": "^0.3.14", + "@theia/editor": "^0.3.14", + "@theia/monaco": "^0.3.14", + "@theia/process": "^0.3.14", "vscode-debugprotocol": "^1.26.0" }, "publishConfig": { @@ -21,7 +21,7 @@ "keywords": [ "theia-extension, debug" ], - "license": "Apache-2.0", + "license": "EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0", "repository": { "type": "git", "url": "https://github.com/theia-ide/theia.git" @@ -42,9 +42,9 @@ "docs": "theiaext docs" }, "devDependencies": { - "@theia/ext-scripts": "^0.3.13" + "@theia/ext-scripts": "^0.3.14" }, "nyc": { "extends": "../../configs/nyc.json" } -} \ No newline at end of file +} diff --git a/packages/debug/src/browser/breakpoint/breakpoint-manager.ts b/packages/debug/src/browser/breakpoint/breakpoint-manager.ts index f2ca7a60a5d51..6fe1dad254dd2 100644 --- a/packages/debug/src/browser/breakpoint/breakpoint-manager.ts +++ b/packages/debug/src/browser/breakpoint/breakpoint-manager.ts @@ -274,7 +274,7 @@ export class BreakpointsManager implements FrontendApplicationContribution { case MouseTargetType.GUTTER_GLYPH_MARGIN: case MouseTargetType.GUTTER_VIEW_ZONE: { const source = DebugUtils.toSource(editor.uri, undefined); - if (DebugUtils.checkPattern(source, this.supportedFilePatterns)) { + if (DebugUtils.checkPattern(source, this.supportedFilePatterns) && event.target.position) { this.toggleBreakpoint(editor, event.target.position); } break; diff --git a/packages/debug/src/browser/debug-command.ts b/packages/debug/src/browser/debug-command.ts index 15d57622cb50e..c93330ae9d910 100644 --- a/packages/debug/src/browser/debug-command.ts +++ b/packages/debug/src/browser/debug-command.ts @@ -30,37 +30,24 @@ export const DEBUG_SESSION_THREAD_CONTEXT_MENU: MenuPath = ['debug-session-threa export const DEBUG_VARIABLE_CONTEXT_MENU: MenuPath = ['debug-variable-context-menu']; export namespace DebugSessionContextMenu { - export const STOP = [...DEBUG_SESSION_CONTEXT_MENU, '1_stop']; + export const DEBUG_CONTROLS = [...DEBUG_SESSION_CONTEXT_MENU, '1_controls']; } export namespace DebugThreadContextMenu { - export const RESUME_THREAD = [...DEBUG_SESSION_THREAD_CONTEXT_MENU, '2_resume']; - export const SUSPEND_THREAD = [...RESUME_THREAD, '1_suspend']; - - export const STEPOUT_THREAD = [...DEBUG_SESSION_THREAD_CONTEXT_MENU, '5_stepout']; - export const STEPIN_THREAD = [...STEPOUT_THREAD, '4_stepin']; - export const STEP_THREAD = [...STEPIN_THREAD, '3_next']; + export const DEBUG_PLAYER = [...DEBUG_SESSION_THREAD_CONTEXT_MENU, '2_player']; + export const DEBUG_STEPPING = [...DEBUG_SESSION_THREAD_CONTEXT_MENU, '3_stepping']; } export namespace DebugVariableContextMenu { - export const MODIFY = [...DEBUG_VARIABLE_CONTEXT_MENU, '1_modify']; + export const DEBUG_EDITION = [...DEBUG_VARIABLE_CONTEXT_MENU, '1_edition']; } export namespace DebugMenus { export const DEBUG = [...MAIN_MENU_BAR, '4_debug']; - export const DEBUG_STOP = [...DEBUG, '2_stop']; - export const DEBUG_START = [...DEBUG_STOP, '1_start']; - - export const SUSPEND_ALL_THREADS = [...DEBUG, '4_suspend_all_threads']; - export const RESUME_ALL_THREADS = [...SUSPEND_ALL_THREADS, '3_resume_all_threads']; - - export const STEPOUT_THREAD = [...DEBUG, '7_stepout']; - export const STEPIN_THREAD = [...STEPOUT_THREAD, '6_stepin']; - export const STEP_THREAD = [...STEPIN_THREAD, '5_next']; - - export const ADD_CONFIGURATION = [...DEBUG, '9_add_configuration']; - export const OPEN_CONFIGURATION = [...ADD_CONFIGURATION, '8_open_configuration']; - export const SHOW_BREAKPOINTS = [...OPEN_CONFIGURATION, '10_breakpoinst']; + export const DEBUG_CONTROLS = [...DEBUG, '1_controls']; + export const DEBUG_THREADS = [...DEBUG, '2_threads']; + export const DEBUG_STEPPING = [...DEBUG, '3_stepping']; + export const DEBUG_CONFIGURATION = [...DEBUG, '4_configuration']; } export namespace DEBUG_COMMANDS { @@ -136,60 +123,88 @@ export class DebugCommandHandlers implements MenuContribution, CommandContributi @inject(DebugSessionManager) protected readonly debugSessionManager: DebugSessionManager, @inject(DebugConfigurationManager) protected readonly debugConfigurationManager: DebugConfigurationManager, @inject(DebugSelectionService) protected readonly debugSelectionHandler: DebugSelectionService, - @inject(BreakpointsDialog) protected readonly breakpointsDialog: BreakpointsDialog) { } + @inject(BreakpointsDialog) protected readonly breakpointsDialog: BreakpointsDialog + ) { } registerMenus(menus: MenuModelRegistry): void { menus.registerSubmenu(DebugMenus.DEBUG, 'Debug'); - menus.registerMenuAction(DebugMenus.DEBUG_START, { - commandId: DEBUG_COMMANDS.START.id + + menus.registerMenuAction(DebugMenus.DEBUG_CONTROLS, { + commandId: DEBUG_COMMANDS.START.id, + order: '1_start', }); - menus.registerMenuAction(DebugMenus.DEBUG_STOP, { - commandId: DEBUG_COMMANDS.STOP.id + menus.registerMenuAction(DebugMenus.DEBUG_CONTROLS, { + commandId: DEBUG_COMMANDS.STOP.id, + order: '2_stop', }); - menus.registerMenuAction(DebugMenus.OPEN_CONFIGURATION, { - commandId: DEBUG_COMMANDS.OPEN_CONFIGURATION.id + + menus.registerMenuAction(DebugMenus.DEBUG_THREADS, { + commandId: DEBUG_COMMANDS.RESUME_ALL_THREADS.id, + order: '3_resume_all_threads', }); - menus.registerMenuAction(DebugMenus.ADD_CONFIGURATION, { - commandId: DEBUG_COMMANDS.ADD_CONFIGURATION.id + menus.registerMenuAction(DebugMenus.DEBUG_THREADS, { + commandId: DEBUG_COMMANDS.SUSPEND_ALL_THREADS.id, + order: '4_suspend_all_threads', }); - menus.registerMenuAction(DebugSessionContextMenu.STOP, { - commandId: DEBUG_COMMANDS.STOP.id + + menus.registerMenuAction(DebugMenus.DEBUG_STEPPING, { + commandId: DEBUG_COMMANDS.STEP.id, + order: '5_next', }); - menus.registerMenuAction(DebugMenus.STEP_THREAD, { - commandId: DEBUG_COMMANDS.STEP.id + menus.registerMenuAction(DebugMenus.DEBUG_STEPPING, { + commandId: DEBUG_COMMANDS.STEPIN.id, + order: '6_stepin', }); - menus.registerMenuAction(DebugMenus.STEPIN_THREAD, { - commandId: DEBUG_COMMANDS.STEPIN.id + menus.registerMenuAction(DebugMenus.DEBUG_STEPPING, { + commandId: DEBUG_COMMANDS.STEPOUT.id, + order: '7_stepout', }); - menus.registerMenuAction(DebugMenus.STEPOUT_THREAD, { - commandId: DEBUG_COMMANDS.STEPOUT.id + + menus.registerMenuAction(DebugMenus.DEBUG_CONFIGURATION, { + commandId: DEBUG_COMMANDS.OPEN_CONFIGURATION.id, + order: '8_open_configuration', }); - menus.registerMenuAction(DebugMenus.SUSPEND_ALL_THREADS, { - commandId: DEBUG_COMMANDS.SUSPEND_ALL_THREADS.id + menus.registerMenuAction(DebugMenus.DEBUG_CONFIGURATION, { + commandId: DEBUG_COMMANDS.ADD_CONFIGURATION.id, + order: '9_add_configuration', }); - menus.registerMenuAction(DebugMenus.RESUME_ALL_THREADS, { - commandId: DEBUG_COMMANDS.RESUME_ALL_THREADS.id + menus.registerMenuAction(DebugMenus.DEBUG_CONFIGURATION, { + commandId: DEBUG_COMMANDS.SHOW_BREAKPOINTS.id, + order: '10_breakpoints', }); - menus.registerMenuAction(DebugMenus.SHOW_BREAKPOINTS, { - commandId: DEBUG_COMMANDS.SHOW_BREAKPOINTS.id + + // debug session context + menus.registerMenuAction(DebugSessionContextMenu.DEBUG_CONTROLS, { + commandId: DEBUG_COMMANDS.STOP.id, + order: '1_stop', }); - menus.registerMenuAction(DebugThreadContextMenu.SUSPEND_THREAD, { - commandId: DEBUG_COMMANDS.SUSPEND_THREAD.id + + // thread context + menus.registerMenuAction(DebugThreadContextMenu.DEBUG_PLAYER, { + commandId: DEBUG_COMMANDS.SUSPEND_THREAD.id, + order: '1_suspend', }); - menus.registerMenuAction(DebugThreadContextMenu.RESUME_THREAD, { - commandId: DEBUG_COMMANDS.RESUME_THREAD.id + menus.registerMenuAction(DebugThreadContextMenu.DEBUG_PLAYER, { + commandId: DEBUG_COMMANDS.RESUME_THREAD.id, + order: '2_resume', }); - menus.registerMenuAction(DebugThreadContextMenu.STEP_THREAD, { - commandId: DEBUG_COMMANDS.STEP.id + menus.registerMenuAction(DebugThreadContextMenu.DEBUG_STEPPING, { + commandId: DEBUG_COMMANDS.STEP.id, + order: '3_next', }); - menus.registerMenuAction(DebugThreadContextMenu.STEPIN_THREAD, { - commandId: DEBUG_COMMANDS.STEPIN.id + menus.registerMenuAction(DebugThreadContextMenu.DEBUG_STEPPING, { + commandId: DEBUG_COMMANDS.STEPIN.id, + order: '4_stepin', }); - menus.registerMenuAction(DebugThreadContextMenu.STEPOUT_THREAD, { - commandId: DEBUG_COMMANDS.STEPOUT.id + menus.registerMenuAction(DebugThreadContextMenu.DEBUG_STEPPING, { + commandId: DEBUG_COMMANDS.STEPOUT.id, + order: '5_stepout', }); - menus.registerMenuAction(DebugVariableContextMenu.MODIFY, { - commandId: DEBUG_COMMANDS.MODIFY_VARIABLE.id + + // variable context + menus.registerMenuAction(DebugVariableContextMenu.DEBUG_EDITION, { + commandId: DEBUG_COMMANDS.MODIFY_VARIABLE.id, + order: '1_modify', }); } diff --git a/packages/debug/src/node/debug-adapter.ts b/packages/debug/src/node/debug-adapter.ts index 9150c9193a3cb..ad18cbe955026 100644 --- a/packages/debug/src/node/debug-adapter.ts +++ b/packages/debug/src/node/debug-adapter.ts @@ -22,6 +22,7 @@ // Some entities copied and modified from https://github.com/Microsoft/vscode-debugadapter-node/blob/master/adapter/src/protocol.ts import * as WebSocket from 'ws'; +import * as net from 'net'; import { injectable, inject } from 'inversify'; import { ILogger, DisposableCollection, Disposable } from '@theia/core'; import { Deferred } from '@theia/core/lib/common/promise-util'; @@ -95,6 +96,15 @@ export class LaunchBasedDebugAdapterFactory implements DebugAdapterFactory { return this.processFactory({ command: command, args: args }); } + + connect(debugServerPort: number): CommunicationProvider { + const socket = net.createConnection(debugServerPort); + return { + input: socket, + output: socket, + dispose: () => socket.end() + }; + } } /** diff --git a/packages/debug/src/node/debug-model.ts b/packages/debug/src/node/debug-model.ts index 5aaa55b344772..25ff82ce31ebb 100644 --- a/packages/debug/src/node/debug-model.ts +++ b/packages/debug/src/node/debug-model.ts @@ -87,6 +87,7 @@ export const DebugAdapterFactory = Symbol('DebugAdapterFactory'); */ export interface DebugAdapterFactory { start(executable: DebugAdapterExecutable): CommunicationProvider; + connect(debugServerPort: number): CommunicationProvider; } /** diff --git a/packages/debug/src/node/debug-service.ts b/packages/debug/src/node/debug-service.ts index e87cb95c31d5b..63b74f61613e8 100644 --- a/packages/debug/src/node/debug-service.ts +++ b/packages/debug/src/node/debug-service.ts @@ -126,8 +126,13 @@ export class DebugAdapterSessionManager { create(config: DebugConfiguration): DebugAdapterSession { const sessionId = UUID.uuid4(); - const executable = this.registry.provideDebugAdapterExecutable(config); - const communicationProvider = this.debugAdapterFactory.start(executable); + let communicationProvider; + if ('debugServer' in config) { + communicationProvider = this.debugAdapterFactory.connect(config.debugServer); + } else { + const executable = this.registry.provideDebugAdapterExecutable(config); + communicationProvider = this.debugAdapterFactory.start(executable); + } const sessionFactory = this.registry.debugAdapterSessionFactory(config.type) || this.debugAdapterSessionFactory; const session = sessionFactory.get(sessionId, communicationProvider); diff --git a/packages/editor/package.json b/packages/editor/package.json index 01d096c748f69..b5a6834b1d7c8 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -1,11 +1,11 @@ { "name": "@theia/editor", - "version": "0.3.13", + "version": "0.3.14", "description": "Theia - Editor Extension", "dependencies": { - "@theia/core": "^0.3.13", - "@theia/languages": "^0.3.13", - "@theia/variable-resolver": "^0.3.13", + "@theia/core": "^0.3.14", + "@theia/languages": "^0.3.14", + "@theia/variable-resolver": "^0.3.14", "@types/base64-arraybuffer": "0.1.0", "base64-arraybuffer": "^0.1.5" }, @@ -42,9 +42,9 @@ "docs": "theiaext docs" }, "devDependencies": { - "@theia/ext-scripts": "^0.3.13" + "@theia/ext-scripts": "^0.3.14" }, "nyc": { "extends": "../../configs/nyc.json" } -} \ No newline at end of file +} diff --git a/packages/editor/src/browser/editor-preferences.ts b/packages/editor/src/browser/editor-preferences.ts index 4aee0adcb0a37..b57c0ccce1f77 100644 --- a/packages/editor/src/browser/editor-preferences.ts +++ b/packages/editor/src/browser/editor-preferences.ts @@ -23,6 +23,7 @@ import { PreferenceSchema, PreferenceChangeEvent } from '@theia/core/lib/browser/preferences'; +import { isOSX } from '@theia/core/lib/common/os'; export const editorPreferenceSchema: PreferenceSchema = { 'type': 'object', @@ -35,7 +36,7 @@ export const editorPreferenceSchema: PreferenceSchema = { }, 'editor.fontSize': { 'type': 'number', - 'default': 12, + 'default': (isOSX) ? 12 : 14, 'description': 'Configure the editor font size' }, 'editor.lineNumbers': { @@ -43,6 +44,7 @@ export const editorPreferenceSchema: PreferenceSchema = { 'on', 'off' ], + 'default': 'on', 'description': 'Control the rendering of line numbers' }, 'editor.renderWhitespace': { @@ -51,6 +53,7 @@ export const editorPreferenceSchema: PreferenceSchema = { 'boundary', 'all' ], + 'default': 'none', 'description': 'Control the rendering of whitespaces in the editor' }, 'editor.autoSave': { @@ -243,6 +246,7 @@ export const editorPreferenceSchema: PreferenceSchema = { }, 'editor.emptySelectionClipboard': { 'type': 'boolean', + 'default': true, 'description': 'Copying without a selection copies the current line.' }, 'editor.wordBasedSuggestions': { @@ -297,6 +301,7 @@ export const editorPreferenceSchema: PreferenceSchema = { }, 'editor.useTabStops': { 'type': 'boolean', + 'default': true, 'description': 'Inserting and deleting whitespace follows tab stops.' }, 'editor.insertSpaces': { diff --git a/packages/editor/src/browser/editor.ts b/packages/editor/src/browser/editor.ts index 0f046f9f86eef..9467d0da7f028 100644 --- a/packages/editor/src/browser/editor.ts +++ b/packages/editor/src/browser/editor.ts @@ -126,7 +126,7 @@ export interface MouseTarget { /** * The 'approximate' editor position */ - readonly position: Position; + readonly position?: Position; /** * Desired mouse column (e.g. when position.column gets clamped to text length -- clicking after text on a line). */ @@ -134,7 +134,7 @@ export interface MouseTarget { /** * The 'approximate' editor range */ - readonly range: Range; + readonly range?: Range; /** * Some extra detail. */ diff --git a/packages/editorconfig/package.json b/packages/editorconfig/package.json index 7b57e09f4ffe0..2bff616cdc8a4 100644 --- a/packages/editorconfig/package.json +++ b/packages/editorconfig/package.json @@ -1,11 +1,11 @@ { "name": "@theia/editorconfig", - "version": "0.3.13", + "version": "0.3.14", "description": "Theia - Editorconfig Extension", "dependencies": { - "@theia/core": "^0.3.13", - "@theia/editor": "^0.3.13", - "@theia/monaco": "^0.3.13", + "@theia/core": "^0.3.14", + "@theia/editor": "^0.3.14", + "@theia/monaco": "^0.3.14", "editorconfig": "^0.15.0" }, "publishConfig": { @@ -42,7 +42,7 @@ "docs": "theiaext docs" }, "devDependencies": { - "@theia/ext-scripts": "^0.3.13" + "@theia/ext-scripts": "^0.3.14" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/editorconfig/src/browser/editorconfig-document-manager.ts b/packages/editorconfig/src/browser/editorconfig-document-manager.ts index 2d22ee1c5da36..36c5458e154e3 100644 --- a/packages/editorconfig/src/browser/editorconfig-document-manager.ts +++ b/packages/editorconfig/src/browser/editorconfig-document-manager.ts @@ -14,8 +14,9 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor'; import { injectable, inject, postConstruct } from 'inversify'; +import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor'; +import { TextDocumentSaveReason } from '@theia/languages/lib/browser'; import { EditorManager, EditorWidget, TextEditor } from '@theia/editor/lib/browser'; import { EditorconfigService } from '../common/editorconfig-interface'; import { KnownProps } from 'editorconfig'; @@ -66,7 +67,7 @@ export class EditorconfigDocumentManager { const properties = this.properties[uri]; const edits = []; - edits.push(...this.getEditsTrimmingTrailingWhitespaces(monacoEditor, properties)); + edits.push(...this.getEditsTrimmingTrailingWhitespaces(monacoEditor, properties, event.reason)); const edit = this.getEditInsertingFinalNewLine(monacoEditor, properties); if (edit) { @@ -216,9 +217,17 @@ export class EditorconfigDocumentManager { * @param editor editor * @param properties editorconfig properties */ - private getEditsTrimmingTrailingWhitespaces(editor: MonacoEditor, properties: KnownProps): monaco.editor.IIdentifiedSingleEditOperation[] { + private getEditsTrimmingTrailingWhitespaces(editor: MonacoEditor, properties: KnownProps, saveReason?: TextDocumentSaveReason): monaco.editor.IIdentifiedSingleEditOperation[] { const edits = []; + if (MonacoEditor.get(this.editorManager.activeEditor) === editor) { + const trimReason = (saveReason !== TextDocumentSaveReason.Manual) ? 'auto-save' : undefined; + editor.commandService.executeCommand('editor.action.trimTrailingWhitespace', { + reason: trimReason + }); + return []; + } + if (this.isSet(properties.trim_trailing_whitespace)) { const lines = editor.document.lineCount; for (let i = 1; i <= lines; i++) { diff --git a/packages/extension-manager/package.json b/packages/extension-manager/package.json index 45550f4838124..e1bc2413dfa38 100644 --- a/packages/extension-manager/package.json +++ b/packages/extension-manager/package.json @@ -1,11 +1,11 @@ { "name": "@theia/extension-manager", - "version": "0.3.13", + "version": "0.3.14", "description": "Theia - Extension Manager", "dependencies": { - "@theia/application-manager": "^0.3.13", - "@theia/core": "^0.3.13", - "@theia/filesystem": "^0.3.13", + "@theia/application-manager": "^0.3.14", + "@theia/core": "^0.3.14", + "@theia/filesystem": "^0.3.14", "@types/fs-extra": "^4.0.2", "@types/sanitize-html": "^1.13.31", "@types/showdown": "^1.7.1", @@ -47,7 +47,7 @@ "docs": "theiaext docs" }, "devDependencies": { - "@theia/ext-scripts": "^0.3.13" + "@theia/ext-scripts": "^0.3.14" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/extension-manager/src/browser/extension-widget.tsx b/packages/extension-manager/src/browser/extension-widget.tsx index 93e3b3f8a1d8e..3612db9cc1b22 100644 --- a/packages/extension-manager/src/browser/extension-widget.tsx +++ b/packages/extension-manager/src/browser/extension-widget.tsx @@ -120,7 +120,7 @@ export class ExtensionWidget extends ReactWidget { const extensionButtonContainer = !extension.dependent ?
{this.createButton(extension)}
: 'installed via ' + extension.dependent; - return
this.extensionClick(extension)}> + return
this.extensionClick(extension)} title={this.getTooltip(extension)}>
{extension.name}
@@ -183,4 +183,8 @@ export class ExtensionWidget extends ReactWidget { {extension.busy ? : btnLabel}
; } + + protected getTooltip(extension: Extension): string { + return `${extension.name}\n${extension.description}`; + } } diff --git a/packages/file-search/package.json b/packages/file-search/package.json index 554dc51960ea9..a372293bcb3fd 100644 --- a/packages/file-search/package.json +++ b/packages/file-search/package.json @@ -1,12 +1,12 @@ { "name": "@theia/file-search", - "version": "0.3.13", + "version": "0.3.14", "description": "Theia - File Search Extension", "dependencies": { - "@theia/core": "^0.3.13", - "@theia/filesystem": "^0.3.13", - "@theia/editor": "^0.3.13", - "@theia/workspace": "^0.3.13", + "@theia/core": "^0.3.14", + "@theia/editor": "^0.3.14", + "@theia/filesystem": "^0.3.14", + "@theia/workspace": "^0.3.14", "fuzzy": "^0.1.3", "vscode-ripgrep": "^1.0.1" }, @@ -44,7 +44,7 @@ "docs": "theiaext docs" }, "devDependencies": { - "@theia/ext-scripts": "^0.3.13" + "@theia/ext-scripts": "^0.3.14" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/file-search/src/browser/file-search-frontend-module.ts b/packages/file-search/src/browser/file-search-frontend-module.ts index 1c566cafe988a..b603507d8e1c8 100644 --- a/packages/file-search/src/browser/file-search-frontend-module.ts +++ b/packages/file-search/src/browser/file-search-frontend-module.ts @@ -16,20 +16,21 @@ import { ContainerModule, interfaces } from 'inversify'; import { CommandContribution } from '@theia/core/lib/common'; -import { WebSocketConnectionProvider, KeybindingContribution } from '@theia/core/lib/browser'; +import { WebSocketConnectionProvider, KeybindingContribution, QuickOpenContribution } from '@theia/core/lib/browser'; import { QuickFileOpenFrontendContribution } from './quick-file-open-contribution'; import { QuickFileOpenService } from './quick-file-open'; import { fileSearchServicePath, FileSearchService } from '../common/file-search-service'; -export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Unbind, isBound: interfaces.IsBound, rebind: interfaces.Rebind) => { +export default new ContainerModule((bind: interfaces.Bind) => { bind(FileSearchService).toDynamicValue(ctx => { const provider = ctx.container.get(WebSocketConnectionProvider); return provider.createProxy(fileSearchServicePath); }).inSingletonScope(); bind(QuickFileOpenFrontendContribution).toSelf().inSingletonScope(); - bind(CommandContribution).toDynamicValue(ctx => ctx.container.get(QuickFileOpenFrontendContribution)); - bind(KeybindingContribution).toDynamicValue(ctx => ctx.container.get(QuickFileOpenFrontendContribution)); + [CommandContribution, KeybindingContribution, QuickOpenContribution].forEach(serviceIdentifier => + bind(serviceIdentifier).toService(QuickFileOpenFrontendContribution) + ); bind(QuickFileOpenService).toSelf().inSingletonScope(); }); diff --git a/packages/file-search/src/browser/quick-file-open-contribution.ts b/packages/file-search/src/browser/quick-file-open-contribution.ts index 47bd710b87fcc..cdf5addecd665 100644 --- a/packages/file-search/src/browser/quick-file-open-contribution.ts +++ b/packages/file-search/src/browser/quick-file-open-contribution.ts @@ -17,12 +17,13 @@ import { injectable, inject } from 'inversify'; import { QuickFileOpenService, quickFileOpen } from './quick-file-open'; import { CommandRegistry, CommandContribution } from '@theia/core/lib/common'; -import { KeybindingRegistry, KeybindingContribution } from '@theia/core/lib/browser'; +import { KeybindingRegistry, KeybindingContribution, QuickOpenContribution, QuickOpenHandlerRegistry } from '@theia/core/lib/browser'; @injectable() -export class QuickFileOpenFrontendContribution implements CommandContribution, KeybindingContribution { +export class QuickFileOpenFrontendContribution implements CommandContribution, KeybindingContribution, QuickOpenContribution { - constructor(@inject(QuickFileOpenService) protected readonly quickFileOpenService: QuickFileOpenService) { } + @inject(QuickFileOpenService) + protected readonly quickFileOpenService: QuickFileOpenService; registerCommands(commands: CommandRegistry): void { commands.registerCommand(quickFileOpen, { @@ -38,4 +39,7 @@ export class QuickFileOpenFrontendContribution implements CommandContribution, K }); } + registerQuickOpenHandlers(handlers: QuickOpenHandlerRegistry): void { + handlers.registerHandler(this.quickFileOpenService, true); + } } diff --git a/packages/file-search/src/browser/quick-file-open.ts b/packages/file-search/src/browser/quick-file-open.ts index b823b77e2f15c..8d7f03309ea14 100644 --- a/packages/file-search/src/browser/quick-file-open.ts +++ b/packages/file-search/src/browser/quick-file-open.ts @@ -16,8 +16,8 @@ import { inject, injectable } from 'inversify'; import { - QuickOpenModel, QuickOpenItem, QuickOpenMode, QuickOpenService, - OpenerService, KeybindingRegistry, Keybinding, QuickOpenGroupItem, QuickOpenGroupItemOptions, QuickOpenItemOptions + QuickOpenModel, QuickOpenItem, QuickOpenMode, PrefixQuickOpenService, + OpenerService, KeybindingRegistry, QuickOpenGroupItem, QuickOpenGroupItemOptions, QuickOpenItemOptions, QuickOpenHandler, QuickOpenOptions, Keybinding } from '@theia/core/lib/browser'; import { FileSystem } from '@theia/filesystem/lib/common/filesystem'; import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service'; @@ -35,7 +35,7 @@ export const quickFileOpen: Command = { }; @injectable() -export class QuickFileOpenService implements QuickOpenModel { +export class QuickFileOpenService implements QuickOpenModel, QuickOpenHandler { @inject(KeybindingRegistry) protected readonly keybindingRegistry: KeybindingRegistry; @@ -45,8 +45,8 @@ export class QuickFileOpenService implements QuickOpenModel { protected readonly workspaceService: WorkspaceService; @inject(OpenerService) protected readonly openerService: OpenerService; - @inject(QuickOpenService) - protected readonly quickOpenService: QuickOpenService; + @inject(PrefixQuickOpenService) + protected readonly quickOpenService: PrefixQuickOpenService; @inject(FileSearchService) protected readonly fileSearchService: FileSearchService; @inject(LabelProvider) @@ -69,17 +69,38 @@ export class QuickFileOpenService implements QuickOpenModel { */ protected currentLookFor: string = ''; - isEnabled(): boolean { - return this.workspaceService.opened; + readonly prefix: string = '...'; + + get description(): string { + return 'Open File'; } - open(): void { - let placeholderText = 'File name to search.'; + getModel(): QuickOpenModel { + return this; + } + + getOptions(): QuickOpenOptions { + let placeholder = 'File name to search.'; const keybinding = this.getKeyCommand(); if (keybinding) { - placeholderText += ` (Press ${keybinding} to show/hide ignored files)`; + placeholder += ` (Press ${keybinding} to show/hide ignored files)`; } + return { + placeholder, + fuzzyMatchLabel: true, + fuzzyMatchDescription: true, + onClose: () => { + this.isOpen = false; + this.cancelIndicator.cancel(); + } + }; + } + + isEnabled(): boolean { + return this.workspaceService.opened; + } + open(): void { // Triggering the keyboard shortcut while the dialog is open toggles // showing the ignored files. if (this.isOpen) { @@ -90,16 +111,7 @@ export class QuickFileOpenService implements QuickOpenModel { this.isOpen = true; } - this.quickOpenService.open(this, { - placeholder: placeholderText, - prefix: this.currentLookFor, - fuzzyMatchLabel: true, - fuzzyMatchDescription: true, - fuzzySort: false, - onClose: () => { - this.isOpen = false; - }, - }); + this.quickOpenService.open(this.currentLookFor); } /** diff --git a/packages/filesystem/package.json b/packages/filesystem/package.json index 5194323801b21..38f0f0db58e4b 100644 --- a/packages/filesystem/package.json +++ b/packages/filesystem/package.json @@ -1,9 +1,9 @@ { "name": "@theia/filesystem", - "version": "0.3.13", + "version": "0.3.14", "description": "Theia - FileSystem Extension", "dependencies": { - "@theia/core": "^0.3.13", + "@theia/core": "^0.3.14", "@types/base64-js": "^1.2.5", "@types/body-parser": "^1.17.0", "@types/fs-extra": "^4.0.2", @@ -64,7 +64,7 @@ "docs": "theiaext docs" }, "devDependencies": { - "@theia/ext-scripts": "^0.3.13" + "@theia/ext-scripts": "^0.3.14" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/filesystem/src/browser/file-dialog-service.ts b/packages/filesystem/src/browser/file-dialog-service.ts index d2aebe57c0958..c10cb6dc314c9 100644 --- a/packages/filesystem/src/browser/file-dialog-service.ts +++ b/packages/filesystem/src/browser/file-dialog-service.ts @@ -20,31 +20,50 @@ import { MaybeArray } from '@theia/core/lib/common'; import { LabelProvider } from '@theia/core/lib/browser'; import { FileSystem, FileStat } from '../common'; import { FileStatNode, DirNode } from './file-tree'; -import { OpenFileDialogFactory, OpenFileDialogProps } from './file-dialog'; +import { OpenFileDialogFactory, OpenFileDialogProps, SaveFileDialogFactory, SaveFileDialogProps } from './file-dialog'; @injectable() export class FileDialogService { @inject(FileSystem) protected readonly fileSystem: FileSystem; @inject(OpenFileDialogFactory) protected readonly openFileDialogFactory: OpenFileDialogFactory; @inject(LabelProvider) protected readonly labelProvider: LabelProvider; + @inject(SaveFileDialogFactory) protected readonly saveFileDialogFactory: SaveFileDialogFactory; - async show(props: OpenFileDialogProps & { canSelectMany: true }, folder?: FileStat): Promise | undefined>; - async show(props: OpenFileDialogProps, folder?: FileStat): Promise; - async show(props: OpenFileDialogProps, folder?: FileStat): Promise | undefined> { + async showOpenDialog(props: OpenFileDialogProps & { canSelectMany: true }, folder?: FileStat): Promise | undefined>; + async showOpenDialog(props: OpenFileDialogProps, folder?: FileStat): Promise; + async showOpenDialog(props: OpenFileDialogProps, folder?: FileStat): Promise | undefined> { const title = props.title || 'Open'; - const folderToOpen = folder || await this.fileSystem.getCurrentUserHome(); - if (folderToOpen) { - const rootUri = new URI(folderToOpen.uri).parent; + const rootNode = await this.getRootNode(folder); + if (rootNode) { + const dialog = this.openFileDialogFactory(Object.assign(props, { title })); + dialog.model.navigateTo(rootNode); + return await dialog.open(); + } + return undefined; + } + + async showSaveDialog(props: SaveFileDialogProps, folder?: FileStat): Promise { + const title = props.title || 'Save'; + const rootNode = await this.getRootNode(folder); + if (rootNode) { + const dialog = this.saveFileDialogFactory(Object.assign(props, { title })); + dialog.model.navigateTo(rootNode); + return await dialog.open(); + } + return undefined; + } + + protected async getRootNode(folderToOpen?: FileStat): Promise { + const folder = folderToOpen || await this.fileSystem.getCurrentUserHome(); + if (folder) { + const rootUri = new URI(folder.uri).parent; const name = this.labelProvider.getName(rootUri); const [rootStat, label] = await Promise.all([ this.fileSystem.getFileStat(rootUri.toString()), - this.labelProvider.getIcon(folderToOpen) + this.labelProvider.getIcon(folder) ]); if (rootStat) { - const rootNode = DirNode.createRoot(rootStat, name, label); - const dialog = this.openFileDialogFactory(Object.assign(props, { title })); - dialog.model.navigateTo(rootNode); - return await dialog.open(); + return DirNode.createRoot(rootStat, name, label); } } return undefined; diff --git a/packages/filesystem/src/browser/file-dialog/file-dialog-container.ts b/packages/filesystem/src/browser/file-dialog/file-dialog-container.ts index bb7e6014bd13d..2118428dbfc9b 100644 --- a/packages/filesystem/src/browser/file-dialog/file-dialog-container.ts +++ b/packages/filesystem/src/browser/file-dialog/file-dialog-container.ts @@ -38,24 +38,25 @@ export function createFileDialogContainer(parent: interfaces.Container): Contain return child; } -export function createOpenFileDialog(parent: interfaces.Container, props: OpenFileDialogProps): OpenFileDialog { +export function createOpenFileDialogContainer(parent: interfaces.Container, props: OpenFileDialogProps): Container { const container = createFileDialogContainer(parent); container.rebind(TreeProps).toConstantValue({ ...defaultTreeProps, - multiSelect: props.canSelectMany + multiSelect: props.canSelectMany, + search: true }); container.bind(OpenFileDialogProps).toConstantValue(props); container.bind(OpenFileDialog).toSelf(); - return container.get(OpenFileDialog); + return container; } -export function createSaveFileDialog(parent: interfaces.Container, props: SaveFileDialogProps): SaveFileDialog { +export function createSaveFileDialogContainer(parent: interfaces.Container, props: SaveFileDialogProps): Container { const container = createFileDialogContainer(parent); container.bind(SaveFileDialogProps).toConstantValue(props); container.bind(SaveFileDialog).toSelf(); - return container.get(SaveFileDialog); + return container; } diff --git a/packages/filesystem/src/browser/file-dialog/file-dialog.ts b/packages/filesystem/src/browser/file-dialog/file-dialog.ts index da7e32640f740..2ed205b942c23 100644 --- a/packages/filesystem/src/browser/file-dialog/file-dialog.ts +++ b/packages/filesystem/src/browser/file-dialog/file-dialog.ts @@ -25,6 +25,7 @@ import { FileDialogModel } from './file-dialog-model'; import { FileDialogWidget } from './file-dialog-widget'; import { FileDialogTreeFiltersRenderer, FileDialogTreeFilters } from './file-dialog-tree-filters-renderer'; import URI from '@theia/core/lib/common/uri'; +import { Panel } from '@phosphor/widgets'; export const OpenFileDialogFactory = Symbol('OpenFileDialogFactory'); export interface OpenFileDialogFactory { @@ -110,13 +111,16 @@ export abstract class FileDialog extends AbstractDialog { protected readonly forward: HTMLSpanElement; protected readonly locationListRenderer: LocationListRenderer; protected readonly treeFiltersRenderer: FileDialogTreeFiltersRenderer | undefined; + protected readonly treePanel: Panel; constructor( @inject(FileDialogProps) readonly props: FileDialogProps, @inject(FileDialogWidget) readonly widget: FileDialogWidget ) { super(props); - this.toDispose.push(widget); + this.treePanel = new Panel(); + this.treePanel.addWidget(this.widget); + this.toDispose.push(this.treePanel); this.toDispose.push(this.model.onChanged(() => this.update())); this.toDispose.push(this.model.onDidOpenFile(() => this.accept())); this.toDispose.push(this.model.onSelectionChanged(() => this.update())); @@ -183,9 +187,9 @@ export abstract class FileDialog extends AbstractDialog { } protected onAfterAttach(msg: Message): void { - Widget.attach(this.widget, this.contentNode); + Widget.attach(this.treePanel, this.contentNode); this.toDisposeOnDetach.push(Disposable.create(() => { - Widget.detach(this.widget); + Widget.detach(this.treePanel); this.locationListRenderer.dispose(); if (this.treeFiltersRenderer) { this.treeFiltersRenderer.dispose(); @@ -224,7 +228,7 @@ export class OpenFileDialog extends FileDialog> { return this.props.openLabel ? this.props.openLabel : 'Open'; } - isValid(value: MaybeArray): string { + protected isValid(value: MaybeArray): string { if (value) { if (this.props.canSelectMany) { if (Array.isArray(value)) { @@ -311,13 +315,11 @@ export class SaveFileDialog extends FileDialog { super.onUpdateRequest(msg); } - isValid(value: URI | undefined): string { + protected isValid(value: URI | undefined): string | boolean { if (this.fileNameField && this.fileNameField.value) { return ''; } - - // Returning a space disables the 'Save' button and doesn't show an error message at the bottom - return ' '; + return false; } get value(): URI | undefined { @@ -351,11 +353,7 @@ export class SaveFileDialog extends FileDialog { this.fileNameField.classList.add(FILENAME_TEXTFIELD_CLASS); fileNamePanel.appendChild(this.fileNameField); - this.fileNameField.onkeyup = () => { - const value = this.value; - const error = this.isValid(value); - this.setErrorMessage(error); - }; + this.fileNameField.onkeyup = () => this.validate(); } } diff --git a/packages/filesystem/src/browser/file-tree/file-tree-widget.tsx b/packages/filesystem/src/browser/file-tree/file-tree-widget.tsx index 2f5f1fd39262a..a979fb0c04745 100644 --- a/packages/filesystem/src/browser/file-tree/file-tree-widget.tsx +++ b/packages/filesystem/src/browser/file-tree/file-tree-widget.tsx @@ -19,6 +19,7 @@ import { ContextMenuRenderer, NodeProps, TreeProps, TreeNode, TreeWidget } from import { DirNode, FileStatNode } from './file-tree'; import { FileTreeModel } from './file-tree-model'; import { DisposableCollection, Disposable } from '@theia/core/lib/common'; +import { UriSelection } from '@theia/core/lib/common/selection'; import * as React from 'react'; export const FILE_TREE_CLASS = 'theia-FileTree'; @@ -80,10 +81,16 @@ export class FileTreeWidget extends TreeWidget { onDragEnter: event => this.handleDragEnterEvent(node, event), onDragOver: event => this.handleDragOverEvent(node, event), onDragLeave: event => this.handleDragLeaveEvent(node, event), - onDrop: event => this.handleDropEvent(node, event) + onDrop: event => this.handleDropEvent(node, event), + title: this.getNodeTooltip(node) }; } + protected getNodeTooltip(node: TreeNode): string | undefined { + const uri = UriSelection.getUri(node); + return uri ? uri.path.toString() : undefined; + } + protected handleDragStartEvent(node: TreeNode, event: React.DragEvent): void { event.stopPropagation(); this.setTreeNodeAsData(event.dataTransfer, node); diff --git a/packages/git/package.json b/packages/git/package.json index 64b8be77dedfc..393f2c25a4d50 100644 --- a/packages/git/package.json +++ b/packages/git/package.json @@ -1,14 +1,14 @@ { "name": "@theia/git", - "version": "0.3.13", + "version": "0.3.14", "description": "Theia - Git Integration", "dependencies": { - "@theia/core": "^0.3.13", - "@theia/editor": "^0.3.13", - "@theia/filesystem": "^0.3.13", - "@theia/languages": "^0.3.13", - "@theia/navigator": "^0.3.13", - "@theia/workspace": "^0.3.13", + "@theia/core": "^0.3.14", + "@theia/editor": "^0.3.14", + "@theia/filesystem": "^0.3.14", + "@theia/languages": "^0.3.14", + "@theia/navigator": "^0.3.14", + "@theia/workspace": "^0.3.14", "@types/diff": "^3.2.2", "@types/fs-extra": "^4.0.2", "diff": "^3.4.0", @@ -54,10 +54,10 @@ "docs": "theiaext docs" }, "devDependencies": { - "@theia/ext-scripts": "^0.3.13", + "@theia/ext-scripts": "^0.3.14", "upath": "^1.0.2" }, "nyc": { "extends": "../../configs/nyc.json" } -} \ No newline at end of file +} diff --git a/packages/git/src/browser/history/git-history-widget.tsx b/packages/git/src/browser/history/git-history-widget.tsx index cb1ae2b2e8e16..e854393e2b6f8 100644 --- a/packages/git/src/browser/history/git-history-widget.tsx +++ b/packages/git/src/browser/history/git-history-widget.tsx @@ -16,7 +16,10 @@ import { injectable, inject } from 'inversify'; import { DiffUris } from '@theia/core/lib/browser/diff-uris'; -import { OpenerService, open, StatefulWidget, SELECTED_CLASS, WidgetManager, ApplicationShell, Message } from '@theia/core/lib/browser'; +import { OpenerService, open, StatefulWidget, SELECTED_CLASS, WidgetManager, ApplicationShell } from '@theia/core/lib/browser'; +import { CancellationTokenSource } from '@theia/core/lib/common/cancellation'; +import { Message } from '@phosphor/messaging'; +import { AutoSizer, List, ListRowRenderer, ListRowProps, InfiniteLoader, IndexRange, ScrollParams } from 'react-virtualized'; import { GIT_RESOURCE_SCHEME } from '../git-resource'; import URI from '@theia/core/lib/common/uri'; import { GIT_HISTORY, GIT_HISTORY_MAX_COUNT } from './git-history-contribution'; @@ -28,7 +31,6 @@ import { GitCommitDetailUri, GitCommitDetailOpenerOptions, GitCommitDetailOpenHa import { GitCommitDetails } from './git-commit-detail-widget'; import { GitNavigableListWidget } from '../git-navigable-list-widget'; import { GitFileChangeNode } from '../git-widget'; -import { Disposable } from 'vscode-jsonrpc'; import * as React from 'react'; export interface GitCommitNode extends GitCommitDetails { @@ -39,7 +41,7 @@ export interface GitCommitNode extends GitCommitDetails { export namespace GitCommitNode { export function is(node: any): node is GitCommitNode { - return 'commitSha' in node && 'commitMessage' in node && 'fileChangeNodes' in node; + return !!node && 'commitSha' in node && 'commitMessage' in node && 'fileChangeNodes' in node; } } @@ -51,6 +53,11 @@ export class GitHistoryWidget extends GitNavigableListWidget protected commits: GitCommitNode[]; protected ready: boolean; protected singleFileMode: boolean; + private cancelIndicator: CancellationTokenSource; + protected listView: GitHistoryList | undefined; + protected hasMoreCommits: boolean; + protected allowScrollToSelected: boolean; + protected pathIsUnderVersionControl: boolean; constructor( @inject(OpenerService) protected readonly openerService: OpenerService, @@ -58,7 +65,7 @@ export class GitHistoryWidget extends GitNavigableListWidget @inject(ApplicationShell) protected readonly shell: ApplicationShell, @inject(FileSystem) protected readonly fileSystem: FileSystem, @inject(Git) protected readonly git: Git, - @inject(GitAvatarService) protected readonly avartarService: GitAvatarService, + @inject(GitAvatarService) protected readonly avatarService: GitAvatarService, @inject(WidgetManager) protected readonly widgetManager: WidgetManager, @inject(GitDiffContribution) protected readonly diffContribution: GitDiffContribution) { super(); @@ -66,51 +73,70 @@ export class GitHistoryWidget extends GitNavigableListWidget this.scrollContainer = 'git-history-list-container'; this.title.label = 'Git History'; this.addClass('theia-git'); - this.options = {}; - this.commits = []; + this.resetState(); + this.cancelIndicator = new CancellationTokenSource(); } - protected onAfterAttach(msg: Message) { + protected onAfterAttach(msg: Message): void { super.onAfterAttach(msg); - (async () => { - const sc = await this.getScrollContainer(); - const listener = (e: UIEvent) => { - const el = (e.srcElement || e.target) as HTMLElement; - if (el.scrollTop + el.clientHeight > el.scrollHeight - 83) { - const ll = this.node.getElementsByClassName('history-lazy-loading')[0]; - ll.className = 'history-lazy-loading show'; - this.addCommits({ - range: { - toRevision: this.commits[this.commits.length - 1].commitSha - }, - maxCount: GIT_HISTORY_MAX_COUNT - }); - } - }; - sc.addEventListener('scroll', listener); - this.toDispose.push(Disposable.create(() => { - sc.removeEventListener('scroll', listener); - })); - })(); + this.addEventListener(this.node, 'ps-scroll-y', (e: Event & { target: { scrollTop: number } }) => { + if (this.listView && this.listView.list && this.listView.list.Grid) { + const { scrollTop } = e.target; + this.listView.list.Grid.handleScrollEvent({ scrollTop }); + } + }); + } + + protected onActivateRequest(msg: Message): void { + super.onActivateRequest(msg); + this.update(); + } + + update(): void { + if (this.listView && this.listView.list) { + this.listView.list.forceUpdateGrid(); + } + super.update(); } async setContent(options?: Git.Options.Log) { - this.options = options || {}; + this.resetState(options); this.ready = false; if (options && options.uri) { const fileStat = await this.fileSystem.getFileStat(options.uri); this.singleFileMode = !!fileStat && !fileStat.isDirectory; } - this.addCommits(options); - this.update(); + await this.addCommits(options); + this.onDataReady(); + if (this.gitNodes.length > 0) { + this.selectNode(this.gitNodes[0]); + } + } + + protected resetState(options?: Git.Options.Log) { + this.options = options || {}; + this.commits = []; + this.gitNodes = []; + this.hasMoreCommits = true; + this.allowScrollToSelected = true; } - protected addCommits(options?: Git.Options.Log) { + protected async addCommits(options?: Git.Options.Log): Promise { const repository = this.repositoryProvider.selectedRepository; + let resolver: () => void; + + this.cancelIndicator.cancel(); + this.cancelIndicator = new CancellationTokenSource(); + const token = this.cancelIndicator.token; if (repository) { const log = this.git.log(repository, options); log.then(async changes => { - this.commits = []; + if (token.isCancellationRequested || !this.hasMoreCommits) { + return; + } + if (options && ((options.maxCount && changes.length < options.maxCount) || (!options.maxCount && this.commits))) { + this.hasMoreCommits = false; + } if (this.commits.length > 0) { changes = changes.slice(1); } @@ -118,7 +144,7 @@ export class GitHistoryWidget extends GitNavigableListWidget const commits: GitCommitNode[] = []; for (const commit of changes) { const fileChangeNodes: GitFileChangeNode[] = []; - const avatarUrl = await this.avartarService.getAvatar(commit.author.email); + const avatarUrl = await this.avatarService.getAvatar(commit.author.email); commits.push({ authorName: commit.author.name, authorDate: new Date(commit.author.timestamp), @@ -135,36 +161,57 @@ export class GitHistoryWidget extends GitNavigableListWidget }); } this.commits.push(...commits); + } else if (options && options.uri) { + this.pathIsUnderVersionControl = await this.git.lsFiles(repository, options.uri, { errorUnmatch: true }); } - this.onDataReady(); + resolver(); }); } else { - this.commits = []; - this.onDataReady(); + setTimeout(() => { + this.commits = []; + resolver(); + }); } + return new Promise(resolve => resolver = resolve); } - protected async addFileChangeNodesToCommit(commit: GitCommitNode) { + protected async addOrRemoveFileChangeNodes(commit: GitCommitNode) { + const id = this.gitNodes.findIndex(node => node === commit); + if (commit.expanded) { + this.removeFileChangeNodes(commit, id); + } else { + this.addFileChangeNodes(commit, id); + } + commit.expanded = !commit.expanded; + this.update(); + } + + protected async addFileChangeNodes(commit: GitCommitNode, gitNodesArrayIndex: number) { if (commit.fileChanges) { + const fileChangeNodes: GitFileChangeNode[] = []; await Promise.all(commit.fileChanges.map(async fileChange => { const fileChangeUri = new URI(fileChange.uri); const icon = await this.labelProvider.getIcon(fileChangeUri); const label = this.labelProvider.getName(fileChangeUri); const description = this.relativePath(fileChangeUri.parent); const caption = this.computeCaption(fileChange); - commit.fileChangeNodes.push({ + fileChangeNodes.push({ ...fileChange, icon, label, description, caption, commitSha: commit.commitSha }); })); - delete commit.fileChanges; - this.update(); + this.gitNodes.splice(gitNodesArrayIndex + 1, 0, ...fileChangeNodes); + } + } + + protected removeFileChangeNodes(commit: GitCommitNode, gitNodesArrayIndex: number) { + if (commit.fileChanges) { + this.gitNodes.splice(gitNodesArrayIndex + 1, commit.fileChanges.length); } } storeState(): object { - const { commits, options, singleFileMode } = this; + const { options, singleFileMode } = this; return { - commits, options, singleFileMode }; @@ -172,39 +219,45 @@ export class GitHistoryWidget extends GitNavigableListWidget // tslint:disable-next-line:no-any restoreState(oldState: any): void { - this.commits = oldState['commits']; this.options = oldState['options']; this.singleFileMode = oldState['singleFileMode']; - this.ready = true; - this.update(); + this.setContent(this.options); } protected onDataReady(): void { this.ready = true; + this.gitNodes = this.commits; this.update(); - const ll = this.node.getElementsByClassName('history-lazy-loading')[0]; - if (ll && ll.className === 'history-lazy-loading show') { - ll.className = 'history-lazy-loading hide'; - } } protected render(): React.ReactNode { - this.gitNodes = []; - return
- { - this.ready ? - < React.Fragment > - {this.renderHistoryHeader()} - {this.renderCommitList()} -
- -
- - : -
- -
+ let content: React.ReactNode; + if (this.ready && this.gitNodes.length > 0) { + content = < React.Fragment > + {this.renderHistoryHeader()} + {this.renderCommitList()} + ; + } else if (this.ready) { + let path: React.ReactNode = ''; + let notInVC: React.ReactNode; + if (this.options.uri) { + const relPath = this.relativePath(this.options.uri); + path = for /{relPath}; + notInVC = !this.pathIsUnderVersionControl ?
/{relPath} is not under version control.
: ''; } + content =
+
+
There is no Git history available{path}.
+ {notInVC} +
+
; + } else { + content =
+ +
; + } + return
+ {content}
; } @@ -232,19 +285,49 @@ export class GitHistoryWidget extends GitNavigableListWidget } protected renderCommitList(): React.ReactNode { - const theList: React.ReactNode[] = []; + const list =
+ this.listView = (listView || undefined)} + rows={this.gitNodes} + hasMoreRows={this.hasMoreCommits} + indexOfSelected={this.allowScrollToSelected ? this.indexOfSelected : -1} + handleScroll={this.handleScroll} + loadMoreRows={this.loadMoreRows} + renderCommit={this.renderCommit} + renderFileChangeList={this.renderFileChangeList} + > +
; + this.allowScrollToSelected = true; + return list; + } - for (const commit of this.commits) { - const head = this.renderCommit(commit); - const body = commit.expanded ? this.renderFileChangeList(commit) : ''; - theList.push(
{head}{body}
); + protected readonly handleScroll = (info: ScrollParams) => this.doHandleScroll(info); + protected doHandleScroll(info: ScrollParams) { + this.node.scrollTo({ top: info.scrollTop }); + } + + protected readonly loadMoreRows = (params: IndexRange) => this.doLoadMoreRows(params); + protected doLoadMoreRows(params: IndexRange): Promise { + let resolver: () => void; + const promise = new Promise(resolve => resolver = resolve); + const lastRow = this.gitNodes[params.stopIndex - 1]; + if (GitCommitNode.is(lastRow)) { + const toRevision = lastRow.commitSha; + this.addCommits({ + range: { toRevision }, + maxCount: GIT_HISTORY_MAX_COUNT, + uri: this.options.uri + }).then(() => { + this.allowScrollToSelected = false; + this.onDataReady(); + resolver(); + }); } - const commitList =
{...theList}
; - return
{commitList}
; + return promise; } - protected renderCommit(commit: GitCommitNode): React.ReactNode { - this.gitNodes.push(commit); + protected readonly renderCommit = (commit: GitCommitNode) => this.doRenderCommit(commit); + protected doRenderCommit(commit: GitCommitNode): React.ReactNode { let expansionToggleIcon = 'caret-right'; if (commit && commit.expanded) { expansionToggleIcon = 'caret-down'; @@ -252,23 +335,22 @@ export class GitHistoryWidget extends GitNavigableListWidget return
{ + e => { if (commit.selected && !this.singleFileMode) { - commit.expanded = !commit.expanded; - if (commit.expanded) { - this.addFileChangeNodesToCommit(commit); - } + this.addOrRemoveFileChangeNodes(commit); this.update(); } else { this.selectNode(commit); } + e.preventDefault(); } } onDoubleClick={ - () => { + e => { if (this.singleFileMode && commit.fileChanges && commit.fileChanges.length > 0) { this.openFile(commit.fileChanges[0], commit.commitSha); } + e.preventDefault(); } }>
@@ -303,18 +385,10 @@ export class GitHistoryWidget extends GitNavigableListWidget } as GitCommitDetailOpenerOptions); } - protected renderFileChangeList(commit: GitCommitNode): React.ReactNode { - const fileChanges = commit.fileChangeNodes; - - this.gitNodes.push(...fileChanges); - - const files: React.ReactNode[] = []; - - for (const fileChange of fileChanges) { - const fileChangeElement: React.ReactNode = this.renderGitItem(fileChange, commit.commitSha); - files.push(fileChangeElement); - } - return
{...files}
; + protected readonly renderFileChangeList = (fileChange: GitFileChangeNode) => this.doRenderFileChangeList(fileChange); + protected doRenderFileChangeList(fileChange: GitFileChangeNode): React.ReactNode { + const fileChangeElement: React.ReactNode = this.renderGitItem(fileChange, fileChange.commitSha || ''); + return fileChangeElement; } protected renderGitItem(change: GitFileChangeNode, commitSha: string): React.ReactNode { @@ -352,7 +426,7 @@ export class GitHistoryWidget extends GitNavigableListWidget const idx = this.commits.findIndex(c => c.commitSha === selected.commitSha); if (GitCommitNode.is(selected)) { if (selected.expanded) { - selected.expanded = false; + this.addOrRemoveFileChangeNodes(selected); } else { if (idx > 0) { this.selectNode(this.commits[idx - 1]); @@ -369,8 +443,7 @@ export class GitHistoryWidget extends GitNavigableListWidget const selected = this.getSelected(); if (selected) { if (GitCommitNode.is(selected) && !selected.expanded && !this.singleFileMode) { - selected.expanded = true; - this.addFileChangeNodesToCommit(selected); + this.addOrRemoveFileChangeNodes(selected); } else { this.selectNextNode(); } @@ -410,3 +483,101 @@ export class GitHistoryWidget extends GitNavigableListWidget open(this.openerService, uriToOpen, { mode: 'reveal' }); } } + +export namespace GitHistoryList { + export interface Props { + readonly rows: GitHistoryListNode[] + readonly indexOfSelected: number + readonly hasMoreRows: boolean + readonly handleScroll: (info: { clientHeight: number; scrollHeight: number; scrollTop: number }) => void + readonly loadMoreRows: (params: IndexRange) => Promise + readonly renderCommit: (commit: GitCommitNode) => React.ReactNode + readonly renderFileChangeList: (fileChange: GitFileChangeNode) => React.ReactNode + } +} +export class GitHistoryList extends React.Component { + list: List | undefined; + + protected commitHeight: number = 50; + protected fileChangeHeight: number = 21; + protected lastFileChangeHeight: number = 31; + + protected readonly checkIfRowIsLoaded = (opts: { index: number }) => this.doCheckIfRowIsLoaded(opts); + protected doCheckIfRowIsLoaded(opts: { index: number }) { + const row = this.props.rows[opts.index]; + return !!row; + } + + render(): React.ReactNode { + return + { + ({ onRowsRendered, registerChild }) => ( + + { + ({ width, height }) => { + this.list = (list || undefined); + registerChild(list); + }} + width={width} + height={height} + onRowsRendered={onRowsRendered} + rowRenderer={this.renderRow} + rowHeight={this.calcRowHeight} + rowCount={this.props.hasMoreRows ? this.props.rows.length + 1 : this.props.rows.length} + tabIndex={-1} + onScroll={this.props.handleScroll} + scrollToIndex={this.props.indexOfSelected} + style={{ + overflowY: 'visible', + overflowX: 'visible' + }} + /> + } + + ) + } + ; + } + + componentDidUpdate(): void { + if (this.list) { + this.list.recomputeRowHeights(this.props.indexOfSelected); + } + } + + protected renderRow: ListRowRenderer = ({ index, key, style }) => { + if (this.checkIfRowIsLoaded({ index })) { + const row = this.props.rows[index]; + if (GitCommitNode.is(row)) { + const head = this.props.renderCommit(row); + return
+ {head} +
; + } else if (GitFileChangeNode.is(row)) { + return
+ {this.props.renderFileChangeList(row)} +
; + } + } else { + return
+ +
; + } + } + + protected readonly calcRowHeight = (options: ListRowProps) => { + const row = this.props.rows[options.index]; + if (GitFileChangeNode.is(row)) { + const nextIsCommit = GitCommitNode.is(this.props.rows[options.index + 1]); + return nextIsCommit ? this.lastFileChangeHeight : this.fileChangeHeight; + } + return this.commitHeight; + } +} diff --git a/packages/git/src/browser/style/history.css b/packages/git/src/browser/style/history.css index ad32b03e623e7..7fc24176d7170 100644 --- a/packages/git/src/browser/style/history.css +++ b/packages/git/src/browser/style/history.css @@ -19,11 +19,15 @@ } .theia-git .commitListElement { - border-bottom: 1px solid var(--theia-layout-color4); + border-top: 1px solid var(--theia-layout-color4); +} + +.theia-git .commitListElement.first { + border: none; } .theia-git .commitListElement .containerHead { - width: 100%; + width: calc(100% - 5px); height: 50px; display: flex; align-items: center; @@ -97,6 +101,14 @@ font-size: smaller; } +.theia-git .git-diff-container .listContainer { + flex: 1; +} + +.theia-git .git-diff-container .listContainer .commitList { + height: 100%; +} + .theia-git .git-diff-container .history-lazy-loading { position: absolute; height: 50px; @@ -184,3 +196,20 @@ width: 20px!important; height: 20px; } + +.no-history-message { + background-color: var(--theia-warn-color0) !important; + color: var(--theia-warn-font-color0); + padding: 5px; +} + +.no-history-message div { + margin: 5px; +} + +.message-container { + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} diff --git a/packages/git/src/browser/style/index.css b/packages/git/src/browser/style/index.css index 55edaf86abe95..f8bcf3e24d417 100644 --- a/packages/git/src/browser/style/index.css +++ b/packages/git/src/browser/style/index.css @@ -21,9 +21,10 @@ background: var(--theia-layout-color0); } -.theia-git:focus { +.theia-git:focus, .theia-git :focus { outline: 0; box-shadow: none; + border: none; } #theia-gitContainer .flexcontainer { diff --git a/packages/git/src/node/dugite-git.spec.ts b/packages/git/src/node/dugite-git.spec.ts index fe053a5d48509..842e1f4dfb70a 100644 --- a/packages/git/src/node/dugite-git.spec.ts +++ b/packages/git/src/node/dugite-git.spec.ts @@ -774,6 +774,41 @@ describe('git', async function () { }); +describe('log', () => { + + async function testLogFromRepoRoot(testLocalGit: string) { + const savedValue = process.env.USE_LOCAL_GIT; + try { + process.env.USE_LOCAL_GIT = testLocalGit; + const root = await createTestRepository(track.mkdirSync('log-test')); + const localUri = FileUri.create(root).toString(); + const repository = { localUri }; + const git = await createGit(); + const result = await git.log(repository, { uri: localUri }); + expect(result.length === 1).to.be.true; + expect(result[0].author.email === 'jon@doe.com').to.be.true; + } catch (err) { + throw err; + } finally { + process.env.USE_LOCAL_GIT = savedValue; + } + } + + // See https://github.com/theia-ide/theia/issues/2143 + it('should not fail with embedded git when executed from the repository root', async () => { + await testLogFromRepoRoot('false'); + }); + + // See https://github.com/theia-ide/theia/issues/2143 + it('should not fail with local git when executed from the repository root', async () => { + await testLogFromRepoRoot('true'); + }); + + // THE ABOVE TEST SHOULD ALWAYS BE THE LAST GIT TEST RUN. + // It changes the underlying git to be the local git, which can't be + // undone. (See https://github.com/theia-ide/theia/issues/2246). +}); + function toPathSegment(repository: Repository, uri: string): string { return upath.relative(FileUri.fsPath(repository.localUri), FileUri.fsPath(uri)); } diff --git a/packages/git/src/node/dugite-git.ts b/packages/git/src/node/dugite-git.ts index 35570a08432cb..28f783aac17ab 100644 --- a/packages/git/src/node/dugite-git.ts +++ b/packages/git/src/node/dugite-git.ts @@ -550,7 +550,7 @@ export class DugiteGit implements Git { [CommitPlaceholders.SHORT_HASH, ...CommitDetailsParser.DEFAULT_PLACEHOLDERS.slice(1)] : CommitDetailsParser.DEFAULT_PLACEHOLDERS; args.push(...['--name-status', '--date=unix', `--format=${this.commitDetailsParser.getFormat(...placeholders)}`, '-z', '--']); if (options && options.uri) { - const file = Path.relative(this.getFsPath(repository), this.getFsPath(options.uri)); + const file = Path.relative(this.getFsPath(repository), this.getFsPath(options.uri)) || '.'; args.push(...[file]); } const result = await this.exec(repository, args); diff --git a/packages/java/package.json b/packages/java/package.json index 27d1835f672e7..681134097e8f6 100644 --- a/packages/java/package.json +++ b/packages/java/package.json @@ -1,12 +1,12 @@ { "name": "@theia/java", - "version": "0.3.13", + "version": "0.3.14", "description": "Theia - Java Extension", "dependencies": { - "@theia/core": "^0.3.13", - "@theia/editor": "^0.3.13", - "@theia/languages": "^0.3.13", - "@theia/monaco": "^0.3.13", + "@theia/core": "^0.3.14", + "@theia/editor": "^0.3.14", + "@theia/languages": "^0.3.14", + "@theia/monaco": "^0.3.14", "@types/glob": "^5.0.30", "@types/tar": "4.0.0", "glob": "^7.1.2", @@ -15,7 +15,7 @@ "tar": "^4.0.0" }, "devDependencies": { - "@theia/ext-scripts": "^0.3.13" + "@theia/ext-scripts": "^0.3.14" }, "scripts": { "prepare": "yarn run clean && yarn run build", @@ -60,4 +60,4 @@ "ls": { "downloadUrl": "https://github.com/kittaakos/eclipse.jdt.ls-gh-715/raw/master/jdt-language-server-latest.tar.gz" } -} \ No newline at end of file +} diff --git a/packages/java/src/browser/monaco-contribution/java-textmate-contribution.ts b/packages/java/src/browser/monaco-contribution/java-textmate-contribution.ts index fc38938760f90..9867b10f24864 100644 --- a/packages/java/src/browser/monaco-contribution/java-textmate-contribution.ts +++ b/packages/java/src/browser/monaco-contribution/java-textmate-contribution.ts @@ -23,7 +23,7 @@ export class JavaTextmateContribution implements LanguageGrammarDefinitionContri registerTextmateLanguage(registry: TextmateRegistry) { const javaDocGrammar = require('../../../data/javadoc.tmlanguage.json'); - registry.registerTextMateGrammarScope('text.html.javadoc', { + registry.registerTextmateGrammarScope('text.html.javadoc', { async getGrammarDefinition() { return { format: 'json', @@ -33,7 +33,7 @@ export class JavaTextmateContribution implements LanguageGrammarDefinitionContri }); const scope = 'source.java'; const javaGrammar = require('../../../data/java.tmlanguage.json'); - registry.registerTextMateGrammarScope(scope, { + registry.registerTextmateGrammarScope(scope, { async getGrammarDefinition() { return { format: 'json', diff --git a/packages/json/README.md b/packages/json/README.md new file mode 100644 index 0000000000000..3d18dec7466ca --- /dev/null +++ b/packages/json/README.md @@ -0,0 +1,109 @@ +## JSON Extension for Theia + +## Capabilities + +The JSON extension supports the following features: +- Syntax Coloring for JSON including support for jsonc (i.e. comments) +- [Code completion](https://microsoft.github.io/language-server-protocol/specification#textDocument_completion) for JSON properties and values based on the document's [JSON schema](http://json-schema.org/) or based on existing properties and values used at other places in the document. JSON schemas are configured through the server configuration options. +- [Hover](https://microsoft.github.io/language-server-protocol/specification#textDocument_hover) for values based on descriptions in the document's [JSON schema](http://json-schema.org/). +- [Document Symbols](https://microsoft.github.io/language-server-protocol/specification#textDocument_documentSymbol) for quick navigation to properties in the document. +- [Document Colors](https://microsoft.github.io/language-server-protocol/specification#textDocument_documentColor) for showing color decorators on values representing colors and [Color Presentation](https://microsoft.github.io/language-server-protocol/specification#textDocument_colorPresentation) for color presentation information to support color pickers. The location of colors is defined by the document's [JSON schema](http://json-schema.org/). All values marked with `"format": "color-hex"` (VSCode specific, non-standard JSON Schema extension) are considered color values. The supported color formats are `#rgb[a]` and `#rrggbb[aa]`. +- [Code Formatting](https://microsoft.github.io/language-server-protocol/specification#textDocument_rangeFormatting) supporting ranges and formatting the whole document. +- [Diagnostics (Validation)](https://microsoft.github.io/language-server-protocol/specification#textDocument_publishDiagnostics) are pushed for all open documents + - syntax errors + - structural validation based on the document's [JSON schema](http://json-schema.org/). + +In order to load JSON schemas, the JSON server uses NodeJS `http` and `fs` modules. For all other features, the JSON server only relies on the documents and settings provided by the client through the LSP. + +## Configuration + +### Settings + +Clients may send a `workspace/didChangeConfiguration` notification to notify the server of settings changes. +The server supports the following settings: + +- http + - `proxy`: The URL of the proxy server to use when fetching schema. When undefined or empty, no proxy is used. + - `proxyStrictSSL`: Whether the proxy server certificate should be verified against the list of supplied CAs. + +- json + - `format` + - `enable`: Whether the server should register the formatting support. This option is only applicable if the client supports *dynamicRegistration* for *rangeFormatting* + - `schema`: Configures association of file names to schema URL or schemas and/or associations of schema URL to schema content. + - `fileMatch`: an array or file names or paths (separated by `/`). `*` can be used as a wildcard. + - `url`: The URL of the schema, optional when also a schema is provided. + - `schema`: The schema content. + +```json + { + "http": { + "proxy": "", + "proxyStrictSSL": true + }, + "json": { + "format": { + "enable": true + }, + "schemas": [ + { + "fileMatch": [ + "foo.json", + "*.superfoo.json" + ], + "url": "http://json.schemastore.org/foo", + "schema": { + "type": "array" + } + } + ] + } + } +``` + +### Schema configuration and custom schema content delivery + +[JSON schemas](http://json-schema.org/) are essential for code assist, hovers, color decorators to work and are required for structural validation. + +To find the schema for a given JSON document, the server uses the following mechanisms: +- JSON documents can define the schema URL using a `$schema` property +- The settings define a schema association based on the documents URL. Settings can either associate a schema URL to a file or path pattern, and they can directly provide a schema. +- Additionally, schema associations can also be provided by a custom 'schemaAssociations' configuration call. + +Schemas are identified by URLs. To load the content of a schema, the JSON language server tries to load from that URL or path. The following URL schemas are supported: +- `http`, `https`: Loaded using NodeJS's HTTP support. Proxies can be configured through the settings. +- `file`: Loaded using NodeJS's `fs` support. +- `vscode`: Loaded by an LSP call to the client. + +#### Schema associations notification + +In addition to the settings, schemas associations can also be provided through a notification from the client to the server. This notification is a JSON language server specific, non-standardized, extension to the LSP. + +Notification: +- method: 'json/schemaAssociations' +- params: `ISchemaAssociations` defined as follows + +```ts +interface ISchemaAssociations { + [pattern: string]: string[]; +} +``` + - keys: a file names or file path (separated by `/`). `*` can be used as a wildcard. + - values: An array of schema URLs + +#### Schema content request + +The schema content for schema URLs that start with `vscode://` will be requested from the client through an LSP request. This request is a JSON language server specific, non-standardized, extension to the LSP. + +Request: +- method: 'vscode/content' +- params: `string` - The schema URL to request. The server will only ask for URLs that start with `vscode://` +- response: `string` - The content of the schema with the given URL + +#### Schema content change notification + +When the client is aware that a schema content has changed, it will notify the server through a notification. This notification is a JSON language server specific, non-standardized, extension to the LSP. +The server will, as a response, clear the schema content from the cache and reload the schema content when required again. + +Notification: +- method: 'json/schemaContent' +- params: `string` the URL of the schema that has changed. \ No newline at end of file diff --git a/packages/json/compile.tsconfig.json b/packages/json/compile.tsconfig.json new file mode 100644 index 0000000000000..b8b72b49c8822 --- /dev/null +++ b/packages/json/compile.tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../configs/base.tsconfig", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib" + }, + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/packages/json/data/json.tmLanguage.json b/packages/json/data/json.tmLanguage.json new file mode 100644 index 0000000000000..910045be39ee9 --- /dev/null +++ b/packages/json/data/json.tmLanguage.json @@ -0,0 +1,213 @@ +{ + "information_for_contributors": [ + "This file has been converted from https://github.com/Microsoft/vscode-JSON.tmLanguage/blob/master/JSON.tmLanguage", + "If you want to provide a fix or improvement, please create a pull request against the original repository.", + "Once accepted there, we are happy to receive an update request." + ], + "version": "https://github.com/Microsoft/vscode-JSON.tmLanguage/commit/9bd83f1c252b375e957203f21793316203f61f70", + "name": "JSON (Javascript Next)", + "scopeName": "source.json", + "patterns": [ + { + "include": "#value" + } + ], + "repository": { + "array": { + "begin": "\\[", + "beginCaptures": { + "0": { + "name": "punctuation.definition.array.begin.json" + } + }, + "end": "\\]", + "endCaptures": { + "0": { + "name": "punctuation.definition.array.end.json" + } + }, + "name": "meta.structure.array.json", + "patterns": [ + { + "include": "#value" + }, + { + "match": ",", + "name": "punctuation.separator.array.json" + }, + { + "match": "[^\\s\\]]", + "name": "invalid.illegal.expected-array-separator.json" + } + ] + }, + "comments": { + "patterns": [ + { + "begin": "/\\*\\*(?!/)", + "captures": { + "0": { + "name": "punctuation.definition.comment.json" + } + }, + "end": "\\*/", + "name": "comment.block.documentation.json" + }, + { + "begin": "/\\*", + "captures": { + "0": { + "name": "punctuation.definition.comment.json" + } + }, + "end": "\\*/", + "name": "comment.block.json" + }, + { + "captures": { + "1": { + "name": "punctuation.definition.comment.json" + } + }, + "match": "(//).*$\\n?", + "name": "comment.line.double-slash.js" + } + ] + }, + "constant": { + "match": "\\b(?:true|false|null)\\b", + "name": "constant.language.json" + }, + "number": { + "match": "(?x) # turn on extended mode\n -? # an optional minus\n (?:\n 0 # a zero\n | # ...or...\n [1-9] # a 1-9 character\n \\d* # followed by zero or more digits\n )\n (?:\n (?:\n \\. # a period\n \\d+ # followed by one or more digits\n )?\n (?:\n [eE] # an e character\n [+-]? # followed by an option +/-\n \\d+ # followed by one or more digits\n )? # make exponent optional\n )? # make decimal portion optional", + "name": "constant.numeric.json" + }, + "object": { + "begin": "\\{", + "beginCaptures": { + "0": { + "name": "punctuation.definition.dictionary.begin.json" + } + }, + "end": "\\}", + "endCaptures": { + "0": { + "name": "punctuation.definition.dictionary.end.json" + } + }, + "name": "meta.structure.dictionary.json", + "patterns": [ + { + "comment": "the JSON object key", + "include": "#objectkey" + }, + { + "include": "#comments" + }, + { + "begin": ":", + "beginCaptures": { + "0": { + "name": "punctuation.separator.dictionary.key-value.json" + } + }, + "end": "(,)|(?=\\})", + "endCaptures": { + "1": { + "name": "punctuation.separator.dictionary.pair.json" + } + }, + "name": "meta.structure.dictionary.value.json", + "patterns": [ + { + "comment": "the JSON object value", + "include": "#value" + }, + { + "match": "[^\\s,]", + "name": "invalid.illegal.expected-dictionary-separator.json" + } + ] + }, + { + "match": "[^\\s\\}]", + "name": "invalid.illegal.expected-dictionary-separator.json" + } + ] + }, + "string": { + "begin": "\"", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.json" + } + }, + "end": "\"", + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.json" + } + }, + "name": "string.quoted.double.json", + "patterns": [ + { + "include": "#stringcontent" + } + ] + }, + "objectkey": { + "begin": "\"", + "beginCaptures": { + "0": { + "name": "punctuation.support.type.property-name.begin.json" + } + }, + "end": "\"", + "endCaptures": { + "0": { + "name": "punctuation.support.type.property-name.end.json" + } + }, + "name": "string.json support.type.property-name.json", + "patterns": [ + { + "include": "#stringcontent" + } + ] + }, + "stringcontent": { + "patterns": [ + { + "match": "(?x) # turn on extended mode\n \\\\ # a literal backslash\n (?: # ...followed by...\n [\"\\\\/bfnrt] # one of these characters\n | # ...or...\n u # a u\n [0-9a-fA-F]{4}) # and four hex digits", + "name": "constant.character.escape.json" + }, + { + "match": "\\\\.", + "name": "invalid.illegal.unrecognized-string-escape.json" + } + ] + }, + "value": { + "patterns": [ + { + "include": "#constant" + }, + { + "include": "#number" + }, + { + "include": "#string" + }, + { + "include": "#array" + }, + { + "include": "#object" + }, + { + "include": "#comments" + } + ] + } + } +} \ No newline at end of file diff --git a/packages/json/data/jsonc.tmLanguage.json b/packages/json/data/jsonc.tmLanguage.json new file mode 100644 index 0000000000000..50028ef0f35ab --- /dev/null +++ b/packages/json/data/jsonc.tmLanguage.json @@ -0,0 +1,213 @@ +{ + "information_for_contributors": [ + "This file has been converted from https://github.com/Microsoft/vscode-JSON.tmLanguage/blob/master/JSON.tmLanguage", + "If you want to provide a fix or improvement, please create a pull request against the original repository.", + "Once accepted there, we are happy to receive an update request." + ], + "version": "https://github.com/Microsoft/vscode-JSON.tmLanguage/commit/9bd83f1c252b375e957203f21793316203f61f70", + "name": "JSON with comments", + "scopeName": "source.json.comments", + "patterns": [ + { + "include": "#value" + } + ], + "repository": { + "array": { + "begin": "\\[", + "beginCaptures": { + "0": { + "name": "punctuation.definition.array.begin.json.comments" + } + }, + "end": "\\]", + "endCaptures": { + "0": { + "name": "punctuation.definition.array.end.json.comments" + } + }, + "name": "meta.structure.array.json.comments", + "patterns": [ + { + "include": "#value" + }, + { + "match": ",", + "name": "punctuation.separator.array.json.comments" + }, + { + "match": "[^\\s\\]]", + "name": "invalid.illegal.expected-array-separator.json.comments" + } + ] + }, + "comments": { + "patterns": [ + { + "begin": "/\\*\\*(?!/)", + "captures": { + "0": { + "name": "punctuation.definition.comment.json.comments" + } + }, + "end": "\\*/", + "name": "comment.block.documentation.json.comments" + }, + { + "begin": "/\\*", + "captures": { + "0": { + "name": "punctuation.definition.comment.json.comments" + } + }, + "end": "\\*/", + "name": "comment.block.json.comments" + }, + { + "captures": { + "1": { + "name": "punctuation.definition.comment.json.comments" + } + }, + "match": "(//).*$\\n?", + "name": "comment.line.double-slash.js" + } + ] + }, + "constant": { + "match": "\\b(?:true|false|null)\\b", + "name": "constant.language.json.comments" + }, + "number": { + "match": "(?x) # turn on extended mode\n -? # an optional minus\n (?:\n 0 # a zero\n | # ...or...\n [1-9] # a 1-9 character\n \\d* # followed by zero or more digits\n )\n (?:\n (?:\n \\. # a period\n \\d+ # followed by one or more digits\n )?\n (?:\n [eE] # an e character\n [+-]? # followed by an option +/-\n \\d+ # followed by one or more digits\n )? # make exponent optional\n )? # make decimal portion optional", + "name": "constant.numeric.json.comments" + }, + "object": { + "begin": "\\{", + "beginCaptures": { + "0": { + "name": "punctuation.definition.dictionary.begin.json.comments" + } + }, + "end": "\\}", + "endCaptures": { + "0": { + "name": "punctuation.definition.dictionary.end.json.comments" + } + }, + "name": "meta.structure.dictionary.json.comments", + "patterns": [ + { + "comment": "the JSON object key", + "include": "#objectkey" + }, + { + "include": "#comments" + }, + { + "begin": ":", + "beginCaptures": { + "0": { + "name": "punctuation.separator.dictionary.key-value.json.comments" + } + }, + "end": "(,)|(?=\\})", + "endCaptures": { + "1": { + "name": "punctuation.separator.dictionary.pair.json.comments" + } + }, + "name": "meta.structure.dictionary.value.json.comments", + "patterns": [ + { + "comment": "the JSON object value", + "include": "#value" + }, + { + "match": "[^\\s,]", + "name": "invalid.illegal.expected-dictionary-separator.json.comments" + } + ] + }, + { + "match": "[^\\s\\}]", + "name": "invalid.illegal.expected-dictionary-separator.json.comments" + } + ] + }, + "string": { + "begin": "\"", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.json.comments" + } + }, + "end": "\"", + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.json.comments" + } + }, + "name": "string.quoted.double.json.comments", + "patterns": [ + { + "include": "#stringcontent" + } + ] + }, + "objectkey": { + "begin": "\"", + "beginCaptures": { + "0": { + "name": "punctuation.support.type.property-name.begin.json.comments" + } + }, + "end": "\"", + "endCaptures": { + "0": { + "name": "punctuation.support.type.property-name.end.json.comments" + } + }, + "name": "string.json.comments support.type.property-name.json.comments", + "patterns": [ + { + "include": "#stringcontent" + } + ] + }, + "stringcontent": { + "patterns": [ + { + "match": "(?x) # turn on extended mode\n \\\\ # a literal backslash\n (?: # ...followed by...\n [\"\\\\/bfnrt] # one of these characters\n | # ...or...\n u # a u\n [0-9a-fA-F]{4}) # and four hex digits", + "name": "constant.character.escape.json.comments" + }, + { + "match": "\\\\.", + "name": "invalid.illegal.unrecognized-string-escape.json.comments" + } + ] + }, + "value": { + "patterns": [ + { + "include": "#constant" + }, + { + "include": "#number" + }, + { + "include": "#string" + }, + { + "include": "#array" + }, + { + "include": "#object" + }, + { + "include": "#comments" + } + ] + } + } +} \ No newline at end of file diff --git a/packages/json/package.json b/packages/json/package.json new file mode 100644 index 0000000000000..dafdff98425a4 --- /dev/null +++ b/packages/json/package.json @@ -0,0 +1,51 @@ +{ + "name": "@theia/json", + "version": "0.3.14", + "description": "Theia - JSON Extension", + "dependencies": { + "@theia/core": "^0.3.14", + "@theia/languages": "^0.3.14", + "@theia/monaco": "^0.3.14", + "vscode-json-languageserver": "^1.0.1" + }, + "devDependencies": { + "@theia/ext-scripts": "^0.3.14" + }, + "scripts": { + "prepare": "yarn run clean && yarn run build", + "clean": "theiaext clean", + "build": "theiaext build", + "watch": "theiaext watch", + "test": "theiaext test", + "docs": "theiaext docs" + }, + "publishConfig": { + "access": "public" + }, + "theiaExtensions": [ + { + "frontend": "lib/browser/json-frontend-module", + "backend": "lib/node/json-backend-module" + } + ], + "keywords": [ + "theia-extension" + ], + "license": "EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0", + "repository": { + "type": "git", + "url": "https://github.com/theia-ide/theia.git" + }, + "bugs": { + "url": "https://github.com/theia-ide/theia/issues" + }, + "homepage": "https://github.com/theia-ide/theia", + "files": [ + "lib", + "src", + "data" + ], + "nyc": { + "extends": "../../configs/nyc.json" + } +} diff --git a/packages/json/src/browser/json-client-contribution.ts b/packages/json/src/browser/json-client-contribution.ts new file mode 100644 index 0000000000000..4f477e2671a20 --- /dev/null +++ b/packages/json/src/browser/json-client-contribution.ts @@ -0,0 +1,96 @@ +/******************************************************************************** + * Copyright (C) 2018 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 + ********************************************************************************/ + +import { injectable, inject } from 'inversify'; +import { + BaseLanguageClientContribution, + Workspace, + Languages, + LanguageClientFactory, + ILanguageClient, + DocumentSelector +} from '@theia/languages/lib/browser'; +import { JSON_LANGUAGE_ID, JSON_LANGUAGE_NAME, JSONC_LANGUAGE_ID } from '../common'; +import { ResourceProvider } from '@theia/core'; +import URI from '@theia/core/lib/common/uri'; + +@injectable() +export class JsonClientContribution extends BaseLanguageClientContribution { + + readonly id = JSON_LANGUAGE_ID; + readonly name = JSON_LANGUAGE_NAME; + + constructor( + @inject(Workspace) protected readonly workspace: Workspace, + @inject(ResourceProvider) protected readonly resourceProvider: ResourceProvider, + @inject(Languages) protected readonly languages: Languages, + @inject(LanguageClientFactory) protected readonly languageClientFactory: LanguageClientFactory + ) { + super(workspace, languages, languageClientFactory); + } + + protected get globPatterns() { + return [ + '**/*.json', + '**/*.jsonc', + ]; + } + + protected get documentSelector(): DocumentSelector | undefined { + return [this.id, JSONC_LANGUAGE_ID]; + } + + protected get configurationSection(): string[] { + return [this.id]; + } + + protected onReady(languageClient: ILanguageClient): void { + // handle content request + languageClient.onRequest('vscode/content', async (uriPath: string) => { + const uri = new URI(uriPath); + const resource = await this.resourceProvider(uri); + const text = await resource.readContents(); + return text; + }); + super.onReady(languageClient); + setTimeout(() => this.initializeJsonSchemaAssociations()); + } + + protected async initializeJsonSchemaAssociations(): Promise { + const client = await this.languageClient; + const url = `${window.location.protocol}//schemastore.azurewebsites.net/api/json/catalog.json`; + const response = await fetch(url); + const schemas: SchemaData[] = (await response.json()).schemas!; + const registry: { [pattern: string]: string[] } = {}; + for (const s of schemas) { + if (s.fileMatch) { + for (const p of s.fileMatch) { + registry[p] = [s.url]; + } + } + } + client.sendNotification('json/schemaAssociations', registry); + } + +} + +interface SchemaData { + name: string; + description: string; + fileMatch?: string[]; + url: string; + schema: any; +} diff --git a/packages/json/src/browser/json-frontend-module.ts b/packages/json/src/browser/json-frontend-module.ts new file mode 100644 index 0000000000000..cb621de82e9df --- /dev/null +++ b/packages/json/src/browser/json-frontend-module.ts @@ -0,0 +1,29 @@ +/******************************************************************************** + * Copyright (C) 2018 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 + ********************************************************************************/ + +import { ContainerModule } from 'inversify'; +import { LanguageClientContribution } from '@theia/languages/lib/browser'; +import { JsonClientContribution } from './json-client-contribution'; +import { JsonGrammarContribution } from './json-grammar-contribution'; +import { LanguageGrammarDefinitionContribution } from '@theia/monaco/lib/browser/textmate'; +import { bindJsonPreferences } from './json-preferences'; + +export default new ContainerModule(bind => { + bindJsonPreferences(bind); + + bind(LanguageClientContribution).to(JsonClientContribution).inSingletonScope(); + bind(LanguageGrammarDefinitionContribution).to(JsonGrammarContribution).inSingletonScope(); +}); diff --git a/packages/json/src/browser/json-grammar-contribution.ts b/packages/json/src/browser/json-grammar-contribution.ts new file mode 100644 index 0000000000000..a406501a60c6a --- /dev/null +++ b/packages/json/src/browser/json-grammar-contribution.ts @@ -0,0 +1,110 @@ +/******************************************************************************** + * Copyright (C) 2018 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 + ********************************************************************************/ + +import { LanguageGrammarDefinitionContribution, TextmateRegistry } from '@theia/monaco/lib/browser/textmate'; +import { injectable } from 'inversify'; +import { JSON_LANGUAGE_ID, JSONC_LANGUAGE_ID } from '../common'; + +@injectable() +export class JsonGrammarContribution implements LanguageGrammarDefinitionContribution { + + readonly config: monaco.languages.LanguageConfiguration = { + 'comments': { + 'lineComment': '//', + 'blockComment': ['/*', '*/'] + }, + 'brackets': [ + ['{', '}'], + ['[', ']'] + ], + 'autoClosingPairs': [ + { 'open': '{', 'close': '}', 'notIn': ['string'] }, + { 'open': '[', 'close': ']', 'notIn': ['string'] }, + { 'open': '(', 'close': ')', 'notIn': ['string'] }, + { 'open': '\'', 'close': '\'', 'notIn': ['string'] }, + { 'open': '/*', 'close': '*/', 'notIn': ['string'] }, + { 'open': '\'', 'close': '\'', 'notIn': ['string', 'comment'] }, + { 'open': '`', 'close': '`', 'notIn': ['string', 'comment'] } + ] + }; + + registerTextmateLanguage(registry: TextmateRegistry) { + monaco.languages.register({ + id: JSON_LANGUAGE_ID, + 'aliases': [ + 'JSON', + 'json' + ], + 'extensions': [ + '.json', + '.bowerrc', + '.jshintrc', + '.jscsrc', + '.eslintrc', + '.babelrc', + '.webmanifest', + '.js.map', + '.css.map' + ], + 'filenames': [ + '.watchmanconfig', + '.ember-cli' + ], + 'mimetypes': [ + 'application/json', + 'application/manifest+json' + ] + }); + + monaco.languages.setLanguageConfiguration(JSON_LANGUAGE_ID, this.config); + + const jsonGrammar = require('../../data/json.tmLanguage.json'); + registry.registerTextmateGrammarScope('source.json', { + async getGrammarDefinition() { + return { + format: 'json', + content: jsonGrammar + }; + } + }); + + registry.mapLanguageIdToTextmateGrammar(JSON_LANGUAGE_ID, 'source.json'); + + // jsonc + monaco.languages.register({ + id: JSONC_LANGUAGE_ID, + 'aliases': [ + 'JSON with Comments' + ], + 'extensions': [ + '.jsonc' + ] + }); + + monaco.languages.setLanguageConfiguration(JSONC_LANGUAGE_ID, this.config); + + const jsoncGrammar = require('../../data/jsonc.tmLanguage.json'); + registry.registerTextmateGrammarScope('source.json.comments', { + async getGrammarDefinition() { + return { + format: 'json', + content: jsoncGrammar + }; + } + }); + registry.mapLanguageIdToTextmateGrammar(JSONC_LANGUAGE_ID, 'source.json.comments'); + } +} diff --git a/packages/json/src/browser/json-preferences.ts b/packages/json/src/browser/json-preferences.ts new file mode 100644 index 0000000000000..69a009ca71d19 --- /dev/null +++ b/packages/json/src/browser/json-preferences.ts @@ -0,0 +1,103 @@ +/******************************************************************************** + * Copyright (C) 2018 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 + ********************************************************************************/ + +import { interfaces } from 'inversify'; +import { + createPreferenceProxy, + PreferenceProxy, + PreferenceService, + PreferenceContribution, + PreferenceSchema, + PreferenceChangeEvent +} from '@theia/core/lib/browser/preferences'; + +export const jsonPreferenceSchema: PreferenceSchema = { + 'type': 'object', + 'properties': { + 'json.schemas': { + 'type': 'array', + 'description': 'Associate schemas to JSON files in the current project', + 'items': { + 'type': 'object', + 'default': { + 'fileMatch': [ + '/myfile' + ], + 'url': 'schemaURL' + }, + 'properties': { + 'url': { + 'type': 'string', + 'default': '/user.schema.json', + 'description': 'A URL to a schema or a relative path to a schema in the current directory' + }, + 'fileMatch': { + 'type': 'array', + 'items': { + 'type': 'string', + 'default': 'MyFile.json', + 'description': 'A file pattern that can contain \'*\' to match against when resolving JSON files to schemas.' + }, + 'minItems': 1, + 'description': 'An array of file patterns to match against when resolving JSON files to schemas.' + } + } + } + }, + 'json.format.enable': { + 'type': 'boolean', + 'default': true, + 'description': 'Enable/disable default JSON formatter' + }, + 'json.trace.server': { + 'type': 'string', + 'enum': [ + 'off', + 'messages', + 'verbose' + ], + 'default': 'off', + 'description': 'Enable/disable tracing communications with the JSON language server' + } + } +}; + +export interface JsonSchemaConfiguration { + url: string + fileMatch: string[] +} + +export interface JsonConfiguration { + 'json.schemas': JsonSchemaConfiguration[] + 'json.format.enable': boolean +} +export type JsonPreferenceChange = PreferenceChangeEvent; + +export const JsonPreferences = Symbol('JsonPreferences'); +export type JsonPreferences = PreferenceProxy; + +export function createJsonPreferences(preferences: PreferenceService): JsonPreferences { + return createPreferenceProxy(preferences, jsonPreferenceSchema); +} + +export function bindJsonPreferences(bind: interfaces.Bind): void { + bind(JsonPreferences).toDynamicValue(ctx => { + const preferences = ctx.container.get(PreferenceService); + return createJsonPreferences(preferences); + }).inSingletonScope(); + + bind(PreferenceContribution).toConstantValue({ schema: jsonPreferenceSchema }); +} diff --git a/packages/json/src/browser/monaco.d.ts b/packages/json/src/browser/monaco.d.ts new file mode 100644 index 0000000000000..0b9896563afaf --- /dev/null +++ b/packages/json/src/browser/monaco.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/json/src/common/index.ts b/packages/json/src/common/index.ts new file mode 100644 index 0000000000000..42e14b0185950 --- /dev/null +++ b/packages/json/src/common/index.ts @@ -0,0 +1,21 @@ +/******************************************************************************** + * Copyright (C) 2018 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 + ********************************************************************************/ + +export const JSON_LANGUAGE_ID = 'json'; +export const JSON_LANGUAGE_NAME = 'JSON'; + +export const JSONC_LANGUAGE_ID = 'jsonc'; +export const JSONC_LANGUAGE_NAME = 'JSONC'; diff --git a/packages/json/src/node/json-backend-module.ts b/packages/json/src/node/json-backend-module.ts new file mode 100644 index 0000000000000..68b899006b25e --- /dev/null +++ b/packages/json/src/node/json-backend-module.ts @@ -0,0 +1,23 @@ +/******************************************************************************** + * Copyright (C) 2018 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 + ********************************************************************************/ + +import { ContainerModule } from 'inversify'; +import { LanguageServerContribution } from '@theia/languages/lib/node'; +import { JsonContribution } from './json-contribution'; + +export default new ContainerModule(bind => { + bind(LanguageServerContribution).to(JsonContribution).inSingletonScope(); +}); diff --git a/packages/json/src/node/json-contribution.ts b/packages/json/src/node/json-contribution.ts new file mode 100644 index 0000000000000..ec79e5bb634fe --- /dev/null +++ b/packages/json/src/node/json-contribution.ts @@ -0,0 +1,51 @@ +/******************************************************************************** + * Copyright (C) 2018 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 + ********************************************************************************/ + +import { injectable } from 'inversify'; +import { BaseLanguageServerContribution, IConnection } from '@theia/languages/lib/node'; +import { JSON_LANGUAGE_ID, JSON_LANGUAGE_NAME } from '../common'; +import * as path from 'path'; + +@injectable() +export class JsonContribution extends BaseLanguageServerContribution { + + readonly id = JSON_LANGUAGE_ID; + readonly name = JSON_LANGUAGE_NAME; + + start(clientConnection: IConnection): void { + const command = 'node'; + const args: string[] = [ + path.resolve(__dirname, './json-starter'), + '--stdio' + ]; + try { + const serverConnection = this.createProcessStreamConnection(command, args); + serverConnection.reader.onError(err => { + console.log(err); + }); + this.forward(clientConnection, serverConnection); + } catch (e) { + console.log(e); + throw e; + } + } + + protected onDidFailSpawnProcess(error: Error): void { + super.onDidFailSpawnProcess(error); + console.error('Error starting json language server.'); + } + +} diff --git a/packages/json/src/node/json-starter.ts b/packages/json/src/node/json-starter.ts new file mode 100644 index 0000000000000..bfee41cba6dbe --- /dev/null +++ b/packages/json/src/node/json-starter.ts @@ -0,0 +1,17 @@ +/******************************************************************************** + * Copyright (C) 2018 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 + ********************************************************************************/ + + require('vscode-json-languageserver/out/jsonServerMain'); diff --git a/packages/json/src/package.spec.ts b/packages/json/src/package.spec.ts new file mode 100644 index 0000000000000..75ef559aa7e94 --- /dev/null +++ b/packages/json/src/package.spec.ts @@ -0,0 +1,28 @@ +/******************************************************************************** + * Copyright (C) 2018 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 + ********************************************************************************/ + +/* note: this bogus test file is required so that + we are able to run mocha unit tests on this + package, without having any actual unit tests in it. + This way a coverage report will be generated, + showing 0% coverage, instead of no report. + This file can be removed once we have real unit + tests in place. */ + +describe('json package', () => { + + it('support code coverage statistics', () => true); +}); diff --git a/packages/keymaps/package.json b/packages/keymaps/package.json index 028b0e1d0f134..39fbe5654cfb2 100644 --- a/packages/keymaps/package.json +++ b/packages/keymaps/package.json @@ -1,16 +1,17 @@ { "name": "@theia/keymaps", - "version": "0.3.13", + "version": "0.3.14", "description": "Theia - Custom Keymaps Extension", "dependencies": { - "@theia/core": "^0.3.13", - "@theia/userstorage": "^0.3.13", - "@theia/workspace": "^0.3.13", + "@theia/core": "^0.3.14", + "@theia/monaco": "^0.3.14", + "@theia/userstorage": "^0.3.14", + "@theia/workspace": "^0.3.14", "ajv": "^5.2.2", "jsonc-parser": "^1.0.1" }, "devDependencies": { - "@theia/ext-scripts": "^0.3.13", + "@theia/ext-scripts": "^0.3.14", "@types/temp": "^0.8.29", "temp": "^0.8.3" }, diff --git a/packages/keymaps/src/browser/keymaps-frontend-module.ts b/packages/keymaps/src/browser/keymaps-frontend-module.ts index 221c76680c326..69283026295a6 100644 --- a/packages/keymaps/src/browser/keymaps-frontend-module.ts +++ b/packages/keymaps/src/browser/keymaps-frontend-module.ts @@ -20,6 +20,8 @@ import { KeymapsFrontendContribution } from './keymaps-frontend-contribution'; import { CommandContribution, MenuContribution } from '@theia/core/lib/common'; import { KeymapsParser } from './keymaps-parser'; +import './monaco-contribution'; + export default new ContainerModule(bind => { bind(KeymapsParser).toSelf().inSingletonScope(); bind(KeymapsService).toSelf().inSingletonScope(); diff --git a/packages/keymaps/src/browser/monaco-contribution.ts b/packages/keymaps/src/browser/monaco-contribution.ts new file mode 100644 index 0000000000000..afc9cd4ef02d9 --- /dev/null +++ b/packages/keymaps/src/browser/monaco-contribution.ts @@ -0,0 +1,25 @@ +/******************************************************************************** + * Copyright (C) 2018 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 + ********************************************************************************/ + +monaco.languages.register({ + id: 'jsonc', + 'aliases': [ + 'JSON with Comments' + ], + 'filenames': [ + 'keymaps.json' + ] +}); diff --git a/packages/keymaps/src/browser/monaco.d.ts b/packages/keymaps/src/browser/monaco.d.ts new file mode 100644 index 0000000000000..ad8ef7c744a72 --- /dev/null +++ b/packages/keymaps/src/browser/monaco.d.ts @@ -0,0 +1 @@ +/// \ No newline at end of file diff --git a/packages/languages/package.json b/packages/languages/package.json index c1fe15cb5220b..80a8dcbf27c56 100644 --- a/packages/languages/package.json +++ b/packages/languages/package.json @@ -1,14 +1,15 @@ { "name": "@theia/languages", - "version": "0.3.13", + "version": "0.3.14", "description": "Theia - Languages Extension", "dependencies": { - "@theia/core": "^0.3.13", - "@theia/output": "^0.3.13", - "@theia/process": "^0.3.13", - "@theia/workspace": "^0.3.13", + "@theia/core": "^0.3.14", + "@theia/output": "^0.3.14", + "@theia/process": "^0.3.14", + "@theia/workspace": "^0.3.14", "@types/uuid": "^3.4.3", - "monaco-languageclient": "next", + "monaco-languageclient": "^0.9.0", + "@typefox/monaco-editor-core": "^0.14.6", "uuid": "^3.2.1" }, "publishConfig": { @@ -45,9 +46,9 @@ "docs": "theiaext docs" }, "devDependencies": { - "@theia/ext-scripts": "^0.3.13" + "@theia/ext-scripts": "^0.3.14" }, "nyc": { "extends": "../../configs/nyc.json" } -} \ No newline at end of file +} diff --git a/packages/languages/src/browser/language-client-contribution.ts b/packages/languages/src/browser/language-client-contribution.ts index 59a30e227f57a..ac0e533abb7c7 100644 --- a/packages/languages/src/browser/language-client-contribution.ts +++ b/packages/languages/src/browser/language-client-contribution.ts @@ -21,7 +21,7 @@ import { FrontendApplication, WebSocketConnectionProvider, WebSocketOptions } fr import { LanguageContribution, ILanguageClient, LanguageClientOptions, DocumentSelector, TextDocument, FileSystemWatcher, - Workspace, Languages + Workspace, Languages, State } from './language-client-services'; import { MessageConnection } from 'vscode-jsonrpc'; import { LanguageClientFactory } from './language-client-factory'; @@ -111,7 +111,20 @@ export abstract class BaseLanguageClientContribution implements LanguageClientCo return toDeactivate; } + protected state: State | undefined; + get running(): boolean { + return this.state === State.Running; + } + restart(): void { + if (this._languageClient) { + this._languageClient.stop(); + } + } + protected onWillStart(languageClient: ILanguageClient): void { + languageClient.onDidChangeState(({ newState }) => { + this.state = newState; + }); languageClient.onReady().then(() => this.onReady(languageClient)); } @@ -133,19 +146,29 @@ export abstract class BaseLanguageClientContribution implements LanguageClientCo } protected createOptions(): LanguageClientOptions { - const { id, documentSelector, fileEvents } = this; + const { id, documentSelector, fileEvents, configurationSection, initializationOptions } = this; return { documentSelector, - synchronize: { fileEvents }, + synchronize: { fileEvents, configurationSection }, initializationFailedHandler: err => { const detail = err instanceof Error ? `: ${err.message}` : '.'; this.messageService.error(`Failed to start ${this.name} language server${detail}`); return false; }, - diagnosticCollectionName: id + diagnosticCollectionName: id, + initializationOptions }; } + // tslint:disable-next-line:no-any + protected get initializationOptions(): any | (() => any) | undefined { + return undefined; + } + + protected get configurationSection(): string | string[] | undefined { + return undefined; + } + protected get workspaceContains(): string[] { return []; } diff --git a/packages/languages/src/browser/language-client-factory.ts b/packages/languages/src/browser/language-client-factory.ts index f4ce177480648..4052e6d299d6d 100644 --- a/packages/languages/src/browser/language-client-factory.ts +++ b/packages/languages/src/browser/language-client-factory.ts @@ -65,6 +65,7 @@ export class LanguageClientFactory { clientOptions.initializationFailedHandler = e => !!initializationFailedHandler && initializationFailedHandler(e); connection.onDispose(() => initializationFailedHandler = () => false); return new MonacoLanguageClient({ + id: contribution.id, name: contribution.name, clientOptions, connectionProvider: { diff --git a/packages/languages/src/browser/languages-frontend-module.ts b/packages/languages/src/browser/languages-frontend-module.ts index 6494a4db78cae..96384cb78d2b9 100644 --- a/packages/languages/src/browser/languages-frontend-module.ts +++ b/packages/languages/src/browser/languages-frontend-module.ts @@ -16,7 +16,7 @@ import { ContainerModule } from 'inversify'; import { bindContributionProvider, CommandContribution } from '@theia/core/lib/common'; -import { FrontendApplicationContribution, KeybindingContribution } from '@theia/core/lib/browser'; +import { FrontendApplicationContribution, KeybindingContribution, QuickOpenContribution } from '@theia/core/lib/browser'; import { Window } from './language-client-services'; import { WindowImpl } from './window-impl'; import { LanguageClientFactory } from './language-client-factory'; @@ -35,8 +35,9 @@ export default new ContainerModule(bind => { bind(FrontendApplicationContribution).to(LanguagesFrontendContribution); bind(WorkspaceSymbolCommand).toSelf().inSingletonScope(); - bind(CommandContribution).toDynamicValue(ctx => ctx.container.get(WorkspaceSymbolCommand)); - bind(KeybindingContribution).toDynamicValue(ctx => ctx.container.get(WorkspaceSymbolCommand)); + for (const identifier of [CommandContribution, KeybindingContribution, QuickOpenContribution]) { + bind(identifier).toService(WorkspaceSymbolCommand); + } bind(LanguageClientProviderImpl).toSelf().inSingletonScope(); bind(LanguageClientProvider).toDynamicValue(ctx => ctx.container.get(LanguageClientProviderImpl)).inSingletonScope(); diff --git a/packages/languages/src/browser/workspace-symbols.ts b/packages/languages/src/browser/workspace-symbols.ts index e00a85344b509..944e7f39d4149 100644 --- a/packages/languages/src/browser/workspace-symbols.ts +++ b/packages/languages/src/browser/workspace-symbols.ts @@ -16,8 +16,8 @@ import { injectable, inject } from 'inversify'; import { - QuickOpenService, QuickOpenModel, QuickOpenItem, OpenerService, - QuickOpenMode, KeybindingContribution, KeybindingRegistry + PrefixQuickOpenService, QuickOpenModel, QuickOpenItem, OpenerService, + QuickOpenMode, KeybindingContribution, KeybindingRegistry, QuickOpenHandler, QuickOpenOptions, QuickOpenContribution, QuickOpenHandlerRegistry } from '@theia/core/lib/browser'; import { Languages, WorkspaceSymbolParams, SymbolInformation } from './language-client-services'; import { CancellationTokenSource, CommandRegistry, CommandHandler, Command, SelectionService } from '@theia/core'; @@ -26,7 +26,10 @@ import { CommandContribution } from '@theia/core/lib/common'; import { Range } from 'vscode-languageserver-types'; @injectable() -export class WorkspaceSymbolCommand implements QuickOpenModel, CommandContribution, KeybindingContribution, CommandHandler { +export class WorkspaceSymbolCommand implements QuickOpenModel, CommandContribution, KeybindingContribution, CommandHandler, QuickOpenHandler, QuickOpenContribution { + + readonly prefix = '#'; + readonly description = 'Go to symbol in workspace'; private command: Command = { id: 'languages.workspace.symbol', @@ -35,7 +38,7 @@ export class WorkspaceSymbolCommand implements QuickOpenModel, CommandContributi constructor(@inject(Languages) protected languages: Languages, @inject(OpenerService) protected readonly openerService: OpenerService, - @inject(QuickOpenService) protected quickOpenService: QuickOpenService, + @inject(PrefixQuickOpenService) protected quickOpenService: PrefixQuickOpenService, @inject(SelectionService) protected selectionService: SelectionService) { } isEnabled() { @@ -43,11 +46,23 @@ export class WorkspaceSymbolCommand implements QuickOpenModel, CommandContributi } execute() { - this.quickOpenService.open(this, { - placeholder: 'Type to search for symbols.', + this.quickOpenService.open(this.prefix); + } + + init(): void { } + + getModel(): QuickOpenModel { + return this; + } + + getOptions(): QuickOpenOptions { + return { fuzzyMatchLabel: true, showItemsWithoutHighlight: true, - }); + onClose: () => { + this.cancellationSource.cancel(); + } + }; } registerCommands(commands: CommandRegistry): void { @@ -61,6 +76,10 @@ export class WorkspaceSymbolCommand implements QuickOpenModel, CommandContributi }); } + registerQuickOpenHandlers(handlers: QuickOpenHandlerRegistry): void { + handlers.registerHandler(this); + } + private cancellationSource = new CancellationTokenSource(); async onType(lookFor: string, acceptor: (items: QuickOpenItem[]) => void): Promise { @@ -81,6 +100,12 @@ export class WorkspaceSymbolCommand implements QuickOpenModel, CommandContributi for (const symbol of symbols) { items.push(this.createItem(symbol)); } + if (items.length === 0) { + items.push(new QuickOpenItem({ + label: lookFor.length === 0 ? 'Type to search for symbols' : 'No symbols matching', + run: () => false + })); + } acceptor(items); } }); diff --git a/packages/languages/src/node/language-server-contribution.ts b/packages/languages/src/node/language-server-contribution.ts index 5db5db29714ff..778af87e84ce9 100644 --- a/packages/languages/src/node/language-server-contribution.ts +++ b/packages/languages/src/node/language-server-contribution.ts @@ -16,7 +16,7 @@ import * as net from 'net'; import * as cp from 'child_process'; -import { injectable, inject, named } from 'inversify'; +import { injectable, inject } from 'inversify'; import { Message, isRequestMessage } from 'vscode-ws-jsonrpc'; import { InitializeParams, InitializeRequest } from 'vscode-languageserver-protocol'; import { @@ -25,7 +25,7 @@ import { forward, IConnection } from 'vscode-ws-jsonrpc/lib/server'; -import { MaybePromise, ILogger } from '@theia/core/lib/common'; +import { MaybePromise } from '@theia/core/lib/common'; import { LanguageContribution } from '../common'; import { RawProcess, RawProcessFactory } from '@theia/process/lib/node/raw-process'; import { ProcessManager } from '@theia/process/lib/node/process-manager'; @@ -51,8 +51,6 @@ export abstract class BaseLanguageServerContribution implements LanguageServerCo @inject(ProcessManager) protected readonly processManager: ProcessManager; - @inject(ILogger) @named('languages') protected readonly logger: ILogger; - abstract start(clientConnection: IConnection): void; protected forward(clientConnection: IConnection, serverConnection: IConnection): void { @@ -66,9 +64,6 @@ export abstract class BaseLanguageServerContribution implements LanguageServerCo initializeParams.processId = process.pid; } } - - this.logger.debug(JSON.stringify(message)); - return message; } diff --git a/packages/languages/src/node/languages-backend-module.ts b/packages/languages/src/node/languages-backend-module.ts index 178b4e639bef3..2edf175ded38c 100644 --- a/packages/languages/src/node/languages-backend-module.ts +++ b/packages/languages/src/node/languages-backend-module.ts @@ -24,7 +24,6 @@ export default new ContainerModule(bind => { bind(MessagingService.Contribution).to(LanguagesBackendContribution).inSingletonScope(); bindContributionProvider(bind, LanguageServerContribution); - // FIXME: get rid of it, replace by a logger per a language bind(ILogger).toDynamicValue(ctx => { const logger = ctx.container.get(ILogger); return logger.child('languages'); diff --git a/packages/markers/package.json b/packages/markers/package.json index 05de27f58bc36..6eb9ac0ab9f71 100644 --- a/packages/markers/package.json +++ b/packages/markers/package.json @@ -1,12 +1,12 @@ { "name": "@theia/markers", - "version": "0.3.13", + "version": "0.3.14", "description": "Theia - Markers Extension", "dependencies": { - "@theia/core": "^0.3.13", - "@theia/filesystem": "^0.3.13", - "@theia/navigator": "^0.3.13", - "@theia/workspace": "^0.3.13" + "@theia/core": "^0.3.14", + "@theia/filesystem": "^0.3.14", + "@theia/navigator": "^0.3.14", + "@theia/workspace": "^0.3.14" }, "publishConfig": { "access": "public" @@ -41,7 +41,7 @@ "docs": "theiaext docs" }, "devDependencies": { - "@theia/ext-scripts": "^0.3.13" + "@theia/ext-scripts": "^0.3.14" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/markers/src/browser/style/index.css b/packages/markers/src/browser/style/index.css index bf5dad1b37fc5..5fee852d89a8e 100644 --- a/packages/markers/src/browser/style/index.css +++ b/packages/markers/src/browser/style/index.css @@ -90,7 +90,7 @@ padding: 0 5px; text-align: center; font-size: calc(var(--theia-ui-font-size0) * 0.8); - color: var(--theia-ui-icon-font-color); + color: var(--theia-inverse-ui-font-color0); font-weight: 400; min-width: 8px; height: calc(var(--theia-private-horizontal-tab-height) * 0.8); diff --git a/packages/merge-conflicts/package.json b/packages/merge-conflicts/package.json index 40bcc57634f5b..29e0eb25136d8 100644 --- a/packages/merge-conflicts/package.json +++ b/packages/merge-conflicts/package.json @@ -1,11 +1,11 @@ { "name": "@theia/merge-conflicts", - "version": "0.3.13", + "version": "0.3.14", "description": "Theia - Merge Conflicts Extension", "dependencies": { - "@theia/core": "^0.3.13", - "@theia/editor": "^0.3.13", - "@theia/languages": "^0.3.13" + "@theia/core": "^0.3.14", + "@theia/editor": "^0.3.14", + "@theia/languages": "^0.3.14" }, "publishConfig": { "access": "public" @@ -40,7 +40,7 @@ "docs": "theiaext docs" }, "devDependencies": { - "@theia/ext-scripts": "^0.3.13" + "@theia/ext-scripts": "^0.3.14" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/messages/package.json b/packages/messages/package.json index 3b3fc13a9ea41..a5a0db02cb736 100644 --- a/packages/messages/package.json +++ b/packages/messages/package.json @@ -1,9 +1,9 @@ { "name": "@theia/messages", - "version": "0.3.13", + "version": "0.3.14", "description": "Theia - Messages Extension", "dependencies": { - "@theia/core": "^0.3.13" + "@theia/core": "^0.3.14" }, "publishConfig": { "access": "public" @@ -39,7 +39,7 @@ "docs": "theiaext docs" }, "devDependencies": { - "@theia/ext-scripts": "^0.3.13" + "@theia/ext-scripts": "^0.3.14" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/messages/src/browser/notifications-message-client.ts b/packages/messages/src/browser/notifications-message-client.ts index bb5a1bdf16771..26de15a6d5054 100644 --- a/packages/messages/src/browser/notifications-message-client.ts +++ b/packages/messages/src/browser/notifications-message-client.ts @@ -33,12 +33,25 @@ export class NotificationsMessageClient extends MessageClient { return this.show(message); } + protected visibleMessages = new Set(); protected show(message: Message): Promise { + const key = this.getKey(message); + if (this.visibleMessages.has(key)) { + return Promise.resolve(undefined); + } + this.visibleMessages.add(key); return new Promise(resolve => { - this.showToast(message, a => resolve(a)); + this.showToast(message, a => { + this.visibleMessages.delete(key); + resolve(a); + }); }); } + protected getKey(m: Message): string { + return `${m.type}-${m.text}-${m.actions ? m.actions.join('|') : '|'}`; + } + protected showToast(message: Message, onCloseFn: (action: string | undefined) => void): void { const icon = this.iconFor(message.type); const text = message.text; diff --git a/packages/metrics/package.json b/packages/metrics/package.json index 29ee769da0996..f2facfbf18b84 100644 --- a/packages/metrics/package.json +++ b/packages/metrics/package.json @@ -1,10 +1,10 @@ { "name": "@theia/metrics", - "version": "0.3.13", + "version": "0.3.14", "description": "Theia - Metrics Extension", "dependencies": { - "@theia/application-manager": "^0.3.13", - "@theia/core": "^0.3.13", + "@theia/application-manager": "^0.3.14", + "@theia/core": "^0.3.14", "prom-client": "^10.2.0" }, "publishConfig": { @@ -40,7 +40,7 @@ "docs": "theiaext docs" }, "devDependencies": { - "@theia/ext-scripts": "^0.3.13" + "@theia/ext-scripts": "^0.3.14" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/mini-browser/package.json b/packages/mini-browser/package.json index 056fb15c99e90..a6e218b67dd2c 100644 --- a/packages/mini-browser/package.json +++ b/packages/mini-browser/package.json @@ -1,10 +1,10 @@ { "name": "@theia/mini-browser", - "version": "0.3.13", + "version": "0.3.14", "description": "Theia - Mini-Browser Extension", "dependencies": { - "@theia/core": "^0.3.13", - "@theia/filesystem": "^0.3.13", + "@theia/core": "^0.3.14", + "@theia/filesystem": "^0.3.14", "@types/mime-types": "^2.1.0", "mime-types": "^2.1.18", "pdfobject": "^2.0.201604172" @@ -43,7 +43,7 @@ "docs": "theiaext docs" }, "devDependencies": { - "@theia/ext-scripts": "^0.3.13" + "@theia/ext-scripts": "^0.3.14" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/monaco/package.json b/packages/monaco/package.json index c3e708fb6481f..73572b9d42416 100644 --- a/packages/monaco/package.json +++ b/packages/monaco/package.json @@ -1,15 +1,15 @@ { "name": "@theia/monaco", - "version": "0.3.13", + "version": "0.3.14", "description": "Theia - Monaco Extension", "dependencies": { - "@theia/core": "^0.3.13", - "@theia/editor": "^0.3.13", - "@theia/filesystem": "^0.3.13", - "@theia/languages": "^0.3.13", - "@theia/markers": "^0.3.13", - "@theia/outline-view": "^0.3.13", - "@theia/workspace": "^0.3.13", + "@theia/core": "^0.3.14", + "@theia/editor": "^0.3.14", + "@theia/filesystem": "^0.3.14", + "@theia/languages": "^0.3.14", + "@theia/markers": "^0.3.14", + "@theia/outline-view": "^0.3.14", + "@theia/workspace": "^0.3.14", "monaco-css": "^2.0.1", "monaco-html": "^2.0.2", "monaco-textmate": "^3.0.0", @@ -50,9 +50,9 @@ "docs": "theiaext docs" }, "devDependencies": { - "@theia/ext-scripts": "^0.3.13" + "@theia/ext-scripts": "^0.3.14" }, "nyc": { "extends": "../../configs/nyc.json" } -} \ No newline at end of file +} diff --git a/packages/monaco/src/browser/monaco-bulk-edit-service.ts b/packages/monaco/src/browser/monaco-bulk-edit-service.ts new file mode 100644 index 0000000000000..ebfb6478aee2b --- /dev/null +++ b/packages/monaco/src/browser/monaco-bulk-edit-service.ts @@ -0,0 +1,30 @@ +/******************************************************************************** + * Copyright (C) 2018 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 + ********************************************************************************/ + +import { injectable, inject } from 'inversify'; +import { MonacoWorkspace } from './monaco-workspace'; + +@injectable() +export class MonacoBulkEditService implements monaco.editor.IBulkEditService { + + @inject(MonacoWorkspace) + protected readonly workspace: MonacoWorkspace; + + apply(edit: monaco.languages.WorkspaceEdit): monaco.Promise { + return this.workspace.applyBulkEdit(edit); + } + +} diff --git a/packages/monaco/src/browser/monaco-command-service.ts b/packages/monaco/src/browser/monaco-command-service.ts index 53e53d5ca5830..a76dff3be9718 100644 --- a/packages/monaco/src/browser/monaco-command-service.ts +++ b/packages/monaco/src/browser/monaco-command-service.ts @@ -27,7 +27,7 @@ export interface MonacoCommandServiceFactory { @injectable() export class MonacoCommandService implements ICommandService { - protected readonly onWillExecuteCommandEmitter = new Emitter(); + readonly _onWillExecuteCommand = new Emitter(); protected delegate: ICommandService | undefined; protected readonly delegateListeners = new DisposableCollection(); @@ -37,15 +37,15 @@ export class MonacoCommandService implements ICommandService { ) { } get onWillExecuteCommand(): monaco.IEvent { - return this.onWillExecuteCommandEmitter.event; + return this._onWillExecuteCommand.event; } setDelegate(delegate: ICommandService | undefined) { this.delegateListeners.dispose(); this.delegate = delegate; if (this.delegate) { - this.delegateListeners.push(this.delegate.onWillExecuteCommand(event => - this.onWillExecuteCommandEmitter.fire(event) + this.delegateListeners.push(this.delegate._onWillExecuteCommand.event(event => + this._onWillExecuteCommand.fire(event) )); } } @@ -54,7 +54,7 @@ export class MonacoCommandService implements ICommandService { const handler = this.commandRegistry.getActiveHandler(commandId, ...args); if (handler) { try { - this.onWillExecuteCommandEmitter.fire({ commandId }); + this._onWillExecuteCommand.fire({ commandId }); return monaco.Promise.wrap(handler.execute(...args)); } catch (err) { return monaco.Promise.wrapError(err); diff --git a/packages/monaco/src/browser/monaco-configurations.ts b/packages/monaco/src/browser/monaco-configurations.ts new file mode 100644 index 0000000000000..59cb7cba40dd8 --- /dev/null +++ b/packages/monaco/src/browser/monaco-configurations.ts @@ -0,0 +1,131 @@ +/******************************************************************************** + * Copyright (C) 2018 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 + ********************************************************************************/ + +// tslint:disable:no-any + +import { injectable, inject, postConstruct } from 'inversify'; +import { JSONExt, JSONObject, JSONValue } from '@phosphor/coreutils'; +import { Configurations, ConfigurationChangeEvent, WorkspaceConfiguration } from 'monaco-languageclient'; +import { Event, Emitter } from '@theia/core/lib/common'; +import { PreferenceServiceImpl, PreferenceChanges } from '@theia/core/lib/browser'; + +@injectable() +export class MonacoConfigurations implements Configurations { + + protected readonly onDidChangeConfigurationEmitter = new Emitter(); + readonly onDidChangeConfiguration: Event = this.onDidChangeConfigurationEmitter.event; + + @inject(PreferenceServiceImpl) + protected readonly preferences: PreferenceServiceImpl; + + protected tree: JSONObject = {}; + + @postConstruct() + protected init(): void { + this.reconcileData(); + this.preferences.onPreferencesChanged(changes => this.reconcileData(changes)); + } + + protected reconcileData(changes?: PreferenceChanges): void { + this.tree = MonacoConfigurations.parse(this.preferences.getPreferences()); + this.onDidChangeConfigurationEmitter.fire({ + affectsConfiguration: section => this.affectsConfiguration(section, changes) + }); + } + protected affectsConfiguration(section: string, changes?: PreferenceChanges): boolean { + if (!changes) { + return true; + } + for (const preferenceName in changes) { + if (section.startsWith(preferenceName) || preferenceName.startsWith(section)) { + return true; + } + } + return false; + } + + getConfiguration(section?: string, resource?: string): WorkspaceConfiguration { + const tree = section ? MonacoConfigurations.lookUp(this.tree, section) : this.tree; + // TODO take resource into the account when the multi-root is supported by preferences + return new MonacoWorkspaceConfiguration(tree); + } + +} +export namespace MonacoConfigurations { + export function parse(raw: { [section: string]: Object | undefined }): JSONObject { + const tree = {}; + for (const section of Object.keys(raw)) { + const value = raw[section]; + if (value) { + assign(tree, section, value); + } + } + return tree; + } + export function assign(data: JSONObject, section: string, value: JSONValue): void { + let node: JSONValue = data; + const parts = section.split('.'); + while (JSONExt.isObject(node) && parts.length > 1) { + const part = parts.shift()!; + node = node[part] || (node[part] = {}); + } + if (JSONExt.isObject(node) && parts.length === 1) { + node[parts[0]] = value; + } + } + export function lookUp(tree: JSONValue | undefined, section: string | undefined): JSONValue | undefined { + if (!section) { + return undefined; + } + let node: JSONValue | undefined = tree; + const parts = section.split('.'); + while (node && JSONExt.isObject(node) && parts.length > 0) { + node = node[parts.shift()!]; + } + return !parts.length ? node : undefined; + } +} + +export class MonacoWorkspaceConfiguration implements WorkspaceConfiguration { + + constructor( + protected readonly tree: JSONValue | undefined + ) { + if (tree && JSONExt.isObject(tree)) { + Object.assign(this, tree); + } + } + + readonly [key: string]: any; + + has(section: string): boolean { + return typeof MonacoConfigurations.lookUp(this.tree, section) !== 'undefined'; + } + + get(section: string, defaultValue?: T): T | undefined { + const value = MonacoConfigurations.lookUp(this.tree, section); + if (typeof value === 'undefined') { + return defaultValue; + } + const result = JSONExt.isObject(value) ? JSONExt.deepCopy(value) : value; + return result as any; + } + + toJSON(): JSONValue | undefined { + return this.tree && JSONExt.isObject(this.tree) ? JSONExt.deepCopy(this.tree) : this.tree; + } + +} diff --git a/packages/monaco/src/browser/monaco-editor-provider.ts b/packages/monaco/src/browser/monaco-editor-provider.ts index 79706e3d1fe46..e321510aa7e89 100644 --- a/packages/monaco/src/browser/monaco-editor-provider.ts +++ b/packages/monaco/src/browser/monaco-editor-provider.ts @@ -31,14 +31,18 @@ import { MonacoEditorService } from './monaco-editor-service'; import { MonacoQuickOpenService } from './monaco-quick-open-service'; import { MonacoTextModelService } from './monaco-text-model-service'; import { MonacoWorkspace } from './monaco-workspace'; +import { MonacoBulkEditService } from './monaco-bulk-edit-service'; import IEditorOverrideServices = monaco.editor.IEditorOverrideServices; @injectable() export class MonacoEditorProvider { + @inject(MonacoBulkEditService) + protected readonly bulkEditService: MonacoBulkEditService; + constructor( - @inject(MonacoEditorService) protected readonly editorService: MonacoEditorService, + @inject(MonacoEditorService) protected readonly codeEditorService: MonacoEditorService, @inject(MonacoTextModelService) protected readonly textModelService: MonacoTextModelService, @inject(MonacoContextMenuService) protected readonly contextMenuService: MonacoContextMenuService, @inject(MonacoToProtocolConverter) protected readonly m2p: MonacoToProtocolConverter, @@ -47,7 +51,7 @@ export class MonacoEditorProvider { @inject(MonacoCommandServiceFactory) protected readonly commandServiceFactory: MonacoCommandServiceFactory, @inject(EditorPreferences) protected readonly editorPreferences: EditorPreferences, @inject(MonacoQuickOpenService) protected readonly quickOpenService: MonacoQuickOpenService, - @inject(MonacoDiffNavigatorFactory) protected readonly diffNavigatorFactory: MonacoDiffNavigatorFactory, + @inject(MonacoDiffNavigatorFactory) protected readonly diffNavigatorFactory: MonacoDiffNavigatorFactory ) { } protected async getModel(uri: URI, toDispose: DisposableCollection): Promise { @@ -60,16 +64,16 @@ export class MonacoEditorProvider { await this.editorPreferences.ready; const commandService = this.commandServiceFactory(); - const { editorService, textModelService, contextMenuService } = this; - const override = { - editorService, + const { codeEditorService, textModelService, contextMenuService } = this; + const IWorkspaceEditService = this.bulkEditService; + const toDispose = new DisposableCollection(); + const editor = await this.createEditor(uri, { + codeEditorService, textModelService, contextMenuService, - commandService - }; - - const toDispose = new DisposableCollection(); - const editor = await this.createEditor(uri, override, toDispose); + commandService, + IWorkspaceEditService + }, toDispose); editor.onDispose(() => toDispose.dispose()); const standaloneCommandService = new monaco.services.StandaloneCommandService(editor.instantiationService); @@ -198,18 +202,18 @@ export class MonacoEditorProvider { referencesController._widget.hide(); referencesController._ignoreModelChangeEvent = true; - const { uri, range } = ref; + const range = monaco.Range.lift(ref.range).collapseToStart(); - referencesController._editorService.openEditor({ - resource: uri, + referencesController._editorService.openCodeEditor({ + resource: ref.uri, options: { selection: range } - }).done(openedEditor => { + }, control).done(openedEditor => { referencesController._ignoreModelChangeEvent = false; if (!openedEditor) { referencesController.closeWidget(); return; } - if (openedEditor.getControl() !== control) { + if (openedEditor !== control) { const model = referencesController._model; // to preserve the references model referencesController._model = undefined; @@ -222,7 +226,7 @@ export class MonacoEditorProvider { const modelPromise = Promise.resolve(model) as any; modelPromise.cancel = () => { }; - openedEditor.getControl()._contributions['editor.contrib.referencesController'].toggleWidget(range, modelPromise, { + openedEditor._contributions['editor.contrib.referencesController'].toggleWidget(range, modelPromise, { getMetaTitle: m => m.references.length > 1 ? ` – ${m.references.length} references` : '' }); return; diff --git a/packages/monaco/src/browser/monaco-editor-service.ts b/packages/monaco/src/browser/monaco-editor-service.ts index ff75a1a8953bc..7d0da5db8f077 100644 --- a/packages/monaco/src/browser/monaco-editor-service.ts +++ b/packages/monaco/src/browser/monaco-editor-service.ts @@ -14,40 +14,55 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { injectable, inject } from 'inversify'; +import { injectable, inject, decorate } from 'inversify'; import { MonacoToProtocolConverter } from 'monaco-languageclient'; import URI from '@theia/core/lib/common/uri'; -import { OpenerService, open, WidgetOpenMode } from '@theia/core/lib/browser'; -import { EditorWidget, EditorOpenerOptions } from '@theia/editor/lib/browser'; +import { OpenerService, open, WidgetOpenMode, ApplicationShell } from '@theia/core/lib/browser'; +import { EditorWidget, EditorOpenerOptions, EditorManager } from '@theia/editor/lib/browser'; import { MonacoEditor } from './monaco-editor'; -import IEditorService = monaco.editor.IEditorService; +import ICodeEditor = monaco.editor.ICodeEditor; +import CommonCodeEditor = monaco.editor.CommonCodeEditor; import IResourceInput = monaco.editor.IResourceInput; -import IEditorReference = monaco.editor.IEditorReference; + +decorate(injectable(), monaco.services.CodeEditorServiceImpl); @injectable() -export class MonacoEditorService implements IEditorService { +export class MonacoEditorService extends monaco.services.CodeEditorServiceImpl { + + @inject(OpenerService) + protected readonly openerService: OpenerService; + + @inject(MonacoToProtocolConverter) + protected readonly m2p: MonacoToProtocolConverter; - constructor( - @inject(OpenerService) protected readonly openerService: OpenerService, - @inject(MonacoToProtocolConverter) protected readonly m2p: MonacoToProtocolConverter - ) { } + @inject(ApplicationShell) + protected readonly shell: ApplicationShell; - openEditor(input: IResourceInput, sideBySide?: boolean | undefined): monaco.Promise { + @inject(EditorManager) + protected readonly editors: EditorManager; + + getActiveCodeEditor(): ICodeEditor | undefined { + const editor = MonacoEditor.getActive(this.editors); + return editor && editor.getControl(); + } + + openCodeEditor(input: IResourceInput, source?: ICodeEditor, sideBySide?: boolean): monaco.Promise { const uri = new URI(input.resource.toString()); - const openerOptions = this.createEditorOpenerOptions(input); + const openerOptions = this.createEditorOpenerOptions(input, source, sideBySide); return monaco.Promise.wrap(open(this.openerService, uri, openerOptions).then(widget => { if (widget instanceof EditorWidget && widget.editor instanceof MonacoEditor) { - return widget.editor; + return widget.editor.getControl(); } return undefined; })); } - protected createEditorOpenerOptions(input: IResourceInput, sideBySide?: boolean | undefined): EditorOpenerOptions { + protected createEditorOpenerOptions(input: IResourceInput, source?: ICodeEditor, sideBySide?: boolean): EditorOpenerOptions { const mode = this.getEditorOpenMode(input); const selection = input.options && this.m2p.asRange(input.options.selection); - return { mode, selection }; + const widgetOptions = this.getWidgetOptions(source, sideBySide); + return { mode, selection, widgetOptions }; } protected getEditorOpenMode(input: IResourceInput): WidgetOpenMode { const options = { @@ -60,5 +75,14 @@ export class MonacoEditorService implements IEditorService { } return options.revealIfVisible ? 'activate' : 'open'; } + protected getWidgetOptions(source?: ICodeEditor, sideBySide?: boolean): ApplicationShell.WidgetOptions | undefined { + const ref = MonacoEditor.getWidgetFor(this.editors, source); + if (!ref) { + return undefined; + } + const area = (ref && this.shell.getAreaFor(ref)) || 'main'; + const mode = ref && sideBySide ? 'split-right' : undefined; + return { area, mode, ref }; + } } diff --git a/packages/monaco/src/browser/monaco-editor.ts b/packages/monaco/src/browser/monaco-editor.ts index afc64a6b9ec75..cd0782789743d 100644 --- a/packages/monaco/src/browser/monaco-editor.ts +++ b/packages/monaco/src/browser/monaco-editor.ts @@ -42,12 +42,11 @@ import IEditorOverrideServices = monaco.editor.IEditorOverrideServices; import IStandaloneCodeEditor = monaco.editor.IStandaloneCodeEditor; import IIdentifiedSingleEditOperation = monaco.editor.IIdentifiedSingleEditOperation; import IBoxSizing = ElementExt.IBoxSizing; -import IEditorReference = monaco.editor.IEditorReference; import SuggestController = monaco.suggestController.SuggestController; import CommonFindController = monaco.findController.CommonFindController; import RenameController = monaco.rename.RenameController; -export class MonacoEditor implements TextEditor, IEditorReference { +export class MonacoEditor implements TextEditor { protected readonly toDispose = new DisposableCollection(); @@ -114,24 +113,23 @@ export class MonacoEditor implements TextEditor, IEditorReference { this.toDispose.push(codeEditor.onDidChangeCursorSelection(e => { this.onSelectionChangedEmitter.fire(this.selection); })); - this.toDispose.push(codeEditor.onDidFocusEditor(() => { + this.toDispose.push(codeEditor.onDidFocusEditorText(() => { this.onFocusChangedEmitter.fire(this.isFocused()); })); - this.toDispose.push(codeEditor.onDidBlurEditor(() => + this.toDispose.push(codeEditor.onDidBlurEditorText(() => this.onFocusChangedEmitter.fire(this.isFocused()) )); this.toDispose.push(codeEditor.onMouseDown(e => { - const { lineNumber, column } = e.target.position; - const event = { + const { position, range } = e.target; + this.onMouseDownEmitter.fire({ target: { ...e.target, mouseColumn: this.m2p.asPosition(undefined, e.target.mouseColumn).character, - range: this.m2p.asRange(e.target.range), - position: this.m2p.asPosition(lineNumber, column), + range: range && this.m2p.asRange(range), + position: position && this.m2p.asPosition(position.lineNumber, position.column) }, event: e.event.browserEvent - }; - this.onMouseDownEmitter.fire(event); + }); })); } @@ -222,7 +220,7 @@ export class MonacoEditor implements TextEditor, IEditorReference { } isFocused(): boolean { - return this.editor.isFocused(); + return this.editor.hasTextFocus(); } get onFocusChanged(): Event { @@ -450,7 +448,7 @@ export namespace MonacoEditor { return get(manager.activeEditor); } - export function get(editorWidget: EditorWidget | undefined) { + export function get(editorWidget: EditorWidget | undefined): MonacoEditor | undefined { if (editorWidget && editorWidget.editor instanceof MonacoEditor) { return editorWidget.editor; } @@ -460,4 +458,14 @@ export namespace MonacoEditor { export function findByDocument(manager: EditorManager, document: MonacoEditorModel): MonacoEditor[] { return getAll(manager).filter(editor => editor.documents.has(document)); } + + export function getWidgetFor(manager: EditorManager, control: monaco.editor.ICodeEditor | undefined): EditorWidget | undefined { + if (!control) { + return undefined; + } + return manager.all.find(widget => { + const editor = get(widget); + return !!editor && editor.getControl() === control; + }); + } } diff --git a/packages/monaco/src/browser/monaco-frontend-module.ts b/packages/monaco/src/browser/monaco-frontend-module.ts index a7fa0eefd9baa..e32047dd95d00 100644 --- a/packages/monaco/src/browser/monaco-frontend-module.ts +++ b/packages/monaco/src/browser/monaco-frontend-module.ts @@ -27,6 +27,7 @@ import { MonacoEditorCommandHandlers } from './monaco-command'; import { MonacoKeybindingContribution } from './monaco-keybinding'; import { MonacoLanguages } from './monaco-languages'; import { MonacoWorkspace } from './monaco-workspace'; +import { MonacoConfigurations } from './monaco-configurations'; import { MonacoEditorService } from './monaco-editor-service'; import { MonacoTextModelService } from './monaco-text-model-service'; import { MonacoContextMenuService } from './monaco-context-menu'; @@ -42,6 +43,7 @@ import MonacoTextmateModuleBinder from './textmate/monaco-textmate-frontend-bind import { QuickInputService } from './monaco-quick-input-service'; import { MonacoSemanticHighlightingService } from './monaco-semantic-highlighting-service'; import { SemanticHighlightingService } from '@theia/editor/lib/browser/semantic-highlight/semantic-highlighting-service'; +import { MonacoBulkEditService } from './monaco-bulk-edit-service'; decorate(injectable(), MonacoToProtocolConverter); decorate(injectable(), ProtocolToMonacoConverter); @@ -57,11 +59,13 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(ProtocolToMonacoConverter).toSelf().inSingletonScope(); bind(MonacoLanguages).toSelf().inSingletonScope(); - bind(Languages).toDynamicValue(ctx => ctx.container.get(MonacoLanguages)); + bind(Languages).toService(MonacoLanguages); + bind(MonacoConfigurations).toSelf().inSingletonScope(); bind(MonacoWorkspace).toSelf().inSingletonScope(); - bind(Workspace).toDynamicValue(ctx => ctx.container.get(MonacoWorkspace)); + bind(Workspace).toService(MonacoWorkspace); + bind(MonacoBulkEditService).toSelf().inSingletonScope(); bind(MonacoEditorService).toSelf().inSingletonScope(); bind(MonacoTextModelService).toSelf().inSingletonScope(); bind(MonacoContextMenuService).toSelf().inSingletonScope(); diff --git a/packages/monaco/src/browser/monaco-loader.ts b/packages/monaco/src/browser/monaco-loader.ts index cf99b6699028a..09578ee424a2d 100644 --- a/packages/monaco/src/browser/monaco-loader.ts +++ b/packages/monaco/src/browser/monaco-loader.ts @@ -66,14 +66,17 @@ export function loadMonaco(vsRequire: any): Promise { 'vs/editor/contrib/suggest/suggestController', 'vs/editor/contrib/find/findController', 'vs/editor/contrib/rename/rename', + 'vs/editor/contrib/snippet/snippetParser', + 'vs/editor/browser/services/codeEditorServiceImpl' ], (css: any, html: any, commands: any, actions: any, registry: any, resolver: any, resolvedKeybinding: any, keyCodes: any, editorExtensions: any, simpleServices: any, standaloneServices: any, quickOpen: any, quickOpenWidget: any, quickOpenModel: any, - filters: any, styler: any, platform: any, modes: any, cancellation: any, suggestController: any, findController: any, rename: any) => { + filters: any, styler: any, platform: any, modes: any, cancellation: any, suggestController: any, findController: any, rename: any, snippetParser: any, + codeEditorServiceImpl: any) => { const global: any = self; global.monaco.commands = commands; global.monaco.actions = actions; global.monaco.keybindings = Object.assign({}, registry, resolver, resolvedKeybinding, keyCodes); - global.monaco.services = Object.assign({}, simpleServices, standaloneServices); + global.monaco.services = Object.assign({}, simpleServices, standaloneServices, codeEditorServiceImpl); global.monaco.quickOpen = Object.assign({}, quickOpen, quickOpenWidget, quickOpenModel); global.monaco.filters = filters; global.monaco.theme = styler; @@ -84,6 +87,7 @@ export function loadMonaco(vsRequire: any): Promise { global.monaco.suggestController = suggestController; global.monaco.findController = findController; global.monaco.rename = rename; + global.monaco.snippetParser = snippetParser; resolve(); }); }); diff --git a/packages/monaco/src/browser/monaco-outline-contribution.ts b/packages/monaco/src/browser/monaco-outline-contribution.ts index 86eec703542f4..aade82c3fc4db 100644 --- a/packages/monaco/src/browser/monaco-outline-contribution.ts +++ b/packages/monaco/src/browser/monaco-outline-contribution.ts @@ -15,12 +15,13 @@ ********************************************************************************/ import { injectable, inject } from 'inversify'; -import SymbolInformation = monaco.modes.SymbolInformation; -import SymbolKind = monaco.modes.SymbolKind; +import DocumentSymbol = monaco.languages.DocumentSymbol; +import SymbolKind = monaco.languages.SymbolKind; import { FrontendApplicationContribution, FrontendApplication, TreeNode } from '@theia/core/lib/browser'; import { Range, EditorManager } from '@theia/editor/lib/browser'; import DocumentSymbolProviderRegistry = monaco.modes.DocumentSymbolProviderRegistry; import CancellationTokenSource = monaco.cancellation.CancellationTokenSource; +import CancellationToken = monaco.cancellation.CancellationToken; import { DisposableCollection } from '@theia/core'; import { OutlineViewService } from '@theia/outline-view/lib/browser/outline-view-service'; import { OutlineSymbolInformationNode } from '@theia/outline-view/lib/browser/outline-view-widget'; @@ -32,7 +33,6 @@ import debounce = require('lodash.debounce'); @injectable() export class MonacoOutlineContribution implements FrontendApplicationContribution { - protected cancellationSource: CancellationTokenSource; protected readonly toDisposeOnClose = new DisposableCollection(); protected readonly toDisposeOnEditor = new DisposableCollection(); @@ -56,8 +56,7 @@ export class MonacoOutlineContribution implements FrontendApplicationContributio }); this.outlineViewService.onDidSelect(async node => { if (MonacoOutlineSymbolInformationNode.is(node) && node.parent) { - const uri = new URI(node.uri); - await this.editorManager.open(uri, { + await this.editorManager.open(node.uri, { mode: 'reveal', selection: node.range }); @@ -65,7 +64,7 @@ export class MonacoOutlineContribution implements FrontendApplicationContributio }); this.outlineViewService.onDidOpen(node => { if (MonacoOutlineSymbolInformationNode.is(node)) { - this.editorManager.open(new URI(node.uri), { + this.editorManager.open(node.uri, { selection: { start: node.range.start } @@ -88,86 +87,87 @@ export class MonacoOutlineContribution implements FrontendApplicationContributio this.updateOutline(); } + protected tokenSource = new CancellationTokenSource(); protected async updateOutline(): Promise { - const editor = this.editorManager.currentEditor; - if (editor) { - const model = MonacoEditor.get(editor)!.getControl().getModel(); - this.publish(await this.computeSymbolInformations(model)); - } else { - this.publish([]); + this.tokenSource.cancel(); + this.tokenSource = new CancellationTokenSource(); + const token = this.tokenSource.token; + + const editor = MonacoEditor.get(this.editorManager.currentEditor); + const model = editor && editor.getControl().getModel(); + const roots = model && await this.createRoots(model, token); + if (token.isCancellationRequested) { + return; } + this.outlineViewService.publish(roots || []); } - protected async computeSymbolInformations(model: monaco.editor.IModel): Promise { - const entries: SymbolInformation[] = []; - const documentSymbolProviders = await DocumentSymbolProviderRegistry.all(model); - - if (this.cancellationSource) { - this.cancellationSource.cancel(); + protected async createRoots(model: monaco.editor.IModel, token: CancellationToken): Promise { + const providers = await DocumentSymbolProviderRegistry.all(model); + if (token.isCancellationRequested) { + return []; } - this.cancellationSource = new CancellationTokenSource(); - const token = this.cancellationSource.token; - for (const documentSymbolProvider of documentSymbolProviders) { + const roots = []; + const uri = new URI(model.uri.toString()); + for (const provider of providers) { try { - const symbolInformation = await documentSymbolProvider.provideDocumentSymbols(model, token); + const symbols = await provider.provideDocumentSymbols(model, token); if (token.isCancellationRequested) { return []; } - if (Array.isArray(symbolInformation)) { - entries.push(...symbolInformation); - } + const nodes = this.createNodes(uri, symbols); + roots.push(...nodes); } catch { - // happens if `provideDocumentSymbols` promise is rejected. - return []; + /* collect symbols from other providers */ } } - - return entries; + return roots; } - protected publish(symbolInformations: SymbolInformation[]): void { + protected createNodes(uri: URI, symbols: DocumentSymbol[]): MonacoOutlineSymbolInformationNode[] { let rangeBased = false; const ids = new Map(); - const nodesByName = symbolInformations.sort(this.orderByPosition).reduce((result, symbol) => { - rangeBased = rangeBased || symbol.location.range.startLineNumber !== symbol.location.range.endLineNumber; - const values = result.get(symbol.name) || []; - const node = this.createNode(symbol, ids); - values.push({ symbol, node }); - result.set(symbol.name, values); + const roots: MonacoOutlineSymbolInformationNode[] = []; + const nodesByName = symbols.sort(this.orderByPosition).reduce((result, symbol) => { + const node = this.createNode(uri, symbol, ids); + if (symbol.children) { + MonacoOutlineSymbolInformationNode.insert(roots, node); + } else { + rangeBased = rangeBased || symbol.range.startLineNumber !== symbol.range.endLineNumber; + const values = result.get(symbol.name) || []; + values.push({ symbol, node }); + result.set(symbol.name, values); + } return result; }, new Map()); - const roots = []; for (const nodes of nodesByName.values()) { for (const { node, symbol } of nodes) { if (!symbol.containerName) { - roots.push(node); + MonacoOutlineSymbolInformationNode.insert(roots, node); } else { const possibleParents = nodesByName.get(symbol.containerName); if (possibleParents) { const parent = possibleParents.find(possibleParent => this.parentContains(symbol, possibleParent.symbol, rangeBased)); if (parent) { - const parentNode = parent.node; - Object.assign(node, { parent: parentNode }); - (parentNode.children as TreeNode[]).push(node); + node.parent = parent.node; + MonacoOutlineSymbolInformationNode.insert(parent.node.children, node); } } } } } - if (roots.length === 0) { + if (!roots.length) { const nodes = nodesByName.values().next().value; if (nodes && !nodes[0].node.parent) { - this.outlineViewService.publish([nodes[0].node]); - } else { - this.outlineViewService.publish([]); + return [nodes[0].node]; } - } else { - this.outlineViewService.publish(roots); + return []; } + return roots; } - protected parentContains(symbol: SymbolInformation, parent: SymbolInformation, rangeBased: boolean): boolean { + protected parentContains(symbol: DocumentSymbol, parent: DocumentSymbol, rangeBased: boolean): boolean { const symbolRange = this.getRangeFromSymbolInformation(symbol); const nodeRange = this.getRangeFromSymbolInformation(parent); const sameStartLine = symbolRange.start.line === nodeRange.start.line; @@ -180,32 +180,39 @@ export class MonacoOutlineContribution implements FrontendApplicationContributio (sameEndLine && endColSmallerOrEqual || endLineSmaller)) || !rangeBased); } - protected getRangeFromSymbolInformation(symbolInformation: SymbolInformation): Range { + protected getRangeFromSymbolInformation(symbolInformation: DocumentSymbol): Range { return { end: { - character: symbolInformation.location.range.endColumn - 1, - line: symbolInformation.location.range.endLineNumber - 1 + character: symbolInformation.range.endColumn - 1, + line: symbolInformation.range.endLineNumber - 1 }, start: { - character: symbolInformation.location.range.startColumn - 1, - line: symbolInformation.location.range.startLineNumber - 1 + character: symbolInformation.range.startColumn - 1, + line: symbolInformation.range.startLineNumber - 1 } }; } - protected createNode(symbol: SymbolInformation, ids: Map): MonacoOutlineSymbolInformationNode { + protected createNode(uri: URI, symbol: DocumentSymbol, ids: Map, parent?: MonacoOutlineSymbolInformationNode): MonacoOutlineSymbolInformationNode { const id = this.createId(symbol.name, ids); - return { - children: [], + const children: MonacoOutlineSymbolInformationNode[] = []; + const node: MonacoOutlineSymbolInformationNode = { + children, id, iconClass: SymbolKind[symbol.kind].toString().toLowerCase(), name: symbol.name, - parent: undefined, - uri: symbol.location.uri.toString(), + parent, + uri, range: this.getRangeFromSymbolInformation(symbol), selected: false, expanded: this.shouldExpand(symbol) }; + if (symbol.children) { + for (const child of symbol.children) { + MonacoOutlineSymbolInformationNode.insert(children, this.createNode(uri, child, ids, node)); + } + } + return node; } protected createId(name: string, ids: Map): string { @@ -215,7 +222,7 @@ export class MonacoOutlineContribution implements FrontendApplicationContributio return name + '_' + index; } - protected shouldExpand(symbol: SymbolInformation): boolean { + protected shouldExpand(symbol: DocumentSymbol): boolean { return [SymbolKind.Class, SymbolKind.Enum, SymbolKind.File, SymbolKind.Interface, SymbolKind.Module, @@ -223,37 +230,62 @@ export class MonacoOutlineContribution implements FrontendApplicationContributio SymbolKind.Package, SymbolKind.Struct].indexOf(symbol.kind) !== -1; } - protected orderByPosition(symbol1: SymbolInformation, symbol2: SymbolInformation): number { - const startLineComparison = symbol1.location.range.startLineNumber - symbol2.location.range.startLineNumber; + protected orderByPosition(symbol: DocumentSymbol, symbol2: DocumentSymbol): number { + const startLineComparison = symbol.range.startLineNumber - symbol2.range.startLineNumber; if (startLineComparison !== 0) { return startLineComparison; } - const startOffsetComparison = symbol1.location.range.startColumn - symbol2.location.range.startColumn; + const startOffsetComparison = symbol.range.startColumn - symbol2.range.startColumn; if (startOffsetComparison !== 0) { return startOffsetComparison; } - const endLineComparison = symbol1.location.range.endLineNumber - symbol2.location.range.endLineNumber; + const endLineComparison = symbol.range.endLineNumber - symbol2.range.endLineNumber; if (endLineComparison !== 0) { return endLineComparison; } - return symbol1.location.range.endColumn - symbol2.location.range.endColumn; + return symbol.range.endColumn - symbol2.range.endColumn; } } export namespace MonacoOutlineContribution { export interface NodeAndSymbol { node: MonacoOutlineSymbolInformationNode; - symbol: SymbolInformation + symbol: DocumentSymbol } } export interface MonacoOutlineSymbolInformationNode extends OutlineSymbolInformationNode { - uri: string; + uri: URI; range: Range; + parent: MonacoOutlineSymbolInformationNode | undefined; + children: MonacoOutlineSymbolInformationNode[]; } export namespace MonacoOutlineSymbolInformationNode { export function is(node: TreeNode): node is MonacoOutlineSymbolInformationNode { return OutlineSymbolInformationNode.is(node) && 'uri' in node && 'range' in node; } + export function insert(nodes: MonacoOutlineSymbolInformationNode[], node: MonacoOutlineSymbolInformationNode): void { + const index = nodes.findIndex(current => compare(node, current) < 0); + if (index === -1) { + nodes.push(node); + } else { + nodes.splice(index, 0, node); + } + } + export function compare(node: MonacoOutlineSymbolInformationNode, node2: MonacoOutlineSymbolInformationNode): number { + const startLineComparison = node.range.start.line - node2.range.start.line; + if (startLineComparison !== 0) { + return startLineComparison; + } + const startColumnComparison = node.range.start.character - node2.range.start.character; + if (startColumnComparison !== 0) { + return startColumnComparison; + } + const endLineComparison = node2.range.end.line - node.range.end.line; + if (endLineComparison !== 0) { + return endLineComparison; + } + return node2.range.end.character - node.range.end.character; + } } diff --git a/packages/monaco/src/browser/monaco-quick-input-service.ts b/packages/monaco/src/browser/monaco-quick-input-service.ts index 3b35f29826388..4e088ed05d7ce 100644 --- a/packages/monaco/src/browser/monaco-quick-input-service.ts +++ b/packages/monaco/src/browser/monaco-quick-input-service.ts @@ -121,7 +121,7 @@ export class QuickInputService { if (isValid) { this.quickOpenService.clearInputDecoration(); } else { - this.quickOpenService.showInputDecoration(monaco.Severity.Error); + this.quickOpenService.showInputDecoration(monaco.MarkerSeverity.Error); } } } diff --git a/packages/monaco/src/browser/monaco-quick-open-service.ts b/packages/monaco/src/browser/monaco-quick-open-service.ts index b71bbf2e1b7d3..fb6622cfe13f8 100644 --- a/packages/monaco/src/browser/monaco-quick-open-service.ts +++ b/packages/monaco/src/browser/monaco-quick-open-service.ts @@ -58,22 +58,39 @@ export class MonacoQuickOpenService extends QuickOpenService { internalOpen(opts: MonacoQuickOpenControllerOpts): void { this.opts = opts; this.previousActiveElement = window.document.activeElement; + this.widget.show(this.opts.prefix || ''); + this.setPlaceHolder(opts.inputAriaLabel); + this.setPassword(opts.password ? true : false); + } + + setPlaceHolder(placeHolder: string): void { const widget = this.widget; - widget.show(this.opts.prefix || ''); - widget.setPlaceHolder(opts.inputAriaLabel); - if (opts.password) { - widget.setPassword(opts.password); - } else { - widget.setPassword(false); + if (widget.inputBox) { + widget.inputBox.setPlaceHolder(placeHolder); } } - clearInputDecoration(): void { - this.widget.clearInputDecoration(); + setPassword(isPassword: boolean): void { + const widget = this.widget; + if (widget.inputBox) { + widget.inputBox.inputElement.type = isPassword ? 'password' : 'text'; + } + } + + showInputDecoration(decoration: monaco.MarkerSeverity): void { + const widget = this.widget; + if (widget.inputBox) { + const type = decoration === monaco.MarkerSeverity.Info ? 1 : + decoration === monaco.MarkerSeverity.Warning ? 2 : 3; + widget.inputBox.showMessage({ type, content: '' }); + } } - showInputDecoration(severity: monaco.Severity): void { - this.widget.showInputDecoration(severity); + clearInputDecoration(): void { + const widget = this.widget; + if (widget.inputBox) { + widget.inputBox.hideMessage(); + } } protected get widget(): monaco.quickOpen.QuickOpenWidget { @@ -184,6 +201,9 @@ export class MonacoQuickOpenControllerOptsImpl implements MonacoQuickOpenControl } protected createEntry(item: QuickOpenItem, lookFor: string): monaco.quickOpen.QuickOpenEntry | undefined { + if (this.options.skipPrefix) { + lookFor = lookFor.substr(this.options.skipPrefix); + } const labelHighlights = this.options.fuzzyMatchLabel ? this.matchesFuzzy(lookFor, item.getLabel()) : item.getLabelHighlights(); const descriptionHighlights = this.options.fuzzyMatchDescription ? this.matchesFuzzy(lookFor, item.getDescription()) : item.getDescriptionHighlights(); const detailHighlights = this.options.fuzzyMatchDetail ? this.matchesFuzzy(lookFor, item.getDetail()) : item.getDetailHighlights(); diff --git a/packages/monaco/src/browser/monaco-workspace.ts b/packages/monaco/src/browser/monaco-workspace.ts index ffce9cf485f65..f8559447d3f59 100644 --- a/packages/monaco/src/browser/monaco-workspace.ts +++ b/packages/monaco/src/browser/monaco-workspace.ts @@ -29,6 +29,7 @@ import { Emitter, TextDocumentWillSaveEvent, TextEdit } from '@theia/languages/l import { MonacoTextModelService } from './monaco-text-model-service'; import { WillSaveMonacoModelEvent, MonacoEditorModel, MonacoModelContentChangedEvent } from './monaco-editor-model'; import { MonacoEditor } from './monaco-editor'; +import { MonacoConfigurations } from './monaco-configurations'; export interface MonacoDidChangeTextDocumentParams extends lang.DidChangeTextDocumentParams { readonly textDocument: MonacoEditorModel; @@ -48,12 +49,6 @@ export class MonacoWorkspace implements lang.Workspace { } }; - readonly synchronization = { - didSave: true, - willSave: true, - willSaveWaitUntil: true - }; - protected resolveReady: () => void; readonly ready = new Promise(resolve => { this.resolveReady = resolve; @@ -95,6 +90,9 @@ export class MonacoWorkspace implements lang.Workspace { @inject(EditorManager) protected readonly editorManager: EditorManager; + @inject(MonacoConfigurations) + readonly configurations: MonacoConfigurations; + @postConstruct() protected init(): void { this.workspaceService.roots.then(roots => { @@ -229,6 +227,13 @@ export class MonacoWorkspace implements lang.Workspace { async applyEdit(changes: lang.WorkspaceEdit): Promise { const workspaceEdit = this.p2m.asWorkspaceEdit(changes); + await this.applyBulkEdit(workspaceEdit); + return true; + } + + async applyBulkEdit(workspaceEdit: monaco.languages.WorkspaceEdit): monaco.Promise { + let totalEdits = 0; + let totalFiles = 0; const uri2Edits = this.groupEdits(workspaceEdit); for (const uri of uri2Edits.keys()) { const editorWidget = await this.editorManager.open(new URI(uri)); @@ -248,9 +253,21 @@ export class MonacoWorkspace implements lang.Workspace { model.pushEditOperations(currentSelections, editOperations, (undoEdits: monaco.editor.IIdentifiedSingleEditOperation[]) => currentSelections); // push again to make this change an undoable operation model.pushStackElement(); + totalFiles += 1; + totalEdits += editOperations.length; } } - return true; + const ariaSummary = this.getAriaSummary(totalEdits, totalFiles); + return { ariaSummary }; + } + protected getAriaSummary(totalEdits: number, totalFiles: number): string { + if (totalEdits === 0) { + return 'Made no edits'; + } + if (totalEdits > 1 && totalFiles > 1) { + return `Made ${totalEdits} text edits in ${totalFiles} files`; + } + return `Made ${totalEdits} text edits in one file`; } protected groupEdits(workspaceEdit: monaco.languages.WorkspaceEdit) { diff --git a/packages/monaco/src/browser/textmate/monaco-builtin-theme-provider.ts b/packages/monaco/src/browser/textmate/monaco-builtin-theme-provider.ts deleted file mode 100644 index 737e38e884312..0000000000000 --- a/packages/monaco/src/browser/textmate/monaco-builtin-theme-provider.ts +++ /dev/null @@ -1,105 +0,0 @@ -/******************************************************************************** - * Copyright (C) 2018 Ericsson 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 - ********************************************************************************/ - -import { MonacoTheme } from './monaco-theme-types'; - -export class BuiltinMonacoThemeProvider { - - protected static readonly rawThemes: { [file: string]: object } = { - './dark_default.json': require('../../../data/monaco-themes/vscode/dark_defaults.json'), - './dark_vs.json': require('../../../data/monaco-themes/vscode/dark_vs.json'), - './dark_plus.json': require('../../../data/monaco-themes/vscode/dark_plus.json'), - - './light_default.json': require('../../../data/monaco-themes/vscode/light_defaults.json'), - './light_vs.json': require('../../../data/monaco-themes/vscode/light_vs.json'), - './light_plus.json': require('../../../data/monaco-themes/vscode/light_plus.json'), - }; - - protected static readonly nameMap: { [name: string]: string } = { - 'light-plus': 'light_plus', - 'dark-plus': 'dark_plus', - }; - - protected static readonly baseMap: { [name: string]: monaco.editor.BuiltinTheme } = { - 'light-plus': 'vs', - }; - - static compileMonacoThemes() { - [ - 'light-plus', 'dark-plus', - ].forEach(name => { - const rawName = this.nameMap[name] || name; - const theme = this.convertVscodeToMonaco( - this.rawThemes[`./${rawName}.json`], - { - name, - base: this.baseMap[name] || 'vs-dark', - inherit: true, - rules: [], - colors: {}, - } - ); - - monaco.editor.defineTheme(theme.name, theme); - }); - } - - // tslint:disable-next-line:no-any - protected static convertVscodeToMonaco(vscodeTheme: any, monacoTheme: MonacoTheme): MonacoTheme { - - // Recursion in order to follow the theme dependencies that vscode has... - if (typeof vscodeTheme.include !== 'undefined') { - const subTheme = this.rawThemes[vscodeTheme.include]; - if (subTheme) { - this.convertVscodeToMonaco(subTheme, monacoTheme); - } - } - - Object.assign(monacoTheme.colors, vscodeTheme.colors); - - if (typeof vscodeTheme.tokenColors !== 'undefined') { - for (const tokenColor of vscodeTheme.tokenColors) { - - if (typeof tokenColor.scope === 'undefined') { - tokenColor.scope = ['']; - } else if (typeof tokenColor.scope === 'string') { - // tokenColor.scope = tokenColor.scope.split(',').map((scope: string) => scope.trim()); // ? - tokenColor.scope = [tokenColor.scope]; - } - - for (const scope of tokenColor.scope) { - - // Converting numbers into a format that monaco understands - const settings = Object.keys(tokenColor.settings).reduce((previous: { [key: string]: string }, current) => { - let value: string = tokenColor.settings[current]; - if (typeof value === typeof '') { - value = value.replace(/^\#/, '').slice(0, 6); - } - previous[current] = value; - return previous; - }, {}); - - monacoTheme.rules.push({ - ...settings, token: scope - }); - } - } - } - - return monacoTheme; - } - -} diff --git a/packages/monaco/src/browser/textmate/monaco-textmate-builtin-theme-provider.ts b/packages/monaco/src/browser/textmate/monaco-textmate-builtin-theme-provider.ts index c3f5c67fbc6eb..f8278b3ea4840 100644 --- a/packages/monaco/src/browser/textmate/monaco-textmate-builtin-theme-provider.ts +++ b/packages/monaco/src/browser/textmate/monaco-textmate-builtin-theme-provider.ts @@ -15,17 +15,18 @@ ********************************************************************************/ import { BuiltinThemeProvider } from '@theia/core/lib/browser/theming'; +import { MonacoThemeRegistry } from './monaco-theme-registry'; export class BuiltinTextmateThemeProvider { static readonly theiaTextmateThemes = [ { ...BuiltinThemeProvider.darkTheme, - editorTheme: 'dark-plus', + editorTheme: MonacoThemeRegistry.DARK_DEFAULT_THEME, }, { ...BuiltinThemeProvider.lightTheme, - editorTheme: 'light-plus', + editorTheme: MonacoThemeRegistry.LIGHT_DEFAULT_THEME, }, ]; diff --git a/packages/monaco/src/browser/textmate/monaco-textmate-frontend-bindings.ts b/packages/monaco/src/browser/textmate/monaco-textmate-frontend-bindings.ts index 60e70d7dad7e2..afa4f269dbcb3 100644 --- a/packages/monaco/src/browser/textmate/monaco-textmate-frontend-bindings.ts +++ b/packages/monaco/src/browser/textmate/monaco-textmate-frontend-bindings.ts @@ -19,10 +19,10 @@ import { FrontendApplicationContribution, isBasicWasmSupported } from '@theia/co import { bindContributionProvider } from '@theia/core'; import { ThemeService } from '@theia/core/lib/browser/theming'; import { BuiltinTextmateThemeProvider } from './monaco-textmate-builtin-theme-provider'; -import { BuiltinMonacoThemeProvider } from './monaco-builtin-theme-provider'; import { TextmateRegistry } from './textmate-registry'; import { LanguageGrammarDefinitionContribution } from './textmate-contribution'; import { MonacoTextmateService, OnigasmPromise } from './monaco-textmate-service'; +import { MonacoThemeRegistry } from './monaco-theme-registry'; import { loadWASM } from 'onigasm'; export function fetchOnigasm(): Promise { @@ -54,8 +54,8 @@ export default (bind: interfaces.Bind, unbind: interfaces.Unbind, isBound: inter bind(FrontendApplicationContribution).toService(MonacoTextmateService); bindContributionProvider(bind, LanguageGrammarDefinitionContribution); bind(TextmateRegistry).toSelf().inSingletonScope(); + bind(MonacoThemeRegistry).toDynamicValue(() => MonacoThemeRegistry.SINGLETON).inSingletonScope(); const themeService = ThemeService.get(); - BuiltinMonacoThemeProvider.compileMonacoThemes(); themeService.register(...BuiltinTextmateThemeProvider.theiaTextmateThemes); }; diff --git a/packages/monaco/src/browser/textmate/monaco-textmate-service.ts b/packages/monaco/src/browser/textmate/monaco-textmate-service.ts index b450119bc2647..7b4539fffbbd1 100644 --- a/packages/monaco/src/browser/textmate/monaco-textmate-service.ts +++ b/packages/monaco/src/browser/textmate/monaco-textmate-service.ts @@ -18,10 +18,12 @@ import { injectable, inject, named } from 'inversify'; import { Registry } from 'monaco-textmate'; import { ILogger, DisposableCollection, ContributionProvider } from '@theia/core'; import { FrontendApplicationContribution, isBasicWasmSupported } from '@theia/core/lib/browser'; +import { ThemeService } from '@theia/core/lib/browser/theming'; import { MonacoTextModelService } from '../monaco-text-model-service'; import { LanguageGrammarDefinitionContribution, getEncodedLanguageId } from './textmate-contribution'; -import { createTextmateTokenizer } from './textmate-tokenizer'; +import { createTextmateTokenizer, TokenizerOption } from './textmate-tokenizer'; import { TextmateRegistry } from './textmate-registry'; +import { MonacoThemeRegistry } from './monaco-theme-registry'; export const OnigasmPromise = Symbol('OnigasmPromise'); export type OnigasmPromise = Promise; @@ -49,6 +51,12 @@ export class MonacoTextmateService implements FrontendApplicationContribution { @inject(OnigasmPromise) protected readonly onigasmPromise: OnigasmPromise; + @inject(ThemeService) + protected readonly themeService: ThemeService; + + @inject(MonacoThemeRegistry) + protected readonly monacoThemeRegistry: MonacoThemeRegistry; + initialize() { if (!isBasicWasmSupported) { console.log('Textmate support deactivated because WebAssembly is not detected.'); @@ -56,7 +64,11 @@ export class MonacoTextmateService implements FrontendApplicationContribution { } for (const grammarProvider of this.grammarProviders.getContributions()) { - grammarProvider.registerTextmateLanguage(this.textmateRegistry); + try { + grammarProvider.registerTextmateLanguage(this.textmateRegistry); + } catch (err) { + console.error(err); + } } this.grammarRegistry = new Registry({ @@ -69,9 +81,17 @@ export class MonacoTextmateService implements FrontendApplicationContribution { format: 'json', content: '{}' }; - } + }, + theme: this.monacoThemeRegistry.getTheme(MonacoThemeRegistry.DARK_DEFAULT_THEME) }); + this.toDispose.push(this.themeService.onThemeChange(themeChange => { + const theme = this.monacoThemeRegistry.getTheme(themeChange.newTheme.editorTheme || MonacoThemeRegistry.DARK_DEFAULT_THEME); + if (theme) { + this.grammarRegistry.setTheme(theme); + } + })); + this.toDispose.push(this.monacoModelService.onDidCreate(model => { if (!this.activatedLanguages.has(model.languageId)) { this.activatedLanguages.add(model.languageId); @@ -95,11 +115,11 @@ export class MonacoTextmateService implements FrontendApplicationContribution { await this.onigasmPromise; try { - monaco.languages.setTokensProvider(languageId, createTextmateTokenizer( - await this.grammarRegistry.loadGrammarWithConfiguration(scopeName, initialLanguage, configuration) - )); - } catch (err) { - this.logger.warn('No grammar for this language id', languageId); + const grammar = await this.grammarRegistry.loadGrammarWithConfiguration(scopeName, initialLanguage, configuration); + const options = configuration.tokenizerOption ? configuration.tokenizerOption : TokenizerOption.DEFAULT; + monaco.languages.setTokensProvider(languageId, createTextmateTokenizer(grammar, options)); + } catch (error) { + this.logger.warn('No grammar for this language id', languageId, error); } } diff --git a/packages/monaco/src/browser/textmate/monaco-theme-registry.ts b/packages/monaco/src/browser/textmate/monaco-theme-registry.ts new file mode 100644 index 0000000000000..7eeb2e2a62f01 --- /dev/null +++ b/packages/monaco/src/browser/textmate/monaco-theme-registry.ts @@ -0,0 +1,124 @@ + +/******************************************************************************** + * Copyright (C) 2018 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 + ********************************************************************************/ + +import { IRawTheme, Registry } from 'monaco-textmate'; + +export interface ThemeMix extends IRawTheme, monaco.editor.IStandaloneThemeData {} + +export class MonacoThemeRegistry { + + protected themes = new Map(); + + public getTheme(name: string): IRawTheme| undefined { + return this.themes.get(name); + } + + /** + * Register VS Code compatible themes + */ + public register(json: any, includes?: {[includePath: string]: any}, givenName?: string, monacoBase?: monaco.editor.BuiltinTheme): ThemeMix { + const name = givenName || json.name!; + const result: ThemeMix = { + name, + base: monacoBase || 'vs', + inherit: true, + colors: {}, + rules: [], + settings: [] + }; + if (this.themes.has(name)) { + return this.themes.get(name)!; + } + this.themes.set(name, result); + if (typeof json.include !== 'undefined') { + if (!includes || !includes[json.include]) { + console.error(`Couldn't resolve includes theme ${json.include}.`); + } else { + const parentTheme = this.register(includes[json.include], includes); + Object.assign(result.colors, parentTheme.colors); + result.rules.push(...parentTheme.rules); + result.settings.push(...parentTheme.settings); + } + } + if (json.tokenColors) { + result.settings.push(...json.tokenColors); + } + if (json.colors) { + Object.assign(result.colors, json.colors); + result.encodedTokensColors = Object.keys(result.colors).map(key => result.colors[key]); + } + if (monacoBase && givenName) { + for (const setting of result.settings) { + this.transform(setting, rule => result.rules.push(rule)); + } + const reg = new Registry(); + reg.setTheme(result); + result.encodedTokensColors = reg.getColorMap(); + // index 0 has to be set to null as it is 'undefined' by default, but monaco code expects it to be null + // tslint:disable-next-line:no-null-keyword + result.encodedTokensColors[0] = null!; + // index 1 and 2 are the default colors + if (result.colors && result.colors['editor.foreground']) { + result.encodedTokensColors[1] = result.colors['editor.foreground']; + } + if (result.colors && result.colors['editor.background']) { + result.encodedTokensColors[2] = result.colors['editor.background']; + } + monaco.editor.defineTheme(givenName, result); + } + return result; + } + + protected transform(tokenColor: any, acceptor: (rule: monaco.editor.ITokenThemeRule) => void) { + if (typeof tokenColor.scope === 'undefined') { + tokenColor.scope = ['']; + } else if (typeof tokenColor.scope === 'string') { + // tokenColor.scope = tokenColor.scope.split(',').map((scope: string) => scope.trim()); // ? + tokenColor.scope = [tokenColor.scope]; + } + + for (const scope of tokenColor.scope) { + + // Converting numbers into a format that monaco understands + const settings = Object.keys(tokenColor.settings).reduce((previous: { [key: string]: string }, current) => { + let value: string = tokenColor.settings[current]; + if (typeof value === typeof '') { + value = value.replace(/^\#/, '').slice(0, 6); + } + previous[current] = value; + return previous; + }, {}); + + acceptor({ + ...settings, token: scope + }); + } + } +} + +export namespace MonacoThemeRegistry { + export const SINGLETON = new MonacoThemeRegistry(); + + export const DARK_DEFAULT_THEME: string = SINGLETON.register(require('../../../data/monaco-themes/vscode/dark_plus.json'), { + './dark_defaults.json': require('../../../data/monaco-themes/vscode/dark_defaults.json'), + './dark_vs.json': require('../../../data/monaco-themes/vscode/dark_vs.json') + }, 'dark-plus', 'vs-dark').name!; + export const LIGHT_DEFAULT_THEME: string = SINGLETON.register(require('../../../data/monaco-themes/vscode/light_plus.json'), { + './light_defaults.json': require('../../../data/monaco-themes/vscode/light_defaults.json'), + './light_vs.json': require('../../../data/monaco-themes/vscode/light_vs.json') + }, 'light-plus', 'vs').name!; +} diff --git a/packages/monaco/src/browser/textmate/textmate-contribution.ts b/packages/monaco/src/browser/textmate/textmate-contribution.ts index 86a7e997e9f0a..63898ccdd53e6 100644 --- a/packages/monaco/src/browser/textmate/textmate-contribution.ts +++ b/packages/monaco/src/browser/textmate/textmate-contribution.ts @@ -24,6 +24,5 @@ export interface LanguageGrammarDefinitionContribution { registerTextmateLanguage(registry: TextmateRegistry): void; } export function getEncodedLanguageId(languageId: string): number { - const identifier = monaco.services.StaticServices.modeService.get().getLanguageIdentifier(languageId); - return identifier && identifier.id; + return monaco.languages.getEncodedLanguageId(languageId); } diff --git a/packages/monaco/src/browser/textmate/textmate-registry.ts b/packages/monaco/src/browser/textmate/textmate-registry.ts index 6be635a61c0be..3851c46859039 100644 --- a/packages/monaco/src/browser/textmate/textmate-registry.ts +++ b/packages/monaco/src/browser/textmate/textmate-registry.ts @@ -16,16 +16,27 @@ import { injectable } from 'inversify'; import { RegistryOptions, IGrammarConfiguration } from 'monaco-textmate'; +import { TokenizerOption } from './textmate-tokenizer'; + +export interface TextmateGrammarConfiguration extends IGrammarConfiguration { + + /** + * Optional options to further refine the tokenization of the grammar. + */ + readonly tokenizerOption?: TokenizerOption; + +} @injectable() export class TextmateRegistry { + readonly scopeToProvider = new Map(); - readonly languageToConfig = new Map(); + readonly languageToConfig = new Map(); readonly languageIdToScope = new Map(); - registerTextMateGrammarScope(scope: string, provider: RegistryOptions): void { + registerTextmateGrammarScope(scope: string, provider: RegistryOptions): void { if (this.scopeToProvider.has(scope)) { - console.warn(new Error(`a registered grammar provider for '${scope}' scope is overriden`)); + console.warn(new Error(`a registered grammar provider for '${scope}' scope is overridden`)); } this.scopeToProvider.set(scope, provider); } @@ -46,14 +57,14 @@ export class TextmateRegistry { return this.languageIdToScope.get(languageId); } - registerGrammarConfiguration(languageId: string, config: IGrammarConfiguration): void { + registerGrammarConfiguration(languageId: string, config: TextmateGrammarConfiguration): void { if (this.languageToConfig.has(languageId)) { - console.warn(new Error(`a registered grammar configuration for '${languageId}' language is overriden`)); + console.warn(new Error(`a registered grammar configuration for '${languageId}' language is overridden`)); } this.languageToConfig.set(languageId, config); } - getGrammarConfiguration(languageId: string): IGrammarConfiguration { + getGrammarConfiguration(languageId: string): TextmateGrammarConfiguration { return this.languageToConfig.get(languageId) || {}; } } diff --git a/packages/monaco/src/browser/textmate/textmate-snippet-completion-provider.ts b/packages/monaco/src/browser/textmate/textmate-snippet-completion-provider.ts new file mode 100644 index 0000000000000..64e8987bd2b3e --- /dev/null +++ b/packages/monaco/src/browser/textmate/textmate-snippet-completion-provider.ts @@ -0,0 +1,62 @@ +/******************************************************************************** + * Copyright (C) 2018 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 + ********************************************************************************/ + +export class TextmateSnippetCompletionProvider implements monaco.languages.CompletionItemProvider { + + private items: monaco.languages.CompletionItem[]; + + constructor(protected config: TextmateSnippets, protected mdLanguage: string = '') { + this.items = []; + for (const name of Object.keys(config)) { + const textmateSnippet = config[name]; + const insertText = Array.isArray(textmateSnippet.body) ? textmateSnippet.body.join('\n') : textmateSnippet.body; + this.items.push({ + label: textmateSnippet.prefix, + detail: textmateSnippet.description, + kind: monaco.languages.CompletionItemKind.Snippet, + // templates shall get a very low priority + sortText : 'ZZZ' + textmateSnippet.prefix, + documentation: { + value: '```' + this.mdLanguage + '\n' + this.replaceVariables(insertText) + '```' + }, + insertText: { + value: insertText + } + }); + } + } + + protected replaceVariables(textmateSnippet: string): string { + return new monaco.snippetParser.SnippetParser().parse(textmateSnippet).toString(); + } + + provideCompletionItems(document: monaco.editor.ITextModel, + position: monaco.Position, + token: monaco.CancellationToken, + context: monaco.languages.CompletionContext): monaco.languages.CompletionItem[] { + return this.items; + } +} + +export interface TextmateSnippets { + [name: string]: TextmateSnippet; +} + +export interface TextmateSnippet { + readonly prefix: string, + readonly body: string[], + readonly description: string +} diff --git a/packages/monaco/src/browser/textmate/textmate-tokenizer.ts b/packages/monaco/src/browser/textmate/textmate-tokenizer.ts index a17bd5adc35c5..b1c2a4fb74457 100644 --- a/packages/monaco/src/browser/textmate/textmate-tokenizer.ts +++ b/packages/monaco/src/browser/textmate/textmate-tokenizer.ts @@ -16,60 +16,63 @@ import { INITIAL, StackElement, IGrammar } from 'monaco-textmate'; -export class State implements monaco.languages.IState { +export class TokenizerState implements monaco.languages.IState { constructor( - public ruleStack: StackElement + public readonly ruleStack: StackElement ) { } clone(): monaco.languages.IState { - return new State(this.ruleStack); + return new TokenizerState(this.ruleStack); } equals(other: monaco.languages.IState): boolean { - return other && - (other instanceof State) && - (other === this || other.ruleStack === this.ruleStack) - ; + return other instanceof TokenizerState && (other === this || other.ruleStack === this.ruleStack); } } -export function createTextmateTokenizer(grammar: IGrammar): monaco.languages.TokensProvider { +/** + * Options for the TextMate tokenizer. + */ +export interface TokenizerOption { + + /** + * Maximum line length that will be handled by the TextMate tokenizer. If the length of the actual line exceeds this + * limit, the tokenizer terminates and the tokenization of any subsequent lines might be broken. + * + * If the `lineLimit` is not defined, it means, there are no line length limits. Otherwise, it must be a positive + * integer or an error will be thrown. + */ + readonly lineLimit?: number; + +} + +export namespace TokenizerOption { + /** + * The default TextMate tokenizer option. + */ + export const DEFAULT: TokenizerOption = { + lineLimit: 400 + }; +} + +export function createTextmateTokenizer(grammar: IGrammar, options: TokenizerOption): monaco.languages.EncodedTokensProvider { + if (options.lineLimit !== undefined && (options.lineLimit <= 0 || !Number.isInteger(options.lineLimit))) { + throw new Error(`The 'lineLimit' must be a positive integer. It was ${options.lineLimit}.`); + } return { - getInitialState: () => new State(INITIAL), - tokenize(line: string, state: State) { - if (line.length > 400) { - console.log(`Line starting with "${line.substr(0, 10)}..." is too long to be tokenized.`); - return { tokens: [], endState: state }; + getInitialState: () => new TokenizerState(INITIAL), + tokenizeEncoded(line: string, state: TokenizerState) { + let processedLine = line; + if (options.lineLimit !== undefined && line.length > options.lineLimit) { + // Line is too long to be tokenized + processedLine = line.substr(0, options.lineLimit); } - const result = grammar.tokenizeLine(line, state.ruleStack); - const tokenTheme = monaco.services.StaticServices.standaloneThemeService.get().getTheme().tokenTheme; - const defaultResult = tokenTheme.match(undefined, 'should.return.default'); - const defaultForeground = monaco.modes.TokenMetadata.getForeground(defaultResult); + const result = grammar.tokenizeLine2(processedLine, state.ruleStack); return { - endState: new State(result.ruleStack), - tokens: result.tokens.map(token => { - const scopes = token.scopes.slice(0); - - // TODO monaco doesn't allow to pass multiple scopes and have their styles merged yet. See https://github.com/Microsoft/monaco-editor/issues/929 - // As a workaround we go through the scopes backwards and pick the first for which the tokenTheme has a special foreground color. - for (let i = scopes.length - 1; i >= 0; i--) { - const scope = scopes[i]; - const match = tokenTheme.match(undefined, scope); - const foregroundColor = monaco.modes.TokenMetadata.getForeground(match); - if (defaultForeground !== foregroundColor) { - return { - ...token, - scopes: scope! - }; - } - } - return { - ...token, - scopes: scopes[0]!, - }; - }), + endState: new TokenizerState(result.ruleStack), + tokens: result.tokens }; } }; diff --git a/packages/monaco/src/typings/monaco/index.d.ts b/packages/monaco/src/typings/monaco/index.d.ts index 36a629753aa7e..b2d9c565626df 100644 --- a/packages/monaco/src/typings/monaco/index.d.ts +++ b/packages/monaco/src/typings/monaco/index.d.ts @@ -14,7 +14,7 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -/// +/// declare module monaco.instantiation { export interface IInstantiationService { @@ -23,6 +23,14 @@ declare module monaco.instantiation { declare module monaco.editor { + export interface IBulkEditResult { + ariaSummary: string; + } + + export interface IBulkEditService { + apply(edit: monaco.languages.WorkspaceEdit): monaco.Promise; + } + export interface IDiffNavigator { readonly ranges: IDiffRange[]; readonly nextIdx: number; @@ -52,10 +60,11 @@ declare module monaco.editor { } export interface IEditorOverrideServices { - editorService?: IEditorService; + codeEditorService?: ICodeEditorService; textModelService?: ITextModelService; contextMenuService?: IContextMenuService; commandService?: monaco.commands.ICommandService; + IWorkspaceEditService?: IBulkEditService; } export interface IResourceInput { @@ -91,8 +100,9 @@ declare module monaco.editor { export interface IEditorOptions { } - export interface IEditorService { - openEditor(input: IResourceInput, sideBySide?: boolean): monaco.Promise; + export interface ICodeEditorService { + getActiveCodeEditor(): monaco.editor.ICodeEditor | undefined; + openCodeEditor(input: monaco.editor.IResourceInput, source?: monaco.editor.ICodeEditor, sideBySide?: boolean): monaco.Promise; } export interface IReference extends monaco.IDisposable { @@ -180,7 +190,7 @@ declare module monaco.commands { } export interface ICommandService { - onWillExecuteCommand: monaco.IEvent; + readonly _onWillExecuteCommand: monaco.Emitter; executeCommand(commandId: string, ...args: any[]): monaco.Promise; executeCommand(commandId: string, ...args: any[]): monaco.Promise; } @@ -313,9 +323,16 @@ declare module monaco.keybindings { } declare module monaco.services { + + export abstract class CodeEditorServiceImpl implements monaco.editor.ICodeEditorService { + abstract getActiveCodeEditor(): monaco.editor.ICodeEditor | undefined; + abstract openCodeEditor(input: monaco.editor.IResourceInput, source?: monaco.editor.ICodeEditor, + sideBySide?: boolean): monaco.Promise; + } + export class StandaloneCommandService implements monaco.commands.ICommandService { constructor(instantiationService: monaco.instantiation.IInstantiationService); - onWillExecuteCommand: monaco.IEvent; + readonly _onWillExecuteCommand: monaco.Emitter; executeCommand(commandId: string, ...args: any[]): monaco.Promise; executeCommand(commandId: string, ...args: any[]): monaco.Promise; } @@ -366,13 +383,8 @@ declare module monaco.services { readonly id: LanguageId; } - export interface IModeService { - getLanguageIdentifier(modeId: string | LanguageId): LanguageIdentifier; - } - export module StaticServices { export const standaloneThemeService: LazyStaticService; - export const modeService: LazyStaticService; } } @@ -412,7 +424,7 @@ declare module monaco.referenceSearch { _widget: ReferenceWidget _model: ReferencesModel | undefined _ignoreModelChangeEvent: boolean; - _editorService: monaco.editor.IEditorService; + _editorService: monaco.editor.ICodeEditorService; closeWidget(): void; _gotoReference(ref: Location): void toggleWidget(range: IRange, modelPromise: Promise & { cancel: () => void }, options: RequestOptions): void; @@ -422,19 +434,28 @@ declare module monaco.referenceSearch { declare module monaco.quickOpen { + export interface IMessage { + content: string; + formatContent?: boolean; // defaults to false + type?: 1 /* INFO */ | 2 /* WARNING */ | 3 /* ERROR */; + } + + export class InputBox { + inputElement: HTMLInputElement; + setPlaceHolder(placeHolder: string): void; + showMessage(message: IMessage): void; + hideMessage(): void; + } + export class QuickOpenWidget implements IDisposable { + inputBox?: InputBox; constructor(container: HTMLElement, callbacks: IQuickOpenCallbacks, options: IQuickOpenOptions, usageLogger?: IQuickOpenUsageLogger); dispose(): void; create(): HTMLElement; - setPlaceHolder(placeHolder: string): void; setInput(input: IModel, autoFocus: IAutoFocus, ariaLabel?: string): void; layout(dimension: monaco.editor.IDimension): void; show(prefix: string, options?: IShowOptions): void; hide(reason?: HideReason): void; - refresh(input?: IModel, autoFocus?: IAutoFocus): void; - setPassword(isPassword: boolean): void; - showInputDecoration(decoration: Severity): void; - clearInputDecoration(): void; } export enum HideReason { @@ -652,42 +673,6 @@ declare module monaco.modes { readonly onDidChange: monaco.IEvent; } - export enum SymbolKind { - File = 0, - Module = 1, - Namespace = 2, - Package = 3, - Class = 4, - Method = 5, - Property = 6, - Field = 7, - Constructor = 8, - Enum = 9, - Interface = 10, - Function = 11, - Variable = 12, - Constant = 13, - String = 14, - Number = 15, - Boolean = 16, - Array = 17, - Object = 18, - Key = 19, - Null = 20, - EnumMember = 21, - Struct = 22, - Event = 23, - Operator = 24, - TypeParameter = 25 - } - - export interface SymbolInformation { - name: string; - containerName?: string; - kind: SymbolKind; - location: monaco.languages.Location; - } - export const DocumentSymbolProviderRegistry: LanguageFeatureRegistry; } @@ -762,3 +747,11 @@ declare module monaco.rename { } } + +declare module monaco.snippetParser { + export class SnippetParser { + parse(value: string): TextmateSnippet; + } + export class TextmateSnippet { + } +} diff --git a/packages/navigator/package.json b/packages/navigator/package.json index b7316c1a112bb..978c79c459293 100644 --- a/packages/navigator/package.json +++ b/packages/navigator/package.json @@ -1,11 +1,11 @@ { "name": "@theia/navigator", - "version": "0.3.13", + "version": "0.3.14", "description": "Theia - Navigator Extension", "dependencies": { - "@theia/core": "^0.3.13", - "@theia/filesystem": "^0.3.13", - "@theia/workspace": "^0.3.13", + "@theia/core": "^0.3.14", + "@theia/filesystem": "^0.3.14", + "@theia/workspace": "^0.3.14", "fuzzy": "^0.1.3" }, "publishConfig": { @@ -41,7 +41,7 @@ "docs": "theiaext docs" }, "devDependencies": { - "@theia/ext-scripts": "^0.3.13" + "@theia/ext-scripts": "^0.3.14" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/navigator/src/browser/navigator-container.ts b/packages/navigator/src/browser/navigator-container.ts index fec19fbf5e1f5..e793c1b66a1d0 100644 --- a/packages/navigator/src/browser/navigator-container.ts +++ b/packages/navigator/src/browser/navigator-container.ts @@ -23,12 +23,12 @@ import { FileNavigatorModel } from './navigator-model'; import { FileNavigatorWidget } from './navigator-widget'; import { NAVIGATOR_CONTEXT_MENU } from './navigator-contribution'; import { NavigatorDecoratorService, NavigatorTreeDecorator } from './navigator-decorator-service'; -import { FileNavigatorSearch } from './navigator-search'; export const FILE_NAVIGATOR_PROPS = { ...defaultTreeProps, contextMenuPath: NAVIGATOR_CONTEXT_MENU, - multiSelect: true + multiSelect: true, + search: true }; export function createFileNavigatorContainer(parent: interfaces.Container): Container { @@ -51,9 +51,6 @@ export function createFileNavigatorContainer(parent: interfaces.Container): Cont child.rebind(TreeDecoratorService).toDynamicValue(ctx => ctx.container.get(NavigatorDecoratorService)).inSingletonScope(); bindContributionProvider(child, NavigatorTreeDecorator); - child.bind(FileNavigatorSearch).toSelf().inSingletonScope(); - child.bind(NavigatorTreeDecorator).toService(FileNavigatorSearch); - return child; } diff --git a/packages/navigator/src/browser/navigator-decorator-service.ts b/packages/navigator/src/browser/navigator-decorator-service.ts index 687c80394ad32..bd92a71a58eda 100644 --- a/packages/navigator/src/browser/navigator-decorator-service.ts +++ b/packages/navigator/src/browser/navigator-decorator-service.ts @@ -30,7 +30,7 @@ export const NavigatorTreeDecorator = Symbol('NavigatorTreeDecorator'); export class NavigatorDecoratorService extends AbstractTreeDecoratorService { constructor(@inject(ContributionProvider) @named(NavigatorTreeDecorator) protected readonly contributions: ContributionProvider) { - super(contributions.getContributions(true)); + super(contributions.getContributions()); } } diff --git a/packages/navigator/src/browser/navigator-frontend-module.ts b/packages/navigator/src/browser/navigator-frontend-module.ts index 11ccd90543fb6..5658b969f48f6 100644 --- a/packages/navigator/src/browser/navigator-frontend-module.ts +++ b/packages/navigator/src/browser/navigator-frontend-module.ts @@ -23,9 +23,6 @@ import { createFileNavigatorWidget } from './navigator-container'; import { WidgetFactory } from '@theia/core/lib/browser/widget-manager'; import { bindFileNavigatorPreferences } from './navigator-preferences'; import { FileNavigatorFilter } from './navigator-filter'; -import { FuzzySearch } from './fuzzy-search'; -import { SearchBox, SearchBoxProps, SearchBoxFactory } from './search-box'; -import { SearchBoxDebounce } from './search-box-debounce'; import '../../src/browser/style/index.css'; export default new ContainerModule(bind => { @@ -37,14 +34,6 @@ export default new ContainerModule(bind => { bind(KeybindingContext).to(NavigatorActiveContext).inSingletonScope(); - bind(FuzzySearch).toSelf().inSingletonScope(); - bind(SearchBoxFactory).toFactory(context => - (options: SearchBoxProps) => { - const debounce = new SearchBoxDebounce(options); - return new SearchBox(options, debounce); - } - ); - bind(FileNavigatorWidget).toDynamicValue(ctx => createFileNavigatorWidget(ctx.container) ); diff --git a/packages/navigator/src/browser/navigator-model.ts b/packages/navigator/src/browser/navigator-model.ts index 9d8d6ad4cbe57..d1ffe6dc0b3da 100644 --- a/packages/navigator/src/browser/navigator-model.ts +++ b/packages/navigator/src/browser/navigator-model.ts @@ -17,10 +17,8 @@ import { injectable, inject, postConstruct } from 'inversify'; import URI from '@theia/core/lib/common/uri'; import { FileNode, FileTreeModel } from '@theia/filesystem/lib/browser'; -import { TreeIterator, Iterators } from '@theia/core/lib/browser/tree/tree-iterator'; import { OpenerService, open, TreeNode, ExpandableTreeNode } from '@theia/core/lib/browser'; import { FileNavigatorTree, WorkspaceRootNode, WorkspaceNode } from './navigator-tree'; -import { FileNavigatorSearch } from './navigator-search'; import { WorkspaceService } from '@theia/workspace/lib/browser'; @injectable() @@ -28,7 +26,6 @@ export class FileNavigatorModel extends FileTreeModel { @inject(OpenerService) protected readonly openerService: OpenerService; @inject(FileNavigatorTree) protected readonly tree: FileNavigatorTree; - @inject(FileNavigatorSearch) protected readonly navigatorSearch: FileNavigatorSearch; @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService; @postConstruct() @@ -142,32 +139,4 @@ export class FileNavigatorModel extends FileTreeModel { node1.id.length >= node2.id.length ? node1 : node2 ) : undefined; } - - protected createBackwardIterator(node: TreeNode | undefined): TreeIterator | undefined { - if (node === undefined) { - return undefined; - } - const { filteredNodes } = this.navigatorSearch; - if (filteredNodes.length === 0) { - return super.createBackwardIterator(node); - } - if (filteredNodes.indexOf(node) === -1) { - return undefined; - } - return Iterators.cycle(filteredNodes.slice().reverse(), node); - } - - protected createIterator(node: TreeNode | undefined): TreeIterator | undefined { - if (node === undefined) { - return undefined; - } - const { filteredNodes } = this.navigatorSearch; - if (filteredNodes.length === 0) { - return super.createIterator(node); - } - if (filteredNodes.indexOf(node) === -1) { - return undefined; - } - return Iterators.cycle(filteredNodes, node); - } } diff --git a/packages/navigator/src/browser/navigator-widget.tsx b/packages/navigator/src/browser/navigator-widget.tsx index 8a44254f3e644..356da036a2508 100644 --- a/packages/navigator/src/browser/navigator-widget.tsx +++ b/packages/navigator/src/browser/navigator-widget.tsx @@ -22,15 +22,13 @@ import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribu import { ContextMenuRenderer, ExpandableTreeNode, TreeProps, TreeModel, TreeNode, - LabelProvider, Widget, SelectableTreeNode, CompositeTreeNode + SelectableTreeNode, CompositeTreeNode } from '@theia/core/lib/browser'; import { FileTreeWidget, FileNode } from '@theia/filesystem/lib/browser'; import { WorkspaceService, WorkspaceCommands } from '@theia/workspace/lib/browser'; import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell'; import { WorkspaceNode } from './navigator-tree'; import { FileNavigatorModel } from './navigator-model'; -import { FileNavigatorSearch } from './navigator-search'; -import { SearchBox, SearchBoxProps, SearchBoxFactory } from './search-box'; import { FileSystem } from '@theia/filesystem/lib/common/filesystem'; import * as React from 'react'; @@ -41,8 +39,6 @@ export const CLASS = 'theia-Files'; @injectable() export class FileNavigatorWidget extends FileTreeWidget { - protected readonly searchBox: SearchBox; - constructor( @inject(TreeProps) readonly props: TreeProps, @inject(FileNavigatorModel) readonly model: FileNavigatorModel, @@ -50,9 +46,6 @@ export class FileNavigatorWidget extends FileTreeWidget { @inject(CommandService) protected readonly commandService: CommandService, @inject(SelectionService) protected readonly selectionService: SelectionService, @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService, - @inject(LabelProvider) protected readonly labelProvider: LabelProvider, - @inject(FileNavigatorSearch) protected readonly navigatorSearch: FileNavigatorSearch, - @inject(SearchBoxFactory) protected readonly searchBoxFactory: SearchBoxFactory, @inject(ApplicationShell) protected readonly shell: ApplicationShell, @inject(FileSystem) protected readonly fileSystem: FileSystem ) { @@ -61,32 +54,18 @@ export class FileNavigatorWidget extends FileTreeWidget { this.title.label = LABEL; this.addClass(CLASS); this.initialize(); - this.searchBox = searchBoxFactory(SearchBoxProps.DEFAULT); } @postConstruct() protected init(): void { super.init(); this.toDispose.pushAll([ - this.searchBox, - this.searchBox.onTextChange(data => this.navigatorSearch.filter(data)), - this.searchBox.onClose(data => this.navigatorSearch.filter(undefined)), - this.searchBox.onNext(() => this.model.selectNextNode()), - this.searchBox.onPrevious(() => this.model.selectPrevNode()), this.navigatorSearch, - this.navigatorSearch, - this.navigatorSearch.onFilteredNodesChanged(nodes => { - const node = nodes.find(SelectableTreeNode.is); - if (node) { - this.model.selectNode(node); - } - }), this.model.onSelectionChanged(selection => { if (this.shell.activeWidget === this) { this.selectionService.selection = selection; } }), this.model.onExpansionChanged(node => { - this.searchBox.hide(); if (node.expanded && node.children.length === 1) { const child = node.children[0]; if (ExpandableTreeNode.is(child) && !child.expanded) { @@ -167,11 +146,6 @@ export class FileNavigatorWidget extends FileTreeWidget { super.onAfterAttach(msg); this.addClipboardListener(this.node, 'copy', e => this.handleCopy(e)); this.addClipboardListener(this.node, 'paste', e => this.handlePaste(e)); - if (this.searchBox.isAttached) { - Widget.detach(this.searchBox); - } - Widget.attach(this.searchBox, this.node.parentElement!); - this.addKeyListener(this.node, this.searchBox.keyCodePredicate.bind(this.searchBox), this.searchBox.handle.bind(this.searchBox)); this.enableDndOnMainPanel(); } diff --git a/packages/navigator/src/browser/style/index.css b/packages/navigator/src/browser/style/index.css index 6a1955af49b25..5492be2512fd7 100644 --- a/packages/navigator/src/browser/style/index.css +++ b/packages/navigator/src/browser/style/index.css @@ -40,5 +40,3 @@ padding-bottom: 4px; width: calc(100% - var(--theia-ui-padding)*4); } - -@import './search-box.css'; diff --git a/packages/outline-view/package.json b/packages/outline-view/package.json index bd8a2bb34db03..47fd3315819b4 100644 --- a/packages/outline-view/package.json +++ b/packages/outline-view/package.json @@ -1,9 +1,9 @@ { "name": "@theia/outline-view", - "version": "0.3.13", + "version": "0.3.14", "description": "Theia - Outline View Extension", "dependencies": { - "@theia/core": "^0.3.13" + "@theia/core": "^0.3.14" }, "publishConfig": { "access": "public" @@ -38,7 +38,7 @@ "docs": "theiaext docs" }, "devDependencies": { - "@theia/ext-scripts": "^0.3.13" + "@theia/ext-scripts": "^0.3.14" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/outline-view/src/browser/outline-view-frontend-module.ts b/packages/outline-view/src/browser/outline-view-frontend-module.ts index 0508dd9a8d1f9..5de706e06c020 100644 --- a/packages/outline-view/src/browser/outline-view-frontend-module.ts +++ b/packages/outline-view/src/browser/outline-view-frontend-module.ts @@ -18,7 +18,14 @@ import { ContainerModule, interfaces } from 'inversify'; import { OutlineViewService } from './outline-view-service'; import { OutlineViewContribution } from './outline-view-contribution'; import { WidgetFactory } from '@theia/core/lib/browser/widget-manager'; -import { FrontendApplicationContribution, createTreeContainer, TreeWidget, bindViewContribution } from '@theia/core/lib/browser'; +import { + FrontendApplicationContribution, + createTreeContainer, + TreeWidget, + bindViewContribution, + TreeProps, + defaultTreeProps +} from '@theia/core/lib/browser'; import { OutlineViewWidgetFactory, OutlineViewWidget } from './outline-view-widget'; export default new ContainerModule(bind => { @@ -36,6 +43,8 @@ export default new ContainerModule(bind => { function createOutlineViewWidget(parent: interfaces.Container): OutlineViewWidget { const child = createTreeContainer(parent); + child.rebind(TreeProps).toConstantValue({ ...defaultTreeProps, search: true }); + child.unbind(TreeWidget); child.bind(OutlineViewWidget).toSelf(); diff --git a/packages/output/package.json b/packages/output/package.json index e254d912a1c7e..6a631fceca5b3 100644 --- a/packages/output/package.json +++ b/packages/output/package.json @@ -1,9 +1,9 @@ { "name": "@theia/output", - "version": "0.3.13", + "version": "0.3.14", "description": "Theia - Output Extension", "dependencies": { - "@theia/core": "^0.3.13" + "@theia/core": "^0.3.14" }, "publishConfig": { "access": "public" @@ -38,7 +38,7 @@ "docs": "theiaext docs" }, "devDependencies": { - "@theia/ext-scripts": "^0.3.13" + "@theia/ext-scripts": "^0.3.14" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/plugin-ext-vscode/package.json b/packages/plugin-ext-vscode/package.json index f6b6dfa6f73de..e93b857af1acf 100644 --- a/packages/plugin-ext-vscode/package.json +++ b/packages/plugin-ext-vscode/package.json @@ -1,9 +1,9 @@ { "name": "@theia/plugin-ext-vscode", - "version": "0.3.13", + "version": "0.3.14", "description": "Theia - Plugin Extension for VsCode", "dependencies": { - "@theia/plugin-ext": "^0.3.13" + "@theia/plugin-ext": "^0.3.14" }, "publishConfig": { "access": "public" @@ -38,7 +38,7 @@ "docs": "theiaext docs" }, "devDependencies": { - "@theia/ext-scripts": "^0.3.13" + "@theia/ext-scripts": "^0.3.14" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/plugin-ext-vscode/src/node/plugin-vscode-file-handler.ts b/packages/plugin-ext-vscode/src/node/plugin-vscode-file-handler.ts index cf6917b4f6036..32ee8983cfd74 100644 --- a/packages/plugin-ext-vscode/src/node/plugin-vscode-file-handler.ts +++ b/packages/plugin-ext-vscode/src/node/plugin-vscode-file-handler.ts @@ -16,15 +16,15 @@ import { PluginDeployerFileHandler, PluginDeployerEntry, PluginDeployerFileHandlerContext } from '@theia/plugin-ext'; import { injectable } from 'inversify'; -import * as os from 'os'; import * as path from 'path'; +import { getTempDir } from '@theia/plugin-ext'; @injectable() export class PluginVsCodeFileHandler implements PluginDeployerFileHandler { private unpackedFolder: string; constructor() { - this.unpackedFolder = path.resolve(os.tmpdir(), 'vscode-unpacked'); + this.unpackedFolder = getTempDir('vscode-unpacked'); } accept(resolvedPlugin: PluginDeployerEntry): boolean { diff --git a/packages/plugin-ext-vscode/src/node/plugin-vscode-init.ts b/packages/plugin-ext-vscode/src/node/plugin-vscode-init.ts index c42da191b4fc5..396dea2a826ab 100644 --- a/packages/plugin-ext-vscode/src/node/plugin-vscode-init.ts +++ b/packages/plugin-ext-vscode/src/node/plugin-vscode-init.ts @@ -14,18 +14,29 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { BackendInitializationFn, createAPI, PluginMetadata } from '@theia/plugin-ext'; +import * as theia from '@theia/plugin'; +import { BackendInitializationFn, PluginAPIFactory, Plugin, emptyPlugin } from '@theia/plugin-ext'; -export const doInitialization: BackendInitializationFn = (rpc: any, pluginMetadata: PluginMetadata) => { - const module = require('module'); - const vscodeModuleName = 'vscode'; - const vscode = createAPI(rpc); +const pluginsApiImpl = new Map(); +const plugins = new Array(); +let defaultApi: typeof theia; +let isLoadOverride = false; +let pluginApiFactory: PluginAPIFactory; + +export const doInitialization: BackendInitializationFn = (apiFactory: PluginAPIFactory, plugin: Plugin) => { + const vscode = apiFactory(plugin); // register the commands that are in the package.json file - const contributes: any = pluginMetadata.source.contributes; + const contributes: any = plugin.rawModel.contributes; if (contributes && contributes.commands) { contributes.commands.forEach((commandItem: any) => { - vscode.commands.registerCommand({ id: commandItem.command, label: commandItem.title }); + let commandLabel: string; + if (commandItem.category) { // if VS Code command has category we will add it before title, so label will looks like 'category: title' + commandLabel = commandItem.category + ': ' + commandItem.title; + } else { + commandLabel = commandItem.title; + } + vscode.commands.registerCommand({id: commandItem.command, label: commandLabel }); }); } @@ -37,27 +48,54 @@ export const doInitialization: BackendInitializationFn = (rpc: any, pluginMetada } }; - // add theia into global goal as 'vscode' - const g = global as any; - g[vscodeModuleName] = vscode; - - // add vscode object as module into npm cache - require.cache[vscodeModuleName] = { - id: vscodeModuleName, - filename: vscodeModuleName, - loaded: true, - exports: g[vscodeModuleName] + // use Theia plugin api instead vscode extensions + (vscode).extensions = { + get all(): any[] { + return vscode.plugins.all; + }, + getExtension(pluginId: string): any | undefined { + return vscode.plugins.getPlugin(pluginId); + } }; - // save original resolve method - const internalResolve = module._resolveFilename; + pluginsApiImpl.set(plugin.model.id, vscode); + plugins.push(plugin); + + if (!isLoadOverride) { + overrideInternalLoad(); + isLoadOverride = true; + pluginApiFactory = apiFactory; + } +}; + +function overrideInternalLoad(): void { + const module = require('module'); + const vscodeModuleName = 'vscode'; + // save original load method + const internalLoad = module._load; - // if we try to resolve vscode module, return the filename entry to use cache. - module._resolveFilename = (request: string, parent: {}) => { - if (vscodeModuleName === request) { - return request; + // if we try to resolve theia module, return the filename entry to use cache. + // tslint:disable-next-line:no-any + module._load = function (request: string, parent: any, isMain: {}) { + if (request !== vscodeModuleName) { + return internalLoad.apply(this, arguments); } - const retVal = internalResolve(request, parent); - return retVal; + + const plugin = findPlugin(parent.filename); + if (plugin) { + const apiImpl = pluginsApiImpl.get(plugin.model.id); + return apiImpl; + } + + if (!defaultApi) { + console.warn(`Could not identify plugin for 'Theia' require call from ${parent.filename}`); + defaultApi = pluginApiFactory(emptyPlugin); + } + + return defaultApi; }; -}; +} + +function findPlugin(filePath: string): Plugin | undefined { + return plugins.find(plugin => filePath.startsWith(plugin.pluginFolder)); +} diff --git a/packages/plugin-ext-vscode/src/node/scanner-vscode.ts b/packages/plugin-ext-vscode/src/node/scanner-vscode.ts index 9d9ef842d392b..fa37c36df1996 100644 --- a/packages/plugin-ext-vscode/src/node/scanner-vscode.ts +++ b/packages/plugin-ext-vscode/src/node/scanner-vscode.ts @@ -27,6 +27,7 @@ export class VsCodePluginScanner implements PluginScanner { getModel(plugin: PluginPackage): PluginModel { return { + id: `${plugin.publisher}.${plugin.name}`, name: plugin.name, publisher: plugin.publisher, version: plugin.version, diff --git a/packages/plugin-ext/package.json b/packages/plugin-ext/package.json index 1898a738d6a0c..be2bf8446c28f 100644 --- a/packages/plugin-ext/package.json +++ b/packages/plugin-ext/package.json @@ -1,16 +1,16 @@ { "name": "@theia/plugin-ext", - "version": "0.3.13", + "version": "0.3.14", "description": "Theia - Plugin Extension", "main": "lib/common/index.js", "typings": "lib/common/index.d.ts", "dependencies": { - "@theia/core": "^0.3.13", - "@theia/filesystem": "^0.3.13", - "@theia/monaco": "^0.3.13", - "@theia/plugin": "^0.3.13", - "@theia/terminal": "^0.3.13", - "@theia/workspace": "^0.3.13", + "@theia/core": "^0.3.14", + "@theia/filesystem": "^0.3.14", + "@theia/monaco": "^0.3.14", + "@theia/plugin": "^0.3.14", + "@theia/terminal": "^0.3.14", + "@theia/workspace": "^0.3.14", "decompress": "^4.2.0", "lodash.clonedeep": "^4.5.0", "ps-tree": "1.1.0", @@ -51,7 +51,7 @@ "docs": "theiaext docs" }, "devDependencies": { - "@theia/ext-scripts": "^0.3.13", + "@theia/ext-scripts": "^0.3.14", "@types/decompress": "^4.2.2", "@types/lodash.clonedeep": "^4.5.3" }, diff --git a/packages/plugin-ext/src/api/model.ts b/packages/plugin-ext/src/api/model.ts index 66e110fb5ddde..02e7a0049fb1f 100644 --- a/packages/plugin-ext/src/api/model.ts +++ b/packages/plugin-ext/src/api/model.ts @@ -15,6 +15,8 @@ ********************************************************************************/ import * as theia from '@theia/plugin'; +import { UriComponents } from '../common/uri-components'; + // Should contains internal Plugin API types export interface Range { @@ -135,3 +137,36 @@ export interface CompletionResultDto extends IdObject { completions: CompletionDto[]; incomplete?: boolean; } + +export interface MarkerData { + code?: string; + severity: MarkerSeverity; + message: string; + source?: string; + startLineNumber: number; + startColumn: number; + endLineNumber: number; + endColumn: number; + relatedInformation?: RelatedInformation[]; + tags?: MarkerTag[]; +} + +export interface RelatedInformation { + resource: UriComponents; + message: string; + startLineNumber: number; + startColumn: number; + endLineNumber: number; + endColumn: number; +} + +export enum MarkerSeverity { + Hint = 1, + Info = 2, + Warning = 4, + Error = 8, +} + +export enum MarkerTag { + Unnecessary = 1, +} diff --git a/packages/plugin-ext/src/api/plugin-api.ts b/packages/plugin-ext/src/api/plugin-api.ts index 5fb1150b03bae..75b5f82cb9bbf 100644 --- a/packages/plugin-ext/src/api/plugin-api.ts +++ b/packages/plugin-ext/src/api/plugin-api.ts @@ -16,7 +16,7 @@ import { createProxyIdentifier, ProxyIdentifier } from './rpc-protocol'; import * as theia from '@theia/plugin'; -import { PluginLifecycle, PluginModel, PluginMetadata } from '../common/plugin-protocol'; +import { PluginLifecycle, PluginModel, PluginMetadata, PluginPackage } from '../common/plugin-protocol'; import { QueryParameters } from '../common/env'; import { TextEditorCursorStyle } from '../common/editor-options'; import { TextEditorLineNumbersStyle, EndOfLine, OverviewRulerLane, IndentAction } from '../plugin/types-impl'; @@ -29,21 +29,80 @@ import { MarkdownString, Range, Completion, - CompletionResultDto + CompletionResultDto, + MarkerData } from './model'; -export interface HostedPluginManagerExt { - $initialize(contextPath: string, pluginMetadata: PluginMetadata): void; - $loadPlugin(contextPath: string, plugin: Plugin): void; - $stopPlugin(contextPath: string): PromiseLike; +export interface PluginInitData { + plugins: PluginMetadata[]; } export interface Plugin { pluginPath: string; + pluginFolder: string; model: PluginModel; + rawModel: PluginPackage; lifecycle: PluginLifecycle; } +export interface PluginAPI { + +} + +export interface PluginManager { + getAllPlugins(): Plugin[]; + getPluginById(pluginId: string): Plugin | undefined; + getPluginExport(pluginId: string): PluginAPI | undefined; + isRunning(pluginId: string): boolean; + activatePlugin(pluginId: string): PromiseLike; +} + +export interface PluginAPIFactory { + (plugin: Plugin): typeof theia; +} + +export const emptyPlugin: Plugin = { + lifecycle: { + startMethod: 'empty', + stopMethod: 'empty' + }, + model: { + id: 'emptyPlugin', + name: 'emptyPlugin', + publisher: 'Theia', + version: 'empty', + displayName: 'empty', + description: 'empty', + engine: { + type: 'empty', + version: 'empty' + }, + entryPoint: { + + } + }, + pluginPath: 'empty', + pluginFolder: 'empty', + rawModel: { + name: 'emptyPlugin', + publisher: 'Theia', + version: 'empty', + displayName: 'empty', + description: 'empty', + engines: { + type: 'empty', + version: 'empty' + }, + packagePath: 'empty' + } +}; + +export interface PluginManagerExt { + $stopPlugin(contextPath: string): PromiseLike; + + $init(pluginInit: PluginInitData): PromiseLike; +} + export interface CommandRegistryMain { $registerCommand(command: theia.Command): void; @@ -586,6 +645,9 @@ export interface LanguagesMain { $setLanguageConfiguration(handle: number, languageId: string, configuration: SerializedLanguageConfiguration): void; $unregister(handle: number): void; $registerCompletionSupport(handle: number, selector: SerializedDocumentFilter[], triggerCharacters: string[], supportsResolveDetails: boolean): void; + + $clearDiagnostics(id: string): void; + $changeDiagnostics(id: string, delta: [UriComponents, MarkerData[]][]): void; } export const PLUGIN_RPC_CONTEXT = { @@ -605,7 +667,7 @@ export const PLUGIN_RPC_CONTEXT = { }; export const MAIN_RPC_CONTEXT = { - HOSTED_PLUGIN_MANAGER_EXT: createProxyIdentifier('HostedPluginManagerExt'), + HOSTED_PLUGIN_MANAGER_EXT: createProxyIdentifier('PluginManagerExt'), COMMAND_REGISTRY_EXT: createProxyIdentifier('CommandRegistryExt'), QUICK_OPEN_EXT: createProxyIdentifier('QuickOpenExt'), WINDOW_STATE_EXT: createProxyIdentifier('WindowStateExt'), diff --git a/packages/plugin-ext/src/common/index.ts b/packages/plugin-ext/src/common/index.ts index fa25649af2daf..ae83ef31f489c 100644 --- a/packages/plugin-ext/src/common/index.ts +++ b/packages/plugin-ext/src/common/index.ts @@ -22,3 +22,4 @@ export * from '../hosted/node/hosted-plugin-uri-postprocessor'; export * from '../common/plugin-protocol'; export * from '../plugin/plugin-context'; export * from '../api/plugin-api'; +export * from '../main/node/temp-dir-util'; diff --git a/packages/plugin-ext/src/common/plugin-protocol.ts b/packages/plugin-ext/src/common/plugin-protocol.ts index 5bf17ccf7c001..aed2f1ee1ae37 100644 --- a/packages/plugin-ext/src/common/plugin-protocol.ts +++ b/packages/plugin-ext/src/common/plugin-protocol.ts @@ -17,7 +17,7 @@ import { JsonRpcServer } from '@theia/core/lib/common/messaging/proxy-factory'; import { RPCProtocol } from '../api/rpc-protocol'; import { Disposable } from '@theia/core/lib/common/disposable'; import { LogPart } from './types'; -import { CharacterPair, CommentRule } from '../api/plugin-api'; +import { CharacterPair, CommentRule, PluginAPIFactory, Plugin } from '../api/plugin-api'; export const hostedServicePath = '/services/hostedPlugin'; @@ -53,6 +53,19 @@ export interface PluginPackage { export interface PluginPackageContribution { languages?: PluginPackageLanguageContribution[]; grammars?: PluginPackageGrammarsContribution[]; + viewsContainers?: { [location: string]: PluginPackageViewContainer[] }; + views?: { [location: string]: PluginPackageView[] }; +} + +export interface PluginPackageViewContainer { + id: string; + title: string; + icon: string; +} + +export interface PluginPackageView { + id: string; + name: string; } export interface PluginPackageGrammarsContribution { @@ -253,6 +266,7 @@ export interface PluginDeployerDirectoryHandler { * This interface describes a plugin model object, which is populated from package.json. */ export interface PluginModel { + id: string; name: string; publisher: string; version: string; @@ -275,6 +289,8 @@ export interface PluginModel { export interface PluginContribution { languages?: LanguageContribution[]; grammars?: GrammarsContribution[]; + viewsContainers?: { [location: string]: ViewContainer[] }; + views?: { [location: string]: View[] }; } export interface GrammarsContribution { @@ -336,6 +352,23 @@ export interface FoldingRules { markers?: FoldingMarkers; } +/** + * Views Containers contribution + */ +export interface ViewContainer { + id: string; + title: string; + icon: string; +} + +/** + * View contribution + */ +export interface View { + id: string; + name: string; +} + /** * This interface describes a plugin lifecycle object. */ @@ -360,7 +393,7 @@ export interface PluginLifecycle { * The export function of initialization module of backend plugin. */ export interface BackendInitializationFn { - (rpc: RPCProtocol, pluginMetadata: PluginMetadata): void; + (apiFactory: PluginAPIFactory, plugin: Plugin): void; } export interface BackendLoadingFn { diff --git a/packages/plugin-ext/src/common/types.ts b/packages/plugin-ext/src/common/types.ts index d18c5c42c98ed..0574539fc99a8 100644 --- a/packages/plugin-ext/src/common/types.ts +++ b/packages/plugin-ext/src/common/types.ts @@ -28,26 +28,26 @@ export function isObject(obj: any): boolean { // tslint:disable-next-line:no-any export function mixin(destination: any, source: any, overwrite: boolean = true): any { - if (!isObject(destination)) { - return source; - } + if (!isObject(destination)) { + return source; + } - if (isObject(source)) { - Object.keys(source).forEach(key => { - if (key in destination) { - if (overwrite) { - if (isObject(destination[key]) && isObject(source[key])) { - mixin(destination[key], source[key], overwrite); - } else { - destination[key] = source[key]; - } - } - } else { - destination[key] = source[key]; - } - }); - } - return destination; + if (isObject(source)) { + Object.keys(source).forEach(key => { + if (key in destination) { + if (overwrite) { + if (isObject(destination[key]) && isObject(source[key])) { + mixin(destination[key], source[key], overwrite); + } else { + destination[key] = source[key]; + } + } + } else { + destination[key] = source[key]; + } + }); + } + return destination; } export enum LogType { diff --git a/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts b/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts index 9c146eff4f61a..9ce1ad0731bd3 100644 --- a/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts +++ b/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts @@ -17,7 +17,7 @@ import { injectable, inject, interfaces } from 'inversify'; import { PluginWorker } from '../../main/browser/plugin-worker'; import { HostedPluginServer, PluginMetadata } from '../../common/plugin-protocol'; import { HostedPluginWatcher } from './hosted-plugin-watcher'; -import { MAIN_RPC_CONTEXT, Plugin } from '../../api/plugin-api'; +import { MAIN_RPC_CONTEXT } from '../../api/plugin-api'; import { setUpPluginApi } from '../../main/browser/main-context'; import { RPCProtocol, RPCProtocolImpl } from '../../api/rpc-protocol'; import { ILogger } from '@theia/core'; @@ -43,9 +43,6 @@ export class HostedPluginSupport { private theiaReadyPromise: Promise; - private backendApiInitialized = false; - private frontendApiInitialized = false; - constructor( @inject(PreferenceServiceImpl) private readonly preferenceServiceImpl: PreferenceServiceImpl ) { @@ -58,81 +55,53 @@ export class HostedPluginSupport { } public initPlugins(): void { - this.server.getHostedPlugin().then((pluginMetadata: any) => { - if (pluginMetadata) { - this.loadPlugin(pluginMetadata, this.container); + Promise.all([this.server.getDeployedMetadata(), this.server.getHostedPlugin()]).then(metadata => { + const plugins = [...metadata['0']]; + if (metadata['1']) { + plugins.push(metadata['1']!); } + this.loadPlugins(plugins, this.container); }); - const backendMetadata = this.server.getDeployedBackendMetadata(); + } - backendMetadata.then((pluginMetadata: PluginMetadata[]) => { - pluginMetadata.forEach(metadata => this.loadPlugin(metadata, this.container)); - }); + loadPlugins(pluginsMetadata: PluginMetadata[], container: interfaces.Container): void { + const [frontend, backend] = this.initContributions(pluginsMetadata); + this.theiaReadyPromise.then(() => { + if (frontend) { + this.worker = new PluginWorker(); + const hostedExtManager = this.worker.rpc.getProxy(MAIN_RPC_CONTEXT.HOSTED_PLUGIN_MANAGER_EXT); + hostedExtManager.$init({ plugins: pluginsMetadata }); + setUpPluginApi(this.worker.rpc, container); + } - this.server.getDeployedFrontendMetadata().then((pluginMetadata: PluginMetadata[]) => { - pluginMetadata.forEach(metadata => this.loadPlugin(metadata, this.container)); + if (backend) { + const rpc = this.createServerRpc(); + const hostedExtManager = rpc.getProxy(MAIN_RPC_CONTEXT.HOSTED_PLUGIN_MANAGER_EXT); + hostedExtManager.$init({ plugins: pluginsMetadata }); + setUpPluginApi(rpc, container); + } }); } - public loadPlugin(pluginMetadata: PluginMetadata, container: interfaces.Container): void { - const pluginModel = pluginMetadata.model; - const pluginLifecycle = pluginMetadata.lifecycle; - this.logger.info('Ask to load the plugin with model ', pluginModel, ' and lifecycle', pluginLifecycle); - if (pluginMetadata.model.contributes) { - this.contributionHandler.handleContributions(pluginMetadata.model.contributes); - } - if (pluginModel.entryPoint!.frontend) { - this.logger.info(`Loading frontend hosted plugin: ${pluginModel.name}`); - this.worker = new PluginWorker(); + private initContributions(pluginsMetadata: PluginMetadata[]): [boolean, boolean] { + const result: [boolean, boolean] = [false, false]; + for (const plugin of pluginsMetadata) { + if (plugin.model.entryPoint.frontend) { + result[0] = true; + } - this.theiaReadyPromise.then(() => { - const hostedExtManager = this.worker.rpc.getProxy(MAIN_RPC_CONTEXT.HOSTED_PLUGIN_MANAGER_EXT); - const plugin: Plugin = { - pluginPath: pluginModel.entryPoint.frontend!, - model: pluginModel, - lifecycle: pluginLifecycle - }; - let frontendInitPath = pluginLifecycle.frontendInitPath; - if (frontendInitPath) { - hostedExtManager.$initialize(frontendInitPath, pluginMetadata); - } else { - frontendInitPath = ''; - } - // we should create only one instance of the plugin api per connection - if (!this.frontendApiInitialized) { - setUpPluginApi(this.worker.rpc, container); - this.frontendApiInitialized = true; - } - hostedExtManager.$loadPlugin(frontendInitPath, plugin); - }); - } - if (pluginModel.entryPoint!.backend) { - this.logger.info(`Loading backend hosted plugin: ${pluginModel.name}`); - const rpc = this.createServerRpc(); + if (plugin.model.entryPoint.backend) { + result[1] = true; + } - this.theiaReadyPromise.then(() => { - const hostedExtManager = rpc.getProxy(MAIN_RPC_CONTEXT.HOSTED_PLUGIN_MANAGER_EXT); - const plugin: Plugin = { - pluginPath: pluginModel.entryPoint.backend!, - model: pluginModel, - lifecycle: pluginLifecycle - }; - let backendInitPath = pluginLifecycle.backendInitPath; - if (backendInitPath) { - hostedExtManager.$initialize(backendInitPath, pluginMetadata); - } else { - backendInitPath = ''; - } - // we should create only one instance of the plugin api per connection - if (!this.backendApiInitialized) { - setUpPluginApi(rpc, container); - this.backendApiInitialized = true; - } - hostedExtManager.$loadPlugin(backendInitPath, plugin); - }); + if (plugin.model.contributes) { + this.contributionHandler.handleContributions(plugin.model.contributes); + } } + + return result; } private createServerRpc(): RPCProtocol { diff --git a/packages/plugin-ext/src/hosted/browser/worker/worker-main.ts b/packages/plugin-ext/src/hosted/browser/worker/worker-main.ts index ed874da7aa69d..9072a28f7bedc 100644 --- a/packages/plugin-ext/src/hosted/browser/worker/worker-main.ts +++ b/packages/plugin-ext/src/hosted/browser/worker/worker-main.ts @@ -16,13 +16,17 @@ import { Emitter } from '@theia/core/lib/common/event'; import { RPCProtocolImpl } from '../../../api/rpc-protocol'; -import { HostedPluginManagerExtImpl } from '../../plugin/hosted-plugin-manager'; -import { MAIN_RPC_CONTEXT, Plugin } from '../../../api/plugin-api'; -import { createAPI, startPlugin } from '../../../plugin/plugin-context'; +import { PluginManagerExtImpl } from '../../../plugin/plugin-manager'; +import { MAIN_RPC_CONTEXT, Plugin, emptyPlugin } from '../../../api/plugin-api'; +import { createAPIFactory } from '../../../plugin/plugin-context'; import { getPluginId, PluginMetadata } from '../../../common/plugin-protocol'; +import * as theia from '@theia/plugin'; +// tslint:disable-next-line:no-any const ctx = self as any; -const plugins = new Map void>(); + +const pluginsApiImpl = new Map(); +const pluginsModulesNames = new Map(); const emitter = new Emitter(); const rpc = new RPCProtocolImpl({ @@ -31,18 +35,17 @@ const rpc = new RPCProtocolImpl({ ctx.postMessage(m); } }); +// tslint:disable-next-line:no-any addEventListener('message', (message: any) => { emitter.fire(message.data); }); +function initialize(contextPath: string, pluginMetadata: PluginMetadata): void { + ctx.importScripts('/context/' + contextPath); +} -const theia = createAPI(rpc); -ctx['theia'] = theia; - -rpc.set(MAIN_RPC_CONTEXT.HOSTED_PLUGIN_MANAGER_EXT, new HostedPluginManagerExtImpl({ - initialize(contextPath: string, pluginMetadata: PluginMetadata): void { - ctx.importScripts('/context/' + contextPath); - }, - loadPlugin(contextPath: string, plugin: Plugin): void { +const pluginManager = new PluginManagerExtImpl({ + // tslint:disable-next-line:no-any + loadPlugin(plugin: Plugin): any { if (isElectron()) { ctx.importScripts(plugin.pluginPath); } else { @@ -54,19 +57,70 @@ rpc.set(MAIN_RPC_CONTEXT.HOSTED_PLUGIN_MANAGER_EXT, new HostedPluginManagerExtIm console.error(`WebWorker: Cannot start plugin "${plugin.model.name}". Frontend plugin not found: "${plugin.lifecycle.frontendModuleName}"`); return; } - startPlugin(plugin, ctx[plugin.lifecycle.frontendModuleName], plugins); + return ctx[plugin.lifecycle.frontendModuleName]; } }, - stopPlugins(contextPath: string, pluginIds: string[]): void { - pluginIds.forEach(pluginId => { - const stopPluginMethod = plugins.get(pluginId); - if (stopPluginMethod) { - stopPluginMethod(); - plugins.delete(pluginId); + init(rawPluginData: PluginMetadata[]): [Plugin[], Plugin[]] { + const result: Plugin[] = []; + const foreign: Plugin[] = []; + for (const plg of rawPluginData) { + const pluginModel = plg.model; + const pluginLifecycle = plg.lifecycle; + if (pluginModel.entryPoint!.frontend) { + let frontendInitPath = pluginLifecycle.frontendInitPath; + if (frontendInitPath) { + initialize(frontendInitPath, plg); + } else { + frontendInitPath = ''; + } + const plugin: Plugin = { + pluginPath: pluginModel.entryPoint.frontend!, + pluginFolder: plg.source.packagePath, + model: pluginModel, + lifecycle: pluginLifecycle, + rawModel: plg.source + }; + result.push(plugin); + const apiImpl = apiFactory(plugin); + pluginsApiImpl.set(plugin.model.id, apiImpl); + pluginsModulesNames.set(plugin.lifecycle.frontendModuleName!, plugin); + } else { + foreign.push({ + pluginPath: pluginModel.entryPoint.backend!, + pluginFolder: plg.source.packagePath, + model: pluginModel, + lifecycle: pluginLifecycle, + rawModel: plg.source + }); } - }); + } + + return [result, foreign]; } -})); +}); + +const apiFactory = createAPIFactory(rpc, pluginManager); +let defaultApi: typeof theia; + +const handler = { + get: (target: any, name: string) => { + const plugin = pluginsModulesNames.get(name); + if (plugin) { + const apiImpl = pluginsApiImpl.get(plugin.model.id); + return apiImpl; + } + + if (!defaultApi) { + defaultApi = apiFactory(emptyPlugin); + } + + return defaultApi; + } +}; +// tslint:disable-next-line:no-null-keyword +ctx['theia'] = new Proxy(Object.create(null), handler); + +rpc.set(MAIN_RPC_CONTEXT.HOSTED_PLUGIN_MANAGER_EXT, pluginManager); function isElectron() { if (typeof navigator === 'object' && typeof navigator.userAgent === 'string' && navigator.userAgent.indexOf('Electron') >= 0) { diff --git a/packages/plugin-ext/src/hosted/node/hosted-plugin.ts b/packages/plugin-ext/src/hosted/node/hosted-plugin.ts index 84c78970ab93e..d86d2e80ec9ad 100644 --- a/packages/plugin-ext/src/hosted/node/hosted-plugin.ts +++ b/packages/plugin-ext/src/hosted/node/hosted-plugin.ts @@ -41,6 +41,11 @@ export class HostedPluginSupport { private cp: cp.ChildProcess | undefined; setClient(client: HostedPluginClient): void { + if (this.client) { + if (this.cp) { + this.runPluginServer(); + } + } this.client = client; } diff --git a/packages/plugin-ext/src/hosted/node/plugin-host.ts b/packages/plugin-ext/src/hosted/node/plugin-host.ts index bcd6ac663fad3..b5e80414f73b7 100644 --- a/packages/plugin-ext/src/hosted/node/plugin-host.ts +++ b/packages/plugin-ext/src/hosted/node/plugin-host.ts @@ -15,62 +15,90 @@ ********************************************************************************/ import { Emitter } from '@theia/core/lib/common/event'; -import { startPlugin } from '../../plugin/plugin-context'; -import { HostedPluginManagerExtImpl } from '../plugin/hosted-plugin-manager'; +import { PluginManagerExtImpl } from '../../plugin/plugin-manager'; import { RPCProtocolImpl } from '../../api/rpc-protocol'; import { MAIN_RPC_CONTEXT, Plugin } from '../../api/plugin-api'; import { PluginMetadata } from '../../common/plugin-protocol'; - +import { createAPIFactory } from '../../plugin/plugin-context'; console.log('PLUGIN_HOST(' + process.pid + ') starting instance'); -const plugins = new Map void>(); - -const emmitter = new Emitter(); +const emitter = new Emitter(); const rpc = new RPCProtocolImpl({ - onMessage: emmitter.event, + onMessage: emitter.event, send: (m: {}) => { if (process.send) { process.send(JSON.stringify(m)); } } }); -process.on('message', (message: any) => { + +process.on('message', (message: string) => { try { - emmitter.fire(JSON.parse(message)); + emitter.fire(JSON.parse(message)); } catch (e) { console.error(e); } }); -rpc.set(MAIN_RPC_CONTEXT.HOSTED_PLUGIN_MANAGER_EXT, new HostedPluginManagerExtImpl({ - - initialize(contextPath: string, pluginMetadata: PluginMetadata): void { - console.log('PLUGIN_HOST(' + process.pid + '): initializing(' + contextPath + ')'); +// tslint:disable-next-line:no-any +function initialize(contextPath: string, plugin: Plugin): any { + console.log('PLUGIN_HOST(' + process.pid + '): initializing(' + contextPath + ')'); + try { const backendInit = require(contextPath); - backendInit.doInitialization(rpc, pluginMetadata); - }, - loadPlugin(contextPath: string, plugin: Plugin): void { + backendInit.doInitialization(apiFactory, plugin); + } catch (e) { + console.error(e); + } +} + +const pluginManager = new PluginManagerExtImpl({ + loadPlugin(plugin: Plugin): void { console.log('PLUGIN_HOST(' + process.pid + '): loadPlugin(' + plugin.pluginPath + ')'); - const backendInit = require(contextPath); - if (backendInit.doLoad) { - backendInit.doLoad(rpc, plugin); - } try { - const pluginMain = require(plugin.pluginPath); - startPlugin(plugin, pluginMain, plugins); - + return require(plugin.pluginPath); } catch (e) { console.error(e); } }, - stopPlugins(contextPath: string, pluginIds: string[]): void { - console.log('PLUGIN_HOST(' + process.pid + '): stopPlugins(' + JSON.stringify(pluginIds) + ')'); - pluginIds.forEach(pluginId => { - const stopPluginMethod = plugins.get(pluginId); - if (stopPluginMethod) { - stopPluginMethod(); - plugins.delete(pluginId); + init(raw: PluginMetadata[]): [Plugin[], Plugin[]] { + const result: Plugin[] = []; + const foreign: Plugin[] = []; + for (const plg of raw) { + const pluginModel = plg.model; + const pluginLifecycle = plg.lifecycle; + if (pluginModel.entryPoint!.backend) { + + let backendInitPath = pluginLifecycle.backendInitPath; + // if no init path, try to init as regular Theia plugin + if (!backendInitPath) { + backendInitPath = __dirname + '/scanners/backend-init-theia.js'; + } + + const plugin: Plugin = { + pluginPath: pluginModel.entryPoint.backend!, + pluginFolder: plg.source.packagePath, + model: pluginModel, + lifecycle: pluginLifecycle, + rawModel: plg.source + }; + + initialize(backendInitPath, plugin); + + result.push(plugin); + } else { + foreign.push({ + pluginPath: pluginModel.entryPoint.frontend!, + pluginFolder: plg.source.packagePath, + model: pluginModel, + lifecycle: pluginLifecycle, + rawModel: plg.source + }); } - }); + } + + return [result, foreign]; } -})); +}); + +rpc.set(MAIN_RPC_CONTEXT.HOSTED_PLUGIN_MANAGER_EXT, pluginManager); +const apiFactory = createAPIFactory(rpc, pluginManager); diff --git a/packages/plugin-ext/src/hosted/node/scanners/backend-init-theia.ts b/packages/plugin-ext/src/hosted/node/scanners/backend-init-theia.ts index 9a6a314832f7f..391530f476bcf 100644 --- a/packages/plugin-ext/src/hosted/node/scanners/backend-init-theia.ts +++ b/packages/plugin-ext/src/hosted/node/scanners/backend-init-theia.ts @@ -14,38 +14,58 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { createAPI } from '../../../plugin/plugin-context'; +import * as theia from '@theia/plugin'; import { BackendInitializationFn } from '../../../common/plugin-protocol'; +import { PluginAPIFactory, Plugin, emptyPlugin } from '../../../api/plugin-api'; -export const doInitialization: BackendInitializationFn = (rpc: any) => { - const theia = createAPI(rpc); +const pluginsApiImpl = new Map(); +const plugins = new Array(); +let defaultApi: typeof theia; +let isLoadOverride = false; +let pluginApiFactory: PluginAPIFactory; - // add theia into global goal - const g = global as any; - g['theia'] = theia; +export const doInitialization: BackendInitializationFn = (apiFactory: PluginAPIFactory, plugin: Plugin) => { - const NODE_MODULE_NAMES = ['@theia/plugin', '@wiptheia/plugin']; - const module = require('module'); + const apiImpl = apiFactory(plugin); + pluginsApiImpl.set(plugin.model.id, apiImpl); + + plugins.push(plugin); + + if (!isLoadOverride) { + overrideInternalLoad(); + isLoadOverride = true; + pluginApiFactory = apiFactory; + } - // add theia object as module into npm cache - NODE_MODULE_NAMES.forEach(moduleName => { - require.cache[moduleName] = { - id: moduleName, - filename: moduleName, - loaded: true, - exports: theia - }; - }); +}; - // save original resolve method - const internalResolve = module._resolveFilename; +function overrideInternalLoad(): void { + const module = require('module'); + // save original load method + const internalLoad = module._load; // if we try to resolve theia module, return the filename entry to use cache. - module._resolveFilename = (request: string, parent: {}) => { - if (NODE_MODULE_NAMES.indexOf(request) !== -1) { - return request; + // tslint:disable-next-line:no-any + module._load = function (request: string, parent: any, isMain: {}) { + if (request !== '@theia/plugin') { + return internalLoad.apply(this, arguments); + } + + const plugin = findPlugin(parent.filename); + if (plugin) { + const apiImpl = pluginsApiImpl.get(plugin.model.id); + return apiImpl; + } + + if (!defaultApi) { + console.warn(`Could not identify plugin for 'Theia' require call from ${parent.filename}`); + defaultApi = pluginApiFactory(emptyPlugin); } - const retVal = internalResolve(request, parent); - return retVal; + + return defaultApi; }; -}; +} + +function findPlugin(filePath: string): Plugin | undefined { + return plugins.find(plugin => filePath.startsWith(plugin.pluginFolder)); +} diff --git a/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts b/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts index b61942a9fb1a7..cefb46a61de85 100644 --- a/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts +++ b/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts @@ -28,7 +28,11 @@ import { PluginPackageLanguageContributionConfiguration, LanguageConfiguration, AutoClosingPairConditional, - AutoClosingPair + AutoClosingPair, + ViewContainer, + PluginPackageViewContainer, + View, + PluginPackageView } from '../../../common/plugin-protocol'; import * as fs from 'fs'; import * as path from 'path'; @@ -49,6 +53,7 @@ export class TheiaPluginScanner implements PluginScanner { getModel(plugin: PluginPackage): PluginModel { const result: PluginModel = { + id: `${plugin.publisher}.${plugin.name}`, name: plugin.name, publisher: plugin.publisher, version: plugin.version, @@ -93,17 +98,66 @@ export class TheiaPluginScanner implements PluginScanner { contributions.grammars = grammars; } + if (rawPlugin.contributes!.viewsContainers) { + contributions.viewsContainers = {}; + + Object.keys(rawPlugin.contributes.viewsContainers!).forEach(location => { + const containers = this.readViewsContainers(rawPlugin.contributes!.viewsContainers![location], rawPlugin.packagePath); + if (location === 'activitybar') { + location = 'left'; + } + + if (contributions.viewsContainers![location]) { + contributions.viewsContainers![location] = contributions.viewsContainers![location].concat(containers); + } else { + contributions.viewsContainers![location] = containers; + } + }); + } + + if (rawPlugin.contributes!.views) { + contributions.views = {}; + + Object.keys(rawPlugin.contributes.views!).forEach(location => { + const views = this.readViews(rawPlugin.contributes!.views![location]); + contributions.views![location] = views; + }); + } + return contributions; } - private readLanguages(rawLanguages: PluginPackageLanguageContribution[], pluginPath: string): LanguageContribution[] { - const result = new Array(); - for (const lang of rawLanguages) { - result.push(this.readLanguage(lang, pluginPath)); - } + private readViewsContainers(rawViewsContainers: PluginPackageViewContainer[], pluginPath: string): ViewContainer[] { + return rawViewsContainers.map(rawViewContainer => this.readViewContainer(rawViewContainer, pluginPath)); + } + + private readViewContainer(rawViewContainer: PluginPackageViewContainer, pluginPath: string): ViewContainer { + const result: ViewContainer = { + id: rawViewContainer.id, + title: rawViewContainer.title, + icon: rawViewContainer.icon + }; + return result; } + private readViews(rawViews: PluginPackageView[]): View[] { + return rawViews.map(rawView => this.readView(rawView)); + } + + private readView(rawView: PluginPackageView): View { + const result: View = { + id: rawView.id, + name: rawView.name + }; + + return result; + } + + private readLanguages(rawLanguages: PluginPackageLanguageContribution[], pluginPath: string): LanguageContribution[] { + return rawLanguages.map(language => this.readLanguage(language, pluginPath)); + } + private readLanguage(rawLang: PluginPackageLanguageContribution, pluginPath: string): LanguageContribution { // TODO: add validation to all parameters const result: LanguageContribution = { @@ -232,6 +286,7 @@ function isCharacterPair(something: CharacterPair): boolean { && something.length === 2 ); } + function isStringArr(something: string[]): boolean { if (!Array.isArray(something)) { return false; diff --git a/packages/plugin-ext/src/hosted/plugin/hosted-plugin-manager.ts b/packages/plugin-ext/src/hosted/plugin/hosted-plugin-manager.ts deleted file mode 100644 index 6cdce9b08b30d..0000000000000 --- a/packages/plugin-ext/src/hosted/plugin/hosted-plugin-manager.ts +++ /dev/null @@ -1,50 +0,0 @@ -/******************************************************************************** - * Copyright (C) 2018 Red Hat, Inc. 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 - ********************************************************************************/ - -import { HostedPluginManagerExt, Plugin } from '../../api/plugin-api'; -import { getPluginId, PluginMetadata } from '../../common/plugin-protocol'; - -export interface PluginHost { - initialize(contextPath: string, pluginMetadata: PluginMetadata): void; - - loadPlugin(contextPath: string, plugin: Plugin): void; - - stopPlugins(contextPath: string, pluginIds: string[]): void; -} - -export class HostedPluginManagerExtImpl implements HostedPluginManagerExt { - - private runningPluginIds: string[]; - - constructor(private readonly host: PluginHost) { - this.runningPluginIds = []; - } - - $initialize(contextPath: string, pluginMetadata: PluginMetadata): void { - this.host.initialize(contextPath, pluginMetadata); - } - - $loadPlugin(contextPath: string, plugin: Plugin): void { - this.runningPluginIds.push(getPluginId(plugin.model)); - this.host.loadPlugin(contextPath, plugin); - } - - $stopPlugin(contextPath: string): PromiseLike { - this.host.stopPlugins(contextPath, this.runningPluginIds); - return Promise.resolve(); - } - -} diff --git a/packages/plugin-ext/src/main/browser/languages-main.ts b/packages/plugin-ext/src/main/browser/languages-main.ts index 997a6e505558a..b40eb8106cf3b 100644 --- a/packages/plugin-ext/src/main/browser/languages-main.ts +++ b/packages/plugin-ext/src/main/browser/languages-main.ts @@ -1,4 +1,3 @@ - /******************************************************************************** * Copyright (C) 2018 Red Hat, Inc. and others. * @@ -23,9 +22,10 @@ import { MAIN_RPC_CONTEXT, LanguagesExt } from '../../api/plugin-api'; -import { SerializedDocumentFilter } from '../../api/model'; +import { SerializedDocumentFilter, MarkerData } from '../../api/model'; import { RPCProtocol } from '../../api/rpc-protocol'; import { fromLanguageSelector } from '../../plugin/type-converters'; +import { UriComponents } from '@theia/plugin-ext/src/common/uri-components'; export class LanguagesMainImpl implements LanguagesMain { @@ -80,8 +80,58 @@ export class LanguagesMainImpl implements LanguagesMain { ? (model, position, suggestion, token) => Promise.resolve(this.proxy.$resolveCompletionItem(handle, model.uri, position, suggestion)) : undefined })); + } + + $clearDiagnostics(id: string): void { + const markers = monaco.editor.getModelMarkers({ owner: id }); + const clearedEditors = new Set(); // uri to resource + for (const marker of markers) { + const uri = marker.resource; + const uriString = uri.toString(); + if (!clearedEditors.has(uriString)) { + const textModel = monaco.editor.getModel(uri); + monaco.editor.setModelMarkers(textModel, id, []); + clearedEditors.add(uriString); + } + } + } + + $changeDiagnostics(id: string, delta: [UriComponents, MarkerData[]][]): void { + for (const [uriComponents, markers] of delta) { + const uri = monaco.Uri.revive(uriComponents); + const textModel = monaco.editor.getModel(uri); + monaco.editor.setModelMarkers(textModel, id, markers.map(reviveMarker)); + } + } +} +function reviveMarker(marker: MarkerData): monaco.editor.IMarkerData { + const monacoMarker: monaco.editor.IMarkerData = { + code: marker.code, + severity: marker.severity, + message: marker.message, + source: marker.source, + startLineNumber: marker.startLineNumber, + startColumn: marker.startColumn, + endLineNumber: marker.endLineNumber, + endColumn: marker.endColumn, + relatedInformation: undefined + }; + if (marker.relatedInformation) { + monacoMarker.relatedInformation = []; + for (const ri of marker.relatedInformation) { + monacoMarker.relatedInformation.push({ + resource: monaco.Uri.revive(ri.resource), + message: ri.message, + startLineNumber: ri.startLineNumber, + startColumn: ri.startColumn, + endLineNumber: ri.endLineNumber, + endColumn: ri.endColumn + }); + } } + + return monacoMarker; } function reviveRegExp(regExp?: SerializedRegExp): RegExp | undefined { diff --git a/packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts b/packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts index 80c8185fe203f..6b321552acccf 100644 --- a/packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts +++ b/packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts @@ -18,6 +18,7 @@ import { injectable, inject } from 'inversify'; import { PluginContribution, IndentationRules, FoldingRules, ScopeMap } from '../../common'; import { TextmateRegistry, getEncodedLanguageId } from '@theia/monaco/lib/browser/textmate'; import { ITokenTypeMap, IEmbeddedLanguagesMap, StandardTokenType } from 'monaco-textmate'; +import { ViewRegistry } from './view/view-registry'; @injectable() export class PluginContributionHandler { @@ -26,9 +27,9 @@ export class PluginContributionHandler { @inject(TextmateRegistry) private readonly grammarsRegistry: TextmateRegistry; - constructor() { - } + @inject(ViewRegistry) + private readonly viewRegistry: ViewRegistry; handleContributions(contributions: PluginContribution): void { if (contributions.languages) { @@ -69,7 +70,7 @@ export class PluginContributionHandler { } } - this.grammarsRegistry.registerTextMateGrammarScope(grammar.scope, { + this.grammarsRegistry.registerTextmateGrammarScope(grammar.scope, { async getGrammarDefinition() { return { format: grammar.format, @@ -88,6 +89,24 @@ export class PluginContributionHandler { } } } + + if (contributions.viewsContainers) { + for (const location in contributions.viewsContainers) { + if (contributions.viewsContainers!.hasOwnProperty(location)) { + const viewContainers = contributions.viewsContainers[location]; + viewContainers.forEach(container => this.viewRegistry.registerViewContainer(location, container)); + } + } + } + + if (contributions.views) { + for (const location in contributions.views) { + if (contributions.views.hasOwnProperty(location)) { + const views = contributions.views[location]; + views.forEach(view => this.viewRegistry.registerView(location, view)); + } + } + } } private createRegex(value: string | undefined): RegExp | undefined { diff --git a/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts b/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts index b6c3cae780ddf..2a86281e3f322 100644 --- a/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts +++ b/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts @@ -20,13 +20,11 @@ import { ContainerModule } from 'inversify'; import { FrontendApplicationContribution, FrontendApplication, WidgetFactory, bindViewContribution } from '@theia/core/lib/browser'; import { MaybePromise, CommandContribution, ResourceResolver } from '@theia/core/lib/common'; import { WebSocketConnectionProvider } from '@theia/core/lib/browser/messaging'; -import { PluginWorker } from './plugin-worker'; import { HostedPluginSupport } from '../../hosted/browser/hosted-plugin'; import { HostedPluginWatcher } from '../../hosted/browser/hosted-plugin-watcher'; import { HostedPluginLogViewer } from '../../hosted/browser/hosted-plugin-log-viewer'; import { HostedPluginManagerClient } from '../../hosted/browser/hosted-plugin-manager-client'; import { PluginApiFrontendContribution } from './plugin-frontend-contribution'; -import { setUpPluginApi } from './main-context'; import { HostedPluginServer, hostedServicePath, PluginServer, pluginServerJsonRpcPath } from '../../common/plugin-protocol'; import { ModalNotification } from './dialogs/modal-notification'; import { PluginWidget } from './plugin-ext-widget'; @@ -42,13 +40,13 @@ import { TextEditorService, TextEditorServiceImpl } from './text-editor-service' import { EditorModelService, EditorModelServiceImpl } from './text-editor-model-service'; import { UntitledResourceResolver } from './editor/untitled-resource'; import { PluginContributionHandler } from './plugin-contribution-handler'; +import { ViewRegistry } from './view/view-registry'; export default new ContainerModule(bind => { bindHostedPluginPreferences(bind); bind(ModalNotification).toSelf().inSingletonScope(); - bind(PluginWorker).toSelf().inSingletonScope(); bind(HostedPluginSupport).toSelf().inSingletonScope(); bind(HostedPluginWatcher).toSelf().inSingletonScope(); bind(HostedPluginLogViewer).toSelf().inSingletonScope(); @@ -68,9 +66,6 @@ export default new ContainerModule(bind => { bind(FrontendApplicationContribution).toDynamicValue(ctx => ({ onStart(app: FrontendApplication): MaybePromise { - const worker = ctx.container.get(PluginWorker); - - setUpPluginApi(worker.rpc, ctx.container); ctx.container.get(HostedPluginSupport).checkAndLoadPlugin(ctx.container); } })); @@ -94,6 +89,7 @@ export default new ContainerModule(bind => { return provider.createProxy(pluginServerJsonRpcPath); }).inSingletonScope(); - bind(PluginContributionHandler).toSelf().inSingletonScope(); + bind(ViewRegistry).toSelf().inSingletonScope(); + bind(PluginContributionHandler).toSelf().inSingletonScope(); }); diff --git a/packages/plugin-ext/src/main/browser/style/index.css b/packages/plugin-ext/src/main/browser/style/index.css index 2c998a8a5133a..f1cea7c3d480b 100644 --- a/packages/plugin-ext/src/main/browser/style/index.css +++ b/packages/plugin-ext/src/main/browser/style/index.css @@ -51,3 +51,4 @@ } @import './plugin-sidebar.css'; +@import './view-registry.css' diff --git a/packages/plugin-ext/src/main/browser/style/view-registry.css b/packages/plugin-ext/src/main/browser/style/view-registry.css new file mode 100644 index 0000000000000..040788e886968 --- /dev/null +++ b/packages/plugin-ext/src/main/browser/style/view-registry.css @@ -0,0 +1,106 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. 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 + ********************************************************************************/ + +.theia-views-container { + width: 100%; + height: 100%; + overflow: hidden; + background: var(--theia-layout-color0); +} + +.theia-views-container:focus { + border: none; + outline: none; +} + +.theia-views-container-title { + height: 26px; + line-height: 26px; + padding: 0px 10px; + color: var(--theia-ui-font-color2); + font-size: var(--theia-ui-font-size0); + text-transform: uppercase; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.theia-views-container-section { + width: 100%; +} + +.theia-views-container-section-title { + position: relative; + height: 22px; + background-color: var(--theia-layout-color2); + line-height: 22px; + overflow: hidden; + color: var(--theia-ui-font-color1); + font-size: var(--theia-ui-font-size0); + text-transform: uppercase; + cursor: pointer; + user-select: none; +} + +.theia-views-container-section-title:hover { + background-color: var(--theia-layout-color1); +} + +.theia-views-container-section-control { + position: absolute; + left: 0px; + top: 0px; + width: 15px; + height: 22px; + padding: 0px 5px; + box-sizing: border-box; +} + +.theia-views-container-section-control[role='opened']:before { + font-family: FontAwesome; + font-size: calc(var(--theia-content-font-size) * 0.8); + content: "\F0D7"; +} + +.theia-views-container-section-control[role='closed']:before { + font-family: FontAwesome; + font-size: calc(var(--theia-content-font-size) * 0.8); + content: "\F0DA"; +} + +.theia-views-container-section-label { + position: absolute; + left: 15px; + right: 0px; + top: 0px; + height: 22px; + line-height: 22px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.theia-views-container-section-content { + min-height: 30px; + text-align: center; + color: var(--theia-ui-font-color3); + font-size: var(--theia-ui-font-size3); + padding: 20px 0px; +} + +.theia-views-container-section-content[role='closed'] { + display: none; +} diff --git a/packages/plugin-ext/src/main/browser/view/view-registry.ts b/packages/plugin-ext/src/main/browser/view/view-registry.ts new file mode 100644 index 0000000000000..eec8a9e3bbde7 --- /dev/null +++ b/packages/plugin-ext/src/main/browser/view/view-registry.ts @@ -0,0 +1,100 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. 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 + ********************************************************************************/ + +import { injectable, inject, postConstruct } from 'inversify'; +import { ViewContainer, View } from '../../../common'; +import { ApplicationShell } from '@theia/core/lib/browser'; +import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; +import { Widget } from '@theia/core/lib/browser/widgets/widget'; +import { ViewsContainerWidget } from './views-container-widget'; + +export interface ViewContainerRegistry { + container: ViewContainer; + area: ApplicationShell.Area; + views: View[] +} + +@injectable() +export class ViewRegistry { + + @inject(ApplicationShell) + protected applicationShell: ApplicationShell; + + @inject(FrontendApplicationStateService) + protected applicationStateService: FrontendApplicationStateService; + + containers: ViewContainerRegistry[] = new Array(); + + @postConstruct() + init() { + this.applicationStateService.reachedState('ready').then(() => { + this.showContainers(); + }); + } + + getArea(location: string): ApplicationShell.Area { + switch (location) { + case 'right': return'right'; + case 'bottom': return 'bottom'; + case 'top': return 'top'; + } + + return 'left'; + } + + registerViewContainer(location: string, viewContainer: ViewContainer) { + const registry: ViewContainerRegistry = { + container: viewContainer, + area: this.getArea(location), + views: [] + }; + this.containers.push(registry); + } + + registerView(location: string, view: View) { + this.containers.forEach(containerRegistry => { + if (location === containerRegistry.container.id) { + containerRegistry.views.push(view); + } + }); + } + + private showContainers() { + // Remember the currently active widget + const activeWidget: Widget | undefined = this.applicationShell.activeWidget; + + // Show views containers + this.containers.forEach(registry => { + const widget = new ViewsContainerWidget(registry.container, registry.views); + const tabBar = this.applicationShell.getTabBarFor(widget); + // const area = this.applicationShell.getAreaFor(widget); + + if (!tabBar) { + const widgetArgs: ApplicationShell.WidgetOptions = { + area: registry.area + }; + + this.applicationShell.addWidget(widget, widgetArgs); + } + }); + + // Restore active widget + if (activeWidget) { + this.applicationShell.activateWidget(activeWidget.id); + } + } + +} diff --git a/packages/plugin-ext/src/main/browser/view/views-container-widget.tsx b/packages/plugin-ext/src/main/browser/view/views-container-widget.tsx new file mode 100644 index 0000000000000..89fa57bcc2d5c --- /dev/null +++ b/packages/plugin-ext/src/main/browser/view/views-container-widget.tsx @@ -0,0 +1,78 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. 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 + ********************************************************************************/ + +import { ViewContainer, View } from '../../../common'; +import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget'; +import * as React from 'react'; + +export class ViewContainerSection extends React.Component { + + constructor(protected view: View) { + super(view); + + this.state = {opened: 'opened'}; + } + + public handleOnClick() { + if ('opened' === this.state.opened) { + this.setState({opened: 'closed'}); + } else { + this.setState({opened: 'opened'}); + } + } + + public render() { + const title = +
this.handleOnClick()}> +
+
{this.view.name}
+
; + + const content = +
{this.props.name}
; + + return
+ {title} + {content} +
; + } +} + +export class ViewsContainerWidget extends ReactWidget { + + constructor(protected viewContainer: ViewContainer, + protected views: View[]) { + super(); + + this.id = `views-container-widget-${viewContainer.id}`; + this.title.closable = true; + this.title.caption = this.title.label = viewContainer.title; + + this.addClass('theia-views-container'); + + this.update(); + } + + protected render(): React.ReactNode { + const list = this.views.map(view => ); + + return +
{this.viewContainer.title}
+ {...list} +
; + } + +} diff --git a/packages/plugin-ext/src/main/node/handlers/plugin-theia-file-handler.ts b/packages/plugin-ext/src/main/node/handlers/plugin-theia-file-handler.ts index 30ae96aa48222..0554b539790e8 100644 --- a/packages/plugin-ext/src/main/node/handlers/plugin-theia-file-handler.ts +++ b/packages/plugin-ext/src/main/node/handlers/plugin-theia-file-handler.ts @@ -16,7 +16,7 @@ import { PluginDeployerFileHandler, PluginDeployerEntry, PluginDeployerFileHandlerContext } from '../../../common/plugin-protocol'; import { injectable } from 'inversify'; -import * as os from 'os'; +import { getTempDir } from '../temp-dir-util'; import * as path from 'path'; @injectable() @@ -24,7 +24,7 @@ export class PluginTheiaFileHandler implements PluginDeployerFileHandler { private unpackedFolder: string; constructor() { - this.unpackedFolder = path.resolve(os.tmpdir(), 'theia-unpacked'); + this.unpackedFolder = getTempDir('theia-unpacked'); } accept(resolvedPlugin: PluginDeployerEntry): boolean { diff --git a/packages/plugin-ext/src/main/node/temp-dir-util.ts b/packages/plugin-ext/src/main/node/temp-dir-util.ts new file mode 100644 index 0000000000000..040da6a508d73 --- /dev/null +++ b/packages/plugin-ext/src/main/node/temp-dir-util.ts @@ -0,0 +1,27 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. 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 + ********************************************************************************/ +import * as os from 'os'; +import * as path from 'path'; +import * as fs from 'fs'; + +export function getTempDir(name: string): string { + let tempDir = os.tmpdir(); + // for mac os 'os.tmpdir()' return symlink, but we need real path + if (process.platform === 'darwin') { + tempDir = fs.realpathSync(tempDir); + } + return path.resolve(tempDir, name); +} diff --git a/packages/plugin-ext/src/plugin/languages.ts b/packages/plugin-ext/src/plugin/languages.ts index 86db374ab512e..f855ae3a9172b 100644 --- a/packages/plugin-ext/src/plugin/languages.ts +++ b/packages/plugin-ext/src/plugin/languages.ts @@ -27,13 +27,13 @@ import { import { RPCProtocol } from '../api/rpc-protocol'; import * as theia from '@theia/plugin'; import { DocumentsExtImpl } from './documents'; -import { Disposable, CompletionList, Range, SnippetString } from './types-impl'; +import { Disposable } from './types-impl'; import URI from 'vscode-uri/lib/umd'; import { match as matchGlobPattern } from '../common/glob'; import { UriComponents } from '../common/uri-components'; -import { CompletionContext, CompletionResultDto, Completion, SerializedDocumentFilter, CompletionDto } from '../api/model'; -import * as Converter from './type-converters'; -import { mixin } from '../common/types'; +import { CompletionContext, CompletionResultDto, Completion, SerializedDocumentFilter } from '../api/model'; +import { CompletionAdapter } from './languages/completion'; +import { Diagnostics } from './languages/diagnostics'; type Adapter = CompletionAdapter; @@ -41,11 +41,18 @@ export class LanguagesExtImpl implements LanguagesExt { private proxy: LanguagesMain; + private diagnostics: Diagnostics; + private callId = 0; private adaptersMap = new Map(); constructor(rpc: RPCProtocol, private readonly documents: DocumentsExtImpl) { this.proxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.LANGUAGES_MAIN); + this.diagnostics = new Diagnostics(rpc); + } + + get onDidChangeDiagnostics() { + return this.diagnostics.onDidChangeDiagnostics; } getLanguages(): Promise { @@ -74,7 +81,6 @@ export class LanguagesExtImpl implements LanguagesExt { this.proxy.$setLanguageConfiguration(callId, language, config); return this.createDisposable(callId); - } private nextCallId(): number { @@ -150,6 +156,15 @@ export class LanguagesExtImpl implements LanguagesExt { } // ### Completion end + // ### Diagnostics begin + getDiagnostics(resource?: URI): theia.Diagnostic[] | [URI, theia.Diagnostic[]][] { + return this.diagnostics.getDiagnostics(resource); + } + + createDiagnosticCollection(name?: string): theia.DiagnosticCollection { + return this.diagnostics.createDiagnosticCollection(name); + } + // ### Diagnostics end } function serializeEnterRules(rules?: theia.OnEnterRule[]): SerializedOnEnterRule[] | undefined { @@ -274,145 +289,3 @@ export function score(selector: LanguageSelector | undefined, candidateUri: URI, return 0; } } - -class CompletionAdapter { - private cacheId = 0; - private cache = new Map(); - - constructor(private readonly delegate: theia.CompletionItemProvider, - private readonly documents: DocumentsExtImpl) { - - } - - provideCompletionItems(resource: URI, position: Position, context: CompletionContext): Promise { - const document = this.documents.getDocumentData(resource); - if (!document) { - return Promise.reject(new Error(`There are no document for ${resource}`)); - } - - const doc = document.document; - - const pos = Converter.toPosition(position); - return Promise.resolve(this.delegate.provideCompletionItems(doc, pos, undefined, context)).then(value => { - const id = this.cacheId++; - const result: CompletionResultDto = { - id, - completions: [], - }; - - let list: CompletionList; - if (!value) { - return undefined; - } else if (Array.isArray(value)) { - list = new CompletionList(value); - } else { - list = value; - result.incomplete = list.isIncomplete; - } - - const wordRangeBeforePos = (doc.getWordRangeAtPosition(pos) as Range || new Range(pos, pos)) - .with({ end: pos }); - - for (let i = 0; i < list.items.length; i++) { - const suggestion = this.convertCompletionItem(list.items[i], pos, wordRangeBeforePos, i, id); - if (suggestion) { - result.completions.push(suggestion); - } - } - this.cache.set(id, list.items); - - return result; - }); - } - - resolveCompletionItem(resource: URI, position: Position, completion: Completion): Promise { - - if (typeof this.delegate.resolveCompletionItem !== 'function') { - return Promise.resolve(completion); - } - - const { parentId, id } = (completion); - const item = this.cache.has(parentId) && this.cache.get(parentId)![id]; - if (!item) { - return Promise.resolve(completion); - } - - return Promise.resolve(this.delegate.resolveCompletionItem(item, undefined)).then(resolvedItem => { - - if (!resolvedItem) { - return completion; - } - - const doc = this.documents.getDocumentData(resource)!.document; - const pos = Converter.toPosition(position); - const wordRangeBeforePos = (doc.getWordRangeAtPosition(pos) as Range || new Range(pos, pos)).with({ end: pos }); - const newCompletion = this.convertCompletionItem(resolvedItem, pos, wordRangeBeforePos, id, parentId); - if (newCompletion) { - mixin(completion, newCompletion, true); - } - - return completion; - }); - } - - releaseCompletionItems(id: number) { - this.cache.delete(id); - return Promise.resolve(); - } - - private convertCompletionItem(item: theia.CompletionItem, position: theia.Position, defaultRange: theia.Range, id: number, parentId: number): CompletionDto | undefined { - if (typeof item.label !== 'string' || item.label.length === 0) { - console.warn('Invalid Completion Item -> must have at least a label'); - return undefined; - } - - const result: CompletionDto = { - id, - parentId, - label: item.label, - type: Converter.fromCompletionItemKind(item.kind), - detail: item.detail, - documentation: item.documentation, - filterText: item.filterText, - sortText: item.sortText, - preselect: item.preselect, - insertText: '', - additionalTextEdits: item.additionalTextEdits && item.additionalTextEdits.map(Converter.fromTextEdit), - command: undefined, // TODO: implement this: this.commands.toInternal(item.command), - commitCharacters: item.commitCharacters - }; - - if (typeof item.insertText === 'string') { - result.insertText = item.insertText; - result.snippetType = 'internal'; - - } else if (item.insertText instanceof SnippetString) { - result.insertText = item.insertText.value; - result.snippetType = 'textmate'; - - } else { - result.insertText = item.label; - result.snippetType = 'internal'; - } - - let range: theia.Range; - if (item.range) { - range = item.range; - } else { - range = defaultRange; - } - result.overwriteBefore = position.character - range.start.character; - result.overwriteAfter = range.end.character - position.character; - - if (!range.isSingleLine || range.start.line !== position.line) { - console.warn('Invalid Completion Item -> must be single line and on the same line'); - return undefined; - } - - return result; - } - - static hasResolveSupport(provider: theia.CompletionItemProvider): boolean { - return typeof provider.resolveCompletionItem === 'function'; - } -} diff --git a/packages/plugin-ext/src/plugin/languages/completion.ts b/packages/plugin-ext/src/plugin/languages/completion.ts new file mode 100644 index 0000000000000..193c7f71ccd55 --- /dev/null +++ b/packages/plugin-ext/src/plugin/languages/completion.ts @@ -0,0 +1,166 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. 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 + ********************************************************************************/ + +import URI from 'vscode-uri/lib/umd'; +import * as theia from '@theia/plugin'; +import { CompletionList, Range, SnippetString } from '../types-impl'; +import { DocumentsExtImpl } from '../documents'; +import * as Converter from '../type-converters'; +import { mixin } from '../../common/types'; +import { Position } from '../../api/plugin-api'; +import { CompletionContext, CompletionResultDto, Completion, CompletionDto } from '../../api/model'; + +export class CompletionAdapter { + private cacheId = 0; + private cache = new Map(); + + constructor(private readonly delegate: theia.CompletionItemProvider, + private readonly documents: DocumentsExtImpl) { + + } + + provideCompletionItems(resource: URI, position: Position, context: CompletionContext): Promise { + const document = this.documents.getDocumentData(resource); + if (!document) { + return Promise.reject(new Error(`There are no document for ${resource}`)); + } + + const doc = document.document; + + const pos = Converter.toPosition(position); + return Promise.resolve(this.delegate.provideCompletionItems(doc, pos, undefined, context)).then(value => { + const id = this.cacheId++; + const result: CompletionResultDto = { + id, + completions: [], + }; + + let list: CompletionList; + if (!value) { + return undefined; + } else if (Array.isArray(value)) { + list = new CompletionList(value); + } else { + list = value; + result.incomplete = list.isIncomplete; + } + + const wordRangeBeforePos = (doc.getWordRangeAtPosition(pos) as Range || new Range(pos, pos)) + .with({ end: pos }); + + for (let i = 0; i < list.items.length; i++) { + const suggestion = this.convertCompletionItem(list.items[i], pos, wordRangeBeforePos, i, id); + if (suggestion) { + result.completions.push(suggestion); + } + } + this.cache.set(id, list.items); + + return result; + }); + } + + resolveCompletionItem(resource: URI, position: Position, completion: Completion): Promise { + + if (typeof this.delegate.resolveCompletionItem !== 'function') { + return Promise.resolve(completion); + } + + const { parentId, id } = (completion); + const item = this.cache.has(parentId) && this.cache.get(parentId)![id]; + if (!item) { + return Promise.resolve(completion); + } + + return Promise.resolve(this.delegate.resolveCompletionItem(item, undefined)).then(resolvedItem => { + + if (!resolvedItem) { + return completion; + } + + const doc = this.documents.getDocumentData(resource)!.document; + const pos = Converter.toPosition(position); + const wordRangeBeforePos = (doc.getWordRangeAtPosition(pos) as Range || new Range(pos, pos)).with({ end: pos }); + const newCompletion = this.convertCompletionItem(resolvedItem, pos, wordRangeBeforePos, id, parentId); + if (newCompletion) { + mixin(completion, newCompletion, true); + } + + return completion; + }); + } + + releaseCompletionItems(id: number) { + this.cache.delete(id); + return Promise.resolve(); + } + + private convertCompletionItem(item: theia.CompletionItem, position: theia.Position, defaultRange: theia.Range, id: number, parentId: number): CompletionDto | undefined { + if (typeof item.label !== 'string' || item.label.length === 0) { + console.warn('Invalid Completion Item -> must have at least a label'); + return undefined; + } + + const result: CompletionDto = { + id, + parentId, + label: item.label, + type: Converter.fromCompletionItemKind(item.kind), + detail: item.detail, + documentation: item.documentation, + filterText: item.filterText, + sortText: item.sortText, + preselect: item.preselect, + insertText: '', + additionalTextEdits: item.additionalTextEdits && item.additionalTextEdits.map(Converter.fromTextEdit), + command: undefined, // TODO: implement this: this.commands.toInternal(item.command), + commitCharacters: item.commitCharacters + }; + + if (typeof item.insertText === 'string') { + result.insertText = item.insertText; + result.snippetType = 'internal'; + + } else if (item.insertText instanceof SnippetString) { + result.insertText = item.insertText.value; + result.snippetType = 'textmate'; + + } else { + result.insertText = item.label; + result.snippetType = 'internal'; + } + + let range: theia.Range; + if (item.range) { + range = item.range; + } else { + range = defaultRange; + } + result.overwriteBefore = position.character - range.start.character; + result.overwriteAfter = range.end.character - position.character; + + if (!range.isSingleLine || range.start.line !== position.line) { + console.warn('Invalid Completion Item -> must be single line and on the same line'); + return undefined; + } + + return result; + } + + static hasResolveSupport(provider: theia.CompletionItemProvider): boolean { + return typeof provider.resolveCompletionItem === 'function'; + } +} diff --git a/packages/plugin-ext/src/plugin/languages/diagnostics.ts b/packages/plugin-ext/src/plugin/languages/diagnostics.ts new file mode 100644 index 0000000000000..7de7189832a5b --- /dev/null +++ b/packages/plugin-ext/src/plugin/languages/diagnostics.ts @@ -0,0 +1,324 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. 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 + ********************************************************************************/ + +import * as theia from '@theia/plugin'; +import { Event, Emitter } from '@theia/core/lib/common/event'; +import { convertDiagnosticToMarkerData } from '../type-converters'; +import { DiagnosticSeverity, MarkerSeverity } from '../types-impl'; +import { MarkerData } from '../../api/model'; +import { RPCProtocol } from '../../api/rpc-protocol'; +import { PLUGIN_RPC_CONTEXT, LanguagesMain } from '../../api/plugin-api'; +import URI from 'vscode-uri'; + +export class DiagnosticCollection implements theia.DiagnosticCollection { + private static DIAGNOSTICS_PRIORITY = [ + DiagnosticSeverity.Error, DiagnosticSeverity.Warning, DiagnosticSeverity.Information, DiagnosticSeverity.Hint + ]; + + private collectionName: string; + private diagnosticsLimitPerResource: number; + private proxy: LanguagesMain; + private onDidChangeDiagnosticsEmitter: Emitter; + + private diagnostics: Map; // uri -> diagnostics + private isDisposed: boolean; + private onDisposeCallback: (() => void) | undefined; + + constructor(name: string, maxCountPerFile: number, proxy: LanguagesMain, onDidChangeDiagnosticsEmitter: Emitter) { + this.collectionName = name; + this.diagnosticsLimitPerResource = maxCountPerFile; + this.proxy = proxy; + this.onDidChangeDiagnosticsEmitter = onDidChangeDiagnosticsEmitter; + + this.diagnostics = new Map(); + this.isDisposed = false; + this.onDisposeCallback = undefined; + } + + get name(): string { + return this.collectionName; + } + + set(uri: theia.Uri, diagnostics: theia.Diagnostic[] | undefined): void; + set(entries: [theia.Uri, theia.Diagnostic[] | undefined][]): void; + set(arg: theia.Uri | [theia.Uri, theia.Diagnostic[] | undefined][], diagnostics?: theia.Diagnostic[] | undefined) { + this.ensureNotDisposed(); + + if (arg instanceof URI) { + this.setDiagnosticsForUri(arg, diagnostics); + } else if (!arg) { + this.clear(); + } else if (arg instanceof Array) { + this.setDiagnostics(arg); + } + } + + private setDiagnosticsForUri(uri: URI, diagnostics?: theia.Diagnostic[]): void { + if (!diagnostics) { + this.diagnostics.delete(uri.toString()); + } else { + this.diagnostics.set(uri.toString(), diagnostics); + } + this.fireDiagnosticChangeEvent(uri); + this.sendChangesToEditor([uri]); + } + + private setDiagnostics(entries: [URI, theia.Diagnostic[] | undefined][]): void { + const delta: URI[] = []; + + // clear old diagnostics for given resources + for (const [uri] of entries) { + this.diagnostics.delete(uri.toString()); + } + + for (const [uri, diagnostics] of entries) { + const uriString = uri.toString(); + + if (!diagnostics) { + // clear existed + this.diagnostics.delete(uriString); + delta.push(uri); + } else { + // merge with existed if any + const existedDiagnostics = this.diagnostics.get(uriString); + if (existedDiagnostics) { + existedDiagnostics.push(...diagnostics); + } else { + this.diagnostics.set(uriString, diagnostics); + } + } + + if (delta.indexOf(uri) === -1) { + delta.push(uri); + } + } + + this.fireDiagnosticChangeEvent(delta); + this.sendChangesToEditor(delta); + } + + delete(uri: URI): void { + if (this.has(uri)) { + this.fireDiagnosticChangeEvent(uri); + this.diagnostics.delete(uri.toString()); + this.proxy.$changeDiagnostics(this.name, [[uri, []]]); + } + } + + clear(): void { + this.ensureNotDisposed(); + this.fireDiagnosticChangeEvent(this.getAllResourcesUris()); + this.diagnostics.clear(); + this.proxy.$clearDiagnostics(this.name); + } + + // tslint:disable-next-line:no-any + forEach(callback: (uri: URI, diagnostics: theia.Diagnostic[], collection: theia.DiagnosticCollection) => any, thisArg?: any): void { + this.ensureNotDisposed(); + this.diagnostics.forEach((diagnostics, uriString) => { + const uri = URI.parse(uriString); + callback.apply(thisArg, [uri, this.getDiagnosticsByUri(uri), this]); + }); + } + + get(uri: URI): theia.Diagnostic[] | undefined { + this.ensureNotDisposed(); + return this.getDiagnosticsByUri(uri); + } + + has(uri: URI): boolean { + this.ensureNotDisposed(); + return (this.diagnostics.get(uri.toString()) instanceof Array); + } + + dispose(): void { + if (!this.isDisposed) { + if (this.onDisposeCallback) { + this.onDisposeCallback(); + } + this.clear(); + this.isDisposed = true; + } + } + + setOnDisposeCallback(onDisposeCallback: (() => void) | undefined) { + this.onDisposeCallback = onDisposeCallback; + } + + private ensureNotDisposed(): void { + if (this.isDisposed) { + throw new Error('Diagnostic collection with name "' + this.name + '" is already disposed.'); + } + } + + private getAllResourcesUris(): string[] { + const resourcesUris: string[] = []; + this.diagnostics.forEach((diagnostics, uri) => resourcesUris.push(uri)); + return resourcesUris; + } + + private getDiagnosticsByUri(uri: URI): theia.Diagnostic[] | undefined { + const diagnostics = this.diagnostics.get(uri.toString()); + return (diagnostics instanceof Array) ? Object.freeze(diagnostics) : undefined; + } + + private fireDiagnosticChangeEvent(arg: string | string[] | URI | URI[]): void { + this.onDidChangeDiagnosticsEmitter.fire({ uris: this.toUrisArray(arg) }); + } + + private toUrisArray(arg: string | string[] | URI | URI[]): URI[] { + if (arg instanceof Array) { + if (arg.length === 0) { + return []; + } + + if (arg[0] instanceof URI) { + return arg as URI[]; + } else { + const result: URI[] = []; + for (const uriString of arg as string[]) { + result.push(URI.parse(uriString)); + } + return result; + } + } else { + if (arg instanceof URI) { + return [arg]; + } else { + return [URI.parse(arg)]; + } + } + } + + private sendChangesToEditor(uris: URI[]): void { + const markers: [URI, MarkerData[]][] = []; + nextUri: + for (const uri of uris) { + const uriMarkers: MarkerData[] = []; + const uriDiagnostics = this.diagnostics.get(uri.toString()); + if (uriDiagnostics) { + if (uriDiagnostics.length > this.diagnosticsLimitPerResource) { + for (const severity of DiagnosticCollection.DIAGNOSTICS_PRIORITY) { + for (const diagnostic of uriDiagnostics) { + if (severity === diagnostic.severity) { + if (uriMarkers.push(convertDiagnosticToMarkerData(diagnostic)) + 1 === this.diagnosticsLimitPerResource) { + const lastMarker = uriMarkers[uriMarkers.length - 1]; + uriMarkers.push({ + severity: MarkerSeverity.Info, + message: 'Limit of diagnostics is reached. ' + (uriDiagnostics.length - this.diagnosticsLimitPerResource) + ' items are hidden', + startLineNumber: lastMarker.startLineNumber, + startColumn: lastMarker.startColumn, + endLineNumber: lastMarker.endLineNumber, + endColumn: lastMarker.endColumn + }); + markers.push([uri, uriMarkers]); + continue nextUri; + } + } + } + } + } else { + uriDiagnostics.forEach(diagnostic => uriMarkers.push(convertDiagnosticToMarkerData(diagnostic))); + markers.push([uri, uriMarkers]); + } + } else { + markers.push([uri, []]); + } + } + + this.proxy.$changeDiagnostics(this.name, markers); + } +} + +export class Diagnostics { + public static MAX_DIAGNOSTICS_PER_FILE = 1000; + private static GENERATED_DIAGNOSTIC_COLLECTION_NAME_PREFIX = '_generated_diagnostic_collection_name_#'; + + private proxy: LanguagesMain; + private diagnosticCollections: Map; // id -> diagnostic colection + private nextId: number; + + private diagnosticsChangedEmitter = new Emitter(); + public readonly onDidChangeDiagnostics: Event = this.diagnosticsChangedEmitter.event; + + constructor(rpc: RPCProtocol) { + this.proxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.LANGUAGES_MAIN); + + this.diagnosticCollections = new Map(); + this.nextId = 0; + } + + getDiagnostics(resource?: URI): theia.Diagnostic[] | [URI, theia.Diagnostic[]][] { + if (resource) { + return this.getAllDiagnisticsForResource(resource); + } else { + return this.getAllDiagnostics(); + } + } + + createDiagnosticCollection(name?: string): theia.DiagnosticCollection { + if (!name) { + do { + name = Diagnostics.GENERATED_DIAGNOSTIC_COLLECTION_NAME_PREFIX + this.getNextId(); + } while (this.diagnosticCollections.has(name)); + } else if (this.diagnosticCollections.has(name)) { + console.warn(`Diagnostic collection with name '${name}' already exist.`); + } + + const diagnosticCollection = new DiagnosticCollection(name, Diagnostics.MAX_DIAGNOSTICS_PER_FILE, this.proxy, this.diagnosticsChangedEmitter); + diagnosticCollection.setOnDisposeCallback(() => { + this.diagnosticCollections.delete(name!); + }); + this.diagnosticCollections.set(name, diagnosticCollection); + return diagnosticCollection; + } + + private getNextId(): number { + return this.nextId++; + } + + private getAllDiagnisticsForResource(uri: URI): theia.Diagnostic[] { + let result: theia.Diagnostic[] = []; + this.diagnosticCollections.forEach(diagnosticCollection => { + const diagnostics = diagnosticCollection.get(uri); + if (diagnostics) { + result = result.concat(...diagnostics); + } + }); + return result; + } + + private getAllDiagnostics(): [URI, theia.Diagnostic[]][] { + const result: [URI, theia.Diagnostic[]][] = []; + // Holds uri index in result array of tuples. + const urisIndexes = new Map(); + let nextIndex = 0; + this.diagnosticCollections.forEach(diagnosticsCollection => + diagnosticsCollection.forEach((uri, diagnostics) => { + let uriIndex = urisIndexes.get(uri.toString()); + if (uriIndex === undefined) { + uriIndex = nextIndex++; + urisIndexes.set(uri.toString(), uriIndex); + result.push([uri, [...diagnostics]]); + } else { + result[uriIndex][1] = result[uriIndex][1].concat(...diagnostics); + } + }) + ); + return result; + } + +} diff --git a/packages/plugin-ext/src/plugin/plugin-context.ts b/packages/plugin-ext/src/plugin/plugin-context.ts index 650163b2186b3..4d245cde10d79 100644 --- a/packages/plugin-ext/src/plugin/plugin-context.ts +++ b/packages/plugin-ext/src/plugin/plugin-context.ts @@ -18,9 +18,8 @@ import { CommandRegistryImpl } from './command-registry'; import { Emitter } from '@theia/core/lib/common/event'; import { CancellationTokenSource } from '@theia/core/lib/common/cancellation'; import { QuickOpenExtImpl } from './quick-open'; -import { MAIN_RPC_CONTEXT, Plugin } from '../api/plugin-api'; +import { MAIN_RPC_CONTEXT, Plugin as InternalPlugin, PluginManager, PluginAPIFactory } from '../api/plugin-api'; import { RPCProtocol } from '../api/rpc-protocol'; -import { getPluginId } from '../common/plugin-protocol'; import { MessageRegistryExt } from './message-registry'; import { StatusBarMessageRegistryExt } from './status-bar-message-registry'; import { WindowStateExtImpl } from './window-state'; @@ -51,6 +50,11 @@ import { CompletionList, TextEdit, CompletionTriggerKind, + Diagnostic, + DiagnosticRelatedInformation, + DiagnosticSeverity, + DiagnosticTag, + Location } from './types-impl'; import { EditorsAndDocumentsExtImpl } from './editors-and-documents'; import { TextEditorsExtImpl } from './text-editors'; @@ -58,14 +62,13 @@ import { DocumentsExtImpl } from './documents'; import Uri from 'vscode-uri'; import { TextEditorCursorStyle } from '../common/editor-options'; import { PreferenceRegistryExtImpl } from './preference-registry'; -import URI from 'vscode-uri'; import { OutputChannelRegistryExt } from './output-channel-registry'; import { TerminalServiceExtImpl } from './terminal-ext'; import { LanguagesExtImpl, score } from './languages'; import { fromDocumentSelector } from './type-converters'; import { DialogsExtImpl } from './dialogs'; -export function createAPI(rpc: RPCProtocol): typeof theia { +export function createAPIFactory(rpc: RPCProtocol, pluginManager: PluginManager): PluginAPIFactory { const commandRegistryExt = rpc.set(MAIN_RPC_CONTEXT.COMMAND_REGISTRY_EXT, new CommandRegistryImpl(rpc)); const quickOpenExt = rpc.set(MAIN_RPC_CONTEXT.QUICK_OPEN_EXT, new QuickOpenExtImpl(rpc)); const dialogsExt = new DialogsExtImpl(rpc); @@ -82,260 +85,293 @@ export function createAPI(rpc: RPCProtocol): typeof theia { const outputChannelRegistryExt = new OutputChannelRegistryExt(rpc); const languagesExt = rpc.set(MAIN_RPC_CONTEXT.LANGUAGES_EXT, new LanguagesExtImpl(rpc, documents)); - const commands: typeof theia.commands = { - // tslint:disable-next-line:no-any - registerCommand(command: theia.Command, handler?: (...args: any[]) => T | Thenable): Disposable { - return commandRegistryExt.registerCommand(command, handler); - }, - // tslint:disable-next-line:no-any - executeCommand(commandId: string, ...args: any[]): PromiseLike { - return commandRegistryExt.executeCommand(commandId, args); - }, - // tslint:disable-next-line:no-any - registerTextEditorCommand(command: theia.Command, callback: (textEditor: theia.TextEditor, edit: theia.TextEditorEdit, ...arg: any[]) => void): Disposable { - throw new Error('Function registerTextEditorCommand is not implemented'); - }, - // tslint:disable-next-line:no-any - registerHandler(commandId: string, handler: (...args: any[]) => any): Disposable { - return commandRegistryExt.registerHandler(commandId, handler); - } - }; - - const window: typeof theia.window = { - get activeTextEditor() { - return editors.getActiveEditor(); - }, - get visibleTextEditors() { - return editors.getVisibleTextEditors(); - }, - onDidChangeActiveTextEditor(listener, thisArg?, disposables?) { - return editors.onDidChangeActiveTextEditor(listener, thisArg, disposables); - }, - onDidChangeVisibleTextEditors(listener, thisArg?, disposables?) { - return editors.onDidChangeVisibleTextEditors(listener, thisArg, disposables); - }, - onDidChangeTextEditorSelection(listener, thisArg?, disposables?) { - return editors.onDidChangeTextEditorSelection(listener, thisArg, disposables); - }, - onDidChangeTextEditorOptions(listener, thisArg?, disposables?) { - return editors.onDidChangeTextEditorOptions(listener, thisArg, disposables); - }, - onDidChangeTextEditorViewColumn(listener, thisArg?, disposables?) { - return editors.onDidChangeTextEditorViewColumn(listener, thisArg, disposables); - }, - onDidChangeTextEditorVisibleRanges(listener, thisArg?, disposables?) { - return editors.onDidChangeTextEditorVisibleRanges(listener, thisArg, disposables); - }, - // tslint:disable-next-line:no-any - showQuickPick(items: any, options: theia.QuickPickOptions, token?: theia.CancellationToken): any { - if (token) { - const coreEvent = Object.assign(token.onCancellationRequested, { maxListeners: 0 }); - const coreCancellationToken = { isCancellationRequested: token.isCancellationRequested, onCancellationRequested: coreEvent }; - return quickOpenExt.showQuickPick(items, options, coreCancellationToken); - } else { - return quickOpenExt.showQuickPick(items, options); - } - }, - showWorkspaceFolderPick(options?: theia.WorkspaceFolderPickOptions) { - return workspaceExt.pickWorkspaceFolder(options); - }, - showInformationMessage(message: string, - optionsOrFirstItem: theia.MessageOptions | string | theia.MessageItem, + return function (plugin: InternalPlugin): typeof theia { + const commands: typeof theia.commands = { // tslint:disable-next-line:no-any - ...items: any[]): PromiseLike { - return messageRegistryExt.showInformationMessage(message, optionsOrFirstItem, items); - }, - showWarningMessage(message: string, - optionsOrFirstItem: theia.MessageOptions | string | theia.MessageItem, + registerCommand(command: theia.Command, handler?: (...args: any[]) => T | Thenable): Disposable { + return commandRegistryExt.registerCommand(command, handler); + }, // tslint:disable-next-line:no-any - ...items: any[]): PromiseLike { - return messageRegistryExt.showWarningMessage(message, optionsOrFirstItem, items); - }, - showErrorMessage(message: string, - optionsOrFirstItem: theia.MessageOptions | string | theia.MessageItem, + executeCommand(commandId: string, ...args: any[]): PromiseLike { + return commandRegistryExt.executeCommand(commandId, args); + }, // tslint:disable-next-line:no-any - ...items: any[]): PromiseLike { - return messageRegistryExt.showErrorMessage(message, optionsOrFirstItem, items); - }, - showOpenDialog(options: theia.OpenDialogOptions): PromiseLike { - return dialogsExt.showOpenDialog(options); - }, - showSaveDialog(options: theia.SaveDialogOptions): PromiseLike { - return dialogsExt.showSaveDialog(options); - }, - // tslint:disable-next-line:no-any - setStatusBarMessage(text: string, arg?: number | PromiseLike): Disposable { - return statusBarMessageRegistryExt.setStatusBarMessage(text, arg); - }, - showInputBox(options?: theia.InputBoxOptions, token?: theia.CancellationToken) { - if (token) { - const coreEvent = Object.assign(token.onCancellationRequested, { maxListeners: 0 }); - const coreCancellationToken = { isCancellationRequested: token.isCancellationRequested, onCancellationRequested: coreEvent }; - return quickOpenExt.showInput(options, coreCancellationToken); - } else { - return quickOpenExt.showInput(options); + registerTextEditorCommand(command: theia.Command, callback: (textEditor: theia.TextEditor, edit: theia.TextEditorEdit, ...arg: any[]) => void): Disposable { + throw new Error('Function registerTextEditorCommand is not implemented'); + }, + // tslint:disable-next-line:no-any + registerHandler(commandId: string, handler: (...args: any[]) => any): Disposable { + return commandRegistryExt.registerHandler(commandId, handler); } - }, - createStatusBarItem(alignment?: theia.StatusBarAlignment, priority?: number): theia.StatusBarItem { - return statusBarMessageRegistryExt.createStatusBarItem(alignment, priority); - }, - createOutputChannel(name: string): theia.OutputChannel { - return outputChannelRegistryExt.createOutputChannel(name); - }, + }; - get state(): theia.WindowState { - return windowStateExt.getWindowState(); - }, - onDidChangeWindowState(listener, thisArg?, disposables?): theia.Disposable { - return windowStateExt.onDidChangeWindowState(listener, thisArg, disposables); - }, + const window: typeof theia.window = { + get activeTextEditor() { + return editors.getActiveEditor(); + }, + get visibleTextEditors() { + return editors.getVisibleTextEditors(); + }, + onDidChangeActiveTextEditor(listener, thisArg?, disposables?) { + return editors.onDidChangeActiveTextEditor(listener, thisArg, disposables); + }, + onDidChangeVisibleTextEditors(listener, thisArg?, disposables?) { + return editors.onDidChangeVisibleTextEditors(listener, thisArg, disposables); + }, + onDidChangeTextEditorSelection(listener, thisArg?, disposables?) { + return editors.onDidChangeTextEditorSelection(listener, thisArg, disposables); + }, + onDidChangeTextEditorOptions(listener, thisArg?, disposables?) { + return editors.onDidChangeTextEditorOptions(listener, thisArg, disposables); + }, + onDidChangeTextEditorViewColumn(listener, thisArg?, disposables?) { + return editors.onDidChangeTextEditorViewColumn(listener, thisArg, disposables); + }, + onDidChangeTextEditorVisibleRanges(listener, thisArg?, disposables?) { + return editors.onDidChangeTextEditorVisibleRanges(listener, thisArg, disposables); + }, + // tslint:disable-next-line:no-any + showQuickPick(items: any, options: theia.QuickPickOptions, token?: theia.CancellationToken): any { + if (token) { + const coreEvent = Object.assign(token.onCancellationRequested, { maxListeners: 0 }); + const coreCancellationToken = { isCancellationRequested: token.isCancellationRequested, onCancellationRequested: coreEvent }; + return quickOpenExt.showQuickPick(items, options, coreCancellationToken); + } else { + return quickOpenExt.showQuickPick(items, options); + } + }, + showWorkspaceFolderPick(options?: theia.WorkspaceFolderPickOptions) { + return workspaceExt.pickWorkspaceFolder(options); + }, + showInformationMessage(message: string, + optionsOrFirstItem: theia.MessageOptions | string | theia.MessageItem, + // tslint:disable-next-line:no-any + ...items: any[]): PromiseLike { + return messageRegistryExt.showInformationMessage(message, optionsOrFirstItem, items); + }, + showWarningMessage(message: string, + optionsOrFirstItem: theia.MessageOptions | string | theia.MessageItem, + // tslint:disable-next-line:no-any + ...items: any[]): PromiseLike { + return messageRegistryExt.showWarningMessage(message, optionsOrFirstItem, items); + }, + showErrorMessage(message: string, + optionsOrFirstItem: theia.MessageOptions | string | theia.MessageItem, + // tslint:disable-next-line:no-any + ...items: any[]): PromiseLike { + return messageRegistryExt.showErrorMessage(message, optionsOrFirstItem, items); + }, + showOpenDialog(options: theia.OpenDialogOptions): PromiseLike { + return dialogsExt.showOpenDialog(options); + }, + showSaveDialog(options: theia.SaveDialogOptions): PromiseLike { + return dialogsExt.showSaveDialog(options); + }, + // tslint:disable-next-line:no-any + setStatusBarMessage(text: string, arg?: number | PromiseLike): Disposable { + return statusBarMessageRegistryExt.setStatusBarMessage(text, arg); + }, + showInputBox(options?: theia.InputBoxOptions, token?: theia.CancellationToken) { + if (token) { + const coreEvent = Object.assign(token.onCancellationRequested, { maxListeners: 0 }); + const coreCancellationToken = { isCancellationRequested: token.isCancellationRequested, onCancellationRequested: coreEvent }; + return quickOpenExt.showInput(options, coreCancellationToken); + } else { + return quickOpenExt.showInput(options); + } + }, + createStatusBarItem(alignment?: theia.StatusBarAlignment, priority?: number): theia.StatusBarItem { + return statusBarMessageRegistryExt.createStatusBarItem(alignment, priority); + }, + createOutputChannel(name: string): theia.OutputChannel { + return outputChannelRegistryExt.createOutputChannel(name); + }, - createTerminal(nameOrOptions: theia.TerminalOptions | (string | undefined), shellPath?: string, shellArgs?: string[]): theia.Terminal { - return terminalExt.createTerminal(nameOrOptions, shellPath, shellArgs); - }, - get onDidCloseTerminal(): theia.Event { - return terminalExt.onDidCloseTerminal; - }, - set onDidCloseTerminal(event: theia.Event) { - terminalExt.onDidCloseTerminal = event; - }, + get state(): theia.WindowState { + return windowStateExt.getWindowState(); + }, + onDidChangeWindowState(listener, thisArg?, disposables?): theia.Disposable { + return windowStateExt.onDidChangeWindowState(listener, thisArg, disposables); + }, - createTextEditorDecorationType(options: theia.DecorationRenderOptions): theia.TextEditorDecorationType { - return editors.createTextEditorDecorationType(options); - } - }; + createTerminal(nameOrOptions: theia.TerminalOptions | (string | undefined), shellPath?: string, shellArgs?: string[]): theia.Terminal { + return terminalExt.createTerminal(nameOrOptions, shellPath, shellArgs); + }, + get onDidCloseTerminal(): theia.Event { + return terminalExt.onDidCloseTerminal; + }, + set onDidCloseTerminal(event: theia.Event) { + terminalExt.onDidCloseTerminal = event; + }, + + createTextEditorDecorationType(options: theia.DecorationRenderOptions): theia.TextEditorDecorationType { + return editors.createTextEditorDecorationType(options); + } + }; - const workspace: typeof theia.workspace = { - get workspaceFolders(): theia.WorkspaceFolder[] | undefined { - return workspaceExt.workspaceFolders; - }, - get name(): string | undefined { - return workspaceExt.name; - }, - onDidChangeWorkspaceFolders(listener, thisArg?, disposables?): theia.Disposable { - return workspaceExt.onDidChangeWorkspaceFolders(listener, thisArg, disposables); - }, - get textDocuments() { - return documents.getAllDocumentData().map(data => data.document); - }, - onDidChangeTextDocument(listener, thisArg?, disposables?) { - return documents.onDidChangeDocument(listener, thisArg, disposables); - }, - onDidCloseTextDocument(listener, thisArg?, disposables?) { - return documents.onDidRemoveDocument(listener, thisArg, disposables); - }, - onDidOpenTextDocument(listener, thisArg?, disposables?) { - return documents.onDidAddDocument(listener, thisArg, disposables); - }, - getConfiguration(section?, resource?): theia.WorkspaceConfiguration { - return preferenceRegistryExt.getConfiguration(section, resource); - }, - onDidChangeConfiguration(listener, thisArgs?, disposables?): theia.Disposable { - return preferenceRegistryExt.onDidChangeConfiguration(listener, thisArgs, disposables); - }, - openTextDocument(uriOrFileNameOrOptions?: theia.Uri | string | { language?: string; content?: string; }) { - let uriPromise: Promise; + const workspace: typeof theia.workspace = { + get workspaceFolders(): theia.WorkspaceFolder[] | undefined { + return workspaceExt.workspaceFolders; + }, + get name(): string | undefined { + return workspaceExt.name; + }, + onDidChangeWorkspaceFolders(listener, thisArg?, disposables?): theia.Disposable { + return workspaceExt.onDidChangeWorkspaceFolders(listener, thisArg, disposables); + }, + get textDocuments() { + return documents.getAllDocumentData().map(data => data.document); + }, + onDidChangeTextDocument(listener, thisArg?, disposables?) { + return documents.onDidChangeDocument(listener, thisArg, disposables); + }, + onDidCloseTextDocument(listener, thisArg?, disposables?) { + return documents.onDidRemoveDocument(listener, thisArg, disposables); + }, + onDidOpenTextDocument(listener, thisArg?, disposables?) { + return documents.onDidAddDocument(listener, thisArg, disposables); + }, + getConfiguration(section?, resource?): theia.WorkspaceConfiguration { + return preferenceRegistryExt.getConfiguration(section, resource); + }, + onDidChangeConfiguration(listener, thisArgs?, disposables?): theia.Disposable { + return preferenceRegistryExt.onDidChangeConfiguration(listener, thisArgs, disposables); + }, + openTextDocument(uriOrFileNameOrOptions?: theia.Uri | string | { language?: string; content?: string; }) { + let uriPromise: Promise; - const options = uriOrFileNameOrOptions as { language?: string; content?: string; }; - if (typeof uriOrFileNameOrOptions === 'string') { - uriPromise = Promise.resolve(URI.file(uriOrFileNameOrOptions)); - } else if (uriOrFileNameOrOptions instanceof URI) { - uriPromise = Promise.resolve(uriOrFileNameOrOptions); - } else if (!options || typeof options === 'object') { - uriPromise = documents.createDocumentData(options); - } else { - throw new Error('illegal argument - uriOrFileNameOrOptions'); + const options = uriOrFileNameOrOptions as { language?: string; content?: string; }; + if (typeof uriOrFileNameOrOptions === 'string') { + uriPromise = Promise.resolve(Uri.file(uriOrFileNameOrOptions)); + } else if (uriOrFileNameOrOptions instanceof Uri) { + uriPromise = Promise.resolve(uriOrFileNameOrOptions); + } else if (!options || typeof options === 'object') { + uriPromise = documents.createDocumentData(options); + } else { + throw new Error('illegal argument - uriOrFileNameOrOptions'); + } + + return uriPromise.then(uri => + documents.ensureDocumentData(uri).then(() => { + const data = documents.getDocumentData(uri); + return data && data.document; + })); } + }; - return uriPromise.then(uri => - documents.ensureDocumentData(uri).then(() => { - const data = documents.getDocumentData(uri); - return data && data.document; - })); - } - }; + const env: typeof theia.env = { + getEnvVariable(envVarName: string): PromiseLike { + return envExt.getEnvVariable(envVarName); + }, + getQueryParameter(queryParamName: string): string | string[] | undefined { + return envExt.getQueryParameter(queryParamName); + }, + getQueryParameters(): QueryParameters { + return envExt.getQueryParameters(); + } + }; - const env: typeof theia.env = { - getEnvVariable(envVarName: string): PromiseLike { - return envExt.getEnvVariable(envVarName); - }, - getQueryParameter(queryParamName: string): string | string[] | undefined { - return envExt.getQueryParameter(queryParamName); - }, - getQueryParameters(): QueryParameters { - return envExt.getQueryParameters(); - } - }; + const languages: typeof theia.languages = { + getLanguages(): PromiseLike { + return languagesExt.getLanguages(); + }, + match(selector: theia.DocumentSelector, document: theia.TextDocument): number { + return score(fromDocumentSelector(selector), document.uri, document.languageId, true); + }, + get onDidChangeDiagnostics(): theia.Event { + return languagesExt.onDidChangeDiagnostics; + }, + getDiagnostics(resource?: Uri) { + return languagesExt.getDiagnostics(resource); + }, + createDiagnosticCollection(name?: string): theia.DiagnosticCollection { + return languagesExt.createDiagnosticCollection(name); + }, + setLanguageConfiguration(language: string, configuration: theia.LanguageConfiguration): theia.Disposable { + return languagesExt.setLanguageConfiguration(language, configuration); + }, + registerCompletionItemProvider(selector: theia.DocumentSelector, provider: theia.CompletionItemProvider, ...triggerCharacters: string[]): theia.Disposable { + return languagesExt.registerCompletionItemProvider(selector, provider, triggerCharacters); + }, + }; - const languages: typeof theia.languages = { - getLanguages(): PromiseLike { - return languagesExt.getLanguages(); - }, - match(selector: theia.DocumentSelector, document: theia.TextDocument): number { - return score(fromDocumentSelector(selector), document.uri, document.languageId, true); - }, - setLanguageConfiguration(language: string, configuration: theia.LanguageConfiguration): theia.Disposable { - return languagesExt.setLanguageConfiguration(language, configuration); - }, - registerCompletionItemProvider(selector: theia.DocumentSelector, provider: theia.CompletionItemProvider, ...triggerCharacters: string[]): theia.Disposable { - return languagesExt.registerCompletionItemProvider(selector, provider, triggerCharacters); - }, - }; + const plugins: typeof theia.plugins = { + get all(): theia.Plugin[] { + return pluginManager.getAllPlugins().map(plg => new Plugin(pluginManager, plg)); + }, + getPlugin(pluginId: string): theia.Plugin | undefined { + const plg = pluginManager.getPluginById(pluginId); + if (plg) { + return new Plugin(pluginManager, plg); + } + return undefined; + } + }; - return { - version: require('../../package.json').version, - commands, - window, - workspace, - env, - languages, - // Types - StatusBarAlignment: StatusBarAlignment, - Disposable: Disposable, - EventEmitter: Emitter, - CancellationTokenSource: CancellationTokenSource, - MarkdownString, - Position: Position, - Range: Range, - Selection: Selection, - ViewColumn: ViewColumn, - TextEditorSelectionChangeKind: TextEditorSelectionChangeKind, - Uri: Uri, - EndOfLine, - TextEditorRevealType, - TextEditorCursorStyle, - TextEditorLineNumbersStyle, - ThemeColor, - SnippetString, - DecorationRangeBehavior, - OverviewRulerLane, - ConfigurationTarget, - RelativePattern, - IndentAction, - CompletionItem, - CompletionItemKind, - CompletionList, - CompletionTriggerKind, - TextEdit, + return { + version: require('../../package.json').version, + commands, + window, + workspace, + env, + languages, + plugins, + // Types + StatusBarAlignment: StatusBarAlignment, + Disposable: Disposable, + EventEmitter: Emitter, + CancellationTokenSource: CancellationTokenSource, + MarkdownString, + Position: Position, + Range: Range, + Selection: Selection, + ViewColumn: ViewColumn, + TextEditorSelectionChangeKind: TextEditorSelectionChangeKind, + Uri: Uri, + EndOfLine, + TextEditorRevealType, + TextEditorCursorStyle, + TextEditorLineNumbersStyle, + ThemeColor, + SnippetString, + DecorationRangeBehavior, + OverviewRulerLane, + ConfigurationTarget, + RelativePattern, + IndentAction, + CompletionItem, + CompletionItemKind, + CompletionList, + DiagnosticSeverity, + DiagnosticRelatedInformation, + Location, + DiagnosticTag, + Diagnostic, + CompletionTriggerKind, + TextEdit, + }; }; } -// tslint:disable-next-line:no-any -export function startPlugin(plugin: Plugin, pluginMain: any, plugins: Map void>): void { - if (typeof pluginMain[plugin.lifecycle.startMethod] === 'function') { - pluginMain[plugin.lifecycle.startMethod].apply(getGlobal(), []); - } else { - console.log('there is no doStart method on plugin'); +class Plugin implements theia.Plugin { + id: string; + pluginPath: string; + isActive: boolean; + packageJSON: any; + pluginType: theia.PluginType; + constructor(private readonly pluginManager: PluginManager, plugin: InternalPlugin) { + this.id = plugin.model.id; + this.pluginPath = plugin.pluginFolder; + this.packageJSON = plugin.rawModel; + this.isActive = true; + this.pluginType = plugin.model.entryPoint.frontend ? 'frontend' : 'backend'; } - if (typeof pluginMain[plugin.lifecycle.stopMethod] === 'function') { - const pluginId = getPluginId(plugin.model); - plugins.set(pluginId, pluginMain[plugin.lifecycle.stopMethod]); + get exports(): T { + return this.pluginManager.getPluginExport(this.id); } -} -// for electron -function getGlobal() { - // tslint:disable-next-line:no-null-keyword - return typeof self === 'undefined' ? typeof global === 'undefined' ? null : global : self; + activate(): PromiseLike { + return this.pluginManager.activatePlugin(this.id).then(() => this.exports); + } } diff --git a/packages/plugin-ext/src/plugin/plugin-manager.ts b/packages/plugin-ext/src/plugin/plugin-manager.ts new file mode 100644 index 0000000000000..b247fc5aa0613 --- /dev/null +++ b/packages/plugin-ext/src/plugin/plugin-manager.ts @@ -0,0 +1,148 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. 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 + ********************************************************************************/ + +import { PluginManagerExt, PluginInitData, PluginManager, Plugin, PluginAPI } from '../api/plugin-api'; +import { PluginMetadata } from '../common/plugin-protocol'; +import * as theia from '@theia/plugin'; + +import { dispose } from '../common/disposable-util'; +import { Deferred } from '@theia/core/lib/common/promise-util'; + +export interface PluginHost { + + // tslint:disable-next-line:no-any + loadPlugin(plugin: Plugin): any; + + init(data: PluginMetadata[]): [Plugin[], Plugin[]]; +} + +interface StopFn { + (): void; +} + +class ActivatedPlugin { + constructor(public readonly pluginContext: theia.PluginContext, + public readonly exports?: PluginAPI, + public readonly stopFn?: StopFn) { + } +} + +export class PluginManagerExtImpl implements PluginManagerExt, PluginManager { + + private registry = new Map(); + private activatedPlugins = new Map(); + private pluginActivationPromises = new Map>(); + + constructor(private readonly host: PluginHost) { + } + + $stopPlugin(contextPath: string): PromiseLike { + this.activatedPlugins.forEach(plugin => { + if (plugin.stopFn) { + plugin.stopFn(); + } + + // dispose any objects + const pluginContext = plugin.pluginContext; + if (pluginContext) { + dispose(pluginContext.subscriptions); + } + }); + return Promise.resolve(); + } + + $init(pluginInit: PluginInitData): PromiseLike { + const [plugins, foreignPlugins] = this.host.init(pluginInit.plugins); + // add foreign plugins + for (const plugin of foreignPlugins) { + this.registry.set(plugin.model.id, plugin); + } + // add own plugins, before initialization + for (const plugin of plugins) { + this.registry.set(plugin.model.id, plugin); + } + // run plugins + for (const plugin of plugins) { + const pluginMain = this.host.loadPlugin(plugin); + this.startPlugin(plugin, pluginMain); + } + + return Promise.resolve(); + } + + // tslint:disable-next-line:no-any + private startPlugin(plugin: Plugin, pluginMain: any): void { + + // Create pluginContext object for this plugin. + const subscriptions: theia.Disposable[] = []; + const pluginContext: theia.PluginContext = { + subscriptions: subscriptions + }; + + let stopFn = undefined; + if (typeof pluginMain[plugin.lifecycle.stopMethod] === 'function') { + stopFn = pluginMain[plugin.lifecycle.stopMethod]; + } + if (typeof pluginMain[plugin.lifecycle.startMethod] === 'function') { + const pluginExport = pluginMain[plugin.lifecycle.startMethod].apply(getGlobal(), [pluginContext]); + this.activatedPlugins.set(plugin.model.id, new ActivatedPlugin(pluginContext, pluginExport, stopFn)); + + // resolve activation promise + if (this.pluginActivationPromises.has(plugin.model.id)) { + this.pluginActivationPromises.get(plugin.model.id)!.resolve(); + this.pluginActivationPromises.delete(plugin.model.id); + } + } else { + console.log('there is no doStart method on plugin'); + } + } + + getAllPlugins(): Plugin[] { + return Array.from(this.registry.values()); + } + getPluginExport(pluginId: string): PluginAPI | undefined { + const activePlugin = this.activatedPlugins.get(pluginId); + if (activePlugin) { + return activePlugin.exports; + } + return undefined; + } + + getPluginById(pluginId: string): Plugin | undefined { + return this.registry.get(pluginId); + } + + isRunning(pluginId: string): boolean { + return this.registry.has(pluginId); + } + + activatePlugin(pluginId: string): PromiseLike { + if (this.pluginActivationPromises.has(pluginId)) { + return this.pluginActivationPromises.get(pluginId)!.promise; + } + + const deferred = new Deferred(); + this.pluginActivationPromises.set(pluginId, deferred); + return deferred.promise; + } + +} + +// for electron +function getGlobal() { + // tslint:disable-next-line:no-null-keyword + return typeof self === 'undefined' ? typeof global === 'undefined' ? null : global : self; +} diff --git a/packages/plugin-ext/src/plugin/type-converters.ts b/packages/plugin-ext/src/plugin/type-converters.ts index 824e484a427ff..2087cecabb8a8 100644 --- a/packages/plugin-ext/src/plugin/type-converters.ts +++ b/packages/plugin-ext/src/plugin/type-converters.ts @@ -15,7 +15,7 @@ ********************************************************************************/ import { EditorPosition, Selection, Position, DecorationOptions } from '../api/plugin-api'; -import { Range, MarkdownString, CompletionType, SingleEditOperation } from '../api/model'; +import { Range, MarkdownString, CompletionType, SingleEditOperation, MarkerData, RelatedInformation } from '../api/model'; import * as theia from '@theia/plugin'; import * as types from './types-impl'; import { LanguageSelector, LanguageFilter, RelativePattern } from './languages'; @@ -281,3 +281,68 @@ export function fromLanguageSelector(selector: theia.DocumentSelector): Language }; } } + +export function convertDiagnosticToMarkerData(diagnostic: theia.Diagnostic): MarkerData { + return { + code: convertCode(diagnostic.code), + severity: convertSeverity(diagnostic.severity), + message: diagnostic.message, + source: diagnostic.source, + startLineNumber: diagnostic.range.start.line + 1, + startColumn: diagnostic.range.start.character + 1, + endLineNumber: diagnostic.range.end.line + 1, + endColumn: diagnostic.range.end.character + 1, + relatedInformation: convertRelatedInformation(diagnostic.relatedInformation), + tags: convertTags(diagnostic.tags) + }; +} + +function convertCode(code: string | number | undefined): string | undefined { + if (typeof code === 'number') { + return String(code); + } else { + return code; + } +} + +function convertSeverity(severity: types.DiagnosticSeverity): types.MarkerSeverity { + switch (severity) { + case types.DiagnosticSeverity.Error: return types.MarkerSeverity.Error; + case types.DiagnosticSeverity.Warning: return types.MarkerSeverity.Warning; + case types.DiagnosticSeverity.Information: return types.MarkerSeverity.Info; + case types.DiagnosticSeverity.Hint: return types.MarkerSeverity.Hint; + } +} + +function convertRelatedInformation(diagnosticsRelatedInformation: theia.DiagnosticRelatedInformation[] | undefined): RelatedInformation[] | undefined { + if (!diagnosticsRelatedInformation) { + return undefined; + } + + const relatedInformation: RelatedInformation[] = []; + for (const item of diagnosticsRelatedInformation) { + relatedInformation.push({ + resource: item.location.uri, + message: item.message, + startLineNumber: item.location.range.start.line + 1, + startColumn: item.location.range.start.character + 1, + endLineNumber: item.location.range.end.line + 1, + endColumn: item.location.range.end.character + 1 + }); + } + return relatedInformation; +} + +function convertTags(tags: types.DiagnosticTag[] | undefined): types.MarkerTag[] | undefined { + if (!tags) { + return undefined; + } + + const markerTags: types.MarkerTag[] = []; + for (const tag of tags) { + switch (tag) { + case types.DiagnosticTag.Unnecessary: markerTags.push(types.MarkerTag.Unnecessary); + } + } + return markerTags; +} diff --git a/packages/plugin-ext/src/plugin/types-impl.ts b/packages/plugin-ext/src/plugin/types-impl.ts index 8272e51be7f88..c24d57aedbf91 100644 --- a/packages/plugin-ext/src/plugin/types-impl.ts +++ b/packages/plugin-ext/src/plugin/types-impl.ts @@ -746,3 +746,65 @@ export class CompletionList { this.isIncomplete = isIncomplete; } } + +export enum DiagnosticSeverity { + Error = 0, + Warning = 1, + Information = 2, + Hint = 3 +} + +export class DiagnosticRelatedInformation { + location: Location; + message: string; + + constructor(location: Location, message: string) { + this.location = location; + this.message = message; + } +} + +export class Location { + uri: URI; + range: Range; + + constructor(uri: URI, rangeOrPosition: Range | Position) { + this.uri = uri; + if (rangeOrPosition instanceof Range) { + this.range = rangeOrPosition; + } else if (rangeOrPosition instanceof Position) { + this.range = new Range(rangeOrPosition, rangeOrPosition); + } + } +} + +export enum DiagnosticTag { + Unnecessary = 1, +} + +export class Diagnostic { + range: Range; + message: string; + severity: DiagnosticSeverity; + source?: string; + code?: string | number; + relatedInformation?: DiagnosticRelatedInformation[]; + tags?: DiagnosticTag[]; + + constructor(range: Range, message: string, severity: DiagnosticSeverity = DiagnosticSeverity.Error) { + this.range = range; + this.message = message; + this.severity = severity; + } +} + +export enum MarkerSeverity { + Hint = 1, + Info = 2, + Warning = 4, + Error = 8, +} + +export enum MarkerTag { + Unnecessary = 1, +} diff --git a/packages/plugin-ext/src/typings/monaco/index.d.ts b/packages/plugin-ext/src/typings/monaco/index.d.ts index 0508377c45898..c84fd9c397acb 100644 --- a/packages/plugin-ext/src/typings/monaco/index.d.ts +++ b/packages/plugin-ext/src/typings/monaco/index.d.ts @@ -14,7 +14,7 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -/// +/// declare module monaco.editor { export interface IStandaloneCodeEditor extends CommonCodeEditor { setDecorations(decorationTypeKey: string, ranges: IDecorationOptions[]): void; diff --git a/packages/plugin/API.md b/packages/plugin/API.md index 471eb8f63dcc3..93016ad71e866 100644 --- a/packages/plugin/API.md +++ b/packages/plugin/API.md @@ -2,6 +2,48 @@ ## Theia Plugin system description +### Plugin API + +Namespace for dealing with installed plug-ins. Plug-ins are represented +by an Plugin-interface which enables reflection on them. +Plug-in writers can provide APIs to other plug-ins by returning their API public +surface from the `start`-call. + +For example some plugin exports it's API: + +```javascript +export function start() { + let api = { + sum(a, b) { + return a + b; + }, + mul(a, b) { + return a * b; + } + }; + // 'export' public api-surface + return api; +} +``` + +Another plugin can use that API: + +```javascript +let mathExt = theia.plugins.getPlugin('genius.math'); +let importedApi = mathExt.exports; +console.log(importedApi.mul(42, 1)); +``` + +Also plugin API allows access to plugin `package.json` content. + +Example: + +```javascript +const fooPlugin = plugins.getPlugin('publisher.plugin_name'); +const fooPluginPackageJson = fooPlugin.packageJSON; +console.log(fooPluginPackageJson.someField); +``` + ### Command API A command is a unique identifier of a function which @@ -141,11 +183,11 @@ Simple example that show a status bar message with statusBarItem: #### Output channel API It is possible to show a container for readonly textual information: - + ```typescript const channel = theia.window.createOutputChannel('test channel'); channel.appendLine('test output'); - + ``` #### Environment API @@ -287,3 +329,82 @@ preferences.onDidChangeConfiguration(e => { preferences.update('tabSize', 2); ``` + +### Languages API + +#### Diagnostics + +To get all existing diagnostics one should use `getDiagnostics` call. If diagnostics are needed for specific URI they could be obtain with following call: + +```typescript +const diagnostics = theia.languages.getDiagnostics(uriToResource) +``` + +To get all diagnostics use: +```typescript +const diagnostics = theia.languages.getDiagnostics() +``` + +For example, following code will get diagnostics for current file in the editor (supposed one is already opened): + +```typescript +const diagnosticsForCurrentFile = theia.languages.getDiagnostics(theia.window.activeTextEditor.document.uri) +``` + +If no diagnostics found empty array will be returned. + +Note, that returned array from `getDiagnostics` call are readonly. + +To tracks changes in diagnostics `onDidChangeDiagnostics` event should be used. Within event handler list of uris with changed diadgnostics is available. Example: + +```typescript +disposables.push( + theia.languages.onDidChangeDiagnostics((event: theia.DiagnosticChangeEvent) => { + // handler code here + } +); +``` + +Also it is possible to add own diagnostics. To do this, one should create diagnostics collection first: + +```typescript +const diagnosticsCollection = theia.languages.createDiagnosticCollection(collectionName); +``` + +Collection name can be ommited. In such case the name will be auto-generated. + +When collection is created, one could operate with diagnostics. The collection object exposes all needed methods: `get`, `set`, `has`, `delete`, `clear`, `forEach` and `dispose`. + +`get`, `has` and `delete` performs corresponding operation by given resource uri. `clear` removes all diagnostics for all uris in the collection. + +Behavior of `set` is more complicated. To replace all diagnostics for given uri the following call should be used: + +```typescript +diagnosticsCollection.set(uri, newDiagnostics) +``` + +if `undefined` is passed instead of diagnostics array the call will clear diagnostics for given uri in this collection (the same as `delete`). +Also it is possible to set all diagnostics at once (it will replace existed ones). To do this, array of tuples in format `[uri, diagnostics]` should be passed as argument for `set`: + +```typescript +const changes: [Uri, Diagnostic[] | undefined][] = []; + +changes.push([uri1, diagnostics1]); +changes.push([uri2, diagnostics2]); +changes.push([uri3, undefined]); +changes.push([uri1, diagnostics4]); // uri1 again + +diagnosticsCollection.set(changes); +``` + +If the same uri is used a few times, corresponding diagnostics will be merged. In case of `undefined` all previous, but not following, diagnostics will be cleared. If `undefined` is given insted of tuples array the whole collection will be cleared. + +To iterate over all diagnostics within the collection `forEach` method could be used: + +```typescript +diagnosticsCollection.forEach((uri, diagnostics) => { + // code here +} +``` + +`dispose` method should be used when the collection is not needed any more. In case of attempt to do an operaton after disposing an error will be thrown. diff --git a/packages/plugin/package.json b/packages/plugin/package.json index e70e3b53cf6a7..c1ef351e7203c 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,6 +1,6 @@ { "name": "@theia/plugin", - "version": "0.3.13", + "version": "0.3.14", "description": "Theia - Plugin API", "types": "./src/theia.d.ts", "publishConfig": { @@ -27,7 +27,7 @@ "docs": "theiaext docs" }, "devDependencies": { - "@theia/ext-scripts": "^0.3.13" + "@theia/ext-scripts": "^0.3.14" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/plugin/src/theia.d.ts b/packages/plugin/src/theia.d.ts index fdb3da1c0ba0d..2a6f50a908e7e 100644 --- a/packages/plugin/src/theia.d.ts +++ b/packages/plugin/src/theia.d.ts @@ -40,6 +40,105 @@ declare module '@theia/plugin' { } + export type PluginType = 'frontend' | 'backend'; + + /** + * Represents an plugin. + * + * To get an instance of an `Plugin` use [getPlugin](#plugins.getPlugin). + */ + export interface Plugin { + + /** + * The canonical plug-in identifier in the form of: `publisher.name`. + */ + readonly id: string; + + /** + * The absolute file path of the directory containing this plug-in. + */ + readonly pluginPath: string; + + /** + * `true` if the plug-in has been activated. + */ + readonly isActive: boolean; + + /** + * The parsed contents of the plug-in's package.json. + */ + readonly packageJSON: any; + + /** + * + */ + readonly pluginType : PluginType; + + /** + * The public API exported by this plug-in. It is an invalid action + * to access this field before this plug-in has been activated. + */ + readonly exports: T; + + /** + * Activates this plug-in and returns its public API. + * + * @return A promise that will resolve when this plug-in has been activated. + */ + activate(): PromiseLike; + } + + /** + * Namespace for dealing with installed plug-ins. Plug-ins are represented + * by an [plug-in](#Plugin)-interface which enables reflection on them. + * + * Plug-in writers can provide APIs to other plug-ins by returning their API public + * surface from the `start`-call. + * + * ```javascript + * export function start() { + * let api = { + * sum(a, b) { + * return a + b; + * }, + * mul(a, b) { + * return a * b; + * } + * }; + * // 'export' public api-surface + * return api; + * } + * ``` + * ```javascript + * let mathExt = plugins.getPlugin('genius.math'); + * let importedApi = mathExt.exports; + * + * console.log(importedApi.mul(42, 1)); + * ``` + */ + export namespace plugins { + /** + * Get an plug-in by its full identifier in the form of: `publisher.name`. + * + * @param pluginId An plug-in identifier. + * @return An plug-in or `undefined`. + */ + export function getPlugin(pluginId: string): Plugin | undefined; + + /** + * Get an plug-in its full identifier in the form of: `publisher.name`. + * + * @param pluginId An plug-in identifier. + * @return An plug-in or `undefined`. + */ + export function getPlugin(pluginId: string): Plugin | undefined; + + /** + * All plug-ins currently known to the system. + */ + export let all: Plugin[]; + } + /** * A command is a unique identifier of a function * which can be executed by a user via a keyboard shortcut, @@ -1485,19 +1584,19 @@ declare module '@theia/plugin' { onDidSelectItem?(item: QuickPickItem | string): any; } - /** - * Options to configure the behaviour of the [workspace folder](#WorkspaceFolder) pick UI. - */ + /** + * Options to configure the behaviour of the [workspace folder](#WorkspaceFolder) pick UI. + */ export interface WorkspaceFolderPickOptions { - /** - * An optional string to show as place holder in the input box to guide the user what to pick on. - */ + /** + * An optional string to show as place holder in the input box to guide the user what to pick on. + */ placeHolder?: string; - /** - * Set to `true` to keep the picker open when focus moves to another part of the editor or to another window. - */ + /** + * Set to `true` to keep the picker open when focus moves to another part of the editor or to another window. + */ ignoreFocusOut?: boolean; } @@ -1918,6 +2017,22 @@ declare module '@theia/plugin' { env?: { [key: string]: string | null }; } + /** + * A plug-in context is a collection of utilities private to a + * plug-in. + * + * An instance of a `PluginContext` is provided as the first + * parameter to the `start` of a plug-in. + */ + export interface PluginContext { + + /** + * An array to which disposables can be added. When this + * extension is deactivated the disposables will be disposed. + */ + subscriptions: { dispose(): any }[]; + } + /** * Common namespace for dealing with window and editor, showing messages and user input. */ @@ -1994,13 +2109,13 @@ declare module '@theia/plugin' { */ export function showQuickPick(items: T[] | PromiseLike, options: QuickPickOptions & { canPickMany: true }, token?: CancellationToken): PromiseLike; - /** - * Shows a selection list of [workspace folders](#workspace.workspaceFolders) to pick from. - * Returns `undefined` if no folder is open. - * - * @param options Configures the behavior of the workspace folder list. - * @return A promise that resolves to the workspace folder or `undefined`. - */ + /** + * Shows a selection list of [workspace folders](#workspace.workspaceFolders) to pick from. + * Returns `undefined` if no folder is open. + * + * @param options Configures the behavior of the workspace folder list. + * @return A promise that resolves to the workspace folder or `undefined`. + */ export function showWorkspaceFolderPick(options?: WorkspaceFolderPickOptions): PromiseLike; /** @@ -3115,7 +3230,243 @@ declare module '@theia/plugin' { resolveCompletionItem?(item: CompletionItem, token?: CancellationToken): ProviderResult; } + /** + * Represents a location inside a resource, such as a line + * inside a text file. + */ + export class Location { + + /** + * The resource identifier of this location. + */ + uri: Uri; + + /** + * The document range of this location. + */ + range: Range; + + /** + * Creates a new location object. + * + * @param uri The resource identifier. + * @param rangeOrPosition The range or position. Positions will be converted to an empty range. + */ + constructor(uri: Uri, rangeOrPosition: Range | Position); + } + + /** + * The event that is fired when diagnostics change. + */ + export interface DiagnosticChangeEvent { + /** + * An array of resources for which diagnostics have changed. + */ + readonly uris: Uri[]; + } + + /** + * Represents the severity of diagnostics. + */ + export enum DiagnosticSeverity { + + /** + * Something not allowed by the rules of a language or other means. + */ + Error = 0, + + /** + * Something suspicious but allowed. + */ + Warning = 1, + + /** + * Something to inform about but not a problem. + */ + Information = 2, + + /** + * Something to hint to a better way of doing it, like proposing + * a refactoring. + */ + Hint = 3 + } + + /** + * Represents a related message and source code location for a diagnostic. This should be + * used to point to code locations that cause or related to a diagnostics, e.g when duplicating + * a symbol in a scope. + */ + export class DiagnosticRelatedInformation { + + /** + * The location of this related diagnostic information. + */ + location: Location; + + /** + * The message of this related diagnostic information. + */ + message: string; + + /** + * Creates a new related diagnostic information object. + * + * @param location The location. + * @param message The message. + */ + constructor(location: Location, message: string); + } + + /** + * Additional metadata about the type of a diagnostic. + */ + export enum DiagnosticTag { + /** + * Unused or unnecessary code. + * + * Diagnostics with this tag are rendered faded out. The amount of fading + * is controlled by the `"editorUnnecessaryCode.opacity"` theme color. For + * example, `"editorUnnecessaryCode.opacity": "#000000c0"` will render the + * code with 75% opacity. For high contrast themes, use the + * `"editorUnnecessaryCode.border"` theme color to underline unnecessary code + * instead of fading it out. + */ + Unnecessary = 1, + } + + /** + * Represents a diagnostic, such as a compiler error or warning. Diagnostic objects + * are only valid in the scope of a file. + */ + export class Diagnostic { + + /** + * The range to which this diagnostic applies. + */ + range: Range; + + /** + * The human-readable message. + */ + message: string; + + /** + * The severity, default is [error](#DiagnosticSeverity.Error). + */ + severity: DiagnosticSeverity; + + /** + * A human-readable string describing the source of this + * diagnostic, e.g. 'typescript' or 'super lint'. + */ + source?: string; + + /** + * A code or identifier for this diagnostics. Will not be surfaced + * to the user, but should be used for later processing, e.g. when + * providing [code actions](#CodeActionContext). + */ + code?: string | number; + + /** + * An array of related diagnostic information, e.g. when symbol-names within + * a scope collide all definitions can be marked via this property. + */ + relatedInformation?: DiagnosticRelatedInformation[]; + + /** + * Additional metadata about the diagnostic. + */ + tags?: DiagnosticTag[]; + + /** + * Creates a new diagnostic object. + * + * @param range The range to which this diagnostic applies. + * @param message The human-readable message. + * @param severity The severity, default is [error](#DiagnosticSeverity.Error). + */ + constructor(range: Range, message: string, severity?: DiagnosticSeverity); + } + + export interface DiagnosticCollection { + + /** + * The name of this diagnostic collection, for instance `typescript`. Every diagnostic + * from this collection will be associated with this name. Also, the task framework uses this + * name when defining [problem matchers](https://code.visualstudio.com/docs/editor/tasks#_defining-a-problem-matcher). + */ + readonly name: string; + + /** + * Assign diagnostics for given resource. Will replace + * existing diagnostics for that resource. + * + * @param uri A resource identifier. + * @param diagnostics Array of diagnostics or `undefined` + */ + set(uri: Uri, diagnostics: Diagnostic[] | undefined): void; + + /** + * Replace all entries in this collection for given uris. + * + * Diagnostics of multiple tuples of the same uri will be merged, e.g + * `[[file1, [d1]], [file1, [d2]]]` is equivalent to `[[file1, [d1, d2]]]`. + * If a diagnostics item is `undefined` as in `[file1, undefined]` + * all previous but not subsequent diagnostics are removed. + * + * @param entries An array of tuples, like `[[file1, [d1, d2]], [file2, [d3, d4, d5]]]`, or `undefined`. + */ + set(entries: [Uri, Diagnostic[] | undefined][] | undefined): void; + + /** + * Remove all diagnostics from this collection that belong + * to the provided `uri`. The same as `#set(uri, undefined)`. + * + * @param uri A resource identifier. + */ + delete(uri: Uri): void; + + /** + * Remove all diagnostics from this collection. The same + * as calling `#set(undefined)`; + */ + clear(): void; + + /** + * Iterate over each entry in this collection. + * + * @param callback Function to execute for each entry. + * @param thisArg The `this` context used when invoking the handler function. + */ + forEach(callback: (uri: Uri, diagnostics: Diagnostic[], collection: DiagnosticCollection) => any, thisArg?: any): void; + + /** + * Get the diagnostics for a given resource. *Note* that you cannot + * modify the diagnostics-array returned from this call. + * + * @param uri A resource identifier. + * @returns An immutable array of [diagnostics](#Diagnostic) or `undefined`. + */ + get(uri: Uri): Diagnostic[] | undefined; + + /** + * Check if this collection contains diagnostics for a + * given resource. + * + * @param uri A resource identifier. + * @returns `true` if this collection has diagnostic for the given resource. + */ + has(uri: Uri): boolean; + + /** + * Dispose and free associated resources. Calls + * [clear](#DiagnosticCollection.clear). + */ + dispose(): void; + } export namespace languages { /** @@ -3163,6 +3514,37 @@ declare module '@theia/plugin' { */ export function match(selector: DocumentSelector, document: TextDocument): number; + /** + * An [event](#Event) which fires when the global set of diagnostics changes. This is + * newly added and removed diagnostics. + */ + export const onDidChangeDiagnostics: Event; + + /** + * Get all diagnostics for a given resource. *Note* that this includes diagnostics from + * all extensions but *not yet* from the task framework. + * + * @param resource A resource + * @returns An array of [diagnostics](#Diagnostic) objects or an empty array. + */ + export function getDiagnostics(resource: Uri): Diagnostic[]; + + /** + * Get all diagnostics. *Note* that this includes diagnostics from + * all extensions but *not yet* from the task framework. + * + * @returns An array of uri-diagnostics tuples or an empty array. + */ + export function getDiagnostics(): [Uri, Diagnostic[]][]; + + /** + * Create a diagnostics collection. + * + * @param name The [name](#DiagnosticCollection.name) of the collection. + * @return A new diagnostic collection. + */ + export function createDiagnosticCollection(name?: string): DiagnosticCollection; + /** * Set a [language configuration](#LanguageConfiguration) for a language. * diff --git a/packages/preferences/package.json b/packages/preferences/package.json index 7799839354a6f..6eabbecaecc76 100644 --- a/packages/preferences/package.json +++ b/packages/preferences/package.json @@ -1,13 +1,14 @@ { "name": "@theia/preferences", - "version": "0.3.13", + "version": "0.3.14", "description": "Theia - Preferences Extension", "dependencies": { - "@theia/core": "^0.3.13", - "@theia/editor": "^0.3.13", - "@theia/filesystem": "^0.3.13", - "@theia/userstorage": "^0.3.13", - "@theia/workspace": "^0.3.13", + "@theia/core": "^0.3.14", + "@theia/editor": "^0.3.14", + "@theia/filesystem": "^0.3.14", + "@theia/monaco": "^0.3.14", + "@theia/userstorage": "^0.3.14", + "@theia/workspace": "^0.3.14", "@types/fs-extra": "^4.0.2", "fs-extra": "^4.0.2", "jsonc-parser": "^1.0.1" @@ -45,7 +46,7 @@ "docs": "theiaext docs" }, "devDependencies": { - "@theia/ext-scripts": "^0.3.13" + "@theia/ext-scripts": "^0.3.14" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/preferences/src/browser/abstract-resource-preference-provider.ts b/packages/preferences/src/browser/abstract-resource-preference-provider.ts index c23f56e1c4326..4ecf703084294 100644 --- a/packages/preferences/src/browser/abstract-resource-preference-provider.ts +++ b/packages/preferences/src/browser/abstract-resource-preference-provider.ts @@ -66,12 +66,13 @@ export abstract class AbstractResourcePreferenceProvider extends PreferenceProvi async setPreference(key: string, value: any): Promise { const resource = await this.resource; if (resource.saveContents) { - const content = await resource.readContents(); + const content = await this.readContents(); const formattingOptions = { tabSize: 3, insertSpaces: true, eol: '' }; const edits = jsoncparser.modify(content, [key], value, { formattingOptions }); const result = jsoncparser.applyEdits(content, edits); await resource.saveContents(result); + this.preferences[key] = value; this.onDidPreferencesChangedEmitter.fire(undefined); } } @@ -79,7 +80,7 @@ export abstract class AbstractResourcePreferenceProvider extends PreferenceProvi protected async readPreferences(): Promise { const newContent = await this.readContents(); const strippedContent = jsoncparser.stripComments(newContent); - this.preferences = jsoncparser.parse(strippedContent); + this.preferences = jsoncparser.parse(strippedContent) || {}; this.onDidPreferencesChangedEmitter.fire(undefined); } diff --git a/packages/preferences/src/browser/monaco-contribution.ts b/packages/preferences/src/browser/monaco-contribution.ts new file mode 100644 index 0000000000000..770768423ba71 --- /dev/null +++ b/packages/preferences/src/browser/monaco-contribution.ts @@ -0,0 +1,25 @@ +/******************************************************************************** + * Copyright (C) 2018 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 + ********************************************************************************/ + +monaco.languages.register({ + id: 'jsonc', + 'aliases': [ + 'JSON with Comments' + ], + 'filenames': [ + 'settings.json' + ] +}); diff --git a/packages/preferences/src/browser/monaco.d.ts b/packages/preferences/src/browser/monaco.d.ts new file mode 100644 index 0000000000000..ad8ef7c744a72 --- /dev/null +++ b/packages/preferences/src/browser/monaco.d.ts @@ -0,0 +1 @@ +/// \ No newline at end of file diff --git a/packages/preferences/src/browser/preference-frontend-module.ts b/packages/preferences/src/browser/preference-frontend-module.ts index 63ad43a97aec9..1ef763641b281 100644 --- a/packages/preferences/src/browser/preference-frontend-module.ts +++ b/packages/preferences/src/browser/preference-frontend-module.ts @@ -24,6 +24,8 @@ import { createPreferencesTreeWidget } from './preference-tree-container'; import { PreferencesMenuFactory } from './preferences-menu-factory'; import { PreferencesContainer, PreferencesTreeWidget } from './preferences-tree-widget'; +import './monaco-contribution'; + export function bindPreferences(bind: interfaces.Bind, unbind: interfaces.Unbind): void { unbind(PreferenceProvider); diff --git a/packages/preferences/src/browser/preference-service.spec.ts b/packages/preferences/src/browser/preference-service.spec.ts index ae9f19f938fff..b46612b0f55ca 100644 --- a/packages/preferences/src/browser/preference-service.spec.ts +++ b/packages/preferences/src/browser/preference-service.spec.ts @@ -27,7 +27,7 @@ import * as temp from 'temp'; import { Emitter } from '@theia/core/lib/common'; import { PreferenceService, PreferenceScope, - PreferenceProviders, PreferenceServiceImpl, PreferenceProvider + PreferenceProviderProvider, PreferenceServiceImpl, PreferenceProvider, bindPreferenceSchemaProvider } from '@theia/core/lib/browser/preferences'; import { FileSystem } from '@theia/filesystem/lib/common/'; import { FileSystemWatcher } from '@theia/filesystem/lib/browser/filesystem-watcher'; @@ -62,11 +62,12 @@ const mockWorkspacePreferenceEmitter = new Emitter(); before(async () => { testContainer = new Container(); + bindPreferenceSchemaProvider(testContainer.bind.bind(testContainer)); testContainer.bind(UserPreferenceProvider).toSelf().inSingletonScope(); testContainer.bind(WorkspacePreferenceProvider).toSelf().inSingletonScope(); - testContainer.bind(PreferenceProviders).toFactory(ctx => (scope: PreferenceScope) => { + testContainer.bind(PreferenceProviderProvider).toFactory(ctx => (scope: PreferenceScope) => { const userProvider = ctx.container.get(UserPreferenceProvider); const workspaceProvider = ctx.container.get(WorkspacePreferenceProvider); @@ -143,7 +144,7 @@ describe('Preference Service', function () { beforeEach(() => { prefService = testContainer.get(PreferenceService); const impl = testContainer.get(PreferenceServiceImpl); - impl.onStart(); + impl.initialize(); }); afterEach(() => { @@ -319,10 +320,12 @@ describe('Preference Service', function () { } const container = new Container(); - container.bind(PreferenceProviders).toFactory(ctx => (scope: PreferenceScope) => new SlowProvider()); + bindPreferenceSchemaProvider(container.bind.bind(container)); + container.bind(PreferenceProviderProvider).toFactory(ctx => (scope: PreferenceScope) => new SlowProvider()); container.bind(PreferenceServiceImpl).toSelf().inSingletonScope(); const service = container.get(PreferenceServiceImpl); + service.initialize(); await service.ready; const n = service.getNumber('mypref'); expect(n).to.equal(2); diff --git a/packages/preferences/src/browser/preference-tree-container.ts b/packages/preferences/src/browser/preference-tree-container.ts index 4f5f649a37779..ac65887ecc2bc 100644 --- a/packages/preferences/src/browser/preference-tree-container.ts +++ b/packages/preferences/src/browser/preference-tree-container.ts @@ -17,13 +17,20 @@ import { interfaces } from 'inversify'; import { PreferencesDecorator } from './preferences-decorator'; import { PreferencesDecoratorService } from './preferences-decorator-service'; -import { createTreeContainer, TreeDecoratorService, TreeWidget} from '@theia/core/lib/browser'; +import { + createTreeContainer, + defaultTreeProps, + TreeDecoratorService, + TreeProps, + TreeWidget +} from '@theia/core/lib/browser'; import { PreferencesTreeWidget } from './preferences-tree-widget'; export function createPreferencesTreeWidget(parent: interfaces.Container): PreferencesTreeWidget { const child = createTreeContainer(parent); child.bind(PreferencesTreeWidget).toSelf(); + child.rebind(TreeProps).toConstantValue({ ...defaultTreeProps, search: true }); child.rebind(TreeWidget).toDynamicValue(ctx => ctx.container.get(PreferencesTreeWidget)); bindPreferencesDecorator(child); diff --git a/packages/preferences/src/browser/preferences-contribution.ts b/packages/preferences/src/browser/preferences-contribution.ts index 7c16dd08185e0..986bae58b7f92 100644 --- a/packages/preferences/src/browser/preferences-contribution.ts +++ b/packages/preferences/src/browser/preferences-contribution.ts @@ -15,31 +15,27 @@ ********************************************************************************/ import { injectable, inject, named } from 'inversify'; -import { Command, MenuModelRegistry, CommandRegistry } from '@theia/core'; -import { UserPreferenceProvider } from './user-preference-provider'; +import { MenuModelRegistry, CommandRegistry } from '@theia/core'; import { CommonMenus, PreferenceScope, PreferenceProvider, - AbstractViewContribution + AbstractViewContribution, + CommonCommands } from '@theia/core/lib/browser'; import { WorkspacePreferenceProvider } from './workspace-preference-provider'; import { FileSystem } from '@theia/filesystem/lib/common'; import { UserStorageService } from '@theia/userstorage/lib/browser'; import { PreferencesContainer } from './preferences-tree-widget'; - -export const PREFERENCES_COMMAND: Command = { - id: 'preferences:open', - label: 'Open Preferences' -}; +import { EditorManager } from '@theia/editor/lib/browser'; @injectable() export class PreferencesContribution extends AbstractViewContribution { @inject(UserStorageService) protected readonly userStorageService: UserStorageService; - @inject(PreferenceProvider) @named(PreferenceScope.User) protected readonly userPreferenceProvider: UserPreferenceProvider; @inject(PreferenceProvider) @named(PreferenceScope.Workspace) protected readonly workspacePreferenceProvider: WorkspacePreferenceProvider; @inject(FileSystem) protected readonly filesystem: FileSystem; + @inject(EditorManager) protected readonly editorManager: EditorManager; constructor() { super({ @@ -49,43 +45,30 @@ export class PreferencesContribution extends AbstractViewContribution { + commands.registerCommand(CommonCommands.OPEN_PREFERENCES, { isEnabled: () => true, - execute: () => this.openPreferences() + execute: (preferenceScope = PreferenceScope.User) => this.openPreferences(preferenceScope) }); } registerMenus(menus: MenuModelRegistry): void { menus.registerMenuAction(CommonMenus.FILE_SETTINGS_SUBMENU_OPEN, { - commandId: PREFERENCES_COMMAND.id, + commandId: CommonCommands.OPEN_PREFERENCES.id, order: 'a10' }); } - protected async openPreferences(): Promise { - const userUri = this.userPreferenceProvider.getUri(); - const content = await this.userStorageService.readContents(userUri); - if (content === '') { - await this.userStorageService.saveContents(userUri, this.getPreferenceTemplateForScope('user')); - } - + protected async openPreferences(preferenceScope: PreferenceScope): Promise { const wsUri = await this.workspacePreferenceProvider.getUri(); - if (!wsUri) { - return; - } - if (!(await this.filesystem.exists(wsUri.toString()))) { - await this.filesystem.createFile(wsUri.toString(), { content: this.getPreferenceTemplateForScope('workspace') }); + if (wsUri && !await this.filesystem.exists(wsUri.toString())) { + await this.filesystem.createFile(wsUri.toString()); } + const widget = await this.widget; + widget.preferenceScope = preferenceScope; super.openView({ activate: true }); + widget.activatePreferenceEditor(preferenceScope); } - private getPreferenceTemplateForScope(scope: string): string { - return `/* -Preference file for ${scope} scope - -Please refer to the documentation online (https://github.com/theia-ide/theia/blob/master/packages/preferences/README.md) to learn how preferences work in Theia -*/`; - } } diff --git a/packages/preferences/src/browser/preferences-decorator.ts b/packages/preferences/src/browser/preferences-decorator.ts index b09f9bb39f68b..0c8582bdc6f9b 100644 --- a/packages/preferences/src/browser/preferences-decorator.ts +++ b/packages/preferences/src/browser/preferences-decorator.ts @@ -50,13 +50,12 @@ export class PreferencesDecorator implements TreeDecorator { tooltip: preferenceValue.description, captionSuffixes: [ { - data: storedValue || preferenceValue.default ? ': ' + (storedValue || preferenceValue.default) : undefined, + data: storedValue !== undefined ? ': ' + storedValue : preferenceValue.default !== undefined ? ': ' + preferenceValue.default : undefined, }, { data: ' ' + preferenceValue.description, fontData: {color: 'var(--theia-ui-font-color2)'} - }], - fontData: this.preferencesService.get(preferenceName) ? {style: 'bold'} : undefined + }] }] as [string, TreeDecoration.Data]; })); this.emitter.fire(() => this.preferencesDecorations); diff --git a/packages/preferences/src/browser/preferences-menu-factory.ts b/packages/preferences/src/browser/preferences-menu-factory.ts index d4fcf2e8f1206..883804e6acdc8 100644 --- a/packages/preferences/src/browser/preferences-menu-factory.ts +++ b/packages/preferences/src/browser/preferences-menu-factory.ts @@ -46,8 +46,8 @@ export class PreferencesMenuFactory { const commandTrue = id + '-true'; commands.addCommand(commandTrue, { label: 'true', - iconClass: savedPreference === 'true' || !savedPreference && property.default === true ? 'fa fa-check' : '', - execute: () => execute(id, 'true') + iconClass: savedPreference === true || savedPreference === 'true' || savedPreference === undefined && property.default === true ? 'fa fa-check' : '', + execute: () => execute(id, true) }); menu.addItem({ type: 'command', @@ -57,8 +57,8 @@ export class PreferencesMenuFactory { const commandFalse = id + '-false'; commands.addCommand(commandFalse, { label: 'false', - iconClass: savedPreference === 'false' || !savedPreference && property.default === false ? 'fa fa-check' : '', - execute: () => execute(id, 'false') + iconClass: savedPreference === false || savedPreference === 'false' || savedPreference === undefined && property.default === false ? 'fa fa-check' : '', + execute: () => execute(id, false) }); menu.addItem({ type: 'command', diff --git a/packages/preferences/src/browser/preferences-tree-widget.ts b/packages/preferences/src/browser/preferences-tree-widget.ts index 5bf68cca968cf..eec26e255b392 100644 --- a/packages/preferences/src/browser/preferences-tree-widget.ts +++ b/packages/preferences/src/browser/preferences-tree-widget.ts @@ -19,7 +19,7 @@ import { Message } from '@phosphor/messaging'; import { PreferencesMenuFactory } from './preferences-menu-factory'; import { PreferencesDecorator } from './preferences-decorator'; import { toArray } from '@phosphor/algorithm'; -import { DockPanel, SplitPanel, Widget } from '@phosphor/widgets'; +import { BoxPanel, DockPanel, SplitPanel, Widget } from '@phosphor/widgets'; import { ApplicationShell, ContextMenuRenderer, @@ -43,12 +43,17 @@ import { EditorWidget, EditorManager } from '@theia/editor/lib/browser'; import { DisposableCollection, Emitter, Event, MessageService } from '@theia/core'; import { Deferred } from '@theia/core/lib/common/promise-util'; +export interface PreferencesEditorWidget extends EditorWidget { + scope?: PreferenceScope; +} + @injectable() export class PreferencesContainer extends SplitPanel implements ApplicationShell.TrackableWidgetProvider, Saveable { static ID = 'preferences_container_widget'; protected treeWidget: PreferencesTreeWidget | undefined; + protected editorsContainer: PreferencesEditorsContainer; private currentEditor: EditorWidget | undefined; private readonly editors: EditorWidget[] = []; private deferredEditors = new Deferred(); @@ -79,6 +84,8 @@ export class PreferencesContainer extends SplitPanel implements ApplicationShell @inject(PreferenceProvider) @named(PreferenceScope.Workspace) protected readonly workspacePreferenceProvider: WorkspacePreferenceProvider; + protected _preferenceScope: PreferenceScope = PreferenceScope.User; + @postConstruct() protected init(): void { this.id = PreferencesContainer.ID; @@ -113,6 +120,14 @@ export class PreferencesContainer extends SplitPanel implements ApplicationShell return this.deferredEditors.promise; } + get preferenceScope(): PreferenceScope { + return this._preferenceScope; + } + + set preferenceScope(preferenceScope: PreferenceScope) { + this._preferenceScope = preferenceScope; + } + protected async onAfterAttach(msg: Message): Promise { this.treeWidget = await this.widgetManager.getOrCreateWidget(PreferencesTreeWidget.ID); this.treeWidget.onPreferenceSelected(value => { @@ -129,10 +144,10 @@ export class PreferencesContainer extends SplitPanel implements ApplicationShell } }); - const editorsContainer = new PreferencesEditorsContainer(this.editorManager, this.userPreferenceProvider, this.workspacePreferenceProvider); - this.toDispose.push(editorsContainer); - editorsContainer.onInit(() => { - toArray(editorsContainer.widgets()).forEach(editor => { + this.editorsContainer = new PreferencesEditorsContainer(this.editorManager, this.userPreferenceProvider, this.workspacePreferenceProvider, this.preferenceScope); + this.toDispose.push(this.editorsContainer); + this.editorsContainer.onInit(() => { + toArray(this.editorsContainer.widgets()).forEach(editor => { const editorWidget = editor as EditorWidget; this.editors.push(editorWidget); const savable = editorWidget.saveable; @@ -142,15 +157,17 @@ export class PreferencesContainer extends SplitPanel implements ApplicationShell }); this.deferredEditors.resolve(this.editors); }); - editorsContainer.onEditorChanged(editor => { + this.editorsContainer.onEditorChanged(editor => { if (this.currentEditor) { this.currentEditor.saveable.save(); } this.currentEditor = editor; }); - this.addWidget(this.treeWidget); - this.addWidget(editorsContainer); + const treePanel = new BoxPanel(); + treePanel.addWidget(this.treeWidget); + this.addWidget(treePanel); + this.addWidget(this.editorsContainer); this.treeWidget.activate(); super.onAfterAttach(msg); } @@ -163,10 +180,19 @@ export class PreferencesContainer extends SplitPanel implements ApplicationShell } protected onCloseRequest(msg: Message) { - this.widgets.forEach(widget => widget.close()); + if (this.treeWidget) { + this.treeWidget.close(); + } + this.editorsContainer.close(); super.onCloseRequest(msg); this.dispose(); } + + public activatePreferenceEditor(preferenceScope: PreferenceScope) { + if (this.editorsContainer) { + this.editorsContainer.activatePreferenceEditor(preferenceScope); + } + } } export class PreferencesEditorsContainer extends DockPanel { @@ -185,13 +211,15 @@ export class PreferencesEditorsContainer extends DockPanel { constructor( protected readonly editorManager: EditorManager, protected readonly userPreferenceProvider: UserPreferenceProvider, - protected readonly workspacePreferenceProvider: WorkspacePreferenceProvider + protected readonly workspacePreferenceProvider: WorkspacePreferenceProvider, + protected preferenceScope: PreferenceScope, ) { super(); } dispose(): void { this.toDispose.dispose(); + super.dispose(); } onCloseRequest(msg: Message) { @@ -206,20 +234,36 @@ export class PreferencesEditorsContainer extends DockPanel { } protected async onAfterAttach(msg: Message): Promise { - const userPreferences = await this.editorManager.getOrCreateByUri(this.userPreferenceProvider.getUri()); + const userPreferences = await this.editorManager.getOrCreateByUri(this.userPreferenceProvider.getUri()) as PreferencesEditorWidget; userPreferences.title.label = 'User Preferences'; + userPreferences.scope = PreferenceScope.User; this.addWidget(userPreferences); const workspacePreferenceUri = await this.workspacePreferenceProvider.getUri(); - const workspacePreferences = workspacePreferenceUri && await this.editorManager.getOrCreateByUri(workspacePreferenceUri); + const workspacePreferences = workspacePreferenceUri && await this.editorManager.getOrCreateByUri(workspacePreferenceUri) as PreferencesEditorWidget; + if (workspacePreferences) { workspacePreferences.title.label = 'Workspace Preferences'; + workspacePreferences.scope = PreferenceScope.Workspace; this.addWidget(workspacePreferences); } + this.activatePreferenceEditor(this.preferenceScope); super.onAfterAttach(msg); this.onInitEmitter.fire(undefined); } + + public activatePreferenceEditor(preferenceScope: PreferenceScope) { + this.preferenceScope = preferenceScope; + for (const widget of toArray(this.widgets())) { + const preferenceEditor = widget as PreferencesEditorWidget; + if (preferenceEditor.scope === preferenceScope) { + this.activateWidget(widget); + break; + } + } + } + } @injectable() @@ -266,6 +310,7 @@ export class PreferencesTreeWidget extends TreeWidget { dispose(): void { this.toDispose.dispose(); + super.dispose(); } protected onAfterAttach(msg: Message): void { diff --git a/packages/preview/package.json b/packages/preview/package.json index c8a765f0807f4..b61000f079e3a 100644 --- a/packages/preview/package.json +++ b/packages/preview/package.json @@ -1,12 +1,12 @@ { "name": "@theia/preview", - "version": "0.3.13", + "version": "0.3.14", "description": "Theia - Preview Extension", "dependencies": { - "@theia/core": "^0.3.13", - "@theia/editor": "^0.3.13", - "@theia/languages": "^0.3.13", - "@theia/mini-browser": "^0.3.13", + "@theia/core": "^0.3.14", + "@theia/editor": "^0.3.14", + "@theia/languages": "^0.3.14", + "@theia/mini-browser": "^0.3.14", "@types/highlight.js": "^9.12.2", "@types/markdown-it": "^0.0.4", "@types/markdown-it-anchor": "^4.0.1", @@ -47,7 +47,7 @@ "docs": "theiaext docs" }, "devDependencies": { - "@theia/ext-scripts": "^0.3.13" + "@theia/ext-scripts": "^0.3.14" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/preview/src/browser/markdown/markdown-preview-handler.spec.ts b/packages/preview/src/browser/markdown/markdown-preview-handler.spec.ts index 8becc4310d68d..a36465162d327 100644 --- a/packages/preview/src/browser/markdown/markdown-preview-handler.spec.ts +++ b/packages/preview/src/browser/markdown/markdown-preview-handler.spec.ts @@ -48,13 +48,23 @@ describe('markdown-preview-handler', () => { }); it('renders html with line information', async () => { - const contentElement = await previewHandler.renderContent({ content: exampleMarkdown1, originUri: new URI('') }); - expect(contentElement.innerHTML).equals(exampleHtml1); + await assertRenderedContent(exampleMarkdown1, exampleHtml1); }); it('renders images', async () => { - const contentElement = await previewHandler.renderContent({ content: exampleMarkdown2, originUri: new URI('file:///Users/me/workspace/DEMO.md') }); - expect(contentElement.innerHTML).equals(exampleHtml2); + await assertRenderedContent(exampleMarkdown2, exampleHtml2); + }); + + it('renders HTML image as block', async () => { + await assertRenderedContent(exampleMarkdown3, exampleHtml3); + }); + + it('renders HTML images inlined', async () => { + await assertRenderedContent(exampleMarkdown4, exampleHtml4); + }); + + it('renders multiple HTML images in a html block', async () => { + await assertRenderedContent(exampleMarkdown5, exampleHtml5); }); it('finds element for source line', () => { @@ -96,6 +106,11 @@ describe('markdown-preview-handler', () => { }); }); +async function assertRenderedContent(source: string, expectation: string) { + const contentElement = await previewHandler.renderContent({ content: source, originUri: new URI('file:///workspace/DEMO.md') }); + expect(contentElement.innerHTML).equals(expectation); +} + const exampleMarkdown1 = // `# Theia - Preview Extension Shows a preview of supported resources. @@ -120,7 +135,53 @@ const exampleMarkdown2 = // const exampleHtml2 = // `

Heading

-

alternativetext

+

alternativetext

+`; + +const exampleMarkdown3 = // + `# Block HTML Image +tada + +# Block HTML Image + tada +`; + +const exampleHtml3 = // + `

Block HTML Image

+tada +

Block HTML Image

+tada +`; + +const exampleMarkdown4 = // + `# Inlined HTML Image +text in paragraph tada +`; + +const exampleHtml4 = // + `

Inlined HTML Image

+

text in paragraph tada

+`; + +const exampleMarkdown5 = // + `# Multiple HTML Images nested in blocks +word

+tada +

+ +

+tada +

+`; + +const exampleHtml5 = // + `

Multiple HTML Images nested in blocks

+

word

+tada

+

+

+tada +

`; /** diff --git a/packages/preview/src/browser/markdown/markdown-preview-handler.ts b/packages/preview/src/browser/markdown/markdown-preview-handler.ts index aa71f1152b177..09c2c37c50f42 100644 --- a/packages/preview/src/browser/markdown/markdown-preview-handler.ts +++ b/packages/preview/src/browser/markdown/markdown-preview-handler.ts @@ -219,7 +219,7 @@ export class MarkdownPreviewHandler implements PreviewHandler { return '
' + engine.utils.escapeHtml(str) + '
'; } }); - const renderers = ['heading_open', 'paragraph_open', 'list_item_open', 'blockquote_open', 'code_block', 'image']; + const renderers = ['heading_open', 'paragraph_open', 'list_item_open', 'blockquote_open', 'code_block', 'image', 'fence']; for (const renderer of renderers) { const originalRenderer = engine.renderer.rules[renderer]; engine.renderer.rules[renderer] = (tokens, index, options, env, self) => { @@ -249,6 +249,44 @@ export class MarkdownPreviewHandler implements PreviewHandler { } return originalImageRenderer(tokens, index, options, env, self); }; + + const domParser = new DOMParser(); + + const parseDOM = (html: string) => + domParser.parseFromString(html, 'text/html').getElementsByTagName('body')[0] as HTMLElement; + + const modifyDOM = (body: HTMLElement, tag: string, procedure: (element: Element) => void) => { + const elements = body.getElementsByTagName(tag); + for (let i = 0; i < elements.length; i++) { + const element = elements.item(i); + procedure(element); + } + }; + + const normalizeAllImgSrcInHTML = (html: string, normalizeLink: (link: string) => string) => { + const body = parseDOM(html); + modifyDOM(body, 'img', img => { + const src = img.getAttributeNode('src'); + if (src) { + src.nodeValue = normalizeLink(src.nodeValue || ''); + } + }); + return body.innerHTML; + }; + + for (const name of ['html_block', 'html_inline']) { + const originalRenderer = engine.renderer.rules[name]; + engine.renderer.rules[name] = (tokens, index, options, env, self) => { + const currentToken = tokens[index]; + const content = currentToken.content; + if (content.includes(' this.linkNormalizer.normalizeLink(documentUri, link)); + } + return originalRenderer(tokens, index, options, env, self); + }; + } + anchor(engine, {}); } return this.engine; diff --git a/packages/process/package.json b/packages/process/package.json index 252055a8699a6..56a7adc8226ac 100644 --- a/packages/process/package.json +++ b/packages/process/package.json @@ -1,9 +1,9 @@ { "name": "@theia/process", - "version": "0.3.13", + "version": "0.3.14", "description": "Theia process support.", "dependencies": { - "@theia/core": "^0.3.13", + "@theia/core": "^0.3.14", "node-pty": "0.7.6", "string-argv": "0.0.2" }, @@ -40,7 +40,7 @@ "docs": "theiaext docs" }, "devDependencies": { - "@theia/ext-scripts": "^0.3.13" + "@theia/ext-scripts": "^0.3.14" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/process/src/node/multi-ring-buffer.spec.ts b/packages/process/src/node/multi-ring-buffer.spec.ts index 89ab84f38e5a2..4afc15b880578 100644 --- a/packages/process/src/node/multi-ring-buffer.spec.ts +++ b/packages/process/src/node/multi-ring-buffer.spec.ts @@ -21,6 +21,14 @@ const expect = chai.expect; describe('MultiRingBuffer', function () { + it('expect buffer to be empty initialized', function () { + const size = 2; + const compareTo = new Buffer('0000', 'hex'); + const ringBuffer = new MultiRingBuffer({ size }); + // tslint:disable-next-line:no-unused-expression + expect(ringBuffer['buffer'].equals(compareTo)).to.be.true; + }); + it('expect enq and deq a string with unicode characters > 1 byte and no wrap around', function () { const ringBufferSize = 15; const ringBuffer = new MultiRingBuffer({ size: ringBufferSize }); diff --git a/packages/process/src/node/multi-ring-buffer.ts b/packages/process/src/node/multi-ring-buffer.ts index fde001720a020..6a118ce25ddb5 100644 --- a/packages/process/src/node/multi-ring-buffer.ts +++ b/packages/process/src/node/multi-ring-buffer.ts @@ -96,14 +96,16 @@ export class MultiRingBuffer { protected readonly streams: Map; protected readerId = 0; - constructor( @inject(MultiRingBufferOptions) protected readonly options: MultiRingBufferOptions) { + constructor( + @inject(MultiRingBufferOptions) protected readonly options: MultiRingBufferOptions + ) { this.maxSize = options.size; if (options.encoding !== undefined) { this.encoding = options.encoding; } else { this.encoding = 'utf8'; } - this.buffer = Buffer.alloc(this.maxSize, this.encoding); + this.buffer = Buffer.alloc(this.maxSize); this.readers = new Map(); this.streams = new Map(); } @@ -199,7 +201,7 @@ export class MultiRingBuffer { deqSize = Math.min(size, maxDeqSize); } - if (wrapped === false) { // no warp + if (wrapped === false) { // no wrap buffer = this.buffer.toString(encoding, pos, pos + deqSize); } else { // wrap buffer = buffer.concat(this.buffer.toString(encoding, pos, this.maxSize), diff --git a/packages/process/src/node/process-manager.ts b/packages/process/src/node/process-manager.ts index d971f868606b9..11f177ce42ba0 100644 --- a/packages/process/src/node/process-manager.ts +++ b/packages/process/src/node/process-manager.ts @@ -26,7 +26,9 @@ export class ProcessManager implements BackendApplicationContribution { protected readonly processes: Map; protected readonly deleteEmitter: Emitter; - constructor(@inject(ILogger) @named('process') protected logger: ILogger) { + constructor( + @inject(ILogger) @named('process') protected logger: ILogger + ) { this.processes = new Map(); this.deleteEmitter = new Emitter(); } diff --git a/packages/process/src/node/process.ts b/packages/process/src/node/process.ts index b6c70e7f49686..a66718850d10d 100644 --- a/packages/process/src/node/process.ts +++ b/packages/process/src/node/process.ts @@ -47,8 +47,8 @@ export abstract class Process { protected readonly processManager: ProcessManager, protected readonly logger: ILogger, @unmanaged() protected readonly type: ProcessType, - protected readonly options: ProcessOptions) { - + protected readonly options: ProcessOptions + ) { this.exitEmitter = new Emitter(); this.errorEmitter = new Emitter(); this.id = this.processManager.register(this); diff --git a/packages/process/src/node/raw-process.ts b/packages/process/src/node/raw-process.ts index 25270098d0efe..9da486ab7774c 100644 --- a/packages/process/src/node/raw-process.ts +++ b/packages/process/src/node/raw-process.ts @@ -56,7 +56,8 @@ export class RawProcess extends Process { constructor( @inject(RawProcessOptions) options: RawProcessOptions, @inject(ProcessManager) processManager: ProcessManager, - @inject(ILogger) @named('process') logger: ILogger) { + @inject(ILogger) @named('process') logger: ILogger + ) { super(processManager, logger, ProcessType.Raw, options); this.logger.debug(`Starting raw process: ${options.command},` diff --git a/packages/process/src/node/terminal-process.ts b/packages/process/src/node/terminal-process.ts index 9625f1961c6f0..75fc97f642b24 100644 --- a/packages/process/src/node/terminal-process.ts +++ b/packages/process/src/node/terminal-process.ts @@ -39,7 +39,8 @@ export class TerminalProcess extends Process { @inject(TerminalProcessOptions) options: TerminalProcessOptions, @inject(ProcessManager) processManager: ProcessManager, @inject(MultiRingBuffer) protected readonly ringBuffer: MultiRingBuffer, - @inject(ILogger) @named('process') logger: ILogger) { + @inject(ILogger) @named('process') logger: ILogger + ) { super(processManager, logger, ProcessType.Terminal, options); this.logger.debug(`Starting terminal process: ${options.command},` diff --git a/packages/python/package.json b/packages/python/package.json index fb0a3df7cf7f2..ad0921774b10c 100644 --- a/packages/python/package.json +++ b/packages/python/package.json @@ -1,11 +1,11 @@ { "name": "@theia/python", - "version": "0.3.13", + "version": "0.3.14", "description": "Theia - Python Extension", "dependencies": { - "@theia/core": "^0.3.13", - "@theia/languages": "^0.3.13", - "@theia/monaco": "^0.3.13" + "@theia/core": "^0.3.14", + "@theia/languages": "^0.3.14", + "@theia/monaco": "^0.3.14" }, "publishConfig": { "access": "public" @@ -42,7 +42,7 @@ "docs": "theiaext docs" }, "devDependencies": { - "@theia/ext-scripts": "^0.3.13" + "@theia/ext-scripts": "^0.3.14" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/python/src/browser/python-grammar-contribution.ts b/packages/python/src/browser/python-grammar-contribution.ts index a3ec0db37e042..f522b291112fc 100644 --- a/packages/python/src/browser/python-grammar-contribution.ts +++ b/packages/python/src/browser/python-grammar-contribution.ts @@ -70,7 +70,7 @@ export class PythonGrammarContribution implements LanguageGrammarDefinitionContr monaco.languages.setLanguageConfiguration(PYTHON_LANGUAGE_ID, this.config); const platformGrammar = require('../../data/MagicPython.tmLanguage.json'); - registry.registerTextMateGrammarScope('source.python', { + registry.registerTextmateGrammarScope('source.python', { async getGrammarDefinition() { return { format: 'json', @@ -80,7 +80,7 @@ export class PythonGrammarContribution implements LanguageGrammarDefinitionContr }); const cGrammar = require('../../data/MagicRegExp.tmLanguage.json'); - registry.registerTextMateGrammarScope('source.regexp.python', { + registry.registerTextmateGrammarScope('source.regexp.python', { async getGrammarDefinition() { return { format: 'json', diff --git a/packages/search-in-workspace/package.json b/packages/search-in-workspace/package.json index 4ed0e622512ba..d2d5d0526ce48 100644 --- a/packages/search-in-workspace/package.json +++ b/packages/search-in-workspace/package.json @@ -1,10 +1,14 @@ { "name": "@theia/search-in-workspace", - "version": "0.3.13", + "version": "0.3.14", "description": "Theia - Search in workspace", "dependencies": { - "@theia/core": "^0.3.13", - "@theia/editor": "^0.3.13", + "@theia/core": "^0.3.14", + "@theia/editor": "^0.3.14", + "@theia/workspace": "^0.3.14", + "@theia/filesystem": "^0.3.14", + "@theia/navigator": "^0.3.14", + "@theia/process": "^0.3.14", "vscode-ripgrep": "^1.0.1" }, "publishConfig": { @@ -41,6 +45,6 @@ "docs": "theiaext docs" }, "devDependencies": { - "@theia/ext-scripts": "^0.3.13" + "@theia/ext-scripts": "^0.3.14" } } diff --git a/packages/search-in-workspace/src/browser/search-in-workspace-frontend-contribution.ts b/packages/search-in-workspace/src/browser/search-in-workspace-frontend-contribution.ts index 7202671a47f97..8443ad57b729a 100644 --- a/packages/search-in-workspace/src/browser/search-in-workspace-frontend-contribution.ts +++ b/packages/search-in-workspace/src/browser/search-in-workspace-frontend-contribution.ts @@ -14,7 +14,7 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { AbstractViewContribution, KeybindingRegistry, LabelProvider, CommonMenus } from '@theia/core/lib/browser'; +import { AbstractViewContribution, KeybindingRegistry, LabelProvider, CommonMenus, FrontendApplication, FrontendApplicationContribution } from '@theia/core/lib/browser'; import { SearchInWorkspaceWidget } from './search-in-workspace-widget'; import { injectable, inject } from 'inversify'; import { CommandRegistry, MenuModelRegistry, SelectionService } from '@theia/core'; @@ -38,7 +38,7 @@ export namespace SearchInWorkspaceCommands { } @injectable() -export class SearchInWorkspaceFrontendContribution extends AbstractViewContribution { +export class SearchInWorkspaceFrontendContribution extends AbstractViewContribution implements FrontendApplicationContribution { @inject(SelectionService) protected readonly selectionService: SelectionService; @inject(LabelProvider) protected readonly labelProvider: LabelProvider; @@ -54,6 +54,10 @@ export class SearchInWorkspaceFrontendContribution extends AbstractViewContribut }); } + async initializeLayout(app: FrontendApplication): Promise { + await this.openView({activate: false}); + } + registerCommands(commands: CommandRegistry): void { super.registerCommands(commands); commands.registerCommand(SearchInWorkspaceCommands.OPEN_SIW_WIDGET, { diff --git a/packages/search-in-workspace/src/browser/search-in-workspace-frontend-module.ts b/packages/search-in-workspace/src/browser/search-in-workspace-frontend-module.ts index 2a018c51ff7a1..71d4161786f1b 100644 --- a/packages/search-in-workspace/src/browser/search-in-workspace-frontend-module.ts +++ b/packages/search-in-workspace/src/browser/search-in-workspace-frontend-module.ts @@ -17,7 +17,7 @@ import { ContainerModule, interfaces } from 'inversify'; import { SearchInWorkspaceService, SearchInWorkspaceClientImpl } from './search-in-workspace-service'; import { SearchInWorkspaceServer } from '../common/search-in-workspace-interface'; -import { WebSocketConnectionProvider, WidgetFactory, createTreeContainer, TreeWidget, bindViewContribution } from '@theia/core/lib/browser'; +import { WebSocketConnectionProvider, WidgetFactory, createTreeContainer, TreeWidget, bindViewContribution, FrontendApplicationContribution } from '@theia/core/lib/browser'; import { ResourceResolver } from '@theia/core'; import { SearchInWorkspaceWidget } from './search-in-workspace-widget'; import { SearchInWorkspaceResultTreeWidget } from './search-in-workspace-result-tree-widget'; @@ -35,6 +35,7 @@ export default new ContainerModule(bind => { bind(SearchInWorkspaceResultTreeWidget).toDynamicValue(ctx => createSearchTreeWidget(ctx.container)); bindViewContribution(bind, SearchInWorkspaceFrontendContribution); + bind(FrontendApplicationContribution).toService(SearchInWorkspaceFrontendContribution); // The object that gets notified of search results. bind(SearchInWorkspaceClientImpl).toSelf().inSingletonScope(); diff --git a/packages/search-in-workspace/src/browser/search-in-workspace-result-tree-widget.tsx b/packages/search-in-workspace/src/browser/search-in-workspace-result-tree-widget.tsx index 8020a538d863c..78b642a14d3b3 100644 --- a/packages/search-in-workspace/src/browser/search-in-workspace-result-tree-widget.tsx +++ b/packages/search-in-workspace/src/browser/search-in-workspace-result-tree-widget.tsx @@ -31,7 +31,7 @@ import { import { SearchInWorkspaceResult, SearchInWorkspaceOptions } from '../common/search-in-workspace-interface'; import { SearchInWorkspaceService } from './search-in-workspace-service'; import { TreeProps } from '@theia/core/lib/browser'; -import { EditorManager, EditorDecoration, TrackedRangeStickiness, OverviewRulerLane, EditorWidget, ReplaceOperation } from '@theia/editor/lib/browser'; +import { EditorManager, EditorDecoration, TrackedRangeStickiness, OverviewRulerLane, EditorWidget, ReplaceOperation, EditorOpenerOptions } from '@theia/editor/lib/browser'; import { inject, injectable, postConstruct } from 'inversify'; import URI from '@theia/core/lib/common/uri'; import { Path, CancellationTokenSource, Emitter, Event } from '@theia/core'; @@ -72,8 +72,8 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget { private cancelIndicator = new CancellationTokenSource(); - protected changeEmitter: Emitter>; - protected focusInputEmitter: Emitter; + protected changeEmitter = new Emitter>(); + protected focusInputEmitter = new Emitter(); @inject(SearchInWorkspaceService) protected readonly searchService: SearchInWorkspaceService; @inject(EditorManager) protected readonly editorManager: EditorManager; @@ -105,6 +105,7 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget { } })); + this.resultTree = new Map(); this.toDispose.push(model.onNodeRefreshed(() => this.changeEmitter.fire(this.resultTree))); } @@ -121,8 +122,8 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget { } }); - this.changeEmitter = new Emitter(); - this.focusInputEmitter = new Emitter(); + this.toDispose.push(this.changeEmitter); + this.toDispose.push(this.focusInputEmitter); this.toDispose.push(this.editorManager.onActiveEditorChanged(() => { this.updateCurrentEditorDecorations(); @@ -153,6 +154,7 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget { async search(searchTerm: string, searchOptions: SearchInWorkspaceOptions): Promise { this.searchTerm = searchTerm; + this.resultTree.clear(); this.resultTree = new Map(); this.cancelIndicator.cancel(); this.cancelIndicator = new CancellationTokenSource(); @@ -167,7 +169,8 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget { return; } const { name, path } = this.filenameAndPath(result.file); - let resultElement = this.resultTree.get(result.file); + const tree = this.resultTree; + let resultElement = tree.get(result.file); if (resultElement) { const resultLine = this.createResultLineNode(result, resultElement); @@ -191,7 +194,7 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget { file: result.file }; resultElement.children.push(this.createResultLineNode(result, resultElement)); - this.resultTree.set(result.file, resultElement); + tree.set(result.file, resultElement); } } }, @@ -423,7 +426,8 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget { } else { fileUri = new URI(node.file).withScheme('file'); } - const editorWidget = await this.editorManager.open(fileUri, { + + const opts: EditorOpenerOptions | undefined = !DiffUris.isDiffUri(fileUri) ? { selection: { start: { line: node.line - 1, @@ -435,9 +439,13 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget { } }, mode: 'reveal' - }); + } : undefined; - this.decorateEditor(resultNode, editorWidget); + const editorWidget = await this.editorManager.open(fileUri, opts); + + if (!DiffUris.isDiffUri(fileUri)) { + this.decorateEditor(resultNode, editorWidget); + } return editorWidget; } @@ -461,14 +469,16 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget { } protected decorateEditor(node: SearchInWorkspaceResultNode | undefined, editorWidget: EditorWidget) { - const key = `${editorWidget.editor.uri.toString()}#search-in-workspace-matches`; - const oldDecorations = this.appliedDecorations.get(key) || []; - const newDecorations = this.createEditorDecorations(node); - const appliedDecorations = editorWidget.editor.deltaDecorations({ - newDecorations, - oldDecorations, - }); - this.appliedDecorations.set(key, appliedDecorations); + if (!DiffUris.isDiffUri(editorWidget.editor.uri)) { + const key = `${editorWidget.editor.uri.toString()}#search-in-workspace-matches`; + const oldDecorations = this.appliedDecorations.get(key) || []; + const newDecorations = this.createEditorDecorations(node); + const appliedDecorations = editorWidget.editor.deltaDecorations({ + newDecorations, + oldDecorations, + }); + this.appliedDecorations.set(key, appliedDecorations); + } } protected createEditorDecorations(resultNode: SearchInWorkspaceResultNode | undefined): EditorDecoration[] { diff --git a/packages/search-in-workspace/src/browser/search-in-workspace-service.ts b/packages/search-in-workspace/src/browser/search-in-workspace-service.ts index 1b77fc7106e77..510787157c3a7 100644 --- a/packages/search-in-workspace/src/browser/search-in-workspace-service.ts +++ b/packages/search-in-workspace/src/browser/search-in-workspace-service.ts @@ -17,7 +17,6 @@ import { injectable, inject, postConstruct } from 'inversify'; import { SearchInWorkspaceServer, SearchInWorkspaceClient, SearchInWorkspaceResult, SearchInWorkspaceOptions } from '../common/search-in-workspace-interface'; import { WorkspaceService } from '@theia/workspace/lib/browser'; -import URI from '@theia/core/lib/common/uri'; import { ILogger } from '@theia/core'; /** @@ -112,8 +111,7 @@ export class SearchInWorkspaceService implements SearchInWorkspaceClient { throw new Error('Search failed: no workspace root.'); } - const rootUri = new URI(root.uri); - const searchId = await this.searchServer.search(what, rootUri.path.toString(), opts); + const searchId = await this.searchServer.search(what, root.uri, opts); this.pendingSearches.set(searchId, callbacks); this.lastKnownSearchId = searchId; diff --git a/packages/search-in-workspace/src/browser/search-in-workspace-widget.tsx b/packages/search-in-workspace/src/browser/search-in-workspace-widget.tsx index 8e66f071f4a13..34854388209b1 100644 --- a/packages/search-in-workspace/src/browser/search-in-workspace-widget.tsx +++ b/packages/search-in-workspace/src/browser/search-in-workspace-widget.tsx @@ -20,6 +20,7 @@ import { SearchInWorkspaceResultTreeWidget } from './search-in-workspace-result- import { SearchInWorkspaceOptions } from '../common/search-in-workspace-interface'; import * as React from 'react'; import * as ReactDOM from 'react-dom'; +import { Disposable } from '@theia/core/lib/common'; export interface SearchFieldState { className: string; @@ -109,6 +110,8 @@ export class SearchInWorkspaceWidget extends BaseWidget implements StatefulWidge this.toDispose.push(this.resultTreeWidget.onFocusInput(b => { this.focusInputField(); })); + + this.toDispose.push(this.resultTreeWidget); } storeState(): object { @@ -156,6 +159,9 @@ export class SearchInWorkspaceWidget extends BaseWidget implements StatefulWidge super.onAfterAttach(msg); ReactDOM.render({this.renderSearchHeader()}, this.searchFormContainer); Widget.attach(this.resultTreeWidget, this.contentNode); + this.toDisposeOnDetach.push(Disposable.create(() => { + Widget.detach(this.resultTreeWidget); + })); } protected onUpdateRequest(msg: Message) { diff --git a/packages/search-in-workspace/src/browser/styles/index.css b/packages/search-in-workspace/src/browser/styles/index.css index b20648d24644e..67b07a8b64709 100644 --- a/packages/search-in-workspace/src/browser/styles/index.css +++ b/packages/search-in-workspace/src/browser/styles/index.css @@ -263,6 +263,8 @@ .t-siw-search-container .resultLine .match { background: var(--theia-word-highlight-match-color1); + display: inline-block; + line-height: normal; } .t-siw-search-container .resultLine .match.strike-through { diff --git a/packages/search-in-workspace/src/common/search-in-workspace-interface.ts b/packages/search-in-workspace/src/common/search-in-workspace-interface.ts index aa4ad903b8dfd..a2b219a6a1d8c 100644 --- a/packages/search-in-workspace/src/common/search-in-workspace-interface.ts +++ b/packages/search-in-workspace/src/common/search-in-workspace-interface.ts @@ -116,7 +116,7 @@ export interface SearchInWorkspaceServer extends JsonRpcServer; + search(what: string, rootUri: string, opts?: SearchInWorkspaceOptions): Promise; /** * Cancel an ongoing search. diff --git a/packages/search-in-workspace/src/node/ripgrep-search-in-workspace-server.slow-spec.ts b/packages/search-in-workspace/src/node/ripgrep-search-in-workspace-server.slow-spec.ts index b9648492b289e..62b2d70c14608 100644 --- a/packages/search-in-workspace/src/node/ripgrep-search-in-workspace-server.slow-spec.ts +++ b/packages/search-in-workspace/src/node/ripgrep-search-in-workspace-server.slow-spec.ts @@ -25,6 +25,7 @@ import { ILogger, isWindows } from '@theia/core'; import { MockLogger } from '@theia/core/lib/common/test/mock-logger'; import { RawProcessFactory, RawProcessOptions, RawProcess, ProcessManager } from '@theia/process/lib/node'; import * as path from 'path'; +import { FileUri } from '@theia/core/lib/node/file-uri'; // Allow creating temporary files, but remove them when we are done. const track = temp.track(); @@ -169,7 +170,7 @@ function compareSearchResults(expected: SearchInWorkspaceResult[], actual: Searc if (lines) { const line = lines[e.line - 1]; e.lineText = line; - e.file = path.join(rootDir, e.file); + e.file = FileUri.fsPath(path.join(rootDir, e.file)); expect(a).deep.eq(e); } else { diff --git a/packages/search-in-workspace/src/node/ripgrep-search-in-workspace-server.ts b/packages/search-in-workspace/src/node/ripgrep-search-in-workspace-server.ts index 36776703bf348..3be962924bdc7 100644 --- a/packages/search-in-workspace/src/node/ripgrep-search-in-workspace-server.ts +++ b/packages/search-in-workspace/src/node/ripgrep-search-in-workspace-server.ts @@ -19,6 +19,7 @@ import { ILogger } from '@theia/core'; import { inject, injectable } from 'inversify'; import { RawProcess, RawProcessFactory, RawProcessOptions } from '@theia/process/lib/node'; import { rgPath } from 'vscode-ripgrep'; +import { FileUri } from '@theia/core/lib/node/file-uri'; @injectable() export class RipgrepSearchInWorkspaceServer implements SearchInWorkspaceServer { @@ -78,7 +79,7 @@ export class RipgrepSearchInWorkspaceServer implements SearchInWorkspaceServer { } // Search for the string WHAT in directory ROOT. Return the assigned search id. - search(what: string, root: string, opts?: SearchInWorkspaceOptions): Promise { + search(what: string, rootUri: string, opts?: SearchInWorkspaceOptions): Promise { // Start the rg process. Use --vimgrep to get one result per // line, --color=always to get color control characters that // we'll use to parse the lines. @@ -101,7 +102,7 @@ export class RipgrepSearchInWorkspaceServer implements SearchInWorkspaceServer { } const processOptions: RawProcessOptions = { command: rgPath, - args: [...args, what, ...globs, root] + args: [...args, what, ...globs, FileUri.fsPath(rootUri)] }; const process: RawProcess = this.rawProcessFactory(processOptions); this.ongoingSearches.set(searchId, process); diff --git a/packages/task/package.json b/packages/task/package.json index 8f0728ef09d35..21dcb7c4e29f7 100644 --- a/packages/task/package.json +++ b/packages/task/package.json @@ -1,14 +1,14 @@ { "name": "@theia/task", - "version": "0.3.13", + "version": "0.3.14", "description": "Theia - Task extension. This extension adds support for executing raw or terminal processes in the backend.", "dependencies": { - "@theia/core": "^0.3.13", - "@theia/markers": "^0.3.13", - "@theia/process": "^0.3.13", - "@theia/terminal": "^0.3.13", - "@theia/variable-resolver": "^0.3.13", - "@theia/workspace": "^0.3.13", + "@theia/core": "^0.3.14", + "@theia/markers": "^0.3.14", + "@theia/process": "^0.3.14", + "@theia/terminal": "^0.3.14", + "@theia/variable-resolver": "^0.3.14", + "@theia/workspace": "^0.3.14", "jsonc-parser": "^1.0.1" }, "publishConfig": { @@ -45,7 +45,7 @@ "docs": "theiaext docs" }, "devDependencies": { - "@theia/ext-scripts": "^0.3.13" + "@theia/ext-scripts": "^0.3.14" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/task/src/browser/quick-open-task.ts b/packages/task/src/browser/quick-open-task.ts index bd16cf9797e38..3e8a5154cfc59 100644 --- a/packages/task/src/browser/quick-open-task.ts +++ b/packages/task/src/browser/quick-open-task.ts @@ -15,23 +15,31 @@ ********************************************************************************/ import { inject, injectable } from 'inversify'; -import { QuickOpenService, QuickOpenModel, QuickOpenItem, QuickOpenMode } from '@theia/core/lib/browser/quick-open/'; +import { QuickOpenService, QuickOpenModel, QuickOpenItem, QuickOpenMode, QuickOpenHandler, QuickOpenOptions } from '@theia/core/lib/browser/quick-open/'; import { TaskService } from './task-service'; import { TaskConfigurations } from './task-configurations'; import { TaskInfo, TaskConfiguration } from '../common/task-protocol'; @injectable() -export class QuickOpenTask implements QuickOpenModel { +export class QuickOpenTask implements QuickOpenModel, QuickOpenHandler { protected items: QuickOpenItem[]; - constructor( - @inject(TaskService) protected readonly taskService: TaskService, - @inject(TaskConfigurations) protected readonly taskConfigurations: TaskConfigurations, - @inject(QuickOpenService) protected readonly quickOpenService: QuickOpenService - ) { } + readonly prefix: string = 'task '; - async open(): Promise { + readonly description: string = 'Run Task'; + + @inject(TaskService) + protected readonly taskService: TaskService; + + @inject(TaskConfigurations) + protected readonly taskConfigurations: TaskConfigurations; + + @inject(QuickOpenService) + protected readonly quickOpenService: QuickOpenService; + + /** Initialize this quick open model with the tasks. */ + async init(): Promise { this.items = []; const configuredTasks = await this.taskConfigurations.getTasks(); @@ -43,7 +51,10 @@ export class QuickOpenTask implements QuickOpenModel { for (const task of providedTasks) { this.items.push(new TaskRunQuickOpenItem(task, this.taskService, true)); } + } + async open(): Promise { + await this.init(); this.quickOpenService.open(this, { placeholder: 'Type the name of a task you want to execute', fuzzyMatchLabel: true, @@ -51,6 +62,17 @@ export class QuickOpenTask implements QuickOpenModel { }); } + getModel(): QuickOpenModel { + return this; + } + + getOptions(): QuickOpenOptions { + return { + fuzzyMatchLabel: true, + fuzzySort: true + }; + } + attach(): void { this.items = []; @@ -75,10 +97,6 @@ export class QuickOpenTask implements QuickOpenModel { }); } - public getItems(lookFor: string): QuickOpenItem[] { - return this.items; - } - onType(lookFor: string, acceptor: (items: QuickOpenItem[]) => void): void { acceptor(this.items); } @@ -86,7 +104,6 @@ export class QuickOpenTask implements QuickOpenModel { protected getRunningTaskLabel(task: TaskInfo): string { return `Task id: ${task.taskId}, label: ${task.config.label}`; } - } export class TaskRunQuickOpenItem extends QuickOpenItem { diff --git a/packages/task/src/browser/task-configurations.ts b/packages/task/src/browser/task-configurations.ts index cb76ab7ea9a10..03e8ddf699a2a 100644 --- a/packages/task/src/browser/task-configurations.ts +++ b/packages/task/src/browser/task-configurations.ts @@ -100,10 +100,10 @@ export class TaskConfigurations implements Disposable { this.refreshTasks(); } if (await this.fileSystem.exists(configFile)) { - return Promise.resolve(true); + return true; } else { this.logger.info(`Config file ${this.TASKFILE} does not exist under ${rootUri}`); - return Promise.resolve(false); + return false; } } diff --git a/packages/task/src/browser/task-frontend-contribution.ts b/packages/task/src/browser/task-frontend-contribution.ts index de607038e14f4..d9dbf2bbd8952 100644 --- a/packages/task/src/browser/task-frontend-contribution.ts +++ b/packages/task/src/browser/task-frontend-contribution.ts @@ -18,7 +18,7 @@ import { inject, injectable, named } from 'inversify'; import { ILogger, ContributionProvider } from '@theia/core/lib/common'; import { QuickOpenTask } from './quick-open-task'; import { MAIN_MENU_BAR, CommandContribution, Command, CommandRegistry, MenuContribution, MenuModelRegistry } from '@theia/core/lib/common'; -import { FrontendApplication, FrontendApplicationContribution } from '@theia/core/lib/browser'; +import { FrontendApplication, FrontendApplicationContribution, QuickOpenContribution, QuickOpenHandlerRegistry } from '@theia/core/lib/browser'; import { WidgetManager } from '@theia/core/lib/browser/widget-manager'; import { TaskContribution, TaskResolverRegistry, TaskProviderRegistry } from './task-contribution'; @@ -45,7 +45,7 @@ export namespace TaskCommands { } @injectable() -export class TaskFrontendContribution implements CommandContribution, MenuContribution, FrontendApplicationContribution { +export class TaskFrontendContribution implements CommandContribution, MenuContribution, FrontendApplicationContribution, QuickOpenContribution { @inject(QuickOpenTask) protected readonly quickOpenTask: QuickOpenTask; @@ -110,4 +110,8 @@ export class TaskFrontendContribution implements CommandContribution, MenuContri order: '1' }); } + + registerQuickOpenHandlers(handlers: QuickOpenHandlerRegistry): void { + handlers.registerHandler(this.quickOpenTask); + } } diff --git a/packages/task/src/browser/task-frontend-module.ts b/packages/task/src/browser/task-frontend-module.ts index fbddeaecffdd2..7d497c4ca4f66 100644 --- a/packages/task/src/browser/task-frontend-module.ts +++ b/packages/task/src/browser/task-frontend-module.ts @@ -15,7 +15,7 @@ ********************************************************************************/ import { ContainerModule } from 'inversify'; -import { FrontendApplicationContribution } from '@theia/core/lib/browser'; +import { FrontendApplicationContribution, QuickOpenContribution } from '@theia/core/lib/browser'; import { CommandContribution, MenuContribution, bindContributionProvider } from '@theia/core/lib/common'; import { WebSocketConnectionProvider } from '@theia/core/lib/browser/messaging'; import { QuickOpenTask } from './quick-open-task'; @@ -32,7 +32,7 @@ export default new ContainerModule(bind => { bind(TaskFrontendContribution).toSelf().inSingletonScope(); bind(TaskService).toSelf().inSingletonScope(); - for (const identifier of [FrontendApplicationContribution, CommandContribution, MenuContribution]) { + for (const identifier of [FrontendApplicationContribution, CommandContribution, MenuContribution, QuickOpenContribution]) { bind(identifier).toService(TaskFrontendContribution); } diff --git a/packages/task/src/browser/task-service.ts b/packages/task/src/browser/task-service.ts index 65cbb770bbd33..2eee6676d4286 100644 --- a/packages/task/src/browser/task-service.ts +++ b/packages/task/src/browser/task-service.ts @@ -224,7 +224,8 @@ export class TaskService implements TaskConfigurationClient { caption: `Task #${taskId}`, label: `Task #${taskId}`, destroyTermOnClose: true - }); + } + ); this.shell.addWidget(widget, { area: 'bottom' }); this.shell.activateWidget(widget.id); widget.start(terminalId); diff --git a/packages/task/src/node/process/process-task-runner.ts b/packages/task/src/node/process/process-task-runner.ts index aa57629b05836..a36c4285b54f9 100644 --- a/packages/task/src/node/process/process-task-runner.ts +++ b/packages/task/src/node/process/process-task-runner.ts @@ -77,7 +77,7 @@ export class ProcessTaskRunner implements TaskRunner { // sanity checks: // - we expect the cwd to be set by the client. if (!taskConfig.cwd) { - return Promise.reject(new Error("Can't run a task when 'cwd' is not provided by the client")); + throw new Error("Can't run a task when 'cwd' is not provided by the client"); } const cwd = FileUri.fsPath(taskConfig.cwd); @@ -93,42 +93,40 @@ export class ProcessTaskRunner implements TaskRunner { // unsuccessfully. So here we look to see if it seems we can find a file of that name // that is likely to be the one we want, before attempting to execute it. const cmd = await this.findCommand(command, cwd); - if (cmd) { - try { - // use terminal or raw process - let proc: TerminalProcess | RawProcess; - const processType = taskConfig.type === 'process' ? 'process' : 'shell'; - if (processType === 'process') { - this.logger.debug('Task: creating underlying raw process'); - proc = this.rawProcessFactory({ - command: command, - args: args, - options: options - }); - } else { - // all Task types without specific TaskRunner will be run as a shell process e.g.: npm, gulp, etc. - this.logger.debug('Task: creating underlying terminal process'); - proc = this.terminalProcessFactory({ - command: command, - args: args, - options: options - }); - } - return this.taskFactory( - { - label: taskConfig.label, - command: cmd, - process: proc, - processType: processType, - context: ctx, - config: taskConfig - }); - } catch (error) { - this.logger.error(`Error occurred while creating task: ${error}`); - return Promise.reject(new Error(error)); + if (!cmd) { + throw new Error(`Command not found: ${command}`); + } + try { + // use terminal or raw process + let proc: TerminalProcess | RawProcess; + const processType = taskConfig.type === 'process' ? 'process' : 'shell'; + if (processType === 'process') { + this.logger.debug('Task: creating underlying raw process'); + proc = this.rawProcessFactory({ + command: command, + args: args, + options: options + }); + } else { + // all Task types without specific TaskRunner will be run as a shell process e.g.: npm, gulp, etc. + this.logger.debug('Task: creating underlying terminal process'); + proc = this.terminalProcessFactory({ + command: command, + args: args, + options: options + }); } - } else { - return Promise.reject(new Error(`Command not found: ${command}`)); + return this.taskFactory({ + label: taskConfig.label, + command: cmd, + process: proc, + processType: processType, + context: ctx, + config: taskConfig + }); + } catch (error) { + this.logger.error(`Error occurred while creating task: ${error}`); + throw error; } } @@ -170,7 +168,6 @@ export class ProcessTaskRunner implements TaskRunner { } } } - } } diff --git a/packages/terminal/package.json b/packages/terminal/package.json index 36c29c3194fae..7ed0f72fd0b1a 100644 --- a/packages/terminal/package.json +++ b/packages/terminal/package.json @@ -1,12 +1,12 @@ { "name": "@theia/terminal", - "version": "0.3.13", + "version": "0.3.14", "description": "Theia - Terminal Extension", "dependencies": { - "@theia/core": "^0.3.13", - "@theia/filesystem": "^0.3.13", - "@theia/process": "^0.3.13", - "@theia/workspace": "^0.3.13", + "@theia/core": "^0.3.14", + "@theia/filesystem": "^0.3.14", + "@theia/process": "^0.3.14", + "@theia/workspace": "^0.3.14", "xterm": "~3.5.0" }, "publishConfig": { @@ -43,7 +43,7 @@ "docs": "theiaext docs" }, "devDependencies": { - "@theia/ext-scripts": "^0.3.13" + "@theia/ext-scripts": "^0.3.14" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/terminal/src/browser/terminal-widget-impl.ts b/packages/terminal/src/browser/terminal-widget-impl.ts index 6801fd8776bd8..3d91e8745d0f4 100644 --- a/packages/terminal/src/browser/terminal-widget-impl.ts +++ b/packages/terminal/src/browser/terminal-widget-impl.ts @@ -170,7 +170,7 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget not defined. */ function lookup(props: CSSStyleDeclaration, name: string): string { /* There is sometimes an extra space in the front, remove it. */ - const value = htmlElementProps.getPropertyValue(name).trim(); + const value = props.getPropertyValue(name).trim(); if (!value) { throw new Error(`Couldn\'t find value of ${name}`); } diff --git a/packages/terminal/src/node/base-terminal-server.ts b/packages/terminal/src/node/base-terminal-server.ts index 0c17b4ed537cf..aac006eccca0d 100644 --- a/packages/terminal/src/node/base-terminal-server.ts +++ b/packages/terminal/src/node/base-terminal-server.ts @@ -26,8 +26,8 @@ export abstract class BaseTerminalServer implements IBaseTerminalServer { constructor( @inject(ProcessManager) protected readonly processManager: ProcessManager, - @inject(ILogger) @named('terminal') protected readonly logger: ILogger) { - + @inject(ILogger) @named('terminal') protected readonly logger: ILogger + ) { processManager.onDelete(id => { const toDispose = this.terminalToDispose.get(id); if (toDispose !== undefined) { @@ -39,68 +39,64 @@ export abstract class BaseTerminalServer implements IBaseTerminalServer { abstract create(options: IBaseTerminalServerOptions): Promise; - attach(id: number): Promise { + async attach(id: number): Promise { const term = this.processManager.get(id); if (term && term instanceof TerminalProcess) { - return Promise.resolve(term.id); + return term.id; } else { this.logger.error(`Couldn't attach - can't find terminal with id: ${id} `); - return Promise.resolve(-1); + return -1; } } - close(id: number): Promise { + async close(id: number): Promise { const term = this.processManager.get(id); if (term instanceof TerminalProcess) { term.kill(); } - return Promise.resolve(); } dispose(): void { // noop } - resize(id: number, cols: number, rows: number): Promise { + async resize(id: number, cols: number, rows: number): Promise { const term = this.processManager.get(id); if (term && term instanceof TerminalProcess) { term.resize(cols, rows); } else { console.error("Couldn't resize terminal " + id + ", because it doesn't exist."); } - return Promise.resolve(); } /* Set the client to receive notifications on. */ - setClient(client: IBaseTerminalClient | undefined) { + setClient(client: IBaseTerminalClient | undefined): void { this.client = client; } - protected postCreate(term: TerminalProcess) { + protected postCreate(term: TerminalProcess): void { const toDispose = new DisposableCollection(); toDispose.push(term.onError(error => { this.logger.error(`Terminal pid: ${term.pid} error: ${error}, closing it.`); if (this.client !== undefined) { - this.client.onTerminalError( - { - 'terminalId': term.id, - 'error': error - }); + this.client.onTerminalError({ + 'terminalId': term.id, + 'error': error + }); } })); toDispose.push(term.onExit(event => { if (this.client !== undefined) { - this.client.onTerminalExitChanged( - { - 'terminalId': term.id, - 'code': event.code, - 'signal': event.signal - }); + this.client.onTerminalExitChanged({ + 'terminalId': term.id, + 'code': event.code, + 'signal': event.signal + }); } })); diff --git a/packages/terminal/src/node/terminal-server.ts b/packages/terminal/src/node/terminal-server.ts index dcd0b1faed64c..cb832fd5dcb9a 100644 --- a/packages/terminal/src/node/terminal-server.ts +++ b/packages/terminal/src/node/terminal-server.ts @@ -29,18 +29,19 @@ export class TerminalServer extends BaseTerminalServer implements ITerminalServe constructor( @inject(TerminalProcessFactory) protected readonly terminalFactory: TerminalProcessFactory, @inject(ProcessManager) protected readonly processManager: ProcessManager, - @inject(ILogger) @named('terminal') protected readonly logger: ILogger) { + @inject(ILogger) @named('terminal') protected readonly logger: ILogger + ) { super(processManager, logger); } - create(options: ITerminalServerOptions): Promise { + async create(options: ITerminalServerOptions): Promise { try { const term = this.terminalFactory(options); this.postCreate(term); - return Promise.resolve(term.id); + return term.id; } catch (error) { this.logger.error('Error while creating terminal', error); - return Promise.resolve(-1); + return -1; } } } diff --git a/packages/textmate-grammars/package.json b/packages/textmate-grammars/package.json index c21371af8ad45..c7a4fc3f8d215 100644 --- a/packages/textmate-grammars/package.json +++ b/packages/textmate-grammars/package.json @@ -1,9 +1,9 @@ { "name": "@theia/textmate-grammars", - "version": "0.3.13", + "version": "0.3.14", "description": "Theia - Textmate Grammars", "dependencies": { - "@theia/monaco": "^0.3.13" + "@theia/monaco": "^0.3.14" }, "publishConfig": { "access": "public" @@ -39,7 +39,7 @@ "docs": "theiaext docs" }, "devDependencies": { - "@theia/ext-scripts": "^0.3.13" + "@theia/ext-scripts": "^0.3.14" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/textmate-grammars/src/browser/bat.ts b/packages/textmate-grammars/src/browser/bat.ts index d51cc23bdc32c..0fb3a0dfce610 100644 --- a/packages/textmate-grammars/src/browser/bat.ts +++ b/packages/textmate-grammars/src/browser/bat.ts @@ -57,7 +57,7 @@ export class BatContribution implements LanguageGrammarDefinitionContribution { } }); const grammar = require('../../data/bat.tmLanguage.json'); - registry.registerTextMateGrammarScope(this.scopeName, { + registry.registerTextmateGrammarScope(this.scopeName, { async getGrammarDefinition() { return { format: 'json', diff --git a/packages/textmate-grammars/src/browser/clojure.ts b/packages/textmate-grammars/src/browser/clojure.ts index 40720d6c960bc..e80d39e6e7bee 100644 --- a/packages/textmate-grammars/src/browser/clojure.ts +++ b/packages/textmate-grammars/src/browser/clojure.ts @@ -39,23 +39,23 @@ export class ClojureContribution implements LanguageGrammarDefinitionContributio ['(', ')'] ], autoClosingPairs: [ - {open: '{', close: '}'}, - {open: '[', close: ']'}, - {open: '(', close: ')'}, - {open: '\'', close: '\''} + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: '(', close: ')' }, + { open: '\'', close: '\'' } ], surroundingPairs: [ - {open: '{', close: '}'}, - {open: '[', close: ']'}, - {open: '(', close: ')'}, - {open: '\'', close: '\''} + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: '(', close: ')' }, + { open: '\'', close: '\'' } ], folding: { offSide: true } }); const grammar = require('../../data/clojure.tmLanguage.json'); - registry.registerTextMateGrammarScope(this.scopeName, { + registry.registerTextmateGrammarScope(this.scopeName, { async getGrammarDefinition() { return { format: 'json', diff --git a/packages/textmate-grammars/src/browser/coffeescript.ts b/packages/textmate-grammars/src/browser/coffeescript.ts index a0567a2dbce5b..131d1c3a23ced 100644 --- a/packages/textmate-grammars/src/browser/coffeescript.ts +++ b/packages/textmate-grammars/src/browser/coffeescript.ts @@ -62,7 +62,7 @@ export class CoffeescriptContribution implements LanguageGrammarDefinitionContri } }); const grammar = require('../../data/coffeescript.tmLanguage.json'); - registry.registerTextMateGrammarScope(this.scopeName, { + registry.registerTextmateGrammarScope(this.scopeName, { async getGrammarDefinition() { return { format: 'json', diff --git a/packages/textmate-grammars/src/browser/csharp.ts b/packages/textmate-grammars/src/browser/csharp.ts index 30c3a95277193..68a5bb7d7478d 100644 --- a/packages/textmate-grammars/src/browser/csharp.ts +++ b/packages/textmate-grammars/src/browser/csharp.ts @@ -63,7 +63,7 @@ export class CSharpContribution implements LanguageGrammarDefinitionContribution } }); const grammar = require('../../data/csharp.tmLanguage.json'); - registry.registerTextMateGrammarScope(this.scopeName, { + registry.registerTextmateGrammarScope(this.scopeName, { async getGrammarDefinition() { return { format: 'json', diff --git a/packages/textmate-grammars/src/browser/css.ts b/packages/textmate-grammars/src/browser/css.ts index bc8199fc73052..974347ff70ceb 100644 --- a/packages/textmate-grammars/src/browser/css.ts +++ b/packages/textmate-grammars/src/browser/css.ts @@ -67,7 +67,7 @@ export class CssContribution implements LanguageGrammarDefinitionContribution { } }); const grammar = require('../../data/css.tmLanguage.json'); - registry.registerTextMateGrammarScope(this.scopeName, { + registry.registerTextmateGrammarScope(this.scopeName, { async getGrammarDefinition() { return { format: 'json', diff --git a/packages/textmate-grammars/src/browser/fsharp.ts b/packages/textmate-grammars/src/browser/fsharp.ts index 610158e37d99b..1eb0bfb26c6ba 100644 --- a/packages/textmate-grammars/src/browser/fsharp.ts +++ b/packages/textmate-grammars/src/browser/fsharp.ts @@ -61,7 +61,7 @@ export class FSharpContribution implements LanguageGrammarDefinitionContribution } }); const grammar = require('../../data/fsharp.tmLanguage.json'); - registry.registerTextMateGrammarScope(this.scopeName, { + registry.registerTextmateGrammarScope(this.scopeName, { async getGrammarDefinition() { return { format: 'json', diff --git a/packages/textmate-grammars/src/browser/groovy.ts b/packages/textmate-grammars/src/browser/groovy.ts index 1461e5a429f1e..23d26bc5f272a 100644 --- a/packages/textmate-grammars/src/browser/groovy.ts +++ b/packages/textmate-grammars/src/browser/groovy.ts @@ -57,7 +57,7 @@ export class GroovyContribution implements LanguageGrammarDefinitionContribution ] }); const grammar = require('../../data/groovy.tmLanguage.json'); - registry.registerTextMateGrammarScope(this.scopeName, { + registry.registerTextmateGrammarScope(this.scopeName, { async getGrammarDefinition() { return { format: 'json', diff --git a/packages/textmate-grammars/src/browser/handlebars.ts b/packages/textmate-grammars/src/browser/handlebars.ts index ff726ac19a4a7..78f614db813f1 100644 --- a/packages/textmate-grammars/src/browser/handlebars.ts +++ b/packages/textmate-grammars/src/browser/handlebars.ts @@ -54,8 +54,8 @@ export class HandlebarsContribution implements LanguageGrammarDefinitionContribu { open: '\'', close: '\'' } ] }); - const grammar = require('../../data/csharp.tmLanguage.json'); - registry.registerTextMateGrammarScope(this.scopeName, { + const grammar = require('../../data/handlebars.tmLanguage.json'); + registry.registerTextmateGrammarScope(this.scopeName, { async getGrammarDefinition() { return { format: 'json', diff --git a/packages/textmate-grammars/src/browser/hlsl.ts b/packages/textmate-grammars/src/browser/hlsl.ts index 0a7b03affcbaf..0bd475c573c65 100644 --- a/packages/textmate-grammars/src/browser/hlsl.ts +++ b/packages/textmate-grammars/src/browser/hlsl.ts @@ -53,7 +53,7 @@ export class HlslContribution implements LanguageGrammarDefinitionContribution { ] }); const grammar = require('../../data/hlsl.tmLanguage.json'); - registry.registerTextMateGrammarScope(this.scopeName, { + registry.registerTextmateGrammarScope(this.scopeName, { async getGrammarDefinition() { return { format: 'json', diff --git a/packages/textmate-grammars/src/browser/html.ts b/packages/textmate-grammars/src/browser/html.ts index 1fe9146e3107e..d202e8542784f 100644 --- a/packages/textmate-grammars/src/browser/html.ts +++ b/packages/textmate-grammars/src/browser/html.ts @@ -84,7 +84,7 @@ export class HtmlContribution implements LanguageGrammarDefinitionContribution { }); const grammar = require('../../data/html.tmLanguage.json'); - registry.registerTextMateGrammarScope(this.scopeName, { + registry.registerTextmateGrammarScope(this.scopeName, { async getGrammarDefinition() { return { format: 'json', diff --git a/packages/textmate-grammars/src/browser/ini.ts b/packages/textmate-grammars/src/browser/ini.ts index 95f2191292a9f..584c2d6d22842 100644 --- a/packages/textmate-grammars/src/browser/ini.ts +++ b/packages/textmate-grammars/src/browser/ini.ts @@ -55,7 +55,7 @@ export class IniContribution implements LanguageGrammarDefinitionContribution { ] }); const grammar = require('../../data/ini.tmLanguage.json'); - registry.registerTextMateGrammarScope(this.scopeName, { + registry.registerTextmateGrammarScope(this.scopeName, { async getGrammarDefinition() { return { format: 'json', diff --git a/packages/textmate-grammars/src/browser/less.ts b/packages/textmate-grammars/src/browser/less.ts index 2b8610d5306fb..e33be2b709ea4 100644 --- a/packages/textmate-grammars/src/browser/less.ts +++ b/packages/textmate-grammars/src/browser/less.ts @@ -64,7 +64,7 @@ export class LessContribution implements LanguageGrammarDefinitionContribution { }); const grammar = require('../../data/less.tmLanguage.json'); - registry.registerTextMateGrammarScope(this.scopeName, { + registry.registerTextmateGrammarScope(this.scopeName, { async getGrammarDefinition() { return { format: 'json', diff --git a/packages/textmate-grammars/src/browser/log.ts b/packages/textmate-grammars/src/browser/log.ts index cc8accaf3a812..0e5d26bcf8751 100644 --- a/packages/textmate-grammars/src/browser/log.ts +++ b/packages/textmate-grammars/src/browser/log.ts @@ -30,7 +30,7 @@ export class LogContribution implements LanguageGrammarDefinitionContribution { aliases: ['log'] }); const grammar = require('../../data/log.tmLanguage.json'); - registry.registerTextMateGrammarScope(this.scopeName, { + registry.registerTextmateGrammarScope(this.scopeName, { async getGrammarDefinition() { return { format: 'json', diff --git a/packages/textmate-grammars/src/browser/lua.ts b/packages/textmate-grammars/src/browser/lua.ts index 97549cd6debea..9b6dffc509e0e 100644 --- a/packages/textmate-grammars/src/browser/lua.ts +++ b/packages/textmate-grammars/src/browser/lua.ts @@ -59,7 +59,7 @@ export class LuaContribution implements LanguageGrammarDefinitionContribution { } }); const grammar = require('../../data/lua.tmLanguage.json'); - registry.registerTextMateGrammarScope(this.scopeName, { + registry.registerTextmateGrammarScope(this.scopeName, { async getGrammarDefinition() { return { format: 'json', diff --git a/packages/textmate-grammars/src/browser/make.ts b/packages/textmate-grammars/src/browser/make.ts index 45f038e14a60a..285e293f02fc3 100644 --- a/packages/textmate-grammars/src/browser/make.ts +++ b/packages/textmate-grammars/src/browser/make.ts @@ -47,7 +47,7 @@ export class MakeContribution implements LanguageGrammarDefinitionContribution { ] }); const grammar = require('../../data/make.tmLanguage.json'); - registry.registerTextMateGrammarScope(this.scopeName, { + registry.registerTextmateGrammarScope(this.scopeName, { async getGrammarDefinition() { return { format: 'json', diff --git a/packages/textmate-grammars/src/browser/markdown.ts b/packages/textmate-grammars/src/browser/markdown.ts index 1ca8a818d5f3b..38baefdd811dd 100644 --- a/packages/textmate-grammars/src/browser/markdown.ts +++ b/packages/textmate-grammars/src/browser/markdown.ts @@ -58,7 +58,7 @@ export class MarkdownContribution implements LanguageGrammarDefinitionContributi }); const grammar = require('../../data/markdown.tmLanguage.json'); - registry.registerTextMateGrammarScope(this.scopeName, { + registry.registerTextmateGrammarScope(this.scopeName, { async getGrammarDefinition() { return { format: 'json', diff --git a/packages/textmate-grammars/src/browser/objective-c.ts b/packages/textmate-grammars/src/browser/objective-c.ts index 856c37ec763fe..c74a9e263a0ae 100644 --- a/packages/textmate-grammars/src/browser/objective-c.ts +++ b/packages/textmate-grammars/src/browser/objective-c.ts @@ -55,7 +55,7 @@ export class ObjectiveCContribution implements LanguageGrammarDefinitionContribu ] }); const grammar = require('../../data/objective-c.tmLanguage.json'); - registry.registerTextMateGrammarScope(this.scopeName, { + registry.registerTextmateGrammarScope(this.scopeName, { async getGrammarDefinition() { return { format: 'json', diff --git a/packages/textmate-grammars/src/browser/perl.ts b/packages/textmate-grammars/src/browser/perl.ts index 58b49f4d98403..741a97ab56013 100644 --- a/packages/textmate-grammars/src/browser/perl.ts +++ b/packages/textmate-grammars/src/browser/perl.ts @@ -31,7 +31,7 @@ export class PerlContribution implements LanguageGrammarDefinitionContribution { firstLine: '^#!.*\\bperl\\b' }); const grammar = require('../../data/perl.tmLanguage.json'); - registry.registerTextMateGrammarScope(this.scopeName, { + registry.registerTextmateGrammarScope(this.scopeName, { async getGrammarDefinition() { return { format: 'json', diff --git a/packages/textmate-grammars/src/browser/powershell.ts b/packages/textmate-grammars/src/browser/powershell.ts index ac9b86fe0219a..b5f905ddb2b37 100644 --- a/packages/textmate-grammars/src/browser/powershell.ts +++ b/packages/textmate-grammars/src/browser/powershell.ts @@ -63,7 +63,7 @@ export class PowershellContribution implements LanguageGrammarDefinitionContribu } }); const grammar = require('../../data/powershell.tmLanguage.json'); - registry.registerTextMateGrammarScope(this.scopeName, { + registry.registerTextmateGrammarScope(this.scopeName, { async getGrammarDefinition() { return { format: 'json', diff --git a/packages/textmate-grammars/src/browser/pug.ts b/packages/textmate-grammars/src/browser/pug.ts index aa1ecebc0712e..a5994a1e9cc41 100644 --- a/packages/textmate-grammars/src/browser/pug.ts +++ b/packages/textmate-grammars/src/browser/pug.ts @@ -57,7 +57,7 @@ export class PugContribution implements LanguageGrammarDefinitionContribution { } }); const grammar = require('../../data/pug.tmLanguage.json'); - registry.registerTextMateGrammarScope(this.scopeName, { + registry.registerTextmateGrammarScope(this.scopeName, { async getGrammarDefinition() { return { format: 'json', diff --git a/packages/textmate-grammars/src/browser/r.ts b/packages/textmate-grammars/src/browser/r.ts index 3606d6fd67f10..238d2fd4042ab 100644 --- a/packages/textmate-grammars/src/browser/r.ts +++ b/packages/textmate-grammars/src/browser/r.ts @@ -52,7 +52,7 @@ export class RContribution implements LanguageGrammarDefinitionContribution { ] }); const grammar = require('../../data/r.tmLanguage.json'); - registry.registerTextMateGrammarScope(this.scopeName, { + registry.registerTextmateGrammarScope(this.scopeName, { async getGrammarDefinition() { return { format: 'json', diff --git a/packages/textmate-grammars/src/browser/razor.ts b/packages/textmate-grammars/src/browser/razor.ts index 6098465903449..05aa0f1dd65ec 100644 --- a/packages/textmate-grammars/src/browser/razor.ts +++ b/packages/textmate-grammars/src/browser/razor.ts @@ -60,7 +60,7 @@ export class RazorContribution implements LanguageGrammarDefinitionContribution } }); const grammar = require('../../data/razor.tmLanguage.json'); - registry.registerTextMateGrammarScope(this.scopeName, { + registry.registerTextmateGrammarScope(this.scopeName, { async getGrammarDefinition() { return { format: 'json', diff --git a/packages/textmate-grammars/src/browser/shaderlab.ts b/packages/textmate-grammars/src/browser/shaderlab.ts index b285d9be561fa..892668a69acbb 100644 --- a/packages/textmate-grammars/src/browser/shaderlab.ts +++ b/packages/textmate-grammars/src/browser/shaderlab.ts @@ -53,7 +53,7 @@ export class ShaderlabContribution implements LanguageGrammarDefinitionContribut ] }); const grammar = require('../../data/shaderlab.tmLanguage.json'); - registry.registerTextMateGrammarScope(this.scopeName, { + registry.registerTextmateGrammarScope(this.scopeName, { async getGrammarDefinition() { return { format: 'json', diff --git a/packages/textmate-grammars/src/browser/shell.ts b/packages/textmate-grammars/src/browser/shell.ts index d29346f297c89..cbddb5238eb3a 100644 --- a/packages/textmate-grammars/src/browser/shell.ts +++ b/packages/textmate-grammars/src/browser/shell.ts @@ -53,7 +53,7 @@ export class ShellContribution implements LanguageGrammarDefinitionContribution }); const grammar = require('../../data/shell.tmLanguage.json'); - registry.registerTextMateGrammarScope(this.scopeName, { + registry.registerTextmateGrammarScope(this.scopeName, { async getGrammarDefinition() { return { format: 'json', diff --git a/packages/textmate-grammars/src/browser/sql.ts b/packages/textmate-grammars/src/browser/sql.ts index 5b9abdf54b7d6..968e7219a657d 100644 --- a/packages/textmate-grammars/src/browser/sql.ts +++ b/packages/textmate-grammars/src/browser/sql.ts @@ -56,7 +56,7 @@ export class SqlContribution implements LanguageGrammarDefinitionContribution { ] }); const grammar = require('../../data/sql.tmLanguage.json'); - registry.registerTextMateGrammarScope(this.scopeName, { + registry.registerTextmateGrammarScope(this.scopeName, { async getGrammarDefinition() { return { format: 'json', diff --git a/packages/textmate-grammars/src/browser/swift.ts b/packages/textmate-grammars/src/browser/swift.ts index 17acbdcca33d1..010149fa8c5ac 100644 --- a/packages/textmate-grammars/src/browser/swift.ts +++ b/packages/textmate-grammars/src/browser/swift.ts @@ -57,7 +57,7 @@ export class SwiftContribution implements LanguageGrammarDefinitionContribution ] }); const grammar = require('../../data/swift.tmLanguage.json'); - registry.registerTextMateGrammarScope(this.scopeName, { + registry.registerTextmateGrammarScope(this.scopeName, { async getGrammarDefinition() { return { format: 'json', diff --git a/packages/textmate-grammars/src/browser/vb.ts b/packages/textmate-grammars/src/browser/vb.ts index 604ed0d32db7d..320b5710632d1 100644 --- a/packages/textmate-grammars/src/browser/vb.ts +++ b/packages/textmate-grammars/src/browser/vb.ts @@ -60,7 +60,7 @@ export class VbContribution implements LanguageGrammarDefinitionContribution { } }); const grammar = require('../../data/vb.tmLanguage.json'); - registry.registerTextMateGrammarScope(this.scopeName, { + registry.registerTextmateGrammarScope(this.scopeName, { async getGrammarDefinition() { return { format: 'json', diff --git a/packages/textmate-grammars/src/browser/xml.ts b/packages/textmate-grammars/src/browser/xml.ts index 9f63c1448f161..575da73a61d32 100644 --- a/packages/textmate-grammars/src/browser/xml.ts +++ b/packages/textmate-grammars/src/browser/xml.ts @@ -109,7 +109,7 @@ export class XmlContribution implements LanguageGrammarDefinitionContribution { }); const grammar = require('../../data/xml.tmLanguage.json'); - registry.registerTextMateGrammarScope(this.scopeName, { + registry.registerTextmateGrammarScope(this.scopeName, { async getGrammarDefinition() { return { format: 'json', diff --git a/packages/textmate-grammars/src/browser/xsl.ts b/packages/textmate-grammars/src/browser/xsl.ts index e28e057884077..2c71f2165090f 100644 --- a/packages/textmate-grammars/src/browser/xsl.ts +++ b/packages/textmate-grammars/src/browser/xsl.ts @@ -43,7 +43,7 @@ export class XslContribution implements LanguageGrammarDefinitionContribution { }); const grammar = require('../../data/xsl.tmLanguage.json'); - registry.registerTextMateGrammarScope(this.scopeName, { + registry.registerTextmateGrammarScope(this.scopeName, { async getGrammarDefinition() { return { format: 'json', diff --git a/packages/textmate-grammars/src/browser/yaml.ts b/packages/textmate-grammars/src/browser/yaml.ts index 0ccdc8434e930..6bae2b288df0b 100644 --- a/packages/textmate-grammars/src/browser/yaml.ts +++ b/packages/textmate-grammars/src/browser/yaml.ts @@ -66,8 +66,8 @@ export class YamlContribution implements LanguageGrammarDefinitionContribution { } }); - const grammar = require('../../data/shell.tmLanguage.json'); - registry.registerTextMateGrammarScope(this.scopeName, { + const grammar = require('../../data/yaml.tmLanguage.json'); + registry.registerTextmateGrammarScope(this.scopeName, { async getGrammarDefinition() { return { format: 'json', diff --git a/packages/typescript/data/snippets/typescript.json b/packages/typescript/data/snippets/typescript.json new file mode 100644 index 0000000000000..0a7195751d821 --- /dev/null +++ b/packages/typescript/data/snippets/typescript.json @@ -0,0 +1,273 @@ +{ + "Constructor": { + "prefix": "ctor", + "body": [ + "/**", + " *", + " */", + "constructor() {", + "\tsuper();", + "\t$0", + "}" + ], + "description": "Constructor" + }, + "Class Definition": { + "prefix": "class", + "body": [ + "class ${1:name} {", + "\tconstructor(${2:parameters}) {", + "\t\t$0", + "\t}", + "}" + ], + "description": "Class Definition" + }, + "Public Method Definition": { + "prefix": "public method", + "body": [ + "/**", + " * ${1:name}", + " */", + "public ${1:name}() {", + "\t$0", + "}" + ], + "description": "Public Method Definition" + }, + "Private Method Definition": { + "prefix": "private method", + "body": [ + "private ${1:name}() {", + "\t$0", + "}" + ], + "description": "Private Method Definition" + }, + "Import external module.": { + "prefix": "import statement", + "body": [ + "import { $0 } from \"${1:module}\";" + ], + "description": "Import external module." + }, + "Property getter": { + "prefix": "get", + "body": [ + "", + "public get ${1:value}() : ${2:string} {", + "\t${3:return $0}", + "}", + "" + ], + "description": "Property getter" + }, + "Log to the console": { + "prefix": "log", + "body": [ + "console.log($1);", + "$0" + ], + "description": "Log to the console" + }, + "Log warning to console": { + "prefix": "warn", + "body": [ + "console.warn($1);", + "$0" + ], + "description": "Log warning to the console" + }, + "Log error to console": { + "prefix": "error", + "body": [ + "console.error($1);", + "$0" + ], + "description": "Log error to the console" + }, + "Define a full property": { + "prefix": "prop", + "body": [ + "", + "private _${1:value} : ${2:string};", + "public get ${1:value}() : ${2:string} {", + "\treturn this._${1:value};", + "}", + "public set ${1:value}(v : ${2:string}) {", + "\tthis._${1:value} = v;", + "}", + "" + ], + "description": "Define a full property" + }, + "Triple-slash reference": { + "prefix": "ref", + "body": [ + "/// ", + "$0" + ], + "description": "Triple-slash reference" + }, + "Property setter": { + "prefix": "set", + "body": [ + "", + "public set ${1:value}(v : ${2:string}) {", + "\tthis.$3 = v;", + "}", + "" + ], + "description": "Property setter" + }, + "Throw Exception": { + "prefix": "throw", + "body": [ + "throw \"$1\";", + "$0" + ], + "description": "Throw Exception" + }, + "For Loop": { + "prefix": "for", + "body": [ + "for (let ${1:index} = 0; ${1:index} < ${2:array}.length; ${1:index}++) {", + "\tconst ${3:element} = ${2:array}[${1:index}];", + "\t$0", + "}" + ], + "description": "For Loop" + }, + "For-Each Loop using =>": { + "prefix": "foreach =>", + "body": [ + "${1:array}.forEach(${2:element} => {", + "\t$0", + "});" + ], + "description": "For-Each Loop using =>" + }, + "For-In Loop": { + "prefix": "forin", + "body": [ + "for (const ${1:key} in ${2:object}) {", + "\tif (${2:object}.hasOwnProperty(${1:key})) {", + "\t\tconst ${3:element} = ${2:object}[${1:key}];", + "\t\t$0", + "\t}", + "}" + ], + "description": "For-In Loop" + }, + "For-Of Loop": { + "prefix": "forof", + "body": [ + "for (const ${1:iterator} of ${2:object}) {", + "\t$0", + "}" + ], + "description": "For-Of Loop" + }, + "Function Statement": { + "prefix": "function", + "body": [ + "function ${1:name}(${2:params}:${3:type}) {", + "\t$0", + "}" + ], + "description": "Function Statement" + }, + "If Statement": { + "prefix": "if", + "body": [ + "if (${1:condition}) {", + "\t$0", + "}" + ], + "description": "If Statement" + }, + "If-Else Statement": { + "prefix": "ifelse", + "body": [ + "if (${1:condition}) {", + "\t$0", + "} else {", + "\t", + "}" + ], + "description": "If-Else Statement" + }, + "New Statement": { + "prefix": "new", + "body": [ + "const ${1:name} = new ${2:type}(${3:arguments});$0" + ], + "description": "New Statement" + }, + "Switch Statement": { + "prefix": "switch", + "body": [ + "switch (${1:key}) {", + "\tcase ${2:value}:", + "\t\t$0", + "\t\tbreak;", + "", + "\tdefault:", + "\t\tbreak;", + "}" + ], + "description": "Switch Statement" + }, + "While Statement": { + "prefix": "while", + "body": [ + "while (${1:condition}) {", + "\t$0", + "}" + ], + "description": "While Statement" + }, + "Do-While Statement": { + "prefix": "dowhile", + "body": [ + "do {", + "\t$0", + "} while (${1:condition});" + ], + "description": "Do-While Statement" + }, + "Try-Catch Statement": { + "prefix": "trycatch", + "body": [ + "try {", + "\t$0", + "} catch (${1:error}) {", + "\t", + "}" + ], + "description": "Try-Catch Statement" + }, + "Set Timeout Function": { + "prefix": "settimeout", + "body": [ + "setTimeout(() => {", + "\t$0", + "}, ${1:timeout});" + ], + "description": "Set Timeout Function" + }, + "Region Start": { + "prefix": "#region", + "body": [ + "//#region $0" + ], + "description": "Folding Region Start" + }, + "Region End": { + "prefix": "#endregion", + "body": [ + "//#endregion" + ], + "description": "Folding Region End" + } +} diff --git a/packages/typescript/package.json b/packages/typescript/package.json index b13e78e596eda..4e91f3d9cc396 100644 --- a/packages/typescript/package.json +++ b/packages/typescript/package.json @@ -1,13 +1,13 @@ { "name": "@theia/typescript", - "version": "0.3.13", + "version": "0.3.14", "description": "Theia - Typescript Extension", "dependencies": { - "@theia/callhierarchy": "^0.3.13", - "@theia/core": "^0.3.13", - "@theia/languages": "^0.3.13", - "@theia/monaco": "^0.3.13", - "typescript-language-server": "next" + "@theia/callhierarchy": "^0.3.14", + "@theia/core": "^0.3.14", + "@theia/languages": "^0.3.14", + "@theia/monaco": "^0.3.14", + "typescript-language-server": "^0.3.3" }, "publishConfig": { "access": "public" @@ -44,9 +44,9 @@ "docs": "theiaext docs" }, "devDependencies": { - "@theia/ext-scripts": "^0.3.13" + "@theia/ext-scripts": "^0.3.14" }, "nyc": { "extends": "../../configs/nyc.json" } -} \ No newline at end of file +} diff --git a/packages/typescript/src/browser/javascript-language-config.ts b/packages/typescript/src/browser/javascript-language-config.ts index 62b8fcac491f5..fb1ba924ce698 100644 --- a/packages/typescript/src/browser/javascript-language-config.ts +++ b/packages/typescript/src/browser/javascript-language-config.ts @@ -25,7 +25,7 @@ export class JavascriptGrammarContribution implements LanguageGrammarDefinitionC registerTextmateLanguage(registry: TextmateRegistry) { this.registerJavaScript(); const grammar = require('../../data/grammars/javascript.tmlanguage.json'); - registry.registerTextMateGrammarScope('source.js', { + registry.registerTextmateGrammarScope('source.js', { async getGrammarDefinition() { return { format: 'json', @@ -34,7 +34,7 @@ export class JavascriptGrammarContribution implements LanguageGrammarDefinitionC } }); - registry.registerTextMateGrammarScope('source.js.regexp', { + registry.registerTextmateGrammarScope('source.js.regexp', { async getGrammarDefinition() { return { format: 'plist', @@ -61,7 +61,7 @@ export class JavascriptGrammarContribution implements LanguageGrammarDefinitionC registry.mapLanguageIdToTextmateGrammar(JAVASCRIPT_LANGUAGE_ID, 'source.js'); const jsxGrammar = require('../../data/grammars/javascript.jsx.tmlanguage.json'); - registry.registerTextMateGrammarScope('source.jsx', { + registry.registerTextmateGrammarScope('source.jsx', { async getGrammarDefinition() { return { format: 'json', diff --git a/packages/typescript/src/browser/typescript-client-contribution.ts b/packages/typescript/src/browser/typescript-client-contribution.ts index 4ae992b14c76f..e802b652348de 100644 --- a/packages/typescript/src/browser/typescript-client-contribution.ts +++ b/packages/typescript/src/browser/typescript-client-contribution.ts @@ -14,9 +14,12 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { injectable, inject } from 'inversify'; -import { BaseLanguageClientContribution, Workspace, Languages, LanguageClientFactory } from '@theia/languages/lib/browser'; +import { injectable, inject, postConstruct } from 'inversify'; +import URI from '@theia/core/lib/common/uri'; +import { BaseLanguageClientContribution, Workspace, Languages, LanguageClientFactory, ILanguageClient, State } from '@theia/languages/lib/browser'; +import { TypeScriptInitializationOptions, TypeScriptInitializeResult } from 'typescript-language-server/lib/ts-protocol'; import { TYPESCRIPT_LANGUAGE_ID, TYPESCRIPT_LANGUAGE_NAME, TYPESCRIPT_REACT_LANGUAGE_ID, JAVASCRIPT_LANGUAGE_ID, JAVASCRIPT_REACT_LANGUAGE_ID } from '../common'; +import { TypescriptPreferences } from './typescript-preferences'; @injectable() export class TypeScriptClientContribution extends BaseLanguageClientContribution { @@ -24,6 +27,9 @@ export class TypeScriptClientContribution extends BaseLanguageClientContribution readonly id = TYPESCRIPT_LANGUAGE_ID; readonly name = TYPESCRIPT_LANGUAGE_NAME; + @inject(TypescriptPreferences) + protected readonly preferences: TypescriptPreferences; + constructor( @inject(Workspace) protected readonly workspace: Workspace, @inject(Languages) protected readonly languages: Languages, @@ -32,13 +38,13 @@ export class TypeScriptClientContribution extends BaseLanguageClientContribution super(workspace, languages, languageClientFactory); } - protected get globPatterns(): string[] { - return [ - '**/*.ts', - '**/*.tsx', - '**/*.js', - '**/*.jsx' - ]; + @postConstruct() + protected init(): void { + this.preferences.onPreferenceChanged(e => { + if (e.preferenceName === 'typescript.server.log') { + this.restart(); + } + }); } protected get documentSelector(): string[] { @@ -64,4 +70,30 @@ export class TypeScriptClientContribution extends BaseLanguageClientContribution ]; } + protected get initializationOptions(): TypeScriptInitializationOptions { + const options: TypeScriptInitializationOptions = {}; + const logVerbosity = this.preferences['typescript.server.log']; + if (logVerbosity !== 'off') { + options.logVerbosity = logVerbosity; + } + return options; + } + + protected _logFileUri: URI | undefined; + get logFileUri(): URI | undefined { + return this._logFileUri; + } + protected onReady(languageClient: ILanguageClient): void { + if (languageClient.initializeResult) { + const initializeResult = languageClient.initializeResult as TypeScriptInitializeResult; + this._logFileUri = initializeResult.logFileUri !== undefined ? new URI(initializeResult.logFileUri) : undefined; + } + languageClient.onDidChangeState(({ newState }) => { + if (newState === State.Stopped) { + this._logFileUri = undefined; + } + }); + super.onReady(languageClient); + } + } diff --git a/packages/typescript/src/browser/typescript-frontend-contribution.ts b/packages/typescript/src/browser/typescript-frontend-contribution.ts index 2b775159f555d..f7727b9f9434e 100644 --- a/packages/typescript/src/browser/typescript-frontend-contribution.ts +++ b/packages/typescript/src/browser/typescript-frontend-contribution.ts @@ -14,7 +14,7 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { injectable, inject } from 'inversify'; +import { injectable, inject, postConstruct } from 'inversify'; import * as tsp from 'typescript/lib/protocol'; import { Commands } from 'typescript-language-server/lib/commands'; import { QuickPickService, KeybindingRegistry, KeybindingContribution } from '@theia/core/lib/browser'; @@ -25,6 +25,7 @@ import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor'; import { TYPESCRIPT_LANGUAGE_ID } from '../common'; import { TypeScriptClientContribution } from './typescript-client-contribution'; import { TypeScriptKeybindingContexts } from './typescript-keybinding-contexts'; +import { FileSystemWatcher, FileMoveEvent } from '@theia/filesystem/lib/browser'; export namespace TypeScriptCommands { export const applyCompletionCodeAction: Command = { @@ -35,6 +36,14 @@ export namespace TypeScriptCommands { label: 'TypeScript: Organize Imports', id: 'typescript.edit.organizeImports' }; + export const openServerLog: Command = { + label: 'TypeScript: Open Server Log', + id: 'typescript.server.openLog' + }; + export const restartServer: Command = { + label: 'TypeScript: Restart Server', + id: 'typescript.server.restart' + }; } @injectable() @@ -49,6 +58,14 @@ export class TypeScriptFrontendContribution implements CommandContribution, Menu @inject(TypeScriptClientContribution) protected readonly clientContribution: TypeScriptClientContribution; + @inject(FileSystemWatcher) + protected readonly fileSystemWatcher: FileSystemWatcher; + + @postConstruct() + protected init(): void { + this.fileSystemWatcher.onDidMove(event => this.renameFile(event)); + } + registerCommands(commands: CommandRegistry): void { commands.registerCommand(TypeScriptCommands.applyCompletionCodeAction, { execute: async (file: string, codeActions: tsp.CodeAction[]) => { @@ -61,6 +78,16 @@ export class TypeScriptFrontendContribution implements CommandContribution, Menu isEnabled: () => !!this.currentEditor, isVisible: () => !!this.currentEditor }); + commands.registerCommand(TypeScriptCommands.openServerLog, { + execute: () => this.openServerLog(), + isEnabled: () => !!this.clientContribution.logFileUri, + isVisible: () => !!this.clientContribution.logFileUri + }); + commands.registerCommand(TypeScriptCommands.restartServer, { + execute: () => this.clientContribution.restart(), + isEnabled: () => this.clientContribution.running, + isVisible: () => this.clientContribution.running + }); } registerMenus(menus: MenuModelRegistry): void { @@ -78,6 +105,13 @@ export class TypeScriptFrontendContribution implements CommandContribution, Menu }); } + openServerLog(): void { + const logFileUri = this.clientContribution.logFileUri; + if (logFileUri) { + this.editorManager.open(logFileUri); + } + } + organizeImports(): void { const editor = MonacoEditor.get(this.currentEditor); if (editor) { @@ -113,4 +147,15 @@ export class TypeScriptFrontendContribution implements CommandContribution, Menu }); } + protected async renameFile({ sourceUri, targetUri }: FileMoveEvent): Promise { + const client = await this.clientContribution.languageClient; + return client.sendRequest(ExecuteCommandRequest.type, { + command: Commands.APPLY_RENAME_FILE, + arguments: [{ + sourceUri: sourceUri.toString(), + targetUri: targetUri.toString() + }] + }); + } + } diff --git a/packages/typescript/src/browser/typescript-frontend-module.ts b/packages/typescript/src/browser/typescript-frontend-module.ts index fe78e11886913..ee2e6cf1e105a 100644 --- a/packages/typescript/src/browser/typescript-frontend-module.ts +++ b/packages/typescript/src/browser/typescript-frontend-module.ts @@ -26,8 +26,11 @@ import { TypescriptGrammarContribution } from './typescript-language-config'; import { JavascriptGrammarContribution } from './javascript-language-config'; import { TypeScriptFrontendContribution } from './typescript-frontend-contribution'; import { TypeScriptEditorTextFocusContext } from './typescript-keybinding-contexts'; +import { bindTypescriptPreferences } from './typescript-preferences'; export default new ContainerModule(bind => { + bindTypescriptPreferences(bind); + bind(TypeScriptClientContribution).toSelf().inSingletonScope(); bind(LanguageClientContribution).toService(TypeScriptClientContribution); diff --git a/packages/typescript/src/browser/typescript-language-config.ts b/packages/typescript/src/browser/typescript-language-config.ts index c725102493c31..2697866c5404d 100644 --- a/packages/typescript/src/browser/typescript-language-config.ts +++ b/packages/typescript/src/browser/typescript-language-config.ts @@ -17,14 +17,16 @@ import { TYPESCRIPT_LANGUAGE_ID, TYPESCRIPT_REACT_LANGUAGE_ID, TYPESCRIPT_LANGUAGE_NAME, TYPESCRIPT_REACT_LANGUAGE_NAME } from '../common'; import { injectable } from 'inversify'; import { LanguageGrammarDefinitionContribution, TextmateRegistry } from '@theia/monaco/lib/browser/textmate'; +import { TextmateSnippetCompletionProvider } from '@theia/monaco/lib/browser/textmate/textmate-snippet-completion-provider'; @injectable() export class TypescriptGrammarContribution implements LanguageGrammarDefinitionContribution { registerTextmateLanguage(registry: TextmateRegistry) { this.registerTypeScript(); + this.registerSnippets(); const grammar = require('../../data/grammars/typescript.tmlanguage.json'); - registry.registerTextMateGrammarScope('source.ts', { + registry.registerTextmateGrammarScope('source.ts', { async getGrammarDefinition() { return { format: 'json', @@ -35,7 +37,7 @@ export class TypescriptGrammarContribution implements LanguageGrammarDefinitionC registry.mapLanguageIdToTextmateGrammar(TYPESCRIPT_LANGUAGE_ID, 'source.ts'); registry.registerGrammarConfiguration(TYPESCRIPT_LANGUAGE_ID, { - 'tokenTypes': { + tokenTypes: { 'entity.name.type.instance.jsdoc': 0, 'entity.name.function.tagged-template': 0, 'meta.import string.quoted': 0, @@ -44,7 +46,7 @@ export class TypescriptGrammarContribution implements LanguageGrammarDefinitionC }); const jsxGrammar = require('../../data/grammars/typescript.tsx.tmlanguage.json'); - registry.registerTextMateGrammarScope('source.tsx', { + registry.registerTextmateGrammarScope('source.tsx', { async getGrammarDefinition() { return { format: 'json', @@ -56,6 +58,12 @@ export class TypescriptGrammarContribution implements LanguageGrammarDefinitionC registry.mapLanguageIdToTextmateGrammar(TYPESCRIPT_REACT_LANGUAGE_ID, 'source.tsx'); } + protected registerSnippets() { + const snippets = require('../../data/snippets/typescript.json'); + monaco.languages.registerCompletionItemProvider(TYPESCRIPT_LANGUAGE_ID, new TextmateSnippetCompletionProvider(snippets, 'ts')); + monaco.languages.registerCompletionItemProvider(TYPESCRIPT_REACT_LANGUAGE_ID, new TextmateSnippetCompletionProvider(snippets, 'ts')); + } + protected registerTypeScript() { monaco.languages.register({ id: TYPESCRIPT_LANGUAGE_ID, diff --git a/packages/typescript/src/browser/typescript-preferences.ts b/packages/typescript/src/browser/typescript-preferences.ts new file mode 100644 index 0000000000000..1c3d940913f84 --- /dev/null +++ b/packages/typescript/src/browser/typescript-preferences.ts @@ -0,0 +1,74 @@ +/******************************************************************************** + * Copyright (C) 2018 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 + ********************************************************************************/ + +import { interfaces } from 'inversify'; +import { + createPreferenceProxy, + PreferenceProxy, + PreferenceService, + PreferenceContribution, + PreferenceSchema, + PreferenceChangeEvent +} from '@theia/core/lib/browser/preferences'; + +export const typescriptPreferenceSchema: PreferenceSchema = { + 'type': 'object', + 'properties': { + 'typescript.trace.server': { + 'type': 'string', + 'enum': [ + 'off', + 'messages', + 'verbose' + ], + 'default': 'off', + 'description': 'Enable/disable tracing communications with the TS language server.' + }, + 'typescript.server.log': { + 'type': 'string', + 'enum': [ + 'off', + 'terse', + 'normal', + 'verbose' + ], + 'default': 'off', + // tslint:disable:max-line-length + 'description': 'Enables logging of the TS server to a file. This log can be used to diagnose TS Server issues. The log may contain file paths, source code, and other potentially sensitive information from your project.' + } + } +}; + +export interface TypescriptConfiguration { + 'typescript.server.log': 'off' | 'terse' | 'normal' | 'verbose' +} +export type TypescriptPreferenceChange = PreferenceChangeEvent; + +export const TypescriptPreferences = Symbol('TypescriptPreferences'); +export type TypescriptPreferences = PreferenceProxy; + +export function createTypescriptPreferences(preferences: PreferenceService): TypescriptPreferences { + return createPreferenceProxy(preferences, typescriptPreferenceSchema); +} + +export function bindTypescriptPreferences(bind: interfaces.Bind): void { + bind(TypescriptPreferences).toDynamicValue(ctx => { + const preferences = ctx.container.get(PreferenceService); + return createTypescriptPreferences(preferences); + }).inSingletonScope(); + + bind(PreferenceContribution).toConstantValue({ schema: typescriptPreferenceSchema }); +} diff --git a/packages/typescript/src/node/typescript-contribution.ts b/packages/typescript/src/node/typescript-contribution.ts index fedd3d9488918..f3d2e90138cac 100644 --- a/packages/typescript/src/node/typescript-contribution.ts +++ b/packages/typescript/src/node/typescript-contribution.ts @@ -33,5 +33,4 @@ export class TypeScriptContribution extends BaseLanguageServerContribution { const serverConnection = this.createProcessStreamConnection(command, args); this.forward(clientConnection, serverConnection); } - } diff --git a/packages/userstorage/package.json b/packages/userstorage/package.json index 232edb3e46f3a..747c4a3a1fd73 100644 --- a/packages/userstorage/package.json +++ b/packages/userstorage/package.json @@ -1,10 +1,10 @@ { "name": "@theia/userstorage", - "version": "0.3.13", + "version": "0.3.14", "description": "Theia - User Storage Extension", "dependencies": { - "@theia/core": "^0.3.13", - "@theia/filesystem": "^0.3.13" + "@theia/core": "^0.3.14", + "@theia/filesystem": "^0.3.14" }, "publishConfig": { "access": "public" @@ -39,7 +39,7 @@ "docs": "theiaext docs" }, "devDependencies": { - "@theia/ext-scripts": "^0.3.13" + "@theia/ext-scripts": "^0.3.14" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/variable-resolver/package.json b/packages/variable-resolver/package.json index dfcf62d341b29..99e61947d72ed 100644 --- a/packages/variable-resolver/package.json +++ b/packages/variable-resolver/package.json @@ -1,9 +1,9 @@ { "name": "@theia/variable-resolver", - "version": "0.3.13", + "version": "0.3.14", "description": "Theia - Variable Resolver Extension", "dependencies": { - "@theia/core": "^0.3.13" + "@theia/core": "^0.3.14" }, "publishConfig": { "access": "public" @@ -44,7 +44,7 @@ "docs": "theiaext docs" }, "devDependencies": { - "@theia/ext-scripts": "^0.3.13" + "@theia/ext-scripts": "^0.3.14" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/workspace/package.json b/packages/workspace/package.json index f645e89ed527a..499d7cdba26d4 100644 --- a/packages/workspace/package.json +++ b/packages/workspace/package.json @@ -1,11 +1,11 @@ { "name": "@theia/workspace", - "version": "0.3.13", + "version": "0.3.14", "description": "Theia - Workspace Extension", "dependencies": { - "@theia/core": "^0.3.13", - "@theia/filesystem": "^0.3.13", - "@theia/variable-resolver": "^0.3.13", + "@theia/core": "^0.3.14", + "@theia/filesystem": "^0.3.14", + "@theia/variable-resolver": "^0.3.14", "@types/fs-extra": "^4.0.2", "fs-extra": "^4.0.2", "moment": "^2.21.0", @@ -45,9 +45,9 @@ "docs": "theiaext docs" }, "devDependencies": { - "@theia/ext-scripts": "^0.3.13" + "@theia/ext-scripts": "^0.3.14" }, "nyc": { "extends": "../../configs/nyc.json" } -} \ No newline at end of file +} diff --git a/packages/workspace/src/browser/workspace-commands.ts b/packages/workspace/src/browser/workspace-commands.ts index 331e6f79b3a9a..8a5e341aa0c58 100644 --- a/packages/workspace/src/browser/workspace-commands.ts +++ b/packages/workspace/src/browser/workspace-commands.ts @@ -165,10 +165,20 @@ export class WorkspaceCommandContribution implements CommandContribution { registry.registerCommand(WorkspaceCommands.FILE_RENAME, this.newUriAwareCommandHandler({ execute: uri => this.getParent(uri).then(parent => { if (parent) { + const initialValue = uri.path.base; const dialog = new SingleTextInputDialog({ title: 'Rename File', - initialValue: uri.path.base, - validate: name => this.validateFileName(name, parent) + initialValue, + initialSelectionRange: { + start: 0, + end: uri.path.name.length + }, + validate: (name, mode) => { + if (initialValue === name && mode === 'preview') { + return false; + } + return this.validateFileName(name, parent); + } }); dialog.open().then(name => { if (name) { @@ -220,7 +230,7 @@ export class WorkspaceCommandContribution implements CommandContribution { isEnabled: () => this.workspaceService.isMultiRootWorkspaceOpened, isVisible: uris => !uris.length || this.areWorkspaceRoots(uris), execute: async uris => { - const node = await this.fileDialogService.show({ title: WorkspaceCommands.ADD_FOLDER.label! }); + const node = await this.fileDialogService.showOpenDialog({ title: WorkspaceCommands.ADD_FOLDER.label! }); this.addFolderToWorkspace(node); } })); diff --git a/packages/workspace/src/browser/workspace-frontend-contribution.ts b/packages/workspace/src/browser/workspace-frontend-contribution.ts index 106531e68baf3..ea53281861028 100644 --- a/packages/workspace/src/browser/workspace-frontend-contribution.ts +++ b/packages/workspace/src/browser/workspace-frontend-contribution.ts @@ -16,7 +16,7 @@ import { injectable, inject } from 'inversify'; import { CommandContribution, CommandRegistry, MenuContribution, MenuModelRegistry } from '@theia/core/lib/common'; -import { open, OpenerService, CommonMenus, StorageService, LabelProvider, ConfirmDialog } from '@theia/core/lib/browser'; +import { open, OpenerService, CommonMenus, StorageService, LabelProvider, ConfirmDialog, KeybindingRegistry, KeybindingContribution } from '@theia/core/lib/browser'; import { FileStatNode, FileDialogService, OpenFileDialogProps } from '@theia/filesystem/lib/browser'; import { FileSystem } from '@theia/filesystem/lib/common'; import { WorkspaceService } from './workspace-service'; @@ -24,7 +24,7 @@ import { WorkspaceCommands } from './workspace-commands'; import { QuickOpenWorkspace } from './quick-open-workspace'; @injectable() -export class WorkspaceFrontendContribution implements CommandContribution, MenuContribution { +export class WorkspaceFrontendContribution implements CommandContribution, KeybindingContribution, MenuContribution { constructor( @inject(FileSystem) protected readonly fileSystem: FileSystem, @@ -81,9 +81,24 @@ export class WorkspaceFrontendContribution implements CommandContribution, MenuC }); } + registerKeybindings(keybindings: KeybindingRegistry): void { + keybindings.registerKeybinding({ + command: WorkspaceCommands.OPEN.id, + keybinding: 'ctrlcmd+alt+o', + }); + keybindings.registerKeybinding({ + command: WorkspaceCommands.OPEN_WORKSPACE.id, + keybinding: 'ctrlcmd+alt+w', + }); + keybindings.registerKeybinding({ + command: WorkspaceCommands.OPEN_RECENT_WORKSPACE.id, + keybinding: 'ctrlcmd+alt+r', + }); + } + protected showFileDialog(props: OpenFileDialogProps): void { this.workspaceService.roots.then(async roots => { - const node = await this.fileDialogService.show(props, roots[0]); + const node = await this.fileDialogService.showOpenDialog(props, roots[0]); this.openFile(node); }); } diff --git a/packages/workspace/src/browser/workspace-frontend-module.ts b/packages/workspace/src/browser/workspace-frontend-module.ts index bcfeead99e3b5..c586df6e85fbe 100644 --- a/packages/workspace/src/browser/workspace-frontend-module.ts +++ b/packages/workspace/src/browser/workspace-frontend-module.ts @@ -16,14 +16,16 @@ import { ContainerModule, interfaces } from 'inversify'; import { CommandContribution, MenuContribution } from '@theia/core/lib/common'; -import { WebSocketConnectionProvider, FrontendApplicationContribution } from '@theia/core/lib/browser'; +import { WebSocketConnectionProvider, FrontendApplicationContribution, KeybindingContribution } from '@theia/core/lib/browser'; import { OpenFileDialogFactory, SaveFileDialogFactory, OpenFileDialogProps, SaveFileDialogProps, - createOpenFileDialog, - createSaveFileDialog + createOpenFileDialogContainer, + createSaveFileDialogContainer, + OpenFileDialog, + SaveFileDialog } from '@theia/filesystem/lib/browser'; import { StorageService } from '@theia/core/lib/browser/storage-service'; import { LabelProviderContribution } from '@theia/core/lib/browser/label-provider'; @@ -50,7 +52,7 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un }).inSingletonScope(); bind(WorkspaceFrontendContribution).toSelf().inSingletonScope(); - for (const identifier of [CommandContribution, MenuContribution]) { + for (const identifier of [CommandContribution, KeybindingContribution, MenuContribution]) { bind(identifier).toDynamicValue(ctx => ctx.container.get(WorkspaceFrontendContribution) ).inSingletonScope(); @@ -58,12 +60,12 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un bind(OpenFileDialogFactory).toFactory(ctx => (props: OpenFileDialogProps) => - createOpenFileDialog(ctx.container, props) + createOpenFileDialogContainer(ctx.container, props).get(OpenFileDialog) ); bind(SaveFileDialogFactory).toFactory(ctx => (props: SaveFileDialogProps) => - createSaveFileDialog(ctx.container, props) + createSaveFileDialogContainer(ctx.container, props).get(SaveFileDialog) ); bind(CommandContribution).to(WorkspaceCommandContribution).inSingletonScope(); diff --git a/packages/workspace/src/browser/workspace-preferences.ts b/packages/workspace/src/browser/workspace-preferences.ts index 36a250d8f08af..30f7f2b82a152 100644 --- a/packages/workspace/src/browser/workspace-preferences.ts +++ b/packages/workspace/src/browser/workspace-preferences.ts @@ -28,16 +28,12 @@ export const workspacePreferenceSchema: PreferenceSchema = { properties: { 'workspace.preserveWindow': { description: 'Enable opening workspaces in current window', - additionalProperties: { - type: 'boolean' - }, + type: 'boolean', default: false }, 'workspace.supportMultiRootWorkspace': { description: 'Enable the multi-root workspace support to test this feature internally', - additionalProperties: { - type: 'boolean' - }, + type: 'boolean', default: false } } diff --git a/yarn.lock b/yarn.lock index 8474bade241d7..cc77a851b5843 100644 --- a/yarn.lock +++ b/yarn.lock @@ -118,6 +118,10 @@ dependencies: samsam "1.3.0" +"@typefox/monaco-editor-core@^0.14.6": + version "0.14.6" + resolved "https://registry.yarnpkg.com/@typefox/monaco-editor-core/-/monaco-editor-core-0.14.6.tgz#32e378f3430913504ea9c7063944444a04429892" + "@types/base64-arraybuffer@0.1.0": version "0.1.0" resolved "https://registry.yarnpkg.com/@types/base64-arraybuffer/-/base64-arraybuffer-0.1.0.tgz#739eea0a974d13ae831f96d97d882ceb0b187543" @@ -626,6 +630,12 @@ add-stream@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/add-stream/-/add-stream-1.0.0.tgz#6a7990437ca736d5e1288db92bd3266d5f5cb2aa" +agent-base@4, agent-base@^4.1.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9" + dependencies: + es6-promisify "^5.0.0" + ajv-errors@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.0.tgz#ecf021fa108fd17dfb5e6b383f2dd233e31ffc59" @@ -1922,13 +1932,6 @@ caller-id@^0.1.0: dependencies: stack-trace "~0.0.7" -camel-case@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-3.0.0.tgz#ca3c3688a4e9cf3a4cda777dc4dcbc713249cf73" - dependencies: - no-case "^2.2.0" - upper-case "^1.1.1" - camelcase-keys@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" @@ -2029,29 +2032,6 @@ chalk@~0.4.0: has-color "~0.1.0" strip-ansi "~0.1.0" -change-case@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/change-case/-/change-case-3.0.2.tgz#fd48746cce02f03f0a672577d1d3a8dc2eceb037" - dependencies: - camel-case "^3.0.0" - constant-case "^2.0.0" - dot-case "^2.1.0" - header-case "^1.0.0" - is-lower-case "^1.1.0" - is-upper-case "^1.1.0" - lower-case "^1.1.1" - lower-case-first "^1.0.0" - no-case "^2.3.2" - param-case "^2.1.0" - pascal-case "^2.0.0" - path-case "^2.1.0" - sentence-case "^2.1.0" - snake-case "^2.1.0" - swap-case "^1.1.0" - title-case "^2.1.0" - upper-case "^1.1.1" - upper-case-first "^1.1.0" - changes-stream@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/changes-stream/-/changes-stream-2.2.0.tgz#9cf2bdbc2173c29c634aec9948e5d23b24d37c18" @@ -2448,13 +2428,6 @@ console-control-strings@^1.0.0, console-control-strings@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" -constant-case@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/constant-case/-/constant-case-2.0.0.tgz#4175764d389d3fa9c8ecd29186ed6005243b6a46" - dependencies: - snake-case "^2.1.0" - upper-case "^1.1.1" - constants-browserify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" @@ -2986,7 +2959,7 @@ debug@2.6.9, debug@^2.1.2, debug@^2.1.3, debug@^2.2.0, debug@^2.3.3, debug@^2.5. dependencies: ms "2.0.0" -debug@^3.0.0, debug@^3.1.0: +debug@3.1.0, debug@^3.0.0, debug@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" dependencies: @@ -3246,12 +3219,6 @@ domutils@^1.5.1: dom-serializer "0" domelementtype "1" -dot-case@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-2.1.1.tgz#34dcf37f50a8e93c2b3bca8bb7fb9155c7da3bee" - dependencies: - no-case "^2.2.0" - dot-prop@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-3.0.0.tgz#1b708af094a49c9a0e7dbcad790aba539dac1177" @@ -3479,6 +3446,12 @@ es6-promise@^4.0.3, es6-promise@^4.0.5, es6-promise@^4.2.4: version "4.2.4" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.4.tgz#dc4221c2b16518760bd8c39a52d8f356fc00ed29" +es6-promisify@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" + dependencies: + es6-promise "^4.0.3" + escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" @@ -3736,7 +3709,7 @@ extglob@^2.0.4: snapdragon "^0.8.1" to-regex "^3.0.1" -extract-zip@^1.0.3, extract-zip@^1.6.5: +extract-zip@^1.0.3: version "1.6.7" resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.7.tgz#a840b4b8af6403264c8db57f4f1a74333ef81fe9" dependencies: @@ -4073,14 +4046,6 @@ fs-extra@^0.30.0: path-is-absolute "^1.0.0" rimraf "^2.2.8" -fs-extra@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-1.0.0.tgz#cd3ce5f7e7cb6145883fcae3191e9877f8587950" - dependencies: - graceful-fs "^4.1.2" - jsonfile "^2.1.0" - klaw "^1.0.0" - fs-extra@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-3.0.1.tgz#3794f378c58b342ea7dbbb23095109c4b3b62291" @@ -4542,13 +4507,6 @@ har-validator@~5.1.0: ajv "^5.3.0" har-schema "^2.0.0" -har-validator@~5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.0.tgz#44657f5688a22cfd4b72486e81b3a3fb11742c29" - dependencies: - ajv "^5.3.0" - har-schema "^2.0.0" - has-ansi@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" @@ -4642,13 +4600,6 @@ hash.js@^1.0.0, hash.js@^1.0.3: inherits "^2.0.3" minimalistic-assert "^1.0.1" -hasha@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/hasha/-/hasha-2.2.0.tgz#78d7cbfc1e6d66303fe79837365984517b2f6ee1" - dependencies: - is-stream "^1.0.1" - pinkie-promise "^2.0.0" - hawk@~3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" @@ -4671,13 +4622,6 @@ he@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" -header-case@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/header-case/-/header-case-1.0.1.tgz#9535973197c144b09613cd65d317ef19963bd02d" - dependencies: - no-case "^2.2.0" - upper-case "^1.1.3" - highlight.js@^9.0.0, highlight.js@^9.12.0: version "9.12.0" resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.12.0.tgz#e6d9dbe57cbefe60751f02af336195870c90c01e" @@ -4766,6 +4710,13 @@ http-https@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/http-https/-/http-https-1.0.0.tgz#2f908dd5f1db4068c058cd6e6d4ce392c913389b" +http-proxy-agent@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz#e4821beef5b2142a2026bd73926fe537631c5405" + dependencies: + agent-base "4" + debug "3.1.0" + http-signature@~1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" @@ -4790,6 +4741,13 @@ https-browserify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" +https-proxy-agent@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz#51552970fa04d723e04c56d04178c3f92592bbc0" + dependencies: + agent-base "^4.1.0" + debug "^3.1.0" + humanize-duration@~3.10.0: version "3.10.1" resolved "https://registry.yarnpkg.com/humanize-duration/-/humanize-duration-3.10.1.tgz#65b550c0aa095156ecb7c340db44ee0bdf71af4b" @@ -5091,12 +5049,6 @@ is-glob@^4.0.0: dependencies: is-extglob "^2.1.1" -is-lower-case@^1.1.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/is-lower-case/-/is-lower-case-1.1.3.tgz#7e147be4768dc466db3bfb21cc60b31e6ad69393" - dependencies: - lower-case "^1.1.0" - is-natural-number@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/is-natural-number/-/is-natural-number-4.0.1.tgz#ab9d76e1db4ced51e35de0c72ebecf09f734cde8" @@ -5191,12 +5143,6 @@ is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" -is-upper-case@^1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/is-upper-case/-/is-upper-case-1.1.2.tgz#8d0b1fa7e7933a1e58483600ec7d9661cbaf756f" - dependencies: - upper-case "^1.1.0" - is-utf8@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" @@ -5493,6 +5439,10 @@ jsonc-parser@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-1.0.3.tgz#1d53d7160e401a783dbceabaad82473f80e6ad7e" +jsonc-parser@^2.0.0-next.1, jsonc-parser@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.0.1.tgz#9d23cd2709714fff508a1a6679d82135bee1ae60" + jsonfile@^2.1.0: version "2.4.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8" @@ -5547,10 +5497,6 @@ jxLoader@*: promised-io "*" walker "1.x" -kew@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/kew/-/kew-0.7.0.tgz#79d93d2d33363d6fdd2970b335d9141ad591d79b" - keyv@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.0.0.tgz#44923ba39e68b12a7cec7df6c3268c031f2ef373" @@ -6026,16 +5972,6 @@ loud-rejection@^1.0.0: currently-unhandled "^0.4.1" signal-exit "^3.0.0" -lower-case-first@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/lower-case-first/-/lower-case-first-1.0.2.tgz#e5da7c26f29a7073be02d52bac9980e5922adfa1" - dependencies: - lower-case "^1.1.2" - -lower-case@^1.1.0, lower-case@^1.1.1, lower-case@^1.1.2: - version "1.1.4" - resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac" - lowercase-keys@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306" @@ -6414,20 +6350,15 @@ monaco-css@^2.0.1: version "2.2.0" resolved "https://registry.yarnpkg.com/monaco-css/-/monaco-css-2.2.0.tgz#644e6e45d05f7704a4e1f563883bef4af9badb1f" -monaco-editor-core@^0.13.2: - version "0.13.2" - resolved "https://registry.yarnpkg.com/monaco-editor-core/-/monaco-editor-core-0.13.2.tgz#3e0ec54207c891e949dd7ad7851009ca422f7a76" - monaco-html@^2.0.2: version "2.2.0" resolved "https://registry.yarnpkg.com/monaco-html/-/monaco-html-2.2.0.tgz#435f2f4ce6e5c7f707fb57e7c8a05b41e5cd1aa0" -monaco-languageclient@next: - version "0.8.0-next.3" - resolved "https://registry.yarnpkg.com/monaco-languageclient/-/monaco-languageclient-0.8.0-next.3.tgz#4dda6d8a193a2a62471d36d04f370d6f3fd996dc" +monaco-languageclient@^0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/monaco-languageclient/-/monaco-languageclient-0.9.0.tgz#4b65684e277edab07625e76eb3d3d93e8f2130fa" dependencies: glob-to-regexp "^0.3.0" - monaco-editor-core "^0.13.2" vscode-base-languageclient "4.4.0" vscode-jsonrpc "^3.6.2" vscode-uri "^1.0.5" @@ -6554,12 +6485,6 @@ nise@^1.0.1: path-to-regexp "^1.7.0" text-encoding "^0.6.4" -no-case@^2.2.0, no-case@^2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.2.tgz#60b813396be39b3f1288a4c1ed5d1e7d28b464ac" - dependencies: - lower-case "^1.1.1" - node-abi@^2.0.0: version "2.4.3" resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.4.3.tgz#43666b7b17e57863e572409edbb82115ac7af28b" @@ -6984,6 +6909,10 @@ p-cancelable@^0.4.0: version "0.4.1" resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.4.1.tgz#35f363d67d52081c8d9585e37bcceb7e0bbcb2a0" +p-debounce@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-debounce/-/p-debounce-1.0.0.tgz#cb7f2cbeefd87a09eba861e112b67527e621e2fd" + p-each-series@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-1.0.0.tgz#930f3d12dd1f50e7434457a22cd6f04ac6ad7f71" @@ -7059,12 +6988,6 @@ parallel-transform@^1.1.0: inherits "^2.0.3" readable-stream "^2.1.5" -param-case@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/param-case/-/param-case-2.1.1.tgz#df94fd8cf6531ecf75e6bef9a0858fbc72be2247" - dependencies: - no-case "^2.2.0" - parse-asn1@^5.0.0: version "5.1.1" resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.1.tgz#f6bf293818332bd0dab54efb16087724745e6ca8" @@ -7113,13 +7036,6 @@ parseurl@~1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3" -pascal-case@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-2.0.1.tgz#2d578d3455f660da65eca18ef95b4e0de912761e" - dependencies: - camel-case "^3.0.0" - upper-case-first "^1.1.0" - pascalcase@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" @@ -7128,12 +7044,6 @@ path-browserify@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a" -path-case@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/path-case/-/path-case-2.1.1.tgz#94b8037c372d3fe2906e465bb45e25d226e8eea5" - dependencies: - no-case "^2.2.0" - path-dirname@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" @@ -7234,20 +7144,6 @@ performance-now@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" -phantomjs-prebuilt@^2.1.13: - version "2.1.16" - resolved "https://registry.yarnpkg.com/phantomjs-prebuilt/-/phantomjs-prebuilt-2.1.16.tgz#efd212a4a3966d3647684ea8ba788549be2aefef" - dependencies: - es6-promise "^4.0.3" - extract-zip "^1.6.5" - fs-extra "^1.0.0" - hasha "^2.2.0" - kew "^0.7.0" - progress "^1.1.8" - request "^2.81.0" - request-progress "^2.0.1" - which "^1.2.10" - pify@^2.0.0, pify@^2.2.0, pify@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -7595,10 +7491,6 @@ progress@2.0.0, progress@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.0.tgz#8a1be366bf8fc23db2bd23f10c6fe920b4389d1f" -progress@^1.1.8: - version "1.1.8" - resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" - prom-client@^10.2.0: version "10.2.3" resolved "https://registry.yarnpkg.com/prom-client/-/prom-client-10.2.3.tgz#a51bf21c239c954a6c5be4b1361fdd380218bb41" @@ -8116,11 +8008,13 @@ replace-ext@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb" -request-progress@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-2.0.1.tgz#5d36bb57961c673aa5b788dbc8141fdf23b44e08" +request-light@^0.2.2: + version "0.2.3" + resolved "https://registry.yarnpkg.com/request-light/-/request-light-0.2.3.tgz#a18635ec6dd92f8705c019c42ef645f684d94f7e" dependencies: - throttleit "^1.0.0" + http-proxy-agent "^2.1.0" + https-proxy-agent "^2.2.1" + vscode-nls "^3.2.2" request-promise-core@1.1.1: version "1.1.1" @@ -8188,7 +8082,7 @@ request@2.87.0: tunnel-agent "^0.6.0" uuid "^3.1.0" -request@^2.45.0, request@^2.81.0, request@^2.82.0, request@^2.83.0, request@^2.87.0: +request@^2.45.0, request@^2.82.0, request@^2.83.0, request@^2.87.0: version "2.88.0" resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" dependencies: @@ -8502,13 +8396,6 @@ send@0.16.2: range-parser "~1.2.0" statuses "~1.4.0" -sentence-case@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/sentence-case/-/sentence-case-2.1.1.tgz#1f6e2dda39c168bf92d13f86d4a918933f667ed4" - dependencies: - no-case "^2.2.0" - upper-case-first "^1.1.2" - serialize-javascript@^1.4.0: version "1.5.0" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.5.0.tgz#1aa336162c88a890ddad5384baebc93a655161fe" @@ -8654,12 +8541,6 @@ slide@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707" -snake-case@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-2.1.0.tgz#41bdb1b73f30ec66a04d4e2cad1b76387d4d6d9f" - dependencies: - no-case "^2.2.0" - snapdragon-node@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" @@ -9112,13 +8993,6 @@ svgo@^0.7.0: sax "~1.2.1" whet.extend "~0.9.9" -swap-case@^1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/swap-case/-/swap-case-1.1.2.tgz#c39203a4587385fad3c850a0bd1bcafa081974e3" - dependencies: - lower-case "^1.1.1" - upper-case "^1.1.1" - symbol-observable@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.1.tgz#8340fc4702c3122df5d22288f88283f513d3fdd4" @@ -9252,10 +9126,6 @@ throttleit@0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-0.0.2.tgz#cfedf88e60c00dd9697b61fdd2a8343a9b680eaf" -throttleit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c" - through2@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.1.tgz#384e75314d49f32de12eebb8136b8eb6b5d59da9" @@ -9299,13 +9169,6 @@ timespan@2.x: version "2.3.0" resolved "https://registry.yarnpkg.com/timespan/-/timespan-2.3.0.tgz#4902ce040bd13d845c8f59b27e9d59bad6f39929" -title-case@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/title-case/-/title-case-2.1.1.tgz#3e127216da58d2bc5becf137ab91dae3a7cd8faa" - dependencies: - no-case "^2.2.0" - upper-case "^1.0.3" - tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" @@ -9524,14 +9387,14 @@ typedoc@^0.8: typedoc-default-themes "^0.5.0" typescript "2.4.1" -typescript-language-server@next: - version "0.3.0-next.7704bd8" - resolved "https://registry.yarnpkg.com/typescript-language-server/-/typescript-language-server-0.3.0-next.7704bd8.tgz#1498c44872a7dadac29373b54d9c4f32b6416b48" +typescript-language-server@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/typescript-language-server/-/typescript-language-server-0.3.3.tgz#5b37764b204a852b5b8a01626eb43db764255568" dependencies: command-exists "1.2.6" commander "^2.11.0" fs-extra "^7.0.0" - lodash.debounce "^4.0.8" + p-debounce "^1.0.0" tempy "^0.2.1" vscode-languageserver "^4.4.0" vscode-uri "^1.0.5" @@ -9674,16 +9537,6 @@ upath@^1.0.0, upath@^1.0.2, upath@^1.0.5: version "1.1.0" resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.0.tgz#35256597e46a581db4793d0ce47fa9aebfc9fabd" -upper-case-first@^1.1.0, upper-case-first@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/upper-case-first/-/upper-case-first-1.1.2.tgz#5d79bedcff14419518fd2edb0a0507c9b6859115" - dependencies: - upper-case "^1.1.1" - -upper-case@^1.0.3, upper-case@^1.1.0, upper-case@^1.1.1, upper-case@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/upper-case/-/upper-case-1.1.3.tgz#f6b4501c2ec4cdd26ba78be7222961de77621598" - uri-js@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" @@ -9858,6 +9711,26 @@ vscode-debugprotocol@^1.26.0: version "1.31.0" resolved "https://registry.yarnpkg.com/vscode-debugprotocol/-/vscode-debugprotocol-1.31.0.tgz#8467eeabeea65f52da5ac03b03c18e10e8b95eb4" +vscode-json-languageserver@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/vscode-json-languageserver/-/vscode-json-languageserver-1.0.1.tgz#58bb0be82d816b50d71b3facfaffcae9a6587139" + dependencies: + jsonc-parser "^2.0.0-next.1" + request-light "^0.2.2" + vscode-json-languageservice "^3.0.12" + vscode-languageserver "^4.0.0" + vscode-nls "^3.2.2" + vscode-uri "^1.0.3" + +vscode-json-languageservice@^3.0.12: + version "3.1.5" + resolved "https://registry.yarnpkg.com/vscode-json-languageservice/-/vscode-json-languageservice-3.1.5.tgz#4ebac4cadcaedd55ea2d0716259b50a89955e00e" + dependencies: + jsonc-parser "^2.0.1" + vscode-languageserver-types "^3.10.1" + vscode-nls "^3.2.4" + vscode-uri "^1.0.5" + vscode-jsonrpc@^3.6.0, vscode-jsonrpc@^3.6.2: version "3.6.2" resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-3.6.2.tgz#3b5eef691159a15556ecc500e9a8a0dd143470c8" @@ -9873,13 +9746,17 @@ vscode-languageserver-types@^3.10.0, vscode-languageserver-types@^3.10.1: version "3.10.1" resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.10.1.tgz#d5d5f44f688a3b2aa9857dc53cb9cacca73fe35a" -vscode-languageserver@^4.4.0: +vscode-languageserver@^4.0.0, vscode-languageserver@^4.4.0: version "4.4.2" resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-4.4.2.tgz#600ae9cc7a6ff1e84d93c7807840c2cb5b22821b" dependencies: vscode-languageserver-protocol "^3.10.3" vscode-uri "^1.0.5" +vscode-nls@^3.2.2, vscode-nls@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.4.tgz#2166b4183c8aea884d20727f5449e62be69fd398" + vscode-nsfw@^1.0.17: version "1.0.17" resolved "https://registry.yarnpkg.com/vscode-nsfw/-/vscode-nsfw-1.0.17.tgz#da3820f26aea3a7e95cadc54bd9e5dae3d47e474" @@ -9894,7 +9771,7 @@ vscode-ripgrep@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/vscode-ripgrep/-/vscode-ripgrep-1.1.0.tgz#93c1e39d88342ee1b15530a12898ce930d511948" -vscode-uri@^1.0.1, vscode-uri@^1.0.5: +vscode-uri@^1.0.1, vscode-uri@^1.0.3, vscode-uri@^1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-1.0.6.tgz#6b8f141b0bbc44ad7b07e94f82f168ac7608ad4d" @@ -9942,13 +9819,6 @@ wdio-mocha-framework@0.5.9: mocha "^3.2.0" wdio-sync "0.6.13" -wdio-phantomjs-service@0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/wdio-phantomjs-service/-/wdio-phantomjs-service-0.2.2.tgz#bc6f1724f0c48db84135f8f900e4e34842812adc" - dependencies: - change-case "^3.0.0" - phantomjs-prebuilt "^2.1.13" - wdio-selenium-standalone-service@0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/wdio-selenium-standalone-service/-/wdio-selenium-standalone-service-0.0.8.tgz#182b1cf88504fb3ee24d933ea426db9d384dd414" @@ -10121,7 +9991,7 @@ which-module@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" -which@1, which@^1.1.1, which@^1.2.10, which@^1.2.12, which@^1.2.14, which@^1.2.8, which@^1.2.9, which@^1.3.0: +which@1, which@^1.1.1, which@^1.2.12, which@^1.2.14, which@^1.2.8, which@^1.2.9, which@^1.3.0: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" dependencies: