Avoid modifying the LSP message before resolving it (#26347)
Closes https://github.com/zed-industries/zed/issues/21277
To the left is current Zed, right is the improved version.
3rd message, from Zed, to resolve the item, does not have `textEdit` on
the right side, and has one on the left.
Seems to not influence the end result though, but at least Zed behaves
more appropriate now.
<img width="1727" alt="image"
src="https://github.com/user-attachments/assets/ca1236fd-9ce2-41ba-88fe-1f3178cdcbde"
/>
Instead of modifying the original LSP completion item, store completion
list defaults and apply them when the item is requested (except `data`
defaults, needed for resolve).
Now, the only place that can modify the completion items is this method,
and Python impl seems to be the one doing it:
ca9c3af56f/crates/languages/src/python.rs (L182-L204)
Seems ok to leave untouched for now.
Release Notes:
- Fixed LSP completion items modified before resolve request
This commit is contained in:
parent
6de3ac3e17
commit
8a7a78fafb
7 changed files with 233 additions and 158 deletions
|
@ -500,7 +500,7 @@ impl CompletionsMenu {
|
||||||
highlight.font_weight = None;
|
highlight.font_weight = None;
|
||||||
if completion
|
if completion
|
||||||
.source
|
.source
|
||||||
.lsp_completion()
|
.lsp_completion(false)
|
||||||
.and_then(|lsp_completion| lsp_completion.deprecated)
|
.and_then(|lsp_completion| lsp_completion.deprecated)
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
{
|
{
|
||||||
|
@ -711,10 +711,12 @@ impl CompletionsMenu {
|
||||||
|
|
||||||
let completion = &completions[mat.candidate_id];
|
let completion = &completions[mat.candidate_id];
|
||||||
let sort_key = completion.sort_key();
|
let sort_key = completion.sort_key();
|
||||||
let sort_text = completion
|
let sort_text =
|
||||||
.source
|
if let CompletionSource::Lsp { lsp_completion, .. } = &completion.source {
|
||||||
.lsp_completion()
|
lsp_completion.sort_text.as_deref()
|
||||||
.and_then(|lsp_completion| lsp_completion.sort_text.as_deref());
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
let score = Reverse(OrderedFloat(mat.score));
|
let score = Reverse(OrderedFloat(mat.score));
|
||||||
|
|
||||||
if mat.score >= 0.2 {
|
if mat.score >= 0.2 {
|
||||||
|
|
|
@ -17017,6 +17017,7 @@ fn snippet_completions(
|
||||||
sort_text: Some(char::MAX.to_string()),
|
sort_text: Some(char::MAX.to_string()),
|
||||||
..lsp::CompletionItem::default()
|
..lsp::CompletionItem::default()
|
||||||
}),
|
}),
|
||||||
|
lsp_defaults: None,
|
||||||
},
|
},
|
||||||
label: CodeLabel {
|
label: CodeLabel {
|
||||||
text: matching_prefix.clone(),
|
text: matching_prefix.clone(),
|
||||||
|
|
|
@ -12334,24 +12334,6 @@ async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext)
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let item_0_out = lsp::CompletionItem {
|
|
||||||
commit_characters: Some(default_commit_characters.clone()),
|
|
||||||
insert_text_format: Some(default_insert_text_format),
|
|
||||||
..item_0
|
|
||||||
};
|
|
||||||
let items_out = iter::once(item_0_out)
|
|
||||||
.chain(items[1..].iter().map(|item| lsp::CompletionItem {
|
|
||||||
commit_characters: Some(default_commit_characters.clone()),
|
|
||||||
data: Some(default_data.clone()),
|
|
||||||
insert_text_mode: Some(default_insert_text_mode),
|
|
||||||
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
|
||||||
range: default_edit_range,
|
|
||||||
new_text: item.label.clone(),
|
|
||||||
})),
|
|
||||||
..item.clone()
|
|
||||||
}))
|
|
||||||
.collect::<Vec<lsp::CompletionItem>>();
|
|
||||||
|
|
||||||
let mut cx = EditorLspTestContext::new_rust(
|
let mut cx = EditorLspTestContext::new_rust(
|
||||||
lsp::ServerCapabilities {
|
lsp::ServerCapabilities {
|
||||||
completion_provider: Some(lsp::CompletionOptions {
|
completion_provider: Some(lsp::CompletionOptions {
|
||||||
|
@ -12370,10 +12352,11 @@ async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext)
|
||||||
|
|
||||||
let completion_data = default_data.clone();
|
let completion_data = default_data.clone();
|
||||||
let completion_characters = default_commit_characters.clone();
|
let completion_characters = default_commit_characters.clone();
|
||||||
|
let completion_items = items.clone();
|
||||||
cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
|
cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
|
||||||
let default_data = completion_data.clone();
|
let default_data = completion_data.clone();
|
||||||
let default_commit_characters = completion_characters.clone();
|
let default_commit_characters = completion_characters.clone();
|
||||||
let items = items.clone();
|
let items = completion_items.clone();
|
||||||
async move {
|
async move {
|
||||||
Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
|
Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
|
||||||
items,
|
items,
|
||||||
|
@ -12422,7 +12405,7 @@ async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext)
|
||||||
.iter()
|
.iter()
|
||||||
.map(|mat| mat.string.clone())
|
.map(|mat| mat.string.clone())
|
||||||
.collect::<Vec<String>>(),
|
.collect::<Vec<String>>(),
|
||||||
items_out
|
items
|
||||||
.iter()
|
.iter()
|
||||||
.map(|completion| completion.label.clone())
|
.map(|completion| completion.label.clone())
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
|
@ -12435,14 +12418,18 @@ async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext)
|
||||||
// with 4 from the end.
|
// with 4 from the end.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
*resolved_items.lock(),
|
*resolved_items.lock(),
|
||||||
[
|
[&items[0..16], &items[items.len() - 4..items.len()]]
|
||||||
&items_out[0..16],
|
.concat()
|
||||||
&items_out[items_out.len() - 4..items_out.len()]
|
.iter()
|
||||||
]
|
.cloned()
|
||||||
.concat()
|
.map(|mut item| {
|
||||||
.iter()
|
if item.data.is_none() {
|
||||||
.cloned()
|
item.data = Some(default_data.clone());
|
||||||
.collect::<Vec<lsp::CompletionItem>>()
|
}
|
||||||
|
item
|
||||||
|
})
|
||||||
|
.collect::<Vec<lsp::CompletionItem>>(),
|
||||||
|
"Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
|
||||||
);
|
);
|
||||||
resolved_items.lock().clear();
|
resolved_items.lock().clear();
|
||||||
|
|
||||||
|
@ -12453,9 +12440,15 @@ async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext)
|
||||||
// Completions that have already been resolved are skipped.
|
// Completions that have already been resolved are skipped.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
*resolved_items.lock(),
|
*resolved_items.lock(),
|
||||||
items_out[items_out.len() - 16..items_out.len() - 4]
|
items[items.len() - 16..items.len() - 4]
|
||||||
.iter()
|
.iter()
|
||||||
.cloned()
|
.cloned()
|
||||||
|
.map(|mut item| {
|
||||||
|
if item.data.is_none() {
|
||||||
|
item.data = Some(default_data.clone());
|
||||||
|
}
|
||||||
|
item
|
||||||
|
})
|
||||||
.collect::<Vec<lsp::CompletionItem>>()
|
.collect::<Vec<lsp::CompletionItem>>()
|
||||||
);
|
);
|
||||||
resolved_items.lock().clear();
|
resolved_items.lock().clear();
|
||||||
|
|
|
@ -1847,7 +1847,6 @@ impl LspCommand for GetCompletions {
|
||||||
let mut completions = if let Some(completions) = completions {
|
let mut completions = if let Some(completions) = completions {
|
||||||
match completions {
|
match completions {
|
||||||
lsp::CompletionResponse::Array(completions) => completions,
|
lsp::CompletionResponse::Array(completions) => completions,
|
||||||
|
|
||||||
lsp::CompletionResponse::List(mut list) => {
|
lsp::CompletionResponse::List(mut list) => {
|
||||||
let items = std::mem::take(&mut list.items);
|
let items = std::mem::take(&mut list.items);
|
||||||
response_list = Some(list);
|
response_list = Some(list);
|
||||||
|
@ -1855,74 +1854,19 @@ impl LspCommand for GetCompletions {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Default::default()
|
Vec::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
let language_server_adapter = lsp_store
|
let language_server_adapter = lsp_store
|
||||||
.update(&mut cx, |lsp_store, _| {
|
.update(&mut cx, |lsp_store, _| {
|
||||||
lsp_store.language_server_adapter_for_id(server_id)
|
lsp_store.language_server_adapter_for_id(server_id)
|
||||||
})?
|
})?
|
||||||
.ok_or_else(|| anyhow!("no such language server"))?;
|
.with_context(|| format!("no language server with id {server_id}"))?;
|
||||||
|
|
||||||
let item_defaults = response_list
|
let lsp_defaults = response_list
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|list| list.item_defaults.as_ref());
|
.and_then(|list| list.item_defaults.clone())
|
||||||
|
.map(Arc::new);
|
||||||
if let Some(item_defaults) = item_defaults {
|
|
||||||
let default_data = item_defaults.data.as_ref();
|
|
||||||
let default_commit_characters = item_defaults.commit_characters.as_ref();
|
|
||||||
let default_edit_range = item_defaults.edit_range.as_ref();
|
|
||||||
let default_insert_text_format = item_defaults.insert_text_format.as_ref();
|
|
||||||
let default_insert_text_mode = item_defaults.insert_text_mode.as_ref();
|
|
||||||
|
|
||||||
if default_data.is_some()
|
|
||||||
|| default_commit_characters.is_some()
|
|
||||||
|| default_edit_range.is_some()
|
|
||||||
|| default_insert_text_format.is_some()
|
|
||||||
|| default_insert_text_mode.is_some()
|
|
||||||
{
|
|
||||||
for item in completions.iter_mut() {
|
|
||||||
if item.data.is_none() && default_data.is_some() {
|
|
||||||
item.data = default_data.cloned()
|
|
||||||
}
|
|
||||||
if item.commit_characters.is_none() && default_commit_characters.is_some() {
|
|
||||||
item.commit_characters = default_commit_characters.cloned()
|
|
||||||
}
|
|
||||||
if item.text_edit.is_none() {
|
|
||||||
if let Some(default_edit_range) = default_edit_range {
|
|
||||||
match default_edit_range {
|
|
||||||
CompletionListItemDefaultsEditRange::Range(range) => {
|
|
||||||
item.text_edit =
|
|
||||||
Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
|
||||||
range: *range,
|
|
||||||
new_text: item.label.clone(),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
CompletionListItemDefaultsEditRange::InsertAndReplace {
|
|
||||||
insert,
|
|
||||||
replace,
|
|
||||||
} => {
|
|
||||||
item.text_edit =
|
|
||||||
Some(lsp::CompletionTextEdit::InsertAndReplace(
|
|
||||||
lsp::InsertReplaceEdit {
|
|
||||||
new_text: item.label.clone(),
|
|
||||||
insert: *insert,
|
|
||||||
replace: *replace,
|
|
||||||
},
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if item.insert_text_format.is_none() && default_insert_text_format.is_some() {
|
|
||||||
item.insert_text_format = default_insert_text_format.cloned()
|
|
||||||
}
|
|
||||||
if item.insert_text_mode.is_none() && default_insert_text_mode.is_some() {
|
|
||||||
item.insert_text_mode = default_insert_text_mode.cloned()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut completion_edits = Vec::new();
|
let mut completion_edits = Vec::new();
|
||||||
buffer.update(&mut cx, |buffer, _cx| {
|
buffer.update(&mut cx, |buffer, _cx| {
|
||||||
|
@ -1930,12 +1874,34 @@ impl LspCommand for GetCompletions {
|
||||||
let clipped_position = buffer.clip_point_utf16(Unclipped(self.position), Bias::Left);
|
let clipped_position = buffer.clip_point_utf16(Unclipped(self.position), Bias::Left);
|
||||||
|
|
||||||
let mut range_for_token = None;
|
let mut range_for_token = None;
|
||||||
completions.retain_mut(|lsp_completion| {
|
completions.retain(|lsp_completion| {
|
||||||
let edit = match lsp_completion.text_edit.as_ref() {
|
let lsp_edit = lsp_completion.text_edit.clone().or_else(|| {
|
||||||
|
let default_text_edit = lsp_defaults.as_deref()?.edit_range.as_ref()?;
|
||||||
|
match default_text_edit {
|
||||||
|
CompletionListItemDefaultsEditRange::Range(range) => {
|
||||||
|
Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||||
|
range: *range,
|
||||||
|
new_text: lsp_completion.label.clone(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
CompletionListItemDefaultsEditRange::InsertAndReplace {
|
||||||
|
insert,
|
||||||
|
replace,
|
||||||
|
} => Some(lsp::CompletionTextEdit::InsertAndReplace(
|
||||||
|
lsp::InsertReplaceEdit {
|
||||||
|
new_text: lsp_completion.label.clone(),
|
||||||
|
insert: *insert,
|
||||||
|
replace: *replace,
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let edit = match lsp_edit {
|
||||||
// If the language server provides a range to overwrite, then
|
// If the language server provides a range to overwrite, then
|
||||||
// check that the range is valid.
|
// check that the range is valid.
|
||||||
Some(completion_text_edit) => {
|
Some(completion_text_edit) => {
|
||||||
match parse_completion_text_edit(completion_text_edit, &snapshot) {
|
match parse_completion_text_edit(&completion_text_edit, &snapshot) {
|
||||||
Some(edit) => edit,
|
Some(edit) => edit,
|
||||||
None => return false,
|
None => return false,
|
||||||
}
|
}
|
||||||
|
@ -1949,14 +1915,15 @@ impl LspCommand for GetCompletions {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let default_edit_range = response_list
|
let default_edit_range = lsp_defaults.as_ref().and_then(|lsp_defaults| {
|
||||||
.as_ref()
|
lsp_defaults
|
||||||
.and_then(|list| list.item_defaults.as_ref())
|
.edit_range
|
||||||
.and_then(|defaults| defaults.edit_range.as_ref())
|
.as_ref()
|
||||||
.and_then(|range| match range {
|
.and_then(|range| match range {
|
||||||
CompletionListItemDefaultsEditRange::Range(r) => Some(r),
|
CompletionListItemDefaultsEditRange::Range(r) => Some(r),
|
||||||
_ => None,
|
_ => None,
|
||||||
});
|
})
|
||||||
|
});
|
||||||
|
|
||||||
let range = if let Some(range) = default_edit_range {
|
let range = if let Some(range) = default_edit_range {
|
||||||
let range = range_from_lsp(*range);
|
let range = range_from_lsp(*range);
|
||||||
|
@ -2006,14 +1973,25 @@ impl LspCommand for GetCompletions {
|
||||||
Ok(completions
|
Ok(completions
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.zip(completion_edits)
|
.zip(completion_edits)
|
||||||
.map(|(lsp_completion, (old_range, mut new_text))| {
|
.map(|(mut lsp_completion, (old_range, mut new_text))| {
|
||||||
LineEnding::normalize(&mut new_text);
|
LineEnding::normalize(&mut new_text);
|
||||||
|
if lsp_completion.data.is_none() {
|
||||||
|
if let Some(default_data) = lsp_defaults
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|item_defaults| item_defaults.data.clone())
|
||||||
|
{
|
||||||
|
// Servers (e.g. JDTLS) prefer unchanged completions, when resolving the items later,
|
||||||
|
// so we do not insert the defaults here, but `data` is needed for resolving, so this is an exception.
|
||||||
|
lsp_completion.data = Some(default_data);
|
||||||
|
}
|
||||||
|
}
|
||||||
CoreCompletion {
|
CoreCompletion {
|
||||||
old_range,
|
old_range,
|
||||||
new_text,
|
new_text,
|
||||||
source: CompletionSource::Lsp {
|
source: CompletionSource::Lsp {
|
||||||
server_id,
|
server_id,
|
||||||
lsp_completion: Box::new(lsp_completion),
|
lsp_completion: Box::new(lsp_completion),
|
||||||
|
lsp_defaults: lsp_defaults.clone(),
|
||||||
resolved: false,
|
resolved: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,10 +49,9 @@ use lsp::{
|
||||||
notification::DidRenameFiles, CodeActionKind, CompletionContext, DiagnosticSeverity,
|
notification::DidRenameFiles, CodeActionKind, CompletionContext, DiagnosticSeverity,
|
||||||
DiagnosticTag, DidChangeWatchedFilesRegistrationOptions, Edit, FileOperationFilter,
|
DiagnosticTag, DidChangeWatchedFilesRegistrationOptions, Edit, FileOperationFilter,
|
||||||
FileOperationPatternKind, FileOperationRegistrationOptions, FileRename, FileSystemWatcher,
|
FileOperationPatternKind, FileOperationRegistrationOptions, FileRename, FileSystemWatcher,
|
||||||
InsertTextFormat, LanguageServer, LanguageServerBinary, LanguageServerBinaryOptions,
|
LanguageServer, LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerId,
|
||||||
LanguageServerId, LanguageServerName, LspRequestFuture, MessageActionItem, MessageType, OneOf,
|
LanguageServerName, LspRequestFuture, MessageActionItem, MessageType, OneOf, RenameFilesParams,
|
||||||
RenameFilesParams, SymbolKind, TextEdit, WillRenameFiles, WorkDoneProgressCancelParams,
|
SymbolKind, TextEdit, WillRenameFiles, WorkDoneProgressCancelParams, WorkspaceFolder,
|
||||||
WorkspaceFolder,
|
|
||||||
};
|
};
|
||||||
use node_runtime::read_package_installed_version;
|
use node_runtime::read_package_installed_version;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
@ -70,6 +69,7 @@ use smol::channel::Sender;
|
||||||
use snippet::Snippet;
|
use snippet::Snippet;
|
||||||
use std::{
|
use std::{
|
||||||
any::Any,
|
any::Any,
|
||||||
|
borrow::Cow,
|
||||||
cell::RefCell,
|
cell::RefCell,
|
||||||
cmp::Ordering,
|
cmp::Ordering,
|
||||||
convert::TryInto,
|
convert::TryInto,
|
||||||
|
@ -4475,6 +4475,7 @@ impl LspStore {
|
||||||
completions: Rc<RefCell<Box<[Completion]>>>,
|
completions: Rc<RefCell<Box<[Completion]>>>,
|
||||||
completion_index: usize,
|
completion_index: usize,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
let server_id = server.server_id();
|
||||||
let can_resolve = server
|
let can_resolve = server
|
||||||
.capabilities()
|
.capabilities()
|
||||||
.completion_provider
|
.completion_provider
|
||||||
|
@ -4491,19 +4492,24 @@ impl LspStore {
|
||||||
CompletionSource::Lsp {
|
CompletionSource::Lsp {
|
||||||
lsp_completion,
|
lsp_completion,
|
||||||
resolved,
|
resolved,
|
||||||
|
server_id: completion_server_id,
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
if *resolved {
|
if *resolved {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
anyhow::ensure!(
|
||||||
|
server_id == *completion_server_id,
|
||||||
|
"server_id mismatch, querying completion resolve for {server_id} but completion server id is {completion_server_id}"
|
||||||
|
);
|
||||||
server.request::<lsp::request::ResolveCompletionItem>(*lsp_completion.clone())
|
server.request::<lsp::request::ResolveCompletionItem>(*lsp_completion.clone())
|
||||||
}
|
}
|
||||||
CompletionSource::Custom => return Ok(()),
|
CompletionSource::Custom => return Ok(()),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let completion_item = request.await?;
|
let resolved_completion = request.await?;
|
||||||
|
|
||||||
if let Some(text_edit) = completion_item.text_edit.as_ref() {
|
if let Some(text_edit) = resolved_completion.text_edit.as_ref() {
|
||||||
// Technically we don't have to parse the whole `text_edit`, since the only
|
// Technically we don't have to parse the whole `text_edit`, since the only
|
||||||
// language server we currently use that does update `text_edit` in `completionItem/resolve`
|
// 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`.
|
// is `typescript-language-server` and they only update `text_edit.new_text`.
|
||||||
|
@ -4520,24 +4526,26 @@ impl LspStore {
|
||||||
completion.old_range = old_range;
|
completion.old_range = old_range;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if completion_item.insert_text_format == Some(InsertTextFormat::SNIPPET) {
|
|
||||||
// vtsls might change the type of completion after resolution.
|
|
||||||
let mut completions = completions.borrow_mut();
|
|
||||||
let completion = &mut completions[completion_index];
|
|
||||||
if let Some(lsp_completion) = completion.source.lsp_completion_mut() {
|
|
||||||
if completion_item.insert_text_format != lsp_completion.insert_text_format {
|
|
||||||
lsp_completion.insert_text_format = completion_item.insert_text_format;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut completions = completions.borrow_mut();
|
let mut completions = completions.borrow_mut();
|
||||||
let completion = &mut completions[completion_index];
|
let completion = &mut completions[completion_index];
|
||||||
completion.source = CompletionSource::Lsp {
|
if let CompletionSource::Lsp {
|
||||||
lsp_completion: Box::new(completion_item),
|
lsp_completion,
|
||||||
resolved: true,
|
resolved,
|
||||||
server_id: server.server_id(),
|
server_id: completion_server_id,
|
||||||
};
|
..
|
||||||
|
} = &mut completion.source
|
||||||
|
{
|
||||||
|
if *resolved {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
anyhow::ensure!(
|
||||||
|
server_id == *completion_server_id,
|
||||||
|
"server_id mismatch, applying completion resolve for {server_id} but completion server id is {completion_server_id}"
|
||||||
|
);
|
||||||
|
*lsp_completion = Box::new(resolved_completion);
|
||||||
|
*resolved = true;
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4549,8 +4557,8 @@ impl LspStore {
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let completion_item = completions.borrow()[completion_index]
|
let completion_item = completions.borrow()[completion_index]
|
||||||
.source
|
.source
|
||||||
.lsp_completion()
|
.lsp_completion(true)
|
||||||
.cloned();
|
.map(Cow::into_owned);
|
||||||
if let Some(lsp_documentation) = completion_item
|
if let Some(lsp_documentation) = completion_item
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|completion_item| completion_item.documentation.clone())
|
.and_then(|completion_item| completion_item.documentation.clone())
|
||||||
|
@ -4626,8 +4634,13 @@ impl LspStore {
|
||||||
CompletionSource::Lsp {
|
CompletionSource::Lsp {
|
||||||
lsp_completion,
|
lsp_completion,
|
||||||
resolved,
|
resolved,
|
||||||
|
server_id: completion_server_id,
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
|
anyhow::ensure!(
|
||||||
|
server_id == *completion_server_id,
|
||||||
|
"remote server_id mismatch, querying completion resolve for {server_id} but completion server id is {completion_server_id}"
|
||||||
|
);
|
||||||
if *resolved {
|
if *resolved {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
@ -4647,7 +4660,7 @@ impl LspStore {
|
||||||
.request(request)
|
.request(request)
|
||||||
.await
|
.await
|
||||||
.context("completion documentation resolve proto request")?;
|
.context("completion documentation resolve proto request")?;
|
||||||
let lsp_completion = serde_json::from_slice(&response.lsp_completion)?;
|
let resolved_lsp_completion = serde_json::from_slice(&response.lsp_completion)?;
|
||||||
|
|
||||||
let documentation = if response.documentation.is_empty() {
|
let documentation = if response.documentation.is_empty() {
|
||||||
CompletionDocumentation::Undocumented
|
CompletionDocumentation::Undocumented
|
||||||
|
@ -4662,11 +4675,23 @@ impl LspStore {
|
||||||
let mut completions = completions.borrow_mut();
|
let mut completions = completions.borrow_mut();
|
||||||
let completion = &mut completions[completion_index];
|
let completion = &mut completions[completion_index];
|
||||||
completion.documentation = Some(documentation);
|
completion.documentation = Some(documentation);
|
||||||
completion.source = CompletionSource::Lsp {
|
if let CompletionSource::Lsp {
|
||||||
server_id,
|
|
||||||
lsp_completion,
|
lsp_completion,
|
||||||
resolved: true,
|
resolved,
|
||||||
};
|
server_id: completion_server_id,
|
||||||
|
lsp_defaults: _,
|
||||||
|
} = &mut completion.source
|
||||||
|
{
|
||||||
|
if *resolved {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
anyhow::ensure!(
|
||||||
|
server_id == *completion_server_id,
|
||||||
|
"remote server_id mismatch, applying completion resolve for {server_id} but completion server id is {completion_server_id}"
|
||||||
|
);
|
||||||
|
*lsp_completion = Box::new(resolved_lsp_completion);
|
||||||
|
*resolved = true;
|
||||||
|
}
|
||||||
|
|
||||||
let old_range = response
|
let old_range = response
|
||||||
.old_start
|
.old_start
|
||||||
|
@ -4750,7 +4775,7 @@ impl LspStore {
|
||||||
let completion = completions.borrow()[completion_index].clone();
|
let completion = completions.borrow()[completion_index].clone();
|
||||||
let additional_text_edits = completion
|
let additional_text_edits = completion
|
||||||
.source
|
.source
|
||||||
.lsp_completion()
|
.lsp_completion(true)
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|lsp_completion| lsp_completion.additional_text_edits.clone());
|
.and_then(|lsp_completion| lsp_completion.additional_text_edits.clone());
|
||||||
if let Some(edits) = additional_text_edits {
|
if let Some(edits) = additional_text_edits {
|
||||||
|
@ -8153,21 +8178,26 @@ impl LspStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn serialize_completion(completion: &CoreCompletion) -> proto::Completion {
|
pub(crate) fn serialize_completion(completion: &CoreCompletion) -> proto::Completion {
|
||||||
let (source, server_id, lsp_completion, resolved) = match &completion.source {
|
let (source, server_id, lsp_completion, lsp_defaults, resolved) = match &completion.source {
|
||||||
CompletionSource::Lsp {
|
CompletionSource::Lsp {
|
||||||
server_id,
|
server_id,
|
||||||
lsp_completion,
|
lsp_completion,
|
||||||
|
lsp_defaults,
|
||||||
resolved,
|
resolved,
|
||||||
} => (
|
} => (
|
||||||
proto::completion::Source::Lsp as i32,
|
proto::completion::Source::Lsp as i32,
|
||||||
server_id.0 as u64,
|
server_id.0 as u64,
|
||||||
serde_json::to_vec(lsp_completion).unwrap(),
|
serde_json::to_vec(lsp_completion).unwrap(),
|
||||||
|
lsp_defaults
|
||||||
|
.as_deref()
|
||||||
|
.map(|lsp_defaults| serde_json::to_vec(lsp_defaults).unwrap()),
|
||||||
*resolved,
|
*resolved,
|
||||||
),
|
),
|
||||||
CompletionSource::Custom => (
|
CompletionSource::Custom => (
|
||||||
proto::completion::Source::Custom as i32,
|
proto::completion::Source::Custom as i32,
|
||||||
0,
|
0,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
|
None,
|
||||||
true,
|
true,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
@ -8178,6 +8208,7 @@ impl LspStore {
|
||||||
new_text: completion.new_text.clone(),
|
new_text: completion.new_text.clone(),
|
||||||
server_id,
|
server_id,
|
||||||
lsp_completion,
|
lsp_completion,
|
||||||
|
lsp_defaults,
|
||||||
resolved,
|
resolved,
|
||||||
source,
|
source,
|
||||||
}
|
}
|
||||||
|
@ -8200,6 +8231,11 @@ impl LspStore {
|
||||||
Some(proto::completion::Source::Lsp) => CompletionSource::Lsp {
|
Some(proto::completion::Source::Lsp) => CompletionSource::Lsp {
|
||||||
server_id: LanguageServerId::from_proto(completion.server_id),
|
server_id: LanguageServerId::from_proto(completion.server_id),
|
||||||
lsp_completion: serde_json::from_slice(&completion.lsp_completion)?,
|
lsp_completion: serde_json::from_slice(&completion.lsp_completion)?,
|
||||||
|
lsp_defaults: completion
|
||||||
|
.lsp_defaults
|
||||||
|
.as_deref()
|
||||||
|
.map(serde_json::from_slice)
|
||||||
|
.transpose()?,
|
||||||
resolved: completion.resolved,
|
resolved: completion.resolved,
|
||||||
},
|
},
|
||||||
_ => anyhow::bail!("Unexpected completion source {}", completion.source),
|
_ => anyhow::bail!("Unexpected completion source {}", completion.source),
|
||||||
|
@ -8288,8 +8324,8 @@ async fn populate_labels_for_completions(
|
||||||
let lsp_completions = new_completions
|
let lsp_completions = new_completions
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|new_completion| {
|
.filter_map(|new_completion| {
|
||||||
if let CompletionSource::Lsp { lsp_completion, .. } = &new_completion.source {
|
if let Some(lsp_completion) = new_completion.source.lsp_completion(true) {
|
||||||
Some(*lsp_completion.clone())
|
Some(lsp_completion.into_owned())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
@ -8309,8 +8345,8 @@ async fn populate_labels_for_completions(
|
||||||
.fuse();
|
.fuse();
|
||||||
|
|
||||||
for completion in new_completions {
|
for completion in new_completions {
|
||||||
match &completion.source {
|
match completion.source.lsp_completion(true) {
|
||||||
CompletionSource::Lsp { lsp_completion, .. } => {
|
Some(lsp_completion) => {
|
||||||
let documentation = if let Some(docs) = lsp_completion.documentation.clone() {
|
let documentation = if let Some(docs) = lsp_completion.documentation.clone() {
|
||||||
Some(docs.into())
|
Some(docs.into())
|
||||||
} else {
|
} else {
|
||||||
|
@ -8328,9 +8364,9 @@ async fn populate_labels_for_completions(
|
||||||
new_text: completion.new_text,
|
new_text: completion.new_text,
|
||||||
source: completion.source,
|
source: completion.source,
|
||||||
confirm: None,
|
confirm: None,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
CompletionSource::Custom => {
|
None => {
|
||||||
let mut label = CodeLabel::plain(completion.new_text.clone(), None);
|
let mut label = CodeLabel::plain(completion.new_text.clone(), None);
|
||||||
ensure_uniform_list_compatible_label(&mut label);
|
ensure_uniform_list_compatible_label(&mut label);
|
||||||
completions.push(Completion {
|
completions.push(Completion {
|
||||||
|
@ -8340,7 +8376,7 @@ async fn populate_labels_for_completions(
|
||||||
new_text: completion.new_text,
|
new_text: completion.new_text,
|
||||||
source: completion.source,
|
source: completion.source,
|
||||||
confirm: None,
|
confirm: None,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -382,6 +382,8 @@ pub enum CompletionSource {
|
||||||
server_id: LanguageServerId,
|
server_id: LanguageServerId,
|
||||||
/// The raw completion provided by the language server.
|
/// The raw completion provided by the language server.
|
||||||
lsp_completion: Box<lsp::CompletionItem>,
|
lsp_completion: Box<lsp::CompletionItem>,
|
||||||
|
/// A set of defaults for this completion item.
|
||||||
|
lsp_defaults: Option<Arc<lsp::CompletionListItemDefaults>>,
|
||||||
/// Whether this completion has been resolved, to ensure it happens once per completion.
|
/// Whether this completion has been resolved, to ensure it happens once per completion.
|
||||||
resolved: bool,
|
resolved: bool,
|
||||||
},
|
},
|
||||||
|
@ -397,17 +399,76 @@ impl CompletionSource {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn lsp_completion(&self) -> Option<&lsp::CompletionItem> {
|
pub fn lsp_completion(&self, apply_defaults: bool) -> Option<Cow<lsp::CompletionItem>> {
|
||||||
if let Self::Lsp { lsp_completion, .. } = self {
|
if let Self::Lsp {
|
||||||
Some(lsp_completion)
|
lsp_completion,
|
||||||
} else {
|
lsp_defaults,
|
||||||
None
|
..
|
||||||
}
|
} = self
|
||||||
}
|
{
|
||||||
|
if apply_defaults {
|
||||||
|
if let Some(lsp_defaults) = lsp_defaults {
|
||||||
|
let mut completion_with_defaults = *lsp_completion.clone();
|
||||||
|
let default_commit_characters = lsp_defaults.commit_characters.as_ref();
|
||||||
|
let default_edit_range = lsp_defaults.edit_range.as_ref();
|
||||||
|
let default_insert_text_format = lsp_defaults.insert_text_format.as_ref();
|
||||||
|
let default_insert_text_mode = lsp_defaults.insert_text_mode.as_ref();
|
||||||
|
|
||||||
fn lsp_completion_mut(&mut self) -> Option<&mut lsp::CompletionItem> {
|
if default_commit_characters.is_some()
|
||||||
if let Self::Lsp { lsp_completion, .. } = self {
|
|| default_edit_range.is_some()
|
||||||
Some(lsp_completion)
|
|| default_insert_text_format.is_some()
|
||||||
|
|| default_insert_text_mode.is_some()
|
||||||
|
{
|
||||||
|
if completion_with_defaults.commit_characters.is_none()
|
||||||
|
&& default_commit_characters.is_some()
|
||||||
|
{
|
||||||
|
completion_with_defaults.commit_characters =
|
||||||
|
default_commit_characters.cloned()
|
||||||
|
}
|
||||||
|
if completion_with_defaults.text_edit.is_none() {
|
||||||
|
match default_edit_range {
|
||||||
|
Some(lsp::CompletionListItemDefaultsEditRange::Range(range)) => {
|
||||||
|
completion_with_defaults.text_edit =
|
||||||
|
Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||||
|
range: *range,
|
||||||
|
new_text: completion_with_defaults.label.clone(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
Some(
|
||||||
|
lsp::CompletionListItemDefaultsEditRange::InsertAndReplace {
|
||||||
|
insert,
|
||||||
|
replace,
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
completion_with_defaults.text_edit =
|
||||||
|
Some(lsp::CompletionTextEdit::InsertAndReplace(
|
||||||
|
lsp::InsertReplaceEdit {
|
||||||
|
new_text: completion_with_defaults.label.clone(),
|
||||||
|
insert: *insert,
|
||||||
|
replace: *replace,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if completion_with_defaults.insert_text_format.is_none()
|
||||||
|
&& default_insert_text_format.is_some()
|
||||||
|
{
|
||||||
|
completion_with_defaults.insert_text_format =
|
||||||
|
default_insert_text_format.cloned()
|
||||||
|
}
|
||||||
|
if completion_with_defaults.insert_text_mode.is_none()
|
||||||
|
&& default_insert_text_mode.is_some()
|
||||||
|
{
|
||||||
|
completion_with_defaults.insert_text_mode =
|
||||||
|
default_insert_text_mode.cloned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Some(Cow::Owned(completion_with_defaults));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(Cow::Borrowed(lsp_completion))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
@ -4640,7 +4701,8 @@ impl Completion {
|
||||||
const DEFAULT_KIND_KEY: usize = 2;
|
const DEFAULT_KIND_KEY: usize = 2;
|
||||||
let kind_key = self
|
let kind_key = self
|
||||||
.source
|
.source
|
||||||
.lsp_completion()
|
// `lsp::CompletionListItemDefaults` has no `kind` field
|
||||||
|
.lsp_completion(false)
|
||||||
.and_then(|lsp_completion| lsp_completion.kind)
|
.and_then(|lsp_completion| lsp_completion.kind)
|
||||||
.and_then(|lsp_completion_kind| match lsp_completion_kind {
|
.and_then(|lsp_completion_kind| match lsp_completion_kind {
|
||||||
lsp::CompletionItemKind::KEYWORD => Some(0),
|
lsp::CompletionItemKind::KEYWORD => Some(0),
|
||||||
|
@ -4654,7 +4716,8 @@ impl Completion {
|
||||||
/// Whether this completion is a snippet.
|
/// Whether this completion is a snippet.
|
||||||
pub fn is_snippet(&self) -> bool {
|
pub fn is_snippet(&self) -> bool {
|
||||||
self.source
|
self.source
|
||||||
.lsp_completion()
|
// `lsp::CompletionListItemDefaults` has `insert_text_format` field
|
||||||
|
.lsp_completion(true)
|
||||||
.map_or(false, |lsp_completion| {
|
.map_or(false, |lsp_completion| {
|
||||||
lsp_completion.insert_text_format == Some(lsp::InsertTextFormat::SNIPPET)
|
lsp_completion.insert_text_format == Some(lsp::InsertTextFormat::SNIPPET)
|
||||||
})
|
})
|
||||||
|
@ -4664,9 +4727,10 @@ impl Completion {
|
||||||
///
|
///
|
||||||
/// Will return `None` if this completion's kind is not [`CompletionItemKind::COLOR`].
|
/// Will return `None` if this completion's kind is not [`CompletionItemKind::COLOR`].
|
||||||
pub fn color(&self) -> Option<Hsla> {
|
pub fn color(&self) -> Option<Hsla> {
|
||||||
let lsp_completion = self.source.lsp_completion()?;
|
// `lsp::CompletionListItemDefaults` has no `kind` field
|
||||||
|
let lsp_completion = self.source.lsp_completion(false)?;
|
||||||
if lsp_completion.kind? == CompletionItemKind::COLOR {
|
if lsp_completion.kind? == CompletionItemKind::COLOR {
|
||||||
return color_extractor::extract_color(lsp_completion);
|
return color_extractor::extract_color(&lsp_completion);
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
|
@ -1000,6 +1000,7 @@ message Completion {
|
||||||
bytes lsp_completion = 5;
|
bytes lsp_completion = 5;
|
||||||
bool resolved = 6;
|
bool resolved = 6;
|
||||||
Source source = 7;
|
Source source = 7;
|
||||||
|
optional bytes lsp_defaults = 8;
|
||||||
|
|
||||||
enum Source {
|
enum Source {
|
||||||
Custom = 0;
|
Custom = 0;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue