on master help sign in
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.

commit1866b43cd355ff6d41d579b4b710a0f602aa79d1 authorAndrii Grynenko <gluk.ua@gmail.com> dateFri 17 Feb 2023, 07:51 (4 years ago) parentec6e575
3 files changed+166−108
helix-lsp/src/lib.rs+79 helix-lsp/src/snippet.rs+83−104 helix-term/src/ui/completion.rs+4−4
helix-lsp/src/lib.rs+79
@@ -60,6 +60,7 @@ pub mod util {
6060 use super::*;
6161 use helix_core::line_ending::{line_end_byte_index, line_end_char_index};
6262 use helix_core::{diagnostic::NumberOrString, Range, Rope, Selection, Tendril, Transaction};
63+ use helix_core::{smallvec, SmallVec};
6364
6465 /// Converts a diagnostic in the document to [`lsp::Diagnostic`].
6566 ///
@@ -282,6 +283,84 @@ pub mod util {
282283 })
283284 }
284285
286+ /// Creates a [Transaction] from the [snippet::Snippet] in a completion response.
287+ /// The transaction applies the edit to all cursors.
288+ pub fn generate_transaction_from_snippet(
289+ doc: &Rope,
290+ selection: &Selection,
291+ edit_range: &lsp::Range,
292+ snippet: snippet::Snippet,
293+ line_ending: &str,
294+ include_placeholder: bool,
295+ offset_encoding: OffsetEncoding,
296+ ) -> Transaction {
297+ let text = doc.slice(..);
298+ let primary_cursor = selection.primary().cursor(text);
299+
300+ let start_offset = match lsp_pos_to_pos(doc, edit_range.start, offset_encoding) {
301+ Some(start) => start as i128 - primary_cursor as i128,
302+ None => return Transaction::new(doc),
303+ };
304+ let end_offset = match lsp_pos_to_pos(doc, edit_range.end, offset_encoding) {
305+ Some(end) => end as i128 - primary_cursor as i128,
306+ None => return Transaction::new(doc),
307+ };
308+
309+ // For each cursor store offsets for the first tabstop
310+ let mut cursor_tabstop_offsets = Vec::<SmallVec<[(i128, i128); 1]>>::new();
311+ let transaction = Transaction::change_by_selection(doc, selection, |range| {
312+ let cursor = range.cursor(text);
313+ let replacement_start = (cursor as i128 + start_offset) as usize;
314+ let replacement_end = (cursor as i128 + end_offset) as usize;
315+ let newline_with_offset = format!(
316+ "{line_ending}{blank:width$}",
317+ line_ending = line_ending,
318+ width = replacement_start - doc.line_to_char(doc.char_to_line(replacement_start)),
319+ blank = ""
320+ );
321+
322+ let (replacement, tabstops) =
323+ snippet::render(&snippet, newline_with_offset, include_placeholder);
324+
325+ let replacement_len = replacement.chars().count();
326+ cursor_tabstop_offsets.push(
327+ tabstops
328+ .first()
329+ .unwrap_or(&smallvec![(replacement_len, replacement_len)])
330+ .iter()
331+ .map(|(from, to)| -> (i128, i128) {
332+ (
333+ *from as i128 - replacement_len as i128,
334+ *to as i128 - replacement_len as i128,
335+ )
336+ })
337+ .collect(),
338+ );
339+
340+ (replacement_start, replacement_end, Some(replacement.into()))
341+ });
342+
343+ // Create new selection based on the cursor tabstop from above
344+ let mut cursor_tabstop_offsets_iter = cursor_tabstop_offsets.iter();
345+ let selection = selection
346+ .clone()
347+ .map(transaction.changes())
348+ .transform_iter(|range| {
349+ cursor_tabstop_offsets_iter
350+ .next()
351+ .unwrap()
352+ .iter()
353+ .map(move |(from, to)| {
354+ Range::new(
355+ (range.anchor as i128 + *from) as usize,
356+ (range.anchor as i128 + *to) as usize,
357+ )
358+ })
359+ });
360+
361+ transaction.with_selection(selection)
362+ }
363+
285364 pub fn generate_transaction_from_edits(
286365 doc: &Rope,
287366 mut edits: Vec<lsp::TextEdit>,
helix-lsp/src/snippet.rs+83−104
@@ -1,9 +1,7 @@
11use std::borrow::Cow;
22
33use anyhow::{anyhow, Result};
4−use helix_core::SmallVec;
5−
6−use crate::{util::lsp_pos_to_pos, OffsetEncoding};
4+use helix_core::{SmallVec, smallvec};
75
86#[derive(Debug, PartialEq, Eq)]
97pub enum CaseChange {
@@ -34,7 +32,7 @@ pub enum SnippetElement<'a> {
3432 },
3533 Placeholder {
3634 tabstop: usize,
37− value: Box<SnippetElement<'a>>,
35+ value: Vec<SnippetElement<'a>>,
3836 },
3937 Choice {
4038 tabstop: usize,
@@ -57,141 +55,108 @@ pub fn parse(s: &str) -> Result<Snippet<'_>> {
5755 parser::parse(s).map_err(|rest| anyhow!("Failed to parse snippet. Remaining input: {}", rest))
5856}
5957
60−pub fn into_transaction<'a>(
61− snippet: Snippet<'a>,
62− doc: &helix_core::Rope,
63− selection: &helix_core::Selection,
64− edit: &lsp_types::TextEdit,
65− line_ending: &str,
66− offset_encoding: OffsetEncoding,
58+fn render_elements(
59+ snippet_elements: &[SnippetElement<'_>],
60+ insert: &mut String,
61+ offset: &mut usize,
62+ tabstops: &mut Vec<(usize, (usize, usize))>,
63+ newline_with_offset: &String,
6764 include_placeholer: bool,
68−) -> helix_core::Transaction {
69− use helix_core::{smallvec, Range, Transaction};
65+) {
7066 use SnippetElement::*;
7167
72− let text = doc.slice(..);
73− let primary_cursor = selection.primary().cursor(text);
74−
75− let start_offset = match lsp_pos_to_pos(doc, edit.range.start, offset_encoding) {
76− Some(start) => start as i128 - primary_cursor as i128,
77− None => return Transaction::new(doc),
78− };
79− let end_offset = match lsp_pos_to_pos(doc, edit.range.end, offset_encoding) {
80− Some(end) => end as i128 - primary_cursor as i128,
81− None => return Transaction::new(doc),
82− };
83−
84− let newline_with_offset = format!(
85− "{line_ending}{blank:width$}",
86− width = edit.range.start.character as usize,
87− blank = ""
88− );
89−
90− let mut offset = 0;
91− let mut insert = String::new();
92− let mut tabstops: Vec<(usize, usize, usize)> = Vec::new();
93−
94− for element in snippet.elements {
68+ for element in snippet_elements {
9569 match element {
96− Text(text) => {
70+ &Text(text) => {
9771 // small optimization to avoid calling replace when it's unnecessary
9872 let text = if text.contains('\n') {
99− Cow::Owned(text.replace('\n', &newline_with_offset))
73+ Cow::Owned(text.replace('\n', newline_with_offset))
10074 } else {
10175 Cow::Borrowed(text)
10276 };
103− offset += text.chars().count();
77+ *offset += text.chars().count();
10478 insert.push_str(&text);
10579 }
106− Variable {
107− name: _name,
108− regex: None,
80+ &Variable {
81+ name: _,
82+ regex: _,
10983 r#default,
11084 } => {
11185 // TODO: variables. For now, fall back to the default, which defaults to "".
11286 let text = r#default.unwrap_or_default();
113− offset += text.chars().count();
87+ *offset += text.chars().count();
11488 insert.push_str(text);
11589 }
116− Tabstop { tabstop } => {
117− tabstops.push((tabstop, offset, offset));
90+ &Tabstop { tabstop } => {
91+ tabstops.push((tabstop, (*offset, *offset)));
11892 }
119− Placeholder { tabstop, value } => match value.as_ref() {
120− // https://doc.rust-lang.org/beta/unstable-book/language-features/box-patterns.html
121− // would make this a bit nicer
122− Text(text) => {
123− if include_placeholer {
124− let len_chars = text.chars().count();
125− tabstops.push((tabstop, offset, offset + len_chars + 1));
126− offset += len_chars;
127− insert.push_str(text);
128− } else {
129− tabstops.push((tabstop, offset, offset));
130− }
131− }
132− other => {
133− log::error!(
134− "Discarding snippet: generating a transaction for placeholder contents {:?} is unimplemented.",
135− other
93+ Placeholder {
94+ tabstop,
95+ value: inner_snippet_elements,
96+ } => {
97+ let start_offset = *offset;
98+ if include_placeholer {
99+ render_elements(
100+ inner_snippet_elements,
101+ insert,
102+ offset,
103+ tabstops,
104+ newline_with_offset,
105+ include_placeholer,
136106 );
137− return Transaction::new(doc);
138107 }
139− },
140− other => {
141− log::error!(
142− "Discarding snippet: generating a transaction for {:?} is unimplemented.",
143− other
144− );
145− return Transaction::new(doc);
108+ tabstops.push((*tabstop, (start_offset, *offset)));
109+ }
110+ &Choice {
111+ tabstop,
112+ choices: _,
113+ } => {
114+ // TODO: choices
115+ tabstops.push((tabstop, (*offset, *offset)));
146116 }
147117 }
148118 }
119+}
149120
150− let transaction = Transaction::change_by_selection(doc, selection, |range| {
151− let cursor = range.cursor(text);
152− (
153− (cursor as i128 + start_offset) as usize,
154− (cursor as i128 + end_offset) as usize,
155− Some(insert.clone().into()),
156− )
157− });
121+#[allow(clippy::type_complexity)] // only used one time
122+pub fn render(
123+ snippet: &Snippet<'_>,
124+ newline_with_offset: String,
125+ include_placeholer: bool,
126+) -> (String, Vec<SmallVec<[(usize, usize); 1]>>) {
127+ let mut insert = String::new();
128+ let mut tabstops = Vec::new();
129+ let mut offset = 0;
130+
131+ render_elements(
132+ &snippet.elements,
133+ &mut insert,
134+ &mut offset,
135+ &mut tabstops,
136+ &newline_with_offset,
137+ include_placeholer,
138+ );
158139
159140 // sort in ascending order (except for 0, which should always be the last one (per lsp doc))
160− tabstops.sort_unstable_by_key(|(n, _o1, _o2)| if *n == 0 { usize::MAX } else { *n });
141+ tabstops.sort_unstable_by_key(|(n, _)| if *n == 0 { usize::MAX } else { *n });
161142
162143 // merge tabstops with the same index (we take advantage of the fact that we just sorted them
163144 // above to simply look backwards)
164145 let mut ntabstops = Vec::<SmallVec<[(usize, usize); 1]>>::new();
165146 {
166147 let mut prev = None;
167− for (tabstop, o1, o2) in tabstops {
148+ for (tabstop, r) in tabstops {
168149 if prev == Some(tabstop) {
169150 let len_1 = ntabstops.len() - 1;
170− ntabstops[len_1].push((o1, o2));
151+ ntabstops[len_1].push(r);
171152 } else {
172153 prev = Some(tabstop);
173− ntabstops.push(smallvec![(o1, o2)]);
154+ ntabstops.push(smallvec![r]);
174155 }
175156 }
176157 }
177158
178− if let Some(first) = ntabstops.first() {
179− let cursor_offset = insert.chars().count() as i128 - (end_offset - start_offset);
180− let mut extra_offset = start_offset;
181− transaction.with_selection(selection.clone().transform_iter(|range| {
182− let cursor = range.cursor(text);
183− let iter = first.iter().map(move |first| {
184− Range::new(
185− (cursor as i128 + first.0 as i128 + extra_offset) as usize,
186− (cursor as i128 + first.1 as i128 + extra_offset) as usize,
187− )
188− });
189− extra_offset += cursor_offset;
190− iter
191− }))
192− } else {
193− transaction
194− }
159+ (insert, ntabstops)
195160}
196161
197162mod parser {
@@ -343,14 +308,15 @@ mod parser {
343308 fn placeholder<'a>() -> impl Parser<'a, Output = SnippetElement<'a>> {
344309 // TODO: why doesn't parse_as work?
345310 // let value = reparse_as(take_until(|c| c == '}'), anything());
311+ // TODO: fix this to parse nested placeholders (take until terminates too early)
346312 let value = filter_map(take_until(|c| c == '}'), |s| {
347− anything().parse(s).map(|parse_result| parse_result.1).ok()
313+ snippet().parse(s).map(|parse_result| parse_result.1).ok()
348314 });
349315
350316 map(seq!("${", digit(), ":", value, "}"), |seq| {
351317 SnippetElement::Placeholder {
352318 tabstop: seq.1,
353− value: Box::new(seq.3),
319+ value: seq.3.elements,
354320 }
355321 })
356322 }
@@ -430,7 +396,7 @@ mod parser {
430396 Text("match("),
431397 Placeholder {
432398 tabstop: 1,
433− value: Box::new(Text("Arg1")),
399+ value: vec!(Text("Arg1")),
434400 },
435401 Text(")")
436402 ]
@@ -447,12 +413,12 @@ mod parser {
447413 Text("local "),
448414 Placeholder {
449415 tabstop: 1,
450− value: Box::new(Text("var")),
416+ value: vec!(Text("var")),
451417 },
452418 Text(" = "),
453419 Placeholder {
454420 tabstop: 1,
455− value: Box::new(Text("value")),
421+ value: vec!(Text("value")),
456422 },
457423 ]
458424 }),
@@ -460,6 +426,19 @@ mod parser {
460426 )
461427 }
462428
429+ #[test]
430+ fn parse_tabstop_nested_in_placeholder() {
431+ assert_eq!(
432+ Ok(Snippet {
433+ elements: vec![Placeholder {
434+ tabstop: 1,
435+ value: vec!(Text("var, "), Tabstop { tabstop: 2 },),
436+ },]
437+ }),
438+ parse("${1:var, $2}")
439+ )
440+ }
441+
463442 #[test]
464443 fn parse_all() {
465444 assert_eq!(
helix-term/src/ui/completion.rs+4−4
@@ -138,14 +138,14 @@ impl Completion {
138138 )
139139 {
140140 match snippet::parse(&edit.new_text) {
141− Ok(snippet) => snippet::into_transaction(
142− snippet,
141+ Ok(snippet) => util::generate_transaction_from_snippet(
143142 doc.text(),
144143 doc.selection(view_id),
145− &edit,
144+ &edit.range,
145+ snippet,
146146 doc.line_ending.as_str(),
147− offset_encoding,
148147 include_placeholder,
148+ offset_encoding,
149149 ),
150150 Err(err) => {
151151 log::error!(