Browse Source

undo/redo insert-node delete-node save-node

rcmerci 4 years ago
parent
commit
51e9a38ad2

+ 2 - 2
gulpfile.js

@@ -46,7 +46,7 @@ const common = {
   },
 
   keepSyncResourceFile () {
-    return gulp.watch(resourceFilePath, { ignoreInitial: true }, common.syncResourceFile)
+      return gulp.watch(resourceFilePath, { ignoreInitial: true, interval: 1000 }, common.syncResourceFile)
   },
 
 
@@ -55,7 +55,7 @@ const common = {
   },
 
   keeypSyncStatic() {
-      return gulp.watch(outputFilePath, { ignoreInitial: true }, common.syncStatic)
+      return gulp.watch(outputFilePath, { ignoreInitial: true, interval: 1000 }, common.syncStatic)
   }
 }
 

+ 3 - 3
src/main/frontend/handler/history.cljs

@@ -2,7 +2,7 @@
   (:require [frontend.db :as db]
             [frontend.handler.editor :as editor]
             [frontend.handler.ui :as ui-handler]
-            [frontend.modules.editor.undo-redo :as undo-redo]
+            [frontend.modules.outliner.yjs :as yjs]
             [frontend.state :as state]
             [frontend.util :as util]
             [goog.dom :as gdom]))
@@ -32,7 +32,7 @@
   (util/stop e)
   (state/set-editor-op! :undo)
   (editor/save-current-block!)
-  (let [{:keys [editor-cursor]} (undo-redo/undo)]
+  (let [{:keys [editor-cursor]} (yjs/undo)]
     (restore-cursor! editor-cursor))
   (state/set-editor-op! nil))
 
@@ -40,6 +40,6 @@
   [e]
   (util/stop e)
   (state/set-editor-op! :redo)
-  (let [{:keys [editor-cursor]} (undo-redo/redo)]
+  (let [{:keys [editor-cursor]} (yjs/redo)]
     (restore-cursor! editor-cursor))
   (state/set-editor-op! nil))

+ 2 - 2
src/main/frontend/modules/editor/undo_redo.cljs

@@ -105,7 +105,7 @@
         db-report (d/transact! conn txs)]
     (do (pipelines/invoke-hooks db-report))))
 
