Add completions.lsp_insert_mode
setting to control what ranges are replaced when a completion is inserted (#27453)
This PR adds `completions.lsp_insert_mode` and effectively changes the default from `"replace"` to `"replace_suffix"`, which automatically detects whether to use the LSP `replace` range instead of `insert` range. `"replace_suffix"` was chosen as a default because it's more conservative than `"replace_subsequence"`, considering that deleting text is usually faster and less disruptive than having to rewrite a long replaced word. Fixes #27197 Fixes #23395 (again) Fixes #4816 (again) Release Notes: - Added new setting `completions.lsp_insert_mode` that changes what will be replaced when an LSP completion is accepted. The default is `"replace_suffix"`, but it accepts 4 values: `"insert"` for replacing only the text before the cursor, `"replace"` for replacing the whole text, `"replace_suffix"` that acts like `"replace"` when the text after the cursor is a suffix of the completion, and `"replace_subsequence"` that acts like `"replace"` when the text around your cursor is a subsequence of the completion (similiar to a fuzzy match). Check [the documentation](https://zed.dev/docs/configuring-zed#LSP-Insert-Mode) for more information. --------- Co-authored-by: João Marcos <marcospb19@hotmail.com> Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
This commit is contained in:
parent
108ae0b5b0
commit
07a77792c5
7 changed files with 467 additions and 13 deletions
|
@ -17,7 +17,9 @@ use gpui::{App, AsyncApp, Entity};
|
|||
use language::{
|
||||
Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind, OffsetRangeExt, PointUtf16,
|
||||
ToOffset, ToPointUtf16, Transaction, Unclipped,
|
||||
language_settings::{InlayHintKind, LanguageSettings, language_settings},
|
||||
language_settings::{
|
||||
AllLanguageSettings, InlayHintKind, LanguageSettings, LspInsertMode, language_settings,
|
||||
},
|
||||
point_from_lsp, point_to_lsp,
|
||||
proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
|
||||
range_from_lsp, range_to_lsp,
|
||||
|
@ -28,6 +30,7 @@ use lsp::{
|
|||
LanguageServer, LanguageServerId, LinkedEditingRangeServerCapabilities, OneOf, RenameOptions,
|
||||
ServerCapabilities,
|
||||
};
|
||||
use settings::Settings as _;
|
||||
use signature_help::{lsp_to_proto_signature, proto_to_lsp_signature};
|
||||
use std::{cmp::Reverse, mem, ops::Range, path::Path, sync::Arc};
|
||||
use text::{BufferId, LineEnding};
|
||||
|
@ -2085,7 +2088,7 @@ impl LspCommand for GetCompletions {
|
|||
.map(Arc::new);
|
||||
|
||||
let mut completion_edits = Vec::new();
|
||||
buffer.update(&mut cx, |buffer, _cx| {
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
let snapshot = buffer.snapshot();
|
||||
let clipped_position = buffer.clip_point_utf16(Unclipped(self.position), Bias::Left);
|
||||
|
||||
|
@ -2122,7 +2125,16 @@ impl LspCommand for GetCompletions {
|
|||
// If the language server provides a range to overwrite, then
|
||||
// check that the range is valid.
|
||||
Some(completion_text_edit) => {
|
||||
match parse_completion_text_edit(&completion_text_edit, &snapshot) {
|
||||
let completion_mode = AllLanguageSettings::get_global(cx)
|
||||
.defaults
|
||||
.completions
|
||||
.lsp_insert_mode;
|
||||
|
||||
match parse_completion_text_edit(
|
||||
&completion_text_edit,
|
||||
&snapshot,
|
||||
completion_mode,
|
||||
) {
|
||||
Some(edit) => edit,
|
||||
None => return false,
|
||||
}
|
||||
|
@ -2303,6 +2315,7 @@ impl LspCommand for GetCompletions {
|
|||
pub(crate) fn parse_completion_text_edit(
|
||||
edit: &lsp::CompletionTextEdit,
|
||||
snapshot: &BufferSnapshot,
|
||||
completion_mode: LspInsertMode,
|
||||
) -> Option<(Range<Anchor>, String)> {
|
||||
match edit {
|
||||
lsp::CompletionTextEdit::Edit(edit) => {
|
||||
|
@ -2321,7 +2334,55 @@ pub(crate) fn parse_completion_text_edit(
|
|||
}
|
||||
|
||||
lsp::CompletionTextEdit::InsertAndReplace(edit) => {
|
||||
let range = range_from_lsp(edit.replace);
|
||||
let replace = match completion_mode {
|
||||
LspInsertMode::Insert => false,
|
||||
LspInsertMode::Replace => true,
|
||||
LspInsertMode::ReplaceSubsequence => {
|
||||
let range_to_replace = range_from_lsp(edit.replace);
|
||||
|
||||
let start = snapshot.clip_point_utf16(range_to_replace.start, Bias::Left);
|
||||
let end = snapshot.clip_point_utf16(range_to_replace.end, Bias::Left);
|
||||
if start != range_to_replace.start.0 || end != range_to_replace.end.0 {
|
||||
false
|
||||
} else {
|
||||
let mut completion_text = edit.new_text.chars();
|
||||
|
||||
let mut text_to_replace = snapshot.chars_for_range(
|
||||
snapshot.anchor_before(start)..snapshot.anchor_after(end),
|
||||
);
|
||||
|
||||
// is `text_to_replace` a subsequence of `completion_text`
|
||||
text_to_replace.all(|needle_ch| {
|
||||
completion_text.any(|haystack_ch| haystack_ch == needle_ch)
|
||||
})
|
||||
}
|
||||
}
|
||||
LspInsertMode::ReplaceSuffix => {
|
||||
let range_after_cursor = lsp::Range {
|
||||
start: edit.insert.end,
|
||||
end: edit.replace.end,
|
||||
};
|
||||
let range_after_cursor = range_from_lsp(range_after_cursor);
|
||||
|
||||
let start = snapshot.clip_point_utf16(range_after_cursor.start, Bias::Left);
|
||||
let end = snapshot.clip_point_utf16(range_after_cursor.end, Bias::Left);
|
||||
if start != range_after_cursor.start.0 || end != range_after_cursor.end.0 {
|
||||
false
|
||||
} else {
|
||||
let text_after_cursor = snapshot
|
||||
.text_for_range(
|
||||
snapshot.anchor_before(start)..snapshot.anchor_after(end),
|
||||
)
|
||||
.collect::<String>();
|
||||
edit.new_text.ends_with(&text_after_cursor)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let range = range_from_lsp(match replace {
|
||||
true => edit.replace,
|
||||
false => edit.insert,
|
||||
});
|
||||
|
||||
let start = snapshot.clip_point_utf16(range.start, Bias::Left);
|
||||
let end = snapshot.clip_point_utf16(range.end, Bias::Left);
|
||||
|
|
|
@ -39,7 +39,8 @@ use language::{
|
|||
LanguageToolchainStore, LocalFile, LspAdapter, LspAdapterDelegate, Patch, PointUtf16,
|
||||
TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped,
|
||||
language_settings::{
|
||||
FormatOnSave, Formatter, LanguageSettings, SelectedFormatter, language_settings,
|
||||
AllLanguageSettings, FormatOnSave, Formatter, LanguageSettings, LspInsertMode,
|
||||
SelectedFormatter, language_settings,
|
||||
},
|
||||
point_to_lsp,
|
||||
proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
|
||||
|
@ -5151,6 +5152,7 @@ impl LspStore {
|
|||
&buffer_snapshot,
|
||||
completions.clone(),
|
||||
completion_index,
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
.log_err()
|
||||
|
@ -5184,6 +5186,7 @@ impl LspStore {
|
|||
snapshot: &BufferSnapshot,
|
||||
completions: Rc<RefCell<Box<[Completion]>>>,
|
||||
completion_index: usize,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<()> {
|
||||
let server_id = server.server_id();
|
||||
let can_resolve = server
|
||||
|
@ -5226,7 +5229,15 @@ impl LspStore {
|
|||
// language server we currently use that does update `text_edit` in `completionItem/resolve`
|
||||
// is `typescript-language-server` and they only update `text_edit.new_text`.
|
||||
// But we should not rely on that.
|
||||
let edit = parse_completion_text_edit(text_edit, snapshot);
|
||||
let completion_mode = cx
|
||||
.read_global(|_: &SettingsStore, cx| {
|
||||
AllLanguageSettings::get_global(cx)
|
||||
.defaults
|
||||
.completions
|
||||
.lsp_insert_mode
|
||||
})
|
||||
.unwrap_or(LspInsertMode::Insert);
|
||||
let edit = parse_completion_text_edit(text_edit, snapshot, completion_mode);
|
||||
|
||||
if let Some((old_range, mut new_text)) = edit {
|
||||
LineEnding::normalize(&mut new_text);
|
||||
|
@ -5482,6 +5493,7 @@ impl LspStore {
|
|||
&snapshot,
|
||||
completions.clone(),
|
||||
completion_index,
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
.context("resolving completion")?;
|
||||
|
@ -7723,7 +7735,16 @@ impl LspStore {
|
|||
})??;
|
||||
|
||||
if let Some(text_edit) = completion.text_edit.as_ref() {
|
||||
let edit = parse_completion_text_edit(text_edit, &buffer_snapshot);
|
||||
let completion_mode = cx
|
||||
.read_global(|_: &SettingsStore, cx| {
|
||||
AllLanguageSettings::get_global(cx)
|
||||
.defaults
|
||||
.completions
|
||||
.lsp_insert_mode
|
||||
})
|
||||
.unwrap_or(LspInsertMode::Insert);
|
||||
|
||||
let edit = parse_completion_text_edit(text_edit, &buffer_snapshot, completion_mode);
|
||||
|
||||
if let Some((old_range, mut text_edit_new_text)) = edit {
|
||||
LineEnding::normalize(&mut text_edit_new_text);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue