Skip to content

Commit

Permalink
GH-6428: Fixed Undo, Redo, and Select All
Browse files Browse the repository at this point in the history
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 <kittaakos@typefox.io>

rewrote it a bit.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>

aligned name.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>

s

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
  • Loading branch information
Akos Kitta committed Mar 14, 2020
1 parent 9457f80 commit 85d24fe
Show file tree
Hide file tree
Showing 2 changed files with 99 additions and 22 deletions.
82 changes: 74 additions & 8 deletions packages/monaco/src/browser/monaco-command-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<MonacoEditorOrNativeTextInputCommandHandler>): command is MonacoEditorOrNativeTextInputCommandHandler {
return !!command.domCommandId;
}

export function isEnabled(command: Partial<MonacoEditorOrNativeTextInputCommandHandler>): command is MonacoEditorOrNativeTextInputCommandHandler {
return !!command.domCommandId && !!isNativeTextInput();
}

/**
* same as `isEnabled`.
*/
export function isVisible(command: Partial<MonacoEditorOrNativeTextInputCommandHandler>): 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;
Expand All @@ -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 <MonacoEditorOrNativeTextInputCommandHandler>{
...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));
Expand All @@ -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);
}

}
39 changes: 25 additions & 14 deletions packages/monaco/src/browser/monaco-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -60,7 +63,12 @@ export namespace MonacoCommands {
export const GO_TO_DEFINITION = 'editor.action.revealDefinition';

export const ACTIONS = new Map<string, MonacoCommand>();
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',
Expand Down Expand Up @@ -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<MonacoEditorOrNativeTextInputCommandHandler> {
return this.isCommonKeyboardAction(action) ? this.newKeyboardHandler(action) : this.newActionHandler(action);
}
protected isCommonKeyboardAction(action: string): boolean {
Expand Down Expand Up @@ -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<MonacoEditorOrNativeTextInputCommandHandler> {
const delegate = action.delegate;
return delegate ? this.newDelegateHandler(delegate) : this.newActionHandler(action.id);
}
Expand All @@ -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),
Expand Down

0 comments on commit 85d24fe

Please sign in to comment.