|
|
@@ -1,6 +1,7 @@
|
|
|
(ns frontend.worker.undo-redo
|
|
|
"undo/redo related fns and op-schema"
|
|
|
(:require [datascript.core :as d]
|
|
|
+ [frontend.schema-register :include-macros true :as sr]
|
|
|
[frontend.worker.db-listener :as db-listener]
|
|
|
[frontend.worker.state :as worker-state]
|
|
|
[logseq.common.config :as common-config]
|
|
|
@@ -9,22 +10,53 @@
|
|
|
[malli.core :as m]
|
|
|
[malli.util :as mu]))
|
|
|
|
|
|
-(def undo-op-schema
|
|
|
+(sr/defkeyword :gen-undo-op?
|
|
|
+ "tx-meta option, generate undo ops from tx-data when true (default true)")
|
|
|
+
|
|
|
+(sr/defkeyword :gen-undo-boundary-op?
|
|
|
+ "tx-meta option, generate `::boundary` undo-op when true (default true).
|
|
|
+usually every transaction's tx-data will generate ops like: [<boundary> <op1> <op2> ...],
|
|
|
+push to undo-stack, result in [...<boundary> <op0> <boundary> <op1> <op2> ...].
|
|
|
+
|
|
|
+when this option is false, only generate [<op1> <op2> ...]. undo-stack: [...<boundary> <op0> <op1> <op2> ...]
|
|
|
+so when undo, it will undo [<op0> <op1> <op2>] instead of [<op1> <op2>]")
|
|
|
+
|
|
|
+(sr/defkeyword ::boundary
|
|
|
+ "boundary of one or more undo-ops.
|
|
|
+when one undo/redo will operate on all ops between two ::boundary")
|
|
|
+
|
|
|
+(sr/defkeyword ::insert-block
|
|
|
+ "when a block is inserted, generate a ::insert-block undo-op.
|
|
|
+when undo this op, the related block will be removed.")
|
|
|
+
|
|
|
+(sr/defkeyword ::move-block
|
|
|
+ "when a block is moved, generate a ::move-block undo-op.")
|
|
|
+
|
|
|
+(sr/defkeyword ::remove-block
|
|
|
+ "when a block is removed, generate a ::remove-block undo-op.
|
|
|
+when undo this op, this original entity-map will be transacted back into db")
|
|
|
+
|
|
|
+(sr/defkeyword ::update-block
|
|
|
+ "when a block is updated, generate a ::update-block undo-op.")
|
|
|
+
|
|
|
+(def ^:private boundary [::boundary])
|
|
|
+
|
|
|
+(def ^:private undo-op-schema
|
|
|
(mu/closed-schema
|
|
|
[:multi {:dispatch first}
|
|
|
- [:boundary
|
|
|
+ [::boundary
|
|
|
[:cat :keyword]]
|
|
|
- [:insert-block
|
|
|
+ [::insert-block
|
|
|
[:cat :keyword
|
|
|
[:map
|
|
|
[:block-uuid :uuid]]]]
|
|
|
- [:move-block
|
|
|
+ [::move-block
|
|
|
[:cat :keyword
|
|
|
[:map
|
|
|
[:block-uuid :uuid]
|
|
|
[:block-origin-left :uuid]
|
|
|
[:block-origin-parent :uuid]]]]
|
|
|
- [:remove-block
|
|
|
+ [::remove-block
|
|
|
[:cat :keyword
|
|
|
[:map
|
|
|
[:block-uuid :uuid]
|
|
|
@@ -34,11 +66,11 @@
|
|
|
[:block/left :uuid]
|
|
|
[:block/parent :uuid]
|
|
|
[:block/content :string]
|
|
|
- [:block/created-at :int]
|
|
|
- [:block/updated-at :int]
|
|
|
- [:block/format :any]
|
|
|
+ [:block/created-at {:optional true} :int]
|
|
|
+ [:block/updated-at {:optional true} :int]
|
|
|
+ [:block/format {:optional true} :any]
|
|
|
[:block/tags {:optional true} [:sequential :uuid]]]]]]]
|
|
|
- [:update-block
|
|
|
+ [::update-block
|
|
|
[:cat :keyword
|
|
|
[:map
|
|
|
[:block-uuid :uuid]
|
|
|
@@ -46,74 +78,103 @@
|
|
|
;; TODO: add more attrs
|
|
|
]]]]))
|
|
|
|
|
|
-(def undo-ops-validator (m/validator [:sequential undo-op-schema]))
|
|
|
+(def ^:private undo-ops-validator (m/validator [:sequential undo-op-schema]))
|
|
|
|
|
|
-(defn reverse-op
|
|
|
+(def ^:private entity-map-pull-pattern
|
|
|
+ [:block/uuid
|
|
|
+ {:block/left [:block/uuid]}
|
|
|
+ {:block/parent [:block/uuid]}
|
|
|
+ :block/content
|
|
|
+ :block/created-at
|
|
|
+ :block/updated-at
|
|
|
+ :block/format
|
|
|
+ {:block/tags [:block/uuid]}])
|
|
|
+
|
|
|
+(defn- ->block-entity-map
|
|
|
+ [db eid]
|
|
|
+ (let [m (d/pull db entity-map-pull-pattern eid)]
|
|
|
+ (cond-> m
|
|
|
+ true (update :block/left :block/uuid)
|
|
|
+ true (update :block/parent :block/uuid)
|
|
|
+ (seq (:block/tags m)) (update :block/tags (partial mapv :block/uuid)))))
|
|
|
+
|
|
|
+(defn- reverse-op
|
|
|
[db op]
|
|
|
(let [block-uuid (:block-uuid (second op))]
|
|
|
(case (first op)
|
|
|
- :boundary op
|
|
|
+ ::boundary op
|
|
|
|
|
|
- :insert-block
|
|
|
- [:remove-block
|
|
|
+ ::insert-block
|
|
|
+ [::remove-block
|
|
|
{:block-uuid block-uuid
|
|
|
- :block-entity-map (d/pull db [:block/uuid
|
|
|
- {:block/left [:block/uuid]}
|
|
|
- {:block/parent [:block/uuid]}
|
|
|
- :block/created-at
|
|
|
- :block/updated-at
|
|
|
- :block/format
|
|
|
- :block/properties
|
|
|
- {:block/tags [:block/uuid]}
|
|
|
- :block/content
|
|
|
- {:block/page [:block/uuid]}]
|
|
|
- [:block/uuid block-uuid])}]
|
|
|
-
|
|
|
- :move-block
|
|
|
+ :block-entity-map (->block-entity-map db [:block/uuid block-uuid])}]
|
|
|
+
|
|
|
+ ::move-block
|
|
|
(let [b (d/entity db [:block/uuid block-uuid])]
|
|
|
- [:move-block
|
|
|
+ [::move-block
|
|
|
{:block-uuid block-uuid
|
|
|
:block-origin-left (:block/uuid (:block/left b))
|
|
|
:block-origin-parent (:block/uuid (:block/parent b))}])
|
|
|
|
|
|
- :remove-block
|
|
|
- [:insert-block {:block-uuid block-uuid}]
|
|
|
+ ::remove-block
|
|
|
+ [::insert-block {:block-uuid block-uuid}]
|
|
|
|
|
|
- :update-block
|
|
|
- (let [block-origin-content (when (:block-origin-content op)
|
|
|
+ ::update-block
|
|
|
+ (let [block-origin-content (when (:block-origin-content (second op))
|
|
|
(:block/content (d/entity db [:block/uuid block-uuid])))]
|
|
|
- [:update-block
|
|
|
+ [::update-block
|
|
|
(cond-> {:block-uuid block-uuid}
|
|
|
block-origin-content (assoc :block-origin-content block-origin-content))]))))
|
|
|
|
|
|
-
|
|
|
(def ^:private apply-conj-vec (partial apply (fnil conj [])))
|
|
|
|
|
|
(defn- push-undo-ops
|
|
|
[repo ops]
|
|
|
+ (assert (undo-ops-validator ops) ops)
|
|
|
(swap! (:undo/repo->undo-stack @worker-state/*state) update repo apply-conj-vec ops))
|
|
|
|
|
|
-(defn- pop-undo-op
|
|
|
+(defn- pop-ops-helper
|
|
|
+ [stack]
|
|
|
+ (let [[ops i]
|
|
|
+ (loop [i (dec (count stack)) r []]
|
|
|
+ (let [peek-op (nth stack i nil)]
|
|
|
+ (cond
|
|
|
+ (neg? i)
|
|
|
+ [r 0]
|
|
|
+
|
|
|
+ (nil? peek-op)
|
|
|
+ [r i]
|
|
|
+
|
|
|
+ (= boundary peek-op)
|
|
|
+ [r i]
|
|
|
+
|
|
|
+ :else
|
|
|
+ (recur (dec i) (conj r peek-op)))))]
|
|
|
+ [ops (subvec stack 0 i)]))
|
|
|
+
|
|
|
+(defn- pop-undo-ops
|
|
|
[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)))
|
|
|
+ (let [repo->undo-stack (:undo/repo->undo-stack @worker-state/*state)
|
|
|
+ undo-stack (@repo->undo-stack repo)
|
|
|
+ [ops undo-stack*] (pop-ops-helper undo-stack)]
|
|
|
+ (swap! repo->undo-stack assoc repo undo-stack*)
|
|
|
+ ops))
|
|
|
|
|
|
(defn- push-redo-ops
|
|
|
[repo ops]
|
|
|
+ (assert (undo-ops-validator ops) ops)
|
|
|
(swap! (:undo/repo->redo-stack @worker-state/*state) update repo apply-conj-vec ops))
|
|
|
|
|
|
-(defn- pop-redo-op
|
|
|
+(defn- pop-redo-ops
|
|
|
[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
|
|
|
+ (let [repo->redo-stack (:undo/repo->redo-stack @worker-state/*state)
|
|
|
+ redo-stack (@repo->redo-stack repo)
|
|
|
+ [ops redo-stack*] (pop-ops-helper redo-stack)]
|
|
|
+ (swap! repo->redo-stack assoc repo redo-stack*)
|
|
|
+ ops))
|
|
|
+
|
|
|
+(defmulti ^:private 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)])]
|
|
|
@@ -126,17 +187,20 @@
|
|
|
(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}
|
|
|
+ (:block/created-at block-entity-map)
|
|
|
+ (assoc :block/created-at (:block/created-at block-entity-map))
|
|
|
+
|
|
|
+ (:block/updated-at block-entity-map)
|
|
|
+ (assoc :block/updated-at (:block/updated-at block-entity-map))
|
|
|
+
|
|
|
(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
|
|
|
- ))))
|
|
|
+ :push-undo-redo))))
|
|
|
|
|
|
-(defmethod reverse-apply-op :insert-block
|
|
|
+(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])]
|
|
|
@@ -152,7 +216,7 @@
|
|
|
{:children? false}))
|
|
|
:push-undo-redo))))
|
|
|
|
|
|
-(defmethod reverse-apply-op :move-block
|
|
|
+(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])]
|
|
|
@@ -166,7 +230,7 @@
|
|
|
(outliner-core/move-blocks! repo conn [block-entity] left-entity sibling?))
|
|
|
:push-undo-redo)))))
|
|
|
|
|
|
-(defmethod reverse-apply-op :update-block
|
|
|
+(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])]
|
|
|
@@ -181,51 +245,40 @@
|
|
|
new-block))
|
|
|
:push-undo-redo))))
|
|
|
|
|
|
-
|
|
|
(defn undo
|
|
|
[repo]
|
|
|
- (when-let [op (pop-undo-op repo)]
|
|
|
+ (if-let [ops (not-empty (pop-undo-ops 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])))))
|
|
|
+ redo-ops-to-push (transient [])]
|
|
|
+ (doseq [op ops]
|
|
|
+ (let [rev-op (reverse-op @conn op)]
|
|
|
+ (when (= :push-undo-redo (reverse-apply-op op conn repo))
|
|
|
+ (conj! redo-ops-to-push rev-op))))
|
|
|
+ (when-let [rev-ops (not-empty (persistent! redo-ops-to-push))]
|
|
|
+ (push-redo-ops repo (cons boundary rev-ops))))
|
|
|
+ (prn "No further undo infomation")))
|
|
|
|
|
|
(defn redo
|
|
|
[repo]
|
|
|
- (when-let [op (pop-redo-op repo)]
|
|
|
+ (if-let [ops (not-empty (pop-redo-ops 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])))))
|
|
|
+ undo-ops-to-push (transient [])]
|
|
|
+ (doseq [op ops]
|
|
|
+ (let [rev-op (reverse-op @conn op)]
|
|
|
+ (when (= :push-undo-redo (reverse-apply-op op conn repo))
|
|
|
+ (conj! undo-ops-to-push rev-op))))
|
|
|
+ (when-let [rev-ops (not-empty (persistent! undo-ops-to-push))]
|
|
|
+ (push-undo-ops repo (cons boundary rev-ops))))
|
|
|
+ (prn "No further redo infomation")))
|
|
|
|
|
|
|
|
|
;;; listen db changes and push undo-ops
|
|
|
|
|
|
-(def ^:private entity-map-pull-pattern
|
|
|
- [:block/uuid
|
|
|
- {:block/left [:block/uuid]}
|
|
|
- {:block/parent [:block/uuid]}
|
|
|
- :block/content
|
|
|
- :block/created-at
|
|
|
- :block/updated-at
|
|
|
- :block/format
|
|
|
- {:block/tags [:block/uuid]}])
|
|
|
-
|
|
|
-(defn- ->block-entity-map
|
|
|
- [db eid]
|
|
|
- (let [m (-> (d/pull db entity-map-pull-pattern eid)
|
|
|
- (update :block/left :block/uuid)
|
|
|
- (update :block/parent :block/uuid))]
|
|
|
- (if (seq (:block/tags m))
|
|
|
- (update m :block/tags (partial mapv :block/uuid))
|
|
|
- m)))
|
|
|
-
|
|
|
(defn- normal-block?
|
|
|
[entity]
|
|
|
(and (:block/parent entity)
|
|
|
(:block/left entity)))
|
|
|
|
|
|
-
|
|
|
(defn- entity-datoms=>ops
|
|
|
[db-before db-after id->attr->datom entity-datoms]
|
|
|
(when-let [e (ffirst entity-datoms)]
|
|
|
@@ -240,43 +293,65 @@
|
|
|
(cond
|
|
|
(and (not add1?) block-uuid
|
|
|
(normal-block? entity-before))
|
|
|
- [[:remove-block
|
|
|
+ [[::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))
|
|
|
- (cond-> [[:move-block
|
|
|
+ (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
|
|
|
+ (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
|
|
|
+ [[::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]
|
|
|
+ [repo db-before db-after same-entity-datoms-coll id->attr->datom gen-boundary-op?]
|
|
|
(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))))
|
|
|
-
|
|
|
+ (push-undo-ops repo (if gen-boundary-op? (cons boundary ops) ops)))))
|
|
|
|
|
|
(defmethod db-listener/listen-db-changes :gen-undo-ops
|
|
|
[_ {:keys [_tx-data tx-meta db-before db-after
|
|
|
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)))
|
|
|
+ (generate-undo-ops repo db-before db-after same-entity-datoms-coll id->attr->datom
|
|
|
+ (:gen-undo-boundary-op? tx-meta true))))
|
|
|
|
|
|
;;; listen db changes and push undo-ops (ends)
|
|
|
+
|
|
|
+(comment
|
|
|
+ (defn- clear-undo-redo-stack
|
|
|
+ []
|
|
|
+ (reset! (:undo/repo->undo-stack @worker-state/*state) {})
|
|
|
+ (reset! (:undo/repo->redo-stack @worker-state/*state) {}))
|
|
|
+ (clear-undo-redo-stack)
|
|
|
+ (add-watch (:undo/repo->undo-stack @worker-state/*state)
|
|
|
+ :xxx
|
|
|
+ (fn [_ _ o n]
|
|
|
+ (cljs.pprint/pprint {:k :undo
|
|
|
+ :o o
|
|
|
+ :n n})))
|
|
|
+
|
|
|
+ (add-watch (:undo/repo->redo-stack @worker-state/*state)
|
|
|
+ :xxx
|
|
|
+ (fn [_ _ o n]
|
|
|
+ (cljs.pprint/pprint {:k :redo
|
|
|
+ :o o
|
|
|
+ :n n})))
|
|
|
+
|
|
|
+ (remove-watch (:undo/repo->undo-stack @worker-state/*state) :xxx)
|
|
|
+ (remove-watch (:undo/repo->redo-stack @worker-state/*state) :xxx))
|