Add dedicated actions for LSP
completions insertion mode (#28121)
Adds actions so you can have customized keybindings for `insert` and `replace` modes. And add `shift-enter` as a default for `replace`, this will override the default setting `completions.lsp_insert_mode` which is set to `replace_suffix`, which tries to "smartly" decide whether to replace or insert based on the surrounding text. For those who come from VSCode, if you want to mimic their behavior, you only have to set `completions.lsp_insert_mode` to `insert`. If you want `tab` and `enter` to do different things, you need to remap them, here is an example: ```jsonc [ // ... { "context": "Editor && showing_completions", "bindings": { "enter": "editor::ConfirmCompletionInsert", "tab": "editor::ConfirmCompletionReplace" } }, ] ``` Closes #24577 - [x] Make LSP completion insertion mode decision in guest's machine (host is currently deciding it and not allowing guests to have their own setting for it) - [x] Add shift-enter as a hotkey for `replace` by default. - [x] Test actions. - [x] Respect the setting being specified per language, instead of using the "defaults". - [x] Move `insert_range` of `Completion` to the Lsp variant of `.source`. - [x] Fix broken default, forgotten after https://github.com/zed-industries/zed/pull/27453#pullrequestreview-2736906628, should be `replace_suffix` and not `insert`. Release Notes: - LSP completions: added actions `ConfirmCompletionInsert` and `ConfirmCompletionReplace` that control how completions are inserted, these override `completions.lsp_insert_mode`, by default, `shift-enter` triggers `ConfirmCompletionReplace` which replaces the whole word.
This commit is contained in:
parent
0459b1d303
commit
b15ee1b1cc
17 changed files with 400 additions and 207 deletions
|
@ -17,9 +17,7 @@ use gpui::{App, AsyncApp, Entity};
|
|||
use language::{
|
||||
Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind, OffsetRangeExt, PointUtf16,
|
||||
ToOffset, ToPointUtf16, Transaction, Unclipped,
|
||||
language_settings::{
|
||||
AllLanguageSettings, InlayHintKind, LanguageSettings, LspInsertMode, language_settings,
|
||||
},
|
||||
language_settings::{InlayHintKind, LanguageSettings, language_settings},
|
||||
point_from_lsp, point_to_lsp,
|
||||
proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
|
||||
range_from_lsp, range_to_lsp,
|
||||
|
@ -30,7 +28,6 @@ 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};
|
||||
|
@ -2161,7 +2158,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);
|
||||
|
||||
|
@ -2198,21 +2195,11 @@ impl LspCommand for GetCompletions {
|
|||
// If the language server provides a range to overwrite, then
|
||||
// check that the range is valid.
|
||||
Some(completion_text_edit) => {
|
||||
let completion_mode = AllLanguageSettings::get_global(cx)
|
||||
.defaults
|
||||
.completions
|
||||
.lsp_insert_mode;
|
||||
|
||||
match parse_completion_text_edit(
|
||||
&completion_text_edit,
|
||||
&snapshot,
|
||||
completion_mode,
|
||||
) {
|
||||
match parse_completion_text_edit(&completion_text_edit, &snapshot) {
|
||||
Some(edit) => edit,
|
||||
None => return false,
|
||||
}
|
||||
}
|
||||
|
||||
// If the language server does not provide a range, then infer
|
||||
// the range based on the syntax tree.
|
||||
None => {
|
||||
|
@ -2264,7 +2251,12 @@ impl LspCommand for GetCompletions {
|
|||
.as_ref()
|
||||
.unwrap_or(&lsp_completion.label)
|
||||
.clone();
|
||||
(range, text)
|
||||
|
||||
ParsedCompletionEdit {
|
||||
replace_range: range,
|
||||
insert_range: None,
|
||||
new_text: text,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -2280,8 +2272,8 @@ impl LspCommand for GetCompletions {
|
|||
Ok(completions
|
||||
.into_iter()
|
||||
.zip(completion_edits)
|
||||
.map(|(mut lsp_completion, (old_range, mut new_text))| {
|
||||
LineEnding::normalize(&mut new_text);
|
||||
.map(|(mut lsp_completion, mut edit)| {
|
||||
LineEnding::normalize(&mut edit.new_text);
|
||||
if lsp_completion.data.is_none() {
|
||||
if let Some(default_data) = lsp_defaults
|
||||
.as_ref()
|
||||
|
@ -2293,9 +2285,10 @@ impl LspCommand for GetCompletions {
|
|||
}
|
||||
}
|
||||
CoreCompletion {
|
||||
old_range,
|
||||
new_text,
|
||||
replace_range: edit.replace_range,
|
||||
new_text: edit.new_text,
|
||||
source: CompletionSource::Lsp {
|
||||
insert_range: edit.insert_range,
|
||||
server_id,
|
||||
lsp_completion: Box::new(lsp_completion),
|
||||
lsp_defaults: lsp_defaults.clone(),
|
||||
|
@ -2385,91 +2378,53 @@ impl LspCommand for GetCompletions {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct ParsedCompletionEdit {
|
||||
pub replace_range: Range<Anchor>,
|
||||
pub insert_range: Option<Range<Anchor>>,
|
||||
pub new_text: String,
|
||||
}
|
||||
|
||||
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) => {
|
||||
let range = range_from_lsp(edit.range);
|
||||
let start = snapshot.clip_point_utf16(range.start, Bias::Left);
|
||||
let end = snapshot.clip_point_utf16(range.end, Bias::Left);
|
||||
if start != range.start.0 || end != range.end.0 {
|
||||
log::info!("completion out of expected range");
|
||||
None
|
||||
} else {
|
||||
Some((
|
||||
snapshot.anchor_before(start)..snapshot.anchor_after(end),
|
||||
edit.new_text.clone(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
) -> Option<ParsedCompletionEdit> {
|
||||
let (replace_range, insert_range, new_text) = match edit {
|
||||
lsp::CompletionTextEdit::Edit(edit) => (edit.range, None, &edit.new_text),
|
||||
lsp::CompletionTextEdit::InsertAndReplace(edit) => {
|
||||
let replace = match completion_mode {
|
||||
LspInsertMode::Insert => false,
|
||||
LspInsertMode::Replace => true,
|
||||
LspInsertMode::ReplaceSubsequence => {
|
||||
let range_to_replace = range_from_lsp(edit.replace);
|
||||
(edit.replace, Some(edit.insert), &edit.new_text)
|
||||
}
|
||||
};
|
||||
|
||||
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 replace_range = {
|
||||
let range = range_from_lsp(replace_range);
|
||||
let start = snapshot.clip_point_utf16(range.start, Bias::Left);
|
||||
let end = snapshot.clip_point_utf16(range.end, Bias::Left);
|
||||
if start != range.start.0 || end != range.end.0 {
|
||||
log::info!("completion out of expected range");
|
||||
return None;
|
||||
}
|
||||
snapshot.anchor_before(start)..snapshot.anchor_after(end)
|
||||
};
|
||||
|
||||
let insert_range = match insert_range {
|
||||
None => None,
|
||||
Some(insert_range) => {
|
||||
let range = range_from_lsp(insert_range);
|
||||
let start = snapshot.clip_point_utf16(range.start, Bias::Left);
|
||||
let end = snapshot.clip_point_utf16(range.end, Bias::Left);
|
||||
if start != range.start.0 || end != range.end.0 {
|
||||
log::info!("completion out of expected range");
|
||||
None
|
||||
} else {
|
||||
Some((
|
||||
snapshot.anchor_before(start)..snapshot.anchor_after(end),
|
||||
edit.new_text.clone(),
|
||||
))
|
||||
log::info!("completion (insert) out of expected range");
|
||||
return None;
|
||||
}
|
||||
Some(snapshot.anchor_before(start)..snapshot.anchor_after(end))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Some(ParsedCompletionEdit {
|
||||
insert_range: insert_range,
|
||||
replace_range: replace_range,
|
||||
new_text: new_text.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue