Apply snippets as transactions

This commit is contained in:
Michael Davis 2022-10-22 09:52:25 -05:00 committed by Blaž Hrastnik
parent 3f90dafa3c
commit b9b1ec2208
3 changed files with 145 additions and 10 deletions

1
Cargo.lock generated
View file

@ -1139,6 +1139,7 @@ dependencies = [
"futures-util",
"helix-core",
"helix-loader",
"helix-parsec",
"log",
"lsp-types",
"serde",

View file

@ -1,3 +1,5 @@
use std::borrow::Cow;
use anyhow::{anyhow, Result};
use crate::{util::lsp_pos_to_pos, OffsetEncoding};
@ -54,6 +56,112 @@ 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,
) -> helix_core::Transaction {
use helix_core::{smallvec, Range, Selection, 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 insert = String::new();
let mut offset = (primary_cursor as i128 + start_offset) as usize;
let mut tabstops: Vec<Range> = Vec::new();
for element in snippet.elements {
match element {
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))
} else {
Cow::Borrowed(text)
};
offset += text.chars().count();
insert.push_str(&text);
}
Variable {
name: _name,
regex: None,
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();
insert.push_str(text);
}
Tabstop { .. } => {
// TODO: tabstop indexing: 0 is final cursor position. 1,2,.. are positions.
// TODO: merge tabstops with the same index
tabstops.push(Range::point(offset));
}
Placeholder {
tabstop: _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) => {
let len_chars = text.chars().count();
tabstops.push(Range::new(offset, offset + len_chars + 1));
offset += len_chars;
insert.push_str(text);
}
other => {
log::error!(
"Discarding snippet: generating a transaction for placeholder contents {:?} is unimplemented.",
other
);
return Transaction::new(doc);
}
},
other => {
log::error!(
"Discarding snippet: generating a transaction for {:?} is unimplemented.",
other
);
return Transaction::new(doc);
}
}
}
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()),
)
});
if let Some(first) = tabstops.first() {
transaction.with_selection(Selection::new(smallvec![*first], 0))
} else {
transaction
}
}
mod parser {
use helix_parsec::*;

View file

@ -119,7 +119,9 @@ impl Completion {
start_offset: usize,
trigger_offset: usize,
) -> Transaction {
let transaction = if let Some(edit) = &item.text_edit {
use helix_lsp::snippet;
if let Some(edit) = &item.text_edit {
let edit = match edit {
lsp::CompletionTextEdit::Edit(edit) => edit.clone(),
lsp::CompletionTextEdit::InsertAndReplace(item) => {
@ -128,12 +130,38 @@ impl Completion {
}
};
util::generate_transaction_from_completion_edit(
doc.text(),
doc.selection(view_id),
edit,
offset_encoding, // TODO: should probably transcode in Client
)
if matches!(item.kind, Some(lsp::CompletionItemKind::SNIPPET))
|| matches!(
item.insert_text_format,
Some(lsp::InsertTextFormat::SNIPPET)
)
{
match snippet::parse(&edit.new_text) {
Ok(snippet) => snippet::into_transaction(
snippet,
doc.text(),
doc.selection(view_id),
&edit,
doc.line_ending.as_str(),
offset_encoding,
),
Err(err) => {
log::error!(
"Failed to parse snippet: {:?}, remaining output: {}",
&edit.new_text,
err
);
Transaction::new(doc.text())
}
}
} else {
util::generate_transaction_from_completion_edit(
doc.text(),
doc.selection(view_id),
edit,
offset_encoding, // TODO: should probably transcode in Client
)
}
} else {
let text = item.insert_text.as_ref().unwrap_or(&item.label);
// Some LSPs just give you an insertText with no offset ¯\_(ツ)_/¯
@ -157,9 +185,7 @@ impl Completion {
(cursor, cursor, Some(text.into()))
})
};
transaction
}
}
fn completion_changes(transaction: &Transaction, trigger_offset: usize) -> Vec<Change> {