mirror of https://github.com/helix-editor/helix
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.pull/6224/head
parent
ec6e575a40
commit
1866b43cd3
|
@ -60,6 +60,7 @@ pub mod util {
|
||||||
use super::*;
|
use super::*;
|
||||||
use helix_core::line_ending::{line_end_byte_index, line_end_char_index};
|
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::{diagnostic::NumberOrString, Range, Rope, Selection, Tendril, Transaction};
|
||||||
|
use helix_core::{smallvec, SmallVec};
|
||||||
|
|
||||||
/// Converts a diagnostic in the document to [`lsp::Diagnostic`].
|
/// 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(
|
pub fn generate_transaction_from_edits(
|
||||||
doc: &Rope,
|
doc: &Rope,
|
||||||
mut edits: Vec<lsp::TextEdit>,
|
mut edits: Vec<lsp::TextEdit>,
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use helix_core::SmallVec;
|
use helix_core::{SmallVec, smallvec};
|
||||||
|
|
||||||
use crate::{util::lsp_pos_to_pos, OffsetEncoding};
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub enum CaseChange {
|
pub enum CaseChange {
|
||||||
|
@ -34,7 +32,7 @@ pub enum SnippetElement<'a> {
|
||||||
},
|
},
|
||||||
Placeholder {
|
Placeholder {
|
||||||
tabstop: usize,
|
tabstop: usize,
|
||||||
value: Box<SnippetElement<'a>>,
|
value: Vec<SnippetElement<'a>>,
|
||||||
},
|
},
|
||||||
Choice {
|
Choice {
|
||||||
tabstop: usize,
|
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))
|
parser::parse(s).map_err(|rest| anyhow!("Failed to parse snippet. Remaining input: {}", rest))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn into_transaction<'a>(
|
fn render_elements(
|
||||||
snippet: Snippet<'a>,
|
snippet_elements: &[SnippetElement<'_>],
|
||||||
doc: &helix_core::Rope,
|
insert: &mut String,
|
||||||
selection: &helix_core::Selection,
|
offset: &mut usize,
|
||||||
edit: &lsp_types::TextEdit,
|
tabstops: &mut Vec<(usize, (usize, usize))>,
|
||||||
line_ending: &str,
|
newline_with_offset: &String,
|
||||||
offset_encoding: OffsetEncoding,
|
|
||||||
include_placeholer: bool,
|
include_placeholer: bool,
|
||||||
) -> helix_core::Transaction {
|
) {
|
||||||
use helix_core::{smallvec, Range, Transaction};
|
|
||||||
use SnippetElement::*;
|
use SnippetElement::*;
|
||||||
|
|
||||||
let text = doc.slice(..);
|
for element in snippet_elements {
|
||||||
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 {
|
|
||||||
match element {
|
match element {
|
||||||
Text(text) => {
|
&Text(text) => {
|
||||||
// small optimization to avoid calling replace when it's unnecessary
|
// small optimization to avoid calling replace when it's unnecessary
|
||||||
let text = if text.contains('\n') {
|
let text = if text.contains('\n') {
|
||||||
Cow::Owned(text.replace('\n', &newline_with_offset))
|
Cow::Owned(text.replace('\n', newline_with_offset))
|
||||||
} else {
|
} else {
|
||||||
Cow::Borrowed(text)
|
Cow::Borrowed(text)
|
||||||
};
|
};
|
||||||
offset += text.chars().count();
|
*offset += text.chars().count();
|
||||||
insert.push_str(&text);
|
insert.push_str(&text);
|
||||||
}
|
}
|
||||||
Variable {
|
&Variable {
|
||||||
name: _name,
|
name: _,
|
||||||
regex: None,
|
regex: _,
|
||||||
r#default,
|
r#default,
|
||||||
} => {
|
} => {
|
||||||
// TODO: variables. For now, fall back to the default, which defaults to "".
|
// TODO: variables. For now, fall back to the default, which defaults to "".
|
||||||
let text = r#default.unwrap_or_default();
|
let text = r#default.unwrap_or_default();
|
||||||
offset += text.chars().count();
|
*offset += text.chars().count();
|
||||||
insert.push_str(text);
|
insert.push_str(text);
|
||||||
}
|
}
|
||||||
Tabstop { tabstop } => {
|
&Tabstop { tabstop } => {
|
||||||
tabstops.push((tabstop, offset, offset));
|
tabstops.push((tabstop, (*offset, *offset)));
|
||||||
}
|
}
|
||||||
Placeholder { tabstop, value } => match value.as_ref() {
|
Placeholder {
|
||||||
// https://doc.rust-lang.org/beta/unstable-book/language-features/box-patterns.html
|
tabstop,
|
||||||
// would make this a bit nicer
|
value: inner_snippet_elements,
|
||||||
Text(text) => {
|
} => {
|
||||||
if include_placeholer {
|
let start_offset = *offset;
|
||||||
let len_chars = text.chars().count();
|
if include_placeholer {
|
||||||
tabstops.push((tabstop, offset, offset + len_chars + 1));
|
render_elements(
|
||||||
offset += len_chars;
|
inner_snippet_elements,
|
||||||
insert.push_str(text);
|
insert,
|
||||||
} else {
|
offset,
|
||||||
tabstops.push((tabstop, offset, offset));
|
tabstops,
|
||||||
}
|
newline_with_offset,
|
||||||
}
|
include_placeholer,
|
||||||
other => {
|
|
||||||
log::error!(
|
|
||||||
"Discarding snippet: generating a transaction for placeholder contents {:?} is unimplemented.",
|
|
||||||
other
|
|
||||||
);
|
);
|
||||||
return Transaction::new(doc);
|
|
||||||
}
|
}
|
||||||
},
|
tabstops.push((*tabstop, (start_offset, *offset)));
|
||||||
other => {
|
}
|
||||||
log::error!(
|
&Choice {
|
||||||
"Discarding snippet: generating a transaction for {:?} is unimplemented.",
|
tabstop,
|
||||||
other
|
choices: _,
|
||||||
);
|
} => {
|
||||||
return Transaction::new(doc);
|
// TODO: choices
|
||||||
|
tabstops.push((tabstop, (*offset, *offset)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let transaction = Transaction::change_by_selection(doc, selection, |range| {
|
#[allow(clippy::type_complexity)] // only used one time
|
||||||
let cursor = range.cursor(text);
|
pub fn render(
|
||||||
(
|
snippet: &Snippet<'_>,
|
||||||
(cursor as i128 + start_offset) as usize,
|
newline_with_offset: String,
|
||||||
(cursor as i128 + end_offset) as usize,
|
include_placeholer: bool,
|
||||||
Some(insert.clone().into()),
|
) -> (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))
|
// 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
|
// merge tabstops with the same index (we take advantage of the fact that we just sorted them
|
||||||
// above to simply look backwards)
|
// above to simply look backwards)
|
||||||
let mut ntabstops = Vec::<SmallVec<[(usize, usize); 1]>>::new();
|
let mut ntabstops = Vec::<SmallVec<[(usize, usize); 1]>>::new();
|
||||||
{
|
{
|
||||||
let mut prev = None;
|
let mut prev = None;
|
||||||
for (tabstop, o1, o2) in tabstops {
|
for (tabstop, r) in tabstops {
|
||||||
if prev == Some(tabstop) {
|
if prev == Some(tabstop) {
|
||||||
let len_1 = ntabstops.len() - 1;
|
let len_1 = ntabstops.len() - 1;
|
||||||
ntabstops[len_1].push((o1, o2));
|
ntabstops[len_1].push(r);
|
||||||
} else {
|
} else {
|
||||||
prev = Some(tabstop);
|
prev = Some(tabstop);
|
||||||
ntabstops.push(smallvec![(o1, o2)]);
|
ntabstops.push(smallvec![r]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(first) = ntabstops.first() {
|
(insert, ntabstops)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mod parser {
|
mod parser {
|
||||||
|
@ -343,14 +308,15 @@ mod parser {
|
||||||
fn placeholder<'a>() -> impl Parser<'a, Output = SnippetElement<'a>> {
|
fn placeholder<'a>() -> impl Parser<'a, Output = SnippetElement<'a>> {
|
||||||
// TODO: why doesn't parse_as work?
|
// TODO: why doesn't parse_as work?
|
||||||
// let value = reparse_as(take_until(|c| c == '}'), anything());
|
// 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| {
|
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| {
|
map(seq!("${", digit(), ":", value, "}"), |seq| {
|
||||||
SnippetElement::Placeholder {
|
SnippetElement::Placeholder {
|
||||||
tabstop: seq.1,
|
tabstop: seq.1,
|
||||||
value: Box::new(seq.3),
|
value: seq.3.elements,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -430,7 +396,7 @@ mod parser {
|
||||||
Text("match("),
|
Text("match("),
|
||||||
Placeholder {
|
Placeholder {
|
||||||
tabstop: 1,
|
tabstop: 1,
|
||||||
value: Box::new(Text("Arg1")),
|
value: vec!(Text("Arg1")),
|
||||||
},
|
},
|
||||||
Text(")")
|
Text(")")
|
||||||
]
|
]
|
||||||
|
@ -447,12 +413,12 @@ mod parser {
|
||||||
Text("local "),
|
Text("local "),
|
||||||
Placeholder {
|
Placeholder {
|
||||||
tabstop: 1,
|
tabstop: 1,
|
||||||
value: Box::new(Text("var")),
|
value: vec!(Text("var")),
|
||||||
},
|
},
|
||||||
Text(" = "),
|
Text(" = "),
|
||||||
Placeholder {
|
Placeholder {
|
||||||
tabstop: 1,
|
tabstop: 1,
|
||||||
value: Box::new(Text("value")),
|
value: vec!(Text("value")),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
|
@ -460,6 +426,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]
|
#[test]
|
||||||
fn parse_all() {
|
fn parse_all() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
|
@ -138,14 +138,14 @@ impl Completion {
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
match snippet::parse(&edit.new_text) {
|
match snippet::parse(&edit.new_text) {
|
||||||
Ok(snippet) => snippet::into_transaction(
|
Ok(snippet) => util::generate_transaction_from_snippet(
|
||||||
snippet,
|
|
||||||
doc.text(),
|
doc.text(),
|
||||||
doc.selection(view_id),
|
doc.selection(view_id),
|
||||||
&edit,
|
&edit.range,
|
||||||
|
snippet,
|
||||||
doc.line_ending.as_str(),
|
doc.line_ending.as_str(),
|
||||||
offset_encoding,
|
|
||||||
include_placeholder,
|
include_placeholder,
|
||||||
|
offset_encoding,
|
||||||
),
|
),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::error!(
|
log::error!(
|
||||||
|
|
Loading…
Reference in New Issue