mirror of
https://github.com/penpot/penpot.git
synced 2026-10-29 05:50:35 +00:00
🐛 Restore selected text when composition is cancelled
Restore the pre-composition content and UTF-16 selection when an IME ends with an empty replacement. Keep styles, selection direction and redo intact without recording cancellation as an edit. Share snapshot restoration with editor undo and redo. Add a regression for styled selections and cancellation after undo. Verify commit, cancellation, deletion and history in four saved showcase examples, including emoji and decomposed Korean Jamo in both writing directions. AI-assisted-by: GPT-6
This commit is contained in:
parent
516dae5319
commit
09212dcf75
@ -562,7 +562,7 @@
|
||||
(fn [^js event]
|
||||
(mf/set-ref-val! composing-ref false)
|
||||
(let [data (or (.-data event) "")]
|
||||
(text-editor/text-editor-composition-end data)
|
||||
(wasm.api/finish-text-editor-composition data)
|
||||
(sync-wasm-text-editor-content!)
|
||||
(wasm.api/request-render-preserving-target "text-composition"))
|
||||
(reset-input-node (mf/ref-val contenteditable-ref))
|
||||
|
||||
@ -769,6 +769,10 @@
|
||||
(h/call wasm/internal-module "_update_shape_text_layout")
|
||||
result)))
|
||||
|
||||
(defn finish-text-editor-composition
|
||||
[text]
|
||||
(text-editor/finish-composition text wselect/use-shape set-shape-text-content))
|
||||
|
||||
(defn restore-text-editor-history
|
||||
[direction]
|
||||
(text-editor/restore-history direction wselect/use-shape set-shape-text-content))
|
||||
|
||||
@ -639,6 +639,30 @@
|
||||
(let [shape-id (:shape-id @editing-history)]
|
||||
(record-content-history shape-id (get-cached-content shape-id))))))))
|
||||
|
||||
(defn- restore-snapshot
|
||||
[shape-id snapshot use-shape set-content]
|
||||
(swap! editing-history assoc :restoring true)
|
||||
(try
|
||||
(use-shape shape-id)
|
||||
(set-content shape-id (:content snapshot))
|
||||
(let [{:keys [anchor-para anchor-offset focus-para focus-offset]} (:selection snapshot)]
|
||||
(h/call wasm/internal-module "_text_editor_restore_selection"
|
||||
anchor-para anchor-offset focus-para focus-offset))
|
||||
{:shape-id shape-id :content (:content snapshot)}
|
||||
(finally
|
||||
(swap! editing-history dissoc :restoring :before))))
|
||||
|
||||
(defn finish-composition
|
||||
"Restore the source and selection when the IME cancels its replacement."
|
||||
[text use-shape set-content]
|
||||
(when (wasm/ready?)
|
||||
(let [{:keys [shape-id before composing]} @editing-history]
|
||||
;; End the WASM preview before restoring selection; restoration is
|
||||
;; deliberately blocked while composition remains active.
|
||||
(text-editor-composition-end text)
|
||||
(when (and composing before (empty? text))
|
||||
(restore-snapshot shape-id before use-shape set-content)))))
|
||||
|
||||
(defn restore-history
|
||||
"Restore one edit from this session. An empty history never reaches canvas undo."
|
||||
[direction use-shape set-content]
|
||||
@ -646,17 +670,9 @@
|
||||
(not (:composing @editing-history)))
|
||||
(let [{:keys [shape-id] :as edits} @editing-history]
|
||||
(when-let [{:keys [history snapshot]} (history/step edits direction (history-snapshot shape-id))]
|
||||
(reset! editing-history (assoc history :restoring true))
|
||||
(try
|
||||
(clear-pending-caret-styles!)
|
||||
(use-shape shape-id)
|
||||
(set-content shape-id (:content snapshot))
|
||||
(let [{:keys [anchor-para anchor-offset focus-para focus-offset]} (:selection snapshot)]
|
||||
(h/call wasm/internal-module "_text_editor_restore_selection"
|
||||
anchor-para anchor-offset focus-para focus-offset))
|
||||
{:shape-id shape-id :content (:content snapshot)}
|
||||
(finally
|
||||
(swap! editing-history dissoc :restoring :before)))))))
|
||||
(reset! editing-history history)
|
||||
(clear-pending-caret-styles!)
|
||||
(restore-snapshot shape-id snapshot use-shape set-content)))))
|
||||
|
||||
;; Typography chosen at a collapsed caret: not applied to existing text, but
|
||||
;; picked up (as a new span) by the next inserted text. Keyed by shape-id.
|
||||
|
||||
@ -308,6 +308,56 @@
|
||||
(reset! cache old-cache)
|
||||
(reset! styles old-styles)))))
|
||||
|
||||
(t/deftest ^:async cancelled-composition-restores-styled-source-without-changing-history
|
||||
(let [edits @#'text-editor/editing-history
|
||||
cache @#'text-editor/shape-text-contents
|
||||
old-edits @edits
|
||||
old-cache @cache
|
||||
source (editor-content [{:text "A"}
|
||||
{:text "中" :fills [{:fill-color "#b84821"}]}
|
||||
{:text "😀文" :font-weight "700"}
|
||||
{:text "B"}])
|
||||
deleted (editor-content [{:text "AB"}])
|
||||
engine (atom source)
|
||||
range (atom nil)
|
||||
redo [{:content (editor-content [{:text "你好"}])
|
||||
:selection (selection 0 2 0 2)}]]
|
||||
(try
|
||||
(await
|
||||
(mock/with-mocks*
|
||||
{wasm/ready? (constantly true)
|
||||
wasm/internal-module
|
||||
#js {"_text_editor_composition_start" (fn [] nil)
|
||||
"_text_editor_composition_end" (fn [] (reset! engine deleted))
|
||||
"_text_editor_restore_selection"
|
||||
(fn [ap ao fp fo] (reset! range (selection ap ao fp fo)))}
|
||||
text-editor/text-editor-encode-text-pre (constantly nil)
|
||||
text-editor/text-editor-encode-text-post (constantly nil)
|
||||
text-editor/text-editor-get-selection (fn [] @range)}
|
||||
(doseq [selected [(selection 0 1 0 5) (selection 0 5 0 1)
|
||||
(selection 0 1 0 1)]]
|
||||
(reset! engine source)
|
||||
(reset! range selected)
|
||||
(reset! cache {:fixture source})
|
||||
(reset! edits (assoc history/empty-history :shape-id :fixture :future redo))
|
||||
(text-editor/text-editor-composition-start)
|
||||
(text-editor/finish-composition
|
||||
"" identity
|
||||
(fn [shape-id content]
|
||||
(reset! engine content)
|
||||
(text-editor/cache-shape-text-content! shape-id content)))
|
||||
;; The usual compositionend sync exports the current WASM tree.
|
||||
(text-editor/update-cached-content! :fixture @engine)
|
||||
(t/is (= source @engine))
|
||||
(t/is (= selected @range))
|
||||
(t/is (= source (text-editor/get-cached-content :fixture)))
|
||||
(t/is (empty? (:past @edits)))
|
||||
(t/is (= redo (:future @edits)))
|
||||
(t/is (not (:composing @edits))))))
|
||||
(finally
|
||||
(reset! edits old-edits)
|
||||
(reset! cache old-cache)))))
|
||||
|
||||
(t/deftest composition-caret-goes-to-the-end-of-the-changed-part
|
||||
;; Typing appends, converting replaces all, and a candidate for a middle
|
||||
;; clause changes only that clause.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user