diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.tsx b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.tsx index 4cdf7f1bec..dee3aac3cb 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.tsx +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.tsx @@ -22,6 +22,7 @@ import MenuUtils from '@joplin/lib/services/commands/MenuUtils'; import CommandService from '@joplin/lib/services/CommandService'; import { themeStyle } from '@joplin/lib/theme'; import { ThemeAppearance } from '@joplin/lib/themes/type'; +import SpellCheckerService from '@joplin/lib/services/spellChecker/SpellCheckerService'; import dialogs from '../../../dialogs'; const Note = require('@joplin/lib/models/Note.js'); @@ -255,54 +256,6 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) { } }, []); - const onEditorContextMenu = useCallback(() => { - const menu = new Menu(); - - const hasSelectedText = editorRef.current && !!editorRef.current.getSelection() ; - const clipboardText = clipboard.readText(); - - menu.append( - new MenuItem({ - label: _('Cut'), - enabled: hasSelectedText, - click: async () => { - editorCutText(); - }, - }) - ); - - menu.append( - new MenuItem({ - label: _('Copy'), - enabled: hasSelectedText, - click: async () => { - editorCopyText(); - }, - }) - ); - - menu.append( - new MenuItem({ - label: _('Paste'), - enabled: true, - click: async () => { - if (clipboardText) { - editorPasteText(); - } else { - // To handle pasting images - onEditorPaste(); - } - }, - }) - ); - - menuUtils.pluginContextMenuItems(props.plugins, MenuItemLocation.EditorContextMenu).forEach((item: any) => { - menu.append(new MenuItem(item)); - }); - - menu.popup(bridge().window()); - }, [props.content, editorCutText, editorPasteText, editorCopyText, onEditorPaste, props.plugins]); - const loadScript = async (script: any) => { return new Promise((resolve) => { let element: any = document.createElement('script'); @@ -409,6 +362,12 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) { padding-bottom: 400px !important; } + .CodeMirror-sizer { + /* Add a fixed right padding to account for the appearance (and disappearance) */ + /* of the sidebar */ + padding-right: 10px !important; + } + .cm-header-1 { font-size: 1.5em; } @@ -623,6 +582,91 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) { editorRef.current.refresh(); }, [rootSize, styles.editor, props.visiblePanes]); + // The below code adds support for spellchecking when it is enabled + // It might be buggy, refer to the below issue + // https://github.com/laurent22/joplin/pull/3974#issuecomment-718936703 + useEffect(() => { + function pointerInsideEditor(x: number, y: number) { + const elements = document.getElementsByClassName('codeMirrorEditor'); + if (!elements.length) return null; + const rect = elements[0].getBoundingClientRect(); + return rect.x < x && rect.y < y && rect.right > x && rect.bottom > y; + } + + function onContextMenu(_event: any, params: any) { + if (!pointerInsideEditor(params.x, params.y)) return; + + const menu = new Menu(); + + const hasSelectedText = editorRef.current && !!editorRef.current.getSelection() ; + const clipboardText = clipboard.readText(); + + menu.append( + new MenuItem({ + label: _('Cut'), + enabled: hasSelectedText, + click: async () => { + editorCutText(); + }, + }) + ); + + menu.append( + new MenuItem({ + label: _('Copy'), + enabled: hasSelectedText, + click: async () => { + editorCopyText(); + }, + }) + ); + + menu.append( + new MenuItem({ + label: _('Paste'), + enabled: true, + click: async () => { + if (clipboardText) { + editorPasteText(); + } else { + // To handle pasting images + onEditorPaste(); + } + }, + }) + ); + + const spellCheckerMenuItems = SpellCheckerService.instance().contextMenuItems(params.misspelledWord, params.dictionarySuggestions); + + for (const item of spellCheckerMenuItems) { + menu.append(new MenuItem(item)); + } + + // Typically CodeMirror handles all interactions itself (highlighting etc.) + // But in the case of clicking a mispelled word, we need electron to handle the click + // The result is that CodeMirror doesn't know what's been selected and doesn't + // move the cursor into the correct location. + // and when the user selects a new spelling it will be inserted in the wrong location + // So in this situation, we use must manually align the internal codemirror selection + // to the contextmenu selection + if (editorRef.current && spellCheckerMenuItems.length > 0) { + editorRef.current.alignSelection(params); + } + + menuUtils.pluginContextMenuItems(props.plugins, MenuItemLocation.EditorContextMenu).forEach((item: any) => { + menu.append(new MenuItem(item)); + }); + + menu.popup(); + } + + bridge().window().webContents.on('context-menu', onContextMenu); + + return () => { + bridge().window().webContents.off('context-menu', onContextMenu); + }; + }, []); + function renderEditor() { return ( @@ -640,7 +684,6 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) { plugins={props.plugins} onChange={codeMirror_change} onScroll={editor_scroll} - onEditorContextMenu={onEditorContextMenu} onEditorPaste={onEditorPaste} /> diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/Editor.tsx b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/Editor.tsx index a29b520b12..46834ef72f 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/Editor.tsx +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/Editor.tsx @@ -27,6 +27,8 @@ import 'codemirror/keymap/sublime'; // Used for swapLineUp and swapLineDown import 'codemirror/mode/meta'; +import Setting from '@joplin/lib/models/Setting'; + // import eventManager from '@joplin/lib/eventManager'; const { reg } = require('@joplin/lib/registry.js'); @@ -88,7 +90,6 @@ export interface EditorProps { plugins: PluginStates; onChange: any; onScroll: any; - onEditorContextMenu: any; onEditorPaste: any; } @@ -122,13 +123,6 @@ function Editor(props: EditorProps, ref: any) { props.onScroll(); }, [props.onScroll]); - // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars - const editor_mousedown = useCallback((_cm: any, event: any) => { - if (event && event.button === 2) { - props.onEditorContextMenu(); - } - }, [props.onEditorContextMenu]); - // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars const editor_paste = useCallback((_cm: any, _event: any) => { props.onEditorPaste(); @@ -163,7 +157,7 @@ function Editor(props: EditorProps, ref: any) { mode: props.mode, readOnly: props.readOnly, autoCloseBrackets: props.autoMatchBraces, - inputStyle: 'textarea', // contenteditable loses cursor position on focus change, use textarea instead + inputStyle: Setting.value('editor.spellcheckBeta') ? 'contenteditable' : 'textarea', lineWrapping: true, lineNumbers: false, indentWithTabs: true, @@ -177,7 +171,6 @@ function Editor(props: EditorProps, ref: any) { setEditor(cm); cm.on('change', editor_change); cm.on('scroll', editor_scroll); - cm.on('mousedown', editor_mousedown); cm.on('paste', editor_paste); cm.on('drop', editor_drop); cm.on('dragover', editor_drag); @@ -191,7 +184,6 @@ function Editor(props: EditorProps, ref: any) { // Clean up codemirror cm.off('change', editor_change); cm.off('scroll', editor_scroll); - cm.off('mousedown', editor_mousedown); cm.off('paste', editor_paste); cm.off('drop', editor_drop); cm.off('dragover', editor_drag); @@ -250,7 +242,7 @@ function Editor(props: EditorProps, ref: any) { } }, [pluginOptions, editor]); - return
; + return ; } export default forwardRef(Editor); diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useCursorUtils.ts b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useCursorUtils.ts index 2a5dca652b..395e2f92e9 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useCursorUtils.ts +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useCursorUtils.ts @@ -100,5 +100,33 @@ export default function useCursorUtils(CodeMirror: any) { }); }); + // params are the oncontextmenu params + CodeMirror.defineExtension('alignSelection', function(params: any) { + // The below is a HACK that uses the selectionText from electron and the coordinates of + // the click to determine what the codemirror selection should be + const alignStrings = (s1: string, s2: string) => { + for (let i = 0; i < s1.length; i++) { + if (s1.substr(i, s2.length) === s2) { return i; } + } + return -1; + }; + + const selectionText = params.selectionText; + const coords = this.coordsChar({ left: params.x, top: params.y }); + const { anchor, head } = this.findWordAt(coords); + const selectedWord = this.getRange(anchor, head); + + if (selectionText.length > selectedWord.length) { + const offset = alignStrings(selectionText, selectedWord); + anchor.ch -= offset; + head.ch = anchor.ch + selectionText.length; + } else if (selectionText.length < selectedWord.length) { + const offset = alignStrings(selectedWord, selectionText); + anchor.ch += offset; + head.ch = anchor.ch + selectionText.length; + } + + this.setSelection(anchor, head); + }); } diff --git a/packages/lib/models/Setting.ts b/packages/lib/models/Setting.ts index 58f113ced8..6ea9f96d65 100644 --- a/packages/lib/models/Setting.ts +++ b/packages/lib/models/Setting.ts @@ -715,7 +715,7 @@ class Setting extends BaseModel { description: () => 'CSS file support is provided for your convenience, but they are advanced settings, and styles you define may break from one version to the next. If you want to use them, please know that it might require regular development work from you to keep them working. The Joplin team cannot make a commitment to keep the application HTML structure stable.', }, - autoUpdateEnabled: { value: false, type: SettingItemType.Bool, section: 'application', public: true, appTypes: ['desktop'], label: () => _('Automatically update the application') }, + autoUpdateEnabled: { value: false, type: SettingItemType.Bool, section: 'application', public: platform !== 'linux', appTypes: ['desktop'], label: () => _('Automatically update the application') }, 'autoUpdate.includePreReleases': { value: false, type: SettingItemType.Bool, section: 'application', public: true, appTypes: ['desktop'], label: () => _('Get pre-releases when checking for updates'), description: () => _('See the pre-release page for more details: %s', 'https://joplinapp.org/prereleases') }, 'clipperServer.autoStart': { value: false, type: SettingItemType.Bool, public: false }, 'sync.interval': { @@ -775,6 +775,16 @@ class Setting extends BaseModel { }, }, + 'editor.spellcheckBeta': { + value: false, + type: SettingItemType.Bool, + public: true, + appTypes: ['desktop'], + advanced: true, + label: () => 'Enable spell checking in Markdown editor? (WARNING BETA feature)', + description: () => 'Spell checker in the Markdown editor was previously unstable (cursor location was not stable, sometimes edits would not be saved or reflected in the viewer, etc.) however it appears to be more reliable now. If you notice any issue, please report it on GitHub or the Joplin Forum (Help -> Joplin Forum)', + }, + 'net.customCertificates': { value: '', type: SettingItemType.String,