on master help sign in
Render every LSP snippets for every cursor
1866b43Andrii Grynenko
helix-lsp/src/lib.rs+79
@@ -60,6 +60,7 @@ pub mod util {
60 use super::*;
60 use super::*;
61 use helix_core::line_ending::{line_end_byte_index, line_end_char_index};
61 use helix_core::line_ending::{line_end_byte_index, line_end_char_index};
62 use helix_core::{diagnostic::NumberOrString, Range, Rope, Selection, Tendril, Transaction};
62 use helix_core::{diagnostic::NumberOrString, Range, Rope, Selection, Tendril, Transaction};
63+ use helix_core::{smallvec, SmallVec};
63
64
64 /// Converts a diagnostic in the document to [`lsp::Diagnostic`].
65 /// Converts a diagnostic in the document to [`lsp::Diagnostic`].
65 ///
66 ///
@@ -282,6 +283,84 @@ pub mod util {
282 })
283 })
283 }
284 }
284
285
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+
285 pub fn generate_transaction_from_edits(
364 pub fn generate_transaction_from_edits(
286 doc: &Rope,
365 doc: &Rope,
287 mut edits: Vec<lsp::TextEdit>,
366 mut edits: Vec<lsp::TextEdit>,
helix-lsp/src/snippet.rs+83−104
@@ -1,9 +1,7 @@
1use std::borrow::Cow;
1use std::borrow::Cow;
2
2
3use anyhow::{anyhow, Result};
3use anyhow::{anyhow, Result};
4−use helix_core::SmallVec;
4+use helix_core::{SmallVec, smallvec};
5−
6−use crate::{util::lsp_pos_to_pos, OffsetEncoding};
7
5
8#[derive(Debug, PartialEq, Eq)]
6#[derive(Debug, PartialEq, Eq)]
9pub enum CaseChange {
7pub enum CaseChange {
@@ -34,7 +32,7 @@ pub enum SnippetElement<'a> {
34 },
32 },
35 Placeholder {
33 Placeholder {
36 tabstop: usize,
34 tabstop: usize,
37− value: Box<SnippetElement<'a>>,
35+ value: Vec<SnippetElement<'a>>,
38 },
36 },
39 Choice {
37 Choice {
40 tabstop: usize,
38 tabstop: usize,
@@ -57,141 +55,108 @@ pub fn parse(s: &str) -> Result<Snippet<'_>> {
57 parser::parse(s).map_err(|rest| anyhow!("Failed to parse snippet. Remaining input: {}", rest))
55 parser::parse(s).map_err(|rest| anyhow!("Failed to parse snippet. Remaining input: {}", rest))
58}
56}
59
57
60−pub fn into_transaction<'a>(
58+fn render_elements(
61− snippet: Snippet<'a>,
59+ snippet_elements: &[SnippetElement<'_>],
62− doc: &helix_core::Rope,
60+ insert: &mut String,
63− selection: &helix_core::Selection,
61+ offset: &mut usize,
64− edit: &lsp_types::TextEdit,
62+ tabstops: &mut Vec<(usize, (usize, usize))>,
65− line_ending: &str,
63+ newline_with_offset: &String,
66− offset_encoding: OffsetEncoding,
67 include_placeholer: bool,
64 include_placeholer: bool,
68−) -> helix_core::Transaction {
65+) {
69− use helix_core::{smallvec, Range, Transaction};
70 use SnippetElement::*;
66 use SnippetElement::*;
71
67
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 {
95 match element {
69 match element {
96− Text(text) => {
70+ &Text(text) => {
97 // small optimization to avoid calling replace when it's unnecessary
71 // small optimization to avoid calling replace when it's unnecessary
98 let text = if text.contains('\n') {
72 let text = if text.contains('\n') {
99− Cow::Owned(text.replace('\n', &newline_with_offset))
73+ Cow::Owned(text.replace('\n', newline_with_offset))
100 } else {
74 } else {
101 Cow::Borrowed(text)
75 Cow::Borrowed(text)
102 };
76 };
103− offset += text.chars().count();
77+ *offset += text.chars().count();
104 insert.push_str(&text);
78 insert.push_str(&text);
105 }
79 }
106− Variable {
80+ &Variable {
107− name: _name,
81+ name: _,
108− regex: None,
82+ regex: _,
109 r#default,
83 r#default,
110 } => {
84 } => {
111 // TODO: variables. For now, fall back to the default, which defaults to "".
85 // TODO: variables. For now, fall back to the default, which defaults to "".
112 let text = r#default.unwrap_or_default();
86 let text = r#default.unwrap_or_default();
113− offset += text.chars().count();
87+ *offset += text.chars().count();
114 insert.push_str(text);
88 insert.push_str(text);
115 }
89 }
116− Tabstop { tabstop } => {
90+ &Tabstop { tabstop } => {
117− tabstops.push((tabstop, offset, offset));
91+ tabstops.push((tabstop, (*offset, *offset)));
118 }
92 }
119− Placeholder { tabstop, value } => match value.as_ref() {
93+ Placeholder {
120− // https://doc.rust-lang.org/beta/unstable-book/language-features/box-patterns.html
94+ tabstop,
121− // would make this a bit nicer
95+ value: inner_snippet_elements,
122− Text(text) => {
96+ } => {
123− if include_placeholer {
97+ let start_offset = *offset;
124− let len_chars = text.chars().count();
98+ if include_placeholer {
125− tabstops.push((tabstop, offset, offset + len_chars + 1));
99+ render_elements(
126− offset += len_chars;
100+ inner_snippet_elements,
127− insert.push_str(text);
101+ insert,
128− } else {
102+ offset,
129− tabstops.push((tabstop, offset, offset));
103+ tabstops,
130− }
104+ newline_with_offset,
131− }
105+ include_placeholer,
132− other => {
133− log::error!(
134− "Discarding snippet: generating a transaction for placeholder contents {:?} is unimplemented.",
135− other
136 );
106 );
137− return Transaction::new(doc);
138 }
107 }
108+ tabstops.push((*tabstop, (start_offset, *offset)));
139− },
109+ }
110+ &Choice {
111+ tabstop,
112+ choices: _,
140− other => {
113+ } => {
141− log::error!(
114+ // TODO: choices
142− "Discarding snippet: generating a transaction for {:?} is unimplemented.",
115+ tabstops.push((tabstop, (*offset, *offset)));
143− other
144− );
145− return Transaction::new(doc);
146 }
116 }
147 }
117 }
148 }
118 }
119+}
149
120
150− let transaction = Transaction::change_by_selection(doc, selection, |range| {
121+#[allow(clippy::type_complexity)] // only used one time
151− let cursor = range.cursor(text);
122+pub fn render(
152− (
123+ snippet: &Snippet<'_>,
153− (cursor as i128 + start_offset) as usize,
124+ newline_with_offset: String,
154− (cursor as i128 + end_offset) as usize,
125+ include_placeholer: bool,
155− Some(insert.clone().into()),
126+) -> (String, Vec<SmallVec<[(usize, usize); 1]>>) {
156− )
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,
157− });
138+ );
158
139
159 // sort in ascending order (except for 0, which should always be the last one (per lsp doc))
140 // 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 });
161
142
162 // merge tabstops with the same index (we take advantage of the fact that we just sorted them
143 // merge tabstops with the same index (we take advantage of the fact that we just sorted them
163 // above to simply look backwards)
144 // above to simply look backwards)
164 let mut ntabstops = Vec::<SmallVec<[(usize, usize); 1]>>::new();
145 let mut ntabstops = Vec::<SmallVec<[(usize, usize); 1]>>::new();
165 {
146 {
166 let mut prev = None;
147 let mut prev = None;
167− for (tabstop, o1, o2) in tabstops {
148+ for (tabstop, r) in tabstops {
168 if prev == Some(tabstop) {
149 if prev == Some(tabstop) {
169 let len_1 = ntabstops.len() - 1;
150 let len_1 = ntabstops.len() - 1;
170− ntabstops[len_1].push((o1, o2));
151+ ntabstops[len_1].push(r);
171 } else {
152 } else {
172 prev = Some(tabstop);
153 prev = Some(tabstop);
173− ntabstops.push(smallvec![(o1, o2)]);
154+ ntabstops.push(smallvec![r]);
174 }
155 }
175 }
156 }
176 }
157 }
177
158
178− if let Some(first) = ntabstops.first() {
159+ (insert, ntabstops)
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− }
195}
160}
196
161
197mod parser {
162mod parser {
@@ -343,14 +308,15 @@ mod parser {
343 fn placeholder<'a>() -> impl Parser<'a, Output = SnippetElement<'a>> {
308 fn placeholder<'a>() -> impl Parser<'a, Output = SnippetElement<'a>> {
344 // TODO: why doesn't parse_as work?
309 // TODO: why doesn't parse_as work?
345 // let value = reparse_as(take_until(|c| c == '}'), anything());
310 // let value = reparse_as(take_until(|c| c == '}'), anything());
311+ // TODO: fix this to parse nested placeholders (take until terminates too early)
346 let value = filter_map(take_until(|c| c == '}'), |s| {
312 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()
348 });
314 });
349
315
350 map(seq!("${", digit(), ":", value, "}"), |seq| {
316 map(seq!("${", digit(), ":", value, "}"), |seq| {
351 SnippetElement::Placeholder {
317 SnippetElement::Placeholder {
352 tabstop: seq.1,
318 tabstop: seq.1,
353− value: Box::new(seq.3),
319+ value: seq.3.elements,
354 }
320 }
355 })
321 })
356 }
322 }
@@ -430,7 +396,7 @@ mod parser {
430 Text("match("),
396 Text("match("),
431 Placeholder {
397 Placeholder {
432 tabstop: 1,
398 tabstop: 1,
433− value: Box::new(Text("Arg1")),
399+ value: vec!(Text("Arg1")),
434 },
400 },
435 Text(")")
401 Text(")")
436 ]
402 ]
@@ -447,12 +413,12 @@ mod parser {
447 Text("local "),
413 Text("local "),
448 Placeholder {
414 Placeholder {
449 tabstop: 1,
415 tabstop: 1,
450− value: Box::new(Text("var")),
416+ value: vec!(Text("var")),
451 },
417 },
452 Text(" = "),
418 Text(" = "),
453 Placeholder {
419 Placeholder {
454 tabstop: 1,
420 tabstop: 1,
455− value: Box::new(Text("value")),
421+ value: vec!(Text("value")),
456 },
422 },
457 ]
423 ]
458 }),
424 }),
@@ -460,6 +426,19 @@ mod parser {
460 )
426 )
461 }
427 }
462
428
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+
463 #[test]
442 #[test]
464 fn parse_all() {
443 fn parse_all() {
465 assert_eq!(
444 assert_eq!(
helix-term/src/ui/completion.rs+4−4
@@ -138,14 +138,14 @@ impl Completion {
138 )
138 )
139 {
139 {
140 match snippet::parse(&edit.new_text) {
140 match snippet::parse(&edit.new_text) {
141− Ok(snippet) => snippet::into_transaction(
141+ Ok(snippet) => util::generate_transaction_from_snippet(
142− snippet,
143 doc.text(),
142 doc.text(),
144 doc.selection(view_id),
143 doc.selection(view_id),
145− &edit,
144+ &edit.range,
145+ snippet,
146 doc.line_ending.as_str(),
146 doc.line_ending.as_str(),
147− offset_encoding,
148 include_placeholder,
147 include_placeholder,
148+ offset_encoding,
149 ),
149 ),
150 Err(err) => {
150 Err(err) => {
151 log::error!(
151 log::error!(