diff --git a/ElectronClient/gui/NoteEditor/NoteBody/AceEditor/AceEditor.tsx b/ElectronClient/gui/NoteEditor/NoteBody/AceEditor/AceEditor.tsx index 6518f5365d..d46c376105 100644 --- a/ElectronClient/gui/NoteEditor/NoteBody/AceEditor/AceEditor.tsx +++ b/ElectronClient/gui/NoteEditor/NoteBody/AceEditor/AceEditor.tsx @@ -78,7 +78,6 @@ function AceEditor(props: NoteBodyEditorProps, ref: any) { const [renderedBody, setRenderedBody] = useState(defaultRenderedBody()); // Viewer content const [editor, setEditor] = useState(null); - const [lastKeys, setLastKeys] = useState([]); const [webviewReady, setWebviewReady] = useState(false); const previousRenderedBody = usePrevious(renderedBody); @@ -335,15 +334,6 @@ function AceEditor(props: NoteBodyEditorProps, ref: any) { wrapSelectionWithStrings('', '', resourceMds.join('\n')); }, [wrapSelectionWithStrings]); - const onEditorKeyDown = useCallback((event: any) => { - setLastKeys(prevLastKeys => { - const keys = prevLastKeys.slice(); - keys.push(event.key); - while (keys.length > 2) keys.splice(0, 1); - return keys; - }); - }, []); - const editorCutText = useCallback(() => { const text = selectedText(selectionRange(editor), props.content); if (!text) return; @@ -450,16 +440,12 @@ function AceEditor(props: NoteBodyEditorProps, ref: any) { } document.querySelector('#note-editor').addEventListener('paste', onEditorPaste, true); - document.querySelector('#note-editor').addEventListener('keydown', onEditorKeyDown); document.querySelector('#note-editor').addEventListener('contextmenu', onEditorContextMenu); // Disable Markdown auto-completion (eg. auto-adding a dash after a line with a dash. // https://github.com/ajaxorg/ace/issues/2754 // @ts-ignore: Keep the function signature as-is despite unusued arguments editor.getSession().getMode().getNextLineIndent = function(state: any, line: string) { - const ls = lastKeys; - if (ls.length >= 2 && ls[ls.length - 1] === 'Enter' && ls[ls.length - 2] === 'Enter') return this.$getIndent(line); - const leftSpaces = lineLeftSpaces(line); const lineNoLeftSpaces = line.trimLeft(); @@ -475,10 +461,9 @@ function AceEditor(props: NoteBodyEditorProps, ref: any) { return () => { document.querySelector('#note-editor').removeEventListener('paste', onEditorPaste, true); - document.querySelector('#note-editor').removeEventListener('keydown', onEditorKeyDown); document.querySelector('#note-editor').removeEventListener('contextmenu', onEditorContextMenu); }; - }, [editor, onEditorPaste, onEditorContextMenu, lastKeys]); + }, [editor, onEditorPaste, onEditorContextMenu]); useEffect(() => { // We disable dragging ot text because it's not really supported, and diff --git a/ElectronClient/gui/NoteEditor/NoteBody/AceEditor/utils/useListIdent.ts b/ElectronClient/gui/NoteEditor/NoteBody/AceEditor/utils/useListIdent.ts index 105e4ce153..a0496f7d36 100644 --- a/ElectronClient/gui/NoteEditor/NoteBody/AceEditor/utils/useListIdent.ts +++ b/ElectronClient/gui/NoteEditor/NoteBody/AceEditor/utils/useListIdent.ts @@ -1,15 +1,60 @@ import { useEffect } from 'react'; import { selectionRange } from './index'; +const markdownUtils = require('lib/markdownUtils'); -interface HookDependencies { - editor: any, +// The line that contains only `- ` is +// recognized as a heading in Ace. +function hyphenEmptyListItem(tokens: any[]) { + return ( + tokens.length === 2 && + tokens[0].type === 'markup.heading.2' && + tokens[0].value === '-' && + tokens[1].type === 'text.xml' && + tokens[1].value === ' ' + ); } -export default function useListIdent(dependencies:HookDependencies) { +// Returns tokens of the line if it starts with a 'markup.list' token. +function listTokens(editor: any, row: number) { + const tokens = editor.session.getTokens(row); + if ( + !(tokens.length > 0 && tokens[0].type === 'markup.list') && + !hyphenEmptyListItem(tokens) + ) { + return []; + } + return tokens; +} + +function countIndent(line: string): number { + return line.match(/\t| {4}/g)?.length || 0; +} + +// Finds the list item with indent level `prevIndent`. +function findPrevListNum(editor: any, row: number, indent: number) { + while (row > 0) { + row--; + const line = editor.session.getLine(row); + + if (countIndent(line) === indent) { + const num = markdownUtils.olLineNumber(line.trimLeft()); + if (num) { + return num; + } + } + } + return 0; +} + +interface HookDependencies { + editor: any; +} + +export default function useListIdent(dependencies: HookDependencies) { const { editor } = dependencies; useEffect(() => { - if (!editor) return; + if (!editor) return () => {}; // Markdown list indentation. (https://github.com/laurent22/joplin/pull/2713) // If the current line starts with `markup.list` token, @@ -20,13 +65,19 @@ export default function useListIdent(dependencies:HookDependencies) { const range = selectionRange(editor); if (range.isEmpty()) { const row = range.start.row; - const tokens = this.session.getTokens(row); + const tokens = listTokens(this, row); - if (tokens.length > 0 && tokens[0].type == 'markup.list') { + if (tokens.length > 0) { if (tokens[0].value.search(/\d+\./) != -1) { - // Resets numbered list to 1. - this.session.replace({ start: { row, column: 0 }, end: { row, column: tokens[0].value.length } }, - tokens[0].value.replace(/\d+\./, '1.')); + const line = this.session.getLine(row); + const n = findPrevListNum(this, row, countIndent(line) + 1) + 1; + this.session.replace( + { + start: { row, column: 0 }, + end: { row, column: tokens[0].value.length }, + }, + tokens[0].value.replace(/\d+\./, `${n}.`) + ); } this.session.indentRows(row, row, '\t'); @@ -36,5 +87,92 @@ export default function useListIdent(dependencies:HookDependencies) { if (originalEditorIndent) originalEditorIndent.call(this); }; + + // Correct the number of numbered list item when outdenting. + editor.commands.addCommand({ + name: 'markdownOutdent', + bindKey: { win: 'Shift+Tab', mac: 'Shift+Tab' }, + multiSelectAction: 'forEachLine', + exec: function(editor: any) { + const range = selectionRange(editor); + + if (range.isEmpty()) { + const row = range.start.row; + + const tokens = editor.session.getTokens(row); + if (tokens.length && tokens[0].type === 'markup.list') { + const matches = tokens[0].value.match(/^(\t+)\d+\./); + if (matches && matches.length) { + const indent = countIndent(matches[1]); + const n = findPrevListNum(editor, row, indent - 1) + 1; + console.log(n); + editor.session.replace( + { + start: { row, column: 0 }, + end: { row, column: tokens[0].value.length }, + }, + tokens[0].value.replace(/\d+\./, `${n}.`) + ); + } + } + } + + editor.blockOutdent(); + }, + readonly: false, + }); + + // Delete a list markup (e.g. `- `) from an empty list item on hitting Enter. + // (https://github.com/laurent22/joplin/pull/2772) + editor.commands.addCommand({ + name: 'markdownEnter', + bindKey: 'Enter', + multiSelectAction: 'forEach', + exec: function(editor: any) { + const range = editor.getSelectionRange(); + const tokens = listTokens(editor, range.start.row); + + const emptyListItem = + tokens.length === 1 || hyphenEmptyListItem(tokens); + const emptyCheckboxItem = + tokens.length === 3 && + ['[ ]', '[x]'].includes(tokens[1].value) && + tokens[2].value === ' '; + + if (!range.isEmpty() || !(emptyListItem || emptyCheckboxItem)) { + editor.insert('\n'); + // Cursor can go out of the view after inserting '\n'. + editor.renderer.scrollCursorIntoView(); + return; + } + + const row = range.start.row; + const line = editor.session.getLine(row); + let indent = editor + .getSession() + .getMode() + .getNextLineIndent(null, line); + if (indent.startsWith('\t')) { + indent = indent.slice(1); + } else { + indent = ''; + } + + editor.session.replace( + { + start: { row, column: 0 }, + end: { row, column: line.length }, + }, + indent + ); + }, + readOnly: false, + }); + + return () => { + editor.indent = originalEditorIndent; + editor.commands.removeCommand('markdownOutdent'); + editor.commands.removeCommand('markdownEnter'); + }; }, [editor]); }