From ab853eec2f1d64706f5ef2142c0b4a9c3bc67483 Mon Sep 17 00:00:00 2001 From: Dan Arad Date: Wed, 20 Jan 2021 12:32:43 +0200 Subject: [PATCH] 6636-custom-editor: support for CustomEditor API Signed-off-by: Dan Arad --- CHANGELOG.md | 6 +- NOTICE.md | 164 +--- examples/api-tests/src/saveable.spec.js | 56 +- .../browser/common-frontend-contribution.ts | 8 - packages/core/src/browser/opener-service.ts | 26 +- .../preferences/preference-contribution.ts | 47 +- packages/core/src/browser/saveable.ts | 2 +- packages/core/src/browser/style/index.css | 6 +- packages/core/src/browser/style/tabs.css | 1 - .../core/src/browser/tree/tree-widget.tsx | 32 +- .../src/common/messaging/proxy-factory.ts | 20 +- .../env-variables/env-variables-server.ts | 6 +- packages/monaco/src/browser/monaco-command.ts | 7 + .../src/browser/monaco-editor-service.ts | 10 +- .../src/browser/monaco-editor-zone-widget.ts | 83 +- .../monaco/src/browser/monaco-workspace.ts | 18 +- packages/monaco/src/browser/style/index.css | 1 - .../src/browser/navigator-contribution.ts | 4 +- .../src/common/plugin-api-rpc-model.ts | 97 --- .../plugin-ext/src/common/plugin-api-rpc.ts | 112 +-- .../plugin-ext/src/common/plugin-protocol.ts | 43 +- .../src/hosted/browser/hosted-plugin.ts | 21 +- .../src/hosted/node/scanners/scanner-theia.ts | 31 +- .../browser/comments/comment-glyph-widget.ts | 65 -- .../comments/comment-thread-widget.tsx | 669 ---------------- .../comments/comments-context-key-service.ts | 68 -- .../browser/comments/comments-contribution.ts | 263 ------- .../browser/comments/comments-decorator.ts | 109 --- .../main/browser/comments/comments-main.ts | 450 ----------- .../main/browser/comments/comments-service.ts | 205 ----- .../custom-editor-contribution.ts | 49 ++ .../custom-editors/custom-editor-opener.tsx | 94 +++ .../custom-editors/custom-editor-service.ts | 108 +++ .../custom-editor-widget-factory.ts | 44 ++ .../custom-editors/custom-editor-widget.ts | 116 +++ .../custom-editors/custom-editors-main.ts | 540 +++++++++++++ .../src/main/browser/custom-editors/glob.ts | 742 ++++++++++++++++++ .../src/main/browser/custom-editors/paths.ts | 250 ++++++ .../plugin-custom-editor-registry.ts | 142 ++++ .../custom-editors/undo-redo-service.ts | 120 +++ .../src/main/browser/main-context.ts | 10 +- .../menus/menus-contribution-handler.ts | 64 -- .../browser/plugin-contribution-handler.ts | 19 +- .../main/browser/plugin-ext-deploy-command.ts | 6 +- .../browser/plugin-ext-frontend-module.ts | 27 +- .../src/main/browser/style/comments.css | 339 -------- .../src/main/browser/webview/pre/main.js | 72 +- .../src/main/browser/webviews-main.ts | 14 +- .../plugin-ext-frontend-electron-module.ts | 4 +- .../electron-webview-widget-factory.ts | 26 + packages/plugin-ext/src/plugin/comments.ts | 502 ------------ .../plugin-ext/src/plugin/custom-editors.ts | 376 +++++++++ .../plugin-ext/src/plugin/plugin-context.ts | 26 +- .../plugin-ext/src/plugin/plugin-manager.ts | 3 +- packages/plugin-ext/src/plugin/webviews.ts | 23 +- packages/plugin/src/theia-proposed.d.ts | 24 + packages/plugin/src/theia.d.ts | 289 +++++++ .../preferences-json-schema-contribution.ts | 45 +- .../src/node/process/process-task-runner.ts | 5 + .../src/browser/terminal-widget-impl.ts | 2 +- .../terminal/src/node/base-terminal-server.ts | 4 +- .../src/browser/workspace-commands.ts | 33 +- 62 files changed, 3343 insertions(+), 3405 deletions(-) delete mode 100644 packages/plugin-ext/src/main/browser/comments/comment-glyph-widget.ts delete mode 100644 packages/plugin-ext/src/main/browser/comments/comment-thread-widget.tsx delete mode 100644 packages/plugin-ext/src/main/browser/comments/comments-context-key-service.ts delete mode 100644 packages/plugin-ext/src/main/browser/comments/comments-contribution.ts delete mode 100644 packages/plugin-ext/src/main/browser/comments/comments-decorator.ts delete mode 100644 packages/plugin-ext/src/main/browser/comments/comments-main.ts delete mode 100644 packages/plugin-ext/src/main/browser/comments/comments-service.ts create mode 100644 packages/plugin-ext/src/main/browser/custom-editors/custom-editor-contribution.ts create mode 100644 packages/plugin-ext/src/main/browser/custom-editors/custom-editor-opener.tsx create mode 100644 packages/plugin-ext/src/main/browser/custom-editors/custom-editor-service.ts create mode 100644 packages/plugin-ext/src/main/browser/custom-editors/custom-editor-widget-factory.ts create mode 100644 packages/plugin-ext/src/main/browser/custom-editors/custom-editor-widget.ts create mode 100644 packages/plugin-ext/src/main/browser/custom-editors/custom-editors-main.ts create mode 100644 packages/plugin-ext/src/main/browser/custom-editors/glob.ts create mode 100644 packages/plugin-ext/src/main/browser/custom-editors/paths.ts create mode 100644 packages/plugin-ext/src/main/browser/custom-editors/plugin-custom-editor-registry.ts create mode 100644 packages/plugin-ext/src/main/browser/custom-editors/undo-redo-service.ts delete mode 100644 packages/plugin-ext/src/main/browser/style/comments.css delete mode 100644 packages/plugin-ext/src/plugin/comments.ts create mode 100644 packages/plugin-ext/src/plugin/custom-editors.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e4fcaea00e2c..2e50089923ac2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,5 @@ # Change Log -## v1.10.0 - -- [plugin] added `createDeployQuickOpenItem` method to create `DeployQuickOpenItem` in order to make extension deploy command extensible [#8919] (https://github.com/eclipse-theia/theia/pull/8919) - ## v1.9.0 - 16/12/2020 - [cli] updated error reporting for the `download-plugins` script [#8798](https://github.com/eclipse-theia/theia/pull/8798) @@ -69,7 +65,7 @@ - [monaco] added ability to compare quick-open entries [#8185](https://github.com/eclipse-theia/theia/pull/8185) - [output] improved extensibility of output channel commands [#8733](https://github.com/eclipse-theia/theia/pull/8733) - [plugin] added ability to use `viewId` as a progress location [#8700](https://github.com/eclipse-theia/theia/pull/8700) -- [plugin] added logic to only store webviews when they have a corresponding serializer [#8680](https://github.com/eclipse-theia/theia/pull/8680) +- [plugin] added logic to only store webviews when they have a corresponding serializer [#8680](https://github.com/eclipse-theia/theia/pull8680) - [plugin] added support for `activeColorTheme` and `onDidChangeActiveColorTheme` API [#8710](https://github.com/eclipse-theia/theia/pull/8710) - [plugin] added support for semantic highlighting [#8593](https://github.com/eclipse-theia/theia/pull/8593) - [plugin] fixed issue where problem matchers specified by task providers are not respected [#8756](https://github.com/eclipse-theia/theia/pull/8756) diff --git a/NOTICE.md b/NOTICE.md index 8032856b53e19..6d5da0aea72dc 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -20,27 +20,22 @@ This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at https://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: -(secondary) GPL-2.0 with Classpath-exception-2.0 which is available at GNU -General Public License v2.0 w/Classpath exception', -'https://www.gnu.org/software/classpath/license.html. +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 +SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: -* https://github.com/eclipse-theia/theia * https://github.com/eclipse/theia-generator-plugin * https://github.com/eclipse/theia-yeoman-plugin * https://github.com/eclipse/theia-plugin-packager -* https://github.com/eclipse-theia/theia-cpp-extensions +* https://github.com/eclipse/theia-cpp-extension * https://github.com/eclipse/theia-python-extension * https://github.com/eclipse/theia-java-extension -* https://github.com/eclipse-theia/theia-example -* https://github.com/eclipse-theia/cryptodetector -* https://github.com/eclipse-theia/generator-theia-extension ## Third-party Content @@ -66,78 +61,11 @@ Code copied from project Microsoft/vscode (1.32.3) * Project: https://code.visualstudio.com/ * Source: https://github.com/Microsoft/vscode -Code copied from project Microsoft/vscode (1.32.3) - -* License: MIT - -code copied from project microsoft/vscode (1.33.1) - -* License: MIT - Code copied from project Microsoft/vscode (1.33.1) * License: MIT -Code copied from project Microsoft/vscode (1.34.0) - -* License: MIT - -Code copied from project microsoft/vscode (1.41.1) - -* License: MIT - -code copied from project vscode (1.26.0) - -* License: MIT - -code copied from project vscode (1.31.0) - -* License: MIT - -code copied from project vscode (1.33.0) - -* License: MIT - -code copied from project vscode (1.33.0) - -* License: MIT - -code copied from project vscode (1.34.0) - -* License: MIT - -code copied from project vscode (1.36.1) - - -code copied from project vscode (1.36.1) - -* License: MIT - -code copied from project vscode (1.37.0) - -* License: MIT - -code copied from project vscode (1.37.0) - -* License: MIT - -code copied from project vscode-browser-preview (0.4.0) - -* License: MIT - -Code copied from VS Code (n/a) - -* License: MIT - -Code copied from VSCode (n/a) - -* License: MIT - -Code copied from vscode (n/a) - -* License: MIT - -Code copied from VSCode (n/a) +code copied from project microsoft/vscode (1.33.1) * License: MIT @@ -163,14 +91,8 @@ Electron (3.1.7) AND (BSD-2-Clause OR MIT OR Apache-2.0) AND ISC AND MIT AND X11 AND BSD-2-Clause-FreeBSD AND Public-Domain AND Unlicense AND MPL-2.0 AND (BSD-3-Clause OR MPL-2.0) AND CC-BY-3.0 AND (AFL-2.0 - -Electron (4.2.11) - -* License: MIT AND BSD-3-Clause AND LicenseRef-Public-Domain - -Electron (9.0.2) - -* License: MIT AND BSD-3-Clause AND LicenseRef-Public-Domain +* Project: https://electronjs.org/ +* Source: https://github.com/electron/electron electron@2.0.14 (2.0.14) @@ -204,38 +126,10 @@ Icon configure-inverse.svg (n/a) * Source: https://github.com/Microsoft/vscode/blob/master/src/vs/workbench/contrib/tasks/common/media/configure-inverse.svg#L1 -Icons copied from microsoft/vscode-icons version: -b73945c70f1117c4e65939dd3e10bdd623cb4ef3 (n/a) - -* License: CC-BY-4.0 - -inversify (5.0.1) - -* License: MIT - -jschardet (1.6.0) - -* License: (LGPL-2.1 OR LGPL-2.1+) AND (MIT OR GPL-2.0) -* Project: https://www.npmjs.com/package/jschardet -* Source: - https://github.com/aadsm/jschardet/tree/28152dd8db5904dc2cf9aa12ef4f8783f713e79a - -jschardet (2.1.1) - -* License: LGPL-2.1 OR LGPL-2.1+ - libffmpeg (FFmpeg) Delivered with Electron (3.1.7) * License: LGPL-2.1+ -libffmpeg (FFmpeg) delivered with Electron (4.2.11) - -* License: LGPG-2.1-or-later AND BSD-3-Clause AND MIT AND IJG - -libffmpeg (FFmpeg) delivered with Electron (9.0.2) - -* License: LGPG-2.1-or-later AND BSD-3-Clause AND MIT AND IJG - long.js (3.2.0) * License: Apache-2.0 @@ -254,7 +148,7 @@ monaco-typescript (2.3.0) native-keymap (1.2.5) -* License: BSD-3-Clause AND MIT +* License: Pending * Project: https://github.com/Microsoft/node-native-keymap * Source: https://github.com/Microsoft/node-native-keymap @@ -270,21 +164,12 @@ node.js dependencies for Theia (n/a) CC0-1.0 AND (BSD-3-Clause OR MPL-2.0) AND Unlicense AND (MIT OR GPL-3.0) AND (MIT OR GPL-2.0) AND (Apache-2.0 OR -Preference code copied from vscode (n/a) - - ps-list (5.0.1) * License: MIT * Project: https://github.com/sindresorhus/ps-list * Source: https://github.com/sindresorhus/ps-list -react-perfect-scrollbar:1.5.3 (1.5.3) - -* License: MIT -* Project: https://github.com/goldenyz/react-perfect-scrollbar -* Source: https://github.com/goldenyz/react-perfect-scrollbar - read-pkg (4.0.1) * License: MIT @@ -314,7 +199,7 @@ theia npm node (n/a) * License: BSD-2-Clause OR (MIT OR Apache-2.0) AND (AFL-2.1 OR BSD-3-Clause) AND Apache-2.0 AND Artistic-2.0 AND BSD-3-Clause AND (BSD-3-Clause OR MIT) AND MPL-2.0 AND CC0-1.0 AND CC-BY-3.0 AND CC-BY-4.0 AND CC-BY-SA-2.5 AND - GPL-2.0 WITH Autoconf-exception + GPL-2.0 WITH Autoconf-ex theia-cpp-extension npm node (n/a) @@ -343,10 +228,6 @@ VS Code (1.33.0) * License: MIT -VS Code built-in extensions (1.30.1) - -* License: Apache-2.0 AND MIT AND Unicode-DFS-2016 AND CC-BY-4.0 AND W3C - vscode (1.26.0) * License: MIT AND LicenseRef-Php_Tmbundle @@ -363,10 +244,6 @@ vscode-debugadapter-node (n/a) * License: MIT -vscode-icons (n/a) - -* License: CC-BY-4.0 AND MIT - vscode-java (0.36.0) * License: EPL-1.0 @@ -379,12 +256,6 @@ vscode-java-debug (0.15.0) * License: MIT -webdriverio (n/a) - -* License: MIT -* Project: http://webdriver.io/ -* Source: https://github.com/webdriverio/webdriverio.git - when (3.7.8) * License: MIT @@ -395,24 +266,12 @@ wjordan/browser-path SHA6719d19077b1454bff8b802f9be79cb1b69ebe7e (n/a) * License: MIT -xterm-addon-fit (0.3.0) - -* License: MIT - -xterm-addon-search (0.5.0) - -* License: MIT - xterm.js (3.9.1) * License: MIT * Project: https://xtermjs.org/ * Source: https://github.com/xtermjs/xterm.js -xterm.js (4.4) - -* License: MIT - yargs (12.0.1) * License: MIT @@ -452,6 +311,7 @@ please check the country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted. + ## Electron NOTICE: @@ -471,3 +331,5 @@ We strongly recommend downstream consumers verify the type of FFmpeg support configured and modify as required. More information on instructions to verify can be found here https://electronjs.org/docs/development/upgrading-chromium#verify-ffmpeg-support + + diff --git a/examples/api-tests/src/saveable.spec.js b/examples/api-tests/src/saveable.spec.js index 9bfdd43f16c8e..f8fa31d75fe7c 100644 --- a/examples/api-tests/src/saveable.spec.js +++ b/examples/api-tests/src/saveable.spec.js @@ -47,8 +47,6 @@ describe('Saveable', function () { const rootUri = workspaceService.tryGetRoots()[0].resource; const fileUri = rootUri.resolve('.test/foo.txt'); - const closeOnFileDelete = 'workbench.editor.closeOnFileDelete'; - /** * @param {FileResource['shouldOverwrite']} shouldOverwrite * @returns {Disposable} @@ -69,7 +67,7 @@ describe('Saveable', function () { beforeEach(async () => { await preferences.set('editor.autoSave', 'off', undefined, rootUri.toString()); - await preferences.set(closeOnFileDelete, true); + await preferences.set('editor.closeOnFileDelete', true); await editorManager.closeAll({ save: false }); await fileService.create(fileUri, 'foo', { fromUserGesture: false, overwrite: true }); widget = /** @type {EditorWidget & SaveableWidget} */ @@ -230,6 +228,22 @@ describe('Saveable', function () { assert.equal(state.value, 'foo', 'fs should NOT be updated after rejected close'); }); + it('delete file for saved with editor.CloseOnFileDelete off', async () => { + await preferences.set('editor.closeOnFileDelete', false); + assert.isFalse(Saveable.isDirty(widget), 'should NOT be dirty before delete'); + assert.isTrue(editor.document.valid, 'should be valid before delete'); + const waitForInvalid = new Deferred(); + const listener = editor.document.onDidChangeValid(() => waitForInvalid.resolve()); + try { + await fileService.delete(fileUri); + await waitForInvalid.promise; + assert.isFalse(editor.document.valid, 'should be INVALID after delete'); + assert.isFalse(widget.isDisposed, 'model should NOT be disposed after delete'); + } finally { + listener.dispose(); + } + }); + it('accept save on close and reject it', async () => { let outOfSync = false; toTearDown.push(setShouldOverwrite(async () => { @@ -449,40 +463,4 @@ describe('Saveable', function () { } }); - it(`'${closeOnFileDelete}' should keep the editor opened when set to 'false'`, async () => { - - await preferences.set(closeOnFileDelete, false); - assert.isFalse(preferences.get(closeOnFileDelete)); - assert.isFalse(Saveable.isDirty(widget)); - - const waitForDidChangeTitle = new Deferred(); - const listener = () => waitForDidChangeTitle.resolve(); - widget.title.changed.connect(listener); - try { - await fileService.delete(fileUri); - await waitForDidChangeTitle.promise; - assert.isTrue(widget.title.label.endsWith('(deleted)')); - assert.isFalse(widget.isDisposed); - } finally { - widget.title.changed.disconnect(listener); - } - }); - - it(`'${closeOnFileDelete}' should close the editor when set to 'true'`, async () => { - - await preferences.set(closeOnFileDelete, true); - assert.isTrue(preferences.get(closeOnFileDelete)); - assert.isFalse(Saveable.isDirty(widget)); - - const waitForDisposed = new Deferred(); - const listener = editor.onDispose(() => waitForDisposed.resolve()); - try { - await fileService.delete(fileUri); - await waitForDisposed.promise; - assert.isTrue(widget.isDisposed); - } finally { - listener.dispose(); - } - }); - }); diff --git a/packages/core/src/browser/common-frontend-contribution.ts b/packages/core/src/browser/common-frontend-contribution.ts index 470e2c2265fbb..d8e9f66869bb5 100644 --- a/packages/core/src/browser/common-frontend-contribution.ts +++ b/packages/core/src/browser/common-frontend-contribution.ts @@ -1865,14 +1865,6 @@ export class CommonFrontendContribution implements FrontendApplicationContributi dark: Color.transparent('button.background', 0.5), light: Color.transparent('button.background', 0.5) }, description: 'Background color of secondary buttons.' - }, - { - id: 'editorGutter.commentRangeForeground', - defaults: { - dark: '#c5c5c5', - light: '#c5c5c5', - hc: '#c5c5c5' - }, description: 'Editor gutter decoration color for commenting ranges.' } ); } diff --git a/packages/core/src/browser/opener-service.ts b/packages/core/src/browser/opener-service.ts index 5739b83870093..08994f11e0ce8 100644 --- a/packages/core/src/browser/opener-service.ts +++ b/packages/core/src/browser/opener-service.ts @@ -16,7 +16,7 @@ import { named, injectable, inject } from 'inversify'; import URI from '../common/uri'; -import { ContributionProvider, Prioritizeable, MaybePromise } from '../common'; +import { ContributionProvider, Prioritizeable, MaybePromise, Emitter, Event, Disposable } from '../common'; export interface OpenerOptions { } @@ -75,6 +75,10 @@ export interface OpenerService { * Reject if such does not exist. */ getOpener(uri: URI, options?: OpenerOptions): Promise; + /** + * Event that fires when a new opener is added or remove + */ + onOpenersStateChanged?: Event; } export async function open(openerService: OpenerService, uri: URI, options?: OpenerOptions): Promise { @@ -85,11 +89,26 @@ export async function open(openerService: OpenerService, uri: URI, options?: Ope @injectable() export class DefaultOpenerService implements OpenerService { + protected readonly additionalHandlers: OpenHandler[] = []; + + protected readonly onOpenersStateChangedEmitter = new Emitter(); + readonly onOpenersStateChanged = this.onOpenersStateChangedEmitter.event; + constructor( @inject(ContributionProvider) @named(OpenHandler) protected readonly handlersProvider: ContributionProvider ) { } + public addHandler(openHandler: OpenHandler): Disposable { + this.additionalHandlers.push(openHandler); + this.onOpenersStateChangedEmitter.fire(); + + return Disposable.create(() => { + this.additionalHandlers.splice(this.additionalHandlers.indexOf(openHandler), 1); + this.onOpenersStateChangedEmitter.fire(); + }); + } + async getOpener(uri: URI, options?: OpenerOptions): Promise { const handlers = await this.prioritize(uri, options); if (handlers.length >= 1) { @@ -114,7 +133,10 @@ export class DefaultOpenerService implements OpenerService { } protected getHandlers(): OpenHandler[] { - return this.handlersProvider.getContributions(); + return [ + ...this.handlersProvider.getContributions(), + ...this.additionalHandlers + ]; } } diff --git a/packages/core/src/browser/preferences/preference-contribution.ts b/packages/core/src/browser/preferences/preference-contribution.ts index 566247a0a320c..019a7a64a7817 100644 --- a/packages/core/src/browser/preferences/preference-contribution.ts +++ b/packages/core/src/browser/preferences/preference-contribution.ts @@ -106,8 +106,6 @@ export class PreferenceSchemaProvider extends PreferenceProvider { protected readonly preferences: { [name: string]: any } = {}; protected readonly combinedSchema: PreferenceDataSchema = { properties: {}, patternProperties: {} }; - protected readonly workspaceSchema: PreferenceDataSchema = { properties: {}, patternProperties: {} }; - protected readonly folderSchema: PreferenceDataSchema = { properties: {}, patternProperties: {} }; @inject(ContributionProvider) @named(PreferenceContribution) protected readonly preferenceContributions: ContributionProvider; @@ -189,9 +187,9 @@ export class PreferenceSchemaProvider extends PreferenceProvider { const overridden = this.overriddenPreferenceName(preferenceName); if (overridden) { delete this.overridePatternProperties.properties[`[${overridden.overrideIdentifier}]`]; - this.removePropFromSchemas(`[${overridden.overrideIdentifier}]`); + delete this.combinedSchema.properties[`[${overridden.overrideIdentifier}]`]; } else { - this.removePropFromSchemas(preferenceName); + delete this.combinedSchema.properties[preferenceName]; } const newValue = change.oldValue; const oldValue = change.newValue; @@ -231,7 +229,7 @@ export class PreferenceSchemaProvider extends PreferenceProvider { if (schemaProps.overridable) { this.overridePatternProperties.properties[preferenceName] = schemaProps; } - this.updateSchemaProps(preferenceName, schemaProps); + this.combinedSchema.properties[preferenceName] = schemaProps; const value = schemaProps.defaultValue = this.getDefaultValue(schemaProps, preferenceName); if (this.testOverrideValue(preferenceName, value)) { @@ -293,18 +291,6 @@ export class PreferenceSchemaProvider extends PreferenceProvider { return this.combinedSchema; } - getSchema(scope: PreferenceScope): PreferenceDataSchema { - switch (scope) { - case PreferenceScope.Default: - case PreferenceScope.User: - return this.combinedSchema; - case PreferenceScope.Workspace: - return this.workspaceSchema; - case PreferenceScope.Folder: - return this.folderSchema; - } - } - setSchema(schema: PreferenceSchema): Disposable { const changes = this.doSetSchema(schema); if (!changes.length) { @@ -389,31 +375,4 @@ export class PreferenceSchemaProvider extends PreferenceProvider { testOverrideValue(name: string, value: any): value is PreferenceSchemaProperties { return PreferenceSchemaProperties.is(value) && OVERRIDE_PROPERTY_PATTERN.test(name); } - - private updateSchemaProps(key: string, property: PreferenceDataProperty): void { - this.combinedSchema.properties[key] = property; - - switch (property.scope) { - case PreferenceScope.Workspace: - this.workspaceSchema.properties[key] = property; - break; - case PreferenceScope.Folder: - this.folderSchema.properties[key] = property; - break; - } - } - - private removePropFromSchemas(key: string): void { - const scope = this.combinedSchema.properties[key].scope; - - delete this.combinedSchema.properties[key]; - switch (scope) { - case PreferenceScope.Workspace: - delete this.workspaceSchema.properties[key]; - break; - case PreferenceScope.Folder: - delete this.folderSchema.properties[key]; - break; - } - } } diff --git a/packages/core/src/browser/saveable.ts b/packages/core/src/browser/saveable.ts index 959cabc9ebe1b..ad4ca17f276f9 100644 --- a/packages/core/src/browser/saveable.ts +++ b/packages/core/src/browser/saveable.ts @@ -58,7 +58,7 @@ export namespace Saveable { } // eslint-disable-next-line @typescript-eslint/no-explicit-any export function isSource(arg: any): arg is SaveableSource { - return !!arg && ('saveable' in arg); + return !!arg && ('saveable' in arg) && is(arg.saveable); } // eslint-disable-next-line @typescript-eslint/no-explicit-any export function is(arg: any): arg is Saveable { diff --git a/packages/core/src/browser/style/index.css b/packages/core/src/browser/style/index.css index 1b23814de8c0f..8ad1cfafc9da1 100644 --- a/packages/core/src/browser/style/index.css +++ b/packages/core/src/browser/style/index.css @@ -114,8 +114,8 @@ blockquote { margin-left: 8px; } -.theia-mod-disabled, .theia-mod-disabled:focus { - opacity: var(--theia-mod-disabled-opacity) !important; +.theia-mod-disabled { + opacity: var(--theia-mod-disabled-opacity); } .theia-header { @@ -161,7 +161,7 @@ blockquote { button.theia-button, .theia-button { border: none; - color: var(--theia-button-foreground) !important; + color: var(--theia-button-foreground); background-color: var(--theia-button-background); min-width: 65px; outline: none; diff --git a/packages/core/src/browser/style/tabs.css b/packages/core/src/browser/style/tabs.css index 699e8efb3be8c..2d94ce03e271e 100644 --- a/packages/core/src/browser/style/tabs.css +++ b/packages/core/src/browser/style/tabs.css @@ -335,7 +335,6 @@ body.theia-editor-highlightModifiedTabs .p-TabBar-content-container { display: flex; flex: 1; - position: relative; /* This is necessary for perfect-scrollbar */ } .p-TabBar-toolbar .item { diff --git a/packages/core/src/browser/tree/tree-widget.tsx b/packages/core/src/browser/tree/tree-widget.tsx index 4408fab712f57..50bd01b616a6f 100644 --- a/packages/core/src/browser/tree/tree-widget.tsx +++ b/packages/core/src/browser/tree/tree-widget.tsx @@ -658,28 +658,40 @@ export class TreeWidget extends ReactWidget implements StatefulWidget { if (fontData === undefined) { return original; } - const modified = { ...original }; // make a copy to mutate + let modified = original; const { color, style } = fontData; if (color) { - modified.color = color; + modified = { + ...modified, + color + }; } if (style) { (Array.isArray(style) ? style : [style]).forEach(s => { - switch (s) { + switch (style) { case 'bold': - modified.fontWeight = s; + modified = { + ...modified, + fontWeight: style + }; break; - case 'normal': - case 'oblique': + case 'normal': // Fall through. + case 'oblique': // Fall through. case 'italic': - modified.fontStyle = s; + modified = { + ...modified, + fontStyle: style + }; break; - case 'underline': + case 'underline': // Fall through. case 'line-through': - modified.textDecoration = s; + modified = { + ...modified, + textDecoration: style + }; break; default: - throw new Error(`Unexpected font style: "${s}".`); + throw new Error(`Unexpected font style: ${style}.`); } }); } diff --git a/packages/core/src/common/messaging/proxy-factory.ts b/packages/core/src/common/messaging/proxy-factory.ts index c8cc24f70a38c..815bde49b952c 100644 --- a/packages/core/src/common/messaging/proxy-factory.ts +++ b/packages/core/src/common/messaging/proxy-factory.ts @@ -132,8 +132,14 @@ export class JsonRpcProxyFactory implements ProxyHandler { * response. */ listen(connection: MessageConnection): void { - connection.onRequest((prop, ...args) => this.onRequest(prop, ...args)); - connection.onNotification((prop, ...args) => this.onNotification(prop, ...args)); + if (this.target) { + for (const prop in this.target) { + if (typeof this.target[prop] === 'function') { + connection.onRequest(prop, (...args) => this.onRequest(prop, ...args)); + connection.onNotification(prop, (...args) => this.onNotification(prop, ...args)); + } + } + } connection.onDispose(() => this.waitForConnection()); connection.listen(); this.connectionPromiseResolve(connection); @@ -153,11 +159,7 @@ export class JsonRpcProxyFactory implements ProxyHandler { */ protected async onRequest(method: string, ...args: any[]): Promise { try { - if (this.target) { - return await this.target[method](...args); - } else { - throw new Error(`no target was set to handle ${method}`); - } + return await this.target[method](...args); } catch (error) { const e = this.serializeError(error); if (e instanceof ResponseError) { @@ -177,9 +179,7 @@ export class JsonRpcProxyFactory implements ProxyHandler { * methods calls. */ protected onNotification(method: string, ...args: any[]): void { - if (this.target) { - this.target[method](...args); - } + this.target[method](...args); } /** diff --git a/packages/core/src/node/env-variables/env-variables-server.ts b/packages/core/src/node/env-variables/env-variables-server.ts index d0e687b701847..92644e0a53625 100644 --- a/packages/core/src/node/env-variables/env-variables-server.ts +++ b/packages/core/src/node/env-variables/env-variables-server.ts @@ -73,9 +73,9 @@ export class EnvVariablesServerImpl implements EnvVariablesServer { const uris: string[] = []; const drives = await drivelist.list(); for (const drive of drives) { - for (const mountpoint of drive.mountpoints) { - if (this.filterHiddenPartitions(mountpoint.path)) { - uris.push(FileUri.create(mountpoint.path).toString()); + for (const mounpoint of drive.mountpoints) { + if (this.filterHiddenPartitions(mounpoint.path)) { + uris.push(FileUri.create(mounpoint.path).toString()); } } } diff --git a/packages/monaco/src/browser/monaco-command.ts b/packages/monaco/src/browser/monaco-command.ts index 9c7499f1857d0..cb9b15c40e7e4 100644 --- a/packages/monaco/src/browser/monaco-command.ts +++ b/packages/monaco/src/browser/monaco-command.ts @@ -160,7 +160,14 @@ export class MonacoEditorCommandHandlers implements CommandContribution { ); }, isEnabled: () => { + /* + * We check monaco focused code editor first since they can contain inline like the debug console and embedded editors like in the peek reference. + * If there is not such then we check last focused editor tracked by us. + */ const editor = codeEditorService.getFocusedCodeEditor() || codeEditorService.getActiveCodeEditor(); + if (!editor) { + return false; + } if (editorActions.has(id)) { const action = editor && editor.getAction(id); return !!action && action.isSupported(); diff --git a/packages/monaco/src/browser/monaco-editor-service.ts b/packages/monaco/src/browser/monaco-editor-service.ts index 634b5afab4a42..c55705a3f9543 100644 --- a/packages/monaco/src/browser/monaco-editor-service.ts +++ b/packages/monaco/src/browser/monaco-editor-service.ts @@ -24,6 +24,8 @@ import { MonacoToProtocolConverter } from './monaco-to-protocol-converter'; import ICodeEditor = monaco.editor.ICodeEditor; import CommonCodeEditor = monaco.editor.CommonCodeEditor; import IResourceInput = monaco.editor.IResourceInput; +import { CustomEditorWidget } from './monaco-workspace'; +import { MonacoEditorModel } from './monaco-editor-model'; decorate(injectable(), monaco.services.CodeEditorServiceImpl); @@ -55,7 +57,13 @@ export class MonacoEditorService extends monaco.services.CodeEditorServiceImpl { * Monaco active editor is either focused or last focused editor. */ getActiveCodeEditor(): monaco.editor.IStandaloneCodeEditor | undefined { - const editor = MonacoEditor.getCurrent(this.editors); + let editor = MonacoEditor.getCurrent(this.editors); + if (!editor && CustomEditorWidget.is(this.shell.activeWidget)) { + const model = this.shell.activeWidget.modelRef.object; + if (model.editorTextModel instanceof MonacoEditorModel) { + editor = MonacoEditor.findByDocument(this.editors, model.editorTextModel)[0]; + } + } return editor && editor.getControl(); } diff --git a/packages/monaco/src/browser/monaco-editor-zone-widget.ts b/packages/monaco/src/browser/monaco-editor-zone-widget.ts index 6e5768e5cb5e9..ead035eb44427 100644 --- a/packages/monaco/src/browser/monaco-editor-zone-widget.ts +++ b/packages/monaco/src/browser/monaco-editor-zone-widget.ts @@ -19,7 +19,6 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, DisposableCollection, Event, Emitter } from '@theia/core'; -import { TrackedRangeStickiness } from '@theia/editor/lib/browser'; export interface MonacoEditorViewZone extends monaco.editor.IViewZone { id: string; @@ -27,8 +26,6 @@ export interface MonacoEditorViewZone extends monaco.editor.IViewZone { export class MonacoEditorZoneWidget implements Disposable { - private arrow: Arrow | undefined; - readonly zoneNode = document.createElement('div'); readonly containerNode = document.createElement('div'); @@ -45,7 +42,7 @@ export class MonacoEditorZoneWidget implements Disposable { ); constructor( - readonly editor: monaco.editor.IStandaloneCodeEditor, readonly showArrow: boolean = true + readonly editor: monaco.editor.IStandaloneCodeEditor ) { this.zoneNode.classList.add('zone-widget'); this.containerNode.classList.add('zone-widget-container'); @@ -56,7 +53,6 @@ export class MonacoEditorZoneWidget implements Disposable { dispose(): void { this.toDispose.dispose(); - this.hide(); } protected _options: MonacoEditorZoneWidget.Options | undefined; @@ -71,10 +67,10 @@ export class MonacoEditorZoneWidget implements Disposable { show(options: MonacoEditorZoneWidget.Options): void { let { afterLineNumber, afterColumn, heightInLines } = this._options = { showFrame: true, ...options }; const lineHeight = this.editor.getOption(monaco.editor.EditorOption.lineHeight); - // adjust heightInLines to viewport - const maxHeightInLines = Math.max(12, (this.editor.getLayoutInfo().height / lineHeight) * 0.8); - heightInLines = Math.min(heightInLines, maxHeightInLines); - let arrowHeight = 0; + const maxHeightInLines = (this.editor.getLayoutInfo().height / lineHeight) * .8; + if (heightInLines >= maxHeightInLines) { + heightInLines = maxHeightInLines; + } this.toHide.dispose(); this.editor.changeViewZones(accessor => { this.zoneNode.style.top = '-1000px'; @@ -96,14 +92,6 @@ export class MonacoEditorZoneWidget implements Disposable { this.editor.changeViewZones(a => a.removeZone(id)); this.viewZone = undefined; })); - if (this.showArrow) { - this.arrow = new Arrow(this.editor); - arrowHeight = Math.round(lineHeight / 3); - this.arrow.height = arrowHeight; - this.arrow.show({ lineNumber: options.afterLineNumber, column: 0 }); - - this.toHide.push(this.arrow); - } const widget: monaco.editor.IOverlayWidget = { getId: () => 'editor-zone-widget-' + id, getDomNode: () => this.zoneNode, @@ -114,6 +102,7 @@ export class MonacoEditorZoneWidget implements Disposable { this.toHide.push(Disposable.create(() => this.editor.removeOverlayWidget(widget))); }); + this.containerNode.style.top = 0 + 'px'; this.containerNode.style.overflow = 'hidden'; this.updateContainerHeight(heightInLines * lineHeight); @@ -133,7 +122,7 @@ export class MonacoEditorZoneWidget implements Disposable { } protected updateTop(top: number): void { - this.zoneNode.style.top = top + (this.showArrow ? 6 : 0) + 'px'; + this.zoneNode.style.top = top + 'px'; } protected updateHeight(zoneHeight: number): void { this.zoneNode.style.height = zoneHeight + 'px'; @@ -177,64 +166,6 @@ export class MonacoEditorZoneWidget implements Disposable { } } - -class IdGenerator { - private lastId: number; - constructor(private prefix: string) { - this.lastId = 0; - } - - nextId(): string { - return this.prefix + (++this.lastId); - } -} - -class Arrow implements Disposable { - - private readonly idGenerator = new IdGenerator('.arrow-decoration-'); - - private readonly ruleName = this.idGenerator.nextId(); - private decorations: string[] = []; - private _height: number = -1; - - constructor( - private readonly _editor: monaco.editor.ICodeEditor - ) {} - - dispose(): void { - this.hide(); - } - - set height(value: number) { - if (this._height !== value) { - this._height = value; - this._updateStyle(); - } - } - - private _updateStyle(): void { - const style = document.createElement('style'); - style.type = 'text/css'; - style.media = 'screen'; - document.getElementsByTagName('head')[0].appendChild(style); - const selector = `.monaco-editor ${this.ruleName}`; - const cssText = `border-style: solid; border-color: transparent transparent var(--theia-peekView-border); border-width: - ${this._height}px; bottom: -${this._height}px; margin-left: -${this._height}px; `; - (style.sheet).insertRule(selector + '{' + cssText + '}', 0); - } - - show(where: monaco.IPosition): void { - this.decorations = this._editor.deltaDecorations( - this.decorations, - [{ range: monaco.Range.fromPositions(where), options: { className: this.ruleName, stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges } }] - ); - } - - hide(): void { - this._editor.deltaDecorations(this.decorations, []); - } -} - export namespace MonacoEditorZoneWidget { export interface Options { afterLineNumber: number, diff --git a/packages/monaco/src/browser/monaco-workspace.ts b/packages/monaco/src/browser/monaco-workspace.ts index 909148bfd5049..5d69d7ba6e9e5 100644 --- a/packages/monaco/src/browser/monaco-workspace.ts +++ b/packages/monaco/src/browser/monaco-workspace.ts @@ -29,6 +29,8 @@ import { ProblemManager } from '@theia/markers/lib/browser'; import { MaybePromise } from '@theia/core/lib/common/types'; import { FileService } from '@theia/filesystem/lib/browser/file-service'; import { FileSystemProviderCapabilities } from '@theia/filesystem/lib/common/files'; +import { ApplicationShell, Widget } from '@theia/core/lib/browser'; +import { Reference } from '@theia/core/lib/common'; export namespace WorkspaceFileEdit { export function is(arg: Edit): arg is monaco.languages.WorkspaceFileEdit { @@ -48,6 +50,17 @@ export namespace WorkspaceTextEdit { } } +export namespace CustomEditorWidget { + export function is(arg: Widget | undefined): arg is CustomEditorWidget { + return !!arg && 'modelRef' in arg; + } +} + +export interface CustomEditorWidget extends Widget { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + readonly modelRef: Reference; +} + export type Edit = monaco.languages.WorkspaceFileEdit | monaco.languages.WorkspaceTextEdit; export interface WorkspaceFoldersChangeEvent { @@ -99,6 +112,9 @@ export class MonacoWorkspace { @inject(ProblemManager) protected readonly problems: ProblemManager; + @inject(ApplicationShell) + protected readonly shell: ApplicationShell; + @postConstruct() protected init(): void { this.resolveReady(); @@ -162,7 +178,7 @@ export class MonacoWorkspace { if (this.suppressedOpenIfDirty.indexOf(model) !== -1) { return; } - if (model.dirty && MonacoEditor.findByDocument(this.editorManager, model).length === 0) { + if (model.dirty && MonacoEditor.findByDocument(this.editorManager, model).length === 0 && !CustomEditorWidget.is(this.shell.activeWidget)) { // create a new reference to make sure the model is not disposed before it is // acquired by the editor, thus losing the changes that made it dirty. this.textModelService.createModelReference(model.textEditorModel.uri).then(ref => { diff --git a/packages/monaco/src/browser/style/index.css b/packages/monaco/src/browser/style/index.css index 9d95d8e526a77..8cb239480ca3e 100644 --- a/packages/monaco/src/browser/style/index.css +++ b/packages/monaco/src/browser/style/index.css @@ -40,7 +40,6 @@ .monaco-editor .zone-widget { position: absolute; z-index: 10; - background-color: var(--theia-editorWidget-background); } .monaco-editor .zone-widget .zone-widget-container { diff --git a/packages/navigator/src/browser/navigator-contribution.ts b/packages/navigator/src/browser/navigator-contribution.ts index e75d1838989be..b747e08db5be5 100644 --- a/packages/navigator/src/browser/navigator-contribution.ts +++ b/packages/navigator/src/browser/navigator-contribution.ts @@ -457,11 +457,11 @@ export class FileNavigatorContribution extends AbstractViewContribution; - collapsibleState?: CommentThreadCollapsibleState; - input?: CommentInput; - onDidChangeInput: TheiaEvent; - onDidChangeRange: TheiaEvent; - onDidChangeLabel: TheiaEvent; - onDidChangeCollapsibleState: TheiaEvent; - isDisposed: boolean; -} - -export interface CommentThreadChangedEventMain extends CommentThreadChangedEvent { - owner: string; -} - -export interface CommentThreadChangedEvent { - /** - * Added comment threads. - */ - readonly added: CommentThread[]; - - /** - * Removed comment threads. - */ - readonly removed: CommentThread[]; - - /** - * Changed comment threads. - */ - readonly changed: CommentThread[]; -} - -export interface CommentingRanges { - readonly resource: URI; - ranges: Range[]; -} - -export interface CommentInfo { - extensionId?: string; - threads: CommentThread[]; - commentingRanges: CommentingRanges; -} diff --git a/packages/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts index dec1f306ef768..1f98fc730c1d4 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts @@ -67,12 +67,7 @@ import { SearchInWorkspaceResult, AuthenticationSession, AuthenticationSessionsChangeEvent, - AuthenticationProviderInformation, - Comment, - CommentOptions, - CommentThreadCollapsibleState, - CommentThread, - CommentThreadChangedEvent, + AuthenticationProviderInformation } from './plugin-api-rpc-model'; import { ExtPluginApi } from './plugin-ext-api-contribution'; import { KeysToAnyValues, KeysToKeysToAnyValue } from './types'; @@ -95,6 +90,7 @@ import type { import { SerializableEnvironmentVariableCollection } from '@theia/terminal/lib/common/base-terminal-protocol'; import { ThemeType } from '@theia/core/lib/browser/theming'; import { Disposable } from '@theia/core/lib/common/disposable'; +import { URI } from 'vscode-uri'; export interface PreferenceData { [scope: number]: any; @@ -705,40 +701,6 @@ export interface TimelineCommandArg { uri: string; } -export namespace CommentsCommandArg { - export function is(arg: Object | undefined): arg is CommentsCommandArg { - return !!arg && typeof arg === 'object' && 'commentControlHandle' in arg && 'commentThreadHandle' in arg && 'text' in arg && !('commentUniqueId' in arg); - } -} -export interface CommentsCommandArg { - commentControlHandle: number; - commentThreadHandle: number; - text: string -} - -export namespace CommentsContextCommandArg { - export function is(arg: Object | undefined): arg is CommentsContextCommandArg { - return !!arg && typeof arg === 'object' && 'commentControlHandle' in arg && 'commentThreadHandle' in arg && 'commentUniqueId' in arg && !('text' in arg); - } -} -export interface CommentsContextCommandArg { - commentControlHandle: number; - commentThreadHandle: number; - commentUniqueId: number -} - -export namespace CommentsEditCommandArg { - export function is(arg: Object | undefined): arg is CommentsEditCommandArg { - return !!arg && typeof arg === 'object' && 'commentControlHandle' in arg && 'commentThreadHandle' in arg && 'commentUniqueId' in arg && 'text' in arg; - } -} -export interface CommentsEditCommandArg { - commentControlHandle: number; - commentThreadHandle: number; - commentUniqueId: number - text: string -} - export interface DecorationsExt { registerDecorationProvider(provider: theia.DecorationProvider): theia.Disposable $provideDecoration(id: number, uri: string): Promise @@ -1424,6 +1386,38 @@ export interface WebviewsMain { $unregisterSerializer(viewType: string): void; } +export interface CustomEditorsExt { + $resolveCustomEditorWebview( + handle: string, + viewType: string, + resource: URI, + title: string, + options: theia.WebviewPanelOptions, + cancellation: CancellationToken): Promise; + $createCustomDocument(resource: UriComponents, viewType: string, backupId: string | undefined, cancellation: CancellationToken): Promise<{ editable: boolean }>; + $disposeCustomDocument(resource: UriComponents, viewType: string): Promise; + $undo(resource: UriComponents, viewType: string, editId: number, isDirty: boolean): Promise; + $redo(resource: UriComponents, viewType: string, editId: number, isDirty: boolean): Promise; + $revert(resource: UriComponents, viewType: string, cancellation: CancellationToken): Promise; + $disposeEdits(resourceComponents: UriComponents, viewType: string, editIds: number[]): void; + $onSave(resource: UriComponents, viewType: string, cancellation: CancellationToken): Promise; + $onSaveAs(resource: UriComponents, viewType: string, targetResource: UriComponents, cancellation: CancellationToken): Promise; + $onMoveCustomEditor(handle: string, newResource: UriComponents, viewType: string): Promise; +} + +export interface CustomTextEditorCapabilities { + readonly supportsMove?: boolean; +} + +export interface CustomEditorsMain { + $registerTextEditorProvider(viewType: string, options: theia.WebviewPanelOptions, capabilities: CustomTextEditorCapabilities): void; + $registerCustomEditorProvider(viewType: string, options: theia.WebviewPanelOptions, supportsMultipleEditorsPerDocument: boolean): void; + $unregisterEditorProvider(viewType: string): void; + $createCustomEditorPanel(handle: string, title: string, options: theia.WebviewPanelOptions & theia.WebviewOptions): Promise; + $onDidEdit(resource: UriComponents, viewType: string, editId: number, label: string | undefined): void; + $onContentChange(resource: UriComponents, viewType: string): void; +} + export interface StorageMain { $set(key: string, value: KeysToAnyValues, isGlobal: boolean): Promise; $get(key: string, isGlobal: boolean): Promise; @@ -1514,35 +1508,6 @@ export interface ClipboardMain { $writeText(value: string): Promise; } -export interface CommentsExt { - $createCommentThreadTemplate(commentControllerHandle: number, uriComponents: UriComponents, range: Range): void; - $updateCommentThreadTemplate(commentControllerHandle: number, threadHandle: number, range: Range): Promise; - $deleteCommentThread(commentControllerHandle: number, commentThreadHandle: number): Promise; - $provideCommentingRanges(commentControllerHandle: number, uriComponents: UriComponents, token: CancellationToken): Promise; -} - -export interface CommentProviderFeatures { - options?: CommentOptions; -} - -export type CommentThreadChanges = Partial<{ - range: Range, - label: string, - contextValue: string, - comments: Comment[], - collapseState: CommentThreadCollapsibleState; -}>; - -export interface CommentsMain { - $registerCommentController(handle: number, id: string, label: string): void; - $unregisterCommentController(handle: number): void; - $updateCommentControllerFeatures(handle: number, features: CommentProviderFeatures): void; - $createCommentThread(handle: number, commentThreadHandle: number, threadId: string, resource: UriComponents, range: Range, extensionId: string): CommentThread | undefined; - $updateCommentThread(handle: number, commentThreadHandle: number, threadId: string, resource: UriComponents, changes: CommentThreadChanges): void; - $deleteCommentThread(handle: number, commentThreadHandle: number): void; - $onDidCommentThreadsChange(handle: number, event: CommentThreadChangedEvent): void; -} - export const PLUGIN_RPC_CONTEXT = { AUTHENTICATION_MAIN: >createProxyIdentifier('AuthenticationMain'), COMMAND_REGISTRY_MAIN: >createProxyIdentifier('CommandRegistryMain'), @@ -1562,6 +1527,7 @@ export const PLUGIN_RPC_CONTEXT = { LANGUAGES_MAIN: createProxyIdentifier('LanguagesMain'), CONNECTION_MAIN: createProxyIdentifier('ConnectionMain'), WEBVIEWS_MAIN: createProxyIdentifier('WebviewsMain'), + CUSTOM_EDITORS_MAIN: createProxyIdentifier('CustomEditorsMain'), STORAGE_MAIN: createProxyIdentifier('StorageMain'), TASKS_MAIN: createProxyIdentifier('TasksMain'), DEBUG_MAIN: createProxyIdentifier('DebugMain'), @@ -1572,8 +1538,7 @@ export const PLUGIN_RPC_CONTEXT = { CLIPBOARD_MAIN: >createProxyIdentifier('ClipboardMain'), LABEL_SERVICE_MAIN: >createProxyIdentifier('LabelServiceMain'), TIMELINE_MAIN: >createProxyIdentifier('TimelineMain'), - THEMING_MAIN: >createProxyIdentifier('ThemingMain'), - COMMENTS_MAIN: >createProxyIdentifier('CommentsMain') + THEMING_MAIN: >createProxyIdentifier('ThemingMain') }; export const MAIN_RPC_CONTEXT = { @@ -1594,6 +1559,7 @@ export const MAIN_RPC_CONTEXT = { LANGUAGES_EXT: createProxyIdentifier('LanguagesExt'), CONNECTION_EXT: createProxyIdentifier('ConnectionExt'), WEBVIEWS_EXT: createProxyIdentifier('WebviewsExt'), + CUSTOM_EDITORS_EXT: createProxyIdentifier('CustomEditorsExt'), STORAGE_EXT: createProxyIdentifier('StorageExt'), TASKS_EXT: createProxyIdentifier('TasksExt'), DEBUG_EXT: createProxyIdentifier('DebugExt'), @@ -1603,8 +1569,8 @@ export const MAIN_RPC_CONTEXT = { DECORATIONS_EXT: createProxyIdentifier('DecorationsExt'), LABEL_SERVICE_EXT: createProxyIdentifier('LabelServiceExt'), TIMELINE_EXT: createProxyIdentifier('TimeLineExt'), - THEMING_EXT: createProxyIdentifier('ThemingExt'), - COMMENTS_EXT: createProxyIdentifier('CommentsExt')}; + THEMING_EXT: createProxyIdentifier('ThemingExt') +}; export interface TasksExt { $provideTasks(handle: number, token?: CancellationToken): Promise; diff --git a/packages/plugin-ext/src/common/plugin-protocol.ts b/packages/plugin-ext/src/common/plugin-protocol.ts index 7acbb69a25710..bcf4a36b96a1c 100644 --- a/packages/plugin-ext/src/common/plugin-protocol.ts +++ b/packages/plugin-ext/src/common/plugin-protocol.ts @@ -71,6 +71,7 @@ export interface PluginPackageContribution { configurationDefaults?: RecursivePartial; languages?: PluginPackageLanguageContribution[]; grammars?: PluginPackageGrammarsContribution[]; + customEditors?: PluginPackageCustomEditor[]; viewsContainers?: { [location: string]: PluginPackageViewContainer[] }; views?: { [location: string]: PluginPackageView[] }; viewsWelcome?: PluginPackageViewWelcome[]; @@ -89,6 +90,23 @@ export interface PluginPackageContribution { resourceLabelFormatters?: ResourceLabelFormatter[]; } +export interface PluginPackageCustomEditor { + viewType: string; + displayName: string; + selector?: CustomEditorSelector[]; + priority?: CustomEditorPriority; +} + +export interface CustomEditorSelector { + readonly filenamePattern?: string; +} + +export enum CustomEditorPriority { + default = 'default', + builtin = 'builtin', + option = 'option', +} + export interface PluginPackageViewContainer { id: string; title: string; @@ -112,7 +130,6 @@ export interface PluginPackageCommand { title: string; category?: string; icon?: string | { light: string; dark: string; }; - enablement?: string; } export interface PluginPackageMenu { @@ -482,6 +499,7 @@ export interface PluginContribution { configurationDefaults?: PreferenceSchemaProperties; languages?: LanguageContribution[]; grammars?: GrammarsContribution[]; + customEditors?: CustomEditor[]; viewsContainers?: { [location: string]: ViewContainer[] }; views?: { [location: string]: View[] }; viewsWelcome?: ViewWelcome[]; @@ -604,6 +622,16 @@ export interface FoldingRules { markers?: FoldingMarkers; } +/** + * Custom Editors contribution + */ +export interface CustomEditor { + viewType: string; + displayName: string; + selector: CustomEditorSelector[]; + priority: CustomEditorPriority; +} + /** * Views Containers contribution */ @@ -638,7 +666,6 @@ export interface PluginCommand { category?: string; iconUrl?: IconUrl; themeIcon?: string; - enablement?: string; } export type IconUrl = string | { light: string; dark: string; }; @@ -665,18 +692,6 @@ export interface Keybinding { win?: string; } -/** - * Keybinding contribution - */ -export interface Keybinding { - keybinding?: string; - command: string; - when?: string; - mac?: string; - linux?: string; - win?: string; -} - /** * This interface describes a plugin lifecycle object. */ diff --git a/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts b/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts index 20153fb49aa1f..6967c830c74cc 100644 --- a/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts +++ b/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts @@ -62,6 +62,8 @@ import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/front import { environment } from '@theia/application-package/lib/environment'; import { JsonSchemaStore } from '@theia/core/lib/browser/json-schema-store'; import { FileService, FileSystemProviderActivationEvent } from '@theia/filesystem/lib/browser/file-service'; +import { PluginCustomEditorRegistry } from '../../main/browser/custom-editors/plugin-custom-editor-registry'; +import { CustomEditorWidget } from '../../main/browser/custom-editors/custom-editor-widget'; export type PluginHost = 'frontend' | string; export type DebugActivationEvent = 'onDebugResolve' | 'onDebugInitialConfigurations' | 'onDebugAdapterProtocolTracker'; @@ -151,6 +153,9 @@ export class HostedPluginSupport { @inject(JsonSchemaStore) protected readonly jsonSchemaStore: JsonSchemaStore; + @inject(PluginCustomEditorRegistry) + protected readonly customEditorRegistry: PluginCustomEditorRegistry; + private theiaReadyPromise: Promise; protected readonly managers = new Map(); @@ -197,9 +202,10 @@ export class HostedPluginSupport { this.taskProviderRegistry.onWillProvideTaskProvider(event => this.ensureTaskActivation(event)); this.taskResolverRegistry.onWillProvideTaskResolver(event => this.ensureTaskActivation(event)); this.fileService.onWillActivateFileSystemProvider(event => this.ensureFileSystemActivation(event)); + this.customEditorRegistry.onPendingOpenCustomEditor(event => this.activateByCustomEditor(event)); this.widgets.onDidCreateWidget(({ factoryId, widget }) => { - if (factoryId === WebviewWidget.FACTORY_ID && widget instanceof WebviewWidget) { + if ((factoryId === WebviewWidget.FACTORY_ID || factoryId === CustomEditorWidget.FACTORY_ID) && widget instanceof WebviewWidget) { const storeState = widget.storeState.bind(widget); const restoreState = widget.restoreState.bind(widget); @@ -556,6 +562,10 @@ export class HostedPluginSupport { await this.activateByEvent(`onCommand:${commandId}`); } + async activateByCustomEditor(viewType: string): Promise { + await this.activateByEvent(`onCustomEditor:${viewType}`); + } + activateByFileSystem(event: FileSystemProviderActivationEvent): Promise { return this.activateByEvent(`onFileSystem:${event.scheme}`); } @@ -713,10 +723,17 @@ export class HostedPluginSupport { this.webviewRevivers.delete(viewType); } - protected preserveWebviews(): void { + protected async preserveWebviews(): Promise { for (const webview of this.widgets.getWidgets(WebviewWidget.FACTORY_ID)) { this.preserveWebview(webview as WebviewWidget); } + for (const webview of this.widgets.getWidgets(CustomEditorWidget.FACTORY_ID)) { + (webview as CustomEditorWidget).modelRef.dispose(); + if ((webview as any)['closeWithoutSaving']) { + delete (webview as any)['closeWithoutSaving']; + } + this.customEditorRegistry.resolveWidget(webview as CustomEditorWidget); + } } protected preserveWebview(webview: WebviewWidget): void { 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 d450055d97fbd..cf1071dba459f 100644 --- a/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts +++ b/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts @@ -47,7 +47,10 @@ import { PluginCommand, IconUrl, ThemeContribution, - IconThemeContribution + IconThemeContribution, + PluginPackageCustomEditor, + CustomEditor, + CustomEditorPriority } from '../../../common/plugin-protocol'; import * as fs from 'fs'; import * as path from 'path'; @@ -177,6 +180,15 @@ export class TheiaPluginScanner implements PluginScanner { console.error(`Could not read '${rawPlugin.name}' contribution 'grammars'.`, rawPlugin.contributes!.grammars, err); } + try { + if (rawPlugin.contributes!.customEditors) { + const customEditors = this.readCustomEditors(rawPlugin.contributes.customEditors!); + contributions.customEditors = customEditors; + } + } catch (err) { + console.error(`Could not read '${rawPlugin.name}' contribution 'customEditors'.`, rawPlugin.contributes!.customEditors, err); + } + try { if (rawPlugin.contributes && rawPlugin.contributes.viewsContainers) { const viewsContainers = rawPlugin.contributes.viewsContainers; @@ -317,7 +329,7 @@ export class TheiaPluginScanner implements PluginScanner { return contributions; } - protected readCommand({ command, title, category, icon, enablement }: PluginPackageCommand, pck: PluginPackage): PluginCommand { + protected readCommand({ command, title, category, icon }: PluginPackageCommand, pck: PluginPackage): PluginCommand { let themeIcon: string | undefined; let iconUrl: IconUrl | undefined; if (icon) { @@ -334,7 +346,7 @@ export class TheiaPluginScanner implements PluginScanner { }; } } - return { command, title, category, iconUrl, themeIcon, enablement }; + return { command, title, category, iconUrl, themeIcon }; } protected toPluginUrl(pck: PluginPackage, relativePath: string): string { @@ -467,6 +479,19 @@ export class TheiaPluginScanner implements PluginScanner { }; } + private readCustomEditors(rawCustomEditors: PluginPackageCustomEditor[]): CustomEditor[] { + return rawCustomEditors.map(rawCustomEditor => this.readCustomEditor(rawCustomEditor)); + } + + private readCustomEditor(rawCustomEditor: PluginPackageCustomEditor): CustomEditor { + return { + viewType: rawCustomEditor.viewType, + displayName: rawCustomEditor.displayName, + selector: rawCustomEditor.selector || [], + priority: rawCustomEditor.priority || CustomEditorPriority.default + }; + } + private readViewsContainers(rawViewsContainers: PluginPackageViewContainer[], pck: PluginPackage): ViewContainer[] { return rawViewsContainers.map(rawViewContainer => this.readViewContainer(rawViewContainer, pck)); } diff --git a/packages/plugin-ext/src/main/browser/comments/comment-glyph-widget.ts b/packages/plugin-ext/src/main/browser/comments/comment-glyph-widget.ts deleted file mode 100644 index 746a39c618e03..0000000000000 --- a/packages/plugin-ext/src/main/browser/comments/comment-glyph-widget.ts +++ /dev/null @@ -1,65 +0,0 @@ -/******************************************************************************** - * Copyright (C) 2020 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 { Disposable } from '@theia/core/lib/common'; - -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -// some code copied and modified from https://github.com/microsoft/vscode/blob/1.49.3/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts - -export class CommentGlyphWidget implements Disposable { - - private lineNumber!: number; - private editor: monaco.editor.ICodeEditor; - private commentsDecorations: string[] = []; - readonly commentsOptions: monaco.editor.IModelDecorationOptions; - constructor(editor: monaco.editor.ICodeEditor) { - this.commentsOptions = { - isWholeLine: true, - linesDecorationsClassName: 'comment-range-glyph comment-thread' - }; - this.editor = editor; - } - - getPosition(): number { - const model = this.editor.getModel(); - const range = model && this.commentsDecorations && this.commentsDecorations.length - ? model.getDecorationRange(this.commentsDecorations[0]) - : null; - - return range ? range.startLineNumber : this.lineNumber; - } - - setLineNumber(lineNumber: number): void { - this.lineNumber = lineNumber; - const commentsDecorations = [{ - range: { - startLineNumber: lineNumber, startColumn: 1, - endLineNumber: lineNumber, endColumn: 1 - }, - options: this.commentsOptions - }]; - - this.commentsDecorations = this.editor.deltaDecorations(this.commentsDecorations, commentsDecorations); - } - - dispose(): void { - if (this.commentsDecorations) { - this.editor.deltaDecorations(this.commentsDecorations, []); - } - } -} diff --git a/packages/plugin-ext/src/main/browser/comments/comment-thread-widget.tsx b/packages/plugin-ext/src/main/browser/comments/comment-thread-widget.tsx deleted file mode 100644 index e95ffb1e162b5..0000000000000 --- a/packages/plugin-ext/src/main/browser/comments/comment-thread-widget.tsx +++ /dev/null @@ -1,669 +0,0 @@ -/******************************************************************************** - * Copyright (C) 2020 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 { MonacoEditorZoneWidget } from '@theia/monaco/lib/browser/monaco-editor-zone-widget'; -import { - Comment, - CommentMode, - CommentThread, - CommentThreadCollapsibleState -} from '../../../common/plugin-api-rpc-model'; -import { CommentGlyphWidget } from './comment-glyph-widget'; -import { BaseWidget, DISABLED_CLASS } from '@theia/core/lib/browser'; -import * as ReactDOM from 'react-dom'; -import * as React from 'react'; -import { MouseTargetType } from '@theia/editor/lib/browser'; -import { CommentsService } from './comments-service'; -import { - ActionMenuNode, - CommandRegistry, - CompositeMenuNode, - MenuModelRegistry, - MenuPath -} from '@theia/core/lib/common'; -import { CommentsContextKeyService } from './comments-context-key-service'; -import { RefObject } from 'react'; - -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -// some code copied and modified from https://github.com/microsoft/vscode/blob/1.49.3/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts - -export const COMMENT_THREAD_CONTEXT: MenuPath = ['comment_thread-context-menu']; -export const COMMENT_CONTEXT: MenuPath = ['comment-context-menu']; -export const COMMENT_TITLE: MenuPath = ['comment-title-menu']; - -export class CommentThreadWidget extends BaseWidget { - - protected readonly zoneWidget: MonacoEditorZoneWidget; - protected readonly commentGlyphWidget: CommentGlyphWidget; - protected readonly contextMenu: CompositeMenuNode; - protected readonly commentFormRef: RefObject = React.createRef(); - - protected isExpanded?: boolean; - - constructor( - editor: monaco.editor.IStandaloneCodeEditor, - private _owner: string, - private _commentThread: CommentThread, - private commentService: CommentsService, - protected readonly menus: MenuModelRegistry, - protected readonly contextKeyService: CommentsContextKeyService, - protected readonly commands: CommandRegistry - ) { - super(); - this.toDispose.push(this.zoneWidget = new MonacoEditorZoneWidget(editor)); - this.toDispose.push(this.commentGlyphWidget = new CommentGlyphWidget(editor)); - this.toDispose.push(this._commentThread.onDidChangeCollapsibleState(state => { - if (state === CommentThreadCollapsibleState.Expanded && !this.isExpanded) { - const lineNumber = this._commentThread.range.startLineNumber; - - this.display({ afterLineNumber: lineNumber, afterColumn: 1, heightInLines: 2}); - return; - } - - if (state === CommentThreadCollapsibleState.Collapsed && this.isExpanded) { - this.hide(); - return; - } - })); - this.contextKeyService.commentIsEmpty.set(true); - this.toDispose.push(this.zoneWidget.editor.onMouseDown(e => this.onEditorMouseDown(e))); - this.toDispose.push(this.contextKeyService.onDidChange(() => { - const commentForm = this.commentFormRef.current; - if (commentForm) { - commentForm.update(); - } - })); - this.contextMenu = this.menus.getMenu(COMMENT_THREAD_CONTEXT); - this.contextMenu.children.map(node => node instanceof ActionMenuNode && node.action.when).forEach(exp => { - if (typeof exp === 'string') { - this.contextKeyService.setExpression(exp); - } - }); - } - - public getGlyphPosition(): number { - return this.commentGlyphWidget.getPosition(); - } - - public collapse(): void { - this._commentThread.collapsibleState = CommentThreadCollapsibleState.Collapsed; - if (this._commentThread.comments && this._commentThread.comments.length === 0) { - this.deleteCommentThread(); - } - - this.hide(); - } - - private deleteCommentThread(): void { - this.dispose(); - this.commentService.disposeCommentThread(this.owner, this._commentThread.threadId); - } - - dispose(): void { - super.dispose(); - if (this.commentGlyphWidget) { - this.commentGlyphWidget.dispose(); - } - } - - toggleExpand(lineNumber: number): void { - if (this.isExpanded) { - this._commentThread.collapsibleState = CommentThreadCollapsibleState.Collapsed; - this.hide(); - if (!this._commentThread.comments || !this._commentThread.comments.length) { - this.deleteCommentThread(); - } - } else { - this._commentThread.collapsibleState = CommentThreadCollapsibleState.Expanded; - this.display({ afterLineNumber: lineNumber, afterColumn: 1, heightInLines: 2 }); - } - } - - hide(): void { - this.zoneWidget.hide(); - this.isExpanded = false; - super.hide(); - } - - display(options: MonacoEditorZoneWidget.Options): void { - this.isExpanded = true; - if (this._commentThread.collapsibleState && this._commentThread.collapsibleState !== CommentThreadCollapsibleState.Expanded) { - return; - } - this.commentGlyphWidget.setLineNumber(options.afterLineNumber); - this._commentThread.collapsibleState = CommentThreadCollapsibleState.Expanded; - this.zoneWidget.show(options); - this.update(); - } - - private onEditorMouseDown(e: monaco.editor.IEditorMouseEvent): void { - const range = e.target.range; - - if (!range) { - return; - } - - if (!e.event.leftButton) { - return; - } - - if (e.target.type !== MouseTargetType.GUTTER_LINE_DECORATIONS) { - return; - } - - const data = e.target.detail; - const gutterOffsetX = data.offsetX - data.glyphMarginWidth - data.lineNumbersWidth - data.glyphMarginLeft; - - // don't collide with folding and git decorations - if (gutterOffsetX > 14) { - return; - } - - const mouseDownInfo = { lineNumber: range.startLineNumber }; - - const { lineNumber } = mouseDownInfo; - - if (!range || range.startLineNumber !== lineNumber) { - return; - } - - if (e.target.type !== MouseTargetType.GUTTER_LINE_DECORATIONS) { - return; - } - - if (!e.target.element) { - return; - } - - if (this.commentGlyphWidget && this.commentGlyphWidget.getPosition() !== lineNumber) { - return; - } - - if (e.target.element.className.indexOf('comment-thread') >= 0) { - this.toggleExpand(lineNumber); - return; - } - - if (this._commentThread.collapsibleState === CommentThreadCollapsibleState.Collapsed) { - this.display({ afterLineNumber: mouseDownInfo.lineNumber, heightInLines: 2 }); - } else { - this.hide(); - } - } - - public get owner(): string { - return this._owner; - } - - public get commentThread(): CommentThread { - return this._commentThread; - } - - private getThreadLabel(): string { - let label: string | undefined; - label = this._commentThread.label; - - if (label === undefined) { - if (this._commentThread.comments && this._commentThread.comments.length) { - const onlyUnique = (value: Comment, index: number, self: Comment[]) => self.indexOf(value) === index; - const participantsList = this._commentThread.comments.filter(onlyUnique).map(comment => `@${comment.userName}`).join(', '); - label = `Participants: ${participantsList}`; - } else { - label = 'Start discussion'; - } - } - - return label; - } - - update(): void { - if (!this.isExpanded) { - return; - } - this.render(); - const headHeight = Math.ceil(this.zoneWidget.editor.getOption(monaco.editor.EditorOption.lineHeight) * 1.2); - const lineHeight = this.zoneWidget.editor.getOption(monaco.editor.EditorOption.lineHeight); - const arrowHeight = Math.round(lineHeight / 3); - const frameThickness = Math.round(lineHeight / 9) * 2; - const body = this.zoneWidget.containerNode.getElementsByClassName('body')[0]; - - const computedLinesNumber = Math.ceil((headHeight + body.clientHeight + arrowHeight + frameThickness + 8 /** margin bottom to avoid margin collapse */) / lineHeight); - this.zoneWidget.show({ afterLineNumber: this._commentThread.range.startLineNumber, heightInLines: computedLinesNumber }); - } - - protected render(): void { - const headHeight = Math.ceil(this.zoneWidget.editor.getOption(monaco.editor.EditorOption.lineHeight) * 1.2); - ReactDOM.render(, this.zoneWidget.containerNode); - } -} - -namespace CommentForm { - export interface Props { - menus: MenuModelRegistry, - commentThread: CommentThread; - commands: CommandRegistry; - contextKeyService: CommentsContextKeyService; - widget: CommentThreadWidget; - } - - export interface State { - expanded: boolean - } -} - -export class CommentForm

extends React.Component { - private readonly menu: CompositeMenuNode; - private inputRef: RefObject = React.createRef(); - private inputValue: string = ''; - private readonly getInput = () => this.inputValue; - private readonly clearInput: () => void = () => { - const input = this.inputRef.current; - if (input) { - this.inputValue = ''; - input.value = this.inputValue; - this.props.contextKeyService.commentIsEmpty.set(true); - } - }; - - update(): void { - this.setState(this.state); - } - - protected expand = () => { - this.setState({ expanded: true }); - // Wait for the widget to be rendered. - setTimeout(() => { - // Update the widget's height. - this.props.widget.update(); - this.inputRef.current?.focus(); - }, 100); - }; - protected collapse = () => { - this.setState({ expanded: false }); - // Wait for the widget to be rendered. - setTimeout(() => { - // Update the widget's height. - this.props.widget.update(); - }, 100); - }; - - componentDidMount(): void { - // Wait for the widget to be rendered. - setTimeout(() => { - this.inputRef.current?.focus(); - }, 100); - } - - private readonly onInput: (event: React.FormEvent) => void = (event: React.FormEvent) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const value = (event.target as any).value; - if (this.inputValue.length === 0 || value.length === 0) { - this.props.contextKeyService.commentIsEmpty.set(value.length === 0); - } - this.inputValue = value; - }; - - constructor(props: P) { - super(props); - this.state = { - expanded: false - }; - - const setState = this.setState.bind(this); - this.setState = newState => { - setState(newState); - }; - - this.menu = this.props.menus.getMenu(COMMENT_THREAD_CONTEXT); - this.menu.children.map(node => node instanceof ActionMenuNode && node.action.when).forEach(exp => { - if (typeof exp === 'string') { - this.props.contextKeyService.setExpression(exp); - } - }); - } - - render(): React.ReactNode { - const { commands, commentThread, contextKeyService } = this.props; - const hasExistingComments = commentThread.comments && commentThread.comments.length > 0; - return

-
- -
- - -
; - } -} - -namespace ReviewComment { - export interface Props { - menus: MenuModelRegistry, - comment: Comment; - commentThread: CommentThread; - contextKeyService: CommentsContextKeyService; - commands: CommandRegistry; - commentForm: RefObject; - } - - export interface State { - hover: boolean - } -} - -export class ReviewComment

