diff --git a/deps/outliner/src/logseq/outliner/core.cljs b/deps/outliner/src/logseq/outliner/core.cljs index 3361fbbe89..2f10fe1c29 100644 --- a/deps/outliner/src/logseq/outliner/core.cljs +++ b/deps/outliner/src/logseq/outliner/core.cljs @@ -929,9 +929,10 @@ (remove nil?))))) (defn ^:api delete-block - "Delete block from the tree." + "FIXME: why expose this fn? there's already a public fn `delete-blocks!` + Delete block from the tree." [repo conn txs-state node {:keys [children? children-check? date-formatter] - :or {children-check? true}}] + :or {children-check? true}}] (if (and children-check? (not children?) (first (:block/_parent (d/entity @conn [:block/uuid (:block/uuid (get-data node))])))) diff --git a/src/main/frontend/worker/db_listener.cljs b/src/main/frontend/worker/db_listener.cljs index b43de140c1..3ca0250db5 100644 --- a/src/main/frontend/worker/db_listener.cljs +++ b/src/main/frontend/worker/db_listener.cljs @@ -1,4 +1,5 @@ (ns frontend.worker.db-listener + "Db listeners for worker-db." (:require [datascript.core :as d])) diff --git a/src/main/frontend/worker/state.cljs b/src/main/frontend/worker/state.cljs index a21e9990f9..c2322574ed 100644 --- a/src/main/frontend/worker/state.cljs +++ b/src/main/frontend/worker/state.cljs @@ -8,11 +8,15 @@ :db/latest-transact-time {} :worker/context {} + ;; FIXME: this name :config is too general :config {} :git/current-repo nil :rtc/batch-processing? false :rtc/remote-batch-txs nil - :rtc/downloading-graph? false})) + :rtc/downloading-graph? false + + :undo/repo->undo-stack (atom {}) + :undo/repo->redo-stack (atom {})})) (defonce *rtc-ws-url (atom nil)) diff --git a/src/main/frontend/worker/undo_redo.cljs b/src/main/frontend/worker/undo_redo.cljs index c84b6b05af..e83f774b56 100644 --- a/src/main/frontend/worker/undo_redo.cljs +++ b/src/main/frontend/worker/undo_redo.cljs @@ -1,10 +1,13 @@ (ns frontend.worker.undo-redo "undo/redo related fns and op-schema" - (:require [frontend.worker.db-listener :as db-listener] - [datascript.core :as d] - [malli.util :as mu] - [malli.core :as m])) - + (:require [datascript.core :as d] + [frontend.worker.db-listener :as db-listener] + [frontend.worker.state :as worker-state] + [logseq.common.config :as common-config] + [logseq.outliner.core :as outliner-core] + [logseq.outliner.transaction :as outliner-tx] + [malli.core :as m] + [malli.util :as mu])) (def undo-op-schema (mu/closed-schema @@ -40,10 +43,10 @@ [:map [:block-uuid :uuid] [:block-origin-content {:optional true} :string] - ;; TODO: add more attrs + ;; TODO: add more attrs ]]]])) -(def undo-op-validator (m/validator undo-op-schema)) +(def undo-ops-validator (m/validator [:sequential undo-op-schema])) (defn reverse-op [db op] @@ -83,7 +86,122 @@ (cond-> {:block-uuid block-uuid} block-origin-content (assoc :block-origin-content block-origin-content))])))) -(def entity-map-pull-pattern + +(def ^:private apply-conj-vec (partial apply (fnil conj []))) + +(defn- push-undo-ops + [repo ops] + (swap! (:undo/repo->undo-stack @worker-state/*state) update repo apply-conj-vec ops)) + +(defn- pop-undo-op + [repo] + (let [repo->undo-stack (:undo/repo->undo-stack @worker-state/*state)] + (when-let [peek-op (peek (@repo->undo-stack repo))] + (swap! repo->undo-stack update repo pop) + peek-op))) + +(defn- push-redo-ops + [repo ops] + (swap! (:undo/repo->redo-stack @worker-state/*state) update repo apply-conj-vec ops)) + +(defn- pop-redo-op + [repo] + (let [repo->redo-stack (:undo/repo->redo-stack @worker-state/*state)] + (when-let [peek-op (peek (@repo->redo-stack repo))] + (swap! repo->redo-stack update repo pop) + peek-op))) + + +(defmulti reverse-apply-op (fn [op _conn _repo] (first op))) +(defmethod reverse-apply-op :remove-block + [op conn repo] + (let [[_ {:keys [block-uuid block-entity-map]}] op] + (when-let [left-entity (d/entity @conn [:block/uuid (:block/left block-entity-map)])] + (let [sibling? (not= (:block/left block-entity-map) (:block/parent block-entity-map))] + (outliner-tx/transact! + {:gen-undo-op? false + :outliner-op :insert-blocks + :transact-opts {:repo repo + :conn conn}} + (outliner-core/insert-blocks! repo conn + [(cond-> {:block/uuid block-uuid + :block/content (:block/content block-entity-map) + :block/created-at (:block/created-at block-entity-map) + :block/updated-at (:block/updated-at block-entity-map) + :block/format :markdown} + (seq (:block/tags block-entity-map)) + (assoc :block/tags (mapv (partial vector :block/uuid) + (:block/tags block-entity-map))))] + left-entity {:sibling? sibling? :keep-uuid? true})) + :push-undo-redo + )))) + +(defmethod reverse-apply-op :insert-block + [op conn repo] + (let [[_ {:keys [block-uuid]}] op] + (when-let [block-entity (d/entity @conn [:block/uuid block-uuid])] + (when (empty? (seq (:block/_parent block-entity))) ;if have children, skip + (outliner-tx/transact! + {:gen-undo-op? false + :outliner-op :delete-blocks + :transact-opts {:repo repo + :conn conn}} + (outliner-core/delete-blocks! repo conn + (common-config/get-date-formatter (worker-state/get-config repo)) + [block-entity] + {:children? false})) + :push-undo-redo)))) + +(defmethod reverse-apply-op :move-block + [op conn repo] + (let [[_ {:keys [block-uuid block-origin-left block-origin-parent]}] op] + (when-let [block-entity (d/entity @conn [:block/uuid block-uuid])] + (when-let [left-entity (d/entity @conn [:block/uuid block-origin-left])] + (let [sibling? (not= block-origin-left block-origin-parent)] + (outliner-tx/transact! + {:gen-undo-op? false + :outliner-op :move-blocks + :transact-opts {:repo repo + :conn conn}} + (outliner-core/move-blocks! repo conn [block-entity] left-entity sibling?)) + :push-undo-redo))))) + +(defmethod reverse-apply-op :update-block + [op conn repo] + (let [[_ {:keys [block-uuid block-origin-content]}] op] + (when-let [block-entity (d/entity @conn [:block/uuid block-uuid])] + (let [new-block (assoc block-entity :block/content block-origin-content)] + (outliner-tx/transact! + {:gen-undo-op? false + :outliner-op :save-block + :transact-opts {:repo repo + :conn conn}} + (outliner-core/save-block! repo conn + (common-config/get-date-formatter (worker-state/get-config repo)) + new-block)) + :push-undo-redo)))) + + +(defn undo + [repo] + (when-let [op (pop-undo-op repo)] + (let [conn (worker-state/get-datascript-conn repo) + rev-op (reverse-op @conn op)] + (when (= :push-undo-redo (reverse-apply-op op conn repo)) + (push-redo-ops repo [rev-op]))))) + +(defn redo + [repo] + (when-let [op (pop-redo-op repo)] + (let [conn (worker-state/get-datascript-conn repo) + rev-op (reverse-op @conn op)] + (when (= :push-undo-redo (reverse-apply-op op conn repo)) + (push-undo-ops repo [rev-op]))))) + + +;;; listen db changes and push undo-ops + +(def ^:private entity-map-pull-pattern [:block/uuid {:block/left [:block/uuid]} {:block/parent [:block/uuid]} @@ -102,16 +220,14 @@ (update m :block/tags (partial mapv :block/uuid)) m))) -(defn normal-block? +(defn- normal-block? [entity] (and (:block/parent entity) (:block/left entity))) -(defn entity-datoms=>op +(defn- entity-datoms=>ops [db-before db-after id->attr->datom entity-datoms] - {:post [(or (nil? %) - (undo-op-validator %))]} (when-let [e (ffirst entity-datoms)] (let [attr->datom (id->attr->datom e)] (when (seq attr->datom) @@ -124,31 +240,37 @@ (cond (and (not add1?) block-uuid (normal-block? entity-before)) - [:remove-block - {:block-uuid (:block/uuid entity-before) - :block-entity-map (->block-entity-map db-before e)}] + [[:remove-block + {:block-uuid (:block/uuid entity-before) + :block-entity-map (->block-entity-map db-before e)}]] (and add1? block-uuid (normal-block? entity-after)) - [:insert-block {:block-uuid (:block/uuid entity-after)}] + [[:insert-block {:block-uuid (:block/uuid entity-after)}]] (and (or add3? add4?) (normal-block? entity-after)) - [:move-block - {:block-uuid (:block/uuid entity-after) - :block-origin-left (:block/uuid (:block/left entity-before)) - :block-origin-parent (:block/uuid (:block/parent entity-before))}] + (cond-> [[:move-block + {:block-uuid (:block/uuid entity-after) + :block-origin-left (:block/uuid (:block/left entity-before)) + :block-origin-parent (:block/uuid (:block/parent entity-before))}]] + (and add2? block-content) + (conj [:update-block + {:block-uuid (:block/uuid entity-after) + :block-origin-content (:block/content entity-before)}])) (and add2? block-content (normal-block? entity-after)) - [:update-block - {:block-uuid (:block/uuid entity-after) - :block-origin-content (:block/content entity-before)}])))))) + [[:update-block + {:block-uuid (:block/uuid entity-after) + :block-origin-content (:block/content entity-before)}]])))))) -(defn generate-undo-ops - [_repo db-before db-after same-entity-datoms-coll id->attr->datom] - (let [ops (keep (partial entity-datoms=>op db-before db-after id->attr->datom) same-entity-datoms-coll)] - (prn ::debug-undo-ops ops))) +(defn- generate-undo-ops + [repo db-before db-after same-entity-datoms-coll id->attr->datom] + (let [ops (mapcat (partial entity-datoms=>ops db-before db-after id->attr->datom) same-entity-datoms-coll)] + (assert (undo-ops-validator ops) ops) + (when (seq ops) + (push-undo-ops repo ops)))) (defmethod db-listener/listen-db-changes :gen-undo-ops @@ -156,3 +278,5 @@ repo id->attr->datom same-entity-datoms-coll]}] (when (:gen-undo-op? tx-meta true) (generate-undo-ops repo db-before db-after same-entity-datoms-coll id->attr->datom))) + +;;; listen db changes and push undo-ops (ends) diff --git a/src/test/frontend/worker/undo_redo_test.cljs b/src/test/frontend/worker/undo_redo_test.cljs index 3da3f07988..988fcf3d7b 100644 --- a/src/test/frontend/worker/undo_redo_test.cljs +++ b/src/test/frontend/worker/undo_redo_test.cljs @@ -8,4 +8,6 @@ ;; TODO: add tests for undo-redo undo-redo/undo-op-schema undo-redo/reverse-op + undo-redo/undo + undo-redo/redo )