Refactor Completions to allow non-LSP ones better (#26300)

A preparation for https://github.com/zed-industries/zed/issues/4957 that
pushes all LSP-related data out from the basic completion item, so that
it's possible to create completion items without any trace of LSP
clearly.

Release Notes:

- N/A
This commit is contained in:
Kirill Bulatov 2025-03-07 22:19:28 +02:00 committed by GitHub
parent ec5e7a2653
commit 103ad635d9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 326 additions and 190 deletions

View file

@ -38,7 +38,7 @@ use language_model::{
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu}; use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
use multi_buffer::MultiBufferRow; use multi_buffer::MultiBufferRow;
use parking_lot::Mutex; use parking_lot::Mutex;
use project::{ActionVariant, CodeAction, ProjectTransaction}; use project::{CodeAction, LspAction, ProjectTransaction};
use prompt_store::PromptBuilder; use prompt_store::PromptBuilder;
use rope::Rope; use rope::Rope;
use settings::{update_settings_file, Settings, SettingsStore}; use settings::{update_settings_file, Settings, SettingsStore};
@ -3569,7 +3569,7 @@ impl CodeActionProvider for AssistantCodeActionProvider {
Task::ready(Ok(vec![CodeAction { Task::ready(Ok(vec![CodeAction {
server_id: language::LanguageServerId(0), server_id: language::LanguageServerId(0),
range: snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end), range: snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end),
lsp_action: ActionVariant::Action(Box::new(lsp::CodeAction { lsp_action: LspAction::Action(Box::new(lsp::CodeAction {
title: "Fix with Assistant".into(), title: "Fix with Assistant".into(),
..Default::default() ..Default::default()
})), })),

View file

@ -27,7 +27,7 @@ use language::{Buffer, Point, Selection, TransactionId};
use language_model::{report_assistant_event, LanguageModelRegistry}; use language_model::{report_assistant_event, LanguageModelRegistry};
use multi_buffer::MultiBufferRow; use multi_buffer::MultiBufferRow;
use parking_lot::Mutex; use parking_lot::Mutex;
use project::ActionVariant; use project::LspAction;
use project::{CodeAction, ProjectTransaction}; use project::{CodeAction, ProjectTransaction};
use prompt_store::PromptBuilder; use prompt_store::PromptBuilder;
use settings::{Settings, SettingsStore}; use settings::{Settings, SettingsStore};
@ -1728,7 +1728,7 @@ impl CodeActionProvider for AssistantCodeActionProvider {
Task::ready(Ok(vec![CodeAction { Task::ready(Ok(vec![CodeAction {
server_id: language::LanguageServerId(0), server_id: language::LanguageServerId(0),
range: snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end), range: snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end),
lsp_action: ActionVariant::Action(Box::new(lsp::CodeAction { lsp_action: LspAction::Action(Box::new(lsp::CodeAction {
title: "Fix with Assistant".into(), title: "Fix with Assistant".into(),
..Default::default() ..Default::default()
})), })),

View file

@ -5,9 +5,9 @@ use assistant_slash_command::{AfterCompletion, SlashCommandLine, SlashCommandWor
use editor::{CompletionProvider, Editor}; use editor::{CompletionProvider, Editor};
use fuzzy::{match_strings, StringMatchCandidate}; use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity, Window}; use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity, Window};
use language::{Anchor, Buffer, LanguageServerId, ToPoint}; use language::{Anchor, Buffer, ToPoint};
use parking_lot::Mutex; use parking_lot::Mutex;
use project::{lsp_store::CompletionDocumentation, CompletionIntent}; use project::{lsp_store::CompletionDocumentation, CompletionIntent, CompletionSource};
use rope::Point; use rope::Point;
use std::{ use std::{
cell::RefCell, cell::RefCell,
@ -125,10 +125,8 @@ impl SlashCommandCompletionProvider {
)), )),
new_text, new_text,
label: command.label(cx), label: command.label(cx),
server_id: LanguageServerId(0),
lsp_completion: Default::default(),
confirm, confirm,
resolved: true, source: CompletionSource::Custom,
}) })
}) })
.collect() .collect()
@ -225,10 +223,8 @@ impl SlashCommandCompletionProvider {
label: new_argument.label, label: new_argument.label,
new_text, new_text,
documentation: None, documentation: None,
server_id: LanguageServerId(0),
lsp_completion: Default::default(),
confirm, confirm,
resolved: true, source: CompletionSource::Custom,
} }
}) })
.collect()) .collect())

View file

@ -10,9 +10,9 @@ use gpui::{
}; };
use language::{ use language::{
language_settings::SoftWrap, Anchor, Buffer, BufferSnapshot, CodeLabel, LanguageRegistry, language_settings::SoftWrap, Anchor, Buffer, BufferSnapshot, CodeLabel, LanguageRegistry,
LanguageServerId, ToOffset, ToOffset,
}; };
use project::{search::SearchQuery, Completion}; use project::{search::SearchQuery, Completion, CompletionSource};
use settings::Settings; use settings::Settings;
use std::{ use std::{
cell::RefCell, cell::RefCell,
@ -309,11 +309,9 @@ impl MessageEditor {
old_range: range.clone(), old_range: range.clone(),
new_text, new_text,
label, label,
documentation: None,
server_id: LanguageServerId(0), // TODO: Make this optional or something?
lsp_completion: Default::default(), // TODO: Make this optional or something?
confirm: None, confirm: None,
resolved: true, documentation: None,
source: CompletionSource::Custom,
} }
}) })
.collect() .collect()

View file

@ -6,11 +6,11 @@ use gpui::{
}; };
use language::Buffer; use language::Buffer;
use language::CodeLabel; use language::CodeLabel;
use lsp::LanguageServerId;
use markdown::Markdown; use markdown::Markdown;
use multi_buffer::{Anchor, ExcerptId}; use multi_buffer::{Anchor, ExcerptId};
use ordered_float::OrderedFloat; use ordered_float::OrderedFloat;
use project::lsp_store::CompletionDocumentation; use project::lsp_store::CompletionDocumentation;
use project::CompletionSource;
use project::{CodeAction, Completion, TaskSourceKind}; use project::{CodeAction, Completion, TaskSourceKind};
use std::{ use std::{
@ -233,11 +233,9 @@ impl CompletionsMenu {
runs: Default::default(), runs: Default::default(),
filter_range: Default::default(), filter_range: Default::default(),
}, },
server_id: LanguageServerId(usize::MAX),
documentation: None, documentation: None,
lsp_completion: Default::default(),
confirm: None, confirm: None,
resolved: true, source: CompletionSource::Custom,
}) })
.collect(); .collect();
@ -500,7 +498,12 @@ impl CompletionsMenu {
// Ignore font weight for syntax highlighting, as we'll use it // Ignore font weight for syntax highlighting, as we'll use it
// for fuzzy matches. // for fuzzy matches.
highlight.font_weight = None; highlight.font_weight = None;
if completion.lsp_completion.deprecated.unwrap_or(false) { if completion
.source
.lsp_completion()
.and_then(|lsp_completion| lsp_completion.deprecated)
.unwrap_or(false)
{
highlight.strikethrough = Some(StrikethroughStyle { highlight.strikethrough = Some(StrikethroughStyle {
thickness: 1.0.into(), thickness: 1.0.into(),
..Default::default() ..Default::default()
@ -708,7 +711,10 @@ 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.lsp_completion.sort_text.as_deref(); let sort_text = completion
.source
.lsp_completion()
.and_then(|lsp_completion| lsp_completion.sort_text.as_deref());
let score = Reverse(OrderedFloat(mat.score)); let score = Reverse(OrderedFloat(mat.score));
if mat.score >= 0.2 { if mat.score >= 0.2 {

View file

@ -138,8 +138,9 @@ use multi_buffer::{
use project::{ use project::{
lsp_store::{CompletionDocumentation, FormatTrigger, LspFormatTarget, OpenLspBufferHandle}, lsp_store::{CompletionDocumentation, FormatTrigger, LspFormatTarget, OpenLspBufferHandle},
project_settings::{GitGutterSetting, ProjectSettings}, project_settings::{GitGutterSetting, ProjectSettings},
CodeAction, Completion, CompletionIntent, DocumentHighlight, InlayHint, Location, LocationLink, CodeAction, Completion, CompletionIntent, CompletionSource, DocumentHighlight, InlayHint,
PrepareRenameResponse, Project, ProjectItem, ProjectTransaction, TaskSourceKind, Location, LocationLink, PrepareRenameResponse, Project, ProjectItem, ProjectTransaction,
TaskSourceKind,
}; };
use rand::prelude::*; use rand::prelude::*;
use rpc::{proto::*, ErrorExt}; use rpc::{proto::*, ErrorExt};
@ -16897,38 +16898,40 @@ fn snippet_completions(
Some(Completion { Some(Completion {
old_range: range, old_range: range,
new_text: snippet.body.clone(), new_text: snippet.body.clone(),
resolved: false, source: CompletionSource::Lsp {
server_id: LanguageServerId(usize::MAX),
resolved: true,
lsp_completion: Box::new(lsp::CompletionItem {
label: snippet.prefix.first().unwrap().clone(),
kind: Some(CompletionItemKind::SNIPPET),
label_details: snippet.description.as_ref().map(|description| {
lsp::CompletionItemLabelDetails {
detail: Some(description.clone()),
description: None,
}
}),
insert_text_format: Some(InsertTextFormat::SNIPPET),
text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
lsp::InsertReplaceEdit {
new_text: snippet.body.clone(),
insert: lsp_range,
replace: lsp_range,
},
)),
filter_text: Some(snippet.body.clone()),
sort_text: Some(char::MAX.to_string()),
..lsp::CompletionItem::default()
}),
},
label: CodeLabel { label: CodeLabel {
text: matching_prefix.clone(), text: matching_prefix.clone(),
runs: vec![], runs: Vec::new(),
filter_range: 0..matching_prefix.len(), filter_range: 0..matching_prefix.len(),
}, },
server_id: LanguageServerId(usize::MAX),
documentation: snippet documentation: snippet
.description .description
.clone() .clone()
.map(|description| CompletionDocumentation::SingleLine(description.into())), .map(|description| CompletionDocumentation::SingleLine(description.into())),
lsp_completion: lsp::CompletionItem {
label: snippet.prefix.first().unwrap().clone(),
kind: Some(CompletionItemKind::SNIPPET),
label_details: snippet.description.as_ref().map(|description| {
lsp::CompletionItemLabelDetails {
detail: Some(description.clone()),
description: None,
}
}),
insert_text_format: Some(InsertTextFormat::SNIPPET),
text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
lsp::InsertReplaceEdit {
new_text: snippet.body.clone(),
insert: lsp_range,
replace: lsp_range,
},
)),
filter_text: Some(snippet.body.clone()),
sort_text: Some(char::MAX.to_string()),
..Default::default()
},
confirm: None, confirm: None,
}) })
}) })

View file

@ -2,9 +2,9 @@ mod signature_help;
use crate::{ use crate::{
lsp_store::{LocalLspStore, LspStore}, lsp_store::{LocalLspStore, LspStore},
ActionVariant, CodeAction, CoreCompletion, DocumentHighlight, Hover, HoverBlock, CodeAction, CompletionSource, CoreCompletion, DocumentHighlight, Hover, HoverBlock,
HoverBlockKind, InlayHint, InlayHintLabel, InlayHintLabelPart, InlayHintLabelPartTooltip, HoverBlockKind, InlayHint, InlayHintLabel, InlayHintLabelPart, InlayHintLabelPartTooltip,
InlayHintTooltip, Location, LocationLink, MarkupContent, PrepareRenameResponse, InlayHintTooltip, Location, LocationLink, LspAction, MarkupContent, PrepareRenameResponse,
ProjectTransaction, ResolveState, ProjectTransaction, ResolveState,
}; };
use anyhow::{anyhow, Context as _, Result}; use anyhow::{anyhow, Context as _, Result};
@ -2011,9 +2011,11 @@ impl LspCommand for GetCompletions {
CoreCompletion { CoreCompletion {
old_range, old_range,
new_text, new_text,
server_id, source: CompletionSource::Lsp {
lsp_completion, server_id,
resolved: false, lsp_completion: Box::new(lsp_completion),
resolved: false,
},
} }
}) })
.collect()) .collect())
@ -2256,11 +2258,11 @@ impl LspCommand for GetCodeActions {
return None; return None;
} }
} }
ActionVariant::Action(Box::new(lsp_action)) LspAction::Action(Box::new(lsp_action))
} }
lsp::CodeActionOrCommand::Command(command) => { lsp::CodeActionOrCommand::Command(command) => {
if available_commands.contains(&command.command) { if available_commands.contains(&command.command) {
ActionVariant::Command(command) LspAction::Command(command)
} else { } else {
return None; return None;
} }

View file

@ -14,8 +14,8 @@ use crate::{
toolchain_store::{EmptyToolchainStore, ToolchainStoreEvent}, toolchain_store::{EmptyToolchainStore, ToolchainStoreEvent},
worktree_store::{WorktreeStore, WorktreeStoreEvent}, worktree_store::{WorktreeStore, WorktreeStoreEvent},
yarn::YarnPathStore, yarn::YarnPathStore,
ActionVariant, CodeAction, Completion, CoreCompletion, Hover, InlayHint, ProjectItem as _, CodeAction, Completion, CompletionSource, CoreCompletion, Hover, InlayHint, LspAction,
ProjectPath, ProjectTransaction, ResolveState, Symbol, ToolchainStore, ProjectItem as _, ProjectPath, ProjectTransaction, ResolveState, Symbol, ToolchainStore,
}; };
use anyhow::{anyhow, Context as _, Result}; use anyhow::{anyhow, Context as _, Result};
use async_trait::async_trait; use async_trait::async_trait;
@ -1629,7 +1629,7 @@ impl LocalLspStore {
action: &mut CodeAction, action: &mut CodeAction,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
match &mut action.lsp_action { match &mut action.lsp_action {
ActionVariant::Action(lsp_action) => { LspAction::Action(lsp_action) => {
if GetCodeActions::can_resolve_actions(&lang_server.capabilities()) if GetCodeActions::can_resolve_actions(&lang_server.capabilities())
&& lsp_action.data.is_some() && lsp_action.data.is_some()
&& (lsp_action.command.is_none() || lsp_action.edit.is_none()) && (lsp_action.command.is_none() || lsp_action.edit.is_none())
@ -1641,7 +1641,7 @@ impl LocalLspStore {
); );
} }
} }
ActionVariant::Command(_) => {} LspAction::Command(_) => {}
} }
anyhow::Ok(()) anyhow::Ok(())
} }
@ -4401,26 +4401,33 @@ impl LspStore {
let mut did_resolve = false; let mut did_resolve = false;
if let Some((client, project_id)) = client { if let Some((client, project_id)) = client {
for completion_index in completion_indices { for completion_index in completion_indices {
let server_id = completions.borrow()[completion_index].server_id; let server_id = {
let completion = &completions.borrow()[completion_index];
if Self::resolve_completion_remote( completion.source.server_id()
project_id, };
server_id, if let Some(server_id) = server_id {
buffer_id, if Self::resolve_completion_remote(
completions.clone(), project_id,
completion_index, server_id,
client.clone(), buffer_id,
) completions.clone(),
.await completion_index,
.log_err() client.clone(),
.is_some() )
{ .await
did_resolve = true; .log_err()
.is_some()
{
did_resolve = true;
}
} }
} }
} else { } else {
for completion_index in completion_indices { for completion_index in completion_indices {
let server_id = completions.borrow()[completion_index].server_id; let Some(server_id) = completions.borrow()[completion_index].source.server_id()
else {
continue;
};
let server_and_adapter = this let server_and_adapter = this
.read_with(&cx, |lsp_store, _| { .read_with(&cx, |lsp_store, _| {
@ -4480,10 +4487,19 @@ impl LspStore {
let request = { let request = {
let completion = &completions.borrow()[completion_index]; let completion = &completions.borrow()[completion_index];
if completion.resolved { match &completion.source {
return Ok(()); CompletionSource::Lsp {
lsp_completion,
resolved,
..
} => {
if *resolved {
return Ok(());
}
server.request::<lsp::request::ResolveCompletionItem>(*lsp_completion.clone())
}
CompletionSource::Custom => return Ok(()),
} }
server.request::<lsp::request::ResolveCompletionItem>(completion.lsp_completion.clone())
}; };
let completion_item = request.await?; let completion_item = request.await?;
@ -4508,15 +4524,20 @@ impl LspStore {
// vtsls might change the type of completion after resolution. // vtsls might change the type of completion after resolution.
let mut completions = completions.borrow_mut(); let mut completions = completions.borrow_mut();
let completion = &mut completions[completion_index]; let completion = &mut completions[completion_index];
if completion_item.insert_text_format != completion.lsp_completion.insert_text_format { if let Some(lsp_completion) = completion.source.lsp_completion_mut() {
completion.lsp_completion.insert_text_format = completion_item.insert_text_format; 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.lsp_completion = completion_item; completion.source = CompletionSource::Lsp {
completion.resolved = true; lsp_completion: Box::new(completion_item),
resolved: true,
server_id: server.server_id(),
};
Ok(()) Ok(())
} }
@ -4527,9 +4548,13 @@ impl LspStore {
completion_index: usize, completion_index: usize,
) -> Result<()> { ) -> Result<()> {
let completion_item = completions.borrow()[completion_index] let completion_item = completions.borrow()[completion_index]
.lsp_completion .source
.clone(); .lsp_completion()
if let Some(lsp_documentation) = completion_item.documentation.clone() { .cloned();
if let Some(lsp_documentation) = completion_item
.as_ref()
.and_then(|completion_item| completion_item.documentation.clone())
{
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(lsp_documentation.into()); completion.documentation = Some(lsp_documentation.into());
@ -4539,25 +4564,33 @@ impl LspStore {
completion.documentation = Some(CompletionDocumentation::Undocumented); completion.documentation = Some(CompletionDocumentation::Undocumented);
} }
// NB: Zed does not have `details` inside the completion resolve capabilities, but certain language servers violate the spec and do not return `details` immediately, e.g. https://github.com/yioneko/vtsls/issues/213 let mut new_label = match completion_item {
// So we have to update the label here anyway... Some(completion_item) => {
let language = snapshot.language(); // NB: Zed does not have `details` inside the completion resolve capabilities, but certain language servers violate the spec and do not return `details` immediately, e.g. https://github.com/yioneko/vtsls/issues/213
let mut new_label = match language { // So we have to update the label here anyway...
Some(language) => { let language = snapshot.language();
adapter match language {
.labels_for_completions(&[completion_item.clone()], language) Some(language) => {
.await? adapter
.labels_for_completions(&[completion_item.clone()], language)
.await?
}
None => Vec::new(),
}
.pop()
.flatten()
.unwrap_or_else(|| {
CodeLabel::fallback_for_completion(
&completion_item,
language.map(|language| language.as_ref()),
)
})
} }
None => Vec::new(), None => CodeLabel::plain(
} completions.borrow()[completion_index].new_text.clone(),
.pop() None,
.flatten() ),
.unwrap_or_else(|| { };
CodeLabel::fallback_for_completion(
&completion_item,
language.map(|language| language.as_ref()),
)
});
ensure_uniform_list_compatible_label(&mut new_label); ensure_uniform_list_compatible_label(&mut new_label);
let mut completions = completions.borrow_mut(); let mut completions = completions.borrow_mut();
@ -4589,12 +4622,19 @@ impl LspStore {
) -> Result<()> { ) -> Result<()> {
let lsp_completion = { let lsp_completion = {
let completion = &completions.borrow()[completion_index]; let completion = &completions.borrow()[completion_index];
if completion.resolved { match &completion.source {
return Ok(()); CompletionSource::Lsp {
lsp_completion,
resolved,
..
} => {
if *resolved {
return Ok(());
}
serde_json::to_string(lsp_completion).unwrap().into_bytes()
}
CompletionSource::Custom => return Ok(()),
} }
serde_json::to_string(&completion.lsp_completion)
.unwrap()
.into_bytes()
}; };
let request = proto::ResolveCompletionDocumentation { let request = proto::ResolveCompletionDocumentation {
project_id, project_id,
@ -4622,8 +4662,11 @@ 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.lsp_completion = lsp_completion; completion.source = CompletionSource::Lsp {
completion.resolved = true; server_id,
lsp_completion,
resolved: true,
};
let old_range = response let old_range = response
.old_start .old_start
@ -4659,17 +4702,12 @@ impl LspStore {
completion: Some(Self::serialize_completion(&CoreCompletion { completion: Some(Self::serialize_completion(&CoreCompletion {
old_range: completion.old_range, old_range: completion.old_range,
new_text: completion.new_text, new_text: completion.new_text,
server_id: completion.server_id, source: completion.source,
lsp_completion: completion.lsp_completion,
resolved: completion.resolved,
})), })),
} }
}; };
let response = client.request(request).await?; if let Some(transaction) = client.request(request).await?.transaction {
completions.borrow_mut()[completion_index].resolved = true;
if let Some(transaction) = response.transaction {
let transaction = language::proto::deserialize_transaction(transaction)?; let transaction = language::proto::deserialize_transaction(transaction)?;
buffer_handle buffer_handle
.update(&mut cx, |buffer, _| { .update(&mut cx, |buffer, _| {
@ -4687,8 +4725,9 @@ impl LspStore {
} }
}) })
} else { } else {
let server_id = completions.borrow()[completion_index].server_id;
let Some(server) = buffer_handle.update(cx, |buffer, cx| { let Some(server) = buffer_handle.update(cx, |buffer, cx| {
let completion = &completions.borrow()[completion_index];
let server_id = completion.source.server_id()?;
Some( Some(
self.language_server_for_local_buffer(buffer, server_id, cx)? self.language_server_for_local_buffer(buffer, server_id, cx)?
.1 .1
@ -4709,7 +4748,11 @@ impl LspStore {
.await .await
.context("resolving completion")?; .context("resolving completion")?;
let completion = completions.borrow()[completion_index].clone(); let completion = completions.borrow()[completion_index].clone();
let additional_text_edits = completion.lsp_completion.additional_text_edits; let additional_text_edits = completion
.source
.lsp_completion()
.as_ref()
.and_then(|lsp_completion| lsp_completion.additional_text_edits.clone());
if let Some(edits) = additional_text_edits { if let Some(edits) = additional_text_edits {
let edits = this let edits = this
.update(&mut cx, |this, cx| { .update(&mut cx, |this, cx| {
@ -7139,8 +7182,7 @@ impl LspStore {
Rc::new(RefCell::new(Box::new([Completion { Rc::new(RefCell::new(Box::new([Completion {
old_range: completion.old_range, old_range: completion.old_range,
new_text: completion.new_text, new_text: completion.new_text,
lsp_completion: completion.lsp_completion, source: completion.source,
server_id: completion.server_id,
documentation: None, documentation: None,
label: CodeLabel { label: CodeLabel {
text: Default::default(), text: Default::default(),
@ -7148,7 +7190,6 @@ impl LspStore {
filter_range: Default::default(), filter_range: Default::default(),
}, },
confirm: None, confirm: None,
resolved: completion.resolved,
}]))), }]))),
0, 0,
false, false,
@ -8112,13 +8153,33 @@ 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 {
CompletionSource::Lsp {
server_id,
lsp_completion,
resolved,
} => (
proto::completion::Source::Lsp as i32,
server_id.0 as u64,
serde_json::to_vec(lsp_completion).unwrap(),
*resolved,
),
CompletionSource::Custom => (
proto::completion::Source::Custom as i32,
0,
Vec::new(),
true,
),
};
proto::Completion { proto::Completion {
old_start: Some(serialize_anchor(&completion.old_range.start)), old_start: Some(serialize_anchor(&completion.old_range.start)),
old_end: Some(serialize_anchor(&completion.old_range.end)), old_end: Some(serialize_anchor(&completion.old_range.end)),
new_text: completion.new_text.clone(), new_text: completion.new_text.clone(),
server_id: completion.server_id.0 as u64, server_id,
lsp_completion: serde_json::to_vec(&completion.lsp_completion).unwrap(), lsp_completion,
resolved: completion.resolved, resolved,
source,
} }
} }
@ -8131,24 +8192,28 @@ impl LspStore {
.old_end .old_end
.and_then(deserialize_anchor) .and_then(deserialize_anchor)
.ok_or_else(|| anyhow!("invalid old end"))?; .ok_or_else(|| anyhow!("invalid old end"))?;
let lsp_completion = serde_json::from_slice(&completion.lsp_completion)?;
Ok(CoreCompletion { Ok(CoreCompletion {
old_range: old_start..old_end, old_range: old_start..old_end,
new_text: completion.new_text, new_text: completion.new_text,
server_id: LanguageServerId(completion.server_id as usize), source: match proto::completion::Source::from_i32(completion.source) {
lsp_completion, Some(proto::completion::Source::Custom) => CompletionSource::Custom,
resolved: completion.resolved, Some(proto::completion::Source::Lsp) => CompletionSource::Lsp {
server_id: LanguageServerId::from_proto(completion.server_id),
lsp_completion: serde_json::from_slice(&completion.lsp_completion)?,
resolved: completion.resolved,
},
_ => anyhow::bail!("Unexpected completion source {}", completion.source),
},
}) })
} }
pub(crate) fn serialize_code_action(action: &CodeAction) -> proto::CodeAction { pub(crate) fn serialize_code_action(action: &CodeAction) -> proto::CodeAction {
let (kind, lsp_action) = match &action.lsp_action { let (kind, lsp_action) = match &action.lsp_action {
ActionVariant::Action(code_action) => ( LspAction::Action(code_action) => (
proto::code_action::Kind::Action as i32, proto::code_action::Kind::Action as i32,
serde_json::to_vec(code_action).unwrap(), serde_json::to_vec(code_action).unwrap(),
), ),
ActionVariant::Command(command) => ( LspAction::Command(command) => (
proto::code_action::Kind::Command as i32, proto::code_action::Kind::Command as i32,
serde_json::to_vec(command).unwrap(), serde_json::to_vec(command).unwrap(),
), ),
@ -8174,10 +8239,10 @@ impl LspStore {
.ok_or_else(|| anyhow!("invalid end"))?; .ok_or_else(|| anyhow!("invalid end"))?;
let lsp_action = match proto::code_action::Kind::from_i32(action.kind) { let lsp_action = match proto::code_action::Kind::from_i32(action.kind) {
Some(proto::code_action::Kind::Action) => { Some(proto::code_action::Kind::Action) => {
ActionVariant::Action(serde_json::from_slice(&action.lsp_action)?) LspAction::Action(serde_json::from_slice(&action.lsp_action)?)
} }
Some(proto::code_action::Kind::Command) => { Some(proto::code_action::Kind::Command) => {
ActionVariant::Command(serde_json::from_slice(&action.lsp_action)?) LspAction::Command(serde_json::from_slice(&action.lsp_action)?)
} }
None => anyhow::bail!("Unknown action kind {}", action.kind), None => anyhow::bail!("Unknown action kind {}", action.kind),
}; };
@ -8215,17 +8280,23 @@ fn remove_empty_hover_blocks(mut hover: Hover) -> Option<Hover> {
} }
async fn populate_labels_for_completions( async fn populate_labels_for_completions(
mut new_completions: Vec<CoreCompletion>, new_completions: Vec<CoreCompletion>,
language: Option<Arc<Language>>, language: Option<Arc<Language>>,
lsp_adapter: Option<Arc<CachedLspAdapter>>, lsp_adapter: Option<Arc<CachedLspAdapter>>,
completions: &mut Vec<Completion>, completions: &mut Vec<Completion>,
) { ) {
let lsp_completions = new_completions let lsp_completions = new_completions
.iter_mut() .iter()
.map(|completion| mem::take(&mut completion.lsp_completion)) .filter_map(|new_completion| {
if let CompletionSource::Lsp { lsp_completion, .. } = &new_completion.source {
Some(*lsp_completion.clone())
} else {
None
}
})
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let labels = if let Some((language, lsp_adapter)) = language.as_ref().zip(lsp_adapter) { let mut labels = if let Some((language, lsp_adapter)) = language.as_ref().zip(lsp_adapter) {
lsp_adapter lsp_adapter
.labels_for_completions(&lsp_completions, language) .labels_for_completions(&lsp_completions, language)
.await .await
@ -8233,34 +8304,45 @@ async fn populate_labels_for_completions(
.unwrap_or_default() .unwrap_or_default()
} else { } else {
Vec::new() Vec::new()
}; }
.into_iter()
.fuse();
for ((completion, lsp_completion), label) in new_completions for completion in new_completions {
.into_iter() match &completion.source {
.zip(lsp_completions) CompletionSource::Lsp { lsp_completion, .. } => {
.zip(labels.into_iter().chain(iter::repeat(None))) let documentation = if let Some(docs) = lsp_completion.documentation.clone() {
{ Some(docs.into())
let documentation = if let Some(docs) = lsp_completion.documentation.clone() { } else {
Some(docs.into()) None
} else { };
None
};
let mut label = label.unwrap_or_else(|| { let mut label = labels.next().flatten().unwrap_or_else(|| {
CodeLabel::fallback_for_completion(&lsp_completion, language.as_deref()) CodeLabel::fallback_for_completion(&lsp_completion, language.as_deref())
}); });
ensure_uniform_list_compatible_label(&mut label); ensure_uniform_list_compatible_label(&mut label);
completions.push(Completion {
completions.push(Completion { label,
old_range: completion.old_range, documentation,
new_text: completion.new_text, old_range: completion.old_range,
label, new_text: completion.new_text,
server_id: completion.server_id, source: completion.source,
documentation, confirm: None,
lsp_completion, })
confirm: None, }
resolved: false, CompletionSource::Custom => {
}) let mut label = CodeLabel::plain(completion.new_text.clone(), None);
ensure_uniform_list_compatible_label(&mut label);
completions.push(Completion {
label,
documentation: None,
old_range: completion.old_range,
new_text: completion.new_text,
source: completion.source,
confirm: None,
})
}
}
} }
} }

View file

@ -364,14 +364,10 @@ pub struct Completion {
pub new_text: String, pub new_text: String,
/// A label for this completion that is shown in the menu. /// A label for this completion that is shown in the menu.
pub label: CodeLabel, pub label: CodeLabel,
/// The id of the language server that produced this completion.
pub server_id: LanguageServerId,
/// The documentation for this completion. /// The documentation for this completion.
pub documentation: Option<CompletionDocumentation>, pub documentation: Option<CompletionDocumentation>,
/// The raw completion provided by the language server. /// Completion data source which it was constructed from.
pub lsp_completion: lsp::CompletionItem, pub source: CompletionSource,
/// Whether this completion has been resolved, to ensure it happens once per completion.
pub resolved: bool,
/// An optional callback to invoke when this completion is confirmed. /// An optional callback to invoke when this completion is confirmed.
/// Returns, whether new completions should be retriggered after the current one. /// Returns, whether new completions should be retriggered after the current one.
/// If `true` is returned, the editor will show a new completion menu after this completion is confirmed. /// If `true` is returned, the editor will show a new completion menu after this completion is confirmed.
@ -379,15 +375,53 @@ pub struct Completion {
pub confirm: Option<Arc<dyn Send + Sync + Fn(CompletionIntent, &mut Window, &mut App) -> bool>>, pub confirm: Option<Arc<dyn Send + Sync + Fn(CompletionIntent, &mut Window, &mut App) -> bool>>,
} }
#[derive(Debug, Clone)]
pub enum CompletionSource {
Lsp {
/// The id of the language server that produced this completion.
server_id: LanguageServerId,
/// The raw completion provided by the language server.
lsp_completion: Box<lsp::CompletionItem>,
/// Whether this completion has been resolved, to ensure it happens once per completion.
resolved: bool,
},
Custom,
}
impl CompletionSource {
pub fn server_id(&self) -> Option<LanguageServerId> {
if let CompletionSource::Lsp { server_id, .. } = self {
Some(*server_id)
} else {
None
}
}
pub fn lsp_completion(&self) -> Option<&lsp::CompletionItem> {
if let Self::Lsp { lsp_completion, .. } = self {
Some(lsp_completion)
} else {
None
}
}
fn lsp_completion_mut(&mut self) -> Option<&mut lsp::CompletionItem> {
if let Self::Lsp { lsp_completion, .. } = self {
Some(lsp_completion)
} else {
None
}
}
}
impl std::fmt::Debug for Completion { impl std::fmt::Debug for Completion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Completion") f.debug_struct("Completion")
.field("old_range", &self.old_range) .field("old_range", &self.old_range)
.field("new_text", &self.new_text) .field("new_text", &self.new_text)
.field("label", &self.label) .field("label", &self.label)
.field("server_id", &self.server_id)
.field("documentation", &self.documentation) .field("documentation", &self.documentation)
.field("lsp_completion", &self.lsp_completion) .field("source", &self.source)
.finish() .finish()
} }
} }
@ -397,9 +431,7 @@ impl std::fmt::Debug for Completion {
pub(crate) struct CoreCompletion { pub(crate) struct CoreCompletion {
old_range: Range<Anchor>, old_range: Range<Anchor>,
new_text: String, new_text: String,
server_id: LanguageServerId, source: CompletionSource,
lsp_completion: lsp::CompletionItem,
resolved: bool,
} }
/// A code action provided by a language server. /// A code action provided by a language server.
@ -411,12 +443,12 @@ pub struct CodeAction {
pub range: Range<Anchor>, pub range: Range<Anchor>,
/// The raw code action provided by the language server. /// The raw code action provided by the language server.
/// Can be either an action or a command. /// Can be either an action or a command.
pub lsp_action: ActionVariant, pub lsp_action: LspAction,
} }
/// An action sent back by a language server. /// An action sent back by a language server.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum ActionVariant { pub enum LspAction {
/// An action with the full data, may have a command or may not. /// An action with the full data, may have a command or may not.
/// May require resolving. /// May require resolving.
Action(Box<lsp::CodeAction>), Action(Box<lsp::CodeAction>),
@ -424,7 +456,7 @@ pub enum ActionVariant {
Command(lsp::Command), Command(lsp::Command),
} }
impl ActionVariant { impl LspAction {
pub fn title(&self) -> &str { pub fn title(&self) -> &str {
match self { match self {
Self::Action(action) => &action.title, Self::Action(action) => &action.title,
@ -4605,27 +4637,38 @@ impl Completion {
/// A key that can be used to sort completions when displaying /// A key that can be used to sort completions when displaying
/// them to the user. /// them to the user.
pub fn sort_key(&self) -> (usize, &str) { pub fn sort_key(&self) -> (usize, &str) {
let kind_key = match self.lsp_completion.kind { const DEFAULT_KIND_KEY: usize = 2;
Some(lsp::CompletionItemKind::KEYWORD) => 0, let kind_key = self
Some(lsp::CompletionItemKind::VARIABLE) => 1, .source
_ => 2, .lsp_completion()
}; .and_then(|lsp_completion| lsp_completion.kind)
.and_then(|lsp_completion_kind| match lsp_completion_kind {
lsp::CompletionItemKind::KEYWORD => Some(0),
lsp::CompletionItemKind::VARIABLE => Some(1),
_ => None,
})
.unwrap_or(DEFAULT_KIND_KEY);
(kind_key, &self.label.text[self.label.filter_range.clone()]) (kind_key, &self.label.text[self.label.filter_range.clone()])
} }
/// 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.lsp_completion.insert_text_format == Some(lsp::InsertTextFormat::SNIPPET) self.source
.lsp_completion()
.map_or(false, |lsp_completion| {
lsp_completion.insert_text_format == Some(lsp::InsertTextFormat::SNIPPET)
})
} }
/// Returns the corresponding color for this completion. /// Returns the corresponding color for this 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> {
match self.lsp_completion.kind { let lsp_completion = self.source.lsp_completion()?;
Some(CompletionItemKind::COLOR) => color_extractor::extract_color(&self.lsp_completion), if lsp_completion.kind? == CompletionItemKind::COLOR {
_ => None, return color_extractor::extract_color(lsp_completion);
} }
None
} }
} }

View file

@ -1,6 +1,6 @@
syntax = "proto3"; syntax = "proto3";
package zed.messages; package zed.messages;
import "google/protobuf/wrappers.proto";
// Looking for a number? Search "// current max" // Looking for a number? Search "// current max"
@ -999,6 +999,12 @@ message Completion {
uint64 server_id = 4; uint64 server_id = 4;
bytes lsp_completion = 5; bytes lsp_completion = 5;
bool resolved = 6; bool resolved = 6;
Source source = 7;
enum Source {
Custom = 0;
Lsp = 1;
}
} }
message GetCodeActions { message GetCodeActions {