From 7b77d2d5f5efa8bc9461e98574e9b1e99d3dfff8 Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Wed, 25 Mar 2026 10:43:48 +0800 Subject: [PATCH] enhance: add selection state to undo/redo history --- src/main/frontend/handler/history.cljs | 19 +++++-- src/main/frontend/state.cljs | 17 ++++-- src/main/frontend/worker/undo_redo.cljs | 31 +++++++---- src/test/frontend/handler/history_test.cljs | 54 ++++++++++++++++++++ src/test/frontend/state_test.cljs | 15 ++++++ src/test/frontend/worker/undo_redo_test.cljs | 21 ++++++++ 6 files changed, 138 insertions(+), 19 deletions(-) diff --git a/src/main/frontend/handler/history.cljs b/src/main/frontend/handler/history.cljs index b0abffb96d..b5d7517864 100644 --- a/src/main/frontend/handler/history.cljs +++ b/src/main/frontend/handler/history.cljs @@ -11,12 +11,21 @@ (defn- restore-cursor! [{:keys [editor-cursors block-content undo?]}] - (let [{:keys [block-uuid container-id start-pos end-pos]} (if undo? (first editor-cursors) (or (last editor-cursors) (first editor-cursors))) + (let [cursor (if undo? + (first editor-cursors) + (or (last editor-cursors) (first editor-cursors))) + {:keys [selected-block-uuids selection-direction block-uuid container-id start-pos end-pos]} cursor + selected-blocks (when (seq selected-block-uuids) + (->> selected-block-uuids + (mapcat util/get-blocks-by-id) + vec)) pos (if undo? (or start-pos end-pos) (or end-pos start-pos))] - (when-let [block (db/pull [:block/uuid block-uuid])] - (editor/edit-block! block pos - {:container-id container-id - :custom-content block-content})))) + (if (seq selected-blocks) + (state/exit-editing-and-set-selected-blocks! selected-blocks selection-direction) + (when-let [block (db/pull [:block/uuid block-uuid])] + (editor/edit-block! block pos + {:container-id container-id + :custom-content block-content}))))) (defn- restore-app-state! [state] diff --git a/src/main/frontend/state.cljs b/src/main/frontend/state.cljs index 9d27688707..56b1002e40 100644 --- a/src/main/frontend/state.cljs +++ b/src/main/frontend/state.cljs @@ -2044,11 +2044,18 @@ Similar to re-frame subscriptions" (defn get-editor-info [] - (when-let [edit-block (get-edit-block)] - {:block-uuid (:block/uuid edit-block) - :container-id (or @(:editor/container-id @state) :unknown-container) - :start-pos @(:editor/start-pos @state) - :end-pos (get-edit-pos)})) + (let [selected-block-uuids (some-> (get-selection-block-ids) seq vec) + selection-info (when selected-block-uuids + {:selected-block-uuids selected-block-uuids + :selection-direction (get-selection-direction)})] + (if-let [edit-block (get-edit-block)] + (cond-> {:block-uuid (:block/uuid edit-block) + :container-id (or @(:editor/container-id @state) :unknown-container) + :start-pos @(:editor/start-pos @state) + :end-pos (get-edit-pos)} + selection-info + (merge selection-info)) + selection-info))) (defn conj-block-ref! [ref-entity] diff --git a/src/main/frontend/worker/undo_redo.cljs b/src/main/frontend/worker/undo_redo.cljs index c9cd03a65f..5c0256e0ed 100644 --- a/src/main/frontend/worker/undo_redo.cljs +++ b/src/main/frontend/worker/undo_redo.cljs @@ -23,6 +23,20 @@ (sr/defkeyword :gen-undo-ops? "tx-meta option, generate undo ops from tx-data when true (default true)")) +(def ^:private selection-editor-info-schema + [:map + [:selected-block-uuids [:sequential :uuid]] + [:selection-direction {:optional true} [:maybe [:enum :up :down]]]]) + +(def ^:private editor-cursor-info-schema + [:map + [:block-uuid :uuid] + [:container-id [:or :int [:enum :unknown-container]]] + [:start-pos [:maybe :int]] + [:end-pos [:maybe :int]] + [:selected-block-uuids {:optional true} [:sequential :uuid]] + [:selection-direction {:optional true} [:maybe [:enum :up :down]]]]) + (def ^:private undo-op-item-schema (mu/closed-schema [:multi {:dispatch first} @@ -42,11 +56,9 @@ [::record-editor-info [:cat :keyword - [:map - [:block-uuid :uuid] - [:container-id [:or :int [:enum :unknown-container]]] - [:start-pos [:maybe :int]] - [:end-pos [:maybe :int]]]]] + [:or + editor-cursor-info-schema + selection-editor-info-schema]]] [::ui-state [:cat :keyword :string]]])) @@ -316,10 +328,11 @@ (push-opposite-op! repo undo? op') (let [editor-cursors (->> (filter #(= ::record-editor-info (first %)) op) (map second)) - block-content (:block/title (d/entity @conn [:block/uuid (:block-uuid - (if undo? - (first editor-cursors) - (last editor-cursors)))]))] + cursor (if undo? + (first editor-cursors) + (or (last editor-cursors) (first editor-cursors))) + block-content (when-let [block-uuid (:block-uuid cursor)] + (:block/title (d/entity @conn [:block/uuid block-uuid])))] {:undo? undo? :editor-cursors editor-cursors :block-content block-content})) diff --git a/src/test/frontend/handler/history_test.cljs b/src/test/frontend/handler/history_test.cljs index 3c051886c5..7000fe828c 100644 --- a/src/test/frontend/handler/history_test.cljs +++ b/src/test/frontend/handler/history_test.cljs @@ -1,7 +1,10 @@ (ns frontend.handler.history-test (:require [clojure.test :refer [deftest is]] + [frontend.db :as db] + [frontend.handler.editor :as editor] [frontend.handler.history :as history] [frontend.state :as state] + [frontend.util :as util] [logseq.db :as ldb])) (deftest restore-cursor-and-state-prefers-ui-state-test @@ -51,3 +54,54 @@ (is (= 1 (count @cursor-calls))) (is (nil? (:ui-state-str (first @cursor-calls)))) (is (= false (:undo? (first @cursor-calls))))))) + +(deftest restore-cursor-prefers-block-selection-test + (let [selection-calls (atom []) + edit-calls (atom [])] + (with-redefs [util/get-blocks-by-id (fn [block-id] + (case block-id + #uuid "00000000-0000-0000-0000-000000000001" [:node-1] + #uuid "00000000-0000-0000-0000-000000000002" [:node-2] + nil)) + state/exit-editing-and-set-selected-blocks! (fn [blocks direction] + (swap! selection-calls conj [blocks direction])) + editor/edit-block! (fn [& args] + (swap! edit-calls conj args)) + db/pull (constantly nil)] + (#'history/restore-cursor! + {:undo? true + :editor-cursors [{:selected-block-uuids [#uuid "00000000-0000-0000-0000-000000000001" + #uuid "00000000-0000-0000-0000-000000000002"] + :selection-direction :down}]}) + (is (= [[[:node-1 :node-2] :down]] + @selection-calls)) + (is (empty? @edit-calls))))) + +(deftest restore-cursor-selection-falls-back-to-editor-cursor-test + (let [selection-calls (atom []) + edit-calls (atom []) + block-uuid #uuid "00000000-0000-0000-0000-000000000003"] + (with-redefs [util/get-blocks-by-id (constantly nil) + state/exit-editing-and-set-selected-blocks! (fn [blocks direction] + (swap! selection-calls conj [blocks direction])) + editor/edit-block! (fn [& args] + (swap! edit-calls conj args)) + db/pull (fn [[_lookup-k id]] + (when (= block-uuid id) + {:db/id 42 + :block/uuid block-uuid}))] + (#'history/restore-cursor! + {:undo? false + :editor-cursors [{:selected-block-uuids [#uuid "00000000-0000-0000-0000-000000000001"] + :selection-direction :up + :block-uuid block-uuid + :container-id 99 + :start-pos 1 + :end-pos 3}]}) + (is (empty? @selection-calls)) + (is (= [[{:db/id 42 + :block/uuid block-uuid} + 3 + {:container-id 99 + :custom-content nil}]] + @edit-calls))))) diff --git a/src/test/frontend/state_test.cljs b/src/test/frontend/state_test.cljs index 5b8cf7a7f8..046fe6b990 100644 --- a/src/test/frontend/state_test.cljs +++ b/src/test/frontend/state_test.cljs @@ -24,3 +24,18 @@ {:shortcuts {:ui/toggle-brackets "t b"}} {:shortcuts {:editor/up ["ctrl+p" "up"]}})) "Map values get merged across configs")) + +(deftest get-editor-info-includes-selection-when-not-editing-test + (let [selected-ids [(random-uuid) (random-uuid)]] + (with-redefs [state/get-edit-block (constantly nil) + state/get-selection-block-ids (constantly selected-ids) + state/get-selection-direction (constantly :down)] + (is (= {:selected-block-uuids selected-ids + :selection-direction :down} + (state/get-editor-info)))))) + +(deftest get-editor-info-returns-nil-when-not-editing-and-no-selection-test + (with-redefs [state/get-edit-block (constantly nil) + state/get-selection-block-ids (constantly nil) + state/get-selection-direction (constantly nil)] + (is (nil? (state/get-editor-info))))) diff --git a/src/test/frontend/worker/undo_redo_test.cljs b/src/test/frontend/worker/undo_redo_test.cljs index 22f16c1ada..c64e02d1ab 100644 --- a/src/test/frontend/worker/undo_redo_test.cljs +++ b/src/test/frontend/worker/undo_redo_test.cljs @@ -86,6 +86,27 @@ :outliner-ops [[:save-block [{:block/uuid block-uuid :block/title title} {}]]]})))) +(deftest undo-redo-selection-editor-info-roundtrip-test + (testing "undo/redo result keeps block selection editor info when no cursor is recorded" + (worker-undo-redo/clear-history! test-repo) + (let [conn (worker-state/get-datascript-conn test-repo) + {:keys [child-uuid]} (seed-page-parent-child!) + selection-info {:selected-block-uuids [child-uuid] + :selection-direction :down}] + (d/transact! conn + [[:db/add [:block/uuid child-uuid] :block/title "selection-history"]] + (local-tx-meta + {:outliner-op :save-block + :undo-redo/editor-info selection-info + :outliner-ops [[:save-block [{:block/uuid child-uuid + :block/title "selection-history"} {}]]]})) + (let [undo-result (worker-undo-redo/undo test-repo)] + (is (= [selection-info] (:editor-cursors undo-result))) + (is (nil? (:block-content undo-result)))) + (let [redo-result (worker-undo-redo/redo test-repo)] + (is (= [selection-info] (:editor-cursors redo-result))) + (is (nil? (:block-content redo-result))))))) + (defn- undo-all! [] (loop [results []]