Add the ability for save and restore selection state in undo/redo (#8652)

*  Capture selection state before changes are applied

Save current selection IDs in commit-changes so undo entries
can track what was selected before each action.

*  Save and restore selection state in undo/redo

Extend undo entry with selected-before and selected-after fields.
On undo, restore selection to what it was before the action.
On redo, restore selection to what it was after the action.
Handles single entries, stacked entries, accumulated transactions,
and undo groups.

Fixes #6007

* ♻️ Wire selected-before through workspace undo stream

Pass the captured selection state from commit data into
the undo entry so it is stored alongside changes.

* 🐛 Fix unmatched delimiter in changes.cljs

* 🐛 Pass selected-before through commit event to undo entry

selected-before was captured in commit-changes but dropped by the
commit function since it was missing from the destructuring and the
commit map. This caused restore-selection to receive nil on undo.

---------

Signed-off-by: eureka928 <meobius123@gmail.com>
Co-authored-by: Mihai <noreply@github.com>
This commit is contained in:
Dream 2026-04-07 10:30:47 -04:00 committed by GitHub
parent 48e8c0bc65
commit 0c08dfb13d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 62 additions and 33 deletions

View File

@ -122,7 +122,8 @@
(defn commit
"Create a commit event instance"
[{:keys [commit-id redo-changes undo-changes origin save-undo? features
file-id file-revn file-vern undo-group tags stack-undo? source ignore-wasm?]}]
file-id file-revn file-vern undo-group tags stack-undo? source ignore-wasm?
selected-before]}]
(assert (cpc/check-changes redo-changes)
"expect valid vector of changes for redo-changes")
@ -148,7 +149,8 @@
:undo-group undo-group
:tags tags
:stack-undo? stack-undo?
:ignore-wasm? ignore-wasm?}]
:ignore-wasm? ignore-wasm?
:selected-before selected-before}]
(ptk/reify ::commit
cljs.core/IDeref
@ -205,15 +207,17 @@
;; Prevent commit changes by a viewer team member (it really should never happen)
(when (:can-edit permissions)
(rx/of (-> params
(assoc :undo-group undo-group)
(assoc :features features)
(assoc :tags tags)
(assoc :stack-undo? stack-undo?)
(assoc :save-undo? save-undo?)
(assoc :file-id file-id)
(assoc :file-revn (resolve-file-revn state file-id))
(assoc :file-vern (resolve-file-vern state file-id))
(assoc :undo-changes uchg)
(assoc :redo-changes rchg)
(commit))))))))
(let [selected (dm/get-in state [:workspace-local :selected])]
(rx/of (-> params
(assoc :undo-group undo-group)
(assoc :features features)
(assoc :tags tags)
(assoc :stack-undo? stack-undo?)
(assoc :save-undo? save-undo?)
(assoc :file-id file-id)
(assoc :file-revn (resolve-file-revn state file-id))
(assoc :file-vern (resolve-file-vern state file-id))
(assoc :undo-changes uchg)
(assoc :redo-changes rchg)
(assoc :selected-before selected)
(commit)))))))))

View File

@ -484,12 +484,13 @@
(rx/filter dch/commit?)
(rx/map deref)
(rx/mapcat
(fn [{:keys [save-undo? undo-changes redo-changes undo-group tags stack-undo?]}]
(fn [{:keys [save-undo? undo-changes redo-changes undo-group tags stack-undo? selected-before]}]
(if (and save-undo? (seq undo-changes))
(let [entry {:undo-changes undo-changes
:redo-changes redo-changes
:undo-group undo-group
:tags tags}]
:tags tags
:selected-before selected-before}]
(rx/of (dwu/append-undo entry stack-undo?)))
(rx/empty))))))
(rx/take-until stoper-s))))

View File

