Skip to content

Commit 28e6a4f

Browse files
committed
enable undo cursor restoration for programmatic edits
Problem: Programmatic document edits currently bypass the editor context that undo cursor restoration depends on. `Document::edit` and `edit_single` mutate the buffer and emit deltas, but they do not record `cursor_before` / `cursor_after`, so undo lacks the initiating editor state needed to restore the cursor correctly. That left floem’s examples using raw document edits from button callbacks and compensating with workarounds like disabling undo in one shared editor. Solution: Add additive editor-aware document edit APIs: `edit_from` and `edit_single_from`. `TextDocument` uses the initiating editor to capture cursor-before state, apply the edit, remap that editor’s cursor through the produced deltas, and record cursor-after state on the revision. Raw `edit` / `edit_single` remain unchanged as low-level document mutation APIs, while UI-driven edits can now preserve expected cursor and undo behavior without changing the broader document-centered reactive model. Reasoning: This keeps the architecture aligned with floem’s existing update flow. Live cursor consistency across editors still comes from document deltas and update listeners; the new APIs only add what undo needs. The examples are moved to the editor-aware path, which removes the need to suppress undo in the secondary editor and makes the intended API split explicit. Associated with issue #1058
1 parent 716c907 commit 28e6a4f

5 files changed

Lines changed: 120 additions & 46 deletions

File tree

