@@ -59,8 +59,8 @@ pub enum OffsetEncoding {
59pub mod util {
59pub mod util {
60 use super::*;
60 use super::*;
61 use helix_core::line_ending::{line_end_byte_index, line_end_char_index};
61 use helix_core::line_ending::{line_end_byte_index, line_end_char_index};
62+ use helix_core::{chars, RopeSlice, SmallVec};
62 use helix_core::{diagnostic::NumberOrString, Range, Rope, Selection, Tendril, Transaction};
63 use helix_core::{diagnostic::NumberOrString, Range, Rope, Selection, Tendril, Transaction};
63− use helix_core::{smallvec, SmallVec};
65 /// Converts a diagnostic in the document to [`lsp::Diagnostic`].
65 /// Converts a diagnostic in the document to [`lsp::Diagnostic`].
@@ -247,13 +247,46 @@ pub mod util {
247 Some(Range::new(start, end))
247 Some(Range::new(start, end))
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) {
258+ .take_while(|ch| chars::char_is_word(*ch))
262+ fn completion_range(
264+ edit_offset: Option<(i128, i128)>,
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 {
273+ let end_offset = cursor as i128 + end_offset;
274+ if end_offset > text.len_chars() as i128 {
277+ (start_offset as usize, end_offset as usize)
279+ None => find_completion_range(text, cursor),
250 /// Creates a [Transaction] from the [lsp::TextEdit] in a completion response.
284 /// Creates a [Transaction] from the [lsp::TextEdit] in a completion response.
251 /// The transaction applies the edit to all cursors.
285 /// The transaction applies the edit to all cursors.
252 pub fn generate_transaction_from_completion_edit(
286 pub fn generate_transaction_from_completion_edit(
253 doc: &Rope,
287 doc: &Rope,
254 selection: &Selection,
288 selection: &Selection,
255− start_offset: i128,
289+ edit_offset: Option<(i128, i128)>,
257 new_text: String,
290 new_text: String,
258 ) -> Transaction {
291 ) -> Transaction {
259 let replacement: Option<Tendril> = if new_text.is_empty() {
292 let replacement: Option<Tendril> = if new_text.is_empty() {
@@ -263,83 +296,163 @@ pub mod util {
265 let text = doc.slice(..);
298 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);
267− Transaction::change_by_selection(doc, selection, |range| {
304+ let (transaction, mut selection) = Transaction::change_by_selection_ignore_overlapping(
268− let cursor = range.cursor(text);
308+ let cursor = range.cursor(text);
269− (
309+ completion_range(text, edit_offset, cursor)
270− (cursor as i128 + start_offset) as usize,
310+ .filter(|(start, end)| text.slice(start..end) == removed_text)
271− (cursor as i128 + end_offset) as usize,
311+ .unwrap_or_else(|| find_completion_range(text, cursor))
272− replacement.clone(),
313+ |_, _| replacement.clone(),
274− })
315+ if transaction.changes().is_empty() {
318+ selection = selection.map(transaction.changes());
319+ transaction.with_selection(selection)
277 /// Creates a [Transaction] from the [snippet::Snippet] in a completion response.
322 /// Creates a [Transaction] from the [snippet::Snippet] in a completion response.
278 /// The transaction applies the edit to all cursors.
323 /// The transaction applies the edit to all cursors.
324+ #[allow(clippy::too_many_arguments)]
279 pub fn generate_transaction_from_snippet(
325 pub fn generate_transaction_from_snippet(
280 doc: &Rope,
326 doc: &Rope,
281 selection: &Selection,
327 selection: &Selection,
282− start_offset: i128,
328+ edit_offset: Option<(i128, i128)>,
284 snippet: snippet::Snippet,
329 snippet: snippet::Snippet,
285 line_ending: &str,
330 line_ending: &str,
286 include_placeholder: bool,
331 include_placeholder: bool,
287 ) -> Transaction {
333 ) -> Transaction {
288 let text = doc.slice(..);
334 let text = doc.slice(..);
290− // For each cursor store offsets for the first tabstop
336+ let mut off = 0i128;
291− let mut cursor_tabstop_offsets = Vec::<SmallVec<[(i128, i128); 1]>>::new();
337+ let mut mapped_doc = doc.clone();
292− let transaction = Transaction::change_by_selection(doc, selection, |range| {
338+ let mut selection_tabstops: SmallVec<[_; 1]> = SmallVec::new();
293− let cursor = range.cursor(text);
339+ let (removed_start, removed_end) =
294− let replacement_start = (cursor as i128 + start_offset) as usize;
340+ completion_range(text, edit_offset, selection.primary().cursor(text))
295− let replacement_end = (cursor as i128 + end_offset) as usize;
341+ .expect("transaction must be valid for primary selection");
296− let newline_with_offset = format!(
342+ let removed_text = text.slice(removed_start..removed_end);
297− "{line_ending}{blank:width$}",
298− line_ending = line_ending,
299− width = replacement_start - doc.line_to_char(doc.char_to_line(replacement_start)),
303− let (replacement, tabstops) =
304− snippet::render(&snippet, newline_with_offset, include_placeholder);
306− let replacement_len = replacement.chars().count();
307− cursor_tabstop_offsets.push(
310− .unwrap_or(&smallvec![(replacement_len, replacement_len)])
312− .map(|(from, to)| -> (i128, i128) {
314− *from as i128 - replacement_len as i128,
315− *to as i128 - replacement_len as i128,
321− (replacement_start, replacement_end, Some(replacement.into()))
324− // Create new selection based on the cursor tabstop from above
344+ let (transaction, selection) = Transaction::change_by_selection_ignore_overlapping(
325− let mut cursor_tabstop_offsets_iter = cursor_tabstop_offsets.iter();
345+ doc,
326− let selection = selection
346+ selection,
328− .map(transaction.changes())
329− .transform_iter(|range| {
347+ |range| {
330− cursor_tabstop_offsets_iter
348+ let cursor = range.cursor(text);
331− .next()
349+ completion_range(text, edit_offset, cursor)
332− .unwrap()
350+ .filter(|(start, end)| text.slice(start..end) == removed_text)
333− .iter()
351+ .unwrap_or_else(|| find_completion_range(text, cursor))
334− .map(move |(from, to)| {
352+ },
335− Range::new(
353+ |replacement_start, replacement_end| {
336− (range.anchor as i128 + *from) as usize,
354+ let mapped_replacement_start = (replacement_start as i128 + off) as usize;
337− (range.anchor as i128 + *to) as usize,
355+ let mapped_replacement_end = (replacement_end as i128 + off) as usize;
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);
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),
369+ let newline_with_offset = format!(
370+ "{line_ending}{blank:width$}",
371+ line_ending = line_ending,
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);
381+ replacement_start as i128 - replacement_end as i128 + replacement.len() as i128;
387+ let changes = transaction.changes();
388+ if changes.is_empty() {
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()
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);
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;
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..]
422+ .map(|tabstop| Range::point(tabstop_anchor + tabstop.1));
423+ mapped_selection.extend(rem_tabstops);
427+ let last_idx = tabstops.len() - 1;
428+ let last_tabstop = tabstop_anchor + tabstops[last_idx].1;
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]
443+ .map(|tabstop| Range::point(tabstop_anchor + tabstop.0));
444+ mapped_selection.extend(rem_tabstops);
449+ let tabstops = tabstops
451+ .map(|tabstop| Range::point(tabstop_anchor + tabstop.0));
452+ mapped_selection.extend(tabstops);
342− transaction.with_selection(selection)
455+ transaction.with_selection(Selection::new(mapped_selection, mapped_primary_idx))
345 pub fn generate_transaction_from_edits(
458 pub fn generate_transaction_from_edits(