wip: undo redo editor cursor

pull/11177/head
Tienson Qin 2024-04-26 14:44:00 +08:00
parent 01382219ca
commit 81c267ed58
8 changed files with 118 additions and 69 deletions

View File

@ -688,18 +688,22 @@
(rtc-get-block-update-log
[_this block-uuid]
(transit/write transit-w (rtc-core/get-block-update-log (uuid block-uuid))))
(ldb/write-transit-str (rtc-core/get-block-update-log (uuid block-uuid))))
(undo
[_this repo page-block-uuid-str]
(when-let [conn (worker-state/get-datascript-conn repo)]
(undo-redo/undo repo (uuid page-block-uuid-str) conn))
nil)
(ldb/write-transit-str (undo-redo/undo repo (uuid page-block-uuid-str) conn))))
(redo
[_this repo page-block-uuid-str]
(when-let [conn (worker-state/get-datascript-conn repo)]
(undo-redo/redo repo (uuid page-block-uuid-str) conn)))
(ldb/write-transit-str (undo-redo/redo repo (uuid page-block-uuid-str) conn))))
(record-editor-info
[_this repo page-block-uuid-str editor-info-str]
(undo-redo/record-editor-info! repo (uuid page-block-uuid-str) (ldb/read-transit-str editor-info-str))
nil)
(keep-alive
[_this]

View File

@ -169,14 +169,13 @@
(when repo
(state/set-editor-last-input-time! repo (util/time-ms))))
;; TODO: remove retry
(defn- edit-block-aux
[repo block content text-range {:keys [container-id]}]
(when block
(state/clear-edit!)
(let [container-id (or container-id
@(:editor/container-id @state/state))]
(state/set-editing! "" content block text-range {:container-id container-id}))
(state/set-editing! (str "edit-block-" (:block/uuid block)) content block text-range {:container-id container-id}))
(mark-last-input-time! repo)))
(defn sanity-block-content

View File

@ -3,7 +3,8 @@
[frontend.state :as state]
[frontend.util :as util]
[goog.dom :as gdom]
[frontend.db :as db]))
[frontend.db :as db]
[logseq.db :as ldb]))
(defn did-mount!
[state]
@ -18,7 +19,12 @@
(when-let [element (gdom/getElement id)]
;; TODO: check whether editor is visible, do less work
(js/setTimeout #(util/scroll-editor-cursor element) 50))
(state/update-tx-after-cursor-state!))
(let [^js worker @state/*db-worker
page-id (:block/uuid (:block/page (db/entity (:db/id (state/get-edit-block)))))
repo (state/get-current-repo)]
(when page-id
(.record-editor-info worker repo (str page-id) (ldb/write-transit-str (state/get-editor-info))))))
state)
;; (defn will-remount!

View File

@ -2,40 +2,39 @@
(:require [frontend.db :as db]
[frontend.db.transact :as db-transact]
[frontend.handler.editor :as editor]
[frontend.handler.route :as route-handler]
[frontend.state :as state]
[frontend.util :as util]
[frontend.util.page :as page-util]
[goog.dom :as gdom]
[promesa.core :as p]))
[promesa.core :as p]
[logseq.db :as ldb]))
(defn restore-cursor!
[{:keys [last-edit-block container pos end-pos]} undo?]
(when (and container last-edit-block)
#_:clj-kondo/ignore
(when-let [container (gdom/getElement container)]
(when-let [block-uuid (:block/uuid last-edit-block)]
(when-let [block (db/pull [:block/uuid block-uuid])]
(editor/edit-block! block (or (when undo? pos) end-pos)
{:custom-content (:block/content block)}))))))
[{:keys [editor-cursors undo?]}]
(let [{:keys [block-uuid container-id start-pos end-pos]} (if undo? (first editor-cursors) (last editor-cursors))]
(when-let [block (db/pull [:block/uuid block-uuid])]
(editor/edit-block! block (or (when undo? start-pos) end-pos)
{:container-id container-id
:custom-content (:block/content block)}))))
(defn- get-route-data
[route-match]
(when (seq route-match)
{:to (get-in route-match [:data :name])
:path-params (:path-params route-match)
:query-params (:query-params route-match)}))
(comment
(defn- get-route-data
[route-match]
(when (seq route-match)
{:to (get-in route-match [:data :name])
:path-params (:path-params route-match)
:query-params (:query-params route-match)})))
(defn restore-app-state!
[state]
(let [route-match (:route-match state)
current-route (:route-match @state/state)
prev-route-data (get-route-data route-match)
current-route-data (get-route-data current-route)]
(when (and (not= prev-route-data current-route-data)
prev-route-data)
(route-handler/redirect! prev-route-data))
(swap! state/state merge state)))
(comment
(defn restore-app-state!
[state]
(let [route-match (:route-match state)
current-route (:route-match @state/state)
prev-route-data (get-route-data route-match)
current-route-data (get-route-data current-route)]
(when (and (not= prev-route-data current-route-data)
prev-route-data)
(route-handler/redirect! prev-route-data))
(swap! state/state merge state))))
(let [*last-request (atom nil)]
(defn undo!
@ -55,7 +54,9 @@
(state/clear-editor-action!)
(state/set-block-op-type! nil)
(let [^js worker @state/*db-worker]
(reset! *last-request (.undo worker repo current-page-uuid-str))))))))))
(reset! *last-request (.undo worker repo current-page-uuid-str))
(p/let [result @*last-request]
(restore-cursor! (ldb/read-transit-str result)))))))))))
(let [*last-request (atom nil)]
(defn redo!
@ -71,4 +72,6 @@
(util/stop e)
(state/clear-editor-action!)
(let [^js worker @state/*db-worker]
(reset! *last-request (.redo worker repo current-page-uuid-str)))))))))
(reset! *last-request (.redo worker repo current-page-uuid-str))
(p/let [result @*last-request]
(restore-cursor! (ldb/read-transit-str result))))))))))

View File

@ -4,7 +4,6 @@
[frontend.state :as state]
[datascript.core :as d]
[frontend.handler.ui :as ui-handler]
[frontend.handler.history :as history]
[frontend.handler.editor :as editor-handler]
[frontend.util :as util]))
@ -20,11 +19,6 @@
(fn [m] (assoc-in m [tx-id :before] editor-cursor)))
(state/set-state! :history/tx-before-editor-cursor nil)))
(defn restore-cursor-and-app-state!
[{:keys [editor-cursor app-state]} undo?]
(history/restore-cursor! editor-cursor undo?)
(history/restore-app-state! app-state))
(defn invoke-hooks
[{:keys [_request-id tx-meta tx-data deleted-block-uuids affected-keys blocks]}]
;; (prn :debug
@ -72,10 +66,10 @@
(state/set-state! :editor/next-edit-block nil)))
(react/refresh! repo affected-keys)
(when-let [state (:ui/restore-cursor-state @state/state)]
(when (or undo? redo?)
(restore-cursor-and-app-state! state undo?)
(state/set-state! :ui/restore-cursor-state nil)))
;; (when-let [state (:ui/restore-cursor-state @state/state)]
;; (when (or undo? redo?)
;; (restore-cursor-and-app-state! state undo?)
;; (state/set-state! :ui/restore-cursor-state nil)))
(state/set-state! :editor/start-pos nil)

View File

@ -8,9 +8,8 @@
[opts & body]
`(let [test?# frontend.util/node-test?]
(when (or test?# (db/request-finished?))
(when (nil? @(:history/tx-before-editor-cursor @state/state))
(state/set-state! :history/tx-before-editor-cursor (state/get-current-edit-block-and-position)))
(let [ops# frontend.modules.outliner.op/*outliner-ops*]
(let [ops# frontend.modules.outliner.op/*outliner-ops*
editor-info# (state/get-editor-info)]
(if ops#
(do ~@body) ; nested transact!
(binding [frontend.modules.outliner.op/*outliner-ops* (transient [])]
@ -27,7 +26,9 @@
(let [request-id# (state/get-worker-next-request-id)
request# #(.apply-outliner-ops ^Object worker# (state/get-current-repo)
(pr-str r#)
(pr-str (assoc ~opts :request-id request-id#)))
(pr-str (assoc ~opts
:request-id request-id#
:editor-info editor-info#)))
response# (state/add-worker-request! request-id# request#)]
response#))))))))))

View File

@ -1966,7 +1966,7 @@ Similar to re-frame subscriptions"
(set-selection-blocks! blocks direction)))
(defn set-editing!
[edit-input-id content block cursor-range & {:keys [move-cursor? ref container-id property-block]
[edit-input-id content block cursor-range & {:keys [move-cursor? container-id property-block]
:or {move-cursor? true}}]
(when-not (exists? js/process)
(if (> (count content)
@ -1975,11 +1975,7 @@ Similar to re-frame subscriptions"
(when (first elements)
(util/scroll-to-element (gobj/get (first elements) "id")))
(exit-editing-and-set-selected-blocks! elements))
(let [edit-input-id (if ref
(or (some-> (gobj/get ref "id") (string/replace "ls-block" "edit-block"))
edit-input-id)
edit-input-id)]
(when (and edit-input-id block
(when (and edit-input-id block
(or
(publishing-enable-editing?)
(not @publishing?)))
@ -1995,12 +1991,12 @@ Similar to re-frame subscriptions"
(if property-block
(set-editing-block-id! [container-id (:block/uuid property-block) (:block/uuid block)])
(set-editing-block-id! [container-id (:block/uuid block)]))
(set-state! :editor/container-id container-id)
(set-state! :editor/block block)
(set-state! :editor/content content :path-in-sub-atom (:block/uuid block))
(set-state! :editor/last-key-code nil)
(set-state! :editor/set-timestamp-block nil)
(set-state! :editor/cursor-range cursor-range)
(set-state! :editor/container-id container-id)
(when-let [input (gdom/getElement edit-input-id)]
(let [pos (count cursor-range)]
@ -2011,7 +2007,7 @@ Similar to re-frame subscriptions"
(cursor/move-cursor-to input pos))
(when (or (util/mobile?) (mobile-util/native-platform?))
(set-state! :mobile/show-action-bar? false))))))))))
(set-state! :mobile/show-action-bar? false)))))))))
(defn action-bar-open?
[]
@ -2364,3 +2360,11 @@ Similar to re-frame subscriptions"
(defn get-next-container-id
[]
(swap! (:ui/container-id @state) inc))
(defn get-editor-info
[]
(when-let [edit-block (get-edit-block)]
{:block-uuid (:block/uuid edit-block)
:container-id @(:editor/container-id @state)
:start-pos @(:editor/start-pos @state)
:end-pos (get-edit-pos)}))

View File

@ -42,6 +42,9 @@ 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.")
(sr/defkeyword ::record-editor-info
"record current editor and cursor")
(sr/defkeyword ::empty-undo-stack
"return by undo, when no more undo ops")
@ -89,7 +92,14 @@ when undo this op, this original entity-map will be transacted back into db")
[:block-origin-collapsed {:optional true} :boolean]
[:block-origin-link {:optional true} [:maybe :uuid]]
;; TODO: add more attrs
]]]]))
]]]
[::record-editor-info
[:cat :keyword
[:map
[:block-uuid :uuid]
[:container-id :int]
[:start-pos [:maybe :int]]
[:end-pos [:maybe :int]]]]]]))
(def ^:private undo-ops-validator (m/validator [:sequential undo-op-schema]))
@ -126,6 +136,8 @@ when undo this op, this original entity-map will be transacted back into db")
(case (first op)
::boundary [op]
::record-editor-info [op]
::insert-blocks
(keep
(fn [block-uuid]
@ -161,7 +173,8 @@ when undo this op, this original entity-map will be transacted back into db")
(some? block-origin-tags) (assoc :block-origin-tags block-origin-tags)
(some? block-origin-collapsed) (assoc :block-origin-collapsed block-origin-collapsed)
;; block-origin-link's value maybe nil, so use contains as cond
(contains? value-keys :block-origin-link) (assoc :block-origin-link block-origin-link))]]))))
(contains? value-keys :block-origin-link) (assoc :block-origin-link block-origin-link))]])
nil)))
(def ^:private apply-conj-vec (partial apply (fnil conj [])))
@ -240,6 +253,10 @@ when undo this op, this original entity-map will be transacted back into db")
(defmulti ^:private reverse-apply-op (fn [op _conn _repo] (first op)))
(defmethod reverse-apply-op :default
[_ _ _]
nil)
(defmethod reverse-apply-op ::remove-block
[op conn repo]
(let [[_ {:keys [block-uuid block-entity-map]}] op
@ -360,6 +377,10 @@ when undo this op, this original entity-map will be transacted back into db")
[:push-undo-redo {}])))))
(defmethod reverse-apply-op ::record-editor-info
[_op _conn _repo]
[:push-undo-redo {}])
(defn- sort&merge-ops
[ops]
(let [groups (group-by first ops)
@ -394,8 +415,10 @@ when undo this op, this original entity-map will be transacted back into db")
(some-> *undo-redo-info-for-test* (reset! {:op op :tx (second r)}))
(apply conj! redo-ops-to-push rev-ops)))))
(when-let [rev-ops (not-empty (sort&merge-ops (persistent! redo-ops-to-push)))]
(push-redo-ops repo page-block-uuid (cons boundary rev-ops)))
nil)
(push-redo-ops repo page-block-uuid (vec (cons boundary rev-ops))))
(let [editor-infos (filter #(= ::record-editor-info (first %)) ops)]
{:undo? true
:editor-cursors (map second editor-infos)}))
(when (empty-undo-stack? repo page-block-uuid)
(prn "No further undo information")
@ -414,8 +437,10 @@ when undo this op, this original entity-map will be transacted back into db")
(some-> *undo-redo-info-for-test* (reset! {:op op :tx (second r)}))
(apply conj! undo-ops-to-push rev-ops)))))
(when-let [rev-ops (not-empty (sort&merge-ops (persistent! undo-ops-to-push)))]
(push-undo-ops repo page-block-uuid (cons boundary rev-ops)))
nil)
(push-undo-ops repo page-block-uuid (vec (cons boundary rev-ops))))
(let [editor-infos (filter #(= ::record-editor-info (first %)) ops)]
{:redo? true
:editor-cursors (map second editor-infos)}))
(when (empty-redo-stack? repo page-block-uuid)
(prn "No further redo information")
@ -502,19 +527,32 @@ when undo this op, this original entity-map will be transacted back into db")
same-entity-datoms-coll))
(defn- generate-undo-ops
[repo db-before db-after same-entity-datoms-coll id->attr->datom gen-boundary-op?]
[repo db-before db-after same-entity-datoms-coll id->attr->datom gen-boundary-op? tx-meta]
(when-let [page-block-uuid (find-page-block-uuid db-before db-after same-entity-datoms-coll)]
(let [ops (mapcat (partial entity-datoms=>ops db-before db-after id->attr->datom) same-entity-datoms-coll)
ops (sort&merge-ops ops)]
ops (sort&merge-ops ops)
editor-info (:editor-info tx-meta)
ops' (if editor-info
(cons [::record-editor-info editor-info] ops)
ops)]
(when (seq ops)
(push-undo-ops repo page-block-uuid (if gen-boundary-op? (cons boundary ops) ops))))))
(push-undo-ops repo page-block-uuid (if gen-boundary-op? (vec (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-ops? tx-meta true)
(generate-undo-ops repo db-before db-after same-entity-datoms-coll id->attr->datom
(:gen-undo-boundary-op? tx-meta true))))
(:gen-undo-boundary-op? tx-meta true)
tx-meta)))
(defn record-editor-info!
[repo page-block-uuid editor-info]
(swap! (:undo/repo->page-block-uuid->undo-ops @worker-state/*state)
update-in [repo page-block-uuid]
(fn [stack]
(when (seq stack)
(conj (vec stack) [::record-editor-info editor-info])))))
;;; listen db changes and push undo-ops (ends)