-(defn- refresh!
+(defn refresh!
   [opts]
   (let [repo (state/get-current-repo)]
    (db/refresh! repo opts)))
@@ -141,7 +141,7 @@
 
 (defn listen-outliner-operation
   [{:keys [tx-data tx-meta] :as tx-report}]
-  (when-not (empty? tx-data)
+  (when (and (seq tx-data) (not (:skip-undo? tx-meta)))
     (reset-redo)
     (let [updated-blocks (db-report/get-blocks tx-report)
           entity {:blocks updated-blocks :txs tx-data

+ 88 - 57
src/main/frontend/modules/outliner/core.cljs

@@ -218,14 +218,29 @@
 (defn- get-page-name [node]
   (:block/name (db/entity (get-in node [:data :block/page :db/id]))))
 
+(defn- get-block-and-children-content-tree [uuid]
+  (-> (db/get-block-and-children (state/get-current-repo) uuid)
+      (tree/blocks->vec-tree uuid)
+      (tree/vec-tree->block-tree)
+      (tree/block-tree-keep-props [:block/uuid :block/content])))
+
 (defn save-node
-  [node]
-  {:pre [(tree/satisfied-inode? node)]}
-  (ds/auto-transact!
-   [db (ds/new-outliner-txs-state)] {:outliner-op :save-node
-                                     :other-meta {:page-name (get-page-name node)
-                                                  :node-id (tree/-get-id node)}}
-   (tree/-save node db)))
+  ([node]
+   (save-node node nil))
+  ([node {:keys [skip-undo?]
+          :or {skip-undo? false}}]
+   {:pre [(tree/satisfied-inode? node)]}
+   (let [content-before (:block/content (db/entity (:db/id (:data node))))
+         content-after (get-in node [:data :block/content])]
+     (ds/auto-transact!
+      [db (ds/new-outliner-txs-state)] {:outliner-op :save-node
+                                        :skip-undo? skip-undo?
+                                        :other-meta
+                                        {:page-name (get-page-name node)
+                                         :node-id (tree/-get-id node)
+                                         :node-content-after content-after
+                                         :node-content-before content-before}}
+      (tree/-save node db)))))
 
 (defn insert-node-as-first-child
   "Insert a node as first child."
@@ -287,15 +302,18 @@
 (defn insert-node
   ([new-node target-node sibling?]
    (insert-node new-node target-node sibling? nil))
-  ([new-node target-node sibling? {:keys [blocks-atom skip-transact?]
-                                   :or {skip-transact? false}}]
+  ([new-node target-node sibling? {:keys [blocks-atom skip-transact? skip-undo?]
+                                   :or {skip-transact? false
+                                        skip-undo? false}}]
    (let [page-name (get-page-name target-node)]
      (ds/auto-transact!
       [txs-state (ds/new-outliner-txs-state)]
       {:outliner-op :insert-node
        :skip-transact? skip-transact?
+       :skip-undo? skip-undo?
        :other-meta {:page-name page-name
                     :node-id (tree/-get-id new-node)
+                    :node-content (get-in new-node [:data :block/content])
                     :target-id (tree/-get-id target-node)
                     :sibling? sibling?}}
       (insert-node-aux new-node target-node sibling? txs-state blocks-atom)))))
@@ -345,39 +363,42 @@
 (defn insert-nodes
   "Insert nodes as children(or siblings) of target-node.
   new-nodes-tree is an vector of blocks, e.g [1 [2 3] 4 [5 [6 7]]]"
-  [new-nodes-tree target-node sibling?]
-  {:pre [(> (count new-nodes-tree) 0)]}
-  (let [page-name (get-page-name target-node)
-        target-id (tree/-get-id target-node)]
-    (ds/auto-transact!
-     [txs-state (ds/new-outliner-txs-state)] {:outliner-op :insert-nodes
-                                              :other-meta {:new-nodes-tree new-nodes-tree
-                                                           :target-id target-id
-                                                           :sibling? sibling?}}
-     (let [loc (zip/vector-zip new-nodes-tree)]
-       ;; TODO: validate new-nodes-tree structure
-       (let [updated-nodes (walk-&-insert-nodes loc target-node sibling? txs-state)
-             loc (zip/vector-zip (zip/root updated-nodes))
-             ;; topmost-last-loc=4, new-nodes-tree=[1 [2 3] 4 [5 [6 7]]]
-             topmost-last-loc (get-node-tree-topmost-last-loc loc)
-             ;; sub-topmost-last-loc=5, new-nodes-tree=[1 [2 3] 4 [5 [6 7]]]
-             sub-topmost-last-loc (get-node-tree-sub-topmost-last-loc loc)
-             right-node (tree/-get-right target-node)
-             down-node (tree/-get-down target-node)]
-         ;; update node's left&parent after inserted nodes
-         (cond
-           (and (not sibling?) (some? right-node) (nil? down-node))
-           nil            ;ignore
-           (and sibling? (some? right-node) topmost-last-loc) ;; right-node.left=N
-           (let [topmost-last-node (zip/node topmost-last-loc)
-                 updated-node (tree/-set-left-id right-node (tree/-get-id topmost-last-node))]
-             (tree/-save updated-node txs-state))
-           (and (not sibling?) (some? down-node) topmost-last-loc) ;; down-node.left=N
-           (let [topmost-last-node (zip/node topmost-last-loc)
-                 updated-node (tree/-set-left-id down-node (tree/-get-id topmost-last-node))]
-             (tree/-save updated-node txs-state))
-           (and sibling? (some? down-node)) ;; unchanged
-           nil))))))
+  ([new-nodes-tree target-node sibling?]
+   (insert-nodes new-nodes-tree target-node sibling? nil))
+  ([new-nodes-tree target-node sibling? {:keys [skip-undo?]
+                                         :or {skip-undo? false}}]
+   {:pre [(> (count new-nodes-tree) 0)]}
+   (let [page-name (get-page-name target-node)
+         target-id (tree/-get-id target-node)]
+     (ds/auto-transact!
+      [txs-state (ds/new-outliner-txs-state)] {:outliner-op :insert-nodes
+                                               :other-meta {:new-nodes-tree new-nodes-tree
+                                                            :target-id target-id
+                                                            :sibling? sibling?}}
+      (let [loc (zip/vector-zip new-nodes-tree)]
+        ;; TODO: validate new-nodes-tree structure
+        (let [updated-nodes (walk-&-insert-nodes loc target-node sibling? txs-state)
+              loc (zip/vector-zip (zip/root updated-nodes))
+              ;; topmost-last-loc=4, new-nodes-tree=[1 [2 3] 4 [5 [6 7]]]
+              topmost-last-loc (get-node-tree-topmost-last-loc loc)
+              ;; sub-topmost-last-loc=5, new-nodes-tree=[1 [2 3] 4 [5 [6 7]]]
+              sub-topmost-last-loc (get-node-tree-sub-topmost-last-loc loc)
+              right-node (tree/-get-right target-node)
+              down-node (tree/-get-down target-node)]
+          ;; update node's left&parent after inserted nodes
+          (cond
+            (and (not sibling?) (some? right-node) (nil? down-node))
+            nil            ;ignore
+            (and sibling? (some? right-node) topmost-last-loc) ;; right-node.left=N
+            (let [topmost-last-node (zip/node topmost-last-loc)
+                  updated-node (tree/-set-left-id right-node (tree/-get-id topmost-last-node))]
+              (tree/-save updated-node txs-state))
+            (and (not sibling?) (some? down-node) topmost-last-loc) ;; down-node.left=N
+            (let [topmost-last-node (zip/node topmost-last-loc)
+                  updated-node (tree/-set-left-id down-node (tree/-get-id topmost-last-node))]
+              (tree/-save updated-node txs-state))
+            (and sibling? (some? down-node)) ;; unchanged
+            nil)))))))
 
 (defn move-node
   [node up?]
@@ -427,21 +448,31 @@
 
 (defn delete-node
   "Delete node from the tree."
-  [node children?]
-  {:pre [(tree/satisfied-inode? node)]}
-  (let [page-name (get-page-name node)
-        node-id (tree/-get-id node)]
-    (ds/auto-transact!
-     [txs-state (ds/new-outliner-txs-state)] {:outliner-op :delete-node
-                                              :other-meta {:page-name page-name
-                                                           :node-id node-id
-                                                           :children? children?}}
-     (let [right-node (tree/-get-right node)]
-       (tree/-del node txs-state children?)
-       (when (tree/satisfied-inode? right-node)
-         (let [left-node (tree/-get-left node)
-               new-right-node (tree/-set-left-id right-node (tree/-get-id left-node))]
-           (tree/-save new-right-node txs-state)))))))
+  ([node children?]
+   (delete-node node children? nil))
+  ([node children? {:keys [skip-undo?]
+                    :or {skip-undo? false}}]
+   {:pre [(tree/satisfied-inode? node)]}
+   (let [page-name (get-page-name node)
+         node-id (tree/-get-id node)
+         tree (get-block-and-children-content-tree node-id)
+         left-id (tree/-get-left-id node)
+         parent-id (tree/-get-parent-id node)]
+     (ds/auto-transact!
+      [txs-state (ds/new-outliner-txs-state)] {:outliner-op :delete-node
+                                               :skip-undo? skip-undo?
+                                               :other-meta {:page-name page-name
+                                                            :node-id node-id
+                                                            :tree tree
+                                                            :children? children?
+                                                            :left-id left-id
+                                                            :parent-id parent-id}}
+      (let [right-node (tree/-get-right node)]
+        (tree/-del node txs-state children?)
+        (when (tree/satisfied-inode? right-node)
+          (let [left-node (tree/-get-left node)
+                new-right-node (tree/-set-left-id right-node (tree/-get-id left-node))]
+            (tree/-save new-right-node txs-state))))))))
 
 (defn- get-left-nodes
   [node limit]

+ 44 - 1
src/main/frontend/modules/outliner/tree.cljs

@@ -1,6 +1,7 @@
 (ns frontend.modules.outliner.tree
   (:require [frontend.db :as db]
-            [frontend.util :as util]))
+            [frontend.util :as util]
+            [clojure.zip :as zip]))
 
 (defprotocol INode
   (-get-id [this])
@@ -73,6 +74,48 @@
             root-block (with-children root-block result)]
         [root-block]))))
 
+(defn vec-tree->block-tree [tree]
+  "
+{:id 1
+ :block/children [{:id 2} {:id 3}]}
+->
+[{:id 1}
+ [{:id 2}
+  {:id 3}]]
+"
+  (let [loc (zip/vector-zip tree)]
+    (loop [loc loc]
+      (if (zip/end? loc)
+        (zip/root loc)
+        (cond
+          (map? (zip/node loc))
+          (let [block (zip/node loc)
+                children (:block/children block)
+                block* (dissoc block :block/children)
+                loc*
+                (cond-> loc
+                  true (zip/replace block*)
+                  (seq children) (-> (zip/insert-right (vec children))
+                                       (zip/next))
+                  true (zip/next))]
+            (recur loc*))
+
+          :else
+          (recur (zip/next loc)))))))
+
+(defn block-tree-keep-props [tree props]
+  (let [loc (zip/vector-zip tree)
+        props (set props)]
+    (loop [loc loc]
+      (if (zip/end? loc)
+        (zip/root loc)
+        (cond
+          (map? (zip/node loc))
+          (let [block (zip/node loc)]
+            (recur (zip/next (zip/replace loc (select-keys block props)))))
+          :else
+          (recur (zip/next loc)))))))
+
 (defn- sort-blocks-aux
   [parents parent-groups]
   (mapv (fn [parent]

+ 251 - 76
src/main/frontend/modules/outliner/yjs.cljs

@@ -3,6 +3,7 @@
             ["y-websocket" :as y-ws]
             [frontend.modules.outliner.tree :as tree]
             [frontend.modules.outliner.core :as outliner-core]
+            [frontend.modules.editor.undo-redo :as undo-redo]
             [frontend.format.block :as block]
             [frontend.format.mldoc :as mldoc]
             [frontend.handler.common :as common-handler]
@@ -209,25 +210,8 @@ return [2 3]
               (swap! id-set #(conj % s))
               (recur (inc i)))))))))
 
