From 66e45818af4c93c64bf0707de1103cb23ecb34e6 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sat, 5 Jul 2025 16:20:41 +0200 Subject: [PATCH] debugger: Improve debug console autocompletions (#33868) Partially fixes: https://github.com/zed-industries/zed/discussions/33777#discussioncomment-13646294 ### Improves debug console autocompletion behavior This PR fixes a regression in completion trigger support for the debug console, as we only looked if a completion trigger, was in the beginning of the search text, but we also had to check if the current text is a word so we also show completions for variables/input that doesn't start with any of the completion triggers. We now also leverage DAP provided information to sort completion items more effectively. This results in improved prioritization, showing variable completions above classes and global scope types. I also added for completion the documentation field, that directly comes from the DAP server. NOTE: I haven't found an adapter that returns this, but it needs to have. **Before** Screenshot 2025-07-03 at 21 00 19 **After** Screenshot 2025-07-03 at 20 59 38 Release Notes: - Debugger: Improve autocompletion sorting for debug console - Debugger: Fix autocompletion menu now shown when you type - Debugger: Fix completion item showing up twice for some adapters --- .../src/session/running/console.rs | 88 ++++++++++++++----- crates/editor/src/code_context_menus.rs | 9 +- crates/project/src/lsp_store.rs | 17 +++- crates/project/src/project.rs | 4 + crates/proto/proto/lsp.proto | 2 + 5 files changed, 93 insertions(+), 27 deletions(-) diff --git a/crates/debugger_ui/src/session/running/console.rs b/crates/debugger_ui/src/session/running/console.rs index b75586020b7c2b10d96f11ad3f97f2dd5b1f2d35..9375c8820b0eb335f1d36534f219f339ec587df1 100644 --- a/crates/debugger_ui/src/session/running/console.rs +++ b/crates/debugger_ui/src/session/running/console.rs @@ -5,7 +5,7 @@ use super::{ use alacritty_terminal::vte::ansi; use anyhow::Result; use collections::HashMap; -use dap::OutputEvent; +use dap::{CompletionItem, CompletionItemType, OutputEvent}; use editor::{Bias, CompletionProvider, Editor, EditorElement, EditorStyle, ExcerptId}; use fuzzy::StringMatchCandidate; use gpui::{ @@ -17,6 +17,7 @@ use menu::{Confirm, SelectNext, SelectPrevious}; use project::{ Completion, CompletionResponse, debugger::session::{CompletionsQuery, OutputToken, Session}, + lsp_store::CompletionDocumentation, search_history::{SearchHistory, SearchHistoryCursor}, }; use settings::Settings; @@ -555,15 +556,27 @@ impl CompletionProvider for ConsoleQueryBarCompletionProvider { buffer: &Entity, position: language::Anchor, text: &str, - _trigger_in_words: bool, + trigger_in_words: bool, menu_is_open: bool, cx: &mut Context, ) -> bool { + let mut chars = text.chars(); + let char = if let Some(char) = chars.next() { + char + } else { + return false; + }; + let snapshot = buffer.read(cx).snapshot(); if !menu_is_open && !snapshot.settings_at(position, cx).show_completions_on_input { return false; } + let classifier = snapshot.char_classifier_at(position).for_completion(true); + if trigger_in_words && classifier.is_word(char) { + return true; + } + self.0 .read_with(cx, |console, cx| { console @@ -596,21 +609,28 @@ impl ConsoleQueryBarCompletionProvider { variable_list.completion_variables(cx) }) { if let Some(evaluate_name) = &variable.evaluate_name { - variables.insert(evaluate_name.clone(), variable.value.clone()); + if variables + .insert(evaluate_name.clone(), variable.value.clone()) + .is_none() + { + string_matches.push(StringMatchCandidate { + id: 0, + string: evaluate_name.clone(), + char_bag: evaluate_name.chars().collect(), + }); + } + } + + if variables + .insert(variable.name.clone(), variable.value.clone()) + .is_none() + { string_matches.push(StringMatchCandidate { id: 0, - string: evaluate_name.clone(), - char_bag: evaluate_name.chars().collect(), + string: variable.name.clone(), + char_bag: variable.name.chars().collect(), }); } - - variables.insert(variable.name.clone(), variable.value.clone()); - - string_matches.push(StringMatchCandidate { - id: 0, - string: variable.name.clone(), - char_bag: variable.name.chars().collect(), - }); } (variables, string_matches) @@ -656,11 +676,13 @@ impl ConsoleQueryBarCompletionProvider { new_text: string_match.string.clone(), label: CodeLabel { filter_range: 0..string_match.string.len(), - text: format!("{} {}", string_match.string, variable_value), + text: string_match.string.clone(), runs: Vec::new(), }, icon_path: None, - documentation: None, + documentation: Some(CompletionDocumentation::MultiLineMarkdown( + variable_value.into(), + )), confirm: None, source: project::CompletionSource::Custom, insert_text_mode: None, @@ -675,6 +697,32 @@ impl ConsoleQueryBarCompletionProvider { }) } + const fn completion_type_score(completion_type: CompletionItemType) -> usize { + match completion_type { + CompletionItemType::Field | CompletionItemType::Property => 0, + CompletionItemType::Variable | CompletionItemType::Value => 1, + CompletionItemType::Method + | CompletionItemType::Function + | CompletionItemType::Constructor => 2, + CompletionItemType::Class + | CompletionItemType::Interface + | CompletionItemType::Module => 3, + _ => 4, + } + } + + fn completion_item_sort_text(completion_item: &CompletionItem) -> String { + completion_item.sort_text.clone().unwrap_or_else(|| { + format!( + "{:03}_{}", + Self::completion_type_score( + completion_item.type_.unwrap_or(CompletionItemType::Text) + ), + completion_item.label.to_ascii_lowercase() + ) + }) + } + fn client_completions( &self, console: &Entity, @@ -699,6 +747,7 @@ impl ConsoleQueryBarCompletionProvider { let completions = completions .into_iter() .map(|completion| { + let sort_text = Self::completion_item_sort_text(&completion); let new_text = completion .text .as_ref() @@ -731,12 +780,11 @@ impl ConsoleQueryBarCompletionProvider { runs: Vec::new(), }, icon_path: None, - documentation: None, + documentation: completion.detail.map(|detail| { + CompletionDocumentation::MultiLineMarkdown(detail.into()) + }), confirm: None, - source: project::CompletionSource::BufferWord { - word_range: buffer_position..language::Anchor::MAX, - resolved: false, - }, + source: project::CompletionSource::Dap { sort_text }, insert_text_mode: None, } }) diff --git a/crates/editor/src/code_context_menus.rs b/crates/editor/src/code_context_menus.rs index 291c03422def426054457c04ab8c9e4e710112a7..8fbae8d6052d89299b10f3cd0c971af79abd3c90 100644 --- a/crates/editor/src/code_context_menus.rs +++ b/crates/editor/src/code_context_menus.rs @@ -1083,11 +1083,10 @@ impl CompletionsMenu { if lsp_completion.kind == Some(CompletionItemKind::SNIPPET) ); - let sort_text = if let CompletionSource::Lsp { lsp_completion, .. } = &completion.source - { - lsp_completion.sort_text.as_deref() - } else { - None + let sort_text = match &completion.source { + CompletionSource::Lsp { lsp_completion, .. } => lsp_completion.sort_text.as_deref(), + CompletionSource::Dap { sort_text } => Some(sort_text.as_str()), + _ => None, }; let (sort_kind, sort_label) = completion.sort_key(); diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index f2b04b9b210712107a2836c8445160a5c31bb5b0..8a14e02e0b40946ed8e81b72e6cea5eb2a6c56ef 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -6043,7 +6043,9 @@ impl LspStore { ); server.request::(*lsp_completion.clone()) } - CompletionSource::BufferWord { .. } | CompletionSource::Custom => { + CompletionSource::BufferWord { .. } + | CompletionSource::Dap { .. } + | CompletionSource::Custom => { return Ok(()); } } @@ -6195,7 +6197,9 @@ impl LspStore { } serde_json::to_string(lsp_completion).unwrap().into_bytes() } - CompletionSource::Custom | CompletionSource::BufferWord { .. } => { + CompletionSource::Custom + | CompletionSource::Dap { .. } + | CompletionSource::BufferWord { .. } => { return Ok(()); } } @@ -11081,6 +11085,10 @@ impl LspStore { serialized_completion.source = proto::completion::Source::Custom as i32; serialized_completion.resolved = true; } + CompletionSource::Dap { sort_text } => { + serialized_completion.source = proto::completion::Source::Dap as i32; + serialized_completion.sort_text = Some(sort_text.clone()); + } } serialized_completion @@ -11135,6 +11143,11 @@ impl LspStore { resolved: completion.resolved, } } + Some(proto::completion::Source::Dap) => CompletionSource::Dap { + sort_text: completion + .sort_text + .context("expected sort text to exist")?, + }, _ => anyhow::bail!("Unexpected completion source {}", completion.source), }, }) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 8a41a75d682e918a4ae698b6fe13ea8c2f9b9816..c7a1f057615c0e75414389935dbbabab9bc7155d 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -456,6 +456,10 @@ pub enum CompletionSource { /// Whether this completion has been resolved, to ensure it happens once per completion. resolved: bool, }, + Dap { + /// The sort text for this completion. + sort_text: String, + }, Custom, BufferWord { word_range: Range, diff --git a/crates/proto/proto/lsp.proto b/crates/proto/proto/lsp.proto index c0eadd5e699f4b89fff5c84169b928598bc706ea..e3c2f69c0b7587580a393b343eff1c4cd932fd72 100644 --- a/crates/proto/proto/lsp.proto +++ b/crates/proto/proto/lsp.proto @@ -222,11 +222,13 @@ message Completion { optional Anchor buffer_word_end = 10; Anchor old_insert_start = 11; Anchor old_insert_end = 12; + optional string sort_text = 13; enum Source { Lsp = 0; Custom = 1; BufferWord = 2; + Dap = 3; } }