From 85d24fe09e0172247352b4d63c5fe9448a075058 Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Fri, 13 Mar 2020 22:56:10 +0100 Subject: [PATCH] GH-6428: Fixed `Undo`, `Redo`, and `Select All` From now on, when executing the `Undo`, `Redo`, and `Select All` command handlers, we do not focus the `current` editor but follow the following execution order: - Executes on the `current` editor if it has text focus. - Otherwise, if the `document.activeElement` is either an `input` or a `textArea`, executes the browser's built-in command on it. - Otherwise, executes on the `current` editor after setting the focus on it. Closes: #6428 Closes: #2756 Signed-off-by: Akos Kitta rewrote it a bit. Signed-off-by: Akos Kitta aligned name. Signed-off-by: Akos Kitta s Signed-off-by: Akos Kitta --- .../src/browser/monaco-command-registry.ts | 82 +++++++++++++++++-- packages/monaco/src/browser/monaco-command.ts | 39 +++++---- 2 files changed, 99 insertions(+), 22 deletions(-) diff --git a/packages/monaco/src/browser/monaco-command-registry.ts b/packages/monaco/src/browser/monaco-command-registry.ts index 83b587a25e2ff..8f23c68a0aa81 100644 --- a/packages/monaco/src/browser/monaco-command-registry.ts +++ b/packages/monaco/src/browser/monaco-command-registry.ts @@ -26,15 +26,61 @@ export interface MonacoEditorCommandHandler { // eslint-disable-next-line @typescript-eslint/no-explicit-any isEnabled?(editor: MonacoEditor, ...args: any[]): boolean; } +/** + * A command handler that will: + * 1. execute on the focused `current` editor. + * 2. otherwise, if the `document.activeElement` is either an `input` or a `textArea`, executes the browser built-in command on it. + * 3. otherwise, invoke a command on the current editor after setting the focus on it. + */ +export interface MonacoEditorOrNativeTextInputCommandHandler extends MonacoEditorCommandHandler { + domCommandId: string; +} +export namespace MonacoEditorOrNativeTextInputCommand { + + export function is(command: Partial): command is MonacoEditorOrNativeTextInputCommandHandler { + return !!command.domCommandId; + } + + export function isEnabled(command: Partial): command is MonacoEditorOrNativeTextInputCommandHandler { + return !!command.domCommandId && !!isNativeTextInput(); + } + + /** + * same as `isEnabled`. + */ + export function isVisible(command: Partial): command is MonacoEditorOrNativeTextInputCommandHandler { + return isEnabled(command); + } + + export function execute({ domCommandId: id }: MonacoEditorOrNativeTextInputCommandHandler): void { + const { activeElement } = document; + if (isNativeTextInput(activeElement)) { + console.trace(`Executing DOM command '${id}' on 'activeElement': ${activeElement}`); + document.execCommand(id); + } else { + console.warn(`Failed to execute the DOM command '${id}'. Expected 'activeElement' to be an 'input' or a 'textArea'. Was: ${activeElement}`); + } + } + + /** + * `element` defaults to `document.activeElement`. + */ + function isNativeTextInput(element: Element | null = document.activeElement): element is HTMLInputElement | HTMLTextAreaElement { + return !!element && ['input', 'textarea'].indexOf(element.tagName.toLowerCase()) >= 0; + } + +} @injectable() export class MonacoCommandRegistry { @inject(MonacoEditorProvider) protected readonly monacoEditors: MonacoEditorProvider; - @inject(CommandRegistry) protected readonly commands: CommandRegistry; + @inject(CommandRegistry) + protected readonly commands: CommandRegistry; - @inject(SelectionService) protected readonly selectionService: SelectionService; + @inject(SelectionService) + protected readonly selectionService: SelectionService; validate(command: string): string | undefined { return this.commands.commandIds.indexOf(command) !== -1 ? command : undefined; @@ -48,20 +94,36 @@ export class MonacoCommandRegistry { } registerHandler(command: string, handler: MonacoEditorCommandHandler): void { - this.commands.registerHandler(command, this.newHandler(handler)); + const delegate = this.newHandler(handler) as MonacoEditorCommandHandler; + this.commands.registerHandler(command, delegate); } protected newHandler(monacoHandler: MonacoEditorCommandHandler): CommandHandler { - return { - execute: (...args) => this.execute(monacoHandler, ...args), - isEnabled: (...args) => this.isEnabled(monacoHandler, ...args), - isVisible: (...args) => this.isVisible(monacoHandler, ...args) + const handler: CommandHandler = { + execute: (...args: any) => this.execute(monacoHandler, ...args), // eslint-disable-line @typescript-eslint/no-explicit-any + isEnabled: (...args: any) => this.isEnabled(monacoHandler, ...args), // eslint-disable-line @typescript-eslint/no-explicit-any + isVisible: (...args: any) => this.isVisible(monacoHandler, ...args) // eslint-disable-line @typescript-eslint/no-explicit-any }; + if (MonacoEditorOrNativeTextInputCommand.is(monacoHandler)) { + const { domCommandId } = monacoHandler; + return { + ...handler, + domCommandId + }; + } + return handler; } // eslint-disable-next-line @typescript-eslint/no-explicit-any protected execute(monacoHandler: MonacoEditorCommandHandler, ...args: any[]): any { const editor = this.monacoEditors.current; + // Only if the monaco editor has the text focus; the cursor blinks inside the editor widget. + if (editor && editor.isFocused()) { + return Promise.resolve(monacoHandler.execute(editor, ...args)); + } + if (MonacoEditorOrNativeTextInputCommand.is(monacoHandler)) { + return Promise.resolve(MonacoEditorOrNativeTextInputCommand.execute(monacoHandler)); + } if (editor) { editor.focus(); return Promise.resolve(monacoHandler.execute(editor, ...args)); @@ -71,13 +133,17 @@ export class MonacoCommandRegistry { // eslint-disable-next-line @typescript-eslint/no-explicit-any protected isEnabled(monacoHandler: MonacoEditorCommandHandler, ...args: any[]): boolean { + if (MonacoEditorOrNativeTextInputCommand.isEnabled(monacoHandler)) { + return true; + } const editor = this.monacoEditors.current; return !!editor && (!monacoHandler.isEnabled || monacoHandler.isEnabled(editor, ...args)); } // eslint-disable-next-line @typescript-eslint/no-explicit-any protected isVisible(monacoHandler: MonacoEditorCommandHandler, ...args: any[]): boolean { - return TextEditorSelection.is(this.selectionService.selection); + return MonacoEditorOrNativeTextInputCommand.isVisible(monacoHandler) + || TextEditorSelection.is(this.selectionService.selection); } } diff --git a/packages/monaco/src/browser/monaco-command.ts b/packages/monaco/src/browser/monaco-command.ts index f9963f4de4bc7..8217b1d810ef8 100644 --- a/packages/monaco/src/browser/monaco-command.ts +++ b/packages/monaco/src/browser/monaco-command.ts @@ -23,21 +23,24 @@ import { QuickOpenService } from '@theia/core/lib/browser/quick-open/quick-open- 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 { MonacoCommandRegistry, MonacoEditorCommandHandler, MonacoEditorOrNativeTextInputCommandHandler } from './monaco-command-registry'; import MenuRegistry = monaco.actions.MenuRegistry; import { MonacoCommandService } from './monaco-command-service'; -export type MonacoCommand = Command & { delegate?: string }; +export interface MonacoCommand extends Command { + readonly delegate?: string; + readonly domCommandId?: 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 + [action: string]: string | MonacoCommand } = {}; - COMMON_ACTIONS[UNDO] = CommonCommands.UNDO.id; - COMMON_ACTIONS[REDO] = CommonCommands.REDO.id; + COMMON_ACTIONS[UNDO] = { ...CommonCommands.UNDO, domCommandId: UNDO }; + COMMON_ACTIONS[REDO] = { ...CommonCommands.REDO, domCommandId: REDO }; COMMON_ACTIONS['actions.find'] = CommonCommands.FIND.id; COMMON_ACTIONS['editor.action.startFindReplaceAction'] = CommonCommands.REPLACE.id; @@ -60,7 +63,12 @@ export namespace MonacoCommands { 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' }); + ACTIONS.set(SELECTION_SELECT_ALL, { + id: SELECTION_SELECT_ALL, + label: 'Select All', + delegate: 'editor.action.selectAll', + domCommandId: 'selectAll' + }); export const EXCLUDE_ACTIONS = new Set([ ...Object.keys(COMMON_ACTIONS), 'editor.action.quickCommand', @@ -138,10 +146,14 @@ export class MonacoEditorCommandHandlers implements CommandContribution { for (const action in MonacoCommands.COMMON_ACTIONS) { const command = MonacoCommands.COMMON_ACTIONS[action]; const handler = this.newCommonActionHandler(action); - this.monacoCommandRegistry.registerHandler(command, handler); + if (Command.is(command) && command.domCommandId) { + handler.domCommandId = command.domCommandId; + } + const commandId = Command.is(command) ? command.id : command; + this.monacoCommandRegistry.registerHandler(commandId, handler); } } - protected newCommonActionHandler(action: string): MonacoEditorCommandHandler { + protected newCommonActionHandler(action: string): MonacoEditorCommandHandler & Partial { return this.isCommonKeyboardAction(action) ? this.newKeyboardHandler(action) : this.newActionHandler(action); } protected isCommonKeyboardAction(action: string): boolean { @@ -269,10 +281,14 @@ export class MonacoEditorCommandHandlers implements CommandContribution { protected registerMonacoActionCommands(): void { for (const action of MonacoCommands.ACTIONS.values()) { const handler = this.newMonacoActionHandler(action); + if (action.domCommandId) { + const { domCommandId } = action; + handler.domCommandId = domCommandId; + } this.monacoCommandRegistry.registerCommand(action, handler); } } - protected newMonacoActionHandler(action: MonacoCommand): MonacoEditorCommandHandler { + protected newMonacoActionHandler(action: MonacoCommand): MonacoEditorCommandHandler & Partial { const delegate = action.delegate; return delegate ? this.newDelegateHandler(delegate) : this.newActionHandler(action.id); } @@ -282,11 +298,6 @@ export class MonacoEditorCommandHandlers implements CommandContribution { execute: (editor, ...args) => editor.getControl()._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),