mirror of
https://github.com/logseq/logseq.git
synced 2026-04-27 15:45:00 +00:00
* enhance(perf): improve performance for both insert and delete * fix: remember cursor pos before executing the body in a transaction Otherwise, the edit-block and position could be changed * fix: disable delete-concat when there's no child or right sibling --------- Co-authored-by: Gabriel Horner <97210743+logseq-cldwalker@users.noreply.github.com> Co-authored-by: Gabriel Horner <gabriel@logseq.com>
269 lines
8.9 KiB
Clojure
269 lines
8.9 KiB
Clojure
(ns frontend.modules.editor.undo-redo
|
|
(:require [datascript.core :as d]
|
|
[frontend.db :as db]
|
|
[frontend.db.conn :as conn]
|
|
[frontend.handler.notification :as notification]
|
|
[frontend.modules.datascript-report.core :as db-report]
|
|
[frontend.util.page :as page-util]
|
|
[frontend.state :as state]
|
|
[clojure.set :as set]
|
|
[medley.core :as medley]))
|
|
|
|
;;;; APIs
|
|
|
|
(def ^:private undo-redo-states (atom {}))
|
|
(def *pause-listener (atom false))
|
|
|
|
(defn get-state
|
|
[]
|
|
(let [repo (state/get-current-repo)]
|
|
(assert (string? repo) "Repo should satisfy string?")
|
|
(if-let [state (get @undo-redo-states repo)]
|
|
state
|
|
(let [new-state {:undo-stack (atom [])
|
|
:redo-stack (atom [])}]
|
|
(swap! undo-redo-states assoc repo new-state)
|
|
new-state))))
|
|
|
|
(defn- get-undo-stack
|
|
[]
|
|
(-> (get-state) :undo-stack))
|
|
|
|
(defn- get-redo-stack
|
|
[]
|
|
(-> (get-state) :redo-stack))
|
|
|
|
(defn push-undo
|
|
[txs]
|
|
(let [undo-stack (get-undo-stack)]
|
|
(swap! undo-stack conj txs)))
|
|
|
|
(comment
|
|
(defn get-content-from-txs
|
|
"For test."
|
|
[txs]
|
|
(filterv (fn [[_ a & y]]
|
|
(= :block/content a))
|
|
txs))
|
|
|
|
(defn get-content-from-stack
|
|
"For test."
|
|
[stack]
|
|
(mapv #(get-content-from-txs (:txs %)) stack))
|
|
|
|
(debug/pprint "pop entity" (get-content-from-txs (:txs removed-e)))
|
|
(debug/pprint "undo-stack" (get-content-from-stack @undo-stack)))
|
|
|
|
(defn pop-undo
|
|
[]
|
|
(let [undo-stack (get-undo-stack)]
|
|
(when-let [stack @undo-stack]
|
|
(when (seq stack)
|
|
(let [removed-e (peek stack)
|
|
popped-stack (pop stack)]
|
|
(reset! undo-stack popped-stack)
|
|
removed-e)))))
|
|
|
|
(defn push-redo
|
|
[txs]
|
|
(let [redo-stack (get-redo-stack)]
|
|
(swap! redo-stack conj txs)))
|
|
|
|
(defn pop-redo
|
|
[]
|
|
(let [redo-stack (get-redo-stack)]
|
|
(when-let [removed-e (peek @redo-stack)]
|
|
(swap! redo-stack pop)
|
|
removed-e)))
|
|
|
|
(defn page-pop-redo
|
|
[page-id]
|
|
(prn "[debug] redo: " (:block/original-name (db/pull page-id)))
|
|
(when-let [redo-stack (get-redo-stack)]
|
|
(when-let [stack @redo-stack]
|
|
(when (seq stack)
|
|
(let [reversed-stack (medley/indexed (reverse stack))
|
|
idx (some (fn [[idx item]]
|
|
(some #(when (or (= (:db/id %) page-id)
|
|
(= (:db/id (:block/page %)) page-id)) idx) (:blocks item))) reversed-stack)]
|
|
(when idx
|
|
(let [idx' (- (count stack) idx 1)
|
|
before (subvec stack 0 idx')
|
|
after (subvec stack (inc idx'))
|
|
others (vec (concat before after))]
|
|
(reset! redo-stack others)
|
|
(prn "[debug] redo remove: " (nth stack idx'))
|
|
(nth stack idx'))))))))
|
|
|
|
(defn- smart-pop-redo
|
|
[]
|
|
(if (:history/page-only-mode? @state/state)
|
|
(if-let [page-id (page-util/get-editing-page-id)]
|
|
(page-pop-redo page-id)
|
|
(pop-redo))
|
|
(pop-redo)))
|
|
|
|
(defn reset-redo
|
|
[]
|
|
(let [redo-stack (get-redo-stack)]
|
|
(reset! redo-stack [])))
|
|
|
|
(defn get-txs
|
|
[redo? txs]
|
|
(let [txs (if redo? txs (reverse txs))]
|
|
(mapv (fn [[id attr value tx add?]]
|
|
(let [op (cond
|
|
(and redo? add?) :db/add
|
|
(and (not redo?) add?) :db/retract
|
|
(and redo? (not add?)) :db/retract
|
|
(and (not redo?) (not add?)) :db/add)]
|
|
[op id attr value tx]))
|
|
txs)))
|
|
|
|
;;;; Invokes
|
|
|
|
(defn- transact!
|
|
[txs tx-meta]
|
|
(let [conn (conn/get-db false)]
|
|
(d/transact! conn txs tx-meta)))
|
|
|
|
(defn- page-pop-undo
|
|
[page-id]
|
|
(let [undo-stack (get-undo-stack)]
|
|
(when-let [stack @undo-stack]
|
|
(when (seq stack)
|
|
(let [reversed-stack (medley/indexed (reverse stack))
|
|
idx (some (fn [[idx item]]
|
|
(some #(when (or (= (:db/id %) page-id)
|
|
(= (:db/id (:block/page %)) page-id)) idx) (:blocks item))) reversed-stack)]
|
|
(when idx
|
|
(let [idx' (- (count stack) idx 1)
|
|
before (subvec stack 0 idx')
|
|
after (subvec stack (inc idx'))
|
|
others (vec (concat before after))]
|
|
(reset! undo-stack others)
|
|
(prn "[debug] undo remove: " (nth stack idx'))
|
|
(nth stack idx'))))))))
|
|
|
|
(defn- smart-pop-undo
|
|
[]
|
|
(if (:history/page-only-mode? @state/state)
|
|
(if-let [page-id (page-util/get-editing-page-id)]
|
|
(page-pop-undo page-id)
|
|
(pop-undo))
|
|
(pop-undo)))
|
|
|
|
(defn- set-editor-content!
|
|
"Prevent block auto-save during undo/redo."
|
|
[]
|
|
(when-let [block (state/get-edit-block)]
|
|
(state/set-edit-content! (state/get-edit-input-id)
|
|
(:block/content (db/entity (:db/id block))))))
|
|
|
|
(defn- get-next-tx-editor-cursor
|
|
[tx-id]
|
|
(let [result (->> (sort (keys (:history/tx->editor-cursor @state/state)))
|
|
(split-with #(not= % tx-id))
|
|
second)]
|
|
(when (> (count result) 1)
|
|
(when-let [next-tx-id (nth result 1)]
|
|
(get-in @state/state [:history/tx->editor-cursor next-tx-id])))))
|
|
|
|
(defn- get-previous-tx-id
|
|
[tx-id]
|
|
(let [result (->> (sort (keys (:history/tx->editor-cursor @state/state)))
|
|
(split-with #(not= % tx-id))
|
|
first)]
|
|
(when (>= (count result) 1)
|
|
(last result))))
|
|
|
|
(defn- get-previous-tx-editor-cursor
|
|
[tx-id]
|
|
(when-let [prev-tx-id (get-previous-tx-id tx-id)]
|
|
(get-in @state/state [:history/tx->editor-cursor prev-tx-id])))
|
|
|
|
(defn undo
|
|
[]
|
|
(when-let [e (smart-pop-undo)]
|
|
(let [{:keys [txs tx-meta tx-id]} e
|
|
new-txs (get-txs false txs)
|
|
current-editor-cursor (get-in @state/state [:history/tx->editor-cursor tx-id])
|
|
save-block? (= (:outliner-op tx-meta) :save-block)
|
|
prev-editor-cursor (get-previous-tx-editor-cursor tx-id)
|
|
editor-cursor (if (and save-block?
|
|
(= (:block/uuid (:last-edit-block prev-editor-cursor))
|
|
(:block/uuid (state/get-edit-block))))
|
|
prev-editor-cursor
|
|
current-editor-cursor)]
|
|
(push-redo e)
|
|
(transact! new-txs (merge {:undo? true}
|
|
tx-meta
|
|
(select-keys e [:pagination-blocks-range])))
|
|
(set-editor-content!)
|
|
(when (:whiteboard/transact? tx-meta)
|
|
(state/pub-event! [:whiteboard/undo e]))
|
|
(assoc e
|
|
:txs-op new-txs
|
|
:editor-cursor editor-cursor))))
|
|
|
|
(defn redo
|
|
[]
|
|
(when-let [{:keys [txs tx-meta tx-id] :as e} (smart-pop-redo)]
|
|
(let [new-txs (get-txs true txs)
|
|
current-editor-cursor (get-in @state/state [:history/tx->editor-cursor tx-id])
|
|
editor-cursor (if (= (:outliner-op tx-meta) :save-block)
|
|
current-editor-cursor
|
|
(get-next-tx-editor-cursor tx-id))]
|
|
(push-undo e)
|
|
(transact! new-txs (merge {:redo? true}
|
|
tx-meta
|
|
(select-keys e [:pagination-blocks-range])))
|
|
(set-editor-content!)
|
|
(when (:whiteboard/transact? tx-meta)
|
|
(state/pub-event! [:whiteboard/redo e]))
|
|
(assoc e
|
|
:txs-op new-txs
|
|
:editor-cursor editor-cursor))))
|
|
|
|
(defn toggle-undo-redo-mode!
|
|
[]
|
|
(swap! state/state update :history/page-only-mode? not)
|
|
(let [mode (if (:history/page-only-mode? @state/state) "Page only" "Global")]
|
|
(notification/show!
|
|
[:p (str "Undo/redo mode: " mode)])))
|
|
|
|
(defn pause-listener!
|
|
[]
|
|
(reset! *pause-listener true))
|
|
|
|
(defn resume-listener!
|
|
[]
|
|
(reset! *pause-listener false))
|
|
|
|
(defn listen-db-changes!
|
|
[{:keys [tx-data tx-meta] :as tx-report}]
|
|
(when (and (seq tx-data)
|
|
(not (or (:undo? tx-meta)
|
|
(:redo? tx-meta)))
|
|
(not @*pause-listener)
|
|
(not (set/subset?
|
|
(set (map :a tx-data))
|
|
#{:block/created-at :block/updated-at})))
|
|
(reset-redo)
|
|
(if (:replace? tx-meta)
|
|
(let [removed-e (pop-undo)
|
|
entity (update removed-e :txs concat tx-data)]
|
|
(push-undo entity))
|
|
(let [updated-blocks (db-report/get-blocks tx-report)
|
|
entity {:tx-id (get-in tx-report [:tempids :db/current-tx])
|
|
:blocks updated-blocks
|
|
:txs tx-data
|
|
:tx-meta tx-meta
|
|
:pagination-blocks-range (get-in [:ui/pagination-blocks-range (get-in tx-report [:db-after :max-tx])] @state/state)
|
|
:app-state (select-keys @state/state
|
|
[:route-match
|
|
:ui/sidebar-open?
|
|
:ui/sidebar-collapsed-blocks
|
|
:sidebar/blocks])}]
|
|
(push-undo entity)))))
|