diff --git a/src/base/common/keyboard.ts b/src/base/common/keyboard.ts index af616c712..d92afedcd 100644 --- a/src/base/common/keyboard.ts +++ b/src/base/common/keyboard.ts @@ -534,7 +534,9 @@ export class Shortcut { } else if (lowerPart === 'meta' || lowerPart === 'cmd') { shortcut.meta = true; } else { + // duplicate main key found, we consider it as invalid shortcut. if (shortcut.key !== KeyCode.None) { + console.warn(`Invalid shortcut string (${string})`); return Shortcut.None; } @@ -542,6 +544,7 @@ export class Shortcut { if (key !== KeyCode.None) { shortcut.key = key; } else { + console.warn(`Invalid shortcut string (${string})`); return Shortcut.None; } } diff --git a/src/code/browser/renderer.desktop.ts b/src/code/browser/renderer.desktop.ts index 998c72aed..738a3626b 100644 --- a/src/code/browser/renderer.desktop.ts +++ b/src/code/browser/renderer.desktop.ts @@ -71,6 +71,9 @@ import { INotificationService } from "src/workbench/services/notification/notifi import { IEncryptionService } from "src/platform/encryption/common/encryptionService"; import { BrowserAITextChannel } from "src/platform/ai/common/aiTextChannel"; import { IAITextService } from "src/platform/ai/common/aiText"; +import { IEditorService } from "src/workbench/services/editor/editor"; +import { EditorService } from "src/workbench/services/editor/editorService"; +import { rendererEditorCommandRegister } from "src/editor/contrib/command/command.register"; /** * @class This is the main entry of the renderer process. @@ -148,6 +151,10 @@ const renderer = new class extends class RendererInstance extends Disposable { const contextService = new ContextService(); instantiationService.store(IContextService, contextService); + // editor-service + const editorService = new EditorService(); + instantiationService.store(IEditorService, editorService); + // registrant-service const registrantService = instantiationService.createInstance(RegistrantService); instantiationService.store(IRegistrantService, registrantService); @@ -321,6 +328,7 @@ const renderer = new class extends class RendererInstance extends Disposable { [ rendererWorkbenchCommandRegister, rendererTitleBarFileCommandRegister, + rendererEditorCommandRegister, ] .forEach(register => register(provider)); } diff --git a/src/editor/common/editorContextKeys.ts b/src/editor/common/editorContextKeys.ts index 2e19932a1..244fc46d2 100644 --- a/src/editor/common/editorContextKeys.ts +++ b/src/editor/common/editorContextKeys.ts @@ -4,9 +4,13 @@ import { CreateContextKeyExpr } from "src/platform/context/common/contextKeyExpr export namespace EditorContextKeys { export const editorFocusedContext = CreateContextKeyExpr.Equal('isEditorFocused', true); - export const isEditorReadonly = CreateContextKeyExpr.Equal('isEditorReadonly', true); export const isEditorWritable = CreateContextKeyExpr.Equal('isEditorWritable', true); export const isEditorEditable = CreateContextKeyExpr.And(editorFocusedContext, isEditorWritable); + export const isEditorReadonly = CreateContextKeyExpr.Or( + CreateContextKeyExpr.Equal('isEditorReadonly', true), + isEditorWritable, + ); + export const isEditorNotEditable = CreateContextKeyExpr.And(editorFocusedContext, isEditorReadonly); export const richtextEditorMode = CreateContextKeyExpr.Equal('editorRenderMode', EditorType.Rich); export const plaintextEditorMode = CreateContextKeyExpr.Equal('editorRenderMode', EditorType.Plain); export const splitViewEditorMode = CreateContextKeyExpr.Equal('editorRenderMode', EditorType.Split); diff --git a/src/editor/contrib/builtInExtensionList.ts b/src/editor/contrib/builtInExtensionList.ts index 1d2830cd4..9c605eb57 100644 --- a/src/editor/contrib/builtInExtensionList.ts +++ b/src/editor/contrib/builtInExtensionList.ts @@ -1,6 +1,5 @@ import { Constructor } from "src/base/common/utilities/type"; import { EditorExtension } from "src/editor/common/editorExtension"; -import { EditorCommandExtension } from "src/editor/contrib/command/command"; import { EditorAutoSaveExtension } from "src/editor/contrib/autoSave"; import { EditorSnippetExtension } from "src/editor/contrib/snippet/snippet"; import { EditorDragAndDropExtension } from "src/editor/contrib/dragAndDrop/dragAndDrop"; @@ -11,7 +10,6 @@ import { EditorAskAIExtension } from "src/editor/contrib/askAI/askAI"; // import { EditorHistoryExtension } from "src/editor/contrib/history/history"; export const enum EditorExtensionIDs { - Command = 'editor-command-extension', AutoSave = 'editor-autosave-extension', Snippet = 'editor-snippet-extension', History = 'editor-history-extension', @@ -29,7 +27,6 @@ export function getBuiltInExtension(): { id: string, ctor: Constructor EditorCommandArguments): void { - registerListCommands(extension, logService, getArguments); - __registerToggleMarkCommands(extension, logService, getArguments); - __registerHeadingCommands(extension, logService, getArguments); - __registerBasicCommands(extension, getArguments); -} - -function getPlatformShortcut(ctrl: string, meta: string): string { - return IS_MAC ? meta : ctrl; -} - -/** - * @description Register Toggle Mark Commands. - * @note These commands need to be constructed after the editor and schema - * are initialized. - */ -function __registerToggleMarkCommands(extension: IEditorCommandExtension, logService: ILogService, getArguments: () => EditorCommandArguments): void { - const schema = extension.getEditorSchema().unwrap(); - const toggleMarkConfigs: [string, string, string][] = [ - [MarkEnum.Strong, 'Ctrl+B', 'Meta+B'], - [MarkEnum.Em, 'Ctrl+I', 'Meta+I'], - [MarkEnum.Codespan, 'Ctrl+`', 'Meta+`'], - ]; - for (const [markID, ctrl, meta] of toggleMarkConfigs) { - const toggleCmdID = `editor-toggle-mark-${markID}`; - const markType = schema.getMarkType(markID); - - if (!markType) { - logService.warn(extension.id, `Cannot register the editor command (${toggleCmdID}) because the mark type does not exists in the editor schema.`); - continue; - } - - extension.registerCommand( - EditorCommands.createToggleMarkCommand( - { - id: toggleCmdID, - when: whenEditorWritable, - shortcutOptions: { - commandArgs: getArguments, - shortcut: Shortcut.fromString(getPlatformShortcut(ctrl, meta)), - weight: ShortcutWeight.Editor, - when: whenEditorWritable, - } - }, - markType, - null, // attrs - { - removeWhenPresent: true, - enterInlineAtoms: true, - } - ) - ); - } -} - -/** - * @description Register Toggle Heading Commands. Ctrl+(1-6) will toggle the - * block into Heading block node. - * @note These commands need to be constructed after the editor and schema - * are initialized. - */ -function __registerHeadingCommands(extension: IEditorCommandExtension, logService: ILogService, getArguments: () => EditorCommandArguments): void { - const schema = extension.getEditorSchema().unwrap(); - const headingCmdID = 'editor-toggle-heading'; - - const nodeType = schema.getNodeType(TokenEnum.Heading); - if (!nodeType) { - logService.warn(extension.id, `Cannot register the editor command (${headingCmdID}) because the token type does not exists in the editor schema.`); - return; - } - - for (let level = 1; level <= 6; level++) { - const cmdID = `${headingCmdID}-${level}`; - extension.registerCommand( - EditorCommands.createSetBlockCommand( - { - id: cmdID, - when: whenEditorWritable, - shortcutOptions: { - commandArgs: getArguments, - shortcut: Shortcut.fromString(getPlatformShortcut(`Ctrl+${level}`, `Meta+${level}`)), - weight: ShortcutWeight.Editor, - when: whenEditorWritable, - } - }, - nodeType, - { level: level } - ) - ); - } -} - -function __registerBasicCommands(extension: IEditorCommandExtension, getArguments: () => EditorCommandArguments): void { - extension.registerCommand(buildEditorCommand( - { - id: 'editor-enter', - when: whenEditorWritable, - shortcutOptions: { - commandArgs: getArguments, - shortcut: Shortcut.fromString('Enter'), - weight: ShortcutWeight.Editor, - when: whenEditorWritable, - } - }, - [ - EditorCommands.InsertNewLineInCodeBlock, - EditorCommands.InsertEmptyParagraphAdjacentToBlock, - EditorCommands.LiftEmptyTextBlock, - EditorCommands.SplitBlockAtSelection, - ], - )); - - extension.registerCommand(buildEditorCommand( - { - id: 'editor-esc', - when: whenEditorReadonly, - shortcutOptions: { - commandArgs: getArguments, - shortcut: Shortcut.fromString('Escape'), - weight: ShortcutWeight.Editor, - when: whenEditorReadonly, - } - }, - [ - EditorCommands.Unselect - ], - )); - - extension.registerCommand(buildEditorCommand( - { - id: 'editor-backspace', - when: whenEditorWritable, - shortcutOptions: { - commandArgs: getArguments, - shortcut: Shortcut.fromString('Backspace'), - weight: ShortcutWeight.Editor, - when: whenEditorWritable, - } - }, - [ - EditorCommands.DeleteSelection, - EditorCommands.JoinBackward, - EditorCommands.SelectNodeBackward, - ], - )); - - - extension.registerCommand(buildEditorCommand( - { - id: 'editor-delete', - when: whenEditorWritable, - shortcutOptions: { - commandArgs: getArguments, - shortcut: [ - Shortcut.fromString('Delete'), - Shortcut.fromString(getPlatformShortcut('Ctrl+Delete', 'Meta+Delete')), - ], - weight: ShortcutWeight.Editor, - when: whenEditorWritable, - } - }, - [ - EditorCommands.DeleteSelection, - EditorCommands.JoinForward, - EditorCommands.SelectNodeForward, - ] - )); - - extension.registerCommand(buildEditorCommand( - { - id: 'editor-select-all', - when: whenEditorReadonly, - shortcutOptions: { - commandArgs: getArguments, - shortcut: Shortcut.fromString(getPlatformShortcut('Ctrl+A', 'Meta+A')), - weight: ShortcutWeight.Editor, - when: whenEditorReadonly, - } - }, - [ - EditorCommands.SelectAll - ] - )); - - // @fix Doesn't work with CM, guess bcz CM is focused but PM is not. - extension.registerCommand(buildEditorCommand( - { - id: 'editor-exit-code-block', - when: whenEditorReadonly, - shortcutOptions: { - commandArgs: getArguments, - shortcut: Shortcut.fromString(getPlatformShortcut('Ctrl+Enter', 'Meta+Enter')), - weight: ShortcutWeight.Editor, - when: whenEditorReadonly, - } - }, - [ - EditorCommands.ExitCodeBlock - ] - )); - - extension.registerCommand(buildEditorCommand( - { - id: 'editor-insert-hard-break', - when: whenEditorWritable, - shortcutOptions: { - commandArgs: getArguments, - shortcut: [ - Shortcut.fromString('Shift+Enter'), - Shortcut.fromString(getPlatformShortcut('Ctrl+Enter', 'Meta+Enter')), - ], - weight: ShortcutWeight.Editor, - when: whenEditorWritable, - } - }, - [ - EditorCommands.ExitCodeBlock, - EditorCommands.InsertHardBreak, - ] - )); - - extension.registerCommand(buildEditorCommand( - { - id: 'editor-save', - when: whenEditorWritable, - shortcutOptions: { - commandArgs: getArguments, - shortcut: Shortcut.fromString(getPlatformShortcut('Ctrl+S', 'Meta+S')), - weight: ShortcutWeight.Editor, - when: whenEditorWritable, - } - }, - [ - EditorCommands.FileSave, - ] - )); - - extension.registerCommand(buildEditorCommand( - { - id: 'editor-undo', - when: whenEditorWritable, - shortcutOptions: { - commandArgs: getArguments, - shortcut: Shortcut.fromString(getPlatformShortcut('Ctrl+Z', 'Meta+Z')), - weight: ShortcutWeight.Editor, - when: whenEditorWritable, - } - }, - [ - EditorCommands.FileUndo, - ] - )); - - extension.registerCommand(buildEditorCommand( - { - id: 'editor-redo', - when: whenEditorWritable, - shortcutOptions: { - commandArgs: getArguments, - shortcut: Shortcut.fromString(getPlatformShortcut('Ctrl+Shift+Z', 'Meta+Shift+Z')), - weight: ShortcutWeight.Editor, - when: whenEditorWritable, - } - }, - [ - EditorCommands.FileRedo, - ] - )); -} +import { EditorCommand } from "src/editor/contrib/command/editorCommand"; /** * @description Contains a list of commands specific for editor. Every basic @@ -309,9 +17,9 @@ function __registerBasicCommands(extension: IEditorCommandExtension, getArgument */ export namespace EditorCommands { - export class Unselect extends EditorCommandBase { + export class Unselect extends EditorCommand { - public run(provider: IServiceProvider, editor: IEditorWidget, state: ProseEditorState, dispatch?: (tr: ProseTransaction) => void, view?: ProseEditorView): boolean { + protected __run(provider: IServiceProvider, editor: IEditorWidget, state: ProseEditorState, dispatch?: (tr: ProseTransaction) => void, view?: ProseEditorView): boolean { const { $to } = state.selection; const tr = state.tr.setSelection(ProseTextSelection.create(state.doc, $to.pos)); dispatch?.(tr); @@ -320,9 +28,9 @@ export namespace EditorCommands { } } - export class SelectAll extends EditorCommandBase { + export class SelectAll extends EditorCommand { - public run(provider: IServiceProvider, editor: IEditorWidget, state: ProseEditorState, dispatch?: (tr: ProseTransaction) => void): boolean { + protected __run(provider: IServiceProvider, editor: IEditorWidget, state: ProseEditorState, dispatch?: (tr: ProseTransaction) => void): boolean { if (!dispatch) { return false; } @@ -391,9 +99,9 @@ export namespace EditorCommands { * If the selection meets this criteria, the function replaces the selection * with a newline character. */ - export class InsertNewLineInCodeBlock extends EditorCommandBase { + export class InsertNewLineInCodeBlock extends EditorCommand { - public run(provider: IServiceProvider, editor: IEditorWidget, state: ProseEditorState, dispatch?: (tr: ProseTransaction) => void): boolean { + protected __run(provider: IServiceProvider, editor: IEditorWidget, state: ProseEditorState, dispatch?: (tr: ProseTransaction) => void): boolean { const { $head, $anchor } = state.selection; if (!$head.parent.type.spec.code || !$head.sameParent($anchor)) { return false; @@ -412,9 +120,9 @@ export namespace EditorCommands { * function adds a paragraph before the block node if it is the first child * of its parent, otherwise, it adds a paragraph after the block node. */ - export class InsertEmptyParagraphAdjacentToBlock extends EditorCommandBase { + export class InsertEmptyParagraphAdjacentToBlock extends EditorCommand { - public run(provider: IServiceProvider, editor: IEditorWidget, state: ProseEditorState, dispatch?: (tr: ProseTransaction) => void): boolean { + protected __run(provider: IServiceProvider, editor: IEditorWidget, state: ProseEditorState, dispatch?: (tr: ProseTransaction) => void): boolean { const { $from, $to } = state.selection; // Check if the selection is not suitable for paragraph insertion. @@ -453,9 +161,9 @@ export namespace EditorCommands { * can be "lifted" (meaning moved out of its current context, like out of a * list or a quote), the function performs this action. */ - export class LiftEmptyTextBlock extends EditorCommandBase { + export class LiftEmptyTextBlock extends EditorCommand { - public run(provider: IServiceProvider, editor: IEditorWidget, state: ProseEditorState, dispatch?: (tr: ProseTransaction) => void): boolean { + protected __run(provider: IServiceProvider, editor: IEditorWidget, state: ProseEditorState, dispatch?: (tr: ProseTransaction) => void): boolean { if (!(state.selection instanceof ProseTextSelection)) { return false; } @@ -497,9 +205,9 @@ export namespace EditorCommands { * the selection is within a block, the function divides the block into two * at that point. */ - export class SplitBlockAtSelection extends EditorCommandBase { + export class SplitBlockAtSelection extends EditorCommand { - public run(provider: IServiceProvider, editor: IEditorWidget, state: ProseEditorState, dispatch?: (tr: ProseTransaction) => void): boolean { + protected __run(provider: IServiceProvider, editor: IEditorWidget, state: ProseEditorState, dispatch?: (tr: ProseTransaction) => void): boolean { const { $from, $to } = state.selection; if (state.selection instanceof ProseNodeSelection && state.selection.node.isBlock) { @@ -566,9 +274,9 @@ export namespace EditorCommands { /** * @description Delete the current selection. */ - export class DeleteSelection extends EditorCommandBase { + export class DeleteSelection extends EditorCommand { - public run(provider: IServiceProvider, editor: IEditorWidget, state: ProseEditorState, dispatch?: (tr: ProseTransaction) => void): boolean { + protected __run(provider: IServiceProvider, editor: IEditorWidget, state: ProseEditorState, dispatch?: (tr: ProseTransaction) => void): boolean { if (state.selection.empty) { return false; } @@ -588,9 +296,9 @@ export namespace EditorCommands { * the previous block. Will use the view for accurate (bidi-aware) start-of * -textblock detection if given. */ - export class JoinBackward extends EditorCommandBase { + export class JoinBackward extends EditorCommand { - public run(provider: IServiceProvider, editor: IEditorWidget, state: ProseEditorState, dispatch?: (tr: ProseTransaction) => void, view?: ProseEditorView): boolean { + protected __run(provider: IServiceProvider, editor: IEditorWidget, state: ProseEditorState, dispatch?: (tr: ProseTransaction) => void, view?: ProseEditorView): boolean { const $cursor = __atBlockStart(state, view); if (!$cursor) { return false; @@ -662,9 +370,9 @@ export namespace EditorCommands { * other block closer to this one in the tree structure. Will use the * view for accurate start-of-textblock detection if given. */ - export class JoinForward extends EditorCommandBase { + export class JoinForward extends EditorCommand { - public run(provider: IServiceProvider, editor: IEditorWidget, state: ProseEditorState, dispatch?: (tr: ProseTransaction) => void, view?: ProseEditorView): boolean { + protected __run(provider: IServiceProvider, editor: IEditorWidget, state: ProseEditorState, dispatch?: (tr: ProseTransaction) => void, view?: ProseEditorView): boolean { const $cursor = __atBlockEnd(state, view); if (!$cursor) { return false; @@ -721,9 +429,9 @@ export namespace EditorCommands { * or other deleting commands, as a fall-back behavior when the schema * doesn't allow deletion at the selected point. */ - export class SelectNodeBackward extends EditorCommandBase { + export class SelectNodeBackward extends EditorCommand { - public run(provider: IServiceProvider, editor: IEditorWidget, state: ProseEditorState, dispatch?: (tr: ProseTransaction) => void, view?: ProseEditorView): boolean { + protected __run(provider: IServiceProvider, editor: IEditorWidget, state: ProseEditorState, dispatch?: (tr: ProseTransaction) => void, view?: ProseEditorView): boolean { const $head = state.selection.$head; const ifEmpty = state.selection.empty; @@ -760,9 +468,9 @@ export namespace EditorCommands { * commands, to provide a fall-back behavior when the schema doesn't * allow deletion at the selected point. */ - export class SelectNodeForward extends EditorCommandBase { + export class SelectNodeForward extends EditorCommand { - public run(provider: IServiceProvider, editor: IEditorWidget, state: ProseEditorState, dispatch?: (tr: ProseTransaction) => void, view?: ProseEditorView): boolean { + protected __run(provider: IServiceProvider, editor: IEditorWidget, state: ProseEditorState, dispatch?: (tr: ProseTransaction) => void, view?: ProseEditorView): boolean { const { empty } = state.selection; if (!empty) { return false; @@ -809,9 +517,9 @@ export namespace EditorCommands { * insertion, it creates the new block at the position right after the * code block and moves the cursor into it. */ - export class ExitCodeBlock extends EditorCommandBase { + export class ExitCodeBlock extends EditorCommand { - public run(provider: IServiceProvider, editor: IEditorWidget, state: ProseEditorState, dispatch?: (tr: ProseTransaction) => void, view?: ProseEditorView): boolean { + protected __run(provider: IServiceProvider, editor: IEditorWidget, state: ProseEditorState, dispatch?: (tr: ProseTransaction) => void, view?: ProseEditorView): boolean { const { $head, $anchor } = state.selection; const isInCodeBlock = $head.parent.type.spec.code; const isSelectionCollapsed = $head.sameParent($anchor); @@ -861,7 +569,7 @@ export namespace EditorCommands { */ export function createToggleMarkCommand( schema: ICommandSchema, - markType: TType, + getMarkType: () => TType, attrs: ProseAttrs | null = null, options?: { /** @@ -890,8 +598,9 @@ export namespace EditorCommands { const removeWhenPresent = options?.removeWhenPresent ?? true; const enterAtoms = options?.enterInlineAtoms ?? true; - return new class extends EditorCommandBase { - public run(provider: IServiceProvider, editor: IEditorWidget, state: ProseEditorState, dispatch?: (tr: ProseTransaction) => void, view?: ProseEditorView): boolean { + return new class extends EditorCommand { + protected __run(provider: IServiceProvider, editor: IEditorWidget, state: ProseEditorState, dispatch?: (tr: ProseTransaction) => void, view?: ProseEditorView): boolean { + const markType = getMarkType(); const { empty, $cursor } = state.selection as ProseTextSelection; let ranges = state.selection.ranges; @@ -992,11 +701,12 @@ export namespace EditorCommands { */ export function createSetBlockCommand( schema: ICommandSchema, - nodeType: TType, + getNodeType: () => TType, attrs: ProseAttrs | null = null ): Command { - return new class extends EditorCommandBase { - public run(provider: IServiceProvider, editor: IEditorWidget, state: ProseEditorState, dispatch?: (tr: ProseTransaction) => void, view?: ProseEditorView): boolean { + return new class extends EditorCommand { + protected __run(provider: IServiceProvider, editor: IEditorWidget, state: ProseEditorState, dispatch?: (tr: ProseTransaction) => void, view?: ProseEditorView): boolean { + const nodeType = getNodeType(); let applicable = false; // Step 1: Check if the block type can be applied to any node in the selection ranges @@ -1050,9 +760,9 @@ export namespace EditorCommands { }(schema); } - export class InsertHardBreak extends EditorCommandBase { + export class InsertHardBreak extends EditorCommand { - public run(provider: IServiceProvider, editor: IEditorWidget, state: ProseEditorState, dispatch?: (tr: ProseTransaction) => void): boolean { + protected __run(provider: IServiceProvider, editor: IEditorWidget, state: ProseEditorState, dispatch?: (tr: ProseTransaction) => void): boolean { const br = (state.schema).getNodeType(TokenEnum.LineBreak); if (!br) { return false; @@ -1066,9 +776,9 @@ export namespace EditorCommands { } } - export class FileSave extends EditorCommandBase { + export class FileSave extends EditorCommand { - public run(provider: IServiceProvider, editor: IEditorWidget): boolean { + protected __run(provider: IServiceProvider, editor: IEditorWidget): boolean { editor.save() .match( () => {}, @@ -1084,16 +794,16 @@ export namespace EditorCommands { } } - export class FileUndo extends EditorCommandBase { - public run(provider: IServiceProvider, editor: IEditorWidget, state: ProseEditorState, dispatch?: (tr: ProseTransaction) => void, view?: ProseEditorView): boolean { + export class FileUndo extends EditorCommand { + protected __run(provider: IServiceProvider, editor: IEditorWidget, state: ProseEditorState, dispatch?: (tr: ProseTransaction) => void, view?: ProseEditorView): boolean { // const historyExtension = editor.getExtension(EditorExtensionIDs.History) as IEditorHistoryExtension; // return historyExtension['undo'](state, dispatch); return undo(state, dispatch, view); } } - export class FileRedo extends EditorCommandBase { - public run(provider: IServiceProvider, editor: IEditorWidget, state: ProseEditorState, dispatch?: (tr: ProseTransaction) => void, view?: ProseEditorView): boolean { + export class FileRedo extends EditorCommand { + protected __run(provider: IServiceProvider, editor: IEditorWidget, state: ProseEditorState, dispatch?: (tr: ProseTransaction) => void, view?: ProseEditorView): boolean { // const historyExtension = editor.getExtension(EditorExtensionIDs.History) as IEditorHistoryExtension; // return historyExtension['redo'](state, dispatch); return redo(state, dispatch, view); diff --git a/src/editor/contrib/command/command.register.ts b/src/editor/contrib/command/command.register.ts new file mode 100644 index 000000000..24e6ca6bd --- /dev/null +++ b/src/editor/contrib/command/command.register.ts @@ -0,0 +1,347 @@ +import { Shortcut } from "src/base/common/keyboard"; +import { IS_MAC } from "src/base/common/platform"; +import { IO } from "src/base/common/utilities/functional"; +import { panic } from "src/base/common/utilities/panic"; +import { EditorContextKeys } from "src/editor/common/editorContextKeys"; +import { MarkEnum, TokenEnum } from "src/editor/common/markdown"; +import { ProseMarkType, ProseNodeType } from "src/editor/common/proseMirror"; +import { EditorCommands } from "src/editor/contrib/command/command.contrib"; +import { EditorListCommands } from "src/editor/contrib/command/listCommand.contrib"; +import { buildEditorCommand } from "src/editor/contrib/command/editorCommand"; +import { ICommandRegistrant } from "src/platform/command/common/commandRegistrant"; +import { createRegister, RegistrantType } from "src/platform/registrant/common/registrant"; +import { IEditorService } from "src/workbench/services/editor/editor"; +import { ShortcutWeight } from "src/workbench/services/shortcut/shortcutRegistrant"; + +export const rendererEditorCommandRegister = createRegister( + RegistrantType.Command, + 'rendererWorkbench', + (registrant, provider) => { + const editorService = provider.getOrCreateService(IEditorService); + __registerListCommands(registrant, editorService); + __registerToggleMarkCommands(registrant, editorService); + __registerHeadingCommands(registrant, editorService); + __registerBasicCommands(registrant, editorService); + } +); + +function getPlatformShortcut(ctrl: string, meta: string): string { + return IS_MAC ? meta : ctrl; +} + +/** + * @description Register Toggle Mark Commands. + * @note These commands need to be constructed after the editor and schema + * are initialized. + */ +function __registerToggleMarkCommands(registrant: ICommandRegistrant, editorService: IEditorService): void { + const toggleMarkConfigs: [string, string, string][] = [ + [MarkEnum.Strong, 'Ctrl+B', 'Meta+B'], + [MarkEnum.Em, 'Ctrl+I', 'Meta+I'], + [MarkEnum.Codespan, 'Ctrl+`', 'Meta+`'], + ]; + for (const [markID, ctrl, meta] of toggleMarkConfigs) { + const toggleCmdID = `editor-toggle-mark-${markID}`; + registrant.registerCommand( + EditorCommands.createToggleMarkCommand( + { + id: toggleCmdID, + when: EditorContextKeys.isEditorEditable, + shortcutOptions: { + commandArgs: [], + shortcut: Shortcut.fromString(getPlatformShortcut(ctrl, meta)), + weight: ShortcutWeight.Editor, + when: EditorContextKeys.isEditorEditable, + } + }, + getSchemaTypeBasedOnCurrEditor(editorService, 'mark', markID), + null, // attrs + { + removeWhenPresent: true, + enterInlineAtoms: true, + } + ) + ); + } +} + +/** + * @description Register Toggle Heading Commands. Ctrl+(1-6) will toggle the + * block into Heading block node. + * @note These commands need to be constructed after the editor and schema + * are initialized. + */ +function __registerHeadingCommands(registrant: ICommandRegistrant, editorService: IEditorService): void { + const headingCmdID = 'editor-toggle-heading'; + + for (let level = 1; level <= 6; level++) { + const cmdID = `${headingCmdID}-${level}`; + registrant.registerCommand( + EditorCommands.createSetBlockCommand( + { + id: cmdID, + when: EditorContextKeys.isEditorEditable, + shortcutOptions: { + commandArgs: [], + shortcut: Shortcut.fromString(getPlatformShortcut(`Ctrl+${level}`, `Meta+${level}`)), + weight: ShortcutWeight.Editor, + when: EditorContextKeys.isEditorEditable, + } + }, + getSchemaTypeBasedOnCurrEditor(editorService, 'node', TokenEnum.Heading), + { level: level } + ) + ); + } +} + +function __registerBasicCommands(registrant: ICommandRegistrant, editorService: IEditorService): void { + registrant.registerCommand(buildEditorCommand( + { + id: 'editor-enter', + when: EditorContextKeys.isEditorEditable, + shortcutOptions: { + commandArgs: [], + shortcut: Shortcut.fromString('Enter'), + weight: ShortcutWeight.Editor, + when: EditorContextKeys.isEditorEditable, + } + }, + [ + EditorCommands.InsertNewLineInCodeBlock, + EditorCommands.InsertEmptyParagraphAdjacentToBlock, + EditorCommands.LiftEmptyTextBlock, + EditorCommands.SplitBlockAtSelection, + ], + )); + + registrant.registerCommand(buildEditorCommand( + { + id: 'editor-esc', + when: EditorContextKeys.isEditorNotEditable, + shortcutOptions: { + commandArgs: [], + shortcut: Shortcut.fromString('Escape'), + weight: ShortcutWeight.Editor, + when: EditorContextKeys.isEditorNotEditable, + } + }, + [ + EditorCommands.Unselect + ], + )); + + registrant.registerCommand(buildEditorCommand( + { + id: 'editor-backspace', + when: EditorContextKeys.isEditorEditable, + shortcutOptions: { + commandArgs: [], + shortcut: Shortcut.fromString('Backspace'), + weight: ShortcutWeight.Editor, + when: EditorContextKeys.isEditorEditable, + } + }, + [ + EditorCommands.DeleteSelection, + EditorCommands.JoinBackward, + EditorCommands.SelectNodeBackward, + ], + )); + + + registrant.registerCommand(buildEditorCommand( + { + id: 'editor-delete', + when: EditorContextKeys.isEditorEditable, + shortcutOptions: { + commandArgs: [], + shortcut: [ + Shortcut.fromString('Delete'), + Shortcut.fromString(getPlatformShortcut('Ctrl+Delete', 'Meta+Delete')), + ], + weight: ShortcutWeight.Editor, + when: EditorContextKeys.isEditorEditable, + } + }, + [ + EditorCommands.DeleteSelection, + EditorCommands.JoinForward, + EditorCommands.SelectNodeForward, + ] + )); + + registrant.registerCommand(buildEditorCommand( + { + id: 'editor-select-all', + when: EditorContextKeys.isEditorNotEditable, + shortcutOptions: { + commandArgs: [], + shortcut: Shortcut.fromString(getPlatformShortcut('Ctrl+A', 'Meta+A')), + weight: ShortcutWeight.Editor, + when: EditorContextKeys.isEditorNotEditable, + } + }, + [ + EditorCommands.SelectAll + ] + )); + + // @fix Doesn't work with CM, guess bcz CM is focused but PM is not. + registrant.registerCommand(buildEditorCommand( + { + id: 'editor-exit-code-block', + when: EditorContextKeys.isEditorNotEditable, + shortcutOptions: { + commandArgs: [], + shortcut: Shortcut.fromString(getPlatformShortcut('Ctrl+Enter', 'Meta+Enter')), + weight: ShortcutWeight.Editor, + when: EditorContextKeys.isEditorNotEditable, + } + }, + [ + EditorCommands.ExitCodeBlock + ] + )); + + registrant.registerCommand(buildEditorCommand( + { + id: 'editor-insert-hard-break', + when: EditorContextKeys.isEditorEditable, + shortcutOptions: { + commandArgs: [], + shortcut: [ + Shortcut.fromString('Shift+Enter'), + Shortcut.fromString(getPlatformShortcut('Ctrl+Enter', 'Meta+Enter')), + ], + weight: ShortcutWeight.Editor, + when: EditorContextKeys.isEditorEditable, + } + }, + [ + EditorCommands.ExitCodeBlock, + EditorCommands.InsertHardBreak, + ] + )); + + registrant.registerCommand(buildEditorCommand( + { + id: 'editor-save', + when: EditorContextKeys.isEditorEditable, + shortcutOptions: { + commandArgs: [], + shortcut: Shortcut.fromString(getPlatformShortcut('Ctrl+S', 'Meta+S')), + weight: ShortcutWeight.Editor, + when: EditorContextKeys.isEditorEditable, + } + }, + [ + EditorCommands.FileSave, + ] + )); + + registrant.registerCommand(buildEditorCommand( + { + id: 'editor-undo', + when: EditorContextKeys.isEditorEditable, + shortcutOptions: { + commandArgs: [], + shortcut: Shortcut.fromString(getPlatformShortcut('Ctrl+Z', 'Meta+Z')), + weight: ShortcutWeight.Editor, + when: EditorContextKeys.isEditorEditable, + } + }, + [ + EditorCommands.FileUndo, + ] + )); + + registrant.registerCommand(buildEditorCommand( + { + id: 'editor-redo', + when: EditorContextKeys.isEditorEditable, + shortcutOptions: { + commandArgs: [], + shortcut: Shortcut.fromString(getPlatformShortcut('Ctrl+Shift+Z', 'Meta+Shift+Z')), + weight: ShortcutWeight.Editor, + when: EditorContextKeys.isEditorEditable, + } + }, + [ + EditorCommands.FileRedo, + ] + )); +} + +function __registerListCommands(registrant: ICommandRegistrant, editorService: IEditorService): void { + const getCurrListItemType = getSchemaTypeBasedOnCurrEditor(editorService, 'node', TokenEnum.ListItem); + + + registrant.registerCommand( + EditorListCommands.splitListItem( + { + id: 'editor-split-list-item', + when: EditorContextKeys.isEditorEditable, + shortcutOptions: { + when: EditorContextKeys.isEditorEditable, + weight: ShortcutWeight.Editor, + shortcut: Shortcut.fromString('Enter'), + commandArgs: [], + } + }, + getCurrListItemType, + undefined, + ), + ); + + registrant.registerCommand( + EditorListCommands.sinkListItem( + { + id: 'editor-sink-list-item', + when: EditorContextKeys.isEditorEditable, + shortcutOptions: { + when: EditorContextKeys.isEditorEditable, + weight: ShortcutWeight.Editor, + shortcut: Shortcut.fromString('Tab'), + commandArgs: [], + } + }, + getCurrListItemType, + ) + ); + + registrant.registerCommand( + EditorListCommands.liftListItem( + { + id: 'editor-lift-list-item', + when: EditorContextKeys.isEditorEditable, + shortcutOptions: { + when: EditorContextKeys.isEditorEditable, + weight: ShortcutWeight.Editor, + shortcut: Shortcut.fromString('Shift+Tab'), + commandArgs: [], + } + }, + getCurrListItemType, + ) + ); +} + +/** + * Since {@link EditorCommand} is executed based the current editor context. So + * the schema must also be determined dynamically. + */ +function getSchemaTypeBasedOnCurrEditor(editorService: IEditorService, type: TType, id: string): IO> { + return function () { + const currEditor = editorService.getFocusedEditor(); + if (!currEditor) { + panic(`Cannot execute the editor command (${id}) because the ${type} type does not exists in the current editor schema.`); + } + const view = currEditor.view.editor.internalView; + return >(type === 'mark' + ? view.state.schema.marks[id]! + : view.state.schema.nodes[id]!); + }; +} + +type GetNodeType = TType extends 'mark' ? ProseMarkType : ProseNodeType; \ No newline at end of file diff --git a/src/editor/contrib/command/command.ts b/src/editor/contrib/command/command.ts deleted file mode 100644 index dd0e3170c..000000000 --- a/src/editor/contrib/command/command.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { EditorState } from "prosemirror-state"; -import { EditorView } from "prosemirror-view"; -import { trySafe } from "src/base/common/error"; -import { Shortcut } from "src/base/common/keyboard"; -import { ILogService } from "src/base/common/logger"; -import { EditorExtensionIDs } from "src/editor/contrib/builtInExtensionList"; -import { EditorExtension, IEditorExtension } from "src/editor/common/editorExtension"; -import { IEditorWidget } from "src/editor/editorWidget"; -import { registerBasicEditorCommands } from "src/editor/contrib/command/command.contrib"; -import { Command } from "src/platform/command/common/command"; -import { ICommandService } from "src/platform/command/common/commandService"; -import { RegistrantType } from "src/platform/registrant/common/registrant"; -import { IRegistrantService } from "src/platform/registrant/common/registrantService"; -import { Priority } from "src/base/common/event"; - -/** - * An interface only for {@link EditorCommandExtension}. - */ -export interface IEditorCommandExtension extends IEditorExtension { - readonly id: EditorExtensionIDs.Command; - registerCommand(command: Command): void; -} - -/** - * @class Extension for handling editor commands with associated keyboard - * shortcuts. This class binds commands to specific shortcuts and registers - * these commands within the {@link CommandService}. - * - * @deprecated // FIX - */ -export class EditorCommandExtension extends EditorExtension implements IEditorCommandExtension { - - // [fields] - - public readonly id = EditorExtensionIDs.Command; - - /** Mapping from {@link Shortcut}-hashed code to command ID. */ - private readonly _commandKeybinding = new Map(); - - /** A set that contains all the editor commands' IDs */ - private readonly _commandSet = new Set(); - - // [constructor] - - constructor( - editorWidget: IEditorWidget, - @IRegistrantService private readonly registrantService: IRegistrantService, - @ICommandService commandService: ICommandService, - @ILogService private readonly logService: ILogService, - ) { - super(editorWidget); - - /** - * Keydown: when key is pressing in the editor: - * 1. we look up for any registered command (from the map), - * 2. if found any, we execute that command from the standard command - * system: {@link CommandService}. - * - * @note Registered with {@link Priority.Low}. Make other extensions has - * possibility to handle the keydown event first. - */ - // this.__register(this.onKeydown(event => { - // const keyEvent = event.event; - // const shortcut = new Shortcut(keyEvent.ctrl, keyEvent.shift, keyEvent.alt, keyEvent.meta, keyEvent.key); - // const commandID = this._commandKeybinding.get(shortcut.toHashcode()); - // if (!commandID) { - // return; - // } - - // /** - // * Whenever a command is executed, we need to invoke `preventDefault` - // * to tell prosemirror to prevent default behavior of the browser. - // * - // * @see https://discuss.prosemirror.net/t/question-allselection-weird-behaviours-when-the-document-contains-a-non-text-node-at-the-end/7749/3 - // */ - // trySafe( - // () => commandService.executeCommand(commandID, editorWidget, event.view.state, event.view.dispatch, event.view), - // { - // onError: () => false, - // onThen: (anyExecuted) => { - // if (anyExecuted) { - // event.preventDefault(); - // } - // } - // } - // ); - // }, undefined, Priority.Low)); - } - - // [protected override methods] - - protected override onViewStateInit(state: EditorState): void { - - /** - * Binds predefined commands to their respective shortcuts. - */ - - // FIX: - registerBasicEditorCommands(this, this.logService, () => { - const view = this._editorWidget.view.editor.internalView; - return [this._editorWidget, view.state, view.dispatch, view]; - }); - } - - protected override onViewDestroy(view: EditorView): void { - const registrant = this.registrantService.getRegistrant(RegistrantType.Command); - - // unregister all the editor commands - for (const registeredID of this._commandSet.values()) { - registrant.unregisterCommand(registeredID); - } - - // cache cleanup - this._commandKeybinding.clear(); - this._commandSet.clear(); - } - - // [public methods] - - public registerCommand(command: Command): void { - this._commandSet.add(command.id); - - /** - * Register the command to the standard {@link CommandService}. - */ - const registrant = this.registrantService.getRegistrant(RegistrantType.Command); - registrant.registerCommand(command); - } -} diff --git a/src/editor/contrib/command/editorCommand.ts b/src/editor/contrib/command/editorCommand.ts index f5e45e247..55182067c 100644 --- a/src/editor/contrib/command/editorCommand.ts +++ b/src/editor/contrib/command/editorCommand.ts @@ -1,18 +1,31 @@ -import { Shift } from "src/base/common/utilities/type"; import { ProseEditorState, ProseTransaction, ProseEditorView } from "src/editor/common/proseMirror"; import { IEditorWidget } from "src/editor/editorWidget"; import { buildChainCommand, Command, ICommandSchema } from "src/platform/command/common/command"; import { IServiceProvider } from "src/platform/instantiation/common/instantiation"; +import { IEditorService } from "src/workbench/services/editor/editor"; /** * @class A base class for every command in the {@link EditorCommandsBasic}. */ -export abstract class EditorCommandBase extends Command { - public abstract override run(provider: IServiceProvider, editor: IEditorWidget, state: ProseEditorState, dispatch?: (tr: ProseTransaction) => void, view?: ProseEditorView): boolean | Promise; +export abstract class EditorCommand extends Command { + protected abstract __run(provider: IServiceProvider, editor: IEditorWidget, state: ProseEditorState, dispatch?: (tr: ProseTransaction) => void, view?: ProseEditorView): boolean | Promise; + + /** + * Whenever {@link EditorCommand} executed, it will be executed based on + * the context of the current editor. If no editors is focused, will not be + * executed. + */ + public override run(provider: IServiceProvider): boolean | Promise { + const editorService = provider.getOrCreateService(IEditorService); + const currEditor = editorService.getFocusedEditor(); + if (!currEditor) { + return false; + } + const view = currEditor.view.editor.internalView; + return this.__run(provider, currEditor, view.state, view.dispatch.bind(view), view); + } } -export type EditorCommandArguments = Shift>; - export function buildEditorCommand(schema: ICommandSchema, ctors: (typeof Command)[]): Command { if (ctors.length === 1) { const command = ctors[0]!; diff --git a/src/editor/contrib/command/commandList.contrib.ts b/src/editor/contrib/command/listCommand.contrib.ts similarity index 75% rename from src/editor/contrib/command/commandList.contrib.ts rename to src/editor/contrib/command/listCommand.contrib.ts index eb2bd169e..b62b695c1 100644 --- a/src/editor/contrib/command/commandList.contrib.ts +++ b/src/editor/contrib/command/listCommand.contrib.ts @@ -1,80 +1,17 @@ import { canJoin, canSplit, liftTarget } from "prosemirror-transform"; -import { Shortcut } from "src/base/common/keyboard"; -import { ILogService } from "src/base/common/logger"; -import { EditorContextKeys } from "src/editor/common/editorContextKeys"; -import { TokenEnum } from "src/editor/common/markdown"; import { ProseEditorState, ProseTransaction, ProseEditorView, ProseNodeType, ProseFragment, ProseSlice, ProseReplaceAroundStep, ProseNodeRange, ProseSelection, ProseNodeSelection, ProseAttrs } from "src/editor/common/proseMirror"; -import { IEditorCommandExtension } from "src/editor/contrib/command/command"; -import { EditorCommandArguments, EditorCommandBase } from "src/editor/contrib/command/editorCommand"; +import { EditorCommand } from "src/editor/contrib/command/editorCommand"; import { IEditorWidget } from "src/editor/editorWidget"; import { Command, ICommandSchema } from "src/platform/command/common/command"; import { IServiceProvider } from "src/platform/instantiation/common/instantiation"; -import { ShortcutWeight } from "src/workbench/services/shortcut/shortcutRegistrant"; - -export function registerListCommands(extension: IEditorCommandExtension, logService: ILogService, getArguments: () => EditorCommandArguments): void { - const schema = extension.getEditorSchema().unwrap(); - - const listItemType = schema.getNodeType(TokenEnum.ListItem); - if (!listItemType) { - logService.warn(extension.id, `Cannot register the editor command (${TokenEnum.ListItem}) because the node type does not exists in the editor schema.`); - return; - } - - extension.registerCommand( - EditorListCommands.splitListItem( - { - id: 'editor-split-list-item', - when: EditorContextKeys.isEditorEditable, - shortcutOptions: { - when: EditorContextKeys.isEditorEditable, - weight: ShortcutWeight.Editor, - shortcut: Shortcut.fromString('Enter'), - commandArgs: getArguments, - } - }, - listItemType, - undefined, - ), - ); - - extension.registerCommand( - EditorListCommands.sinkListItem( - { - id: 'editor-sink-list-item', - when: EditorContextKeys.isEditorEditable, - shortcutOptions: { - when: EditorContextKeys.isEditorEditable, - weight: ShortcutWeight.Editor, - shortcut: Shortcut.fromString('Tab'), - commandArgs: getArguments, - } - }, - listItemType, - ) - ); - - extension.registerCommand( - EditorListCommands.liftListItem( - { - id: 'editor-lift-list-item', - when: EditorContextKeys.isEditorEditable, - shortcutOptions: { - when: EditorContextKeys.isEditorEditable, - weight: ShortcutWeight.Editor, - shortcut: Shortcut.fromString('Shift+Tab'), - commandArgs: getArguments, - } - }, - listItemType, - ) - ); -} export namespace EditorListCommands { - export function splitListItem(schema: ICommandSchema, listItemType: TType, itemAttrs?: ProseAttrs): Command { - return new class extends EditorCommandBase { - public override run(provider: IServiceProvider, editor: IEditorWidget, state: ProseEditorState, dispatch?: (tr: ProseTransaction) => void, view?: ProseEditorView): boolean | Promise { + export function splitListItem(schema: ICommandSchema, getListItemType: () => TType, itemAttrs?: ProseAttrs): Command { + return new class extends EditorCommand { + protected __run(provider: IServiceProvider, editor: IEditorWidget, state: ProseEditorState, dispatch?: (tr: ProseTransaction) => void, view?: ProseEditorView): boolean | Promise { + const listItemType = getListItemType(); + const { $from, $to, node } = state.selection as ProseNodeSelection; if ((node && node.isBlock) || $from.depth < 2 || !$from.sameParent($to)) { return false; @@ -153,9 +90,10 @@ export namespace EditorListCommands { }(schema); } - export function liftListItem(schema: ICommandSchema, listItemType: TType): Command { - return new class extends EditorCommandBase { - public override run(provider: IServiceProvider, editor: IEditorWidget, state: ProseEditorState, dispatch?: (tr: ProseTransaction) => void, view?: ProseEditorView): boolean | Promise { + export function liftListItem(schema: ICommandSchema, getListItemType: () => TType): Command { + return new class extends EditorCommand { + protected __run(provider: IServiceProvider, editor: IEditorWidget, state: ProseEditorState, dispatch?: (tr: ProseTransaction) => void, view?: ProseEditorView): boolean | Promise { + const listItemType = getListItemType(); const { $from, $to } = state.selection; const range = $from.blockRange($to, node => node.childCount > 0 && node.firstChild!.type === listItemType); if (!range) { @@ -250,9 +188,10 @@ export namespace EditorListCommands { }(schema); } - export function sinkListItem(schema: ICommandSchema, listItemType: TType): Command { - return new class extends EditorCommandBase { - public override run(provider: IServiceProvider, editor: IEditorWidget, state: ProseEditorState, dispatch?: (tr: ProseTransaction) => void, view?: ProseEditorView): boolean | Promise { + export function sinkListItem(schema: ICommandSchema, getListItemType: () => TType): Command { + return new class extends EditorCommand { + protected __run(provider: IServiceProvider, editor: IEditorWidget, state: ProseEditorState, dispatch?: (tr: ProseTransaction) => void, view?: ProseEditorView): boolean | Promise { + const listItemType = getListItemType(); const { $from, $to } = state.selection; const range = $from.blockRange($to, node => node.childCount > 0 && node.firstChild!.type === listItemType); if (!range) { diff --git a/src/editor/editorWidget.ts b/src/editor/editorWidget.ts index 6f8219eb4..b6d5b6e01 100644 --- a/src/editor/editorWidget.ts +++ b/src/editor/editorWidget.ts @@ -23,6 +23,7 @@ import { EditorViewModel } from "src/editor/viewModel/editorViewModel"; import { IEditorViewModel, IViewModelBuildData } from "src/editor/common/viewModel"; import { IEditorInputEmulator } from "src/editor/view/inputEmulator"; import { KeyCode } from "src/base/common/keyboard"; +import { IEditorHostService } from "src/workbench/services/editor/editor"; // region - [interface] @@ -83,6 +84,11 @@ export interface IEditorWidget extends */ readonly onDidRenderModeChange: Register; + /** + * Get a unique ID of the editor. + */ + getID(): string; + /** * @description Opens the source in the editor. * @param source The source in URI form. @@ -132,6 +138,9 @@ export class EditorWidget extends Disposable implements IEditorWidget { // region - [fields] + private static EDITOR_ID = 0; + private readonly _id: number = ++EditorWidget.EDITOR_ID; + /** * The HTML container of the entire editor. */ @@ -287,8 +296,10 @@ export class EditorWidget extends Disposable implements IEditorWidget { @IInstantiationService private readonly instantiationService: IInstantiationService, @ILifecycleService private readonly lifecycleService: IBrowserLifecycleService, @IConfigurationService private readonly configurationService: IConfigurationService, + @IEditorHostService private readonly editorService: IEditorHostService, ) { super(); + this.editorService.onCreate(); this._container = this.__register(new FastElement(container)); this._model = null; @@ -316,6 +327,8 @@ export class EditorWidget extends Disposable implements IEditorWidget { get readonly(): boolean { return !this._options.getOptions().writable.value; } get renderMode(): EditorType | null { return null; } // TODO + public getID(): string { return `editor-widget-${this._id}`; } + // region - [public] public async open(source: URI): Promise> { @@ -344,6 +357,10 @@ export class EditorWidget extends Disposable implements IEditorWidget { // cache data this._editorData = this.__register(new EditorData(this._model, this._viewModel, this._view, undefined)); + + // track + this.editorService.create(this); + return ok(); }); } @@ -360,10 +377,14 @@ export class EditorWidget extends Disposable implements IEditorWidget { } public override dispose(): void { + this.editorService.onClose(this); + super.dispose(); this.__detachData(); this._extensions.dispose(); this.logService.debug('EditorWidget', 'Editor disposed.'); + + this.editorService.close(); } public updateOptions(newOption: Partial): void { @@ -482,6 +503,9 @@ export class EditorWidget extends Disposable implements IEditorWidget { this._options.updateOptions(newOption); } })); + + this.__register(this.onDidFocus(e => this.editorService.focus(this))); + this.__register(this.onDidBlur(e => this.editorService.blur(this))); } private __registerMVVMListeners(model: IEditorModel, viewModel: IEditorViewModel, view: IEditorView): void { diff --git a/src/platform/registrant/common/registrant.ts b/src/platform/registrant/common/registrant.ts index 09c202cad..030da0e54 100644 --- a/src/platform/registrant/common/registrant.ts +++ b/src/platform/registrant/common/registrant.ts @@ -100,7 +100,7 @@ export type GetRegistrantByType = T extends (keyof Reg export function createRegister( type: T, description: string, - register: (registrant: GetRegistrantByType) => void, + register: (registrant: GetRegistrantByType, provider: IServiceProvider) => void, ): (provider: IServiceProvider) => void { return (provider: IServiceProvider) => { @@ -110,7 +110,7 @@ export function createRegister( const service = provider.getOrCreateService(IRegistrantService); const registrant = service.getRegistrant(type); try { - register(registrant); + register(registrant, provider); } catch (error: any) { logService.error('createRegister', 'failed registering.', error, { type: type, description: description }); } diff --git a/src/workbench/services/editor/editor.ts b/src/workbench/services/editor/editor.ts new file mode 100644 index 000000000..71c939ad9 --- /dev/null +++ b/src/workbench/services/editor/editor.ts @@ -0,0 +1,43 @@ +import { Register } from "src/base/common/event"; +import { IEditorWidget } from "src/editor/editorWidget"; +import { createService, IService, refineDecorator } from "src/platform/instantiation/common/decorator"; + +export const IEditorService = createService('editor-service'); + +/** Do not use this unless you know what you are doing. */ +export const IEditorHostService = refineDecorator(IEditorService); + +/** + * A service that tracks the lifecycle and status of {@link IEditorWidget} + * instances. + * @note It does NOT manage the actual lifecycle of editors, but rather observes + * and emits events related to their creation, focus state, and closure. + */ +export interface IEditorService extends IService { + + readonly onCreateEditor: Register; + readonly onDidCreateEditor: Register; + readonly onCloseEditor: Register; + readonly onDidCloseEditor: Register; + + /** + * Fires the current focused editor. + */ + readonly onDidFocusedEditorChange: Register; + + /** + * The only editor that the user is currently typing with. + */ + getFocusedEditor(): IEditorWidget | undefined; + + getEditors(): readonly IEditorWidget[]; +} + +export interface IEditorHostService extends IEditorService { + onCreate(): void; + create(editor: IEditorWidget): void; + onClose(editor: IEditorWidget): void; + close(): void; + focus(editor: IEditorWidget): void; + blur(editor: IEditorWidget): void; +} \ No newline at end of file diff --git a/src/workbench/services/editor/editorService.ts b/src/workbench/services/editor/editorService.ts new file mode 100644 index 000000000..38d4e74ce --- /dev/null +++ b/src/workbench/services/editor/editorService.ts @@ -0,0 +1,85 @@ +import { Disposable } from "src/base/common/dispose"; +import { Emitter } from "src/base/common/event"; +import { IEditorWidget } from "src/editor/editorWidget"; +import { IEditorHostService } from "src/workbench/services/editor/editor"; + +export class EditorService extends Disposable implements IEditorHostService { + + declare _serviceMarker: undefined; + + // [event] + + private readonly _onCreateEditor = this.__register(new Emitter()); + public readonly onCreateEditor = this._onCreateEditor.registerListener; + + private readonly _onDidCreateEditor = this.__register(new Emitter()); + public readonly onDidCreateEditor = this._onDidCreateEditor.registerListener; + + private readonly _onCloseEditor = this.__register(new Emitter()); + public readonly onCloseEditor = this._onCloseEditor.registerListener; + + private readonly _onDidCloseEditor = this.__register(new Emitter()); + public readonly onDidCloseEditor = this._onDidCloseEditor.registerListener; + + private readonly _onDidFocusedEditorChange = this.__register(new Emitter()); + public readonly onDidFocusedEditorChange = this._onDidFocusedEditorChange.registerListener; + + // [field] + + private readonly _editors: Map; + private _focusedEditor: IEditorWidget | undefined; + + // [constructor] + + constructor() { + super(); + this._editors = new Map(); + this._focusedEditor = undefined; + } + + // [public methods] + + public override dispose(): void { + super.dispose(); + this._editors.clear(); + } + + public onCreate(): void { + this._onCreateEditor.fire(); + } + + public create(editor: IEditorWidget): void { + this._editors.set(editor.getID(), editor); + this._onDidCreateEditor.fire(editor); + } + + public onClose(editor: IEditorWidget): void { + this._editors.delete(editor.getID()); + this._onCloseEditor.fire(editor); + } + + public close(): void { + this._onDidCloseEditor.fire(); + } + + public focus(editor: IEditorWidget): void { + this._focusedEditor = editor; + this._onDidFocusedEditorChange.fire(editor); + } + + public blur(editor: IEditorWidget): void { + this._focusedEditor = undefined; + this._onDidFocusedEditorChange.fire(undefined); + } + + public getFocusedEditor(): IEditorWidget | undefined { + return this._focusedEditor; + } + + public getEditors(): readonly IEditorWidget[] { + return [...this._editors.values()]; + } + + // [private methods] + +} \ No newline at end of file diff --git a/test/base/common/keyboard.test.ts b/test/base/common/keyboard.test.ts index bcbdaa10a..d63bf2957 100644 --- a/test/base/common/keyboard.test.ts +++ b/test/base/common/keyboard.test.ts @@ -100,6 +100,9 @@ suite('keyboard-test', () => { assert.strictEqual(Shortcut.fromString('PageDown').equal(new Shortcut(false, false, false, false, KeyCode.PageDown)), true); assert.strictEqual(Shortcut.fromString('CTrL+PageDown').equal(new Shortcut(true, false, false, false, KeyCode.PageDown)), true); assert.strictEqual(Shortcut.fromString('SHiFt+AlT+0').equal(new Shortcut(false, true, true, false, KeyCode.Digit0)), true); + + // more cases + assert.strictEqual(Shortcut.fromString('Ctrl+A').equal(new Shortcut(true, false, false, false, KeyCode.KeyA)), true); }); const enum KeyModifier { diff --git a/test/editor/contrib/editorCommands.test.ts b/test/editor/contrib/editorCommands.test.ts deleted file mode 100644 index aba1ac0ff..000000000 --- a/test/editor/contrib/editorCommands.test.ts +++ /dev/null @@ -1,219 +0,0 @@ -import * as assert from 'assert'; -import { ProseUtilsTest } from 'test/editor/view/editorHelpers'; -import ist from 'ist'; -import { ProseEditorState, ProseNode, ProseNodeSelection, ProseSchema, ProseSelection, ProseTextSelection } from 'src/editor/common/proseMirror'; -import { EditorCommands } from 'src/editor/contrib/command/command.contrib'; -import { nullObject } from 'test/utils/helpers'; -import { EditorCommandBase } from 'src/editor/contrib/command/editorCommand'; - -const { doc, p, blockquote, hr, ul, li } = ProseUtilsTest.defaultNodes; -// import {schema, eq, doc, blockquote, pre, h1, p, li, ol, ul, em, strong, hr, img} from "prosemirror-test-builder"; - -function getFrom(node: ProseNode): number | undefined { - return (node as any).tag['a']; -} - -function getTo(node: ProseNode): number | undefined { - return (node as any).tag['b']; -} - -/** - * Selection is marked by and during this test environment. - */ -function getSelection(doc: ProseNode): ProseSelection { - const a = getFrom(doc); - - if (a !== undefined) { - const $a = doc.resolve(a); - if ($a.parent.inlineContent) { - const b = getTo(doc); - const $b = b !== undefined ? doc.resolve(b) : undefined; - return new ProseTextSelection($a, $b); - } - - else return new ProseNodeSelection($a); - } - - return ProseSelection.atStart(doc); -} - -function isEqualNode(a: ProseNode, b: ProseNode): boolean { - return a.eq(b); -} - -function createStateBy(doc: ProseNode) { - return ProseEditorState.create({ doc, selection: getSelection(doc) }); -} - -function execCommand(doc: ProseNode, cmd: EditorCommandBase, result: ProseNode | null) { - let state = createStateBy(doc); - - cmd.run(undefined!, undefined!, state, tr => state = state.apply(tr)); - ist(state.doc, result || doc, isEqualNode); - - if (result && getFrom(result) !== undefined) { - ist(state.selection, getSelection(result), isEqualNode); - } -} - -suite.skip('editorCommands-test', () => { - - suite('DeleteSelection', () => { - const cmd = new EditorCommands.DeleteSelection(nullObject()); - - test('deletes part of a text node', () => { - execCommand( - doc(p('foo')), cmd, - doc(p('fo')), - ); - }); - - test('can delete across blocks', () => { - execCommand( - doc(p('foo'), p('bar')), cmd, - doc(p('fr')), - ); - }); - - test('deletes node selections', () => { - execCommand( - doc(p('foo'), '', hr()), cmd, - doc(p('foo')), - ); - }); - - test('moves selection after deleted node', () => { - execCommand( - doc(p('a'), '', p('b'), blockquote(p('c'))), cmd, - doc(p('a'), blockquote(p('c'))) - ); - }); - - test('moves selection before deleted node at end', () => { - execCommand( - doc(p('a'), '', p('b')), cmd, - doc(p('a')), - ); - }); - }); - - suite('JoinBackward', () => { - const cmd = new EditorCommands.JoinBackward(nullObject()); - - test('can join paragraphs', () => - execCommand( - doc(p('hi'), p('there')), cmd, - doc(p('hithere'))), - ); - - test('can join out of a nested node', () => { - execCommand( - doc(p('hi'), blockquote(p('there'))), cmd, - doc(p('hi'), p('there')), - ); - }); - - test('moves a block into an adjacent wrapper', () => { - execCommand( - doc(blockquote(p('hi')), p('there')), cmd, - doc(blockquote(p('hi'), p('there'))), - ); - }); - - test('moves a block into an adjacent wrapper from another wrapper', () => { - execCommand( - doc(blockquote(p('hi')), blockquote(p('there'))), cmd, - doc(blockquote(p('hi'), p('there'))), - ); - }); - - test('joins the wrapper to a subsequent one if applicable', () => { - execCommand( - doc(blockquote(p('hi')), p('there'), blockquote(p('x'))), cmd, - doc(blockquote(p('hi'), p('there'), p('x'))), - ); - }); - - // FIX - test('moves a block into a list item', () => { - execCommand( - doc(ul(li(p('hi'))), p('there')), cmd, - doc(ul(li(p('hi')), li(p('there')))), - ); - }); - - // FIX - test('joins lists', () => { - execCommand( - doc(ul(li(p('hi'))), ul(li(p('there')))), cmd, - doc(ul(li(p('hi')), li(p('there')))), - ); - }); - - // FIX - test('joins list items', () => { - execCommand( - doc(ul(li(p('hi')), li(p('there')))), cmd, - doc(ul(li(p('hi'), p('there')))), - ); - }); - - test('lifts out of a list at the start', () => { - execCommand( - doc(ul(li(p('there')))), cmd, - doc(p('there')), - ); - }); - - // FIX - test('joins lists before and after', () => { - execCommand( - doc(ul(li(p('hi'))), p('there'), ul(li(p('x')))), cmd, - doc(ul(li(p('hi')), li(p('there')), li(p('x')))), - ); - }); - - test('deletes leaf nodes before', () => { - execCommand( - doc(hr, p('there')), cmd, - doc(p('there')), - ); - }); - - test('lifts before it deletes', () => { - execCommand( - doc(hr, blockquote(p('there'))), cmd, - doc(hr, p('there')), - ); - }); - - test('does nothing at start of doc', () => { - execCommand( - doc(p('foo')), cmd, - null, - ); - }); - - test('can join single-textblock-child nodes', () => { - const schema = new ProseSchema({ - nodes: { - text: { inline: true }, - doc: { content: 'block+' }, - block: { content: 'para' }, - para: { content: 'text*' } - } - }); - const doc = schema - .node('doc', null, [ - schema.node('block', null, [schema.node('para', null, [schema.text('a')])]), - schema.node('block', null, [schema.node('para', null, [schema.text('b')])]) - ]); - let state = ProseEditorState.create({ doc, selection: ProseTextSelection.near(doc.resolve(7)) }); - - - - ist(cmd.run(undefined!, undefined!, state, tr => state = state.apply(tr))); - ist(state.doc.toString(), 'doc(block(para("ab")))'); - }); - }); -}); \ No newline at end of file