WIP: apply snippets as transactions

This commit is contained in:
Michael Davis 2022-10-22 09:52:25 -05:00
parent 5c4b93b4b4
commit 79ac545873
No known key found for this signature in database
2 changed files with 135 additions and 38 deletions

View File

@ -1,10 +1,5 @@
use anyhow::{anyhow, Result};
#[derive(Debug, PartialEq, Eq)]
pub struct Snippet<'a> {
parts: Vec<SnippetElement<'a>>,
}
#[derive(Debug, PartialEq, Eq)]
pub enum CaseChange {
Upcase,
@ -48,9 +43,88 @@ pub enum SnippetElement<'a> {
Text(&'a str),
}
pub fn parse<'a>(input: &'a str) -> Result<Snippet<'a>> {
parser::parse(input)
.map_err(|rest| anyhow!("Failed to parse snippet. Remaining input: {}", rest))
#[derive(Debug, PartialEq, Eq)]
pub struct Snippet<'a> {
elements: Vec<SnippetElement<'a>>,
}
pub fn parse<'a>(s: &'a str) -> Result<Snippet<'a>> {
parser::parse(s).map_err(|rest| anyhow!("Failed to parse snippet. Remaining input: {}", rest))
}
pub fn into_transaction<'a>(
snippet: Snippet<'a>,
text: &helix_core::Rope,
trigger_offset: usize,
) -> helix_core::Transaction {
use helix_core::{smallvec, Range, Selection, Transaction};
use SnippetElement::*;
let mut insert = String::new();
let mut offset = trigger_offset;
let mut tabstops: Vec<Range> = Vec::new();
for element in snippet.elements {
match element {
Text(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));
offset += len_chars;
insert.push_str(text);
}
other => {
log::error!(
"Discarding snippet: generating a transaction for placeholder contents {:?} is unimplemented.",
other
);
return Transaction::new(text);
}
},
other => {
log::error!(
"Discarding snippet: generating a transaction for {:?} is unimplemented.",
other
);
return Transaction::new(text);
}
}
}
let transaction = Transaction::change(
text,
std::iter::once((trigger_offset, trigger_offset, Some(insert.into()))),
);
if let Some(first) = tabstops.first() {
transaction.with_selection(Selection::new(smallvec![*first], 0))
} else {
transaction
}
}
mod parser {
@ -253,7 +327,7 @@ fn anything<'a>() -> impl Parser<'a, Output = SnippetElement<'a>> {
}
fn snippet<'a>() -> impl Parser<'a, Output = Snippet<'a>> {
map(one_or_more(anything()), |parts| Snippet { parts })
map(one_or_more(anything()), |parts| Snippet { elements: parts })
}
pub fn parse(s: &str) -> Result<Snippet, &str> {
@ -274,7 +348,7 @@ fn empty_string_is_error() {
fn parse_placeholders_in_function_call() {
assert_eq!(
Ok(Snippet {
parts: vec![
elements: vec![
Text("match("),
Placeholder {
tabstop: 1,
@ -291,7 +365,7 @@ fn parse_placeholders_in_function_call() {
fn parse_placeholders_in_statement() {
assert_eq!(
Ok(Snippet {
parts: vec![
elements: vec![
Text("local "),
Placeholder {
tabstop: 1,
@ -312,7 +386,7 @@ fn parse_placeholders_in_statement() {
fn parse_all() {
assert_eq!(
Ok(Snippet {
parts: vec![
elements: vec![
Text("hello "),
Tabstop { tabstop: 1 },
Tabstop { tabstop: 2 },
@ -349,7 +423,7 @@ fn parse_all() {
fn regex_capture_replace() {
assert_eq!(
Ok(Snippet {
parts: vec![Variable {
elements: vec![Variable {
name: "TM_FILENAME",
default: None,
regex: Some(Regex {

View File

@ -109,32 +109,55 @@ fn item_to_transaction(
start_offset: usize,
trigger_offset: usize,
) -> Transaction {
let transaction = if let Some(edit) = &item.text_edit {
let edit = match edit {
lsp::CompletionTextEdit::Edit(edit) => edit.clone(),
lsp::CompletionTextEdit::InsertAndReplace(item) => {
unimplemented!("completion: insert_and_replace {:?}", item)
use helix_lsp::snippet;
match item {
CompletionItem {
text_edit: Some(edit),
..
} => {
let edit = match edit {
lsp::CompletionTextEdit::Edit(edit) => edit.clone(),
lsp::CompletionTextEdit::InsertAndReplace(item) => {
unimplemented!("completion: insert_and_replace {:?}", item)
}
};
util::generate_transaction_from_edits(
doc.text(),
vec![edit],
offset_encoding, // TODO: should probably transcode in Client
)
}
CompletionItem {
insert_text: Some(insert_text),
insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
..
} => match snippet::parse(insert_text) {
Ok(snippet) => {
snippet::into_transaction(snippet, doc.text(), trigger_offset)
}
};
util::generate_transaction_from_edits(
doc.text(),
vec![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 ¯\_(ツ)_/¯
// in these cases we need to check for a common prefix and remove it
let prefix = Cow::from(doc.text().slice(start_offset..trigger_offset));
let text = text.trim_start_matches::<&str>(&prefix);
Transaction::change(
doc.text(),
vec![(trigger_offset, trigger_offset, Some(text.into()))].into_iter(),
)
};
transaction
Err(err) => {
log::error!(
"Failed to parse snippet: {:?}, remaining output: {}",
insert_text,
err
);
Transaction::new(doc.text())
}
},
_ => {
let text = item.insert_text.as_ref().unwrap_or(&item.label);
// Some LSPs just give you an insertText with no offset ¯\_(ツ)_/¯
// in these cases we need to check for a common prefix and remove it
let prefix = Cow::from(doc.text().slice(start_offset..trigger_offset));
let text = text.trim_start_matches::<&str>(&prefix);
Transaction::change(
doc.text(),
vec![(trigger_offset, trigger_offset, Some(text.into()))].into_iter(),
)
}
}
}
fn completion_changes(transaction: &Transaction, trigger_offset: usize) -> Vec<Change> {