Skip to content

Commit

Permalink
use partial ChangeSet to accuaretly render snippet
Browse files Browse the repository at this point in the history
  • Loading branch information
pascalkuthe committed Mar 8, 2023
1 parent 8fccfa9 commit 22394dd
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 36 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

55 changes: 35 additions & 20 deletions helix-core/src/transaction.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use smallvec::SmallVec;

use crate::{Range, Rope, Selection, Tendril};
use std::borrow::Cow;

Expand Down Expand Up @@ -111,6 +109,24 @@ impl ChangeSet {
}
}

/// runs `f` with this changeset extended to length `len`
pub(crate) fn with_len<T>(&mut self, len: usize, f: impl FnOnce(&Self) -> T) -> T {
use Operation::*;
assert!(len >= self.len, "length most{len}, {}", self.len);
let n = len - self.len;
self.retain(n);
let res = f(self);
self.len -= n;
self.len_after -= n;

if let Some(Retain(count)) = self.changes.last_mut() {
*count -= n;
} else {
self.changes.pop();
}
res
}

/// Combine two changesets together.
/// In other words, If `this` goes `docA` → `docB` and `other` represents `docB` → `docC`, the
/// returned value will represent the change `docA` → `docC`.
Expand Down Expand Up @@ -477,22 +493,17 @@ impl Transaction {
/// function passes `None` to the `process_changes` function if the
/// `changes` iterator yields `None` **or if the change overlaps with a
/// previous change** and is ignored.
pub fn change_ignore_overlapping<I, T>(
pub fn change_ignore_overlapping<T>(
doc: &Rope,
changes: I,
size_hint: usize,
mut gen_next_change: impl FnMut(&ChangeSet) -> Option<Option<(Change, T)>>,
mut process_change: impl FnMut(Option<T>),
) -> Self
where
I: Iterator<Item = Option<(Change, T)>>,
{
) -> Self {
let len = doc.len_chars();

let (lower, upper) = changes.size_hint();
let size = upper.unwrap_or(lower);
let mut changeset = ChangeSet::with_capacity(2 * size + 1); // rough estimate
let mut changeset = ChangeSet::with_capacity(2 * size_hint + 1); // rough estimate

let mut last = 0;
for mut change in changes {
while let Some(mut change) = changeset.with_len(len, &mut gen_next_change) {
change = change.filter(|&((from, _, _), _)| last <= from);
let Some(((from, to, tendril), data)) = change else {
process_change(None);
Expand Down Expand Up @@ -568,7 +579,7 @@ impl Transaction {
pub fn change_by_selection_ignore_overlapping<T>(
doc: &Rope,
selection: &Selection,
mut create_change: impl FnMut(&Range) -> Option<(Change, T)>,
mut create_change: impl FnMut(&Range, &ChangeSet) -> Option<(Change, T)>,
mut process_applied_change: impl FnMut(&Range, T),
) -> (Transaction, usize) {
let mut last_selection_idx = None;
Expand All @@ -586,16 +597,20 @@ impl Transaction {
}
idx += 1;
};
let mut selection = selection.iter();
let transaction = Self::change_ignore_overlapping(
doc,
selection.iter().map(|range| {
let (change, data) = create_change(range)?;
Some((change, (range, data)))
}),
selection.len(),
|changset| {
selection.next().map(|range| {
let (change, data) = create_change(range, changset)?;
Some((change, (range, data)))
})
},
process_change,
);
// map the transaction trough changes
((transaction, new_primary_idx))
// map the transaction trough changes
(transaction, new_primary_idx)
}

/// Insert text at each selection head.
Expand Down
1 change: 1 addition & 0 deletions helix-lsp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ thiserror = "1.0"
tokio = { version = "1.26", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
tokio-stream = "0.1.12"
which = "4.4"
once_cell = "1.17"
48 changes: 37 additions & 11 deletions helix-lsp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ pub mod util {
use helix_core::line_ending::{line_end_byte_index, line_end_char_index};
use helix_core::{chars, Assoc, RopeSlice, SmallVec};
use helix_core::{diagnostic::NumberOrString, Range, Rope, Selection, Tendril, Transaction};
use once_cell::unsync::Lazy;

/// Converts a diagnostic in the document to [`lsp::Diagnostic`].
///
Expand Down Expand Up @@ -311,7 +312,7 @@ pub mod util {
let (transaction, primary_idx) = Transaction::change_by_selection_ignore_overlapping(
doc,
selection,
|range| {
|range, _| {
let cursor = range.cursor(text);
let (start, end) = completion_pos(text, start_offset, end_offset, cursor)?;
Some(((start as usize, end, replacement.clone()), ()))
Expand All @@ -327,6 +328,7 @@ pub mod util {

/// Creates a [Transaction] from the [snippet::Snippet] in a completion response.
/// The transaction applies the edit to all cursors.
#[allow(clippy::too_many_arguments)]
pub fn generate_transaction_from_snippet(
doc: &Rope,
selection: &Selection,
Expand All @@ -335,31 +337,55 @@ pub mod util {
snippet: snippet::Snippet,
line_ending: &str,
include_placeholder: bool,
tab_width: usize,
) -> Transaction {
let text = doc.slice(..);
let mut new_selection: SmallVec<[_; 1]> = SmallVec::new();

let (transaction, primary_idx) = Transaction::change_by_selection_ignore_overlapping(
doc,
selection,
|range| {
|range, changeset| {
let cursor = range.cursor(text);
let (replacement_start, replacement_end) =
completion_pos(text, start_offset, end_offset, cursor)?;
let newline_with_offset = format!(
"{line_ending}{blank:width$}",
line_ending = line_ending,
width =
replacement_start - doc.line_to_char(doc.char_to_line(replacement_start)),
blank = ""
);
// Asssoc::After is used here because there should be no edits after
// `replacement_start` (those haven't been generated yet)
let mapped_replacement_start = changeset.map_pos(replacement_start, Assoc::After);
// if a snippet needs access to it's position (line number or offset) we must apply
// previous edits to the rope, as this has some ovherhead we do it lazily
// TODO: can we avoid apply the previous edits so this becomes O(N) instead of
// O(NlogN) with the number of selection ranges?
let mapped_doc = Lazy::new(|| {
let mut mapped_doc = doc.clone();
changeset.apply(&mut mapped_doc);
mapped_doc
});
let newline_with_offset = Lazy::new(|| {
let line_idx = mapped_doc.char_to_line(mapped_replacement_start);
let pos_on_line = mapped_replacement_start - mapped_doc.line_to_char(line_idx);
// we only care about the actual offset here (not virtual text/softwrap)
// so it's ok to use the deprecated function here
#[allow(deprecated)]
let width = helix_core::visual_coords_at_pos(
mapped_doc.line(line_idx),
pos_on_line,
tab_width,
)
.col;
format!(
"{line_ending}{blank:width$}",
line_ending = line_ending,
blank = ""
)
});

let (replacement, tabstops) =
snippet::render(&snippet, newline_with_offset, include_placeholder);
snippet::render(&snippet, &newline_with_offset, include_placeholder);

Some((
(replacement_start, replacement_end, Some(replacement.into())),
(replacement_start, tabstops),
(mapped_replacement_start, tabstops),
))
},
|range, tabstops| new_selection.push((*range, tabstops)),
Expand Down
11 changes: 6 additions & 5 deletions helix-lsp/src/snippet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::borrow::Cow;

use anyhow::{anyhow, Result};
use helix_core::{smallvec, SmallVec};
use once_cell::unsync::Lazy;

#[derive(Debug, PartialEq, Eq)]
pub enum CaseChange {
Expand Down Expand Up @@ -55,12 +56,12 @@ pub fn parse(s: &str) -> Result<Snippet<'_>> {
parser::parse(s).map_err(|rest| anyhow!("Failed to parse snippet. Remaining input: {}", rest))
}

fn render_elements(
fn render_elements<F: FnOnce() -> String>(
snippet_elements: &[SnippetElement<'_>],
insert: &mut String,
offset: &mut usize,
tabstops: &mut Vec<(usize, (usize, usize))>,
newline_with_offset: &String,
newline_with_offset: &Lazy<String, F>,
include_placeholer: bool,
) {
use SnippetElement::*;
Expand Down Expand Up @@ -119,9 +120,9 @@ fn render_elements(
}

#[allow(clippy::type_complexity)] // only used one time
pub fn render(
pub fn render<F: FnOnce() -> String>(
snippet: &Snippet<'_>,
newline_with_offset: String,
newline_with_offset: &Lazy<String, F>,
include_placeholer: bool,
) -> (String, Vec<SmallVec<[(usize, usize); 1]>>) {
let mut insert = String::new();
Expand All @@ -133,7 +134,7 @@ pub fn render(
&mut insert,
&mut offset,
&mut tabstops,
&newline_with_offset,
newline_with_offset,
include_placeholer,
);

Expand Down
1 change: 1 addition & 0 deletions helix-term/src/ui/completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ impl Completion {
snippet,
doc.line_ending.as_str(),
include_placeholder,
doc.tab_width(),
),
Err(err) => {
log::error!(
Expand Down

0 comments on commit 22394dd

Please sign in to comment.