From 103ad635d9d1520238a8ab50c2e40c7f0f326d97 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 7 Mar 2025 22:19:28 +0200 Subject: [PATCH] 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 --- crates/assistant/src/inline_assistant.rs | 4 +- crates/assistant2/src/inline_assistant.rs | 4 +- .../src/slash_command.rs | 12 +- .../src/chat_panel/message_editor.rs | 10 +- crates/editor/src/code_context_menus.rs | 18 +- crates/editor/src/editor.rs | 55 ++-- crates/project/src/lsp_command.rs | 16 +- crates/project/src/lsp_store.rs | 300 +++++++++++------- crates/project/src/project.rs | 89 ++++-- crates/proto/proto/zed.proto | 8 +- 10 files changed, 326 insertions(+), 190 deletions(-) diff --git a/crates/assistant/src/inline_assistant.rs b/crates/assistant/src/inline_assistant.rs index 9e33895f4f..7b7f83f315 100644 --- a/crates/assistant/src/inline_assistant.rs +++ b/crates/assistant/src/inline_assistant.rs @@ -38,7 +38,7 @@ use language_model::{ use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu}; use multi_buffer::MultiBufferRow; use parking_lot::Mutex; -use project::{ActionVariant, CodeAction, ProjectTransaction}; +use project::{CodeAction, LspAction, ProjectTransaction}; use prompt_store::PromptBuilder; use rope::Rope; use settings::{update_settings_file, Settings, SettingsStore}; @@ -3569,7 +3569,7 @@ impl CodeActionProvider for AssistantCodeActionProvider { Task::ready(Ok(vec![CodeAction { server_id: language::LanguageServerId(0), 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(), ..Default::default() })), diff --git a/crates/assistant2/src/inline_assistant.rs b/crates/assistant2/src/inline_assistant.rs index 607f02344d..5b15794d49 100644 --- a/crates/assistant2/src/inline_assistant.rs +++ b/crates/assistant2/src/inline_assistant.rs @@ -27,7 +27,7 @@ use language::{Buffer, Point, Selection, TransactionId}; use language_model::{report_assistant_event, LanguageModelRegistry}; use multi_buffer::MultiBufferRow; use parking_lot::Mutex; -use project::ActionVariant; +use project::LspAction; use project::{CodeAction, ProjectTransaction}; use prompt_store::PromptBuilder; use settings::{Settings, SettingsStore}; @@ -1728,7 +1728,7 @@ impl CodeActionProvider for AssistantCodeActionProvider { Task::ready(Ok(vec![CodeAction { server_id: language::LanguageServerId(0), 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(), ..Default::default() })), diff --git a/crates/assistant_context_editor/src/slash_command.rs b/crates/assistant_context_editor/src/slash_command.rs index 083d020aeb..fde6816e45 100644 --- a/crates/assistant_context_editor/src/slash_command.rs +++ b/crates/assistant_context_editor/src/slash_command.rs @@ -5,9 +5,9 @@ use assistant_slash_command::{AfterCompletion, SlashCommandLine, SlashCommandWor use editor::{CompletionProvider, Editor}; use fuzzy::{match_strings, StringMatchCandidate}; 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 project::{lsp_store::CompletionDocumentation, CompletionIntent}; +use project::{lsp_store::CompletionDocumentation, CompletionIntent, CompletionSource}; use rope::Point; use std::{ cell::RefCell, @@ -125,10 +125,8 @@ impl SlashCommandCompletionProvider { )), new_text, label: command.label(cx), - server_id: LanguageServerId(0), - lsp_completion: Default::default(), confirm, - resolved: true, + source: CompletionSource::Custom, }) }) .collect() @@ -225,10 +223,8 @@ impl SlashCommandCompletionProvider { label: new_argument.label, new_text, documentation: None, - server_id: LanguageServerId(0), - lsp_completion: Default::default(), confirm, - resolved: true, + source: CompletionSource::Custom, } }) .collect()) diff --git a/crates/collab_ui/src/chat_panel/message_editor.rs b/crates/collab_ui/src/chat_panel/message_editor.rs index fe72c29a34..49b2547175 100644 --- a/crates/collab_ui/src/chat_panel/message_editor.rs +++ b/crates/collab_ui/src/chat_panel/message_editor.rs @@ -10,9 +10,9 @@ use gpui::{ }; use language::{ 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 std::{ cell::RefCell, @@ -309,11 +309,9 @@ impl MessageEditor { old_range: range.clone(), new_text, 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, - resolved: true, + documentation: None, + source: CompletionSource::Custom, } }) .collect() diff --git a/crates/editor/src/code_context_menus.rs b/crates/editor/src/code_context_menus.rs index 6537b21d22..83d84950e0 100644 --- a/crates/editor/src/code_context_menus.rs +++ b/crates/editor/src/code_context_menus.rs @@ -6,11 +6,11 @@ use gpui::{ }; use language::Buffer; use language::CodeLabel; -use lsp::LanguageServerId; use markdown::Markdown; use multi_buffer::{Anchor, ExcerptId}; use ordered_float::OrderedFloat; use project::lsp_store::CompletionDocumentation; +use project::CompletionSource; use project::{CodeAction, Completion, TaskSourceKind}; use std::{ @@ -233,11 +233,9 @@ impl CompletionsMenu { runs: Default::default(), filter_range: Default::default(), }, - server_id: LanguageServerId(usize::MAX), documentation: None, - lsp_completion: Default::default(), confirm: None, - resolved: true, + source: CompletionSource::Custom, }) .collect(); @@ -500,7 +498,12 @@ impl CompletionsMenu { // Ignore font weight for syntax highlighting, as we'll use it // for fuzzy matches. 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 { thickness: 1.0.into(), ..Default::default() @@ -708,7 +711,10 @@ impl CompletionsMenu { let completion = &completions[mat.candidate_id]; 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)); if mat.score >= 0.2 { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index f2ede14142..6400fe6a25 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -138,8 +138,9 @@ use multi_buffer::{ use project::{ lsp_store::{CompletionDocumentation, FormatTrigger, LspFormatTarget, OpenLspBufferHandle}, project_settings::{GitGutterSetting, ProjectSettings}, - CodeAction, Completion, CompletionIntent, DocumentHighlight, InlayHint, Location, LocationLink, - PrepareRenameResponse, Project, ProjectItem, ProjectTransaction, TaskSourceKind, + CodeAction, Completion, CompletionIntent, CompletionSource, DocumentHighlight, InlayHint, + Location, LocationLink, PrepareRenameResponse, Project, ProjectItem, ProjectTransaction, + TaskSourceKind, }; use rand::prelude::*; use rpc::{proto::*, ErrorExt}; @@ -16897,38 +16898,40 @@ fn snippet_completions( Some(Completion { old_range: range, 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 { text: matching_prefix.clone(), - runs: vec![], + runs: Vec::new(), filter_range: 0..matching_prefix.len(), }, - server_id: LanguageServerId(usize::MAX), documentation: snippet .description .clone() .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, }) }) diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 3083bc7047..037ecacbb8 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -2,9 +2,9 @@ mod signature_help; use crate::{ lsp_store::{LocalLspStore, LspStore}, - ActionVariant, CodeAction, CoreCompletion, DocumentHighlight, Hover, HoverBlock, + CodeAction, CompletionSource, CoreCompletion, DocumentHighlight, Hover, HoverBlock, HoverBlockKind, InlayHint, InlayHintLabel, InlayHintLabelPart, InlayHintLabelPartTooltip, - InlayHintTooltip, Location, LocationLink, MarkupContent, PrepareRenameResponse, + InlayHintTooltip, Location, LocationLink, LspAction, MarkupContent, PrepareRenameResponse, ProjectTransaction, ResolveState, }; use anyhow::{anyhow, Context as _, Result}; @@ -2011,9 +2011,11 @@ impl LspCommand for GetCompletions { CoreCompletion { old_range, new_text, - server_id, - lsp_completion, - resolved: false, + source: CompletionSource::Lsp { + server_id, + lsp_completion: Box::new(lsp_completion), + resolved: false, + }, } }) .collect()) @@ -2256,11 +2258,11 @@ impl LspCommand for GetCodeActions { return None; } } - ActionVariant::Action(Box::new(lsp_action)) + LspAction::Action(Box::new(lsp_action)) } lsp::CodeActionOrCommand::Command(command) => { if available_commands.contains(&command.command) { - ActionVariant::Command(command) + LspAction::Command(command) } else { return None; } diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 4060cdf2fe..304a3662ca 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -14,8 +14,8 @@ use crate::{ toolchain_store::{EmptyToolchainStore, ToolchainStoreEvent}, worktree_store::{WorktreeStore, WorktreeStoreEvent}, yarn::YarnPathStore, - ActionVariant, CodeAction, Completion, CoreCompletion, Hover, InlayHint, ProjectItem as _, - ProjectPath, ProjectTransaction, ResolveState, Symbol, ToolchainStore, + CodeAction, Completion, CompletionSource, CoreCompletion, Hover, InlayHint, LspAction, + ProjectItem as _, ProjectPath, ProjectTransaction, ResolveState, Symbol, ToolchainStore, }; use anyhow::{anyhow, Context as _, Result}; use async_trait::async_trait; @@ -1629,7 +1629,7 @@ impl LocalLspStore { action: &mut CodeAction, ) -> anyhow::Result<()> { match &mut action.lsp_action { - ActionVariant::Action(lsp_action) => { + LspAction::Action(lsp_action) => { if GetCodeActions::can_resolve_actions(&lang_server.capabilities()) && lsp_action.data.is_some() && (lsp_action.command.is_none() || lsp_action.edit.is_none()) @@ -1641,7 +1641,7 @@ impl LocalLspStore { ); } } - ActionVariant::Command(_) => {} + LspAction::Command(_) => {} } anyhow::Ok(()) } @@ -4401,26 +4401,33 @@ impl LspStore { let mut did_resolve = false; if let Some((client, project_id)) = client { for completion_index in completion_indices { - let server_id = completions.borrow()[completion_index].server_id; - - if Self::resolve_completion_remote( - project_id, - server_id, - buffer_id, - completions.clone(), - completion_index, - client.clone(), - ) - .await - .log_err() - .is_some() - { - did_resolve = true; + let server_id = { + let completion = &completions.borrow()[completion_index]; + completion.source.server_id() + }; + if let Some(server_id) = server_id { + if Self::resolve_completion_remote( + project_id, + server_id, + buffer_id, + completions.clone(), + completion_index, + client.clone(), + ) + .await + .log_err() + .is_some() + { + did_resolve = true; + } } } } else { 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 .read_with(&cx, |lsp_store, _| { @@ -4480,10 +4487,19 @@ impl LspStore { let request = { let completion = &completions.borrow()[completion_index]; - if completion.resolved { - return Ok(()); + match &completion.source { + CompletionSource::Lsp { + lsp_completion, + resolved, + .. + } => { + if *resolved { + return Ok(()); + } + server.request::(*lsp_completion.clone()) + } + CompletionSource::Custom => return Ok(()), } - server.request::(completion.lsp_completion.clone()) }; let completion_item = request.await?; @@ -4508,15 +4524,20 @@ impl LspStore { // vtsls might change the type of completion after resolution. let mut completions = completions.borrow_mut(); let completion = &mut completions[completion_index]; - if completion_item.insert_text_format != completion.lsp_completion.insert_text_format { - completion.lsp_completion.insert_text_format = completion_item.insert_text_format; + 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 completion = &mut completions[completion_index]; - completion.lsp_completion = completion_item; - completion.resolved = true; + completion.source = CompletionSource::Lsp { + lsp_completion: Box::new(completion_item), + resolved: true, + server_id: server.server_id(), + }; Ok(()) } @@ -4527,9 +4548,13 @@ impl LspStore { completion_index: usize, ) -> Result<()> { let completion_item = completions.borrow()[completion_index] - .lsp_completion - .clone(); - if let Some(lsp_documentation) = completion_item.documentation.clone() { + .source + .lsp_completion() + .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 completion = &mut completions[completion_index]; completion.documentation = Some(lsp_documentation.into()); @@ -4539,25 +4564,33 @@ impl LspStore { 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 - // So we have to update the label here anyway... - let language = snapshot.language(); - let mut new_label = match language { - Some(language) => { - adapter - .labels_for_completions(&[completion_item.clone()], language) - .await? + let mut new_label = match completion_item { + Some(completion_item) => { + // 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 + // So we have to update the label here anyway... + let language = snapshot.language(); + match language { + Some(language) => { + 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(), - } - .pop() - .flatten() - .unwrap_or_else(|| { - CodeLabel::fallback_for_completion( - &completion_item, - language.map(|language| language.as_ref()), - ) - }); + None => CodeLabel::plain( + completions.borrow()[completion_index].new_text.clone(), + None, + ), + }; ensure_uniform_list_compatible_label(&mut new_label); let mut completions = completions.borrow_mut(); @@ -4589,12 +4622,19 @@ impl LspStore { ) -> Result<()> { let lsp_completion = { let completion = &completions.borrow()[completion_index]; - if completion.resolved { - return Ok(()); + match &completion.source { + 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 { project_id, @@ -4622,8 +4662,11 @@ impl LspStore { let mut completions = completions.borrow_mut(); let completion = &mut completions[completion_index]; completion.documentation = Some(documentation); - completion.lsp_completion = lsp_completion; - completion.resolved = true; + completion.source = CompletionSource::Lsp { + server_id, + lsp_completion, + resolved: true, + }; let old_range = response .old_start @@ -4659,17 +4702,12 @@ impl LspStore { completion: Some(Self::serialize_completion(&CoreCompletion { old_range: completion.old_range, new_text: completion.new_text, - server_id: completion.server_id, - lsp_completion: completion.lsp_completion, - resolved: completion.resolved, + source: completion.source, })), } }; - let response = client.request(request).await?; - completions.borrow_mut()[completion_index].resolved = true; - - if let Some(transaction) = response.transaction { + if let Some(transaction) = client.request(request).await?.transaction { let transaction = language::proto::deserialize_transaction(transaction)?; buffer_handle .update(&mut cx, |buffer, _| { @@ -4687,8 +4725,9 @@ impl LspStore { } }) } else { - let server_id = completions.borrow()[completion_index].server_id; let Some(server) = buffer_handle.update(cx, |buffer, cx| { + let completion = &completions.borrow()[completion_index]; + let server_id = completion.source.server_id()?; Some( self.language_server_for_local_buffer(buffer, server_id, cx)? .1 @@ -4709,7 +4748,11 @@ impl LspStore { .await .context("resolving completion")?; 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 { let edits = this .update(&mut cx, |this, cx| { @@ -7139,8 +7182,7 @@ impl LspStore { Rc::new(RefCell::new(Box::new([Completion { old_range: completion.old_range, new_text: completion.new_text, - lsp_completion: completion.lsp_completion, - server_id: completion.server_id, + source: completion.source, documentation: None, label: CodeLabel { text: Default::default(), @@ -7148,7 +7190,6 @@ impl LspStore { filter_range: Default::default(), }, confirm: None, - resolved: completion.resolved, }]))), 0, false, @@ -8112,13 +8153,33 @@ impl LspStore { } 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 { old_start: Some(serialize_anchor(&completion.old_range.start)), old_end: Some(serialize_anchor(&completion.old_range.end)), new_text: completion.new_text.clone(), - server_id: completion.server_id.0 as u64, - lsp_completion: serde_json::to_vec(&completion.lsp_completion).unwrap(), - resolved: completion.resolved, + server_id, + lsp_completion, + resolved, + source, } } @@ -8131,24 +8192,28 @@ impl LspStore { .old_end .and_then(deserialize_anchor) .ok_or_else(|| anyhow!("invalid old end"))?; - let lsp_completion = serde_json::from_slice(&completion.lsp_completion)?; - Ok(CoreCompletion { old_range: old_start..old_end, new_text: completion.new_text, - server_id: LanguageServerId(completion.server_id as usize), - lsp_completion, - resolved: completion.resolved, + source: match proto::completion::Source::from_i32(completion.source) { + Some(proto::completion::Source::Custom) => CompletionSource::Custom, + 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 { let (kind, lsp_action) = match &action.lsp_action { - ActionVariant::Action(code_action) => ( + LspAction::Action(code_action) => ( proto::code_action::Kind::Action as i32, serde_json::to_vec(code_action).unwrap(), ), - ActionVariant::Command(command) => ( + LspAction::Command(command) => ( proto::code_action::Kind::Command as i32, serde_json::to_vec(command).unwrap(), ), @@ -8174,10 +8239,10 @@ impl LspStore { .ok_or_else(|| anyhow!("invalid end"))?; let lsp_action = match proto::code_action::Kind::from_i32(action.kind) { 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) => { - 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), }; @@ -8215,17 +8280,23 @@ fn remove_empty_hover_blocks(mut hover: Hover) -> Option { } async fn populate_labels_for_completions( - mut new_completions: Vec, + new_completions: Vec, language: Option>, lsp_adapter: Option>, completions: &mut Vec, ) { let lsp_completions = new_completions - .iter_mut() - .map(|completion| mem::take(&mut completion.lsp_completion)) + .iter() + .filter_map(|new_completion| { + if let CompletionSource::Lsp { lsp_completion, .. } = &new_completion.source { + Some(*lsp_completion.clone()) + } else { + None + } + }) .collect::>(); - 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 .labels_for_completions(&lsp_completions, language) .await @@ -8233,34 +8304,45 @@ async fn populate_labels_for_completions( .unwrap_or_default() } else { Vec::new() - }; + } + .into_iter() + .fuse(); - for ((completion, lsp_completion), label) in new_completions - .into_iter() - .zip(lsp_completions) - .zip(labels.into_iter().chain(iter::repeat(None))) - { - let documentation = if let Some(docs) = lsp_completion.documentation.clone() { - Some(docs.into()) - } else { - None - }; + for completion in new_completions { + match &completion.source { + CompletionSource::Lsp { lsp_completion, .. } => { + let documentation = if let Some(docs) = lsp_completion.documentation.clone() { + Some(docs.into()) + } else { + None + }; - let mut label = label.unwrap_or_else(|| { - CodeLabel::fallback_for_completion(&lsp_completion, language.as_deref()) - }); - ensure_uniform_list_compatible_label(&mut label); - - completions.push(Completion { - old_range: completion.old_range, - new_text: completion.new_text, - label, - server_id: completion.server_id, - documentation, - lsp_completion, - confirm: None, - resolved: false, - }) + let mut label = labels.next().flatten().unwrap_or_else(|| { + CodeLabel::fallback_for_completion(&lsp_completion, language.as_deref()) + }); + ensure_uniform_list_compatible_label(&mut label); + completions.push(Completion { + label, + documentation, + old_range: completion.old_range, + new_text: completion.new_text, + source: completion.source, + confirm: None, + }) + } + 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, + }) + } + } } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index a109e98f5f..2db83dcb3f 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -364,14 +364,10 @@ pub struct Completion { pub new_text: String, /// A label for this completion that is shown in the menu. pub label: CodeLabel, - /// The id of the language server that produced this completion. - pub server_id: LanguageServerId, /// The documentation for this completion. pub documentation: Option, - /// The raw completion provided by the language server. - pub lsp_completion: lsp::CompletionItem, - /// Whether this completion has been resolved, to ensure it happens once per completion. - pub resolved: bool, + /// Completion data source which it was constructed from. + pub source: CompletionSource, /// An optional callback to invoke when this completion is confirmed. /// 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. @@ -379,15 +375,53 @@ pub struct Completion { pub confirm: Option 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, + /// Whether this completion has been resolved, to ensure it happens once per completion. + resolved: bool, + }, + Custom, +} + +impl CompletionSource { + pub fn server_id(&self) -> Option { + 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 { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Completion") .field("old_range", &self.old_range) .field("new_text", &self.new_text) .field("label", &self.label) - .field("server_id", &self.server_id) .field("documentation", &self.documentation) - .field("lsp_completion", &self.lsp_completion) + .field("source", &self.source) .finish() } } @@ -397,9 +431,7 @@ impl std::fmt::Debug for Completion { pub(crate) struct CoreCompletion { old_range: Range, new_text: String, - server_id: LanguageServerId, - lsp_completion: lsp::CompletionItem, - resolved: bool, + source: CompletionSource, } /// A code action provided by a language server. @@ -411,12 +443,12 @@ pub struct CodeAction { pub range: Range, /// The raw code action provided by the language server. /// Can be either an action or a command. - pub lsp_action: ActionVariant, + pub lsp_action: LspAction, } /// An action sent back by a language server. #[derive(Clone, Debug)] -pub enum ActionVariant { +pub enum LspAction { /// An action with the full data, may have a command or may not. /// May require resolving. Action(Box), @@ -424,7 +456,7 @@ pub enum ActionVariant { Command(lsp::Command), } -impl ActionVariant { +impl LspAction { pub fn title(&self) -> &str { match self { Self::Action(action) => &action.title, @@ -4605,27 +4637,38 @@ impl Completion { /// A key that can be used to sort completions when displaying /// them to the user. pub fn sort_key(&self) -> (usize, &str) { - let kind_key = match self.lsp_completion.kind { - Some(lsp::CompletionItemKind::KEYWORD) => 0, - Some(lsp::CompletionItemKind::VARIABLE) => 1, - _ => 2, - }; + const DEFAULT_KIND_KEY: usize = 2; + let kind_key = self + .source + .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()]) } /// Whether this completion is a snippet. 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. /// /// Will return `None` if this completion's kind is not [`CompletionItemKind::COLOR`]. pub fn color(&self) -> Option { - match self.lsp_completion.kind { - Some(CompletionItemKind::COLOR) => color_extractor::extract_color(&self.lsp_completion), - _ => None, + let lsp_completion = self.source.lsp_completion()?; + if lsp_completion.kind? == CompletionItemKind::COLOR { + return color_extractor::extract_color(lsp_completion); } + None } } diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 17bf7a36ac..ae16516b7f 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -1,6 +1,6 @@ - syntax = "proto3"; package zed.messages; +import "google/protobuf/wrappers.proto"; // Looking for a number? Search "// current max" @@ -999,6 +999,12 @@ message Completion { uint64 server_id = 4; bytes lsp_completion = 5; bool resolved = 6; + Source source = 7; + + enum Source { + Custom = 0; + Lsp = 1; + } } message GetCodeActions {