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

Kirill Bulatov created

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

Change summary

crates/assistant/src/inline_assistant.rs             |   4 
crates/assistant2/src/inline_assistant.rs            |   4 
crates/assistant_context_editor/src/slash_command.rs |  12 
crates/collab_ui/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(-)

Detailed changes

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()
                 })),

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()
                 })),

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())

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()

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 {

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,
                 })
             })

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;
                         }

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::request::ResolveCompletionItem>(*lsp_completion.clone())
+                }
+                CompletionSource::Custom => return Ok(()),
             }
-            server.request::<lsp::request::ResolveCompletionItem>(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<Hover> {
 }
 
 async fn populate_labels_for_completions(
-    mut new_completions: Vec<CoreCompletion>,
+    new_completions: Vec<CoreCompletion>,
     language: Option<Arc<Language>>,
     lsp_adapter: Option<Arc<CachedLspAdapter>>,
     completions: &mut Vec<Completion>,
 ) {
     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::<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
             .labels_for_completions(&lsp_completions, language)
             .await
@@ -8233,34 +8304,45 @@ async fn populate_labels_for_completions(
             .unwrap_or_default()
     } else {
         Vec::new()
-    };
-
-    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
-        };
+    }
+    .into_iter()
+    .fuse();
 
-        let mut label = label.unwrap_or_else(|| {
-            CodeLabel::fallback_for_completion(&lsp_completion, language.as_deref())
-        });
-        ensure_uniform_list_compatible_label(&mut label);
+    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
+                };
 
-        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,
+                })
+            }
+        }
     }
 }
 

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<CompletionDocumentation>,
-    /// 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<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 {
     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<Anchor>,
     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<Anchor>,
     /// 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<lsp::CodeAction>),
@@ -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<Hsla> {
-        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
     }
 }
 

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 {