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 Feb 25, 2023
1 parent 8b4cf75 commit dbc09a2
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 16 deletions.
1 change: 1 addition & 0 deletions book/src/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ on unix operating systems.
| `auto-save` | Enable automatic saving on 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-replacer` | 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 infoboxes | `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
49 changes: 49 additions & 0 deletions helix-core/src/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,42 @@ impl Transaction {
self
}

/// Generate a transaction from a set of potentially overallping changes.
/// Changes that overlap are ignored
pub fn change_ignore_overlapping<I>(doc: &Rope, changes: I) -> Self
where
I: Iterator<Item = Change>,
{
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 (from, to, tendril) in changes {
if last > from {
continue;
}

// 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 +549,19 @@ impl Transaction {
Self::change(doc, selection.iter().map(f))
}

/// Generate a transaction with a change per selection range.
/// Overlapping changes are ignored
pub fn change_by_selection_ignore_overlapping<F>(
doc: &Rope,
selection: &Selection,
f: F,
) -> Self
where
F: FnMut(&Range) -> Change,
{
Self::change_ignore_overlapping(doc, selection.iter().map(f))
}

/// 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
11 changes: 9 additions & 2 deletions helix-lsp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -271,10 +271,17 @@ pub mod util {
None => return Transaction::new(doc),
};

Transaction::change_by_selection(doc, selection, |range| {
Transaction::change_by_selection_ignore_overlapping(doc, selection, |range| {
let cursor = range.cursor(text);
let start = cursor as i128 + start_offset;
let end = (cursor as i128 + end_offset) as usize;
// if inserting a completion at this cursor would go out of bounds
// return a trivial edit
if start < 0 || end > doc.len_chars() {
return (0, 0, None);
}
(
(cursor as i128 + start_offset) as usize,
start as usize,
(cursor as i128 + end_offset) as usize,
replacement.clone(),
)
Expand Down
59 changes: 45 additions & 14 deletions helix-term/src/ui/completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use tui::{buffer::Buffer as Surface, text::Span};

use std::borrow::Cow;

use helix_core::{Change, Transaction};
use helix_core::{chars, Change, Transaction};
use helix_view::{graphics::Rect, Document, Editor};

use crate::commands;
Expand Down Expand Up @@ -106,6 +106,7 @@ impl Completion {
start_offset: usize,
trigger_offset: usize,
) -> Self {
let replace_mode = editor.config().completion_replace;
// Sort completion items according to their preselect status (given by the LSP server)
items.sort_by_key(|item| !item.preselect.unwrap_or(false));

Expand All @@ -118,13 +119,18 @@ impl Completion {
offset_encoding: helix_lsp::OffsetEncoding,
start_offset: usize,
trigger_offset: usize,
replace_mode: bool,
) -> Transaction {
let transaction = if let Some(edit) = &item.text_edit {
let edit = match edit {
lsp::CompletionTextEdit::Edit(edit) => edit.clone(),
lsp::CompletionTextEdit::InsertAndReplace(item) => {
// TODO: support using "insert" instead of "replace" via user config
lsp::TextEdit::new(item.replace, item.new_text.clone())
let range = if replace_mode {
item.replace
} else {
item.insert
};
lsp::TextEdit::new(range, item.new_text.clone())
}
};

Expand All @@ -136,21 +142,44 @@ impl Completion {
)
} else {
let text = item.insert_text.as_ref().unwrap_or(&item.label);
let doc_text = doc.text().slice(..);
let primary_cursor = doc.selection(view_id).primary().cursor(doc_text);

// TODO: this needs to be true for the numbers to work out correctly
// in the closure below. It's passed in to a callback as this same
// TODO: this needs to be true for the closure below to work out
// It's passed in to a callback as this same
// formula, but can the value change between the LSP request and
// response? If it does, can we recover?
debug_assert!(
doc.selection(view_id)
.primary()
.cursor(doc.text().slice(..))
== trigger_offset
);
debug_assert!(primary_cursor == trigger_offset);
let start_offset = primary_cursor - start_offset;

Transaction::change_by_selection(doc.text(), doc.selection(view_id), |_| {
(start_offset, trigger_offset, Some(text.into()))
})
Transaction::change_by_selection_ignore_overlapping(
doc.text(),
doc.selection(view_id),
|range| {
let cursor = range.cursor(doc_text);
// if inserting a completion at this cursor would go out of bounds
// return a trivial edit
if cursor < start_offset {
return (0, 0, None);
}
let end_offset = if replace_mode {
// in replace mode replace the rest of the word
doc_text
.chars_at(primary_cursor)
.take_while(|ch| chars::char_is_word(*ch))
.count()
} else {
// otherwise only replace up to the current edit
0
};

(
cursor - start_offset,
cursor + end_offset,
Some(text.into()),
)
},
)
};

transaction
Expand Down Expand Up @@ -184,6 +213,7 @@ impl Completion {
offset_encoding,
start_offset,
trigger_offset,
replace_mode,
);

// initialize a savepoint
Expand All @@ -206,6 +236,7 @@ impl Completion {
offset_encoding,
start_offset,
trigger_offset,
replace_mode,
);

doc.apply(&transaction, view.id);
Expand Down
4 changes: 4 additions & 0 deletions helix-view/src/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,9 @@ pub struct Config {
)]
pub idle_timeout: Duration,
pub completion_trigger_len: u8,
/// Whether to instruct the LSP to replace the entire word when applying a completion
/// or to only insert new text
pub completion_replace: bool,
/// Whether to display infoboxes. Defaults to true.
pub auto_info: bool,
pub file_picker: FilePickerConfig,
Expand Down Expand Up @@ -772,6 +775,7 @@ impl Default for Config {
indent_guides: IndentGuidesConfig::default(),
color_modes: false,
soft_wrap: SoftWrap::default(),
completion_replace: false,
}
}
}
Expand Down

0 comments on commit dbc09a2

Please sign in to comment.