diff --git a/helix-core/src/history.rs b/helix-core/src/history.rs index 825092423e00..b99e969f93ba 100644 --- a/helix-core/src/history.rs +++ b/helix-core/src/history.rs @@ -122,32 +122,16 @@ impl History { /// Returns the changes since the given revision composed into a transaction. /// Returns None if there are no changes between the current and given revisions. pub fn changes_since(&self, revision: usize) -> Option { - use std::cmp::Ordering::*; + let lca = self.lowest_common_ancestor(revision, self.current); + let up = self.path_up(revision, lca); + let down = self.path_up(self.current, lca); + let up_txns = up + .iter() + .rev() + .map(|&n| self.revisions[n].inversion.clone()); + let down_txns = down.iter().map(|&n| self.revisions[n].transaction.clone()); - match revision.cmp(&self.current) { - Equal => None, - Less => { - let mut child = self.revisions[revision].last_child?.get(); - let mut transaction = self.revisions[child].transaction.clone(); - while child != self.current { - child = self.revisions[child].last_child?.get(); - transaction = transaction.compose(self.revisions[child].transaction.clone()); - } - Some(transaction) - } - Greater => { - let mut inversion = self.revisions[revision].inversion.clone(); - let mut parent = self.revisions[revision].parent; - while parent != self.current { - parent = self.revisions[parent].parent; - if parent == 0 { - return None; - } - inversion = inversion.compose(self.revisions[parent].inversion.clone()); - } - Some(inversion) - } - } + up_txns.chain(down_txns).reduce(|acc, tx| tx.compose(acc)) } /// Undo the last edit. diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 5c6807f0f9ae..a6f88362c8f6 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2564,7 +2564,7 @@ async fn make_format_callback( if let Ok(format) = format { if doc.version() == doc_version { apply_transaction(&format, doc, view); - doc.append_changes_to_history(view.id); + doc.append_changes_to_history(view); doc.detect_indent_and_line_ending(); view.ensure_cursor_in_view(doc, scrolloff); } else { @@ -3321,7 +3321,7 @@ fn undo(cx: &mut Context) { let count = cx.count(); let (view, doc) = current!(cx.editor); for _ in 0..count { - if !doc.undo(view.id) { + if !doc.undo(view) { cx.editor.set_status("Already at oldest change"); break; } @@ -3332,7 +3332,7 @@ fn redo(cx: &mut Context) { let count = cx.count(); let (view, doc) = current!(cx.editor); for _ in 0..count { - if !doc.redo(view.id) { + if !doc.redo(view) { cx.editor.set_status("Already at newest change"); break; } @@ -3344,7 +3344,7 @@ fn earlier(cx: &mut Context) { let (view, doc) = current!(cx.editor); for _ in 0..count { // rather than doing in batch we do this so get error halfway - if !doc.earlier(view.id, UndoKind::Steps(1)) { + if !doc.earlier(view, UndoKind::Steps(1)) { cx.editor.set_status("Already at oldest change"); break; } @@ -3356,7 +3356,7 @@ fn later(cx: &mut Context) { let (view, doc) = current!(cx.editor); for _ in 0..count { // rather than doing in batch we do this so get error halfway - if !doc.later(view.id, UndoKind::Steps(1)) { + if !doc.later(view, UndoKind::Steps(1)) { cx.editor.set_status("Already at newest change"); break; } @@ -3365,7 +3365,7 @@ fn later(cx: &mut Context) { fn commit_undo_checkpoint(cx: &mut Context) { let (view, doc) = current!(cx.editor); - doc.append_changes_to_history(view.id); + doc.append_changes_to_history(view); } // Yank / Paste @@ -3677,7 +3677,7 @@ fn replace_selections_with_clipboard_impl( }); apply_transaction(&transaction, doc, view); - doc.append_changes_to_history(view.id); + doc.append_changes_to_history(view); } Err(e) => return Err(e.context("Couldn't get system clipboard contents")), } @@ -4884,7 +4884,7 @@ fn shell(cx: &mut compositor::Context, cmd: &str, behavior: &ShellBehavior) { let transaction = Transaction::change(doc.text(), changes.into_iter()) .with_selection(Selection::new(ranges, selection.primary_index())); apply_transaction(&transaction, doc, view); - doc.append_changes_to_history(view.id); + doc.append_changes_to_history(view); } // after replace cursor may be out of bounds, do this to diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 8faf1d087f92..1f80de5f8d4e 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -760,8 +760,9 @@ pub fn apply_workspace_edit( text_edits, offset_encoding, ); - apply_transaction(&transaction, doc, view_mut!(editor, view_id)); - doc.append_changes_to_history(view_id); + let view = view_mut!(editor, view_id); + apply_transaction(&transaction, doc, view); + doc.append_changes_to_history(view); }; if let Some(ref changes) = workspace_edit.changes { diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 351692fd24df..2fa903a7e605 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -464,7 +464,7 @@ fn set_line_ending( }), ); apply_transaction(&transaction, doc, view); - doc.append_changes_to_history(view.id); + doc.append_changes_to_history(view); Ok(()) } @@ -481,7 +481,7 @@ fn earlier( let uk = args.join(" ").parse::().map_err(|s| anyhow!(s))?; let (view, doc) = current!(cx.editor); - let success = doc.earlier(view.id, uk); + let success = doc.earlier(view, uk); if !success { cx.editor.set_status("Already at oldest change"); } @@ -500,7 +500,7 @@ fn later( let uk = args.join(" ").parse::().map_err(|s| anyhow!(s))?; let (view, doc) = current!(cx.editor); - let success = doc.later(view.id, uk); + let success = doc.later(view, uk); if !success { cx.editor.set_status("Already at newest change"); } @@ -909,7 +909,7 @@ fn replace_selections_with_clipboard_impl( }); apply_transaction(&transaction, doc, view); - doc.append_changes_to_history(view.id); + doc.append_changes_to_history(view); Ok(()) } Err(e) => Err(e.context("Couldn't get system clipboard contents")), @@ -1573,7 +1573,7 @@ fn sort_impl( ); apply_transaction(&transaction, doc, view); - doc.append_changes_to_history(view.id); + doc.append_changes_to_history(view); Ok(()) } @@ -1617,7 +1617,7 @@ fn reflow( }); apply_transaction(&transaction, doc, view); - doc.append_changes_to_history(view.id); + doc.append_changes_to_history(view); view.ensure_cursor_in_view(doc, scrolloff); Ok(()) diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 737125031e99..7bda74d283eb 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -1319,7 +1319,7 @@ impl Component for EditorView { // Store a history state if not in insert mode. Otherwise wait till we exit insert // to include any edits to the paste in the history state. if mode != Mode::Insert { - doc.append_changes_to_history(view.id); + doc.append_changes_to_history(view); } EventResult::Consumed(None) @@ -1337,9 +1337,7 @@ impl Component for EditorView { cx.editor.status_msg = None; let mode = cx.editor.mode(); - let (view, doc) = current!(cx.editor); - let original_doc_id = doc.id(); - let original_doc_revision = doc.get_current_revision(); + let (view, _) = current!(cx.editor); let focus = view.id; if let Some(on_next_key) = self.on_next_key.take() { @@ -1415,31 +1413,13 @@ impl Component for EditorView { let view = view_mut!(cx.editor, focus); let doc = doc_mut!(cx.editor, &view.doc); + view.ensure_cursor_in_view(doc, config.scrolloff); + // Store a history state if not in insert mode. This also takes care of // committing changes when leaving insert mode. if mode != Mode::Insert { - doc.append_changes_to_history(view.id); + doc.append_changes_to_history(view); } - - // If the current document has been changed, apply the changes to all views. - // This ensures that selections in jumplists follow changes. - if doc.id() == original_doc_id - && doc.get_current_revision() != original_doc_revision - { - if let Some(transaction) = - doc.history.get_mut().changes_since(original_doc_revision) - { - let doc = doc!(cx.editor, &original_doc_id); - for (view, _focused) in cx.editor.tree.views_mut() { - view.apply(&transaction, doc); - } - } - } - - let view = view_mut!(cx.editor, focus); - let doc = doc_mut!(cx.editor, &view.doc); - - view.ensure_cursor_in_view(doc, config.scrolloff); } EventResult::Consumed(callback) diff --git a/helix-term/tests/test/splits.rs b/helix-term/tests/test/splits.rs index a51de365850e..a34a24b7cbdf 100644 --- a/helix-term/tests/test/splits.rs +++ b/helix-term/tests/test/splits.rs @@ -151,5 +151,15 @@ async fn test_changes_in_splits_apply_to_all_views() -> anyhow::Result<()> { // was not updated and after the `kd` step, pointed outside of the document. test(("#[|]#", "v[wkdqd", "#[|]#")).await?; + // Transactions are applied to the views for windows lazily when they are focused. + // This case panics if the transactions and inversions are not applied in the + // correct order as we switch between windows. + test(( + "#[|]#", + "[[[vuuuwUUUquuu", + "#[|]#", + )) + .await?; + Ok(()) } diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 0eb54f25fa6e..ad47f838fa90 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -641,7 +641,7 @@ impl Document { // of the encoding. let transaction = helix_core::diff::compare_ropes(self.text(), &rope); apply_transaction(&transaction, self, view); - self.append_changes_to_history(view.id); + self.append_changes_to_history(view); self.reset_modified(); self.detect_indent_and_line_ending(); @@ -857,11 +857,11 @@ impl Document { success } - fn undo_redo_impl(&mut self, view_id: ViewId, undo: bool) -> bool { + fn undo_redo_impl(&mut self, view: &mut View, undo: bool) -> bool { let mut history = self.history.take(); let txn = if undo { history.undo() } else { history.redo() }; let success = if let Some(txn) = txn { - self.apply_impl(txn, view_id) + self.apply_impl(txn, view.id) } else { false }; @@ -870,18 +870,20 @@ impl Document { if success { // reset changeset to fix len self.changes = ChangeSet::new(self.text()); + // Sync with changes with the jumplist selections. + view.sync_changes(self); } success } /// Undo the last modification to the [`Document`]. Returns whether the undo was successful. - pub fn undo(&mut self, view_id: ViewId) -> bool { - self.undo_redo_impl(view_id, true) + pub fn undo(&mut self, view: &mut View) -> bool { + self.undo_redo_impl(view, true) } /// Redo the last modification to the [`Document`]. Returns whether the redo was successful. - pub fn redo(&mut self, view_id: ViewId) -> bool { - self.undo_redo_impl(view_id, false) + pub fn redo(&mut self, view: &mut View) -> bool { + self.undo_redo_impl(view, false) } pub fn savepoint(&mut self) { @@ -894,7 +896,7 @@ impl Document { } } - fn earlier_later_impl(&mut self, view_id: ViewId, uk: UndoKind, earlier: bool) -> bool { + fn earlier_later_impl(&mut self, view: &mut View, uk: UndoKind, earlier: bool) -> bool { let txns = if earlier { self.history.get_mut().earlier(uk) } else { @@ -902,29 +904,31 @@ impl Document { }; let mut success = false; for txn in txns { - if self.apply_impl(&txn, view_id) { + if self.apply_impl(&txn, view.id) { success = true; } } if success { // reset changeset to fix len self.changes = ChangeSet::new(self.text()); + // Sync with changes with the jumplist selections. + view.sync_changes(self); } success } /// Undo modifications to the [`Document`] according to `uk`. - pub fn earlier(&mut self, view_id: ViewId, uk: UndoKind) -> bool { - self.earlier_later_impl(view_id, uk, true) + pub fn earlier(&mut self, view: &mut View, uk: UndoKind) -> bool { + self.earlier_later_impl(view, uk, true) } /// Redo modifications to the [`Document`] according to `uk`. - pub fn later(&mut self, view_id: ViewId, uk: UndoKind) -> bool { - self.earlier_later_impl(view_id, uk, false) + pub fn later(&mut self, view: &mut View, uk: UndoKind) -> bool { + self.earlier_later_impl(view, uk, false) } /// Commit pending changes to history - pub fn append_changes_to_history(&mut self, view_id: ViewId) { + pub fn append_changes_to_history(&mut self, view: &mut View) { if self.changes.is_empty() { return; } @@ -934,7 +938,7 @@ impl Document { // Instead of doing this messy merge we could always commit, and based on transaction // annotations either add a new layer or compose into the previous one. let transaction = - Transaction::from(changes).with_selection(self.selection(view_id).clone()); + Transaction::from(changes).with_selection(self.selection(view.id).clone()); // HAXX: we need to reconstruct the state as it was before the changes.. let old_state = self.old_state.take().expect("no old_state available"); @@ -942,6 +946,9 @@ impl Document { let mut history = self.history.take(); history.commit_revision(&transaction, &old_state); self.history.set(history); + + // Update jumplist entries in the view. + view.apply(&transaction, self); } pub fn id(&self) -> DocumentId { diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 6eaa89aac814..5a1ac6b1fe1d 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -959,7 +959,8 @@ impl Editor { fn _refresh(&mut self) { let config = self.config(); for (view, _) in self.tree.views_mut() { - let doc = &self.documents[&view.doc]; + let doc = doc_mut!(self, &view.doc); + view.sync_changes(doc); view.ensure_cursor_in_view(doc, config.scrolloff) } } @@ -971,6 +972,7 @@ impl Editor { let doc = doc_mut!(self, &doc_id); doc.ensure_view_init(view.id); + view.sync_changes(doc); align_view(doc, view, Align::Center); } @@ -1240,6 +1242,12 @@ impl Editor { if prev_id != view_id { self.mode = Mode::Normal; self.ensure_cursor_in_view(view_id); + + // Update jumplist selections with new document changes. + for (view, _focused) in self.tree.views_mut() { + let doc = doc_mut!(self, &view.doc); + view.sync_changes(doc); + } } } diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs index c917a1abc054..845a545862d6 100644 --- a/helix-view/src/view.rs +++ b/helix-view/src/view.rs @@ -3,7 +3,10 @@ use helix_core::{ pos_at_visual_coords, visual_coords_at_pos, Position, RopeSlice, Selection, Transaction, }; -use std::{collections::VecDeque, fmt}; +use std::{ + collections::{HashMap, VecDeque}, + fmt, +}; const JUMP_LIST_CAPACITY: usize = 30; @@ -102,6 +105,11 @@ pub struct View { pub object_selections: Vec, /// GutterTypes used to fetch Gutter (constructor) and width for rendering gutters: Vec, + /// A mapping between documents and the last history revision the view was updated at. + /// Changes between documents and views are synced lazily when switching windows. This + /// mapping keeps track of the last applied history revision so that only new changes + /// are applied. + doc_revisions: HashMap, } impl fmt::Debug for View { @@ -126,6 +134,7 @@ impl View { last_modified_docs: [None, None], object_selections: Vec::new(), gutters: gutter_types, + doc_revisions: HashMap::new(), } } @@ -349,10 +358,33 @@ impl View { /// Applies a [`Transaction`] to the view. /// Instead of calling this function directly, use [crate::apply_transaction] /// which applies a transaction to the [`Document`] and view together. - pub fn apply(&mut self, transaction: &Transaction, doc: &Document) -> bool { + pub fn apply(&mut self, transaction: &Transaction, doc: &mut Document) { self.jumps.apply(transaction, doc); - // TODO: remove the boolean return. This is unused. - true + self.doc_revisions + .insert(doc.id(), doc.get_current_revision()); + } + + pub fn sync_changes(&mut self, doc: &mut Document) { + let latest_revision = doc.get_current_revision(); + let current_revision = *self + .doc_revisions + .entry(doc.id()) + .or_insert(latest_revision); + + if current_revision == latest_revision { + return; + } + + log::debug!( + "Syncing view {:?} between {} and {}", + self.id, + current_revision, + latest_revision + ); + + if let Some(transaction) = doc.history.get_mut().changes_since(current_revision) { + self.apply(&transaction, doc); + } } }