-(defn- uuid-tree->node-tree [uuid-tree format page-block]
-  (let [contentmap (contentmap)
-        content-tree
-        (loop [loc (zip/vector-zip uuid-tree)]
-          (if (zip/end? loc)
-            (zip/root loc)
-            (cond
-              (string? (zip/node loc))
-              (recur (zip/next
-                      (zip/replace
-                       loc
-                       (property/insert-property
-                        format
-                        (property/remove-id-property
-                         format
-                         (.toString (.get contentmap (zip/node loc))))
-                        "ID" (zip/node loc)))))
-              :else (recur (zip/next loc)))))
-        node-tree
+(defn- content-tree->node-tree [content-tree format page-block]
+  (let [node-tree
         (loop [loc (zip/vector-zip content-tree)]
           (if (zip/end? loc)
             (zip/root loc)
@@ -259,6 +243,26 @@ return [2 3]
               :else (recur (zip/next loc)))))]
     node-tree))
 
+(defn- uuid-tree->node-tree [uuid-tree format page-block]
+  (let [contentmap (contentmap)
+        content-tree
+        (loop [loc (zip/vector-zip uuid-tree)]
+          (if (zip/end? loc)
+            (zip/root loc)
+            (cond
+              (string? (zip/node loc))
+              (recur (zip/next
+                      (zip/replace
+                       loc
+                       (property/insert-property
+                        format
+                        (property/remove-id-property
+                         format
+                         (.toString (.get contentmap (zip/node loc))))
+                        "ID" (zip/node loc)))))
+              :else (recur (zip/next loc)))))]
+    (content-tree->node-tree content-tree format page-block)))
+
 (defn- ->content-map [blocks map]
   (clojure.walk/postwalk (fn [v]
                            (when (and (map? v) (:block/uuid v))
@@ -404,7 +408,6 @@ return [2 3]
                                    :block/uuid (uuid id)} )
         new-node (outliner-core/block new-block)
         sibling? (not= parent-id left-id)]
-    (def zzz [new-node target-node sibling?])
     (outliner-core/insert-node new-node target-node sibling?)
     (db/refresh! (state/get-current-repo) {:key :block/insert :data [new-block]})))
 
@@ -580,6 +583,7 @@ return [2 3]
         (js/console.trace)))))
 
 
