Skip to content

Commit

Permalink
make replace completions optional, fix issues with multicursors
Browse files Browse the repository at this point in the history
  • Loading branch information
pascalkuthe committed Mar 8, 2023
1 parent 2695c17 commit db3d24c
Show file tree
Hide file tree
Showing 5 changed files with 269 additions and 78 deletions.
1 change: 1 addition & 0 deletions book/src/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ signal to the Helix process on Unix operating systems, such as by using the comm
| `auto-save` | Enable automatic saving on the focus moving away from Helix. Requires [focus event support](https://github.com/helix-editor/helix/wiki/Terminal-Support) from your terminal | `false` |
| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant | `400` |
| `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` |
| `completion-replace` | Set to `true` to make completions always replace the entire word and not just the part before the cursor | `false` |
| `auto-info` | Whether to display info boxes | `true` |
| `true-color` | Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative | `false` |
| `rulers` | List of column positions at which to display the rulers. Can be overridden by language specific `rulers` in `languages.toml` file | `[]` |
Expand Down
85 changes: 85 additions & 0 deletions helix-core/src/transaction.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use smallvec::SmallVec;

use crate::{Range, Rope, Selection, Tendril};
use std::borrow::Cow;

Expand Down Expand Up @@ -466,6 +468,56 @@ impl Transaction {
self
}

/// Generate a transaction from a set of potentially overallping changes.
/// If a change overlaps a previous changes it's ignored
///
/// The `process_change` callback is called for each change (in the order
/// yielded by `changes`). This callback can be used to process additional
/// information (usually selections) associated with each change. This
/// function passes `None` to the `process_changes` function if the
/// `changes` iterator yields `None` **or if the change overlaps with a
/// previous change** and is ignored.
pub fn change_ignore_overlapping<I, T>(
doc: &Rope,
changes: I,
mut process_change: impl FnMut(Option<T>),
) -> Self
where
I: Iterator<Item = Option<(Change, T)>>,
{
let len = doc.len_chars();

let (lower, upper) = changes.size_hint();
let size = upper.unwrap_or(lower);
let mut changeset = ChangeSet::with_capacity(2 * size + 1); // rough estimate

let mut last = 0;
for mut change in changes {
change = change.filter(|&((from, _, _), _)| last <= from);
let Some(((from, to, tendril), data)) = change else {
process_change(None);
continue;
};
process_change(Some(data));

// Retain from last "to" to current "from"
changeset.retain(from - last);
let span = to - from;
match tendril {
Some(text) => {
changeset.insert(text);
changeset.delete(span);
}
None => changeset.delete(span),
}
last = to;
}

changeset.retain(len - last);

Self::from(changeset)
}

/// Generate a transaction from a set of changes.
pub fn change<I>(doc: &Rope, changes: I) -> Self
where
Expand Down Expand Up @@ -513,6 +565,39 @@ impl Transaction {
Self::change(doc, selection.iter().map(f))
}

pub fn change_by_selection_ignore_overlapping<T>(
doc: &Rope,
selection: &Selection,
mut create_change: impl FnMut(&Range) -> Option<(Change, T)>,
mut process_applied_change: impl FnMut(&Range, T),
) -> (Transaction, usize) {
let mut last_selection_idx = None;
let mut new_primary_idx = 0;
let mut idx = 0;
let mut applied_changes = 0;
let process_change = |change: Option<_>| {
if let Some((range, data)) = change {
last_selection_idx = Some(applied_changes);
process_applied_change(range, data);
applied_changes += 1;
}
if idx == selection.primary_index() {
new_primary_idx = last_selection_idx.unwrap_or(0);
}
idx += 1;
};
let transaction = Self::change_ignore_overlapping(
doc,
selection.iter().map(|range| {
let (change, data) = create_change(range)?;
Some((change, (range, data)))
}),
process_change,
);
// map the transaction trough changes
((transaction, new_primary_idx))
}

/// Insert text at each selection head.
pub fn insert(doc: &Rope, selection: &Selection, text: Tendril) -> Self {
Self::change_by_selection(doc, selection, |range| {
Expand Down
199 changes: 136 additions & 63 deletions helix-lsp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ pub enum OffsetEncoding {
pub mod util {
use super::*;
use helix_core::line_ending::{line_end_byte_index, line_end_char_index};
use helix_core::{chars, Assoc, RopeSlice, SmallVec};
use helix_core::{diagnostic::NumberOrString, Range, Rope, Selection, Tendril, Transaction};
use helix_core::{smallvec, SmallVec};

/// Converts a diagnostic in the document to [`lsp::Diagnostic`].
///
Expand Down Expand Up @@ -247,13 +247,56 @@ pub mod util {
Some(Range::new(start, end))
}

fn completion_pos(
text: RopeSlice,
start_offset: Option<i128>,
end_offset: Option<i128>,
cursor: usize,
) -> Option<(usize, usize)> {
let replacement_start = match start_offset {
Some(start_offset) => {
let start_offset = cursor as i128 + start_offset;
if start_offset < 0 {
return None;
}
start_offset as usize
}
None => {
cursor
+ text
.chars_at(cursor)
.skip(1)
.take_while(|ch| chars::char_is_word(*ch))
.count()
}
};
let replacement_end = match end_offset {
Some(end_offset) => {
let replacement_end = cursor as i128 + end_offset;
if replacement_end > text.len_chars() as i128 {
return None;
}
replacement_end as usize
}
None => {
cursor
- text
.chars_at(cursor)
.reversed()
.take_while(|ch| chars::char_is_word(*ch))
.count()
}
};
Some((replacement_start, replacement_end))
}

/// Creates a [Transaction] from the [lsp::TextEdit] in a completion response.
/// The transaction applies the edit to all cursors.
pub fn generate_transaction_from_completion_edit(
doc: &Rope,
selection: &Selection,
start_offset: i128,
end_offset: i128,
start_offset: Option<i128>,
end_offset: Option<i128>,
new_text: String,
) -> Transaction {
let replacement: Option<Tendril> = if new_text.is_empty() {
Expand All @@ -263,83 +306,113 @@ pub mod util {
};

let text = doc.slice(..);
let mut new_selection = SmallVec::new();

Transaction::change_by_selection(doc, selection, |range| {
let cursor = range.cursor(text);
(
(cursor as i128 + start_offset) as usize,
(cursor as i128 + end_offset) as usize,
replacement.clone(),
)
})
let (transaction, primary_idx) = Transaction::change_by_selection_ignore_overlapping(
doc,
selection,
|range| {
let cursor = range.cursor(text);
let (start, end) = completion_pos(text, start_offset, end_offset, cursor)?;
Some(((start as usize, end, replacement.clone()), ()))
},
|range, _| new_selection.push(*range),
);
if transaction.changes().is_empty() {
return transaction;
}
let selection = Selection::new(new_selection, primary_idx).map(transaction.changes());
transaction.with_selection(selection)
}

/// Creates a [Transaction] from the [snippet::Snippet] in a completion response.
/// The transaction applies the edit to all cursors.
pub fn generate_transaction_from_snippet(
doc: &Rope,
selection: &Selection,
start_offset: i128,
end_offset: i128,
start_offset: Option<i128>,
end_offset: Option<i128>,
snippet: snippet::Snippet,
line_ending: &str,
include_placeholder: bool,
) -> Transaction {
let text = doc.slice(..);
let mut new_selection: SmallVec<[_; 1]> = SmallVec::new();

// For each cursor store offsets for the first tabstop
let mut cursor_tabstop_offsets = Vec::<SmallVec<[(i128, i128); 1]>>::new();
let transaction = Transaction::change_by_selection(doc, selection, |range| {
let cursor = range.cursor(text);
let replacement_start = (cursor as i128 + start_offset) as usize;
let replacement_end = (cursor as i128 + end_offset) as usize;
let newline_with_offset = format!(
"{line_ending}{blank:width$}",
line_ending = line_ending,
width = replacement_start - doc.line_to_char(doc.char_to_line(replacement_start)),
blank = ""
);

let (replacement, tabstops) =
snippet::render(&snippet, newline_with_offset, include_placeholder);

let replacement_len = replacement.chars().count();
cursor_tabstop_offsets.push(
tabstops
.first()
.unwrap_or(&smallvec![(replacement_len, replacement_len)])
.iter()
.map(|(from, to)| -> (i128, i128) {
(
*from as i128 - replacement_len as i128,
*to as i128 - replacement_len as i128,
)
})
.collect(),
);

(replacement_start, replacement_end, Some(replacement.into()))
});
let (transaction, primary_idx) = Transaction::change_by_selection_ignore_overlapping(
doc,
selection,
|range| {
let cursor = range.cursor(text);
let (replacement_start, replacement_end) =
completion_pos(text, start_offset, end_offset, cursor)?;
let newline_with_offset = format!(
"{line_ending}{blank:width$}",
line_ending = line_ending,
width =
replacement_start - doc.line_to_char(doc.char_to_line(replacement_start)),
blank = ""
);

let (replacement, tabstops) =
snippet::render(&snippet, newline_with_offset, include_placeholder);

Some((
(replacement_start, replacement_end, Some(replacement.into())),
(replacement_start, tabstops),
))
},
|range, tabstops| new_selection.push((*range, tabstops)),
);

let changes = transaction.changes();
if changes.is_empty() {
return transaction;
}

let mut mapped_selection = SmallVec::with_capacity(new_selection.len());
let mut mapped_primary_idx = 0;
for (i, (range, (tabstop_anchor, tabstops))) in new_selection.into_iter().enumerate() {
if i == primary_idx {
mapped_primary_idx = mapped_selection.len()
}

// Create new selection based on the cursor tabstop from above
let mut cursor_tabstop_offsets_iter = cursor_tabstop_offsets.iter();
let selection = selection
.clone()
.map(transaction.changes())
.transform_iter(|range| {
cursor_tabstop_offsets_iter
.next()
.unwrap()
let range = range.map(changes);
let tabstops = tabstops.first().filter(|tabstops| !tabstops.is_empty());
let Some(tabstops) = tabstops else{
// no tabstop normal mapping
mapped_selection.push(range);
continue;
};
let tabstop_anchor = changes.map_pos(tabstop_anchor, Assoc::Before);
// expand the selection to cover the tabstop to retain the helix selection semantic
// the tabstop closest to the range simply replaces `head` while anchor remains in place
// the remainaing tabstops recive their own single-width cursor
if range.head < range.anchor {
let primary_tabstop = tabstop_anchor + tabstops[0].1;
debug_assert!(primary_tabstop <= range.anchor);
let range = Range::new(range.anchor, primary_tabstop);
mapped_selection.push(range);
let rem_tabstops = tabstops[1..]
.iter()
.map(move |(from, to)| {
Range::new(
(range.anchor as i128 + *from) as usize,
(range.anchor as i128 + *to) as usize,
)
})
});
.map(|tabstop| Range::point(tabstop_anchor + tabstop.1));
mapped_selection.extend(rem_tabstops);
} else {
let last_idx = tabstops.len();
let primary_tabstop = tabstop_anchor + tabstops[0].1;
debug_assert!(primary_tabstop >= range.anchor);
let range = Range::new(range.anchor, primary_tabstop);
let primary_tabstop = tabstop_anchor + tabstops[last_idx].0;
let range = range.put_cursor(text, primary_tabstop, true);
mapped_selection.push(range);
let rem_tabstops = tabstops[..last_idx]
.iter()
.map(|tabstop| Range::point(tabstop_anchor + tabstop.0));
mapped_selection.extend(rem_tabstops);
};
}

transaction.with_selection(selection)
transaction.with_selection(Selection::new(mapped_selection, mapped_primary_idx))
}

pub fn generate_transaction_from_edits(
Expand Down
Loading

0 comments on commit db3d24c

Please sign in to comment.