extends React.Component { - - constructor(props: P) { - super(props); - this.state = { - hover: false - }; - - const setState = this.setState.bind(this); - this.setState = newState => { - setState(newState); - }; - } - - protected detectHover = (element: HTMLElement | null) => { - if (element) { - window.requestAnimationFrame(() => { - const hover = element.matches(':hover'); - this.setState({ hover }); - }); - } - }; - - protected showHover = () => this.setState({ hover: true }); - protected hideHover = () => this.setState({ hover: false }); - - render(): React.ReactNode { - const { comment, commentForm, contextKeyService, menus, commands, commentThread } = this.props; - const commentUniqueId = comment.uniqueIdInThread; - const { hover } = this.state; - contextKeyService.comment.set(comment.contextValue); - return

-
- -
-
-
- {comment.userName} - {comment.label} -
-
- {hover && menus.getMenu(COMMENT_TITLE).children.map((node, index) => node instanceof ActionMenuNode && - )} -
-
-
- - -
-
; - } -} - -namespace CommentBody { - export interface Props { - value: string - isVisible: boolean - } -} - -export class CommentBody extends React.Component { - render(): React.ReactNode { - const { value, isVisible } = this.props; - if (!isVisible) { - return false; - } - return
-
-

{value}

-
-
; - } -} - -namespace CommentEditContainer { - export interface Props { - contextKeyService: CommentsContextKeyService - menus: MenuModelRegistry, - comment: Comment; - commentThread: CommentThread; - commentForm: RefObject; - commands: CommandRegistry; - } -} - -export class CommentEditContainer extends React.Component { - private readonly inputRef: RefObject = React.createRef(); - private dirtyCommentMode: CommentMode | undefined; - private dirtyCommentFormState: boolean | undefined; - - componentDidUpdate(prevProps: Readonly, prevState: Readonly<{}>): void { - const commentFormState = this.props.commentForm.current?.state; - const mode = this.props.comment.mode; - if (this.dirtyCommentMode !== mode || (this.dirtyCommentFormState !== commentFormState?.expanded && !commentFormState?.expanded)) { - const currentInput = this.inputRef.current; - if (currentInput) { - // Wait for the widget to be rendered. - setTimeout(() => { - currentInput.focus(); - currentInput.setSelectionRange(currentInput.value.length, currentInput.value.length); - }, 50); - } - } - this.dirtyCommentMode = mode; - this.dirtyCommentFormState = commentFormState?.expanded; - } - - render(): React.ReactNode { - const { menus, comment, commands, commentThread, contextKeyService } = this.props; - if (!(comment.mode === CommentMode.Editing)) { - return false; - } - return
-
-
-