Apply snippets as transactions
This commit is contained in:
parent
3f90dafa3c
commit
b9b1ec2208
3 changed files with 145 additions and 10 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1139,6 +1139,7 @@ dependencies = [
|
|||
"futures-util",
|
||||
"helix-core",
|
||||
"helix-loader",
|
||||
"helix-parsec",
|
||||
"log",
|
||||
"lsp-types",
|
||||
"serde",
|
||||
|
|
|
@ -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::*;
|
||||
|
||||
|
|
|
@ -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 {
|
|||
}
|
||||
};
|
||||
|
||||
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> {
|
||||
|
|
Loading…
Add table
Reference in a new issue