diff --git a/helix-lsp/src/snippet.rs b/helix-lsp/src/snippet.rs index de165776b..ec56d4e2c 100644 --- a/helix-lsp/src/snippet.rs +++ b/helix-lsp/src/snippet.rs @@ -1,10 +1,5 @@ use anyhow::{anyhow, Result}; -#[derive(Debug, PartialEq, Eq)] -pub struct Snippet<'a> { - parts: Vec>, -} - #[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> { - parser::parse(input) - .map_err(|rest| anyhow!("Failed to parse snippet. Remaining input: {}", rest)) +#[derive(Debug, PartialEq, Eq)] +pub struct Snippet<'a> { + elements: Vec>, +} + +pub fn parse<'a>(s: &'a str) -> Result> { + 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 = 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 { @@ -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 { diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index de7c3232b..146060cb8 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -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 {