on master help sign in
Apply transactions to all views (#4733)
42e37a5Michael Davis
helix-core/src/history.rs+17−1
@@ -54,7 +54,7 @@ pub struct History {
54}
54}
55
55
56/// A single point in history. See [History] for more information.
56/// A single point in history. See [History] for more information.
57−#[derive(Debug)]
57+#[derive(Debug, Clone)]
58struct Revision {
58struct Revision {
59 parent: usize,
59 parent: usize,
60 last_child: Option<NonZeroUsize>,
60 last_child: Option<NonZeroUsize>,
@@ -119,6 +119,22 @@ impl History {
119 self.current == 0
119 self.current == 0
120 }
120 }
121
121
122+ /// Returns the changes since the given revision composed into a transaction.
123+ /// Returns None if there are no changes between the current and given revisions.
124+ pub fn changes_since(&self, revision: usize) -> Option<Transaction> {
125+ if self.at_root() || self.current >= revision {
126+ return None;
127+ }
128+
129+ // The bounds are checked in the if condition above:
130+ // `revision` is known to be `< self.current`.
131+ self.revisions[revision..self.current]
132+ .iter()
133+ .map(|revision| &revision.transaction)
134+ .cloned()
135+ .reduce(|acc, transaction| acc.compose(transaction))
136+ }
137+
122 /// Undo the last edit.
138 /// Undo the last edit.
123 pub fn undo(&mut self) -> Option<&Transaction> {
139 pub fn undo(&mut self) -> Option<&Transaction> {
124 if self.at_root() {
140 if self.at_root() {
helix-term/src/ui/editor.rs+23−3
@@ -1337,7 +1337,9 @@ impl Component for EditorView {
1337 cx.editor.status_msg = None;
1337 cx.editor.status_msg = None;
1338
1338
1339 let mode = cx.editor.mode();
1339 let mode = cx.editor.mode();
1340− let (view, _) = current!(cx.editor);
1340+ let (view, doc) = current!(cx.editor);
1341+ let original_doc_id = doc.id();
1342+ let original_doc_revision = doc.get_current_revision();
1341 let focus = view.id;
1343 let focus = view.id;
1342
1344
1343 if let Some(on_next_key) = self.on_next_key.take() {
1345 if let Some(on_next_key) = self.on_next_key.take() {
@@ -1413,13 +1415,31 @@ impl Component for EditorView {
1413 let view = view_mut!(cx.editor, focus);
1415 let view = view_mut!(cx.editor, focus);
1414 let doc = doc_mut!(cx.editor, &view.doc);
1416 let doc = doc_mut!(cx.editor, &view.doc);
1415
1417
1416− view.ensure_cursor_in_view(doc, config.scrolloff);
1417−
1418 // Store a history state if not in insert mode. This also takes care of
1418 // Store a history state if not in insert mode. This also takes care of
1419 // committing changes when leaving insert mode.
1419 // committing changes when leaving insert mode.
1420 if mode != Mode::Insert {
1420 if mode != Mode::Insert {
1421 doc.append_changes_to_history(view.id);
1421 doc.append_changes_to_history(view.id);
1422 }
1422 }
1423+
1424+ // If the current document has been changed, apply the changes to all views.
1425+ // This ensures that selections in jumplists follow changes.
1426+ if doc.id() == original_doc_id
1427+ && doc.get_current_revision() > original_doc_revision
1428+ {
1429+ if let Some(transaction) =
1430+ doc.history.get_mut().changes_since(original_doc_revision)
1431+ {
1432+ let doc = doc!(cx.editor, &original_doc_id);
1433+ for (view, _focused) in cx.editor.tree.views_mut() {
1434+ view.apply(&transaction, doc);
1435+ }
1436+ }
1437+ }
1438+
1439+ let view = view_mut!(cx.editor, focus);
1440+ let doc = doc_mut!(cx.editor, &view.doc);
1441+
1442+ view.ensure_cursor_in_view(doc, config.scrolloff);
1423 }
1443 }
1424
1444
1425 EventResult::Consumed(callback)
1445 EventResult::Consumed(callback)
helix-term/tests/test/splits.rs+26
@@ -127,3 +127,29 @@ async fn test_split_write_quit_same_file() -> anyhow::Result<()> {
127
127
128 Ok(())
128 Ok(())
129}
129}
130+
131+#[tokio::test(flavor = "multi_thread")]
132+async fn test_changes_in_splits_apply_to_all_views() -> anyhow::Result<()> {
133+ // See <https://github.com/helix-editor/helix/issues/4732>.
134+ // Transactions must be applied to any view that has the changed document open.
135+ // This sequence would panic since the jumplist entry would be modified in one
136+ // window but not the other. Attempting to update the changelist in the other
137+ // window would cause a panic since it would point outside of the document.
138+
139+ // The key sequence here:
140+ // * <C-w>v Create a vertical split of the current buffer.
141+ // Both views look at the same doc.
142+ // * [<space> Add a line ending to the beginning of the document.
143+ // The cursor is now at line 2 in window 2.
144+ // * <C-s> Save that selection to the jumplist in window 2.
145+ // * <C-w>w Switch to window 1.
146+ // * kd Delete line 1 in window 1.
147+ // * <C-w>q Close window 1, focusing window 2.
148+ // * d Delete line 1 in window 2.
149+ //
150+ // This panicked in the past because the jumplist entry on line 2 of window 2
151+ // was not updated and after the `kd` step, pointed outside of the document.
152+ test(("#[|]#", "<C-w>v[<space><C-s><C-w>wkd<C-w>qd", "#[|]#")).await?;
153+
154+ Ok(())
155+}
helix-view/src/lib.rs+3−5
@@ -71,12 +71,10 @@ pub fn align_view(doc: &Document, view: &mut View, align: Align) {
71pub fn apply_transaction(
71pub fn apply_transaction(
72 transaction: &helix_core::Transaction,
72 transaction: &helix_core::Transaction,
73 doc: &mut Document,
73 doc: &mut Document,
74− view: &mut View,
74+ view: &View,
75) -> bool {
75) -> bool {
76− // This is a short function but it's easy to call `Document::apply`
76+ // TODO remove this helper function. Just call Document::apply everywhere directly.
77− // without calling `View::apply` or in the wrong order. The transaction
78− // must be applied to the document before the view.
79− doc.apply(transaction, view.id) && view.apply(transaction, doc)
77+ doc.apply(transaction, view.id)
80}
78}
81
79
82pub use document::Document;
80pub use document::Document;
helix-view/src/macros.rs+1−1
@@ -67,7 +67,7 @@ macro_rules! view {
67#[macro_export]
67#[macro_export]
68macro_rules! doc {
68macro_rules! doc {
69 ($editor:expr, $id:expr) => {{
69 ($editor:expr, $id:expr) => {{
70− $editor.documents[$id]
70+ &$editor.documents[$id]
71 }};
71 }};
72 ($editor:expr) => {{
72 ($editor:expr) => {{
73 $crate::current_ref!($editor).1
73 $crate::current_ref!($editor).1
helix-view/src/view.rs+1
@@ -351,6 +351,7 @@ impl View {
351 /// which applies a transaction to the [`Document`] and view together.
351 /// which applies a transaction to the [`Document`] and view together.
352 pub fn apply(&mut self, transaction: &Transaction, doc: &Document) -> bool {
352 pub fn apply(&mut self, transaction: &Transaction, doc: &Document) -> bool {
353 self.jumps.apply(transaction, doc);
353 self.jumps.apply(transaction, doc);
354+ // TODO: remove the boolean return. This is unused.
354 true
355 true
355 }
356 }
356}
357}