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] 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<Decoration>();
 	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<EditorView> => {
 	await loadLangauges();
 
@@ -23,6 +26,7 @@ const createTestEditor = async (
 			}),
 			indentUnit.of('\t'),
 			EditorState.tabSize.of(4),
+			extraExtensions,
 		],
 	});