Skip to content

Commit 2077f5e

Browse files
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.
1 parent 7210c58 commit 2077f5e

File tree

2 files changed

+61
-8
lines changed

2 files changed

+61
-8
lines changed

helix-lsp/src/lib.rs

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ pub enum OffsetEncoding {
5757

5858
pub 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

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::compositor::{Component, Context, Event, EventResult};
2-
use helix_view::{apply_transaction, editor::CompleteAction};
2+
use helix_view::{apply_transaction, editor::CompleteAction, ViewId};
33
use tui::buffer::Buffer as Surface;
44
use 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,

0 commit comments

Comments
 (0)