diff --git a/packages/app-cli/tests/services_KeymapService.js b/packages/app-cli/tests/services_KeymapService.js index b227fd8e6..337ee862c 100644 --- a/packages/app-cli/tests/services_KeymapService.js +++ b/packages/app-cli/tests/services_KeymapService.js @@ -152,7 +152,7 @@ describe('services_KeymapService', () => { { command: 'newNote', accelerator: 'Ctrl+Alt+Shift+N' }, { command: 'synchronize', accelerator: 'F15' }, { command: 'textBold', accelerator: 'Shift+F5' }, - { command: 'showLocalSearch', accelerator: 'Ctrl+Alt+S' }, + { command: 'showLocalSearch', accelerator: 'Ctrl+Alt+L' }, { command: 'gotoAnything', accelerator: 'Ctrl+Shift+G' }, { command: 'print', accelerator: null /* Disabled */ }, { command: 'focusElementNoteTitle', accelerator: 'Ctrl+Alt+Shift+T' }, @@ -174,7 +174,7 @@ describe('services_KeymapService', () => { { command: 'newNote', accelerator: 'Ctrl+Alt+Shift+N' }, { command: 'synchronize', accelerator: null /* Disabled */ }, { command: 'textBold', accelerator: 'Shift+F5' }, - { command: 'showLocalSearch', accelerator: 'Ctrl+Alt+S' }, + { command: 'showLocalSearch', accelerator: 'Ctrl+Alt+L' }, { command: 'gotoAnything', accelerator: 'Ctrl+Shift+G' }, { command: 'print', accelerator: 'Alt+P' }, { command: 'focusElementNoteTitle', accelerator: 'Ctrl+Alt+Shift+T' }, @@ -222,7 +222,7 @@ describe('services_KeymapService', () => { { command: 'newNote', accelerator: 'Ctrl+Alt+Shift+N' }, { command: 'synchronize', accelerator: 'Ctrl+F11' }, { command: 'textBold', accelerator: 'Shift+F5' }, - { command: 'showLocalSearch', accelerator: 'Ctrl+Alt+S' }, + { command: 'showLocalSearch', accelerator: 'Ctrl+Alt+L' }, { command: 'gotoAnything', accelerator: 'Ctrl+Shift+G' }, { command: 'print', accelerator: 'Alt+P' }, { command: 'help', accelerator: null /* Disabled */ }, @@ -243,7 +243,7 @@ describe('services_KeymapService', () => { const customKeymaps = [ [ { commmmmand: 'gotoAnything', accelerator: 'Ctrl+Shift+G' }, - { command: 'showLocalSearch', accelerator: 'Ctrl+Alt+S' }, + { command: 'showLocalSearch', accelerator: 'Ctrl+Alt+L' }, { command: 'print', accelerator: 'Alt+P' }, ], [ @@ -252,13 +252,13 @@ describe('services_KeymapService', () => { { command: 'gotoAnything', accelerator: 'Ctrl+Shift+G' }, ], [ - { command: 'showLocalSearch', accel: 'Ctrl+Alt+S' }, + { command: 'showLocalSearch', accel: 'Ctrl+Alt+L' }, { command: 'print', accelerator: 'Alt+P' }, { command: 'gotoAnything', accelerator: 'Ctrl+Shift+G' }, ], [ { command: 'print' }, - { command: 'showLocalSearch', accelerator: 'Ctrl+Alt+S' }, + { command: 'showLocalSearch', accelerator: 'Ctrl+Alt+L' }, { command: 'gotoAnything', accelerator: 'Ctrl+Shift+G' }, ], ]; @@ -311,12 +311,12 @@ describe('services_KeymapService', () => { const customKeymaps_Linux = [ [ - { command: 'showLocalSearch', accelerator: 'Ctrl+Alt+S' /* Duplicate */ }, + { command: 'showLocalSearch', accelerator: 'Ctrl+Alt+L' /* Duplicate */ }, { command: 'print', accelerator: 'Alt+P' }, - { command: 'gotoAnything', accelerator: 'Ctrl+Alt+S' /* Duplicate */ }, + { command: 'gotoAnything', accelerator: 'Ctrl+Alt+L' /* Duplicate */ }, ], [ - { command: 'showLocalSearch', accelerator: 'Ctrl+Alt+S' }, + { command: 'showLocalSearch', accelerator: 'Ctrl+Alt+L' }, { command: 'print', accelerator: 'Ctrl+P' /* Default of gotoAnything */ }, { command: 'focusElementNoteTitle', accelerator: 'Ctrl+Alt+Shift+J' }, ], diff --git a/packages/app-cli/tests/support/plugins/codemirror_content_script/src/joplinMatchHighlighter.js b/packages/app-cli/tests/support/plugins/codemirror_content_script/src/joplinMatchHighlighter.js index caf446566..8e373bf66 100644 --- a/packages/app-cli/tests/support/plugins/codemirror_content_script/src/joplinMatchHighlighter.js +++ b/packages/app-cli/tests/support/plugins/codemirror_content_script/src/joplinMatchHighlighter.js @@ -4,12 +4,11 @@ function plugin(CodeMirror) { // This is a dummy command that is registered with codemirror. // Once created here it can be called by any other codemirror command - // using cm.execCommand(stringName) or by binding the command to a key in the keymap + // using cm.execCommand(stringName) or register a joplin command called 'editor.printSomething' + // through the joplin.commands api CodeMirror.commands.printSomething = function(cm) { console.log("Something"); } - // Here we manually bind the keys using the codemirror keymap - CodeMirror.keyMap.basic["Ctrl-U"] = "printSomething" } module.exports = { diff --git a/packages/app-desktop/gui/MenuBar.tsx b/packages/app-desktop/gui/MenuBar.tsx index 1c95c7bbb..6d43a1bec 100644 --- a/packages/app-desktop/gui/MenuBar.tsx +++ b/packages/app-desktop/gui/MenuBar.tsx @@ -509,6 +509,9 @@ function useMenu(props: Props) { menuItemDic.textPaste, menuItemDic.textSelectAll, separator(), + menuItemDic['editor.undo'], + menuItemDic['editor.redo'], + separator(), menuItemDic.textBold, menuItemDic.textItalic, menuItemDic.textLink, @@ -517,6 +520,14 @@ function useMenu(props: Props) { menuItemDic.insertDateTime, menuItemDic.attachFile, separator(), + menuItemDic['editor.deleteLine'], + menuItemDic['editor.toggleComment'], + menuItemDic['editor.sortSelectedLines'], + menuItemDic['editor.indentLess'], + menuItemDic['editor.indentMore'], + menuItemDic['editor.swapLineDown'], + menuItemDic['editor.swapLineUp'], + separator(), menuItemDic.focusSearch, menuItemDic.showLocalSearch, ], diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.tsx b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.tsx index e3f419675..2cb4b8e12 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.tsx +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.tsx @@ -164,6 +164,18 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) { replaceSelection: (value: any) => { return editorRef.current.replaceSelection(value); }, + textCopy: () => { + editorCopyText(); + }, + textCut: () => { + editorCutText(); + }, + textPaste: () => { + editorPaste(); + }, + textSelectAll: () => { + return editorRef.current.execCommand('selectAll'); + }, textBold: () => wrapSelectionWithStrings('**', '**', _('strong text')), textItalic: () => wrapSelectionWithStrings('*', '*', _('emphasised text')), textLink: async () => { @@ -210,6 +222,8 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) { if (commands[cmd.name]) { commandOutput = commands[cmd.name](cmd.value); + } else if (editorRef.current.supportsCommand(cmd)) { + commandOutput = editorRef.current.execCommandFromJoplinCommand(cmd); } else { reg.logger().warn('CodeMirror: unsupported Joplin command: ', cmd); } @@ -255,6 +269,17 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) { } }, []); + const editorPaste = useCallback(() => { + const clipboardText = clipboard.readText(); + + if (clipboardText) { + editorPasteText(); + } else { + // To handle pasting images + void onEditorPaste(); + } + }, [editorPasteText, onEditorPaste]); + const loadScript = async (script: any) => { return new Promise((resolve) => { let element: any = document.createElement('script'); @@ -598,7 +623,6 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) { const menu = new Menu(); const hasSelectedText = editorRef.current && !!editorRef.current.getSelection() ; - const clipboardText = clipboard.readText(); menu.append( new MenuItem({ @@ -625,12 +649,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) { label: _('Paste'), enabled: true, click: async () => { - if (clipboardText) { - editorPasteText(); - } else { - // To handle pasting images - void onEditorPaste(); - } + editorPaste(); }, }) ); diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/Editor.tsx b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/Editor.tsx index 46834ef72..7c9004441 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/Editor.tsx +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/Editor.tsx @@ -105,8 +105,8 @@ function Editor(props: EditorProps, ref: any) { useLineSorting(CodeMirror); useEditorSearch(CodeMirror); useJoplinMode(CodeMirror); - useKeymap(CodeMirror); const pluginOptions: any = useExternalPlugins(CodeMirror, props.plugins); + useKeymap(CodeMirror); useImperativeHandle(ref, () => { return editor; diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useKeymap.ts b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useKeymap.ts index 37d2e2d31..1908bc67c 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useKeymap.ts +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useKeymap.ts @@ -1,6 +1,9 @@ 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 shim from '@joplin/lib/shim'; +const { reg } = require('@joplin/lib/registry.js'); export default function useKeymap(CodeMirror: any) { @@ -23,6 +26,77 @@ export default function useKeymap(CodeMirror: any) { CodeMirror.Vim.mapCommand('o', 'action', 'insertListElement', { after: true }, { context: 'normal', isEdit: true, interlaceInsertRepeat: true }); } + function isEditorCommand(command: string) { + return command.startsWith('editor.'); + } + + // Converts a command of the form editor.command to just command + function editorCommandToCodeMirror(command: String) { + 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) + // This function uses simple regex to translate the Electron + // accelerator to a CodeMirror accelerator + function normalizeAccelerator(accelerator: String) { + return accelerator.replace(/\+/g, '-').replace('Option', 'Alt'); + } + + // 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) + // we will register all keypresses with the codemirror editor to guarentee they + // work no matter where the focus is + function registerJoplinCommand(key: KeymapItem) { + if (!key.command || !key.accelerator) return; + + let command = ''; + if (isEditorCommand(key.command)) { + command = editorCommandToCodeMirror(key.command); + } else { + // We need to register Joplin commands with codemirror + command = `joplin${key.command}`; + // Not all commands are registered with the command service + // (for example, the Quit command) + // This check will ensure that codemirror only takesover the commands that are + // see gui/KeymapConfig/getLabel.ts for more information + const commandNames = CommandService.instance().commandNames(); + if (commandNames.includes(key.command)) { + CodeMirror.commands[command] = () => { + void CommandService.instance().execute(key.command); + }; + } + } + + // CodeMirror and Electron have slightly different formats for defining accelerators + const acc = normalizeAccelerator(key.accelerator); + + CodeMirror.keyMap.default[acc] = command; + } + + // Called on initialization, and whenever the keymap changes + function registerKeymap() { + const keymapItems = KeymapService.instance().getKeymapItems(); + // Register all commands with the codemirror editor + keymapItems.forEach((key) => { registerJoplinCommand(key); }); + } + + CodeMirror.defineExtension('supportsCommand', function(cmd: EditorCommand) { + return isEditorCommand(cmd.name) && editorCommandToCodeMirror(cmd.name) in CodeMirror.commands; + }); + + // Used when an editor command is executed using the CommandService.instance().execute + // function (rather than being initiated by a keypress in the editor) + CodeMirror.defineExtension('execCommandFromJoplin', function(cmd: EditorCommand) { + if (cmd.value) { + reg.logger().warn('CodeMirror commands cannot accept a value:', cmd); + } + + return this.execCommand(editorCommandToCodeMirror(cmd.name)); + }); + useEffect(() => { // This enables the special modes (emacs and vim) to initiate sync by the save action CodeMirror.commands.save = save; @@ -46,61 +120,57 @@ export default function useKeymap(CodeMirror: any) { 'Esc': 'singleSelection', }; + // Some keybindings are added here and not to the global registry because users + // often expect multiple keys to bind to the same command for example, redo is mapped to + // both Ctrl+Shift+Z AND Ctrl+Y + // Doing this mapping here will make those commands available but will allow users to + // override them using the KeymapService + CodeMirror.keyMap.default = { + // Windows / Linux + 'Ctrl-Z': 'undo', + 'Shift-Ctrl-Z': 'redo', + 'Ctrl-Y': 'redo', + 'Ctrl-Up': 'goLineUp', + 'Ctrl-Down': 'go,ineDown', + 'Ctrl+Home': 'goDocStart', + 'Ctrl+End': 'goDocEnd', + 'Ctrl+Left': 'goGroupLeft', + 'Ctrl+Right': 'goGroupRight', + 'Alt+Left': 'goLineStart', + 'Alt+Right': 'goLineEnd', + 'Ctrl+Backspace': 'delGroupBefore', + 'Ctrl+Delete': 'delGroupAfter', + + 'fallthrough': 'basic', + }; if (shim.isMac()) { CodeMirror.keyMap.default = { // MacOS - 'Cmd-A': 'selectAll', - 'Cmd-D': 'deleteLine', - 'Cmd-Z': 'undo', 'Shift-Cmd-Z': 'redo', 'Cmd-Y': 'redo', - 'Cmd-Home': 'goDocStart', - 'Cmd-Up': 'goDocStart', 'Cmd-End': 'goDocEnd', 'Cmd-Down': 'goDocEnd', - 'Cmd-Left': 'goLineLeft', - 'Cmd-Right': 'goLineRight', - 'Alt-Left': 'goGroupLeft', - 'Alt-Right': 'goGroupRight', - 'Alt-Backspace': 'delGroupBefore', - 'Alt-Delete': 'delGroupAfter', - 'Cmd-[': 'indentLess', - 'Cmd-]': 'indentMore', - 'Cmd-/': 'toggleComment', - 'Cmd-Opt-S': 'sortSelectedLines', - 'Opt-Up': 'swapLineUp', - 'Opt-Down': 'swapLineDown', - - 'fallthrough': 'basic', - }; - } else { - CodeMirror.keyMap.default = { - // Windows/linux - 'Ctrl-A': 'selectAll', - 'Ctrl-D': 'deleteLine', - 'Ctrl-Z': 'undo', - 'Shift-Ctrl-Z': 'redo', - 'Ctrl-Y': 'redo', - 'Ctrl-Home': 'goDocStart', - 'Ctrl-End': 'goDocEnd', - 'Ctrl-Up': 'goLineUp', - 'Ctrl-Down': 'goLineDown', - 'Ctrl-Left': 'goGroupLeft', - 'Ctrl-Right': 'goGroupRight', - 'Alt-Left': 'goLineStart', - 'Alt-Right': 'goLineEnd', - 'Ctrl-Backspace': 'delGroupBefore', - 'Ctrl-Delete': 'delGroupAfter', - 'Ctrl-[': 'indentLess', - 'Ctrl-]': 'indentMore', - 'Ctrl-/': 'toggleComment', - 'Ctrl-Alt-S': 'sortSelectedLines', - 'Alt-Up': 'swapLineUp', - 'Alt-Down': 'swapLineDown', + 'Cmd-Home': 'goDocStart', + 'Cmd-Up': 'goDocStart', + 'Ctrl-D': 'delCharAfter', + 'Cmd+Home': 'goDocStart', + 'Cmd+End': 'goDocEnd', + 'Cmd+Left': 'goGroupLeft', + 'Cmd+Right': 'goGroupRight', + 'Ctrl+A': 'goLineStart', + 'Ctrl+E': 'goLineEnd', + 'Alt+Backspace': 'delGroupBefore', + 'Alt+Delete': 'delGroupAfter', 'fallthrough': 'basic', }; } + + const keymapService = KeymapService.instance(); + + registerKeymap(); + keymapService.on('keymapChange', registerKeymap); + setupEmacs(); setupVim(); }, []); diff --git a/packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.ts b/packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.ts index 75e496fd9..8ce64d5cf 100644 --- a/packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.ts +++ b/packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.ts @@ -83,6 +83,42 @@ const declarations: CommandDeclaration[] = [ label: () => _('Insert Date Time'), iconName: 'icon-add-date', }, + { + name: 'editor.deleteLine', + label: _('Delete line'), + }, + { + name: 'editor.undo', + label: _('Undo'), + }, + { + name: 'editor.redo', + label: _('Redo'), + }, + { + name: 'editor.indentLess', + label: _('Indent less'), + }, + { + name: 'editor.indentMore', + label: _('Indent more'), + }, + { + name: 'editor.toggleComment', + label: _('Toggle comment'), + }, + { + name: 'editor.sortSelectedLines', + label: _('Sort selected lines'), + }, + { + name: 'editor.swapLineUp', + label: _('Swap line up'), + }, + { + name: 'editor.swapLineDown', + label: _('Swap line down'), + }, { name: 'selectedText', }, diff --git a/packages/app-desktop/gui/menuCommandNames.ts b/packages/app-desktop/gui/menuCommandNames.ts index 468cab5f3..ae6f4efc4 100644 --- a/packages/app-desktop/gui/menuCommandNames.ts +++ b/packages/app-desktop/gui/menuCommandNames.ts @@ -34,5 +34,14 @@ export default function() { 'toggleNoteList', 'toggleSideBar', 'toggleVisiblePanes', + 'editor.deleteLine', + 'editor.undo', + 'editor.redo', + 'editor.indentLess', + 'editor.indentMore', + 'editor.toggleComment', + 'editor.sortSelectedLines', + 'editor.swapLineUp', + 'editor.swapLineDown', ]; } diff --git a/packages/lib/services/KeymapService.ts b/packages/lib/services/KeymapService.ts index b808b27e1..afa3f475d 100644 --- a/packages/lib/services/KeymapService.ts +++ b/packages/lib/services/KeymapService.ts @@ -45,6 +45,15 @@ const defaultKeymapItems = { { accelerator: 'Cmd+P', command: 'gotoAnything' }, { accelerator: 'Shift+Cmd+P', command: 'commandPalette' }, { accelerator: 'F1', command: 'help' }, + { accelerator: 'Cmd+D', command: 'editor.deleteLine' }, + { accelerator: 'Cmd+Z', command: 'editor.undo' }, + { accelerator: 'Cmd+Y', command: 'editor.redo' }, + { accelerator: 'Cmd+[', command: 'editor.indentLess' }, + { accelerator: 'Cmd+]', command: 'editor.indentMore' }, + { accelerator: 'Cmd+/', command: 'editor.toggleComment' }, + { accelerator: 'Option+Cmd+A', command: 'editor.sortSelectedLines' }, + { accelerator: 'Option+Up', command: 'editor.swapLineUp' }, + { accelerator: 'Option+Down', command: 'editor.swapLineDown' }, ], default: [ { accelerator: 'Ctrl+N', command: 'newNote' }, @@ -77,6 +86,15 @@ const defaultKeymapItems = { { accelerator: 'Ctrl+P', command: 'gotoAnything' }, { accelerator: 'Ctrl+Shift+P', command: 'commandPalette' }, { accelerator: 'F1', command: 'help' }, + { accelerator: 'Ctrl+D', command: 'editor.deleteLine' }, + { accelerator: 'Ctrl+Z', command: 'editor.undo' }, + { accelerator: 'Ctrl+Y', command: 'editor.redo' }, + { accelerator: 'Ctrl+[', command: 'editor.indentLess' }, + { accelerator: 'Ctrl+]', command: 'editor.indentMore' }, + { accelerator: 'Ctrl+/', command: 'editor.toggleComment' }, + { accelerator: 'Ctrl+Alt+S', command: 'editor.sortSelectedLines' }, + { accelerator: 'Alt+Up', command: 'editor.swapLineUp' }, + { accelerator: 'Alt+Down', command: 'editor.swapLineDown' }, ], }; diff --git a/readme/faq.md b/readme/faq.md index 5ba2c42d9..0221af315 100644 --- a/readme/faq.md +++ b/readme/faq.md @@ -14,6 +14,12 @@ Now try to install again and it should work. More info there: https://github.com/electron-userland/electron-builder/issues/4057 +## How can I pass arguments to the Linux installation script? + +You can pass [arguments](https://github.com/laurent22/joplin/blob/dev/Joplin_install_and_update.sh#L37) to the installation script by using this command. + +`wget -O - https://raw.githubusercontent.com/laurent22/joplin/dev/Joplin_install_and_update.sh \| bash -s -- --argument1 --argument2` + ## How can I edit my note in an external text editor? The editor command (may include arguments) defines which editor will be used to open a note. If none is provided it will try to auto-detect the default editor. If this does nothing or you want to change it for Joplin, you need to configure it in the Preferences -> Text editor command.