examples/editor/src/main.rs

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
use floem::{
22
prelude::*,
33
views::editor::{
4-
command::{Command, CommandExecuted},
5-
core::{
6-
command::EditCommand, cursor::CursorAffinity, editor::EditType, selection::Selection,
7-
},
4+
core::{cursor::CursorAffinity, editor::EditType, selection::Selection},
85
text::{default_dark_color, SimpleStyling},
96
},
107
};
@@ -23,36 +20,37 @@ fn app_view() -> impl IntoView {
2320
.style(|s| s.size_full())
2421
.editor_style(default_dark_color)
2522
.editor_style(move |s| s.hide_gutter(hide_gutter_a.get()));
23+
let focus_editor_a = editor_a.editor().clone();
2624
let editor_b = editor_a
2725
.shared_editor()
2826
.editor_style(default_dark_color)
2927
.editor_style(move |s| s.hide_gutter(hide_gutter_b.get()))
3028
.style(|s| s.size_full())
31-
.pre_command(|ev| {
32-
if matches!(ev.cmd, Command::Edit(EditCommand::Undo)) {
33-
println!("Undo command executed on editor B, ignoring!");
34-
return CommandExecuted::Yes;
35-
}
36-
CommandExecuted::No
37-
})
3829
.update(|_| {
3930
// This hooks up to both editors!
4031
println!("Editor changed");
4132
})
4233
.placeholder("Some placeholder text");
4334
let doc = editor_a.doc();
35+
let clear_editor_a = editor_a.editor().clone();
4436

4537
Stack::new((
4638
editor_a,
4739
editor_b,
4840
Stack::new((
4941
Button::new("Clear").action(move || {
50-
doc.edit_single(
42+
doc.edit_single_from(
43+
&clear_editor_a,
5144
Selection::region(0, doc.text().len(), CursorAffinity::Backward),
5245
"",
5346
EditType::DeleteSelection,
5447
);
5548
}),
49+
Button::new("Focus A").action(move || {
50+
if let Some(id) = focus_editor_a.editor_view_id.get_untracked() {
51+
id.request_focus();
52+
}
53+
}),
5654
Button::new("Flip Gutter").action(move || {
5755
hide_gutter_a.update(|hide| *hide = !*hide);
5856
hide_gutter_b.update(|hide| *hide = !*hide);

examples/syntax-editor/src/main.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,12 +215,14 @@ mod tests {
215215
.style(|s| s.size_full());
216216

217217
let doc = editor.doc();
218+
let clear_editor = editor.editor().clone();
218219

219220
Stack::new((
220221
editor,
221222
Stack::new((
222223
Button::new("Clear").action(move || {
223-
doc.edit_single(
224+
doc.edit_single_from(
225+
&clear_editor,
224226
Selection::region(0, doc.text().len(), CursorAffinity::Backward),
225227
"",
226228
EditType::DeleteSelection,

examples/widget-gallery/src/texteditor.rs

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,7 @@ use floem::{
22
action::inspect,
33
prelude::*,
44
views::editor::{
5-
command::{Command, CommandExecuted},
6-
core::{
7-
command::EditCommand, cursor::CursorAffinity, editor::EditType, selection::Selection,
8-
},
5+
core::{cursor::CursorAffinity, editor::EditType, selection::Selection},
96
text::{SimpleStyling, default_dark_color},
107
},
118
};
@@ -28,26 +25,21 @@ pub fn editor_view() -> impl IntoView {
2825
.editor_style(default_dark_color)
2926
.editor_style(move |s| s.hide_gutter(!hide_gutter_a.get()))
3027
.style(|s| s.size_full())
31-
.pre_command(|ev| {
32-
if matches!(ev.cmd, Command::Edit(EditCommand::Undo)) {
33-
println!("Undo command executed on editor B, ignoring!");
34-
return CommandExecuted::Yes;
35-
}
36-
CommandExecuted::No
37-
})
3828
.update(|_| {
3929
// This hooks up to both editors!
4030
println!("Editor changed");
4131
})
4232
.placeholder("Some placeholder text");
4333
let doc = editor_a.doc();
34+
let clear_editor_a = editor_a.editor().clone();
4435

4536
Stack::new((
4637
editor_a,
4738
editor_b,
4839
Stack::new((
4940
Button::new("Clear").action(move || {
50-
doc.edit_single(
41+
doc.edit_single_from(
42+
&clear_editor_a,
5143
Selection::region(0, doc.text().len(), CursorAffinity::Backward),
5244
"",
5345
EditType::DeleteSelection,

src/views/editor/text.rs

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -188,24 +188,44 @@ pub trait Document: DocumentPhantom + ::std::any::Any {
188188
self.edit(&mut iter, edit_type);
189189
}
190190

191-
/// Perform the edit(s) on this document.
191+
/// Perform a single edit while preserving editor-specific semantics for the initiating editor.
192192
///
193-
/// This intentionally does not require an `Editor` as this is primarily intended for use by
194-
/// code that wants to modify the document from 'outside' the usual keybinding/command logic.
193+
/// Use this when a UI action edits the document on behalf of a live editor and should record
194+
/// that editor's cursor state for undo.
195+
fn edit_single_from(
196+
&self,
197+
editor: &Editor,
198+
selection: Selection,
199+
content: &str,
200+
edit_type: EditType,
201+
) {
202+
let mut iter = std::iter::once((selection, content));
203+
self.edit_from(editor, &mut iter, edit_type);
204+
}
205+
206+
/// Perform the edit(s) on this document.
195207
///
196-
/// ```rust,ignore
197-
/// let editor: TextEditor = text_editor();
198-
/// let doc: Rc<dyn Document> = editor.doc();
208+
/// This intentionally does not require an `Editor`. It is the raw document mutation path for
209+
/// code that wants to modify text without an initiating editor context.
199210
///
200-
/// stack((
201-
/// editor,
202-
/// button(|| "Append 'Hello'").on_click_stop(move |_| {
203-
/// let text = doc.text();
204-
/// doc.edit_single(Selection::caret(text.len()), "Hello", EditType::InsertChars);
205-
/// })
206-
/// ))
207-
/// ```
211+
/// Because it has no initiating editor context, it can't record that editor's cursor history
212+
/// for undo. If a UI action is editing on behalf of a live editor, prefer
213+
/// [`Document::edit_from`] or [`Document::edit_single_from`] instead.
208214
fn edit(&self, iter: &mut dyn Iterator<Item = (Selection, &str)>, edit_type: EditType);
215+
216+
/// Perform the edit(s) on this document using the provided editor as the initiating context.
217+
///
218+
/// The default implementation falls back to [`Document::edit`], which keeps this additive for
219+
/// custom `Document` implementations that do not yet preserve initiating-editor cursor history
220+
/// for undo.
221+
fn edit_from(
222+
&self,
223+
_editor: &Editor,
224+
iter: &mut dyn Iterator<Item = (Selection, &str)>,
225+
edit_type: EditType,
226+
) {
227+
self.edit(iter, edit_type);
228+
}
209229
}
210230

211231
pub trait DocumentPhantom {
@@ -516,9 +536,29 @@ where
516536
self.doc.edit_single(selection, content, edit_type)
517537
}
518538

539+
fn edit_single_from(
540+
&self,
541+
editor: &Editor,
542+
selection: Selection,
543+
content: &str,
544+
edit_type: EditType,
545+
) {
546+
self.doc
547+
.edit_single_from(editor, selection, content, edit_type)
548+
}
549+
519550
fn edit(&self, iter: &mut dyn Iterator<Item = (Selection, &str)>, edit_type: EditType) {
520551
self.doc.edit(iter, edit_type)
521552
}
553+
554+
fn edit_from(
555+
&self,
556+
editor: &Editor,
557+
iter: &mut dyn Iterator<Item = (Selection, &str)>,
558+
edit_type: EditType,
559+
) {
560+
self.doc.edit_from(editor, iter, edit_type)
561+
}
522562
}
523563
impl<D, F> DocumentPhantom for ExtCmdDocument<D, F>
524564
where

src/views/editor/text_document.rs

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,46 @@ impl TextDocument {
148148
});
149149
}
150150

151+
fn apply_programmatic_edit(
152+
&self,
153+
editor: Option<&Editor>,
154+
iter: &mut dyn Iterator<Item = (Selection, &str)>,
155+
edit_type: EditType,
156+
) {
157+
let mut cursor = editor.map(|editor| editor.cursor.get_untracked());
158+
let cursor_before = cursor.as_ref().map(|cursor| cursor.mode.clone());
159+
160+
let deltas = self
161+
.buffer
162+
.try_update(|buffer| buffer.edit(iter, edit_type));
163+
let deltas = deltas.map(|x| [x]);
164+
let deltas = deltas.as_ref().map(|x| x as &[_]).unwrap_or(&[]);
165+
166+
if deltas.is_empty() {
167+
return;
168+
}
169+
170+
if let Some(cursor) = cursor.as_mut() {
171+
for delta in deltas.iter().map(|(_, delta, _)| delta) {
172+
cursor.apply_delta(delta);
173+
}
174+
}
175+
176+
if let (Some(cursor_before), Some(cursor)) = (cursor_before, cursor.as_ref()) {
177+
self.buffer.update(|buffer| {
178+
buffer.set_cursor_before(cursor_before);
179+
buffer.set_cursor_after(cursor.mode.clone());
180+
});
181+
}
182+
183+
self.update_cache_rev();
184+
self.on_update(editor, deltas);
185+
186+
if let (Some(editor), Some(cursor)) = (editor, cursor) {
187+
editor.cursor.set(cursor);
188+
}
189+
}
190+
151191
fn placeholder(&self, editor_id: EditorId) -> Option<String> {
152192
self.placeholders
153193
.with_untracked(|placeholders| placeholders.get(&editor_id).cloned())
@@ -231,14 +271,16 @@ impl Document for TextDocument {
231271
}
232272

233273
fn edit(&self, iter: &mut dyn Iterator<Item = (Selection, &str)>, edit_type: EditType) {
234-
let deltas = self
235-
.buffer
236-
.try_update(|buffer| buffer.edit(iter, edit_type));
237-
let deltas = deltas.map(|x| [x]);
238-
let deltas = deltas.as_ref().map(|x| x as &[_]).unwrap_or(&[]);
274+
self.apply_programmatic_edit(None, iter, edit_type);
275+
}
239276

240-
self.update_cache_rev();
241-
self.on_update(None, deltas);
277+
fn edit_from(
278+
&self,
279+
editor: &Editor,
280+
iter: &mut dyn Iterator<Item = (Selection, &str)>,
281+
edit_type: EditType,
282+
) {
283+
self.apply_programmatic_edit(Some(editor), iter, edit_type);
242284
}
243285
}
244286
impl DocumentPhantom for TextDocument {

0 commit comments

Comments
 (0)