on master help sign in
Apply completion edits to all cursors (#4496)

Completion edits - either basic `insert_text` strings or structured `text_edit`s - are assumed by the LSP spec to apply to the current cursor (or at least the trigger point). We can use the range (if any) and text given by the Language Server to create a transaction that changes all ranges in the current selection though, allowing auto- complete to affect multiple cursors.

commit2077f5e26a4cdaadde4b505fc64eadb9e6849c0d authorMichael Davis <mcarsondavis@gmail.com> dateMon 5 Dec 2022, 19:29 (4 years ago) parent7210c58
2 files changed+61−8
helix-lsp/src/lib.rs+37−1 helix-term/src/ui/completion.rs+24−7
helix-lsp/src/lib.rs+37−1
@@ -57,7 +57,7 @@ pub enum OffsetEncoding {
5757
5858pub mod util {
5959 use super::*;
60− use helix_core::{diagnostic::NumberOrString, Range, Rope, Transaction};
60+ use helix_core::{diagnostic::NumberOrString, Range, Rope, Selection, Tendril, Transaction};
6161
6262 /// Converts a diagnostic in the document to [`lsp::Diagnostic`].
6363 ///
@@ -196,6 +196,42 @@ pub mod util {
196196 Some(Range::new(start, end))
197197 }
198198
199+ /// Creates a [Transaction] from the [lsp::TextEdit] in a completion response.
200+ /// The transaction applies the edit to all cursors.
201+ pub fn generate_transaction_from_completion_edit(
202+ doc: &Rope,
203+ selection: &Selection,
204+ edit: lsp::TextEdit,
205+ offset_encoding: OffsetEncoding,
206+ ) -> Transaction {
207+ let replacement: Option<Tendril> = if edit.new_text.is_empty() {
208+ None
209+ } else {
210+ Some(edit.new_text.into())
211+ };
212+
213+ let text = doc.slice(..);
214+ let primary_cursor = selection.primary().cursor(text);
215+
216+ let start_offset = match lsp_pos_to_pos(doc, edit.range.start, offset_encoding) {
217+ Some(start) => start as i128 - primary_cursor as i128,
218+ None => return Transaction::new(doc),
219+ };
220+ let end_offset = match lsp_pos_to_pos(doc, edit.range.end, offset_encoding) {
221+ Some(end) => end as i128 - primary_cursor as i128,
222+ None => return Transaction::new(doc),
223+ };
224+
225+ Transaction::change_by_selection(doc, selection, |range| {
226+ let cursor = range.cursor(text);
227+ (
228+ (cursor as i128 + start_offset) as usize,
229+ (cursor as i128 + end_offset) as usize,
230+ replacement.clone(),
231+ )
232+ })
233+ }
234+
199235 pub fn generate_transaction_from_edits(
200236 doc: &Rope,
201237 mut edits: Vec<lsp::TextEdit>,
helix-term/src/ui/completion.rs+24−7
@@ -1,5 +1,5 @@
11use crate::compositor::{Component, Context, Event, EventResult};
2−use helix_view::{apply_transaction, editor::CompleteAction};
2+use helix_view::{apply_transaction, editor::CompleteAction, ViewId};
33use tui::buffer::Buffer as Surface;
44use tui::text::Spans;
55
@@ -107,6 +107,7 @@ impl Completion {
107107 let menu = Menu::new(items, (), move |editor: &mut Editor, item, event| {
108108 fn item_to_transaction(
109109 doc: &Document,
110+ view_id: ViewId,
110111 item: &CompletionItem,
111112 offset_encoding: helix_lsp::OffsetEncoding,
112113 start_offset: usize,
@@ -121,9 +122,10 @@ impl Completion {
121122 }
122123 };
123124
124− util::generate_transaction_from_edits(
125+ util::generate_transaction_from_completion_edit(
125126 doc.text(),
126− vec![edit],
127+ doc.selection(view_id),
128+ edit,
127129 offset_encoding, // TODO: should probably transcode in Client
128130 )
129131 } else {
@@ -132,10 +134,23 @@ impl Completion {
132134 // in these cases we need to check for a common prefix and remove it
133135 let prefix = Cow::from(doc.text().slice(start_offset..trigger_offset));
134136 let text = text.trim_start_matches::<&str>(&prefix);
135− Transaction::change(
136− doc.text(),
137− vec![(trigger_offset, trigger_offset, Some(text.into()))].into_iter(),
138− )
137+
138+ // TODO: this needs to be true for the numbers to work out correctly
139+ // in the closure below. It's passed in to a callback as this same
140+ // formula, but can the value change between the LSP request and
141+ // response? If it does, can we recover?
142+ debug_assert!(
143+ doc.selection(view_id)
144+ .primary()
145+ .cursor(doc.text().slice(..))
146+ == trigger_offset
147+ );
148+
149+ Transaction::change_by_selection(doc.text(), doc.selection(view_id), |range| {
150+ let cursor = range.cursor(doc.text().slice(..));
151+
152+ (cursor, cursor, Some(text.into()))
153+ })
139154 };
140155
141156 transaction
@@ -164,6 +179,7 @@ impl Completion {
164179
165180 let transaction = item_to_transaction(
166181 doc,
182+ view.id,
167183 item,
168184 offset_encoding,
169185 start_offset,
@@ -185,6 +201,7 @@ impl Completion {
185201
186202 let transaction = item_to_transaction(
187203 doc,
204+ view.id,
188205 item,
189206 offset_encoding,
190207 start_offset,