enhance: add selection state to undo/redo history

This commit is contained in:
Tienson Qin
2026-03-25 10:43:48 +08:00
parent 7509b76c1d
commit 7b77d2d5f5
6 changed files with 138 additions and 19 deletions

View File

@@ -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]

View File

@@ -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]

View File

@@ -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}))

View File

@@ -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)))))

View File

@@ -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)))))

View File

@@ -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 []]