Reorganize Document::apply_impl (#11304)

These changes are ported from
<https://redirect.github.com/helix-editor/helix/pull/9801>. It's a
cleanup of `Document::apply_impl` that uses some early returns to
reduce nesting and some reordering of the steps. The early returns
bail out of `apply_impl` early if the transaction fails to apply or
if the changes are empty (in which case we emit the SelectionDidChange
event). It's a somewhat cosmetic refactor that makes the function easier
to reason about but it also makes it harder to introduce bugs by mapping
positions through empty changesets for example.

Co-authored-by: Pascal Kuthe <pascalkuthe@pm.me>
pull/8324/head
Michael Davis 4 months ago committed by GitHub
parent 9b65448f53
commit a1e20a3426
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1222,34 +1222,12 @@ impl Document {
use helix_core::Assoc; use helix_core::Assoc;
let old_doc = self.text().clone(); let old_doc = self.text().clone();
let changes = transaction.changes();
if !changes.apply(&mut self.text) {
return false;
}
let success = transaction.changes().apply(&mut self.text); if changes.is_empty() {
if success {
if emit_lsp_notification {
helix_event::dispatch(DocumentDidChange {
doc: self,
view: view_id,
old_text: &old_doc,
});
}
for selection in self.selections.values_mut() {
*selection = selection
.clone()
// Map through changes
.map(transaction.changes())
// Ensure all selections across all views still adhere to invariants.
.ensure_invariants(self.text.slice(..));
}
for view_data in self.view_data.values_mut() {
view_data.view_position.anchor = transaction
.changes()
.map_pos(view_data.view_position.anchor, Assoc::Before);
}
// if specified, the current selection should instead be replaced by transaction.selection
if let Some(selection) = transaction.selection() { if let Some(selection) = transaction.selection() {
self.selections.insert( self.selections.insert(
view_id, view_id,
@ -1260,129 +1238,160 @@ impl Document {
view: view_id, view: view_id,
}); });
} }
return true;
}
self.modified_since_accessed = true; self.modified_since_accessed = true;
self.version += 1;
for selection in self.selections.values_mut() {
*selection = selection
.clone()
// Map through changes
.map(transaction.changes())
// Ensure all selections across all views still adhere to invariants.
.ensure_invariants(self.text.slice(..));
} }
if !transaction.changes().is_empty() { for view_data in self.view_data.values_mut() {
self.version += 1; view_data.view_position.anchor = transaction
// start computing the diff in parallel .changes()
if let Some(diff_handle) = &self.diff_handle { .map_pos(view_data.view_position.anchor, Assoc::Before);
diff_handle.update_document(self.text.clone(), false); }
}
// generate revert to savepoint // generate revert to savepoint
if !self.savepoints.is_empty() { if !self.savepoints.is_empty() {
let revert = transaction.invert(&old_doc); let revert = transaction.invert(&old_doc);
self.savepoints self.savepoints
.retain_mut(|save_point| match save_point.upgrade() { .retain_mut(|save_point| match save_point.upgrade() {
Some(savepoint) => { Some(savepoint) => {
let mut revert_to_savepoint = savepoint.revert.lock(); let mut revert_to_savepoint = savepoint.revert.lock();
*revert_to_savepoint = *revert_to_savepoint =
revert.clone().compose(mem::take(&mut revert_to_savepoint)); revert.clone().compose(mem::take(&mut revert_to_savepoint));
true true
} }
None => false, None => false,
}) })
}
// update tree-sitter syntax tree
if let Some(syntax) = &mut self.syntax {
// TODO: no unwrap
let res = syntax.update(
old_doc.slice(..),
self.text.slice(..),
transaction.changes(),
);
if res.is_err() {
log::error!("TS parser failed, disabling TS for the current buffer: {res:?}");
self.syntax = None;
} }
}
// update tree-sitter syntax tree // TODO: all of that should likely just be hooks
if let Some(syntax) = &mut self.syntax { // start computing the diff in parallel
// TODO: no unwrap if let Some(diff_handle) = &self.diff_handle {
let res = syntax.update( diff_handle.update_document(self.text.clone(), false);
old_doc.slice(..), }
self.text.slice(..),
transaction.changes(), // map diagnostics over changes too
); changes.update_positions(self.diagnostics.iter_mut().map(|diagnostic| {
if res.is_err() { let assoc = if diagnostic.starts_at_word {
log::error!("TS parser failed, disabling TS for the current buffer: {res:?}"); Assoc::BeforeWord
self.syntax = None; } else {
} Assoc::After
};
(&mut diagnostic.range.start, assoc)
}));
changes.update_positions(self.diagnostics.iter_mut().filter_map(|diagnostic| {
if diagnostic.zero_width {
// for zero width diagnostics treat the diagnostic as a point
// rather than a range
return None;
}
let assoc = if diagnostic.ends_at_word {
Assoc::AfterWord
} else {
Assoc::Before
};
Some((&mut diagnostic.range.end, assoc))
}));
self.diagnostics.retain_mut(|diagnostic| {
if diagnostic.zero_width {
diagnostic.range.end = diagnostic.range.start
} else if diagnostic.range.start >= diagnostic.range.end {
return false;
} }
diagnostic.line = self.text.char_to_line(diagnostic.range.start);
true
});
let changes = transaction.changes(); self.diagnostics
.sort_by_key(|diagnostic| (diagnostic.range, diagnostic.severity, diagnostic.provider));
// map diagnostics over changes too // Update the inlay hint annotations' positions, helping ensure they are displayed in the proper place
changes.update_positions(self.diagnostics.iter_mut().map(|diagnostic| { let apply_inlay_hint_changes = |annotations: &mut Vec<InlineAnnotation>| {
let assoc = if diagnostic.starts_at_word { changes.update_positions(
Assoc::BeforeWord annotations
} else { .iter_mut()
Assoc::After .map(|annotation| (&mut annotation.char_idx, Assoc::After)),
}; );
(&mut diagnostic.range.start, assoc) };
}));
changes.update_positions(self.diagnostics.iter_mut().filter_map(|diagnostic| {
if diagnostic.zero_width {
// for zero width diagnostics treat the diagnostic as a point
// rather than a range
return None;
}
let assoc = if diagnostic.ends_at_word {
Assoc::AfterWord
} else {
Assoc::Before
};
Some((&mut diagnostic.range.end, assoc))
}));
self.diagnostics.retain_mut(|diagnostic| {
if diagnostic.zero_width {
diagnostic.range.end = diagnostic.range.start
} else if diagnostic.range.start >= diagnostic.range.end {
return false;
}
diagnostic.line = self.text.char_to_line(diagnostic.range.start);
true
});
self.diagnostics.sort_by_key(|diagnostic| { self.inlay_hints_oudated = true;
(diagnostic.range, diagnostic.severity, diagnostic.provider) for text_annotation in self.inlay_hints.values_mut() {
}); let DocumentInlayHints {
id: _,
type_inlay_hints,
parameter_inlay_hints,
other_inlay_hints,
padding_before_inlay_hints,
padding_after_inlay_hints,
} = text_annotation;
apply_inlay_hint_changes(padding_before_inlay_hints);
apply_inlay_hint_changes(type_inlay_hints);
apply_inlay_hint_changes(parameter_inlay_hints);
apply_inlay_hint_changes(other_inlay_hints);
apply_inlay_hint_changes(padding_after_inlay_hints);
}
// Update the inlay hint annotations' positions, helping ensure they are displayed in the proper place helix_event::dispatch(DocumentDidChange {
let apply_inlay_hint_changes = |annotations: &mut Vec<InlineAnnotation>| { doc: self,
changes.update_positions( view: view_id,
annotations old_text: &old_doc,
.iter_mut() });
.map(|annotation| (&mut annotation.char_idx, Assoc::After)),
);
};
self.inlay_hints_oudated = true; // if specified, the current selection should instead be replaced by transaction.selection
for text_annotation in self.inlay_hints.values_mut() { if let Some(selection) = transaction.selection() {
let DocumentInlayHints { self.selections.insert(
id: _, view_id,
type_inlay_hints, selection.clone().ensure_invariants(self.text.slice(..)),
parameter_inlay_hints, );
other_inlay_hints, helix_event::dispatch(SelectionDidChange {
padding_before_inlay_hints, doc: self,
padding_after_inlay_hints, view: view_id,
} = text_annotation; });
}
apply_inlay_hint_changes(padding_before_inlay_hints);
apply_inlay_hint_changes(type_inlay_hints);
apply_inlay_hint_changes(parameter_inlay_hints);
apply_inlay_hint_changes(other_inlay_hints);
apply_inlay_hint_changes(padding_after_inlay_hints);
}
if emit_lsp_notification { if emit_lsp_notification {
// TODO: move to hook // TODO: move to hook
// emit lsp notification // emit lsp notification
for language_server in self.language_servers() { for language_server in self.language_servers() {
let notify = language_server.text_document_did_change( let notify = language_server.text_document_did_change(
self.versioned_identifier(), self.versioned_identifier(),
&old_doc, &old_doc,
self.text(), self.text(),
changes, changes,
); );
if let Some(notify) = notify { if let Some(notify) = notify {
tokio::spawn(notify); tokio::spawn(notify);
}
} }
} }
} }
success
true
} }
fn apply_inner( fn apply_inner(

Loading…
Cancel
Save