on master help sign in
store multiple snapshots on the document at once

Fixing autocomplete required moving the document savepoint before the asynchronous completion request. However, this in turn causes new bugs: If the completion popup is open, the savepoint is restored when the popup closes (or another entry is selected). However, at that point a new completion request might already have been created which would have replaced the new savepoint (therefore leading to incorrectly applied complies). This commit fixes that bug by allowing in arbitrary number of savepoints to be tracked on the document. The savepoints are reference counted and therefore remain valid as long as any reference to them remains. Weak reference are stored on the document and any reference that can not be upgraded anymore (hence no strong reference remain) are automatically discarded.

commite8898fd9a8ac8120827fb2d6f4752b3cb2431a62 authorPascal Kuthe <pascal.kuthe@semimod.de> dateThu 2 Mar 2023, 21:56 (4 years ago) parent2588fa3
6 files changed+72−24
Cargo.lock+1 helix-term/src/commands.rs+2−1 helix-term/src/ui/completion.rs+4−4 helix-term/src/ui/editor.rs+10−4 helix-view/Cargo.toml+1 helix-view/src/document.rs+54−15
Cargo.lock+1
@@ -1241,6 +1241,7 @@ dependencies = [
12411241 "libc",
12421242 "log",
12431243 "once_cell",
1244+ "parking_lot",
12441245 "serde",
12451246 "serde_json",
12461247 "slotmap",
helix-term/src/commands.rs+2−1
@@ -4181,7 +4181,7 @@ pub fn completion(cx: &mut Context) {
41814181 iter.reverse();
41824182 let offset = iter.take_while(|ch| chars::char_is_word(*ch)).count();
41834183 let start_offset = cursor.saturating_sub(offset);
4184− doc.savepoint(&view);
4184+ let savepoint = doc.savepoint(view);
41854185
41864186 cx.callback(
41874187 future,
@@ -4209,6 +4209,7 @@ pub fn completion(cx: &mut Context) {
42094209 let ui = compositor.find::<ui::EditorView>().unwrap();
42104210 ui.set_completion(
42114211 editor,
4212+ savepoint,
42124213 items,
42134214 offset_encoding,
42144215 start_offset,
helix-term/src/ui/completion.rs+4−4
@@ -1,12 +1,13 @@
11use crate::compositor::{Component, Context, Event, EventResult};
22use helix_view::{
3+ document::SavePoint,
34 editor::CompleteAction,
45 theme::{Modifier, Style},
56 ViewId,
67};
78use tui::{buffer::Buffer as Surface, text::Span};
89
9−use std::borrow::Cow;
10+use std::{borrow::Cow, sync::Arc};
1011
1112use helix_core::{Change, Transaction};
1213use helix_view::{graphics::Rect, Document, Editor};
@@ -101,6 +102,7 @@ impl Completion {
101102
102103 pub fn new(
103104 editor: &Editor,
105+ savepoint: Arc<SavePoint>,
104106 mut items: Vec<CompletionItem>,
105107 offset_encoding: helix_lsp::OffsetEncoding,
106108 start_offset: usize,
@@ -213,11 +215,10 @@ impl Completion {
213215 let (view, doc) = current!(editor);
214216
215217 // if more text was entered, remove it
216− doc.restore(view);
218+ doc.restore(view, &savepoint);
217219
218220 match event {
219221 PromptEvent::Abort => {
220− doc.restore(view);
221222 editor.last_completion = None;
222223 }
223224 PromptEvent::Update => {
@@ -235,7 +236,6 @@ impl Completion {
235236 );
236237
237238 // initialize a savepoint
238− doc.savepoint(&view);
239239 doc.apply(&transaction, view.id);
240240
241241 editor.last_completion = Some(CompleteAction {
helix-term/src/ui/editor.rs+10−4
@@ -940,17 +940,25 @@ impl EditorView {
940940 }
941941 }
942942
943+ #[allow(clippy::too_many_arguments)]
943944 pub fn set_completion(
944945 &mut self,
945946 editor: &mut Editor,
947+ savepoint: Arc<SavePoint>,
946948 items: Vec<helix_lsp::lsp::CompletionItem>,
947949 offset_encoding: helix_lsp::OffsetEncoding,
948950 start_offset: usize,
949951 trigger_offset: usize,
950952 size: Rect,
951953 ) {
952− let mut completion =
953− Completion::new(editor, items, offset_encoding, start_offset, trigger_offset);
954+ let mut completion = Completion::new(
955+ editor,
956+ savepoint,
957+ items,
958+ offset_encoding,
959+ start_offset,
960+ trigger_offset,
961+ );
954962
955963 if completion.is_empty() {
956964 // skip if we got no completion results
@@ -969,8 +977,6 @@ impl EditorView {
969977 self.completion = None;
970978
971979 // Clear any savepoints
972− let doc = doc_mut!(editor);
973− doc.savepoint = None;
974980 editor.clear_idle_timer(); // don't retrigger
975981 }
976982
helix-view/Cargo.toml+1
@@ -43,6 +43,7 @@ toml = "0.7"
4343log = "~0.4"
4444
4545which = "4.4"
46+parking_lot = "0.12.1"
4647
4748
4849[target.'cfg(windows)'.dependencies]
helix-view/src/document.rs+54−15
@@ -9,6 +9,7 @@ use helix_core::text_annotations::TextAnnotations;
99use helix_core::Range;
1010use helix_vcs::{DiffHandle, DiffProviderRegistry};
1111
12+use ::parking_lot::Mutex;
1213use serde::de::{self, Deserialize, Deserializer};
1314use serde::Serialize;
1415use std::borrow::Cow;
@@ -18,7 +19,7 @@ use std::fmt::Display;
1819use std::future::Future;
1920use std::path::{Path, PathBuf};
2021use std::str::FromStr;
21−use std::sync::Arc;
22+use std::sync::{Arc, Weak};
2223use std::time::SystemTime;
2324
2425use helix_core::{
@@ -105,6 +106,13 @@ pub struct DocumentSavedEvent {
105106pub type DocumentSavedEventResult = Result<DocumentSavedEvent, anyhow::Error>;
106107pub type DocumentSavedEventFuture = BoxFuture<'static, DocumentSavedEventResult>;
107108
109+#[derive(Debug)]
110+pub struct SavePoint {
111+ /// The view this savepoint is associated with
112+ pub view: ViewId,
113+ revert: Mutex<Transaction>,
114+}
115+
108116pub struct Document {
109117 pub(crate) id: DocumentId,
110118 text: Rope,
@@ -136,7 +144,7 @@ pub struct Document {
136144 pub history: Cell<History>,
137145 pub config: Arc<dyn DynAccess<Config>>,
138146
139− pub savepoint: Option<Transaction>,
147+ savepoints: Vec<Weak<SavePoint>>,
140148
141149 // Last time we wrote to the file. This will carry the time the file was last opened if there
142150 // were no saves.
@@ -389,7 +397,7 @@ impl Document {
389397 diagnostics: Vec::new(),
390398 version: 0,
391399 history: Cell::new(History::default()),
392− savepoint: None,
400+ savepoints: Vec::new(),
393401 last_saved_time: SystemTime::now(),
394402 last_saved_revision: 0,
395403 modified_since_accessed: false,
@@ -846,11 +854,18 @@ impl Document {
846854 }
847855
848856 // generate revert to savepoint
849− if self.savepoint.is_some() {
850− take_with(&mut self.savepoint, |prev_revert| {
851− let revert = transaction.invert(&old_doc);
852− Some(revert.compose(prev_revert.unwrap()))
853− });
857+ if !self.savepoints.is_empty() {
858+ let revert = transaction.invert(&old_doc);
859+ self.savepoints
860+ .retain_mut(|save_point| match save_point.upgrade() {
861+ Some(savepoint) => {
862+ let mut revert_to_savepoint = savepoint.revert.lock();
863+ *revert_to_savepoint =
864+ revert.clone().compose(mem::take(&mut revert_to_savepoint));
865+ true
866+ }
867+ None => false,
868+ })
854869 }
855870
856871 // update tree-sitter syntax tree
@@ -940,15 +955,39 @@ impl Document {
940955 self.undo_redo_impl(view, false)
941956 }
942957
943− pub fn savepoint(&mut self) {
944− self.savepoint =
945− Some(Transaction::new(self.text()).with_selection(self.selection(view.id).clone()));
958+ /// Creates a reference counted snapshot (called savpepoint) of the document.
959+ ///
960+ /// The snapshot will remain valid (and updated) idenfinitly as long as ereferences to it exist.
961+ /// Restoring the snapshot will restore the selection and the contents of the document to
962+ /// the state it had when this function was called.
963+ pub fn savepoint(&mut self, view: &View) -> Arc<SavePoint> {
964+ let revert = Transaction::new(self.text()).with_selection(self.selection(view.id).clone());
965+ let savepoint = Arc::new(SavePoint {
966+ view: view.id,
967+ revert: Mutex::new(revert),
968+ });
969+ self.savepoints.push(Arc::downgrade(&savepoint));
970+ savepoint
946971 }
947972
948− pub fn restore(&mut self, view: &mut View) {
949− if let Some(revert) = self.savepoint.take() {
950− self.apply(&revert, view.id);
951− }
973+ pub fn restore(&mut self, view: &mut View, savepoint: &SavePoint) {
974+ assert_eq!(
975+ savepoint.view, view.id,
976+ "Savepoint must not be used with a different view!"
977+ );
978+ // search and remove savepoint using a ptr comparison
979+ // this avoids a deadlock as we need to lock the mutex
980+ let savepoint_idx = self
981+ .savepoints
982+ .iter()
983+ .position(|savepoint_ref| savepoint_ref.as_ptr() == savepoint as *const _)
984+ .expect("Savepoint must belong to this document");
985+
986+ let savepoint_ref = self.savepoints.remove(savepoint_idx);
987+ let mut revert = savepoint.revert.lock();
988+ self.apply(&revert, view.id);
989+ *revert = Transaction::new(self.text()).with_selection(self.selection(view.id).clone());
990+ self.savepoints.push(savepoint_ref)
952991 }
953992
954993 fn earlier_later_impl(&mut self, view: &mut View, uk: UndoKind, earlier: bool) -> bool {