summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock1
-rw-r--r--helix-lsp/src/snippet.rs108
-rw-r--r--helix-term/src/ui/completion.rs46
3 files changed, 145 insertions, 10 deletions
diff --git a/Cargo.lock b/Cargo.lock
index eec2a976..cc7265f3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1139,6 +1139,7 @@ dependencies = [
"futures-util",
"helix-core",
"helix-loader",
+ "helix-parsec",
"log",
"lsp-types",
"serde",
diff --git a/helix-lsp/src/snippet.rs b/helix-lsp/src/snippet.rs
index 27b103d5..f7423749 100644
--- a/helix-lsp/src/snippet.rs
+++ b/helix-lsp/src/snippet.rs
@@ -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::*;
diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs
index a24da20a..6897305d 100644
--- a/helix-term/src/ui/completion.rs
+++ b/helix-term/src/ui/completion.rs
@@ -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> {