+
 ;;;;;;;;;;;;;;;;;;;;;;;;;;
 ;; outliner op + yjs op ;;
 ;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -630,36 +634,39 @@ return [2 3]
         (insert-nodes-aux structs pos struct)
         (assoc-contents contents (contentmap))))))
 
-(defn insert-nodes-op [new-nodes-tree target-node sibling?]
-  (let [target-block (:data target-node)]
-    (when-some [page-name (or (:block/name target-block)
-                              (:block/name (db/entity (:db/id (:block/page target-block)))))]
-      (let [struct (structarray page-name)
-            block-page (:block/page target-block)
-            block-file (:block/file target-block)
-            new-nodes-tree*
-            (clojure.walk/postwalk (fn [node]
-                                     (if (instance? outliner-core/Block node)
-                                       (let [block (:data node)
-                                             id (str (:block/uuid block))
-                                             content (property/insert-property
-                                                      :markdown
-                                                      (property/remove-id-property :markdown  (:block/content block))
-                                                      "ID" id)]
-                                         (outliner-core/block
-                                          (content->block content :markdown
-                                                          {:block/page block-page
-                                                           :block/uuid (uuid id)
-                                                           :block/file block-file})))
-                                       node))
-                               new-nodes-tree)]
-        (insert-nodes-yjs struct new-nodes-tree* (str (:block/uuid target-block)) sibling?)
-        (distinct-struct struct (atom #{}))
-        (merge-doc @doc-remote @doc-local)
-        (when *debug*
-          (validate-struct struct)
-          (validate-no-left-conflict page-name))
-        (outliner-core/insert-nodes new-nodes-tree* target-node sibling?)))))
+(defn insert-nodes-op
+  ([new-nodes-tree target-node sibling?]
+   (insert-nodes-op new-nodes-tree target-node sibling? {:skip-undo? false}))
+  ([new-nodes-tree target-node sibling? {:keys [skip-undo?]}]
+   (let [target-block (:data target-node)]
+     (when-some [page-name (or (:block/name target-block)
+                               (:block/name (db/entity (:db/id (:block/page target-block)))))]
+       (let [struct (structarray page-name)
+             block-page (:block/page target-block)
+             block-file (:block/file target-block)
+             new-nodes-tree*
+             (clojure.walk/postwalk (fn [node]
+                                      (if (instance? outliner-core/Block node)
+                                        (let [block (:data node)
+                                              id (str (:block/uuid block))
+                                              content (property/insert-property
+                                                       :markdown
+                                                       (property/remove-id-property :markdown  (:block/content block))
+                                                       "ID" id)]
+                                          (outliner-core/block
+                                           (content->block content :markdown
+                                                           {:block/page block-page
+                                                            :block/uuid (uuid id)
+                                                            :block/file block-file})))
+                                        node))
+                                    new-nodes-tree)]
+         (insert-nodes-yjs struct new-nodes-tree* (str (:block/uuid target-block)) sibling?)
+         (distinct-struct struct (atom #{}))
+         (merge-doc @doc-remote @doc-local)
+         (when *debug*
+           (validate-struct struct)
+           (validate-no-left-conflict page-name))
+         (outliner-core/insert-nodes new-nodes-tree* target-node sibling?))))))
 
 (defn insert-node-yjs [struct new-node target-uuid sibling?]
   (insert-nodes-yjs struct [new-node] target-uuid sibling?))
@@ -788,32 +795,39 @@ return [2 3]
   (let [delete-ids (delete-node-struct-yjs struct id children?)]
     (dissoc-contents delete-ids (contentmap))))
 
-(defn delete-node-op [node children?]
-  (let [block (:data node)]
-    (when-some [page-name (:block/name (db/entity (:db/id (:block/page block))))]
-      (let [uuid (str (:block/uuid block))
-            struct (structarray page-name)]
-        (println "[YJS] delete-node-op: " uuid children?)
-        (delete-node-yjs struct uuid children?)
-        (merge-doc @doc-remote @doc-local)
-        (when *debug*
-          (validate-struct struct)
-          (validate-no-left-conflict page-name))
-        (outliner-core/delete-node node children?)))))
-
-(defn save-node-op [node]
-  (let [block (:data node)
-        contentmap (contentmap)]
-    (when-some [page-name (:block/name (db/entity (:db/id (:block/page block))))]
-      (when-some [block-uuid (:block/uuid block)]
-        (let [struct (structarray page-name)]
-          (.set contentmap (str block-uuid) (:block/content block))
-          (distinct-struct struct (atom #{}))
-          (merge-doc @doc-remote @doc-local)
-          (when *debug*
-            (validate-struct struct)
-            (validate-no-left-conflict page-name))
-          (outliner-core/save-node node))))))
+(defn delete-node-op
+  ([node children?]
+   (delete-node-op node children? {:skip-undo? false}))
+  ([node children? {:keys [skip-undo?]}]
+   (let [block (:data node)]
+     (when-some [page-name (:block/name (db/entity (:db/id (:block/page block))))]
+       (let [uuid (str (:block/uuid block))
+             struct (structarray page-name)]
+         (println "[YJS] delete-node-op: " uuid children?)
+         (delete-node-yjs struct uuid children?)
+         (merge-doc @doc-remote @doc-local)
+         (when *debug*
+           (validate-struct struct)
+           (validate-no-left-conflict page-name))
+         (outliner-core/delete-node node children? {:skip-undo? skip-undo?}))))))
+
+(defn save-node-op
+  ([node]
+   (save-node-op node {:skip-undo? false}))
+  ([node {:keys [skip-undo?]}]
+   (let [block (:data node)
+         contentmap (contentmap)]
+     (when-some [page-name (:block/name (db/entity (:db/id (:block/page block))))]
+       (when-some [block-uuid (:block/uuid block)]
+         (let [struct (structarray page-name)]
+           (.set contentmap (str block-uuid) (:block/content block))
+           (distinct-struct struct (atom #{}))
+           (merge-doc @doc-remote @doc-local)
+           (println "[YJS] save-node-op: " (str block-uuid))
+           (when *debug*
+             (validate-struct struct)
+             (validate-no-left-conflict page-name))
+           (outliner-core/save-node node {:skip-undo? skip-undo?})))))))
 
 (defn- outdentable? [pos]
   (> (count pos) 1))
@@ -978,6 +992,167 @@ return [2 3]
 ;; (defn move-node-op [node up?]
 ;;   (outliner-core/move-node node up?))
 
+
+;;;;;;;;;;;;;;;
+;; undo/redo ;;
+;;;;;;;;;;;;;;;
+
+(defn- block-tree->content-tree [tree format]
+  (let [loc (zip/vector-zip tree)]
+    (loop [loc loc]
+      (if (zip/end? loc)
+        (zip/root loc)
+        (cond
+          (map? (zip/node loc))
+          (let [block (zip/node loc)
+                uuid (str (:block/uuid block))
+                content (:block/content block)]
+            (recur
+             (zip/next
+              (zip/replace loc
+                           (property/insert-property
+                            format
+                            (property/remove-id-property format content)
+                            "ID" uuid)))))
+          :else
+          (recur (zip/next loc)))))))
+
+(defn undo-save-node [page-name txn-meta]
+  {:pre [(= :save-node (:outliner-op txn-meta))
+         (= page-name (get-in txn-meta [:other-meta :page-name]))]}
+  (let [node-id (get-in txn-meta [:other-meta :node-id])
+        format :markdown
+        content (get-in txn-meta [:other-meta :node-content-before])
+        content* (property/insert-property
+                  format
+                  (property/remove-id-property format content)
+                  "ID" (str node-id))
+        struct (structarray page-name)
+        page-block (db/pull (:db/id (db/get-page page-name)))
+        format :markdown
+        node (first (content-tree->node-tree [content*] format page-block))]
+    (when (find-pos struct (str node-id))
+      (save-node-op node {:skip-undo? true}))))
+
+(defn redo-save-node [page-name txn-meta]
+  {:pre [(= :save-node (:outliner-op txn-meta))
+         (= page-name (get-in txn-meta [:other-meta :page-name]))]}
+  (let [node-id (get-in txn-meta [:other-meta :node-id])
+        format :markdown
+        content (get-in txn-meta [:other-meta :node-content-after])
+        content* (property/insert-property
+                  format
+                  (property/remove-id-property format content)
+                  "ID" (str node-id))
+        struct (structarray page-name)
+        page-block (db/pull (:db/id (db/get-page page-name)))
+        node (first (content-tree->node-tree [content*] format page-block))]
+    (when (find-pos struct (str node-id))
+      (save-node-op node {:skip-undo? true}))))
+
+(defn undo-insert-node [page-name txn-meta]
+  {:pre [(= :insert-node (:outliner-op txn-meta))
+         (= page-name (get-in txn-meta [:other-meta :page-name]))]}
+  (let [node-id (get-in txn-meta [:other-meta :node-id])
+        struct (structarray page-name)
+        node (outliner-core/block (db/pull [:block/uuid node-id]))]
+    (when (find-pos struct (str node-id))
+      (delete-node-op node false {:skip-undo? true}))))
+
+(defn redo-insert-node [page-name txn-meta]
+  {:pre [(= :insert-node (:outliner-op txn-meta))
+         (= page-name (get-in txn-meta [:other-meta :page-name]))]}
+  (let [format :markdown
+        node-id (get-in txn-meta [:other-meta :node-id])
+        target-id (get-in txn-meta [:other-meta :target-id])
+        target-node (outliner-core/block (db/pull [:block/uuid target-id]))
+        sibling? (get-in txn-meta [:other-meta :sibling?])
+        content (get-in txn-meta [:other-meta :node-content])
+        content* (property/insert-property
+                  format
+                  (property/remove-id-property format content)
+                  "ID" (str node-id))
+        struct (structarray page-name)
+        page-block (db/pull (:db/id (db/get-page page-name)))
+        node-tree (content-tree->node-tree [content*] format page-block)]
+    (when (find-pos struct (str target-id))
+      (insert-nodes-op node-tree target-node sibling? {:skip-undo? true}))))
+
+(defn undo-delete-node [page-name txn-meta]
+  {:pre [(= :delete-node (:outliner-op txn-meta))
+         (= page-name (get-in txn-meta [:other-meta :page-name]))]}
+  (let [node-id (get-in txn-meta [:other-meta :node-id])
+        content-block-tree (get-in txn-meta [:other-meta :tree])
+        children? (get-in txn-meta [:other-meta :children?])
+        left-id (get-in txn-meta [:other-meta :left-id])
+        parent-id (get-in txn-meta [:other-meta :parent-id])
+        [target-id sibling?] (if (= left-id parent-id)
+                               [parent-id false]
+                               [left-id true])
+        target-node (outliner-core/block (db/pull [:block/uuid target-id]))
+        page-block (db/pull (:db/id (db/get-page page-name)))
+        node-tree (content-tree->node-tree
+                   (block-tree->content-tree content-block-tree :markdown)
+                   :markdown page-block)]
+    (insert-nodes-op node-tree target-node sibling? {:skip-undo? true})))
+
+(defn redo-delete-node [page-name txn-meta]
+  {:pre [(= :delete-node (:outliner-op txn-meta))
+         (= page-name (get-in txn-meta [:other-meta :page-name]))]}
+  (let [node-id (get-in txn-meta [:other-meta :node-id])
+        node (outliner-core/block (db/pull [:block/uuid node-id]))
+        children? (get-in txn-meta [:other-meta :children?])]
+    (delete-node-op node children? {:skip-undo? true})))
+
+(defn undo-op [page-name txn-meta]
+  (case (:outliner-op txn-meta)
+    :insert-node
+    (undo-insert-node page-name txn-meta)
+    :delete-node
+    (undo-delete-node page-name txn-meta)
+    :save-node
+    (undo-save-node page-name txn-meta)
+    (println "[UNDO]" page-name (:outliner-op txn-meta))))
+
+(defn redo-op [page-name txn-meta]
+  (case (:outliner-op txn-meta)
+    :insert-node
+    (redo-insert-node page-name txn-meta)
+    :delete-node
+    (redo-delete-node page-name txn-meta)
+    :save-node
+    (redo-save-node page-name txn-meta)
+    (println "[REDO]" page-name (:outliner-op txn-meta))))
+
+(defn undo []
+  (let [[e prev-e] (undo-redo/pop-undo)]
+    (when e
+      (let [{:keys [blocks txs]} e
+            editor-cursor
+            (if (= (get-in e [:editor-cursor :last-edit-block :block/uuid])
+                   (get-in prev-e [:editor-cursor :last-edit-block :block/uuid])) ; same block
+              (:editor-cursor prev-e)
+              (:editor-cursor e))]
+        (undo-redo/push-redo e)
+        (let [page-name
+              (or (:block/name (first blocks))
+                  (:block/name (db/entity (:db/id (:block/page (first blocks))))))]
+          (undo-op page-name e))
+        (let [blocks
+              (map (fn [x] (undo-redo/get-by-id [:block/uuid (:block/uuid x)])) blocks)]
+          (undo-redo/refresh! {:key :block/change :data (vec blocks)}))
+        (assoc e :editor-cursor editor-cursor)))))
+
+(defn redo []
+  (when-let [{:keys [blocks txs] :as e} (undo-redo/pop-redo)]
+    (undo-redo/push-undo e)
+    (let [page-name (or (:block/name (first blocks))
+                        (:block/name (db/entity (:db/id (:block/page (first blocks))))))]
+      (redo-op page-name e))
+    (let [blocks (map (fn [x] (undo-redo/get-by-id [:block/uuid (:block/uuid x)])) blocks)]
+      (undo-redo/refresh! {:key :block/change :data (vec blocks)}))
+    e))
+
 ;;;;;;;;;;;;;;;;;;;;;;;;;
 ;; functions for debug ;;
 ;;;;;;;;;;;;;;;;;;;;;;;;;