@@ -59,8 +59,8 @@ pub enum OffsetEncoding {
5959pub mod util {
6060 use super::*;
6161 use helix_core::line_ending::{line_end_byte_index, line_end_char_index};
62+ use helix_core::{chars, RopeSlice, SmallVec};
6263 use helix_core::{diagnostic::NumberOrString, Range, Rope, Selection, Tendril, Transaction};
63− use helix_core::{smallvec, SmallVec};
6464
6565 /// Converts a diagnostic in the document to [`lsp::Diagnostic`].
6666 ///
@@ -247,13 +247,46 @@ pub mod util {
247247 Some(Range::new(start, end))
248248 }
249249
250+ /// If the LS did not provide a range for the completion or the range of the
251+ /// primary cursor can not be used for the secondary cursor, this function
252+ /// can be used to find the completion range for a cursor
253+ fn find_completion_range(text: RopeSlice, cursor: usize) -> (usize, usize) {
254+ let start = cursor
255+ - text
256+ .chars_at(cursor)
257+ .reversed()
258+ .take_while(|ch| chars::char_is_word(*ch))
259+ .count();
260+ (start, cursor)
261+ }
262+ fn completion_range(
263+ text: RopeSlice,
264+ edit_offset: Option<(i128, i128)>,
265+ cursor: usize,
266+ ) -> Option<(usize, usize)> {
267+ let res = match edit_offset {
268+ Some((start_offset, end_offset)) => {
269+ let start_offset = cursor as i128 + start_offset;
270+ if start_offset < 0 {
271+ return None;
272+ }
273+ let end_offset = cursor as i128 + end_offset;
274+ if end_offset > text.len_chars() as i128 {
275+ return None;
276+ }
277+ (start_offset as usize, end_offset as usize)
278+ }
279+ None => find_completion_range(text, cursor),
280+ };
281+ Some(res)
282+ }
283+
250284 /// Creates a [Transaction] from the [lsp::TextEdit] in a completion response.
251285 /// The transaction applies the edit to all cursors.
252286 pub fn generate_transaction_from_completion_edit(
253287 doc: &Rope,
254288 selection: &Selection,
255− start_offset: i128,
256− end_offset: i128,
289+ edit_offset: Option<(i128, i128)>,
257290 new_text: String,
258291 ) -> Transaction {
259292 let replacement: Option<Tendril> = if new_text.is_empty() {
@@ -263,83 +296,163 @@ pub mod util {
263296 };
264297
265298 let text = doc.slice(..);
299+ let (removed_start, removed_end) =
300+ completion_range(text, edit_offset, selection.primary().cursor(text))
301+ .expect("transaction must be valid for primary selection");
302+ let removed_text = text.slice(removed_start..removed_end);
266303
267− Transaction::change_by_selection(doc, selection, |range| {
268− let cursor = range.cursor(text);
269− (
270− (cursor as i128 + start_offset) as usize,
271− (cursor as i128 + end_offset) as usize,
272− replacement.clone(),
273− )
274− })
304+ let (transaction, mut selection) = Transaction::change_by_selection_ignore_overlapping(
305+ doc,
306+ selection,
307+ |range| {
308+ let cursor = range.cursor(text);
309+ completion_range(text, edit_offset, cursor)
310+ .filter(|(start, end)| text.slice(start..end) == removed_text)
311+ .unwrap_or_else(|| find_completion_range(text, cursor))
312+ },
313+ |_, _| replacement.clone(),
314+ );
315+ if transaction.changes().is_empty() {
316+ return transaction;
317+ }
318+ selection = selection.map(transaction.changes());
319+ transaction.with_selection(selection)
275320 }
276321
277322 /// Creates a [Transaction] from the [snippet::Snippet] in a completion response.
278323 /// The transaction applies the edit to all cursors.
324+ #[allow(clippy::too_many_arguments)]
279325 pub fn generate_transaction_from_snippet(
280326 doc: &Rope,
281327 selection: &Selection,
282− start_offset: i128,
283− end_offset: i128,
328+ edit_offset: Option<(i128, i128)>,
284329 snippet: snippet::Snippet,
285330 line_ending: &str,
286331 include_placeholder: bool,
332+ tab_width: usize,
287333 ) -> Transaction {
288334 let text = doc.slice(..);
289335
290− // For each cursor store offsets for the first tabstop
291− let mut cursor_tabstop_offsets = Vec::<SmallVec<[(i128, i128); 1]>>::new();
292− let transaction = Transaction::change_by_selection(doc, selection, |range| {
293− let cursor = range.cursor(text);
294− let replacement_start = (cursor as i128 + start_offset) as usize;
295− let replacement_end = (cursor as i128 + end_offset) as usize;
296− let newline_with_offset = format!(
297− "{line_ending}{blank:width$}",
298− line_ending = line_ending,
299− width = replacement_start - doc.line_to_char(doc.char_to_line(replacement_start)),
300− blank = ""
301− );
302−
303− let (replacement, tabstops) =
304− snippet::render(&snippet, newline_with_offset, include_placeholder);
305−
306− let replacement_len = replacement.chars().count();
307− cursor_tabstop_offsets.push(
308− tabstops
309− .first()
310− .unwrap_or(&smallvec![(replacement_len, replacement_len)])
311− .iter()
312− .map(|(from, to)| -> (i128, i128) {
313− (
314− *from as i128 - replacement_len as i128,
315− *to as i128 - replacement_len as i128,
316− )
317− })
318− .collect(),
319− );
320−
321− (replacement_start, replacement_end, Some(replacement.into()))
322− });
336+ let mut off = 0i128;
337+ let mut mapped_doc = doc.clone();
338+ let mut selection_tabstops: SmallVec<[_; 1]> = SmallVec::new();
339+ let (removed_start, removed_end) =
340+ completion_range(text, edit_offset, selection.primary().cursor(text))
341+ .expect("transaction must be valid for primary selection");
342+ let removed_text = text.slice(removed_start..removed_end);
323343
324− // Create new selection based on the cursor tabstop from above
325− let mut cursor_tabstop_offsets_iter = cursor_tabstop_offsets.iter();
326− let selection = selection
327− .clone()
328− .map(transaction.changes())
329− .transform_iter(|range| {
330− cursor_tabstop_offsets_iter
331− .next()
332− .unwrap()
333− .iter()
334− .map(move |(from, to)| {
335− Range::new(
336− (range.anchor as i128 + *from) as usize,
337− (range.anchor as i128 + *to) as usize,
338− )
339− })
340− });
344+ let (transaction, selection) = Transaction::change_by_selection_ignore_overlapping(
345+ doc,
346+ selection,
347+ |range| {
348+ let cursor = range.cursor(text);
349+ completion_range(text, edit_offset, cursor)
350+ .filter(|(start, end)| text.slice(start..end) == removed_text)
351+ .unwrap_or_else(|| find_completion_range(text, cursor))
352+ },
353+ |replacement_start, replacement_end| {
354+ let mapped_replacement_start = (replacement_start as i128 + off) as usize;
355+ let mapped_replacement_end = (replacement_end as i128 + off) as usize;
356+
357+ let line_idx = mapped_doc.char_to_line(mapped_replacement_start);
358+ let pos_on_line = mapped_replacement_start - mapped_doc.line_to_char(line_idx);
359+
360+ // we only care about the actual offset here (not virtual text/softwrap)
361+ // so it's ok to use the deprecated function here
362+ #[allow(deprecated)]
363+ let width = helix_core::visual_coords_at_pos(
364+ mapped_doc.line(line_idx),
365+ pos_on_line,
366+ tab_width,
367+ )
368+ .col;
369+ let newline_with_offset = format!(
370+ "{line_ending}{blank:width$}",
371+ line_ending = line_ending,
372+ blank = ""
373+ );
374+
375+ let (replacement, tabstops) =
376+ snippet::render(&snippet, &newline_with_offset, include_placeholder);
377+ selection_tabstops.push((mapped_replacement_start, tabstops));
378+ mapped_doc.remove(mapped_replacement_start..mapped_replacement_end);
379+ mapped_doc.insert(mapped_replacement_start, &replacement);
380+ off +=
381+ replacement_start as i128 - replacement_end as i128 + replacement.len() as i128;
382+
383+ Some(replacement)
384+ },
385+ );
341386
342− transaction.with_selection(selection)
387+ let changes = transaction.changes();
388+ if changes.is_empty() {
389+ return transaction;
390+ }
391+
392+ let mut mapped_selection = SmallVec::with_capacity(selection.len());
393+ let mut mapped_primary_idx = 0;
394+ let primary_range = selection.primary();
395+ for (range, (tabstop_anchor, tabstops)) in selection.into_iter().zip(selection_tabstops) {
396+ if range == primary_range {
397+ mapped_primary_idx = mapped_selection.len()
398+ }
399+
400+ let range = range.map(changes);
401+ let tabstops = tabstops.first().filter(|tabstops| !tabstops.is_empty());
402+ let Some(tabstops) = tabstops else{
403+ // no tabstop normal mapping
404+ mapped_selection.push(range);
405+ continue;
406+ };
407+
408+ // expand the selection to cover the tabstop to retain the helix selection semantic
409+ // the tabstop closest to the range simply replaces `head` while anchor remains in place
410+ // the remaining tabstops receive their own single-width cursor
411+ if range.head < range.anchor {
412+ let first_tabstop = tabstop_anchor + tabstops[0].1;
413+
414+ // if selection is forward but was moved to the right it is
415+ // contained entirely in the replacement text, just do a point
416+ // selection (fallback below)
417+ if range.anchor >= first_tabstop {
418+ let range = Range::new(range.anchor, first_tabstop);
419+ mapped_selection.push(range);
420+ let rem_tabstops = tabstops[1..]
421+ .iter()
422+ .map(|tabstop| Range::point(tabstop_anchor + tabstop.1));
423+ mapped_selection.extend(rem_tabstops);
424+ continue;
425+ }
426+ } else {
427+ let last_idx = tabstops.len() - 1;
428+ let last_tabstop = tabstop_anchor + tabstops[last_idx].1;
429+
430+ // if selection is forward but was moved to the right it is
431+ // contained entirely in the replacement text, just do a point
432+ // selection (fallback below)
433+ if range.anchor <= last_tabstop {
434+ // we can't properly compute the the next grapheme
435+ // here because the transaction hasn't been applied yet
436+ // that is not a problem because the range gets grapheme aligned anyway
437+ // tough so just adding one will always cause head to be grapheme
438+ // aligned correctly when applied to the document
439+ let range = Range::new(range.anchor, last_tabstop + 1);
440+ mapped_selection.push(range);
441+ let rem_tabstops = tabstops[..last_idx]
442+ .iter()
443+ .map(|tabstop| Range::point(tabstop_anchor + tabstop.0));
444+ mapped_selection.extend(rem_tabstops);
445+ continue;
446+ }
447+ };
448+
449+ let tabstops = tabstops
450+ .iter()
451+ .map(|tabstop| Range::point(tabstop_anchor + tabstop.0));
452+ mapped_selection.extend(tabstops);
453+ }
454+
455+ transaction.with_selection(Selection::new(mapped_selection, mapped_primary_idx))
343456 }
344457
345458 pub fn generate_transaction_from_edits(