on master help sign in
treat replace/insertmode consistently, default to insert
d63e570Pascal Kuthe
book/src/configuration.md+1
@@ -50,6 +50,7 @@ signal to the Helix process on Unix operating systems, such as by using the comm
50| `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` |
50| `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` |
51| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant | `400` |
51| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant | `400` |
52| `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` |
52| `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` |
53+| `completion-replace` | Set to `true` to make completions always replace the entire word and not just the part before the cursor | `false` |
53| `auto-info` | Whether to display info boxes | `true` |
54| `auto-info` | Whether to display info boxes | `true` |
54| `true-color` | Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative | `false` |
55| `true-color` | Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative | `false` |
55| `rulers` | List of column positions at which to display the rulers. Can be overridden by language specific `rulers` in `languages.toml` file | `[]` |
56| `rulers` | List of column positions at which to display the rulers. Can be overridden by language specific `rulers` in `languages.toml` file | `[]` |
helix-lsp/src/lib.rs+32−13
@@ -250,18 +250,27 @@ pub mod util {
250 /// If the LS did not provide a range for the completion or the range of the
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
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
252 /// can be used to find the completion range for a cursor
253− fn find_completion_range(text: RopeSlice, cursor: usize) -> (usize, usize) {
253+ fn find_completion_range(text: RopeSlice, replace_mode: bool, cursor: usize) -> (usize, usize) {
254 let start = cursor
254 let start = cursor
255 - text
255 - text
256 .chars_at(cursor)
256 .chars_at(cursor)
257 .reversed()
257 .reversed()
258 .take_while(|ch| chars::char_is_word(*ch))
258 .take_while(|ch| chars::char_is_word(*ch))
259 .count();
259 .count();
260+ let mut end = cursor;
261+ if replace_mode {
262+ end += text
263+ .chars_at(cursor)
264+ .skip(1)
265+ .take_while(|ch| chars::char_is_word(*ch))
266+ .count();
267+ }
260− (start, cursor)
268+ (start, end)
261 }
269 }
262 fn completion_range(
270 fn completion_range(
263 text: RopeSlice,
271 text: RopeSlice,
264 edit_offset: Option<(i128, i128)>,
272 edit_offset: Option<(i128, i128)>,
273+ replace_mode: bool,
265 cursor: usize,
274 cursor: usize,
266 ) -> Option<(usize, usize)> {
275 ) -> Option<(usize, usize)> {
267 let res = match edit_offset {
276 let res = match edit_offset {
@@ -276,7 +285,7 @@ pub mod util {
276 }
285 }
277 (start_offset as usize, end_offset as usize)
286 (start_offset as usize, end_offset as usize)
278 }
287 }
279− None => find_completion_range(text, cursor),
288+ None => find_completion_range(text, replace_mode, cursor),
280 };
289 };
281 Some(res)
290 Some(res)
282 }
291 }
@@ -287,6 +296,7 @@ pub mod util {
287 doc: &Rope,
296 doc: &Rope,
288 selection: &Selection,
297 selection: &Selection,
289 edit_offset: Option<(i128, i128)>,
298 edit_offset: Option<(i128, i128)>,
299+ replace_mode: bool,
290 new_text: String,
300 new_text: String,
291 ) -> Transaction {
301 ) -> Transaction {
292 let replacement: Option<Tendril> = if new_text.is_empty() {
302 let replacement: Option<Tendril> = if new_text.is_empty() {
@@ -296,9 +306,13 @@ pub mod util {
296 };
306 };
297
307
298 let text = doc.slice(..);
308 let text = doc.slice(..);
299− let (removed_start, removed_end) =
309+ let (removed_start, removed_end) = completion_range(
310+ text,
311+ edit_offset,
312+ replace_mode,
300− completion_range(text, edit_offset, selection.primary().cursor(text))
313+ selection.primary().cursor(text),
314+ )
301− .expect("transaction must be valid for primary selection");
315+ .expect("transaction must be valid for primary selection");
302 let removed_text = text.slice(removed_start..removed_end);
316 let removed_text = text.slice(removed_start..removed_end);
303
317
304 let (transaction, mut selection) = Transaction::change_by_selection_ignore_overlapping(
318 let (transaction, mut selection) = Transaction::change_by_selection_ignore_overlapping(
@@ -306,9 +320,9 @@ pub mod util {
306 selection,
320 selection,
307 |range| {
321 |range| {
308 let cursor = range.cursor(text);
322 let cursor = range.cursor(text);
309− completion_range(text, edit_offset, cursor)
323+ completion_range(text, edit_offset, replace_mode, cursor)
310 .filter(|(start, end)| text.slice(start..end) == removed_text)
324 .filter(|(start, end)| text.slice(start..end) == removed_text)
311− .unwrap_or_else(|| find_completion_range(text, cursor))
325+ .unwrap_or_else(|| find_completion_range(text, replace_mode, cursor))
312 },
326 },
313 |_, _| replacement.clone(),
327 |_, _| replacement.clone(),
314 );
328 );
@@ -326,6 +340,7 @@ pub mod util {
326 doc: &Rope,
340 doc: &Rope,
327 selection: &Selection,
341 selection: &Selection,
328 edit_offset: Option<(i128, i128)>,
342 edit_offset: Option<(i128, i128)>,
343+ replace_mode: bool,
329 snippet: snippet::Snippet,
344 snippet: snippet::Snippet,
330 line_ending: &str,
345 line_ending: &str,
331 include_placeholder: bool,
346 include_placeholder: bool,
@@ -336,9 +351,13 @@ pub mod util {
336 let mut off = 0i128;
351 let mut off = 0i128;
337 let mut mapped_doc = doc.clone();
352 let mut mapped_doc = doc.clone();
338 let mut selection_tabstops: SmallVec<[_; 1]> = SmallVec::new();
353 let mut selection_tabstops: SmallVec<[_; 1]> = SmallVec::new();
339− let (removed_start, removed_end) =
354+ let (removed_start, removed_end) = completion_range(
355+ text,
356+ edit_offset,
357+ replace_mode,
340− completion_range(text, edit_offset, selection.primary().cursor(text))
358+ selection.primary().cursor(text),
359+ )
341− .expect("transaction must be valid for primary selection");
360+ .expect("transaction must be valid for primary selection");
342 let removed_text = text.slice(removed_start..removed_end);
361 let removed_text = text.slice(removed_start..removed_end);
343
362
344 let (transaction, selection) = Transaction::change_by_selection_ignore_overlapping(
363 let (transaction, selection) = Transaction::change_by_selection_ignore_overlapping(
@@ -346,9 +365,9 @@ pub mod util {
346 selection,
365 selection,
347 |range| {
366 |range| {
348 let cursor = range.cursor(text);
367 let cursor = range.cursor(text);
349− completion_range(text, edit_offset, cursor)
368+ completion_range(text, edit_offset, replace_mode, cursor)
350 .filter(|(start, end)| text.slice(start..end) == removed_text)
369 .filter(|(start, end)| text.slice(start..end) == removed_text)
351− .unwrap_or_else(|| find_completion_range(text, cursor))
370+ .unwrap_or_else(|| find_completion_range(text, replace_mode, cursor))
352 },
371 },
353 |replacement_start, replacement_end| {
372 |replacement_start, replacement_end| {
354 let mapped_replacement_start = (replacement_start as i128 + off) as usize;
373 let mapped_replacement_start = (replacement_start as i128 + off) as usize;
helix-term/src/ui/completion.rs+16−8
@@ -108,6 +108,7 @@ impl Completion {
108 start_offset: usize,
108 start_offset: usize,
109 trigger_offset: usize,
109 trigger_offset: usize,
110 ) -> Self {
110 ) -> Self {
111+ let replace_mode = editor.config().completion_replace;
111 // Sort completion items according to their preselect status (given by the LSP server)
112 // Sort completion items according to their preselect status (given by the LSP server)
112 items.sort_by_key(|item| !item.preselect.unwrap_or(false));
113 items.sort_by_key(|item| !item.preselect.unwrap_or(false));
113
114
@@ -120,18 +121,23 @@ impl Completion {
120 offset_encoding: helix_lsp::OffsetEncoding,
121 offset_encoding: helix_lsp::OffsetEncoding,
121 trigger_offset: usize,
122 trigger_offset: usize,
122 include_placeholder: bool,
123 include_placeholder: bool,
124+ replace_mode: bool,
123 ) -> Transaction {
125 ) -> Transaction {
124 use helix_lsp::snippet;
126 use helix_lsp::snippet;
125 let selection = doc.selection(view_id);
127 let selection = doc.selection(view_id);
126 let text = doc.text().slice(..);
128 let text = doc.text().slice(..);
127 let primary_cursor = selection.primary().cursor(text);
129 let primary_cursor = selection.primary().cursor(text);
128
130
129− let (start_offset, end_offset, new_text) = if let Some(edit) = &item.text_edit {
131+ let (edit_offset, new_text) = if let Some(edit) = &item.text_edit {
130 let edit = match edit {
132 let edit = match edit {
131 lsp::CompletionTextEdit::Edit(edit) => edit.clone(),
133 lsp::CompletionTextEdit::Edit(edit) => edit.clone(),
132 lsp::CompletionTextEdit::InsertAndReplace(item) => {
134 lsp::CompletionTextEdit::InsertAndReplace(item) => {
133− // TODO: support using "insert" instead of "replace" via user config
135+ let range = if replace_mode {
136+ item.replace
137+ } else {
138+ item.insert
139+ };
134− lsp::TextEdit::new(item.replace, item.new_text.clone())
140+ lsp::TextEdit::new(range, item.new_text.clone())
135 }
141 }
136 };
142 };
137
143
@@ -157,7 +163,7 @@ impl Completion {
157 // document changed (and not just the selection) then we will
163 // document changed (and not just the selection) then we will
158 // likely delete the wrong text (same if we applied an edit sent by the LS)
164 // likely delete the wrong text (same if we applied an edit sent by the LS)
159 debug_assert!(primary_cursor == trigger_offset);
165 debug_assert!(primary_cursor == trigger_offset);
160− (None, Some(0), new_text)
166+ (None, new_text)
161 };
167 };
162
168
163 if matches!(item.kind, Some(lsp::CompletionItemKind::SNIPPET))
169 if matches!(item.kind, Some(lsp::CompletionItemKind::SNIPPET))
@@ -170,8 +176,8 @@ impl Completion {
170 Ok(snippet) => util::generate_transaction_from_snippet(
176 Ok(snippet) => util::generate_transaction_from_snippet(
171 doc.text(),
177 doc.text(),
172 selection,
178 selection,
173− start_offset,
179+ edit_offset,
174− end_offset,
180+ replace_mode,
175 snippet,
181 snippet,
176 doc.line_ending.as_str(),
182 doc.line_ending.as_str(),
177 include_placeholder,
183 include_placeholder,
@@ -190,8 +196,8 @@ impl Completion {
190 util::generate_transaction_from_completion_edit(
196 util::generate_transaction_from_completion_edit(
191 doc.text(),
197 doc.text(),
192 selection,
198 selection,
193− start_offset,
199+ edit_offset,
194− end_offset,
200+ replace_mode,
195 new_text,
201 new_text,
196 )
202 )
197 }
203 }
@@ -224,6 +230,7 @@ impl Completion {
224 offset_encoding,
230 offset_encoding,
225 trigger_offset,
231 trigger_offset,
226 true,
232 true,
233+ replace_mode,
227 );
234 );
228
235
229 // initialize a savepoint
236 // initialize a savepoint
@@ -245,6 +252,7 @@ impl Completion {
245 offset_encoding,
252 offset_encoding,
246 trigger_offset,
253 trigger_offset,
247 false,
254 false,
255+ replace_mode,
248 );
256 );
249
257
250 doc.apply(&transaction, view.id);
258 doc.apply(&transaction, view.id);
helix-view/src/editor.rs+4
@@ -251,6 +251,9 @@ pub struct Config {
251 )]
251 )]
252 pub idle_timeout: Duration,
252 pub idle_timeout: Duration,
253 pub completion_trigger_len: u8,
253 pub completion_trigger_len: u8,
254+ /// Whether to instruct the LSP to replace the entire word when applying a completion
255+ /// or to only insert new text
256+ pub completion_replace: bool,
254 /// Whether to display infoboxes. Defaults to true.
257 /// Whether to display infoboxes. Defaults to true.
255 pub auto_info: bool,
258 pub auto_info: bool,
256 pub file_picker: FilePickerConfig,
259 pub file_picker: FilePickerConfig,
@@ -738,6 +741,7 @@ impl Default for Config {
738 color_modes: false,
741 color_modes: false,
739 soft_wrap: SoftWrap::default(),
742 soft_wrap: SoftWrap::default(),
740 text_width: 80,
743 text_width: 80,
744+ completion_replace: false,
741 }
745 }
742 }
746 }
743}
747}