on master help sign in
Apply snippets as transactions
commitb9b1ec22084cfe82ced7e34412dab0351828fc53 authorMichael Davis <mcarsondavis@gmail.com> dateSat 22 Oct 2022, 09:52 (4 years ago) parent3f90daf
3 files changed+145−10
Cargo.lock+1 helix-lsp/src/snippet.rs+108 helix-term/src/ui/completion.rs+36−10
Cargo.lock+1
@@ -1139,6 +1139,7 @@ dependencies = [
11391139 "futures-util",
11401140 "helix-core",
11411141 "helix-loader",
1142+ "helix-parsec",
11421143 "log",
11431144 "lsp-types",
11441145 "serde",
helix-lsp/src/snippet.rs+108
@@ -1,3 +1,5 @@
1+use std::borrow::Cow;
2+
13use anyhow::{anyhow, Result};
24
35use crate::{util::lsp_pos_to_pos, OffsetEncoding};
@@ -54,6 +56,112 @@ pub fn parse(s: &str) -> Result<Snippet<'_>> {
5456 parser::parse(s).map_err(|rest| anyhow!("Failed to parse snippet. Remaining input: {}", rest))
5557}
5658
59+pub fn into_transaction<'a>(
60+ snippet: Snippet<'a>,
61+ doc: &helix_core::Rope,
62+ selection: &helix_core::Selection,
63+ edit: &lsp_types::TextEdit,
64+ line_ending: &str,
65+ offset_encoding: OffsetEncoding,
66+) -> helix_core::Transaction {
67+ use helix_core::{smallvec, Range, Selection, Transaction};
68+ use SnippetElement::*;
69+
70+ let text = doc.slice(..);
71+ let primary_cursor = selection.primary().cursor(text);
72+
73+ let start_offset = match lsp_pos_to_pos(doc, edit.range.start, offset_encoding) {
74+ Some(start) => start as i128 - primary_cursor as i128,
75+ None => return Transaction::new(doc),
76+ };
77+ let end_offset = match lsp_pos_to_pos(doc, edit.range.end, offset_encoding) {
78+ Some(end) => end as i128 - primary_cursor as i128,
79+ None => return Transaction::new(doc),
80+ };
81+
82+ let newline_with_offset = format!(
83+ "{line_ending}{blank:width$}",
84+ width = edit.range.start.character as usize,
85+ blank = ""
86+ );
87+
88+ let mut insert = String::new();
89+ let mut offset = (primary_cursor as i128 + start_offset) as usize;
90+ let mut tabstops: Vec<Range> = Vec::new();
91+
92+ for element in snippet.elements {
93+ match element {
94+ Text(text) => {
95+ // small optimization to avoid calling replace when it's unnecessary
96+ let text = if text.contains('\n') {
97+ Cow::Owned(text.replace('\n', &newline_with_offset))
98+ } else {
99+ Cow::Borrowed(text)
100+ };
101+ offset += text.chars().count();
102+ insert.push_str(&text);
103+ }
104+ Variable {
105+ name: _name,
106+ regex: None,
107+ r#default,
108+ } => {
109+ // TODO: variables. For now, fall back to the default, which defaults to "".
110+ let text = r#default.unwrap_or_default();
111+ offset += text.chars().count();
112+ insert.push_str(text);
113+ }
114+ Tabstop { .. } => {
115+ // TODO: tabstop indexing: 0 is final cursor position. 1,2,.. are positions.
116+ // TODO: merge tabstops with the same index
117+ tabstops.push(Range::point(offset));
118+ }
119+ Placeholder {
120+ tabstop: _tabstop,
121+ value,
122+ } => match value.as_ref() {
123+ // https://doc.rust-lang.org/beta/unstable-book/language-features/box-patterns.html
124+ // would make this a bit nicer
125+ Text(text) => {
126+ let len_chars = text.chars().count();
127+ tabstops.push(Range::new(offset, offset + len_chars + 1));
128+ offset += len_chars;
129+ insert.push_str(text);
130+ }
131+ other => {
132+ log::error!(
133+ "Discarding snippet: generating a transaction for placeholder contents {:?} is unimplemented.",
134+ other
135+ );
136+ return Transaction::new(doc);
137+ }
138+ },
139+ other => {
140+ log::error!(
141+ "Discarding snippet: generating a transaction for {:?} is unimplemented.",
142+ other
143+ );
144+ return Transaction::new(doc);
145+ }
146+ }
147+ }
148+
149+ let transaction = Transaction::change_by_selection(doc, selection, |range| {
150+ let cursor = range.cursor(text);
151+ (
152+ (cursor as i128 + start_offset) as usize,
153+ (cursor as i128 + end_offset) as usize,
154+ Some(insert.clone().into()),
155+ )
156+ });
157+
158+ if let Some(first) = tabstops.first() {
159+ transaction.with_selection(Selection::new(smallvec![*first], 0))
160+ } else {
161+ transaction
162+ }
163+}
164+
57165mod parser {
58166 use helix_parsec::*;
59167
helix-term/src/ui/completion.rs+36−10
@@ -119,7 +119,9 @@ impl Completion {
119119 start_offset: usize,
120120 trigger_offset: usize,
121121 ) -> Transaction {
122− let transaction = if let Some(edit) = &item.text_edit {
122+ use helix_lsp::snippet;
123+
124+ if let Some(edit) = &item.text_edit {
123125 let edit = match edit {
124126 lsp::CompletionTextEdit::Edit(edit) => edit.clone(),
125127 lsp::CompletionTextEdit::InsertAndReplace(item) => {
@@ -128,12 +130,38 @@ impl Completion {
128130 }
129131 };
130132
131− util::generate_transaction_from_completion_edit(
132− doc.text(),
133− doc.selection(view_id),
134− edit,
135− offset_encoding, // TODO: should probably transcode in Client
136− )
133+ if matches!(item.kind, Some(lsp::CompletionItemKind::SNIPPET))
134+ || matches!(
135+ item.insert_text_format,
136+ Some(lsp::InsertTextFormat::SNIPPET)
137+ )
138+ {
139+ match snippet::parse(&edit.new_text) {
140+ Ok(snippet) => snippet::into_transaction(
141+ snippet,
142+ doc.text(),
143+ doc.selection(view_id),
144+ &edit,
145+ doc.line_ending.as_str(),
146+ offset_encoding,
147+ ),
148+ Err(err) => {
149+ log::error!(
150+ "Failed to parse snippet: {:?}, remaining output: {}",
151+ &edit.new_text,
152+ err
153+ );
154+ Transaction::new(doc.text())
155+ }
156+ }
157+ } else {
158+ util::generate_transaction_from_completion_edit(
159+ doc.text(),
160+ doc.selection(view_id),
161+ edit,
162+ offset_encoding, // TODO: should probably transcode in Client
163+ )
164+ }
137165 } else {
138166 let text = item.insert_text.as_ref().unwrap_or(&item.label);
139167 // Some LSPs just give you an insertText with no offset ¯\_(ツ)_/¯
@@ -157,9 +185,7 @@ impl Completion {
157185
158186 (cursor, cursor, Some(text.into()))
159187 })
160− };
161−
162− transaction
188+ }
163189 }
164190
165191 fn completion_changes(transaction: &Transaction, trigger_offset: usize) -> Vec<Change> {