Skip to content

Commit 42e37a5

Browse files
Apply transactions to all views (#4733)
* Add a test case for updating jumplists across windows * Apply transactions to all views on history changes This ensures that jumplist selections follow changes in documents, even when there are multiple views (for example a split where both windows edit the same document). * Leave TODOs for cleaning up View::apply * Use Iterator::reduce to compose history transactions Co-authored-by: Blaž Hrastnik <[email protected]> Co-authored-by: Blaž Hrastnik <[email protected]>
1 parent 642a961 commit 42e37a5

File tree

6 files changed

+71
-10
lines changed

6 files changed

+71
-10
lines changed

helix-core/src/history.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ pub struct History {
5454
}
5555

5656
/// A single point in history. See [History] for more information.
57-
#[derive(Debug)]
57+
#[derive(Debug, Clone)]
5858
struct Revision {
5959
parent: usize,
6060
last_child: Option<NonZeroUsize>,
@@ -119,6 +119,22 @@ impl History {
119119
self.current == 0
120120
}
121121

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+
122138
/// Undo the last edit.
123139
pub fn undo(&mut self) -> Option<&Transaction> {
124140
if self.at_root() {

helix-term/src/ui/editor.rs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1337,7 +1337,9 @@ impl Component for EditorView {
13371337
cx.editor.status_msg = None;
13381338

13391339
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();
13411343
let focus = view.id;
13421344

13431345
if let Some(on_next_key) = self.on_next_key.take() {
@@ -1413,13 +1415,31 @@ impl Component for EditorView {
14131415
let view = view_mut!(cx.editor, focus);
14141416
let doc = doc_mut!(cx.editor, &view.doc);
14151417

1416-
view.ensure_cursor_in_view(doc, config.scrolloff);
1417-
14181418
// Store a history state if not in insert mode. This also takes care of
14191419
// committing changes when leaving insert mode.
14201420
if mode != Mode::Insert {
14211421
doc.append_changes_to_history(view.id);
14221422
}
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);
14231443
}
14241444

14251445
EventResult::Consumed(callback)

helix-term/tests/test/splits.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,29 @@ async fn test_split_write_quit_same_file() -> anyhow::Result<()> {
127127

128128
Ok(())
129129
}
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

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,10 @@ pub fn align_view(doc: &Document, view: &mut View, align: Align) {
7171
pub fn apply_transaction(
7272
transaction: &helix_core::Transaction,
7373
doc: &mut Document,
74-
view: &mut View,
74+
view: &View,
7575
) -> bool {
76-
// This is a short function but it's easy to call `Document::apply`
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)
76+
// TODO remove this helper function. Just call Document::apply everywhere directly.
77+
doc.apply(transaction, view.id)
8078
}
8179

8280
pub use document::Document;

helix-view/src/macros.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ macro_rules! view {
6767
#[macro_export]
6868
macro_rules! doc {
6969
($editor:expr, $id:expr) => {{
70-
$editor.documents[$id]
70+
&$editor.documents[$id]
7171
}};
7272
($editor:expr) => {{
7373
$crate::current_ref!($editor).1

helix-view/src/view.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,7 @@ impl View {
351351
/// which applies a transaction to the [`Document`] and view together.
352352
pub fn apply(&mut self, transaction: &Transaction, doc: &Document) -> bool {
353353
self.jumps.apply(transaction, doc);
354+
// TODO: remove the boolean return. This is unused.
354355
true
355356
}
356357
}

0 commit comments

Comments
 (0)