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/common/command.ts b/packages/core/src/common/command.ts index 7c931371a2bae..c8ab0ac28109c 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..31c3d49ff46ff 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 { MonacoEditorService } from './monaco-editor-service'; import { MonacoCommandService } from './monaco-command-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,121 @@ export class MonacoEditorCommandHandlers implements CommandContribution { @inject(QuickOpenService) protected readonly quickOpenService: QuickOpenService; + @inject(MonacoEditorService) + protected readonly editorService: MonacoEditorService; + 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 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 = this.editorService.getActiveCodeEditor(); + if (editorActions.has(id)) { + const action = editor && editor.getAction(id); + if (!action) { + return; + } + return action.run(); } - ); + if (editorRegistry.getEditorCommand(id)) { + if (!editor) { + return; + } + return (editor._commandService as MonacoCommandService).executeMonacoCommand(id, ...args); + } + let instantiationService; + if (id.startsWith('_execute') || id === 'setContext' || MonacoCommands.COMMON_ACTIONS.has(id)) { + instantiationService = monaco.services.StaticServices.instantiationService.get(); + } else if (editor) { + instantiationService = editor._instantiationService; + } + if (!instantiationService) { + return; + } + return instantiationService.invokeFunction( + monacoCommands.get(id)!.handler, + ...args + ); + }, + isEnabled: () => { + const editor = this.editorService.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 +295,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..1a39eb73787e5 100644 --- a/packages/monaco/src/browser/monaco-editor-provider.ts +++ b/packages/monaco/src/browser/monaco-editor-provider.ts @@ -98,7 +98,12 @@ export class MonacoEditorProvider { } monaco.services.StaticServices.init = o => { const result = init(o); + /** + * See `MonacoEditorCommandHandlers.registerMonacoCommands` to learn why we install these global services. + */ result[0].set(monaco.services.ICodeEditorService, codeEditorService); + result[0].set(monaco.contextkey.IContextKeyService, contextKeyService); + result[0].set(monaco.services.ITextModelService, textModelService); return result; }; } @@ -121,12 +126,10 @@ export class MonacoEditorProvider { protected async doCreateEditor(factory: (override: IEditorOverrideServices, toDispose: DisposableCollection) => Promise): Promise { const commandService = this.commandServiceFactory(); const contextKeyService = this.contextKeyService.createScoped(); - const { codeEditorService, textModelService, contextMenuService } = this; + const { contextMenuService } = this; const IWorkspaceEditService = this.bulkEditService; const toDispose = new DisposableCollection(commandService); const editor = await factory({ - codeEditorService, - textModelService, contextMenuService, commandService, IWorkspaceEditService, 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..c79ff7ca2de83 100644 --- a/packages/monaco/src/typings/monaco/index.d.ts +++ b/packages/monaco/src/typings/monaco/index.d.ts @@ -532,6 +532,9 @@ declare module monaco.services { tokenize(line: string, state: monaco.languages.IState, offsetDelta: number): any; } + // https://github.com/theia-ide/vscode/blob/d24b5f70c69b3e75cd10c6b5247a071265ccdd38/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; @@ -1054,16 +1057,20 @@ declare module monaco.filters { declare module monaco.editorExtensions { + // https://github.com/theia-ide/vscode/blob/d24b5f70c69b3e75cd10c6b5247a071265ccdd38/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 +1231,10 @@ declare module monaco.contextKeyService { } declare module monaco.contextkey { + + // https://github.com/theia-ide/vscode/blob/d24b5f70c69b3e75cd10c6b5247a071265ccdd38/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[];