diff --git a/CHANGELOG.md b/CHANGELOG.md index 494deee01f4cd..ea13c5b50ef79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Breaking changes: +- [core] `CommandRegistry.registerHandler` registers a new handler with a higher priority than previous [#7539](https://github.com/eclipse-theia/theia/pull/7539) - [plugin] removed `configStorage` argument from `PluginManager.registerPlugin`. Use `PluginManager.configStorage` property instead. [#7265](https://github.com/eclipse-theia/theia/pull/7265#discussion_r399956070) - [process] `TerminalProcess` doesn't handle shell quoting, the shell process arguments must be prepared from the caller. Removed all methods related to shell escaping inside this class. You should use functions located in `@theia/process/lib/common/shell-quoting.ts` in order to process arguments for shells. diff --git a/examples/api-tests/src/find-replace.spec.js b/examples/api-tests/src/find-replace.spec.js new file mode 100644 index 0000000000000..a6d677db5d3b6 --- /dev/null +++ b/examples/api-tests/src/find-replace.spec.js @@ -0,0 +1,130 @@ +/******************************************************************************** + * Copyright (C) 2020 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +// @ts-check +describe('Find and Replace', function () { + + const { assert } = chai; + + const Uri = require('@theia/core/lib/common/uri'); + const { animationFrame } = require('@theia/core/lib/browser/browser'); + const { DisposableCollection } = require('@theia/core/lib/common/disposable'); + const { CommonCommands } = require('@theia/core/lib/browser/common-frontend-contribution'); + const { EditorManager } = require('@theia/editor/lib/browser/editor-manager'); + const { WorkspaceService } = require('@theia/workspace/lib/browser/workspace-service'); + const { CommandRegistry } = require('@theia/core/lib/common/command'); + const { KeybindingRegistry } = require('@theia/core/lib/browser/keybinding'); + const { ContextKeyService } = require('@theia/core/lib/browser/context-key-service'); + const { FileNavigatorContribution } = require('@theia/navigator/lib/browser/navigator-contribution'); + const { ApplicationShell } = require('@theia/core/lib/browser/shell/application-shell'); + + const container = window.theia.container; + const editorManager = container.get(EditorManager); + const workspaceService = container.get(WorkspaceService); + const commands = container.get(CommandRegistry); + const keybindings = container.get(KeybindingRegistry); + const contextKeyService = container.get(ContextKeyService); + const navigatorContribution = container.get(FileNavigatorContribution); + const shell = container.get(ApplicationShell); + + const rootUri = new Uri.default(workspaceService.tryGetRoots()[0].uri); + const fileUri = rootUri.resolve('webpack.config.js'); + + const toTearDown = new DisposableCollection(); + + /** + * @template T + * @param {() => Promise | T} condition + * @returns {Promise} + */ + function waitForAnimation(condition) { + return new Promise(async (resolve, dispose) => { + toTearDown.push({ dispose }); + do { + await animationFrame(); + } while (!condition()); + resolve(); + }); + } + + before(() => { + shell.leftPanelHandler.collapse(); + }); + + beforeEach(async function () { + await navigatorContribution.closeView(); + await editorManager.closeAll({ save: false }); + }); + + afterEach(async () => { + toTearDown.dispose(); + await navigatorContribution.closeView(); + await editorManager.closeAll({ save: false }); + }); + + after(() => { + shell.leftPanelHandler.collapse(); + }); + + /** + * @param {import('@theia/core/lib/common/command').Command} command + */ + async function assertEditorFindReplace(command) { + assert.isFalse(contextKeyService.match('findWidgetVisible')); + assert.isFalse(contextKeyService.match('findInputFocussed')); + assert.isFalse(contextKeyService.match('replaceInputFocussed')); + + keybindings.dispatchCommand(command.id); + await waitForAnimation(() => contextKeyService.match('findInputFocussed')); + + assert.isTrue(contextKeyService.match('findWidgetVisible')); + assert.isTrue(contextKeyService.match('findInputFocussed')); + assert.isFalse(contextKeyService.match('replaceInputFocussed')); + + keybindings.dispatchKeyDown('Tab'); + await waitForAnimation(() => !contextKeyService.match('findInputFocussed')); + assert.isTrue(contextKeyService.match('findWidgetVisible')); + assert.isFalse(contextKeyService.match('findInputFocussed')); + assert.equal(contextKeyService.match('replaceInputFocussed'), command === CommonCommands.REPLACE); + } + + for (const command of [CommonCommands.FIND, CommonCommands.REPLACE]) { + it(command.label + ' in the active editor', async function () { + await navigatorContribution.openView({ activate: true }); + + await editorManager.open(fileUri, { mode: 'activate' }); + + await assertEditorFindReplace(command); + }); + + it(command.label + ' in the active explorer without the current editor', async function () { + await navigatorContribution.openView({ activate: true }); + + // should not throw + await commands.executeCommand(command.id); + }); + + it(command.label + ' in the active explorer with the current editor', async function () { + await editorManager.open(fileUri, { mode: 'activate' }); + + await navigatorContribution.openView({ activate: true }); + + await assertEditorFindReplace(command); + }); + + } + +}); diff --git a/examples/api-tests/src/undo-redo-selectAll.spec.js b/examples/api-tests/src/undo-redo-selectAll.spec.js new file mode 100644 index 0000000000000..db7afc2b3b8f6 --- /dev/null +++ b/examples/api-tests/src/undo-redo-selectAll.spec.js @@ -0,0 +1,188 @@ +/******************************************************************************** + * Copyright (C) 2020 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +// @ts-check +describe('Undo, Redo and Select All', function () { + this.timeout(5000); + + const { assert } = chai; + + const Uri = require('@theia/core/lib/common/uri'); + const { animationFrame } = require('@theia/core/lib/browser/browser'); + const { DisposableCollection } = require('@theia/core/lib/common/disposable'); + const { CommonCommands } = require('@theia/core/lib/browser/common-frontend-contribution'); + const { EditorManager } = require('@theia/editor/lib/browser/editor-manager'); + const { WorkspaceService } = require('@theia/workspace/lib/browser/workspace-service'); + const { CommandRegistry } = require('@theia/core/lib/common/command'); + const { KeybindingRegistry } = require('@theia/core/lib/browser/keybinding'); + const { FileNavigatorContribution } = require('@theia/navigator/lib/browser/navigator-contribution'); + const { ApplicationShell } = require('@theia/core/lib/browser/shell/application-shell'); + const { MonacoEditor } = require('@theia/monaco/lib/browser/monaco-editor'); + const { ScmContribution } = require('@theia/scm/lib/browser/scm-contribution'); + + const container = window.theia.container; + const editorManager = container.get(EditorManager); + const workspaceService = container.get(WorkspaceService); + const commands = container.get(CommandRegistry); + const keybindings = container.get(KeybindingRegistry); + const navigatorContribution = container.get(FileNavigatorContribution); + const shell = container.get(ApplicationShell); + const scmContribution = container.get(ScmContribution); + + const rootUri = new Uri.default(workspaceService.tryGetRoots()[0].uri); + const fileUri = rootUri.resolve('webpack.config.js'); + + const toTearDown = new DisposableCollection(); + + /** + * @template T + * @param {() => Promise | T} condition + * @returns {Promise} + */ + function waitForAnimation(condition) { + return new Promise(async (resolve, dispose) => { + toTearDown.push({ dispose }); + do { + await animationFrame(); + } while (!condition()); + resolve(); + }); + } + + before(() => { + shell.leftPanelHandler.collapse(); + }); + + beforeEach(async function () { + await scmContribution.closeView(); + await navigatorContribution.closeView(); + await editorManager.closeAll({ save: false }); + }); + + afterEach(async () => { + toTearDown.dispose(); + await scmContribution.closeView(); + await navigatorContribution.closeView(); + await editorManager.closeAll({ save: false }); + }); + + after(() => { + shell.leftPanelHandler.collapse(); + }); + + /** + * @param {import('@theia/editor/lib/browser/editor-widget').EditorWidget} widget + */ + async function assertInEditor(widget) { + const originalContent = widget.editor.document.getText(); + const editor = /** @type {MonacoEditor} */ (MonacoEditor.get(widget)); + editor.getControl().pushUndoStop(); + editor.getControl().executeEdits('test', [{ + range: new monaco.Range(1, 1, 1, 1), + text: 'A' + }]); + editor.getControl().pushUndoStop(); + + const modifiedContent = widget.editor.document.getText(); + assert.notEqual(modifiedContent, originalContent); + + keybindings.dispatchCommand(CommonCommands.UNDO.id); + await waitForAnimation(() => widget.editor.document.getText() === originalContent); + assert.equal(widget.editor.document.getText(), originalContent); + + keybindings.dispatchCommand(CommonCommands.REDO.id); + await waitForAnimation(() => widget.editor.document.getText() === modifiedContent); + assert.equal(widget.editor.document.getText(), modifiedContent); + + const originalSelection = widget.editor.selection; + keybindings.dispatchCommand(CommonCommands.SELECT_ALL.id); + await waitForAnimation(() => widget.editor.selection.end.line !== originalSelection.end.line); + assert.notDeepEqual(widget.editor.selection, originalSelection); + } + + it('in the active editor', async function () { + await navigatorContribution.openView({ activate: true }); + + const widget = await editorManager.open(fileUri, { mode: 'activate' }); + await assertInEditor(widget); + }); + + it('in the active explorer without the current editor', async function () { + await navigatorContribution.openView({ activate: true }); + + // should not throw + await commands.executeCommand(CommonCommands.UNDO.id); + await commands.executeCommand(CommonCommands.REDO.id); + await commands.executeCommand(CommonCommands.SELECT_ALL.id); + }); + + it('in the active explorer with the current editor', async function () { + const widget = await editorManager.open(fileUri, { mode: 'activate' }); + + await navigatorContribution.openView({ activate: true }); + + await assertInEditor(widget); + }); + + async function assertInScm() { + const scmInput = document.activeElement; + if (!(scmInput instanceof HTMLTextAreaElement)) { + assert.isTrue(scmInput instanceof HTMLTextAreaElement); + return; + } + + const originalValue = scmInput.value; + document.execCommand('insertText', false, 'A'); + await waitForAnimation(() => scmInput.value !== originalValue); + const modifiedValue = scmInput.value; + assert.notEqual(originalValue, modifiedValue); + + keybindings.dispatchCommand(CommonCommands.UNDO.id); + await waitForAnimation(() => scmInput.value === originalValue); + assert.equal(scmInput.value, originalValue); + + keybindings.dispatchCommand(CommonCommands.REDO.id); + await waitForAnimation(() => scmInput.value === modifiedValue); + assert.equal(scmInput.value, modifiedValue); + + const selection = document.getSelection(); + if (!selection) { + assert.isDefined(selection); + return; + } + + selection.empty(); + assert.equal(selection.rangeCount, 0); + + keybindings.dispatchCommand(CommonCommands.SELECT_ALL.id); + await waitForAnimation(() => !!selection.rangeCount); + assert.notEqual(selection.rangeCount, 0); + assert.isTrue(selection.containsNode(scmInput)); + } + + it('in the active scm in workspace without the current editor', async function () { + await scmContribution.openView({ activate: true }); + await assertInScm(); + }); + + it('in the active scm in workspace with the current editor', async function () { + await editorManager.open(fileUri, { mode: 'activate' }); + + await scmContribution.openView({ activate: true }); + await assertInScm(); + }); + +}); diff --git a/packages/core/src/browser/common-frontend-contribution.ts b/packages/core/src/browser/common-frontend-contribution.ts index 83bfa4cbe5337..843541f66f4fe 100644 --- a/packages/core/src/browser/common-frontend-contribution.ts +++ b/packages/core/src/browser/common-frontend-contribution.ts @@ -109,6 +109,10 @@ export namespace CommonCommands { id: 'core.redo', label: 'Redo' }; + export const SELECT_ALL: Command = { + id: 'core.selectAll', + label: 'Select All' + }; export const FIND: Command = { id: 'core.find', @@ -500,11 +504,22 @@ export class CommonFrontendContribution implements FrontendApplicationContributi } }); - commandRegistry.registerCommand(CommonCommands.UNDO); - commandRegistry.registerCommand(CommonCommands.REDO); + commandRegistry.registerCommand(CommonCommands.UNDO, { + execute: () => document.execCommand('undo') + }); + commandRegistry.registerCommand(CommonCommands.REDO, { + execute: () => document.execCommand('redo') + }); + commandRegistry.registerCommand(CommonCommands.SELECT_ALL, { + execute: () => document.execCommand('selectAll') + }); - commandRegistry.registerCommand(CommonCommands.FIND); - commandRegistry.registerCommand(CommonCommands.REPLACE); + commandRegistry.registerCommand(CommonCommands.FIND, { + execute: () => { /* no-op */ } + }); + commandRegistry.registerCommand(CommonCommands.REPLACE, { + execute: () => { /* no-op */ } + }); commandRegistry.registerCommand(CommonCommands.NEXT_TAB, { isEnabled: () => this.shell.currentTabBar !== undefined, @@ -577,7 +592,7 @@ export class CommonFrontendContribution implements FrontendApplicationContributi isEnabled: () => { const currentWidget = this.shell.getCurrentWidget('main'); return currentWidget !== undefined && - this.shell.mainAreaTabBars.some(tb => tb.titles.some(title => title.owner !== currentWidget && title.closable)); + this.shell.mainAreaTabBars.some(tb => tb.titles.some(title => title.owner !== currentWidget && title.closable)); }, execute: () => { const currentWidget = this.shell.getCurrentWidget('main'); @@ -705,6 +720,10 @@ export class CommonFrontendContribution implements FrontendApplicationContributi command: CommonCommands.REDO.id, keybinding: 'ctrlcmd+shift+z' }, + { + command: CommonCommands.SELECT_ALL.id, + keybinding: 'ctrlcmd+a' + }, { command: CommonCommands.FIND.id, keybinding: 'ctrlcmd+f' @@ -841,16 +860,16 @@ export class CommonFrontendContribution implements FrontendApplicationContributi this.quickOpenService.open({ onType: (_, accept) => accept(items) }, { - placeholder: 'Select File Icon Theme', - fuzzyMatchLabel: true, - selectIndex: () => items.findIndex(item => item.id === this.iconThemes.current), - onClose: () => { - if (resetTo) { - previewTheme.cancel(); - this.iconThemes.current = resetTo; + placeholder: 'Select File Icon Theme', + fuzzyMatchLabel: true, + selectIndex: () => items.findIndex(item => item.id === this.iconThemes.current), + onClose: () => { + if (resetTo) { + previewTheme.cancel(); + this.iconThemes.current = resetTo; + } } - } - }); + }); } protected selectColorTheme(): void { @@ -880,19 +899,19 @@ export class CommonFrontendContribution implements FrontendApplicationContributi this.quickOpenService.open({ onType: (_, accept) => accept(items) }, { - placeholder: 'Select Color Theme (Up/Down Keys to Preview)', - fuzzyMatchLabel: true, - selectIndex: () => { - const current = this.themeService.getCurrentTheme().id; - return items.findIndex(item => item.id === current); - }, - onClose: () => { - if (resetTo) { - previewTheme.cancel(); - this.themeService.setCurrentTheme(resetTo); + placeholder: 'Select Color Theme (Up/Down Keys to Preview)', + fuzzyMatchLabel: true, + selectIndex: () => { + const current = this.themeService.getCurrentTheme().id; + return items.findIndex(item => item.id === current); + }, + onClose: () => { + if (resetTo) { + previewTheme.cancel(); + this.themeService.setCurrentTheme(resetTo); + } } - } - }); + }); } registerColors(colors: ColorRegistry): void { diff --git a/packages/core/src/browser/keybinding.ts b/packages/core/src/browser/keybinding.ts index 429a1f50ac34c..9bab414ea2a4b 100644 --- a/packages/core/src/browser/keybinding.ts +++ b/packages/core/src/browser/keybinding.ts @@ -571,6 +571,15 @@ export class KeybindingRegistry { return true; } + dispatchCommand(id: string, target?: EventTarget): void { + const keybindings = this.getKeybindingsForCommand(id); + if (keybindings.length) { + for (const keyCode of this.resolveKeybinding(keybindings[0])) { + this.dispatchKeyDown(keyCode, target); + } + } + } + dispatchKeyDown(input: KeyboardEventInit | KeyCode | string, target: EventTarget = document.activeElement || window): void { const eventInit = this.asKeyboardEventInit(input); const emulatedKeyboardEvent = new KeyboardEvent('keydown', eventInit); diff --git a/packages/core/src/common/command.ts b/packages/core/src/common/command.ts index 7ce830ec693db..4b6c2f2146527 100644 --- a/packages/core/src/common/command.ts +++ b/packages/core/src/common/command.ts @@ -236,13 +236,17 @@ export class CommandRegistry implements CommandService { /** * Register the given handler for the given command identifier. + * + * If there is already a handler for the given command + * then the given handler is registered as more specific, and + * has higher priority during enablement, visibility and toggle state evaluations. */ registerHandler(commandId: string, handler: CommandHandler): Disposable { let handlers = this._handlers[commandId]; if (!handlers) { this._handlers[commandId] = handlers = []; } - handlers.push(handler); + handlers.unshift(handler); return { dispose: () => { const idx = handlers.indexOf(handler); diff --git a/packages/monaco/src/browser/monaco-command-registry.ts b/packages/monaco/src/browser/monaco-command-registry.ts index 83b587a25e2ff..d4d7168dc044f 100644 --- a/packages/monaco/src/browser/monaco-command-registry.ts +++ b/packages/monaco/src/browser/monaco-command-registry.ts @@ -63,7 +63,6 @@ export class MonacoCommandRegistry { protected execute(monacoHandler: MonacoEditorCommandHandler, ...args: any[]): any { const editor = this.monacoEditors.current; if (editor) { - editor.focus(); return Promise.resolve(monacoHandler.execute(editor, ...args)); } return Promise.resolve(); diff --git a/packages/monaco/src/browser/monaco-command.ts b/packages/monaco/src/browser/monaco-command.ts index 3cad3ff68c418..701c1b96aaf53 100644 --- a/packages/monaco/src/browser/monaco-command.ts +++ b/packages/monaco/src/browser/monaco-command.ts @@ -17,66 +17,34 @@ import { injectable, inject } from 'inversify'; import { ProtocolToMonacoConverter } from 'monaco-languageclient/lib'; import { Position, Location } from '@theia/languages/lib/browser'; -import { Command, CommandContribution, CommandRegistry } from '@theia/core'; +import { CommandContribution, CommandRegistry, CommandHandler } from '@theia/core/lib/common/command'; import { CommonCommands } from '@theia/core/lib/browser'; import { QuickOpenService } from '@theia/core/lib/browser/quick-open/quick-open-service'; import { QuickOpenItem, QuickOpenMode } from '@theia/core/lib/browser/quick-open/quick-open-model'; import { EditorCommands } from '@theia/editor/lib/browser'; import { MonacoEditor } from './monaco-editor'; import { MonacoCommandRegistry, MonacoEditorCommandHandler } from './monaco-command-registry'; -import MenuRegistry = monaco.actions.MenuRegistry; -import { MonacoCommandService } from './monaco-command-service'; +import { MonacoEditorService } from './monaco-editor-service'; +import { MonacoTextModelService } from './monaco-text-model-service'; -// vs code doesn't use iconClass anymore, but icon instead, so some adaptation is required to reuse it on theia side -export type MonacoIcon = { dark?: monaco.Uri; light?: monaco.Uri } | monaco.theme.ThemeIcon; -export type MonacoCommand = Command & { icon?: MonacoIcon, delegate?: string }; export namespace MonacoCommands { - export const UNDO = 'undo'; - export const REDO = 'redo'; - export const COMMON_KEYBOARD_ACTIONS = new Set([UNDO, REDO]); - export const COMMON_ACTIONS: { - [action: string]: string - } = {}; - COMMON_ACTIONS[UNDO] = CommonCommands.UNDO.id; - COMMON_ACTIONS[REDO] = CommonCommands.REDO.id; - COMMON_ACTIONS['actions.find'] = CommonCommands.FIND.id; - COMMON_ACTIONS['editor.action.startFindReplaceAction'] = CommonCommands.REPLACE.id; + export const COMMON_ACTIONS = new Map([ + ['undo', CommonCommands.UNDO.id], + ['redo', CommonCommands.REDO.id], + ['editor.action.selectAll', CommonCommands.SELECT_ALL.id], + ['actions.find', CommonCommands.FIND.id], + ['editor.action.startFindReplaceAction', CommonCommands.REPLACE.id] + ]); - export const SELECTION_SELECT_ALL = 'editor.action.select.all'; export const GO_TO_DEFINITION = 'editor.action.revealDefinition'; - export const ACTIONS = new Map(); - ACTIONS.set(SELECTION_SELECT_ALL, { id: SELECTION_SELECT_ALL, label: 'Select All', delegate: 'editor.action.selectAll' }); export const EXCLUDE_ACTIONS = new Set([ - ...Object.keys(COMMON_ACTIONS), 'editor.action.quickCommand', 'editor.action.clipboardCutAction', 'editor.action.clipboardCopyAction', 'editor.action.clipboardPasteAction' ]); - const icons = new Map(); - for (const menuItem of MenuRegistry.getMenuItems(7)) { - - const commandItem = menuItem.command; - if (commandItem && commandItem.icon) { - icons.set(commandItem.id, commandItem.icon); - } - } - for (const command of monaco.editorExtensions.EditorExtensionsRegistry.getEditorActions()) { - const id = command.id; - if (!EXCLUDE_ACTIONS.has(id)) { - const label = command.label; - const icon = icons.get(id); - ACTIONS.set(id, { id, label, icon }); - } - } - for (const keybinding of monaco.keybindings.KeybindingsRegistry.getDefaultKeybindings()) { - const id = keybinding.command; - if (!ACTIONS.has(id) && !EXCLUDE_ACTIONS.has(id)) { - ACTIONS.set(id, { id, delegate: id }); - } - } } @injectable() @@ -94,49 +62,120 @@ export class MonacoEditorCommandHandlers implements CommandContribution { @inject(QuickOpenService) protected readonly quickOpenService: QuickOpenService; + @inject(MonacoEditorService) + protected readonly codeEditorService: MonacoEditorService; + + @inject(MonacoTextModelService) + protected readonly textModelService: MonacoTextModelService; + + @inject(monaco.contextKeyService.ContextKeyService) + protected readonly contextKeyService: monaco.contextKeyService.ContextKeyService; + registerCommands(): void { - this.registerCommonCommandHandlers(); + this.registerMonacoCommands(); this.registerEditorCommandHandlers(); - this.registerMonacoActionCommands(); - this.registerInternalLanguageServiceCommands(); } - protected registerInternalLanguageServiceCommands(): void { - const instantiationService = monaco.services.StaticServices.instantiationService.get(); + /** + * Register commands from Monaco to Theia registry. + * + * Monaco has different kind of commands which should be handled differently by Theia. + * + * ### Editor Actions + * + * They should be registered with a label to be visible in the quick command palette. + * + * Such actions should be enabled only if the current editor is available and + * it supports such action in the current context. + * + * ### Editor Commands + * + * Such actions should be enabled only if the current editor is available. + * + * `actions.find` and `editor.action.startFindReplaceAction` are registed as handlers for `find` and `replace`. + * If handlers are not enabled then the core should prevent the default browser behaviour. + * Other Theia extensions can register alternative implementations using custom enablement. + * + * ### Global Commands + * + * These commands are not necessary dependend on the current editor and enabled always. + * But they depend on services which are global in VS Code, but bound to the editor in Monaco, + * i.e. `ICodeEditorService` or `IContextKeyService`. We should take care of providing Theia implementations for such services. + * + * #### Global Native or Editor Commands + * + * Namely: `undo`, `redo` and `editor.action.selectAll`. They depend on `ICodeEditorService`. + * They will try to delegate to the current editor and if it is not available delegate to the browser. + * They are registered as handlers for corresponding core commands always. + * Other Theia extensions can provide alternative implementations by introducing a dependency to `@theia/monaco` extension. + * + * #### Global Language Commands + * + * Like `_executeCodeActionProvider`, they depend on `ICodeEditorService` and `ITextModelService`. + * + * #### Global Context Commands + * + * It is `setContext`. It depends on `IContextKeyService`. + * + * #### Global Editor Commands + * + * Like `openReferenceToSide` and `openReference`, they depend on `IListService`. + * We treat all commands which don't match any other category of global commands as global editor commands + * and execute them using the instantiation service of the current editor. + */ + protected registerMonacoCommands(): void { + const editorRegistry = monaco.editorExtensions.EditorExtensionsRegistry; + const editorActions = new Map(editorRegistry.getEditorActions().map(({ id, label }) => [id, label])); + + const { codeEditorService, textModelService, contextKeyService } = this; + const [, globalInstantiationService] = monaco.services.StaticServices.init({ codeEditorService, textModelService, contextKeyService }); const monacoCommands = monaco.commands.CommandsRegistry.getCommands(); - for (const command of monacoCommands.keys()) { - if (command.startsWith('_execute')) { - this.commandRegistry.registerCommand( - { - id: command - }, - { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - execute: (...args: any) => instantiationService.invokeFunction( - monacoCommands.get(command)!.handler, - ...args - ) + for (const id of monacoCommands.keys()) { + if (MonacoCommands.EXCLUDE_ACTIONS.has(id)) { + continue; + } + const handler: CommandHandler = { + execute: (...args) => { + const editor = codeEditorService.getFocusedCodeEditor() || codeEditorService.getActiveCodeEditor(); + if (editorActions.has(id)) { + const action = editor && editor.getAction(id); + if (!action) { + return; + } + return action.run(); } - ); + const editorCommand = !!editorRegistry.getEditorCommand(id) || + !(id.startsWith('_execute') || id === 'setContext' || MonacoCommands.COMMON_ACTIONS.has(id)); + const instantiationService = editorCommand ? editor && editor['_instantiationService'] : globalInstantiationService; + if (!instantiationService) { + return; + } + return instantiationService.invokeFunction( + monacoCommands.get(id)!.handler, + ...args + ); + }, + isEnabled: () => { + const editor = codeEditorService.getFocusedCodeEditor() || codeEditorService.getActiveCodeEditor(); + if (editorActions.has(id)) { + const action = editor && editor.getAction(id); + return !!action && action.isSupported(); + } + if (!!editorRegistry.getEditorCommand(id)) { + return !!editor; + } + return true; + } + }; + const label = editorActions.get(id); + this.commandRegistry.registerCommand({ id, label }, handler); + const coreCommand = MonacoCommands.COMMON_ACTIONS.get(id); + if (coreCommand) { + this.commandRegistry.registerHandler(coreCommand, handler); } } } - protected registerCommonCommandHandlers(): void { - // eslint-disable-next-line guard-for-in - for (const action in MonacoCommands.COMMON_ACTIONS) { - const command = MonacoCommands.COMMON_ACTIONS[action]; - const handler = this.newCommonActionHandler(action); - this.monacoCommandRegistry.registerHandler(command, handler); - } - } - protected newCommonActionHandler(action: string): MonacoEditorCommandHandler { - return this.isCommonKeyboardAction(action) ? this.newKeyboardHandler(action) : this.newActionHandler(action); - } - protected isCommonKeyboardAction(action: string): boolean { - return MonacoCommands.COMMON_KEYBOARD_ACTIONS.has(action); - } - protected registerEditorCommandHandlers(): void { this.monacoCommandRegistry.registerHandler(EditorCommands.SHOW_REFERENCES.id, this.newShowReferenceHandler()); this.monacoCommandRegistry.registerHandler(EditorCommands.CONFIG_INDENTATION.id, this.newConfigIndentationHandler()); @@ -255,42 +294,4 @@ export class MonacoEditorCommandHandlers implements CommandContribution { } } - protected registerMonacoActionCommands(): void { - for (const action of MonacoCommands.ACTIONS.values()) { - const handler = this.newMonacoActionHandler(action); - this.monacoCommandRegistry.registerCommand(action, handler); - } - } - protected newMonacoActionHandler(action: MonacoCommand): MonacoEditorCommandHandler { - const delegate = action.delegate; - return delegate ? this.newDelegateHandler(delegate) : this.newActionHandler(action.id); - } - - protected newKeyboardHandler(action: string): MonacoEditorCommandHandler { - return { - execute: (editor, ...args) => { - const modelData = editor.getControl()._modelData; - if (modelData) { - modelData.cursor.trigger('keyboard', action, args); - } - } - }; - } - protected newCommandHandler(action: string): MonacoEditorCommandHandler { - return { - execute: (editor, ...args) => editor.commandService.executeCommand(action, ...args) - }; - } - protected newActionHandler(action: string): MonacoEditorCommandHandler { - return { - execute: editor => editor.runAction(action), - isEnabled: editor => editor.isActionSupported(action) - }; - } - protected newDelegateHandler(action: string): MonacoEditorCommandHandler { - return { - execute: (editor, ...args) => (editor.commandService as MonacoCommandService).executeMonacoCommand(action, ...args) - }; - } - } diff --git a/packages/monaco/src/browser/monaco-editor-provider.ts b/packages/monaco/src/browser/monaco-editor-provider.ts index cc5bb6ab88b11..b53e3d7289b5b 100644 --- a/packages/monaco/src/browser/monaco-editor-provider.ts +++ b/packages/monaco/src/browser/monaco-editor-provider.ts @@ -105,6 +105,10 @@ export class MonacoEditorProvider { protected async getModel(uri: URI, toDispose: DisposableCollection): Promise { const reference = await this.textModelService.createModelReference(uri); + // if document is invalid makes sure that all events from underlying resource are processed before throwing invalid model + if (!reference.object.valid) { + await reference.object.sync(); + } if (!reference.object.valid) { reference.dispose(); throw Object.assign(new Error(`'${uri.toString()}' is invalid`), { code: 'MODEL_IS_INVALID' }); diff --git a/packages/monaco/src/browser/monaco-editor-service.ts b/packages/monaco/src/browser/monaco-editor-service.ts index e42bcdfd4116d..12c7b5c351f58 100644 --- a/packages/monaco/src/browser/monaco-editor-service.ts +++ b/packages/monaco/src/browser/monaco-editor-service.ts @@ -51,8 +51,11 @@ export class MonacoEditorService extends monaco.services.CodeEditorServiceImpl { super(monaco.services.StaticServices.standaloneThemeService.get()); } - getActiveCodeEditor(): ICodeEditor | undefined { - const editor = MonacoEditor.getActive(this.editors); + /** + * Monaco active editor is either focused or last focused editor. + */ + getActiveCodeEditor(): monaco.editor.IStandaloneCodeEditor | undefined { + const editor = MonacoEditor.getCurrent(this.editors); return editor && editor.getControl(); } diff --git a/packages/monaco/src/browser/monaco-keybinding.ts b/packages/monaco/src/browser/monaco-keybinding.ts index b66dd53803e75..0c4a444ed7f50 100644 --- a/packages/monaco/src/browser/monaco-keybinding.ts +++ b/packages/monaco/src/browser/monaco-keybinding.ts @@ -16,7 +16,6 @@ import { injectable, inject } from 'inversify'; import { KeybindingContribution, KeybindingRegistry } from '@theia/core/lib/browser'; -import { EditorKeybindingContexts } from '@theia/editor/lib/browser'; import { MonacoCommands } from './monaco-command'; import { MonacoCommandRegistry } from './monaco-command-registry'; import { environment } from '@theia/core'; @@ -46,15 +45,5 @@ export class MonacoKeybindingContribution implements KeybindingContribution { registry.registerKeybinding({ command, keybinding, when }); } } - - // `Select All` is not an editor action just like everything else. - const selectAllCommand = this.commands.validate(MonacoCommands.SELECTION_SELECT_ALL); - if (selectAllCommand) { - registry.registerKeybinding({ - command: selectAllCommand, - keybinding: 'ctrlcmd+a', - context: EditorKeybindingContexts.editorTextFocus - }); - } } } diff --git a/packages/monaco/src/browser/monaco-loader.ts b/packages/monaco/src/browser/monaco-loader.ts index 1be82b0f75944..6238a5c87c879 100644 --- a/packages/monaco/src/browser/monaco-loader.ts +++ b/packages/monaco/src/browser/monaco-loader.ts @@ -70,6 +70,7 @@ export function loadMonaco(vsRequire: any): Promise { 'vs/editor/contrib/snippet/snippetParser', 'vs/platform/configuration/common/configuration', 'vs/platform/configuration/common/configurationModels', + 'vs/editor/common/services/resolverService', 'vs/editor/browser/services/codeEditorService', 'vs/editor/browser/services/codeEditorServiceImpl', 'vs/platform/markers/common/markerService', @@ -83,6 +84,7 @@ export function loadMonaco(vsRequire: any): Promise { filters: any, styler: any, colorRegistry: any, color: any, platform: any, modes: any, suggest: any, snippetParser: any, configuration: any, configurationModels: any, + resolverService: any, codeEditorService: any, codeEditorServiceImpl: any, markerService: any, contextKey: any, contextKeyService: any, @@ -92,7 +94,7 @@ export function loadMonaco(vsRequire: any): Promise { global.monaco.actions = actions; global.monaco.keybindings = Object.assign({}, keybindingsRegistry, keybindingResolver, resolvedKeybinding, keybindingLabels, keyCodes); global.monaco.services = Object.assign({}, simpleServices, standaloneServices, standaloneLanguages, configuration, configurationModels, - codeEditorService, codeEditorServiceImpl, markerService); + resolverService, codeEditorService, codeEditorServiceImpl, markerService); global.monaco.quickOpen = Object.assign({}, quickOpenWidget, quickOpenModel); global.monaco.filters = filters; global.monaco.theme = styler; diff --git a/packages/monaco/src/typings/monaco/index.d.ts b/packages/monaco/src/typings/monaco/index.d.ts index 420106caf6e49..478e27d693dc8 100644 --- a/packages/monaco/src/typings/monaco/index.d.ts +++ b/packages/monaco/src/typings/monaco/index.d.ts @@ -18,6 +18,7 @@ /// declare module monaco.instantiation { + // https://github.com/theia-ide/vscode/blob/standalone/0.19.x/src/vs/platform/instantiation/common/instantiation.ts#L86 export interface IInstantiationService { invokeFunction: (fn: any, ...args: any) => any } @@ -25,6 +26,10 @@ declare module monaco.instantiation { declare module monaco.editor { + export interface ICodeEditor { + protected readonly _instantiationService: monaco.instantiation.IInstantiationService; + } + export interface IBulkEditResult { ariaSummary: string; } @@ -532,6 +537,9 @@ declare module monaco.services { tokenize(line: string, state: monaco.languages.IState, offsetDelta: number): any; } + // https://github.com/theia-ide/vscode/blob/standalone/0.19.x/src/vs/editor/common/services/resolverService.ts#L12 + export const ITextModelService: any; + // https://github.com/theia-ide/vscode/blob/standalone/0.19.x/src/vs/editor/browser/services/codeEditorService.ts#L13 export const ICodeEditorService: any; @@ -570,6 +578,11 @@ declare module monaco.services { registerDecorationType: monaco.editor.ICodeEditorService['registerDecorationType']; removeDecorationType: monaco.editor.ICodeEditorService['removeDecorationType']; resolveDecorationOptions: monaco.editor.ICodeEditorService['resolveDecorationOptions']; + /** + * It respects inline and emebedded editors in comparison to `getActiveCodeEditor` + * which only respect standalone and diff modified editors. + */ + getFocusedCodeEditor(): monaco.editor.ICodeEditor | undefined; } // https://github.com/theia-ide/vscode/blob/standalone/0.19.x/src/vs/editor/standalone/browser/simpleServices.ts#L245 @@ -1054,16 +1067,20 @@ declare module monaco.filters { declare module monaco.editorExtensions { + // https://github.com/theia-ide/vscode/blob/standalone/0.19.x/src/vs/editor/browser/editorExtensions.ts#L141 + export abstract class EditorCommand { + } + // https://github.com/theia-ide/vscode/blob/standalone/0.19.x/src/vs/editor/browser/editorExtensions.ts#L205 - export interface EditorAction { + export abstract class EditorAction extends EditorCommand { id: string; label: string; - alias: string; } export module EditorExtensionsRegistry { // https://github.com/theia-ide/vscode/blob/standalone/0.19.x/src/vs/editor/browser/editorExtensions.ts#L341 export function getEditorActions(): EditorAction[]; + export function getEditorCommand(commandId: string): EditorCommand | undefined; } } declare module monaco.modes { @@ -1224,6 +1241,10 @@ declare module monaco.contextKeyService { } declare module monaco.contextkey { + + // https://github.com/theia-ide/vscode/blob/standalone/0.19.x/src/vs/platform/contextkey/common/contextkey.ts#L819 + export const IContextKeyService: any; + // https://github.com/theia-ide/vscode/blob/standalone/0.19.x/src/vs/platform/contextkey/common/contextkey.ts#L29 export class ContextKeyExpr { keys(): string[];