summaryrefslogtreecommitdiff
path: root/helix-lsp
diff options
context:
space:
mode:
authorAndrii Grynenko2023-02-17 15:51:00 +0000
committerBlaž Hrastnik2023-03-08 01:48:35 +0000
commit1866b43cd355ff6d41d579b4b710a0f602aa79d1 (patch)
tree8550d0bc46df11e0a28c301becf75642ac70317d /helix-lsp
parentec6e575a408372400b7789b90cdf6ac271f51182 (diff)
Render every LSP snippets for every cursor
This refactors the snippet logic to be largely unaware of the rest of the document. The completion application logic is moved into generate_transaction_from_snippet which is extended to support dynamically computing replacement text.
Diffstat (limited to 'helix-lsp')
-rw-r--r--helix-lsp/src/lib.rs79
-rw-r--r--helix-lsp/src/snippet.rs187
2 files changed, 162 insertions, 104 deletions
diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs
index cce848ab..5b4f7ee4 100644
--- a/helix-lsp/src/lib.rs
+++ b/helix-lsp/src/lib.rs
@@ -60,6 +60,7 @@ pub mod util {
use super::*;
use helix_core::line_ending::{line_end_byte_index, line_end_char_index};
use helix_core::{diagnostic::NumberOrString, Range, Rope, Selection, Tendril, Transaction};
+ use helix_core::{smallvec, SmallVec};
/// Converts a diagnostic in the document to [`lsp::Diagnostic`].
///
@@ -282,6 +283,84 @@ pub mod util {
})
}
+ /// Creates a [Transaction] from the [snippet::Snippet] in a completion response.
+ /// The transaction applies the edit to all cursors.
+ pub fn generate_transaction_from_snippet(
+ doc: &Rope,
+ selection: &Selection,
+ edit_range: &lsp::Range,
+ snippet: snippet::Snippet,
+ line_ending: &str,
+ include_placeholder: bool,
+ offset_encoding: OffsetEncoding,
+ ) -> Transaction {
+ let text = doc.slice(..);
+ let primary_cursor = selection.primary().cursor(text);
+
+ let start_offset = match lsp_pos_to_pos(doc, edit_range.start, offset_encoding) {
+ Some(start) => start as i128 - primary_cursor as i128,
+ None => return Transaction::new(doc),
+ };
+ let end_offset = match lsp_pos_to_pos(doc, edit_range.end, offset_encoding) {
+ Some(end) => end as i128 - primary_cursor as i128,
+ None => return Transaction::new(doc),
+ };
+
+ // For each cursor store offsets for the first tabstop
+ let mut cursor_tabstop_offsets = Vec::<SmallVec<[(i128, i128); 1]>>::new();
+ let transaction = Transaction::change_by_selection(doc, selection, |range| {
+ let cursor = range.cursor(text);
+ let replacement_start = (cursor as i128 + start_offset) as usize;
+ let replacement_end = (cursor as i128 + end_offset) as usize;
+ 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 = ""
+ );
+
+ let (replacement, tabstops) =
+ snippet::render(&snippet, newline_with_offset, include_placeholder);
+
+ let replacement_len = replacement.chars().count();
+ cursor_tabstop_offsets.push(
+ tabstops
+ .first()
+ .unwrap_or(&smallvec![(replacement_len, replacement_len)])
+ .iter()
+ .map(|(from, to)| -> (i128, i128) {
+ (
+ *from as i128 - replacement_len as i128,
+ *to as i128 - replacement_len as i128,
+ )
+ })
+ .collect(),
+ );
+
+ (replacement_start, replacement_end, Some(replacement.into()))
+ });
+
+ // Create new selection based on the cursor tabstop from above
+ let mut cursor_tabstop_offsets_iter = cursor_tabstop_offsets.iter();
+ let selection = selection
+ .clone()
+ .map(transaction.changes())
+ .transform_iter(|range| {
+ cursor_tabstop_offsets_iter
+ .next()
+ .unwrap()
+ .iter()
+ .map(move |(from, to)| {
+ Range::new(
+ (range.anchor as i128 + *from) as usize,
+ (range.anchor as i128 + *to) as usize,
+ )
+ })
+ });
+
+ transaction.with_selection(selection)
+ }
+
pub fn generate_transaction_from_edits(
doc: &Rope,
mut edits: Vec<lsp::TextEdit>,
diff --git a/helix-lsp/src/snippet.rs b/helix-lsp/src/snippet.rs
index ab0f406d..63054cdb 100644
--- a/helix-lsp/src/snippet.rs
+++ b/helix-lsp/src/snippet.rs
@@ -1,9 +1,7 @@
use std::borrow::Cow;
use anyhow::{anyhow, Result};
-use helix_core::SmallVec;
-
-use crate::{util::lsp_pos_to_pos, OffsetEncoding};
+use helix_core::{SmallVec, smallvec};
#[derive(Debug, PartialEq, Eq)]
pub enum CaseChange {
@@ -34,7 +32,7 @@ pub enum SnippetElement<'a> {
},
Placeholder {
tabstop: usize,
- value: Box<SnippetElement<'a>>,
+ value: Vec<SnippetElement<'a>>,
},
Choice {
tabstop: usize,
@@ -57,141 +55,108 @@ pub fn parse(s: &str) -> Result<Snippet<'_>> {
parser::parse(s).map_err(|rest| anyhow!("Failed to parse snippet. Remaining input: {}", rest))
}
-pub fn into_transaction<'a>(
- snippet: Snippet<'a>,
- doc: &helix_core::Rope,
- selection: &helix_core::Selection,
- edit: &lsp_types::TextEdit,
- line_ending: &str,
- offset_encoding: OffsetEncoding,
+fn render_elements(
+ snippet_elements: &[SnippetElement<'_>],
+ insert: &mut String,
+ offset: &mut usize,
+ tabstops: &mut Vec<(usize, (usize, usize))>,
+ newline_with_offset: &String,
include_placeholer: bool,
-) -> helix_core::Transaction {
- use helix_core::{smallvec, Range, Transaction};
+) {
use SnippetElement::*;
- let text = doc.slice(..);
- let primary_cursor = selection.primary().cursor(text);
-
- let start_offset = match lsp_pos_to_pos(doc, edit.range.start, offset_encoding) {
- Some(start) => start as i128 - primary_cursor as i128,
- None => return Transaction::new(doc),
- };
- let end_offset = match lsp_pos_to_pos(doc, edit.range.end, offset_encoding) {
- Some(end) => end as i128 - primary_cursor as i128,
- None => return Transaction::new(doc),
- };
-
- let newline_with_offset = format!(
- "{line_ending}{blank:width$}",
- width = edit.range.start.character as usize,
- blank = ""
- );
-
- let mut offset = 0;
- let mut insert = String::new();
- let mut tabstops: Vec<(usize, usize, usize)> = Vec::new();
-
- for element in snippet.elements {
+ for element in snippet_elements {
match element {
- Text(text) => {
+ &Text(text) => {
// small optimization to avoid calling replace when it's unnecessary
let text = if text.contains('\n') {
- Cow::Owned(text.replace('\n', &newline_with_offset))
+ Cow::Owned(text.replace('\n', newline_with_offset))
} else {
Cow::Borrowed(text)
};
- offset += text.chars().count();
+ *offset += text.chars().count();
insert.push_str(&text);
}
- Variable {
- name: _name,
- regex: None,
+ &Variable {
+ name: _,
+ regex: _,
r#default,
} => {
// TODO: variables. For now, fall back to the default, which defaults to "".
let text = r#default.unwrap_or_default();
- offset += text.chars().count();
+ *offset += text.chars().count();
insert.push_str(text);
}
- Tabstop { tabstop } => {
- tabstops.push((tabstop, offset, offset));
+ &Tabstop { tabstop } => {
+ tabstops.push((tabstop, (*offset, *offset)));
}
- Placeholder { tabstop, value } => match value.as_ref() {
- // https://doc.rust-lang.org/beta/unstable-book/language-features/box-patterns.html
- // would make this a bit nicer
- Text(text) => {
- if include_placeholer {
- let len_chars = text.chars().count();
- tabstops.push((tabstop, offset, offset + len_chars + 1));
- offset += len_chars;
- insert.push_str(text);
- } else {
- tabstops.push((tabstop, offset, offset));
- }
- }
- other => {
- log::error!(
- "Discarding snippet: generating a transaction for placeholder contents {:?} is unimplemented.",
- other
+ Placeholder {
+ tabstop,
+ value: inner_snippet_elements,
+ } => {
+ let start_offset = *offset;
+ if include_placeholer {
+ render_elements(
+ inner_snippet_elements,
+ insert,
+ offset,
+ tabstops,
+ newline_with_offset,
+ include_placeholer,
);
- return Transaction::new(doc);
}
- },
- other => {
- log::error!(
- "Discarding snippet: generating a transaction for {:?} is unimplemented.",
- other
- );
- return Transaction::new(doc);
+ tabstops.push((*tabstop, (start_offset, *offset)));
+ }
+ &Choice {
+ tabstop,
+ choices: _,
+ } => {
+ // TODO: choices
+ tabstops.push((tabstop, (*offset, *offset)));
}
}
}
+}
- let transaction = Transaction::change_by_selection(doc, selection, |range| {
- let cursor = range.cursor(text);
- (
- (cursor as i128 + start_offset) as usize,
- (cursor as i128 + end_offset) as usize,
- Some(insert.clone().into()),
- )
- });
+#[allow(clippy::type_complexity)] // only used one time
+pub fn render(
+ snippet: &Snippet<'_>,
+ newline_with_offset: String,
+ include_placeholer: bool,
+) -> (String, Vec<SmallVec<[(usize, usize); 1]>>) {
+ let mut insert = String::new();
+ let mut tabstops = Vec::new();
+ let mut offset = 0;
+
+ render_elements(
+ &snippet.elements,
+ &mut insert,
+ &mut offset,
+ &mut tabstops,
+ &newline_with_offset,
+ include_placeholer,
+ );
// sort in ascending order (except for 0, which should always be the last one (per lsp doc))
- tabstops.sort_unstable_by_key(|(n, _o1, _o2)| if *n == 0 { usize::MAX } else { *n });
+ tabstops.sort_unstable_by_key(|(n, _)| if *n == 0 { usize::MAX } else { *n });
// merge tabstops with the same index (we take advantage of the fact that we just sorted them
// above to simply look backwards)
let mut ntabstops = Vec::<SmallVec<[(usize, usize); 1]>>::new();
{
let mut prev = None;
- for (tabstop, o1, o2) in tabstops {
+ for (tabstop, r) in tabstops {
if prev == Some(tabstop) {
let len_1 = ntabstops.len() - 1;
- ntabstops[len_1].push((o1, o2));
+ ntabstops[len_1].push(r);
} else {
prev = Some(tabstop);
- ntabstops.push(smallvec![(o1, o2)]);
+ ntabstops.push(smallvec![r]);
}
}
}
- if let Some(first) = ntabstops.first() {
- let cursor_offset = insert.chars().count() as i128 - (end_offset - start_offset);
- let mut extra_offset = start_offset;
- transaction.with_selection(selection.clone().transform_iter(|range| {
- let cursor = range.cursor(text);
- let iter = first.iter().map(move |first| {
- Range::new(
- (cursor as i128 + first.0 as i128 + extra_offset) as usize,
- (cursor as i128 + first.1 as i128 + extra_offset) as usize,
- )
- });
- extra_offset += cursor_offset;
- iter
- }))
- } else {
- transaction
- }
+ (insert, ntabstops)
}
mod parser {
@@ -343,14 +308,15 @@ mod parser {
fn placeholder<'a>() -> impl Parser<'a, Output = SnippetElement<'a>> {
// TODO: why doesn't parse_as work?
// let value = reparse_as(take_until(|c| c == '}'), anything());
+ // TODO: fix this to parse nested placeholders (take until terminates too early)
let value = filter_map(take_until(|c| c == '}'), |s| {
- anything().parse(s).map(|parse_result| parse_result.1).ok()
+ snippet().parse(s).map(|parse_result| parse_result.1).ok()
});
map(seq!("${", digit(), ":", value, "}"), |seq| {
SnippetElement::Placeholder {
tabstop: seq.1,
- value: Box::new(seq.3),
+ value: seq.3.elements,
}
})
}
@@ -430,7 +396,7 @@ mod parser {
Text("match("),
Placeholder {
tabstop: 1,
- value: Box::new(Text("Arg1")),
+ value: vec!(Text("Arg1")),
},
Text(")")
]
@@ -447,12 +413,12 @@ mod parser {
Text("local "),
Placeholder {
tabstop: 1,
- value: Box::new(Text("var")),
+ value: vec!(Text("var")),
},
Text(" = "),
Placeholder {
tabstop: 1,
- value: Box::new(Text("value")),
+ value: vec!(Text("value")),
},
]
}),
@@ -461,6 +427,19 @@ mod parser {
}
#[test]
+ fn parse_tabstop_nested_in_placeholder() {
+ assert_eq!(
+ Ok(Snippet {
+ elements: vec![Placeholder {
+ tabstop: 1,
+ value: vec!(Text("var, "), Tabstop { tabstop: 2 },),
+ },]
+ }),
+ parse("${1:var, $2}")
+ )
+ }
+
+ #[test]
fn parse_all() {
assert_eq!(
Ok(Snippet {