From 815a0a5db4f550a2476f9d20992587624d21a1d8 Mon Sep 17 00:00:00 2001 From: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com> Date: Wed, 13 Dec 2023 11:45:17 -0800 Subject: [PATCH 1/2] Mobile: Fixes #9477: Fix inline code at beginning of line in table breaks formatting (#9478) --- .eslintignore | 1 + .gitignore | 1 + .../markdown/decoratorExtension.test.ts | 30 +++++++++++++++++++ .../CodeMirror/markdown/decoratorExtension.ts | 21 +++++++++---- .../CodeMirror/testUtil/createTestEditor.ts | 8 +++-- 5 files changed, 54 insertions(+), 7 deletions(-) create mode 100644 packages/editor/CodeMirror/markdown/decoratorExtension.test.ts diff --git a/.eslintignore b/.eslintignore index 170ff2c62..df145bf59 100644 --- a/.eslintignore +++ b/.eslintignore @@ -542,6 +542,7 @@ packages/editor/CodeMirror/editorCommands/swapLine.js packages/editor/CodeMirror/getScrollFraction.js packages/editor/CodeMirror/markdown/computeSelectionFormatting.test.js packages/editor/CodeMirror/markdown/computeSelectionFormatting.js +packages/editor/CodeMirror/markdown/decoratorExtension.test.js packages/editor/CodeMirror/markdown/decoratorExtension.js packages/editor/CodeMirror/markdown/markdownCommands.bulletedVsChecklist.test.js packages/editor/CodeMirror/markdown/markdownCommands.test.js diff --git a/.gitignore b/.gitignore index 24e029020..0fb6527e2 100644 --- a/.gitignore +++ b/.gitignore @@ -524,6 +524,7 @@ packages/editor/CodeMirror/editorCommands/swapLine.js packages/editor/CodeMirror/getScrollFraction.js packages/editor/CodeMirror/markdown/computeSelectionFormatting.test.js packages/editor/CodeMirror/markdown/computeSelectionFormatting.js +packages/editor/CodeMirror/markdown/decoratorExtension.test.js packages/editor/CodeMirror/markdown/decoratorExtension.js packages/editor/CodeMirror/markdown/markdownCommands.bulletedVsChecklist.test.js packages/editor/CodeMirror/markdown/markdownCommands.test.js diff --git a/packages/editor/CodeMirror/markdown/decoratorExtension.test.ts b/packages/editor/CodeMirror/markdown/decoratorExtension.test.ts new file mode 100644 index 000000000..754292074 --- /dev/null +++ b/packages/editor/CodeMirror/markdown/decoratorExtension.test.ts @@ -0,0 +1,30 @@ +import { EditorSelection } from '@codemirror/state'; +import createTestEditor from '../testUtil/createTestEditor'; +import decoratorExtension from './decoratorExtension'; + +jest.retryTimes(2); + +describe('decoratorExtension', () => { + it('should highlight code blocks within tables', async () => { + // Regression test for https://github.com/laurent22/joplin/issues/9477 + const editorText = ` +left | right +--------|------- +\`foo\` | bar + `; + const editor = await createTestEditor( + editorText, + + // Put the initial cursor at the start of "foo" + EditorSelection.cursor(editorText.indexOf('foo')), + + ['TableRow', 'InlineCode'], + [decoratorExtension], + ); + + const codeBlock = editor.contentDOM.querySelector('.cm-inlineCode'); + + expect(codeBlock.textContent).toBe('`foo`'); + expect(codeBlock.parentElement.classList.contains('.cm-tableRow')); + }); +}); diff --git a/packages/editor/CodeMirror/markdown/decoratorExtension.ts b/packages/editor/CodeMirror/markdown/decoratorExtension.ts index 558de6dab..ae4433f5a 100644 --- a/packages/editor/CodeMirror/markdown/decoratorExtension.ts +++ b/packages/editor/CodeMirror/markdown/decoratorExtension.ts @@ -72,7 +72,7 @@ const taskMarkerDecoration = Decoration.mark({ attributes: { class: 'cm-taskMarker' }, }); -type DecorationDescription = { pos: number; length?: number; decoration: Decoration }; +type DecorationDescription = { pos: number; length: number; decoration: Decoration }; // Returns a set of [Decoration]s, associated with block syntax groups that require // full-line styling. @@ -87,6 +87,7 @@ const computeDecorations = (view: EditorView) => { const line = view.state.doc.lineAt(pos); decorations.push({ pos: line.from, + length: 0, decoration, }); @@ -185,13 +186,23 @@ const computeDecorations = (view: EditorView) => { }); } - decorations.sort((a, b) => a.pos - b.pos); + // Decorations need to be sorted in ascending order first by start position, + // then by length. Adding items to the RangeSetBuilder in an incorrect order + // causes an exception to be thrown. + decorations.sort((a, b) => { + const posComparison = a.pos - b.pos; + if (posComparison !== 0) { + return posComparison; + } + + const lengthComparison = a.length - b.length; + return lengthComparison; + }); - // Items need to be added to a RangeSetBuilder in ascending order const decorationBuilder = new RangeSetBuilder(); for (const { pos, length, decoration } of decorations) { - // Null length => entire line - decorationBuilder.add(pos, pos + (length ?? 0), decoration); + // Zero length => entire line + decorationBuilder.add(pos, pos + length, decoration); } return decorationBuilder.finish(); }; diff --git a/packages/editor/CodeMirror/testUtil/createTestEditor.ts b/packages/editor/CodeMirror/testUtil/createTestEditor.ts index c3917c893..543c9fcfb 100644 --- a/packages/editor/CodeMirror/testUtil/createTestEditor.ts +++ b/packages/editor/CodeMirror/testUtil/createTestEditor.ts @@ -1,7 +1,7 @@ import { markdown } from '@codemirror/lang-markdown'; import { GFM as GithubFlavoredMarkdownExt } from '@lezer/markdown'; import { indentUnit, syntaxTree } from '@codemirror/language'; -import { SelectionRange, EditorSelection, EditorState } from '@codemirror/state'; +import { SelectionRange, EditorSelection, EditorState, Extension } from '@codemirror/state'; import { EditorView } from '@codemirror/view'; import { MarkdownMathExtension } from '../markdown/markdownMathParser'; import forceFullParse from './forceFullParse'; @@ -10,7 +10,10 @@ import loadLangauges from './loadLanguages'; // Creates and returns a minimal editor with markdown extensions. Waits to return the editor // until all syntax tree tags in `expectedSyntaxTreeTags` exist. const createTestEditor = async ( - initialText: string, initialSelection: SelectionRange, expectedSyntaxTreeTags: string[], + initialText: string, + initialSelection: SelectionRange, + expectedSyntaxTreeTags: string[], + extraExtensions: Extension[] = [], ): Promise => { await loadLangauges(); @@ -23,6 +26,7 @@ const createTestEditor = async ( }), indentUnit.of('\t'), EditorState.tabSize.of(4), + extraExtensions, ], }); From d4157e14fe3929012a57a3a60b28f79ac6afccf4 Mon Sep 17 00:00:00 2001 From: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com> Date: Sun, 17 Dec 2023 12:58:22 -0800 Subject: [PATCH 2/2] Mobile: Fixes #9532: Fix cursor location on opening the editor and attachments inserted in wrong location (#9536) --- .../components/NoteEditor/NoteEditor.tsx | 2 ++ .../app-mobile/components/screens/Note.tsx | 24 ++++++++++--------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/packages/app-mobile/components/NoteEditor/NoteEditor.tsx b/packages/app-mobile/components/NoteEditor/NoteEditor.tsx index a6a217401..0a2188702 100644 --- a/packages/app-mobile/components/NoteEditor/NoteEditor.tsx +++ b/packages/app-mobile/components/NoteEditor/NoteEditor.tsx @@ -270,6 +270,7 @@ function NoteEditor(props: Props, ref: any) { const setInitialSelectionJS = props.initialSelection ? ` cm.select(${props.initialSelection.start}, ${props.initialSelection.end}); + cm.execCommand('scrollSelectionIntoView'); ` : ''; const editorSettings: EditorSettings = { @@ -331,6 +332,7 @@ function NoteEditor(props: Props, ref: any) { const settings = ${JSON.stringify(editorSettings)}; cm = codeMirrorBundle.initCodeMirror(parentElement, initialText, settings); + ${setInitialSelectionJS} window.onresize = () => { diff --git a/packages/app-mobile/components/screens/Note.tsx b/packages/app-mobile/components/screens/Note.tsx index 61061a4be..5bc21ab33 100644 --- a/packages/app-mobile/components/screens/Note.tsx +++ b/packages/app-mobile/components/screens/Note.tsx @@ -10,6 +10,7 @@ import NoteEditor from '../NoteEditor/NoteEditor'; const FileViewer = require('react-native-file-viewer').default; const React = require('react'); const { Keyboard, View, TextInput, StyleSheet, Linking, Image, Share } = require('react-native'); +import type { NativeSyntheticEvent } from 'react-native'; import { Platform, PermissionsAndroid } from 'react-native'; const { connect } = require('react-redux'); // const { MarkdownEditor } = require('@joplin/lib/../MarkdownEditor/index.js'); @@ -50,8 +51,9 @@ import isEditableResource from '../NoteEditor/ImageEditor/isEditableResource'; import VoiceTypingDialog from '../voiceTyping/VoiceTypingDialog'; import { voskEnabled } from '../../services/voiceTyping/vosk'; import { isSupportedLanguage } from '../../services/voiceTyping/vosk.android'; -import { ChangeEvent as EditorChangeEvent, UndoRedoDepthChangeEvent } from '@joplin/editor/events'; +import { ChangeEvent as EditorChangeEvent, SelectionRangeChangeEvent, UndoRedoDepthChangeEvent } from '@joplin/editor/events'; import { join } from 'path'; +import { SelectionRange } from '../NoteEditor/types'; const urlUtils = require('@joplin/lib/urlUtils'); // import Vosk from 'react-native-vosk'; @@ -64,6 +66,7 @@ class NoteScreenComponent extends BaseScreenComponent { // This isn't in this.state because we don't want changing scroll to trigger // a re-render. private lastBodyScroll: number|undefined = undefined; + private selection: SelectionRange; public static navigationOptions(): any { return { header: null }; @@ -251,7 +254,6 @@ class NoteScreenComponent extends BaseScreenComponent { this.undoRedoService_stackChange = this.undoRedoService_stackChange.bind(this); this.screenHeader_undoButtonPress = this.screenHeader_undoButtonPress.bind(this); this.screenHeader_redoButtonPress = this.screenHeader_redoButtonPress.bind(this); - this.body_selectionChange = this.body_selectionChange.bind(this); this.onBodyViewerLoadEnd = this.onBodyViewerLoadEnd.bind(this); this.onBodyViewerCheckboxChange = this.onBodyViewerCheckboxChange.bind(this); this.onBodyChange = this.onBodyChange.bind(this); @@ -520,13 +522,13 @@ class NoteScreenComponent extends BaseScreenComponent { this.scheduleSave(); } - private body_selectionChange(event: any) { - if (this.useEditorBeta()) { - this.selection = event.selection; - } else { - this.selection = event.nativeEvent.selection; - } - } + private onPlainEdtiorSelectionChange = (event: NativeSyntheticEvent) => { + this.selection = event.nativeEvent.selection; + }; + + private onMarkdownEditorSelectionChange = (event: SelectionRangeChangeEvent) => { + this.selection = { start: event.from, end: event.to }; + }; public makeSaveAction() { return async () => { @@ -1392,7 +1394,7 @@ class NoteScreenComponent extends BaseScreenComponent { multiline={true} value={note.body} onChangeText={(text: string) => this.body_changeText(text)} - onSelectionChange={this.body_selectionChange} + onSelectionChange={this.onPlainEdtiorSelectionChange} blurOnSubmit={false} selectionColor={theme.textSelectionColor} keyboardAppearance={theme.keyboardAppearance} @@ -1413,7 +1415,7 @@ class NoteScreenComponent extends BaseScreenComponent { initialText={note.body} initialSelection={this.selection} onChange={this.onBodyChange} - onSelectionChange={this.body_selectionChange} + onSelectionChange={this.onMarkdownEditorSelectionChange} onUndoRedoDepthChange={this.onUndoRedoDepthChange} onAttach={() => this.showAttachMenu()} readOnly={this.state.readOnly}