diff --git a/book/src/configuration.md b/book/src/configuration.md index aebf5ff0f..4b62ca521 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -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 | `[]` | diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index 58e8d83dc..1463ccb3c 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -250,18 +250,27 @@ pub mod util { /// If the LS did not provide a range for the completion or the range of the /// primary cursor can not be used for the secondary cursor, this function /// can be used to find the completion range for a cursor - fn find_completion_range(text: RopeSlice, cursor: usize) -> (usize, usize) { + fn find_completion_range(text: RopeSlice, replace_mode: bool, cursor: usize) -> (usize, usize) { let start = cursor - text .chars_at(cursor) .reversed() .take_while(|ch| chars::char_is_word(*ch)) .count(); - (start, cursor) + let mut end = cursor; + if replace_mode { + end += text + .chars_at(cursor) + .skip(1) + .take_while(|ch| chars::char_is_word(*ch)) + .count(); + } + (start, end) } fn completion_range( text: RopeSlice, edit_offset: Option<(i128, i128)>, + replace_mode: bool, cursor: usize, ) -> Option<(usize, usize)> { let res = match edit_offset { @@ -276,7 +285,7 @@ pub mod util { } (start_offset as usize, end_offset as usize) } - None => find_completion_range(text, cursor), + None => find_completion_range(text, replace_mode, cursor), }; Some(res) } @@ -287,6 +296,7 @@ pub mod util { doc: &Rope, selection: &Selection, edit_offset: Option<(i128, i128)>, + replace_mode: bool, new_text: String, ) -> Transaction { let replacement: Option = if new_text.is_empty() { @@ -296,9 +306,13 @@ pub mod util { }; let text = doc.slice(..); - let (removed_start, removed_end) = - completion_range(text, edit_offset, selection.primary().cursor(text)) - .expect("transaction must be valid for primary selection"); + let (removed_start, removed_end) = completion_range( + text, + edit_offset, + replace_mode, + selection.primary().cursor(text), + ) + .expect("transaction must be valid for primary selection"); let removed_text = text.slice(removed_start..removed_end); let (transaction, mut selection) = Transaction::change_by_selection_ignore_overlapping( @@ -306,9 +320,9 @@ pub mod util { selection, |range| { let cursor = range.cursor(text); - completion_range(text, edit_offset, cursor) + completion_range(text, edit_offset, replace_mode, cursor) .filter(|(start, end)| text.slice(start..end) == removed_text) - .unwrap_or_else(|| find_completion_range(text, cursor)) + .unwrap_or_else(|| find_completion_range(text, replace_mode, cursor)) }, |_, _| replacement.clone(), ); @@ -326,6 +340,7 @@ pub mod util { doc: &Rope, selection: &Selection, edit_offset: Option<(i128, i128)>, + replace_mode: bool, snippet: snippet::Snippet, line_ending: &str, include_placeholder: bool, @@ -336,9 +351,13 @@ pub mod util { let mut off = 0i128; let mut mapped_doc = doc.clone(); let mut selection_tabstops: SmallVec<[_; 1]> = SmallVec::new(); - let (removed_start, removed_end) = - completion_range(text, edit_offset, selection.primary().cursor(text)) - .expect("transaction must be valid for primary selection"); + let (removed_start, removed_end) = completion_range( + text, + edit_offset, + replace_mode, + selection.primary().cursor(text), + ) + .expect("transaction must be valid for primary selection"); let removed_text = text.slice(removed_start..removed_end); let (transaction, selection) = Transaction::change_by_selection_ignore_overlapping( @@ -346,9 +365,9 @@ pub mod util { selection, |range| { let cursor = range.cursor(text); - completion_range(text, edit_offset, cursor) + completion_range(text, edit_offset, replace_mode, cursor) .filter(|(start, end)| text.slice(start..end) == removed_text) - .unwrap_or_else(|| find_completion_range(text, cursor)) + .unwrap_or_else(|| find_completion_range(text, replace_mode, cursor)) }, |replacement_start, replacement_end| { let mapped_replacement_start = (replacement_start as i128 + off) as usize; diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index 99c337811..6303793b4 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -108,6 +108,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)); @@ -120,18 +121,23 @@ impl Completion { offset_encoding: helix_lsp::OffsetEncoding, trigger_offset: usize, include_placeholder: bool, + replace_mode: bool, ) -> Transaction { use helix_lsp::snippet; let selection = doc.selection(view_id); let text = doc.text().slice(..); let primary_cursor = selection.primary().cursor(text); - let (start_offset, end_offset, new_text) = if let Some(edit) = &item.text_edit { + let (edit_offset, new_text) = 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()) } }; @@ -157,7 +163,7 @@ impl Completion { // document changed (and not just the selection) then we will // likely delete the wrong text (same if we applied an edit sent by the LS) debug_assert!(primary_cursor == trigger_offset); - (None, Some(0), new_text) + (None, new_text) }; if matches!(item.kind, Some(lsp::CompletionItemKind::SNIPPET)) @@ -170,8 +176,8 @@ impl Completion { Ok(snippet) => util::generate_transaction_from_snippet( doc.text(), selection, - start_offset, - end_offset, + edit_offset, + replace_mode, snippet, doc.line_ending.as_str(), include_placeholder, @@ -190,8 +196,8 @@ impl Completion { util::generate_transaction_from_completion_edit( doc.text(), selection, - start_offset, - end_offset, + edit_offset, + replace_mode, new_text, ) } @@ -224,6 +230,7 @@ impl Completion { offset_encoding, trigger_offset, true, + replace_mode, ); // initialize a savepoint @@ -245,6 +252,7 @@ impl Completion { offset_encoding, trigger_offset, false, + replace_mode, ); doc.apply(&transaction, view.id); diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index c6541105a..1b4664ffb 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -251,6 +251,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, @@ -738,6 +741,7 @@ impl Default for Config { color_modes: false, soft_wrap: SoftWrap::default(), text_width: 80, + completion_replace: false, } } }