Skip to content

Commit 7e42f25

Browse files
committed
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.
1 parent 6f16586 commit 7e42f25

File tree

6 files changed

+78
-24
lines changed

6 files changed

+78
-24
lines changed

Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

helix-term/src/commands.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -4183,7 +4183,7 @@ pub fn completion(cx: &mut Context) {
41834183
iter.reverse();
41844184
let offset = iter.take_while(|ch| chars::char_is_word(*ch)).count();
41854185
let start_offset = cursor.saturating_sub(offset);
4186-
doc.savepoint(&view);
4186+
let savepoint = doc.savepoint(&view);
41874187

41884188
cx.callback(
41894189
future,
@@ -4211,6 +4211,7 @@ pub fn completion(cx: &mut Context) {
42114211
let ui = compositor.find::<ui::EditorView>().unwrap();
42124212
ui.set_completion(
42134213
editor,
4214+
savepoint,
42144215
items,
42154216
offset_encoding,
42164217
start_offset,

helix-term/src/ui/completion.rs

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
use crate::compositor::{Component, Context, Event, EventResult};
22
use helix_view::{
3+
document::SavePoint,
34
editor::CompleteAction,
45
theme::{Modifier, Style},
56
ViewId,
67
};
78
use tui::{buffer::Buffer as Surface, text::Span};
89

9-
use std::borrow::Cow;
10+
use std::{borrow::Cow, sync::Arc};
1011

1112
use helix_core::{Change, Transaction};
1213
use 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,
@@ -172,11 +174,10 @@ impl Completion {
172174
let (view, doc) = current!(editor);
173175

174176
// if more text was entered, remove it
175-
doc.restore(view);
177+
doc.restore(view, &savepoint);
176178

177179
match event {
178180
PromptEvent::Abort => {
179-
doc.restore(view);
180181
editor.last_completion = None;
181182
}
182183
PromptEvent::Update => {
@@ -193,7 +194,6 @@ impl Completion {
193194
);
194195

195196
// initialize a savepoint
196-
doc.savepoint(&view);
197197
doc.apply(&transaction, view.id);
198198

199199
editor.last_completion = Some(CompleteAction {

helix-term/src/ui/editor.rs

+9-4
Original file line numberDiff line numberDiff line change
@@ -943,14 +943,21 @@ impl EditorView {
943943
pub fn set_completion(
944944
&mut self,
945945
editor: &mut Editor,
946+
savepoint: Arc<SavePoint>,
946947
items: Vec<helix_lsp::lsp::CompletionItem>,
947948
offset_encoding: helix_lsp::OffsetEncoding,
948949
start_offset: usize,
949950
trigger_offset: usize,
950951
size: Rect,
951952
) {
952-
let mut completion =
953-
Completion::new(editor, items, offset_encoding, start_offset, trigger_offset);
953+
let mut completion = Completion::new(
954+
editor,
955+
savepoint,
956+
items,
957+
offset_encoding,
958+
start_offset,
959+
trigger_offset,
960+
);
954961

955962
if completion.is_empty() {
956963
// skip if we got no completion results
@@ -969,8 +976,6 @@ impl EditorView {
969976
self.completion = None;
970977

971978
// Clear any savepoints
972-
let doc = doc_mut!(editor);
973-
doc.savepoint = None;
974979
editor.clear_idle_timer(); // don't retrigger
975980
}
976981

helix-view/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ toml = "0.7"
4343
log = "~0.4"
4444

4545
which = "4.4"
46+
parking_lot = "0.12.1"
4647

4748

4849
[target.'cfg(windows)'.dependencies]

helix-view/src/document.rs

+61-15
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use helix_core::text_annotations::TextAnnotations;
99
use helix_core::Range;
1010
use helix_vcs::{DiffHandle, DiffProviderRegistry};
1111

12+
use ::parking_lot::Mutex;
1213
use serde::de::{self, Deserialize, Deserializer};
1314
use serde::Serialize;
1415
use std::borrow::Cow;
@@ -18,7 +19,7 @@ use std::fmt::Display;
1819
use std::future::Future;
1920
use std::path::{Path, PathBuf};
2021
use std::str::FromStr;
21-
use std::sync::Arc;
22+
use std::sync::{Arc, Weak};
2223
use std::time::SystemTime;
2324

2425
use helix_core::{
@@ -105,6 +106,13 @@ pub struct DocumentSavedEvent {
105106
pub type DocumentSavedEventResult = Result<DocumentSavedEvent, anyhow::Error>;
106107
pub type DocumentSavedEventFuture = BoxFuture<'static, DocumentSavedEventResult>;
107108

109+
#[derive(Debug)]
110+
pub struct SavePoint {
111+
/// The view this savepoint is associated here
112+
pub view: ViewId,
113+
revert: Mutex<Transaction>,
114+
}
115+
108116
pub 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,46 @@ 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 savepoint. Until `doc.restore` is called,
959+
///
960+
/// `Document::savepoint` is set to `None`, or `savepoint` is called again
961+
/// a transaction to revert the document back to this savepoint is automatically
962+
/// maintained. `Document::restore` can be used to reset the document contents
963+
/// and view selection back to the state when this function was called.
964+
///
965+
/// # Returns
966+
///
967+
/// An `id` that can uniquely identify this savepoint. A document
968+
/// can only track a single savepoint at a time, to check if your savepoint still
969+
/// applies simply
970+
pub fn savepoint(&mut self, view: &View) -> Arc<SavePoint> {
971+
let revert = Transaction::new(self.text()).with_selection(self.selection(view.id).clone());
972+
let savepoint = Arc::new(SavePoint {
973+
view: view.id,
974+
revert: Mutex::new(revert),
975+
});
976+
self.savepoints.push(Arc::downgrade(&savepoint));
977+
savepoint
946978
}
947979

948-
pub fn restore(&mut self, view: &mut View) {
949-
if let Some(revert) = self.savepoint.take() {
950-
self.apply(&revert, view.id);
951-
}
980+
pub fn restore(&mut self, view: &mut View, savepoint: &SavePoint) {
981+
assert_eq!(
982+
savepoint.view, view.id,
983+
"Savepoint must not be used with a different view!"
984+
);
985+
// search and remove savepoint using a ptr comparison
986+
// this avoids a deadlock as we need to lock the mutex
987+
let savepoint_idx = self
988+
.savepoints
989+
.iter()
990+
.position(|savepoint_ref| savepoint_ref.as_ptr() == savepoint as *const _)
991+
.expect("Savepoint must belong to this document");
992+
993+
let savepoint_ref = self.savepoints.remove(savepoint_idx);
994+
let mut revert = savepoint.revert.lock();
995+
self.apply(&revert, view.id);
996+
*revert = Transaction::new(self.text()).with_selection(self.selection(view.id).clone());
997+
self.savepoints.push(savepoint_ref)
952998
}
953999

9541000
fn earlier_later_impl(&mut self, view: &mut View, uk: UndoKind, earlier: bool) -> bool {

0 commit comments

Comments
 (0)