@ -60,7 +60,9 @@
[:undo-changes [:vector cpc/schema:change]]
[:redo-changes [:vector cpc/schema:change]]
[:undo-group ::sm/uuid]
[:tags [:set :keyword]]])
[:tags [:set :keyword]]
[:selected-before {:optional true} [:maybe [:set ::sm/uuid]]]
[:selected-after {:optional true} [:maybe [:set ::sm/uuid]]]])
(def check-undo-entry
(sm/check-fn schema:undo-entry))
@ -103,24 +105,28 @@
(defn- stack-undo-entry
"Extends the current undo entry in the workspace with new changes if it
exists, or creates a new entry if it doesn't."
[state {:keys [undo-changes redo-changes] :as entry}]
[state {:keys [undo-changes redo-changes selected-after] :as entry}]
(let [index (get-in state [:workspace-undo :index] -1)]
(if (>= index 0)
(update-in state [:workspace-undo :items index]
(fn [item]
(-> item
(update :undo-changes #(into undo-changes %))
(update :redo-changes #(into % redo-changes)))))
(update :redo-changes #(into % redo-changes))
(assoc :selected-after selected-after))))
(add-undo-entry state entry))))
(defn- accumulate-undo-entry
"Extends the current undo transaction with new changes."
[state {:keys [undo-changes redo-changes undo-group tags]}]
[state {:keys [undo-changes redo-changes undo-group tags selected-before selected-after]}]
(-> state
(update-in [:workspace-undo :transaction :undo-changes] #(into undo-changes %))
(update-in [:workspace-undo :transaction :redo-changes] #(into % redo-changes))
(cond-> (nil? (get-in state [:workspace-undo :transaction :undo-group]))
(assoc-in [:workspace-undo :transaction :undo-group] undo-group))
(cond-> (nil? (get-in state [:workspace-undo :transaction :selected-before]))
(assoc-in [:workspace-undo :transaction :selected-before] selected-before))
(assoc-in [:workspace-undo :transaction :selected-after] selected-after)
(assoc-in [:workspace-undo :transaction :tags] tags)))
(defn append-undo
@ -137,18 +143,20 @@
(ptk/reify ::append-undo
ptk/UpdateEvent
(update [_ state]
(cond
(and (get-in state [:workspace-undo :transaction])
(or (not stack?)
(d/not-empty? (get-in state [:workspace-undo :transaction :undo-changes]))
(d/not-empty? (get-in state [:workspace-undo :transaction :redo-changes]))))
(accumulate-undo-entry state entry)
(let [selected-after (dm/get-in state [:workspace-local :selected])
entry (assoc entry :selected-after selected-after)]
(cond
(and (get-in state [:workspace-undo :transaction])
(or (not stack?)
(d/not-empty? (get-in state [:workspace-undo :transaction :undo-changes]))
(d/not-empty? (get-in state [:workspace-undo :transaction :redo-changes]))))
(accumulate-undo-entry state entry)
stack?
(stack-undo-entry state entry)
stack?
(stack-undo-entry state entry)
:else
(add-undo-entry state entry)))))
:else
(add-undo-entry state entry))))))
(def empty-tx
{:undo-changes [] :redo-changes []})
@ -234,6 +242,16 @@
(rx/map first)
(rx/map commit-undo-transaction))))))
(defn- restore-selection
"Restores the selection state from an undo entry."
[selected-ids]
(ptk/reify ::restore-selection
ptk/UpdateEvent
(update [_ state]
(if (some? selected-ids)
(assoc-in state [:workspace-local :selected] selected-ids)
state))))
(defn undo-to-index
"Repeat undoing or redoing until dest-index is reached."
[dest-index]
@ -302,12 +320,15 @@
(find-first-group-idx index))]
(if undo-group
(rx/of (undo-to-index (dec undo-group-index)))
(let [first-item (get items undo-group-index)]
(rx/of (undo-to-index (dec undo-group-index))
(restore-selection (:selected-before first-item))))
(rx/of (materialize-undo changes (dec index))
(dch/commit-changes {:redo-changes changes
:undo-changes []
:save-undo? false
:origin it})
(restore-selection (:selected-before item))
(assure-valid-current-page)))))))))))
(def redo
@ -337,12 +358,15 @@
redo-group-index (when undo-group
(find-last-group-idx (inc index)))]
(if undo-group
(rx/of (undo-to-index redo-group-index))
(let [last-item (get items redo-group-index)]
(rx/of (undo-to-index redo-group-index)
(restore-selection (:selected-after last-item))))
(rx/of (materialize-undo changes (inc index))
(dch/commit-changes {:redo-changes changes
:undo-changes []
:origin it
:save-undo? false})))))))))))
:save-undo? false})
(restore-selection (:selected-after item))))))))))))
(defn- assure-valid-current-page
[]