Browse Source

Fix wrong cursor posistion (#10838)

* fix unable to use 'del' to delete the last character
if the cursor is at the beginning of the block

* fix right arrow calculate character length incorrect
if the cursor is at the beginning of the block

* fix moving up or down across blocks may enter the middle of a character

* fix moving up or down inside a block may enter the middle of a character
megayu 1 year ago
parent
commit
c25c432755

+ 4 - 4
src/main/frontend/components/editor.cljs

@@ -652,10 +652,10 @@
             :visibility "hidden"
             :top 0
             :left 0}}
-   (let [content (str content "0")]
-     (for [[idx c] (map-indexed
-                    vector
-                    (string/split content ""))]
+   (let [content (str content "0")
+         graphemes (util/split-graphemes content)
+         graphemes-char-index (reductions #(+ %1 (count %2)) 0 graphemes)]
+     (for [[idx c] (zipmap graphemes-char-index graphemes)]
        (if (= c "\n")
          [:span {:id (str "mock-text_" idx)
                  :key idx} "0" [:br]]

+ 1 - 13
src/main/frontend/handler/block.cljs

@@ -306,18 +306,6 @@
                   :own-order-list-index own-order-list-index
                   :own-order-number-list? (= own-order-list-type "number"))))
 
-(defn- text-range-by-lst-fst-line [content [direction pos]]
-  (case direction
-    :up
-    (let [last-new-line (or (string/last-index-of content \newline) -1)
-          end (+ last-new-line pos 1)]
-      (subs content 0 end))
-    :down
-    (-> (string/split-lines content)
-        first
-        (or "")
-        (subs 0 pos))))
-
 (defn mark-last-input-time!
   [repo]
   (when repo
@@ -394,7 +382,7 @@
             content-length (count content)
             text-range (cond
                          (vector? pos)
-                         (text-range-by-lst-fst-line content pos)
+                         (util/get-text-range content (last pos) (= (first pos) :down))
 
                          (and (> tail-len 0) (>= (count content) tail-len))
                          (subs content 0 (- (count content) tail-len))

+ 1 - 1
src/main/frontend/handler/editor.cljs

@@ -2586,7 +2586,7 @@
 (defn- move-cross-boundary-up-down
   [direction move-opts]
   (let [input (state/get-input)
-        line-pos (util/get-first-or-last-line-pos input)
+        line-pos (util/get-line-pos (.-value input) (util/get-selection-start input))
         repo (state/get-current-repo)
         f (case direction
             :up util/get-prev-block-non-collapsed

+ 51 - 11
src/main/frontend/util.cljc

@@ -414,12 +414,49 @@
   (when input
     (.-selectionDirection input)))
 
-(defn get-first-or-last-line-pos
-  [input]
-  (let [pos (get-selection-start input)
-        value (.-value input)
-        last-newline-pos (or (string/last-index-of value \newline (dec pos)) -1)]
-    (- pos last-newline-pos 1)))
+#?(:cljs
+   (defn split-graphemes
+     [s]
+     (let [^js splitter (GraphemeSplitter.)]
+       (.splitGraphemes splitter s))))
+
+#?(:cljs
+   (defn get-graphemes-pos
+     "Return the length of the substrings in s between start and from-index.
+
+      multi-char count as 1, like emoji characters"
+     [s from-index]
+     (let [^js splitter (GraphemeSplitter.)]
+       (.countGraphemes splitter (subs s 0 from-index)))))
+
+#?(:cljs
+   (defn get-line-pos
+     "Return the length of the substrings in s between the last index of newline
+      in s searching backward from from-newline-index and from-newline-index.
+
+      multi-char count as 1, like emoji characters"
+     [s from-newline-index]
+     (let [^js splitter (GraphemeSplitter.)
+           last-newline-pos (string/last-index-of s \newline (dec from-newline-index))
+           before-last-newline-length (or last-newline-pos -1)
+           last-newline-content (subs s (inc before-last-newline-length) from-newline-index)]
+       (.countGraphemes splitter last-newline-content))))
+
+#?(:cljs
+   (defn get-text-range
+     "Return the substring of the first grapheme-num characters of s if first-line? is true,
+      otherwise return the substring of s before the last \n and the first grapheme-num characters.
+
+      grapheme-num treats multi-char as 1, like emoji characters"
+     [s grapheme-num first-line?]
+     (let [newline-pos (if first-line?
+                         0
+                         (inc (or (string/last-index-of s \newline) -1)))
+           ^js splitter (GraphemeSplitter.)
+           ^js newline-graphemes (.splitGraphemes splitter (subs s newline-pos))
+           ^js newline-graphemes (.slice newline-graphemes 0 grapheme-num)
+           content (.join newline-graphemes "")]
+       (subs s 0 (+ newline-pos (count content))))))
 
 #?(:cljs
    (defn stop [e]
@@ -684,7 +721,7 @@
    (defn safe-dec-current-pos-from-end
      [input current-pos]
      (if-let [len (and (string? input) (.-length input))]
-       (when-let [input (and (>= len 2) (<= current-pos len)
+       (if-let [input (and (>= len 2) (<= current-pos len)
                              (.substring input (max (- current-pos 20) 0) current-pos))]
          (try
            (let [^js splitter (GraphemeSplitter.)
@@ -692,7 +729,8 @@
              (- current-pos (.-length (.pop input))))
            (catch :default e
              (js/console.error e)
-             (dec current-pos))))
+             (dec current-pos)))
+         (dec current-pos))
        (dec current-pos))))
 
 #?(:cljs
@@ -700,7 +738,7 @@
    (defn safe-inc-current-pos-from-start
      [input current-pos]
      (if-let [len (and (string? input) (.-length input))]
-       (when-let [input (and (>= len 2) (<= current-pos len)
+       (if-let [input (and (>= len 2) (<= current-pos len)
                              (.substr input current-pos 20))]
          (try
            (let [^js splitter (GraphemeSplitter.)
@@ -708,7 +746,8 @@
              (+ current-pos (.-length (.shift input))))
            (catch :default e
              (js/console.error e)
-             (inc current-pos))))
+             (inc current-pos)))
+         (inc current-pos))
        (inc current-pos))))
 
 #?(:cljs
@@ -1334,10 +1373,11 @@
              scroll-top  (.-scrollTop main-node)
 
              current-pos (get-selection-start el)
+             grapheme-pos (get-graphemes-pos (.-value (.textContent el)) current-pos)
              mock-text   (some-> (gdom/getElement "mock-text")
                                  gdom/getChildren
                                  array-seq
-                                 (nth-safe current-pos))
+                                 (nth-safe grapheme-pos))
              offset-top   (and mock-text (.-offsetTop mock-text))
              offset-height (and mock-text (.-offsetHeight mock-text))
 

+ 6 - 7
src/main/frontend/util/cursor.cljs

@@ -31,12 +31,13 @@
   ([input] (get-caret-pos input (util/get-selection-start input)))
   ([input pos]
    (when input
-     (let [rect (bean/->clj (.. input (getBoundingClientRect) (toJSON)))]
+     (let [rect (bean/->clj (.. input (getBoundingClientRect) (toJSON)))
+           grapheme-pos (util/get-graphemes-pos (.-value input) pos)]
        (try
          (some-> (gdom/getElement "mock-text")
                  gdom/getChildren
                  array-seq
-                 (util/nth-safe pos)
+                 (util/nth-safe grapheme-pos)
                  mock-char-pos
                  (assoc :rect rect))
          (catch :default e
@@ -71,9 +72,8 @@
   ([input n]
    (when input
      (let [{:keys [pos]} (get-caret-pos input)
-           pos (if (and (= n 1) (not (zero? pos)))
-                 (or (util/safe-inc-current-pos-from-start (.-value input) pos)
-                     (inc pos))
+           pos (if (= n 1)
+                 (util/safe-inc-current-pos-from-start (.-value input) pos)
                  (+ pos n))]
        (move-cursor-to input pos)))))
 
@@ -85,8 +85,7 @@
      (let [{:keys [pos]} (get-caret-pos input)
            pos (if (= n 1)
                  (util/safe-dec-current-pos-from-end (.-value input) pos)
-                 (- pos n))
-           pos (max 0 (or pos (dec pos)))]
+                 (- pos n))]
        (move-cursor-to input pos)))))
 
 (defn- get-input-content&pos

+ 33 - 0
src/test/frontend/util_test.cljs

@@ -13,6 +13,7 @@
     (is (= 3 (util/safe-dec-current-pos-from-end "abc😀d" 5)))
     (is (= 3 (util/safe-dec-current-pos-from-end "abc😀" 5)))
     (is (= 0 (util/safe-dec-current-pos-from-end "😀" 2)))
+    (is (= 0 (util/safe-dec-current-pos-from-end "a" 1)))
     (is (= 4 (util/safe-dec-current-pos-from-end "abcde" 5)))
     (is (= 1 (util/safe-dec-current-pos-from-end "中文" 2))))
 
@@ -20,8 +21,40 @@
     (is (= 5 (util/safe-inc-current-pos-from-start "abc😀d" 3)))
     (is (= 2 (util/safe-inc-current-pos-from-start "😀" 0)))
     (is (= 2 (util/safe-inc-current-pos-from-start "abcde" 1)))
+    (is (= 1 (util/safe-inc-current-pos-from-start "a" 0)))
     (is (= 1 (util/safe-inc-current-pos-from-start "中文" 0)))))
 
+(deftest test-get-line-pos
+  (testing "get-line-pos"
+    (is (= 3 (util/get-line-pos "abcde" 3)))
+    (is (= 4 (util/get-line-pos "abcd\ne" 4)))
+    (is (= 0 (util/get-line-pos "abcd\ne" 5)))
+    (is (= 4 (util/get-line-pos "abc😀d" 5)))
+    (is (= 1 (util/get-line-pos "abc\nde" 5)))
+    (is (= 1 (util/get-line-pos "abc\n😀d" 6)))
+    (is (= 2 (util/get-line-pos "ab\nc😀d" 6)))
+    (is (= 1 (util/get-line-pos "abc\nde\nf" 5)))
+    (is (= 1 (util/get-line-pos "abc\n😀d\ne" 6)))
+    (is (= 2 (util/get-line-pos "ab\nc😀d\ne" 6)))))
+
+(deftest test-get-text-range
+  (testing "get-text-range"
+    (is (= "" (util/get-text-range "abcdefg" 0 true)))
+    (is (= "" (util/get-text-range "abcdefg" 0 false)))
+    (is (= "abcdefg" (util/get-text-range "abcdefg" 10 true)))
+    (is (= "abcdefg" (util/get-text-range "abcdefg" 10 false)))
+    (is (= "abc" (util/get-text-range "abcdefg" 3 true)))
+    (is (= "abc" (util/get-text-range "abcdefg" 3 false)))
+    (is (= "abc" (util/get-text-range "abcdefg\nhijklmn" 3 true)))
+    (is (= "abcdefg\nhij" (util/get-text-range "abcdefg\nhijklmn" 3 false)))
+    (is (= "abcdefg\nhijklmn" (util/get-text-range "abcdefg\nhijklmn" 10 false)))
+    (is (= "abcdefg\nhijklmn\nopq" (util/get-text-range "abcdefg\nhijklmn\nopqrst" 3 false)))
+    (is (= "a😀b" (util/get-text-range "a😀bcdefg" 3 true)))
+    (is (= "a😀b" (util/get-text-range "a😀bcdefg" 3 false)))
+    (is (= "a😀b" (util/get-text-range "a😀bcdefg\nhijklmn" 3 true)))
+    (is (= "a😀bcdefg\nhij" (util/get-text-range "a😀bcdefg\nhijklmn" 3 false)))
+    (is (= "a😀bcdefg\nh😀i" (util/get-text-range "a😀bcdefg\nh😀ijklmn" 3 false)))))
+
 (deftest test-memoize-last
   (testing "memoize-last add test"
     (let [actual-ops (atom 0)