1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-21 09:38:01 +02:00

Desktop: Fixes #10020: Beta markdown editor: Support overriding built-in keyboard shortcuts (#10022)

This commit is contained in:
Henry Heino 2024-03-09 02:49:28 -08:00 committed by GitHub
parent c35085d1d5
commit 91004f5714
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 223 additions and 55 deletions

View File

@ -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

22
.gitignore vendored
View File

@ -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

View File

@ -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);
},
);
});

View File

@ -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;

View File

@ -9,3 +9,8 @@ export function defaultRenderedBody(): RenderedBody {
pluginAssets: [],
};
}
export enum CodeMirrorVersion {
CodeMirror5,
CodeMirror6,
}

View File

@ -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';

View File

@ -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;
}

View File

@ -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<CodeMirrorControl>) => {
setupVim(editor);
}, [editor]);
useKeymap(editor);
return (
<div
style={props.style}

View File

@ -0,0 +1,49 @@
import { useEffect } from 'react';
import CommandService from '@joplin/lib/services/CommandService';
import KeymapService, { KeymapItem } from '@joplin/lib/services/KeymapService';
import CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl';
import normalizeAccelerator from '../../utils/normalizeAccelerator';
import { CodeMirrorVersion } from '../../utils/types';
const useKeymap = (editorControl: CodeMirrorControl) => {
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;

View File

@ -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);

View File

@ -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

View File

@ -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,
}),

View File

@ -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;

View File

@ -88,6 +88,7 @@ firstname
lastname
signup
activatable
Prec
titlewrapper
notyf
Notyf