From 91004f57148e1701996bbca9055a7f6affa30ec2 Mon Sep 17 00:00:00 2001 From: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com> Date: Sat, 9 Mar 2024 02:49:28 -0800 Subject: [PATCH] Desktop: Fixes #10020: Beta markdown editor: Support overriding built-in keyboard shortcuts (#10022) --- .eslintignore | 22 +++++---- .gitignore | 22 +++++---- .../utils/normalizeAccelerator.test.ts | 21 ++++++++ .../CodeMirror/utils/normalizeAccelerator.ts | 35 +++++++++++++ .../NoteBody/CodeMirror/utils/types.ts | 5 ++ .../NoteBody/CodeMirror/v5/Editor.tsx | 16 +++--- .../{ => v5}/utils/useCursorUtils.test.ts | 0 .../{ => v5}/utils/useCursorUtils.ts | 0 .../{ => v5}/utils/useExternalPlugins.ts | 0 .../{ => v5}/utils/useJoplinCommands.ts | 0 .../{ => v5}/utils/useJoplinMode.ts | 0 .../CodeMirror/{ => v5}/utils/useKeymap.ts | 29 ++--------- .../{ => v5}/utils/useLineSorting.ts | 0 .../CodeMirror/{ => v5}/utils/useListIdent.ts | 0 .../{ => v5}/utils/useScrollUtils.ts | 0 .../NoteBody/CodeMirror/v6/Editor.tsx | 3 ++ .../NoteBody/CodeMirror/v6/utils/useKeymap.ts | 49 +++++++++++++++++++ .../CodeMirror/CodeMirrorControl.test.ts | 27 ++++++++++ .../editor/CodeMirror/CodeMirrorControl.ts | 19 ++++++- packages/editor/CodeMirror/createEditor.ts | 9 ++-- .../CodeMirror/testUtil/pressReleaseKey.ts | 20 ++++++++ packages/tools/cspell/dictionary4.txt | 1 + 22 files changed, 223 insertions(+), 55 deletions(-) create mode 100644 packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/normalizeAccelerator.test.ts create mode 100644 packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/normalizeAccelerator.ts rename packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/{ => v5}/utils/useCursorUtils.test.ts (100%) rename packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/{ => v5}/utils/useCursorUtils.ts (100%) rename packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/{ => v5}/utils/useExternalPlugins.ts (100%) rename packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/{ => v5}/utils/useJoplinCommands.ts (100%) rename packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/{ => v5}/utils/useJoplinMode.ts (100%) rename packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/{ => v5}/utils/useKeymap.ts (81%) rename packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/{ => v5}/utils/useLineSorting.ts (100%) rename packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/{ => v5}/utils/useListIdent.ts (100%) rename packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/{ => v5}/utils/useScrollUtils.ts (100%) create mode 100644 packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useKeymap.ts create mode 100644 packages/editor/CodeMirror/testUtil/pressReleaseKey.ts diff --git a/.eslintignore b/.eslintignore index 618ef565f..aab144c51 100644 --- a/.eslintignore +++ b/.eslintignore @@ -255,27 +255,30 @@ packages/app-desktop/gui/Navigator.js packages/app-desktop/gui/NoteContentPropertiesDialog.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/Toolbar.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/index.js +packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/normalizeAccelerator.test.js +packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/normalizeAccelerator.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/types.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useContextMenu.js -packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useCursorUtils.test.js -packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useCursorUtils.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useEditorSearchExtension.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useEditorSearchHandler.js -packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useExternalPlugins.js -packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useJoplinCommands.js -packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useJoplinMode.js -packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useKeymap.js -packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useLineSorting.js -packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useListIdent.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useScrollHandler.js -packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useScrollUtils.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useStyles.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useWebviewIpcMessage.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/CodeMirror.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/Editor.js +packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useCursorUtils.test.js +packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useCursorUtils.js +packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useExternalPlugins.js +packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useJoplinCommands.js +packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useJoplinMode.js +packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useKeymap.js +packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useLineSorting.js +packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useListIdent.js +packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useScrollUtils.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/CodeMirror.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/Editor.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/useEditorCommands.js +packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useKeymap.js packages/app-desktop/gui/NoteEditor/NoteBody/PlainEditor/PlainEditor.js packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/styles/index.js @@ -648,6 +651,7 @@ packages/editor/CodeMirror/testUtil/createEditorSettings.js packages/editor/CodeMirror/testUtil/createTestEditor.js packages/editor/CodeMirror/testUtil/forceFullParse.js packages/editor/CodeMirror/testUtil/loadLanguages.js +packages/editor/CodeMirror/testUtil/pressReleaseKey.js packages/editor/CodeMirror/testUtil/typeText.js packages/editor/CodeMirror/theme.js packages/editor/CodeMirror/util/isInSyntaxNode.js diff --git a/.gitignore b/.gitignore index 665c9ffed..0b3a8a44b 100644 --- a/.gitignore +++ b/.gitignore @@ -235,27 +235,30 @@ packages/app-desktop/gui/Navigator.js packages/app-desktop/gui/NoteContentPropertiesDialog.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/Toolbar.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/index.js +packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/normalizeAccelerator.test.js +packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/normalizeAccelerator.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/types.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useContextMenu.js -packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useCursorUtils.test.js -packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useCursorUtils.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useEditorSearchExtension.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useEditorSearchHandler.js -packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useExternalPlugins.js -packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useJoplinCommands.js -packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useJoplinMode.js -packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useKeymap.js -packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useLineSorting.js -packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useListIdent.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useScrollHandler.js -packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useScrollUtils.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useStyles.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useWebviewIpcMessage.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/CodeMirror.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/Editor.js +packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useCursorUtils.test.js +packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useCursorUtils.js +packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useExternalPlugins.js +packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useJoplinCommands.js +packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useJoplinMode.js +packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useKeymap.js +packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useLineSorting.js +packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useListIdent.js +packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useScrollUtils.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/CodeMirror.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/Editor.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/useEditorCommands.js +packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useKeymap.js packages/app-desktop/gui/NoteEditor/NoteBody/PlainEditor/PlainEditor.js packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/styles/index.js @@ -628,6 +631,7 @@ packages/editor/CodeMirror/testUtil/createEditorSettings.js packages/editor/CodeMirror/testUtil/createTestEditor.js packages/editor/CodeMirror/testUtil/forceFullParse.js packages/editor/CodeMirror/testUtil/loadLanguages.js +packages/editor/CodeMirror/testUtil/pressReleaseKey.js packages/editor/CodeMirror/testUtil/typeText.js packages/editor/CodeMirror/theme.js packages/editor/CodeMirror/util/isInSyntaxNode.js diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/normalizeAccelerator.test.ts b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/normalizeAccelerator.test.ts new file mode 100644 index 000000000..23b584838 --- /dev/null +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/normalizeAccelerator.test.ts @@ -0,0 +1,21 @@ +import normalizeAccelerator from './normalizeAccelerator'; +import { CodeMirrorVersion } from './types'; + +describe('normalizeAccelerator', () => { + test.each([ + ['Z', { v6: 'z', v5: 'Z' }], + ['Alt+A', { v6: 'Alt-a', v5: 'Alt-A' }], + ['Shift+A', { v6: 'Shift-a', v5: 'Shift-A' }], + ['Shift+Up', { v6: 'Shift-Up', v5: 'Shift-Up' }], + ])( + 'should convert single-letter key names to lowercase for CM6, keep case unchanged for CM5 (%j)', + (original, expected) => { + expect(normalizeAccelerator( + original, CodeMirrorVersion.CodeMirror6, + )).toBe(expected.v6); + expect(normalizeAccelerator( + original, CodeMirrorVersion.CodeMirror5, + )).toBe(expected.v5); + }, + ); +}); diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/normalizeAccelerator.ts b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/normalizeAccelerator.ts new file mode 100644 index 000000000..a42ad3248 --- /dev/null +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/normalizeAccelerator.ts @@ -0,0 +1,35 @@ +import { CodeMirrorVersion } from './types'; + +// CodeMirror and Electron register accelerators slightly different +// CodeMirror requires a - between keys while Electron want's a + +// CodeMirror doesn't recognize Option (it uses Alt instead) +// CodeMirror requires Shift to be first +// CodeMirror 6 requires Shift if the key name is uppercase. +const normalizeAccelerator = (accelerator: string, editorVersion: CodeMirrorVersion) => { + const command = accelerator.replace(/\+/g, '-').replace('Option', 'Alt'); + // From here is taken out of codemirror/lib/codemirror.js, modified + // to also support CodeMirror 6. + const parts = command.split(/-(?!$)/); + let name = parts[parts.length - 1]; + + // In CodeMirror 6, an uppercase single-letter key name makes the editor + // require the shift key to activate the shortcut. If a key name like `Up`, + // however, `.toLowerCase` breaks the shortcut. + if (editorVersion === CodeMirrorVersion.CodeMirror6 && name.length === 1) { + name = name.toLowerCase(); + } + + let alt, ctrl, shift, cmd; + for (let i = 0; i < parts.length - 1; i++) { + const mod = parts[i]; + if (/^(cmd|meta|m)$/i.test(mod)) { cmd = true; } else if (/^a(lt)?$/i.test(mod)) { alt = true; } else if (/^(c|ctrl|control)$/i.test(mod)) { ctrl = true; } else if (/^s(hift)?$/i.test(mod)) { shift = true; } else { throw new Error(`Unrecognized modifier name: ${mod}`); } + } + if (alt) { name = `Alt-${name}`; } + if (ctrl) { name = `Ctrl-${name}`; } + if (cmd) { name = `Cmd-${name}`; } + if (shift) { name = `Shift-${name}`; } + return name; + // End of code taken from codemirror/lib/codemirror.js +}; + +export default normalizeAccelerator; diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/types.ts b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/types.ts index 79c6bb8e3..084483a1e 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/types.ts +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/types.ts @@ -9,3 +9,8 @@ export function defaultRenderedBody(): RenderedBody { pluginAssets: [], }; } + +export enum CodeMirrorVersion { + CodeMirror5, + CodeMirror6, +} diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/Editor.tsx b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/Editor.tsx index 063f16c60..04c232e8b 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/Editor.tsx +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/Editor.tsx @@ -12,15 +12,15 @@ import 'codemirror/addon/scroll/annotatescrollbar'; import 'codemirror/addon/search/matchesonscrollbar'; import 'codemirror/addon/search/searchcursor'; -import useListIdent from '../utils/useListIdent'; -import useScrollUtils from '../utils/useScrollUtils'; -import useCursorUtils from '../utils/useCursorUtils'; -import useLineSorting from '../utils/useLineSorting'; +import useListIdent from './utils/useListIdent'; +import useScrollUtils from './utils/useScrollUtils'; +import useCursorUtils from './utils/useCursorUtils'; +import useLineSorting from './utils/useLineSorting'; import useEditorSearch from '../utils/useEditorSearchExtension'; -import useJoplinMode from '../utils/useJoplinMode'; -import useKeymap from '../utils/useKeymap'; -import useExternalPlugins from '../utils/useExternalPlugins'; -import useJoplinCommands from '../utils/useJoplinCommands'; +import useJoplinMode from './utils/useJoplinMode'; +import useKeymap from './utils/useKeymap'; +import useExternalPlugins from './utils/useExternalPlugins'; +import useJoplinCommands from './utils/useJoplinCommands'; import 'codemirror/keymap/emacs'; import 'codemirror/keymap/vim'; diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useCursorUtils.test.ts b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useCursorUtils.test.ts similarity index 100% rename from packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useCursorUtils.test.ts rename to packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useCursorUtils.test.ts diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useCursorUtils.ts b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useCursorUtils.ts similarity index 100% rename from packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useCursorUtils.ts rename to packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useCursorUtils.ts diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useExternalPlugins.ts b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useExternalPlugins.ts similarity index 100% rename from packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useExternalPlugins.ts rename to packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useExternalPlugins.ts diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useJoplinCommands.ts b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useJoplinCommands.ts similarity index 100% rename from packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useJoplinCommands.ts rename to packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useJoplinCommands.ts diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useJoplinMode.ts b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useJoplinMode.ts similarity index 100% rename from packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useJoplinMode.ts rename to packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useJoplinMode.ts diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useKeymap.ts b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useKeymap.ts similarity index 81% rename from packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useKeymap.ts rename to packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useKeymap.ts index ae0cfc4b0..e16503794 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useKeymap.ts +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useKeymap.ts @@ -1,11 +1,13 @@ import { useEffect } from 'react'; import CommandService from '@joplin/lib/services/CommandService'; import KeymapService, { KeymapItem } from '@joplin/lib/services/KeymapService'; -import { EditorCommand } from '../../../utils/types'; +import { EditorCommand } from '../../../../utils/types'; import shim from '@joplin/lib/shim'; import { reg } from '@joplin/lib/registry'; import setupVim from '@joplin/editor/CodeMirror/util/setupVim'; import { EventName } from '@joplin/lib/eventManager'; +import normalizeAccelerator from '../../utils/normalizeAccelerator'; +import { CodeMirrorVersion } from '../../utils/types'; export default function useKeymap(CodeMirror: any) { @@ -28,29 +30,6 @@ export default function useKeymap(CodeMirror: any) { return command.slice(7); // 7 is the length of editor. } - // CodeMirror and Electron register accelerators slightly different - // CodeMirror requires a - between keys while Electron want's a + - // CodeMirror doesn't recognize Option (it uses Alt instead) - // CodeMirror requires Shift to be first - function normalizeAccelerator(accelerator: string) { - const command = accelerator.replace(/\+/g, '-').replace('Option', 'Alt'); - // From here is taken out of codemirror/lib/codemirror.js - const parts = command.split(/-(?!$)/); - - let name = parts[parts.length - 1]; - let alt, ctrl, shift, cmd; - for (let i = 0; i < parts.length - 1; i++) { - const mod = parts[i]; - if (/^(cmd|meta|m)$/i.test(mod)) { cmd = true; } else if (/^a(lt)?$/i.test(mod)) { alt = true; } else if (/^(c|ctrl|control)$/i.test(mod)) { ctrl = true; } else if (/^s(hift)?$/i.test(mod)) { shift = true; } else { throw new Error(`Unrecognized modifier name: ${mod}`); } - } - if (alt) { name = `Alt-${name}`; } - if (ctrl) { name = `Ctrl-${name}`; } - if (cmd) { name = `Cmd-${name}`; } - if (shift) { name = `Shift-${name}`; } - return name; - // End of code taken from codemirror/lib/codemirror.js - } - // Because there is sometimes a clash between these keybindings and the Joplin window ones // (This specifically can happen with the Ctrl-B and Ctrl-I keybindings when // codemirror is in contenteditable mode) @@ -74,7 +53,7 @@ export default function useKeymap(CodeMirror: any) { } // CodeMirror and Electron have slightly different formats for defining accelerators - const acc = normalizeAccelerator(key.accelerator); + const acc = normalizeAccelerator(key.accelerator, CodeMirrorVersion.CodeMirror5); CodeMirror.keyMap.default[acc] = command; } diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useLineSorting.ts b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useLineSorting.ts similarity index 100% rename from packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useLineSorting.ts rename to packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useLineSorting.ts diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useListIdent.ts b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useListIdent.ts similarity index 100% rename from packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useListIdent.ts rename to packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useListIdent.ts diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useScrollUtils.ts b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useScrollUtils.ts similarity index 100% rename from packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useScrollUtils.ts rename to packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useScrollUtils.ts diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/Editor.tsx b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/Editor.tsx index 19fe5bb12..c008dc404 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/Editor.tsx +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/Editor.tsx @@ -10,6 +10,7 @@ import shim from '@joplin/lib/shim'; import PluginService from '@joplin/lib/services/plugins/PluginService'; import setupVim from '@joplin/editor/CodeMirror/util/setupVim'; import { dirname } from 'path'; +import useKeymap from './utils/useKeymap'; import useEditorSearch from '../utils/useEditorSearchExtension'; interface Props extends EditorProps { @@ -145,6 +146,8 @@ const Editor = (props: Props, ref: ForwardedRef) => { setupVim(editor); }, [editor]); + useKeymap(editor); + return (
{ + useEffect(() => { + if (!editorControl) return () => {}; + + // Some commands aren't registered with the command service + // (e.g. Quit). Don't have CodeMirror handle these. + // See gui/KeymapConfig/getLabel.ts. + const isCommandRegistered = (commandName: string) => { + const commandNames = CommandService.instance().commandNames(); + return commandNames.includes(commandName); + }; + + const keymapItemToCodeMirror = (binding: KeymapItem) => { + if (!binding.accelerator || !isCommandRegistered(binding.command)) { + return null; + } + + return { + key: normalizeAccelerator( + binding.accelerator, CodeMirrorVersion.CodeMirror6, + ), + run: () => { + void CommandService.instance().execute(binding.command); + return true; + }, + }; + }; + + const keymapItems = KeymapService.instance().getKeymapItems(); + const addedKeymap = editorControl.prependKeymap( + keymapItems + .map(item => keymapItemToCodeMirror(item)) + .filter(item => !!item), + ); + + return () => { + addedKeymap.remove(); + }; + }, [editorControl]); +}; + +export default useKeymap; diff --git a/packages/editor/CodeMirror/CodeMirrorControl.test.ts b/packages/editor/CodeMirror/CodeMirrorControl.test.ts index 726c02f1d..d01d29038 100644 --- a/packages/editor/CodeMirror/CodeMirrorControl.test.ts +++ b/packages/editor/CodeMirror/CodeMirrorControl.test.ts @@ -1,5 +1,7 @@ import { ViewPlugin } from '@codemirror/view'; import createEditorControl from './testUtil/createEditorControl'; +import { EditorCommandType } from '../types'; +import pressReleaseKey from './testUtil/pressReleaseKey'; describe('CodeMirrorControl', () => { it('clearHistory should clear the undo/redo history', () => { @@ -58,6 +60,31 @@ describe('CodeMirrorControl', () => { expect(command).toHaveBeenCalledTimes(1); }); + it('should support overriding default keybindings', () => { + const control = createEditorControl('test'); + control.execCommand(EditorCommandType.SelectAll); + + const testCommand = jest.fn(() => true); + const keybindings = control.prependKeymap([ + // Override the default binding for ctrl-d (search) + { key: 'Ctrl-d', run: testCommand }, + ]); + + // Should call the override command rather than the default handler + const keyData = { + key: 'd', + code: 'KeyD', + ctrlKey: true, + }; + pressReleaseKey(control.editor, keyData); + expect(testCommand).toHaveBeenCalledTimes(1); + + // Calling keybindings.remove should deregister the override. + keybindings.remove(); + pressReleaseKey(control.editor, keyData); + expect(testCommand).toHaveBeenCalledTimes(1); + }); + it('should toggle comments', () => { const control = createEditorControl('Hello\nWorld\n'); control.select(1, 5); diff --git a/packages/editor/CodeMirror/CodeMirrorControl.ts b/packages/editor/CodeMirror/CodeMirrorControl.ts index 415be3049..4c1d26c4d 100644 --- a/packages/editor/CodeMirror/CodeMirrorControl.ts +++ b/packages/editor/CodeMirror/CodeMirrorControl.ts @@ -1,4 +1,4 @@ -import { EditorView } from '@codemirror/view'; +import { EditorView, KeyBinding, keymap } from '@codemirror/view'; import { EditorCommandType, EditorControl, EditorSettings, LogMessageCallback, ContentScriptData, SearchState } from '../types'; import CodeMirror5Emulation from './CodeMirror5Emulation/CodeMirror5Emulation'; import editorCommands from './editorCommands/editorCommands'; @@ -166,6 +166,23 @@ export default class CodeMirrorControl extends CodeMirror5Emulation implements E // CodeMirror-specific methods // + public prependKeymap(bindings: readonly KeyBinding[]) { + const compartment = new Compartment(); + this.editor.dispatch({ + effects: StateEffect.appendConfig.of([ + compartment.of(keymap.of(bindings)), + ]), + }); + + return { + remove: () => { + this.editor.dispatch({ + effects: compartment.reconfigure([]), + }); + }, + }; + } + public joplinExtensions = { // Some plugins want to enable autocompletion from *just* that plugin, without also // enabling autocompletion for text within code blocks (and other built-in completion diff --git a/packages/editor/CodeMirror/createEditor.ts b/packages/editor/CodeMirror/createEditor.ts index 2d8076d9b..ff53d09e2 100644 --- a/packages/editor/CodeMirror/createEditor.ts +++ b/packages/editor/CodeMirror/createEditor.ts @@ -1,4 +1,4 @@ -import { Compartment, EditorState } from '@codemirror/state'; +import { Compartment, EditorState, Prec } from '@codemirror/state'; import { indentOnInput, syntaxHighlighting } from '@codemirror/language'; import { openSearchPanel, closeSearchPanel, getSearchQuery, search, @@ -238,7 +238,10 @@ const createEditor = ( notifySelectionChange(viewUpdate); notifySelectionFormattingChange(viewUpdate); }), - keymap.of([ + + // Give the default keymap low precedence so that it is overridden + // by extensions with default precedence. + Prec.low(keymap.of([ // Custom mod-f binding: Toggle the external dialog implementation // (don't show/hide the Panel dialog). keyCommand('Mod-f', (_: EditorView) => { @@ -268,7 +271,7 @@ const createEditor = ( }, true), ...standardKeymap, ...historyKeymap, ...searchKeymap, - ]), + ])), ], doc: initialText, }), diff --git a/packages/editor/CodeMirror/testUtil/pressReleaseKey.ts b/packages/editor/CodeMirror/testUtil/pressReleaseKey.ts new file mode 100644 index 000000000..718552734 --- /dev/null +++ b/packages/editor/CodeMirror/testUtil/pressReleaseKey.ts @@ -0,0 +1,20 @@ +import { EditorView } from '@codemirror/view'; + +interface KeyInfo { + key: string; + code: string; + ctrlKey?: boolean; + metaKey?: boolean; + shiftKey?: boolean; +} + +const pressReleaseKey = (editor: EditorView, key: KeyInfo) => { + editor.contentDOM.dispatchEvent( + new KeyboardEvent('keydown', key), + ); + editor.contentDOM.dispatchEvent( + new KeyboardEvent('keyup', key), + ); +}; + +export default pressReleaseKey; diff --git a/packages/tools/cspell/dictionary4.txt b/packages/tools/cspell/dictionary4.txt index 0b649d1f6..47345f49e 100644 --- a/packages/tools/cspell/dictionary4.txt +++ b/packages/tools/cspell/dictionary4.txt @@ -88,6 +88,7 @@ firstname lastname signup activatable +Prec titlewrapper notyf Notyf