From d5a80357237612da52e2e50a760c3d95f67b17bf Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Fri, 1 Dec 2023 12:46:24 +0100 Subject: [PATCH] Fix cursor motion loop on misplaced zero-width non-joiner FIX: Fix an issue in the bidirectional motion that could cause the cursor to get stuck in a loop when a zero-width non-joiner char was placed on a direction boundary. Closes https://github.com/codemirror/dev/issues/1302 --- src/bidi.ts | 7 +++---- test/webtest-bidi.ts | 17 ++++++++++++++++- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/bidi.ts b/src/bidi.ts index 2167b6f..82ea8c3 100644 --- a/src/bidi.ts +++ b/src/bidi.ts @@ -62,9 +62,8 @@ function charType(ch: number) { 0x590 <= ch && ch <= 0x5f4 ? T.R : 0x600 <= ch && ch <= 0x6f9 ? ArabicTypes[ch - 0x600] : 0x6ee <= ch && ch <= 0x8ac ? T.AL : - 0x2000 <= ch && ch <= 0x200b ? T.NI : - 0xfb50 <= ch && ch <= 0xfdff ? T.AL : - ch == 0x200c ? T.NI : T.L + 0x2000 <= ch && ch <= 0x200c ? T.NI : + 0xfb50 <= ch && ch <= 0xfdff ? T.AL : T.L } const BidiRE = /[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac\ufb50-\ufdff]/ @@ -436,7 +435,7 @@ export function moveVisually(line: Line, order: readonly BidiSpan[], dir: Direct let nextIndex = findClusterBreak(line.text, startIndex, indexForward) movedOver = line.text.slice(Math.min(startIndex, nextIndex), Math.max(startIndex, nextIndex)) - if (nextIndex != span.side(forward, dir)) + if (nextIndex > span.from && nextIndex < span.to) return EditorSelection.cursor(nextIndex + line.from, indexForward ? -1 : 1, span.level) let nextSpan = spanI == (forward ? order.length - 1 : 0) ? null : order[spanI + (forward ? 1 : -1)] if (!nextSpan && span.level != dir) diff --git a/test/webtest-bidi.ts b/test/webtest-bidi.ts index 0ad920d..4d3fa5b 100644 --- a/test/webtest-bidi.ts +++ b/test/webtest-bidi.ts @@ -1,7 +1,7 @@ import {tempView} from "./tempview.js" import ist from "ist" import {__test, BidiSpan, Direction, Decoration, DecorationSet, EditorView} from "@codemirror/view" -import {Text, EditorSelection, Range, StateField, Extension} from "@codemirror/state" +import {Text, EditorSelection, SelectionRange, Range, StateField, Extension} from "@codemirror/state" function queryBrowserOrder(strings: readonly string[]) { let scratch = document.body.appendChild(document.createElement("div")) @@ -104,6 +104,21 @@ function tests(dir: Direction) { ist(__test.moveVisually(line, order, Direction.LTR, EditorSelection.cursor(points[i], 0, 0), false)!.from, points[i - 1]) } }) + + it("handles a misplaced non-joiner without going in a loop", () => { + let doc = "ءAB\u200cء", line = Text.of([doc]).line(1) + let order = __test.computeOrder(doc, Direction.RTL, []) + for (let pos: SelectionRange | null = EditorSelection.cursor(0), count = 0; count++;) { + ist(count, 6, "<") + pos = __test.moveVisually(line, order, Direction.RTL, pos, true) + if (!pos) break + } + for (let pos: SelectionRange | null = EditorSelection.cursor(doc.length), count = 0; count++;) { + ist(count, 6, "<") + pos = __test.moveVisually(line, order, Direction.RTL, pos, false) + if (!pos) break + } + }) }) }