Add option for code context menu items to have dynamic width (#37404)

Danilo Leal and Michael Sloan created

Follow up to https://github.com/zed-industries/zed/pull/30598

This PR introduces the `display_options` field in the
`CompletionResponse`, allowing a code context menu width to be
dynamically dictated based on its larger item. This will allow us to
have the @-mentions and slash commands completion menus in the agent
panel not be bigger than it needs to be. It may also be relevant/useful
in the future for other use cases.

For now, we set all instances of code context menus to use a fixed
width, as defined in the PR linked above, which means this PR shouldn't
cause any visual change.

Release Notes:

- N/A

Co-authored-by: Michael Sloan <mgsloan+github@gmail.com>

Change summary

crates/agent_ui/src/acp/completion_provider.rs            |  5 
crates/agent_ui/src/context_picker/completion_provider.rs |  6 +
crates/agent_ui/src/slash_command.rs                      |  9 +
crates/collab_ui/src/chat_panel/message_editor.rs         |  6 +
crates/debugger_ui/src/session/running/console.rs         |  4 
crates/editor/src/code_context_menus.rs                   | 41 ++++++++
crates/editor/src/editor.rs                               | 29 ++++-
crates/inspector_ui/src/div_inspector.rs                  |  6 +
crates/keymap_editor/src/keymap_editor.rs                 |  3 
crates/project/src/lsp_store.rs                           | 10 +
crates/project/src/project.rs                             | 12 ++
11 files changed, 110 insertions(+), 21 deletions(-)

Detailed changes

crates/agent_ui/src/acp/completion_provider.rs 🔗

@@ -15,7 +15,8 @@ use language::{Buffer, CodeLabel, HighlightId};
 use lsp::CompletionContext;
 use project::lsp_store::CompletionDocumentation;
 use project::{
-    Completion, CompletionIntent, CompletionResponse, Project, ProjectPath, Symbol, WorktreeId,
+    Completion, CompletionDisplayOptions, CompletionIntent, CompletionResponse, Project,
+    ProjectPath, Symbol, WorktreeId,
 };
 use prompt_store::PromptStore;
 use rope::Point;
@@ -771,6 +772,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
 
                     Ok(vec![CompletionResponse {
                         completions,
+                        display_options: CompletionDisplayOptions::default(),
                         // Since this does its own filtering (see `filter_completions()` returns false),
                         // there is no benefit to computing whether this set of completions is incomplete.
                         is_incomplete: true,
@@ -862,6 +864,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
 
                     Ok(vec![CompletionResponse {
                         completions,
+                        display_options: CompletionDisplayOptions::default(),
                         // Since this does its own filtering (see `filter_completions()` returns false),
                         // there is no benefit to computing whether this set of completions is incomplete.
                         is_incomplete: true,

crates/agent_ui/src/context_picker/completion_provider.rs 🔗

@@ -13,7 +13,10 @@ use http_client::HttpClientWithUrl;
 use itertools::Itertools;
 use language::{Buffer, CodeLabel, HighlightId};
 use lsp::CompletionContext;
-use project::{Completion, CompletionIntent, CompletionResponse, ProjectPath, Symbol, WorktreeId};
+use project::{
+    Completion, CompletionDisplayOptions, CompletionIntent, CompletionResponse, ProjectPath,
+    Symbol, WorktreeId,
+};
 use prompt_store::PromptStore;
 use rope::Point;
 use text::{Anchor, OffsetRangeExt, ToPoint};
@@ -897,6 +900,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
 
             Ok(vec![CompletionResponse {
                 completions,
+                display_options: CompletionDisplayOptions::default(),
                 // Since this does its own filtering (see `filter_completions()` returns false),
                 // there is no benefit to computing whether this set of completions is incomplete.
                 is_incomplete: true,

crates/agent_ui/src/slash_command.rs 🔗

@@ -7,7 +7,10 @@ use fuzzy::{StringMatchCandidate, match_strings};
 use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity, Window};
 use language::{Anchor, Buffer, ToPoint};
 use parking_lot::Mutex;
-use project::{CompletionIntent, CompletionSource, lsp_store::CompletionDocumentation};
+use project::{
+    CompletionDisplayOptions, CompletionIntent, CompletionSource,
+    lsp_store::CompletionDocumentation,
+};
 use rope::Point;
 use std::{
     ops::Range,
@@ -133,6 +136,7 @@ impl SlashCommandCompletionProvider {
 
                 vec![project::CompletionResponse {
                     completions,
+                    display_options: CompletionDisplayOptions::default(),
                     is_incomplete: false,
                 }]
             })
@@ -237,6 +241,7 @@ impl SlashCommandCompletionProvider {
 
                 Ok(vec![project::CompletionResponse {
                     completions,
+                    display_options: CompletionDisplayOptions::default(),
                     // TODO: Could have slash commands indicate whether their completions are incomplete.
                     is_incomplete: true,
                 }])
@@ -244,6 +249,7 @@ impl SlashCommandCompletionProvider {
         } else {
             Task::ready(Ok(vec![project::CompletionResponse {
                 completions: Vec::new(),
+                display_options: CompletionDisplayOptions::default(),
                 is_incomplete: true,
             }]))
         }
@@ -305,6 +311,7 @@ impl CompletionProvider for SlashCommandCompletionProvider {
         else {
             return Task::ready(Ok(vec![project::CompletionResponse {
                 completions: Vec::new(),
+                display_options: CompletionDisplayOptions::default(),
                 is_incomplete: false,
             }]));
         };

crates/collab_ui/src/chat_panel/message_editor.rs 🔗

@@ -12,7 +12,9 @@ use language::{
     Anchor, Buffer, BufferSnapshot, CodeLabel, LanguageRegistry, ToOffset,
     language_settings::SoftWrap,
 };
-use project::{Completion, CompletionResponse, CompletionSource, search::SearchQuery};
+use project::{
+    Completion, CompletionDisplayOptions, CompletionResponse, CompletionSource, search::SearchQuery,
+};
 use settings::Settings;
 use std::{
     ops::Range,
@@ -275,6 +277,7 @@ impl MessageEditor {
 
         Task::ready(Ok(vec![CompletionResponse {
             completions: Vec::new(),
+            display_options: CompletionDisplayOptions::default(),
             is_incomplete: false,
         }]))
     }
@@ -317,6 +320,7 @@ impl MessageEditor {
 
         CompletionResponse {
             is_incomplete: completions.len() >= LIMIT,
+            display_options: CompletionDisplayOptions::default(),
             completions,
         }
     }

crates/debugger_ui/src/session/running/console.rs 🔗

@@ -15,7 +15,7 @@ use gpui::{
 use language::{Anchor, Buffer, CodeLabel, TextBufferSnapshot, ToOffset};
 use menu::{Confirm, SelectNext, SelectPrevious};
 use project::{
-    Completion, CompletionResponse,
+    Completion, CompletionDisplayOptions, CompletionResponse,
     debugger::session::{CompletionsQuery, OutputToken, Session},
     lsp_store::CompletionDocumentation,
     search_history::{SearchHistory, SearchHistoryCursor},
@@ -685,6 +685,7 @@ impl ConsoleQueryBarCompletionProvider {
 
             Ok(vec![project::CompletionResponse {
                 is_incomplete: completions.len() >= LIMIT,
+                display_options: CompletionDisplayOptions::default(),
                 completions,
             }])
         })
@@ -797,6 +798,7 @@ impl ConsoleQueryBarCompletionProvider {
 
             Ok(vec![project::CompletionResponse {
                 completions,
+                display_options: CompletionDisplayOptions::default(),
                 is_incomplete: false,
             }])
         })

crates/editor/src/code_context_menus.rs 🔗

@@ -11,9 +11,9 @@ use language::{Buffer, LanguageName, LanguageRegistry};
 use markdown::{Markdown, MarkdownElement};
 use multi_buffer::{Anchor, ExcerptId};
 use ordered_float::OrderedFloat;
-use project::CompletionSource;
 use project::lsp_store::CompletionDocumentation;
 use project::{CodeAction, Completion, TaskSourceKind};
+use project::{CompletionDisplayOptions, CompletionSource};
 use task::DebugScenario;
 use task::TaskContext;
 
@@ -232,6 +232,7 @@ pub struct CompletionsMenu {
     markdown_cache: Rc<RefCell<VecDeque<(MarkdownCacheKey, Entity<Markdown>)>>>,
     language_registry: Option<Arc<LanguageRegistry>>,
     language: Option<LanguageName>,
+    display_options: CompletionDisplayOptions,
     snippet_sort_order: SnippetSortOrder,
 }
 
@@ -271,6 +272,7 @@ impl CompletionsMenu {
         is_incomplete: bool,
         buffer: Entity<Buffer>,
         completions: Box<[Completion]>,
+        display_options: CompletionDisplayOptions,
         snippet_sort_order: SnippetSortOrder,
         language_registry: Option<Arc<LanguageRegistry>>,
         language: Option<LanguageName>,
@@ -304,6 +306,7 @@ impl CompletionsMenu {
             markdown_cache: RefCell::new(VecDeque::new()).into(),
             language_registry,
             language,
+            display_options,
             snippet_sort_order,
         };
 
@@ -375,6 +378,7 @@ impl CompletionsMenu {
             markdown_cache: RefCell::new(VecDeque::new()).into(),
             language_registry: None,
             language: None,
+            display_options: CompletionDisplayOptions::default(),
             snippet_sort_order,
         }
     }
@@ -737,6 +741,33 @@ impl CompletionsMenu {
         cx: &mut Context<Editor>,
     ) -> AnyElement {
         let show_completion_documentation = self.show_completion_documentation;
+        let widest_completion_ix = if self.display_options.dynamic_width {
+            let completions = self.completions.borrow();
+            let widest_completion_ix = self
+                .entries
+                .borrow()
+                .iter()
+                .enumerate()
+                .max_by_key(|(_, mat)| {
+                    let completion = &completions[mat.candidate_id];
+                    let documentation = &completion.documentation;
+
+                    let mut len = completion.label.text.chars().count();
+                    if let Some(CompletionDocumentation::SingleLine(text)) = documentation {
+                        if show_completion_documentation {
+                            len += text.chars().count();
+                        }
+                    }
+
+                    len
+                })
+                .map(|(ix, _)| ix);
+            drop(completions);
+            widest_completion_ix
+        } else {
+            None
+        };
+
         let selected_item = self.selected_item;
         let completions = self.completions.clone();
         let entries = self.entries.clone();
@@ -863,7 +894,13 @@ impl CompletionsMenu {
         .max_h(max_height_in_lines as f32 * window.line_height())
         .track_scroll(self.scroll_handle.clone())
         .with_sizing_behavior(ListSizingBehavior::Infer)
-        .w(rems(34.));
+        .map(|this| {
+            if self.display_options.dynamic_width {
+                this.with_width_from_item(widest_completion_ix)
+            } else {
+                this.w(rems(34.))
+            }
+        });
 
         Popover::new().child(list).into_any_element()
     }

crates/editor/src/editor.rs 🔗

@@ -147,21 +147,22 @@ use multi_buffer::{
 use parking_lot::Mutex;
 use persistence::DB;
 use project::{
-    BreakpointWithPosition, CodeAction, Completion, CompletionIntent, CompletionResponse,
-    CompletionSource, DisableAiSettings, DocumentHighlight, InlayHint, Location, LocationLink,
-    PrepareRenameResponse, Project, ProjectItem, ProjectPath, ProjectTransaction, TaskSourceKind,
-    debugger::breakpoint_store::Breakpoint,
+    BreakpointWithPosition, CodeAction, Completion, CompletionDisplayOptions, CompletionIntent,
+    CompletionResponse, CompletionSource, DisableAiSettings, DocumentHighlight, InlayHint,
+    Location, LocationLink, PrepareRenameResponse, Project, ProjectItem, ProjectPath,
+    ProjectTransaction, TaskSourceKind,
     debugger::{
         breakpoint_store::{
-            BreakpointEditAction, BreakpointSessionState, BreakpointState, BreakpointStore,
-            BreakpointStoreEvent,
+            Breakpoint, BreakpointEditAction, BreakpointSessionState, BreakpointState,
+            BreakpointStore, BreakpointStoreEvent,
         },
         session::{Session, SessionEvent},
     },
     git_store::{GitStoreEvent, RepositoryEvent},
     lsp_store::{CompletionDocumentation, FormatTrigger, LspFormatTarget, OpenLspBufferHandle},
-    project_settings::{DiagnosticSeverity, GoToDiagnosticSeverityFilter},
-    project_settings::{GitGutterSetting, ProjectSettings},
+    project_settings::{
+        DiagnosticSeverity, GitGutterSetting, GoToDiagnosticSeverityFilter, ProjectSettings,
+    },
 };
 use rand::{seq::SliceRandom, thread_rng};
 use rpc::{ErrorCode, ErrorExt, proto::PeerId};
@@ -5635,17 +5636,25 @@ impl Editor {
             // that having one source with `is_incomplete: true` doesn't cause all to be re-queried.
             let mut completions = Vec::new();
             let mut is_incomplete = false;
+            let mut display_options: Option<CompletionDisplayOptions> = None;
             if let Some(provider_responses) = provider_responses.await.log_err()
                 && !provider_responses.is_empty()
             {
                 for response in provider_responses {
                     completions.extend(response.completions);
                     is_incomplete = is_incomplete || response.is_incomplete;
+                    match display_options.as_mut() {
+                        None => {
+                            display_options = Some(response.display_options);
+                        }
+                        Some(options) => options.merge(&response.display_options),
+                    }
                 }
                 if completion_settings.words == WordsCompletionMode::Fallback {
                     words = Task::ready(BTreeMap::default());
                 }
             }
+            let display_options = display_options.unwrap_or_default();
 
             let mut words = words.await;
             if let Some(word_to_exclude) = &word_to_exclude {
@@ -5687,6 +5696,7 @@ impl Editor {
                         is_incomplete,
                         buffer.clone(),
                         completions.into(),
+                        display_options,
                         snippet_sort_order,
                         languages,
                         language,
@@ -22260,6 +22270,7 @@ fn snippet_completions(
     if scopes.is_empty() {
         return Task::ready(Ok(CompletionResponse {
             completions: vec![],
+            display_options: CompletionDisplayOptions::default(),
             is_incomplete: false,
         }));
     }
@@ -22284,6 +22295,7 @@ fn snippet_completions(
             if last_word.is_empty() {
                 return Ok(CompletionResponse {
                     completions: vec![],
+                    display_options: CompletionDisplayOptions::default(),
                     is_incomplete: true,
                 });
             }
@@ -22405,6 +22417,7 @@ fn snippet_completions(
 
         Ok(CompletionResponse {
             completions,
+            display_options: CompletionDisplayOptions::default(),
             is_incomplete,
         })
     })

crates/inspector_ui/src/div_inspector.rs 🔗

@@ -14,7 +14,10 @@ use language::{
     DiagnosticSeverity, LanguageServerId, Point, ToOffset as _, ToPoint as _,
 };
 use project::lsp_store::CompletionDocumentation;
-use project::{Completion, CompletionResponse, CompletionSource, Project, ProjectPath};
+use project::{
+    Completion, CompletionDisplayOptions, CompletionResponse, CompletionSource, Project,
+    ProjectPath,
+};
 use std::fmt::Write as _;
 use std::ops::Range;
 use std::path::Path;
@@ -664,6 +667,7 @@ impl CompletionProvider for RustStyleCompletionProvider {
                     confirm: None,
                 })
                 .collect(),
+            display_options: CompletionDisplayOptions::default(),
             is_incomplete: false,
         }]))
     }

crates/keymap_editor/src/keymap_editor.rs 🔗

@@ -22,7 +22,7 @@ use gpui::{
 };
 use language::{Language, LanguageConfig, ToOffset as _};
 use notifications::status_toast::{StatusToast, ToastIcon};
-use project::Project;
+use project::{CompletionDisplayOptions, Project};
 use settings::{BaseKeymap, KeybindSource, KeymapFile, Settings as _, SettingsAssets};
 use ui::{
     ActiveTheme as _, App, Banner, BorrowAppContext, ContextMenu, IconButtonShape, Indicator,
@@ -2911,6 +2911,7 @@ impl CompletionProvider for KeyContextCompletionProvider {
                     confirm: None,
                 })
                 .collect(),
+            display_options: CompletionDisplayOptions::default(),
             is_incomplete: false,
         }]))
     }

crates/project/src/lsp_store.rs 🔗

@@ -16,10 +16,10 @@ pub mod lsp_ext_command;
 pub mod rust_analyzer_ext;
 
 use crate::{
-    CodeAction, ColorPresentation, Completion, CompletionResponse, CompletionSource,
-    CoreCompletion, DocumentColor, Hover, InlayHint, LocationLink, LspAction, LspPullDiagnostics,
-    ManifestProvidersStore, Project, ProjectItem, ProjectPath, ProjectTransaction,
-    PulledDiagnostics, ResolveState, Symbol,
+    CodeAction, ColorPresentation, Completion, CompletionDisplayOptions, CompletionResponse,
+    CompletionSource, CoreCompletion, DocumentColor, Hover, InlayHint, LocationLink, LspAction,
+    LspPullDiagnostics, ManifestProvidersStore, Project, ProjectItem, ProjectPath,
+    ProjectTransaction, PulledDiagnostics, ResolveState, Symbol,
     buffer_store::{BufferStore, BufferStoreEvent},
     environment::ProjectEnvironment,
     lsp_command::{self, *},
@@ -5828,6 +5828,7 @@ impl LspStore {
                 .await;
                 Ok(vec![CompletionResponse {
                     completions,
+                    display_options: CompletionDisplayOptions::default(),
                     is_incomplete: completion_response.is_incomplete,
                 }])
             })
@@ -5920,6 +5921,7 @@ impl LspStore {
                         .await;
                     Some(CompletionResponse {
                         completions,
+                        display_options: CompletionDisplayOptions::default(),
                         is_incomplete: completion_response.is_incomplete,
                     })
                 });

crates/project/src/project.rs 🔗

@@ -574,11 +574,23 @@ impl std::fmt::Debug for Completion {
 /// Response from a source of completions.
 pub struct CompletionResponse {
     pub completions: Vec<Completion>,
+    pub display_options: CompletionDisplayOptions,
     /// When false, indicates that the list is complete and so does not need to be re-queried if it
     /// can be filtered instead.
     pub is_incomplete: bool,
 }
 
+#[derive(Default)]
+pub struct CompletionDisplayOptions {
+    pub dynamic_width: bool,
+}
+
+impl CompletionDisplayOptions {
+    pub fn merge(&mut self, other: &CompletionDisplayOptions) {
+        self.dynamic_width = self.dynamic_width && other.dynamic_width;
+    }
+}
+
 /// Response from language server completion request.
 #[derive(Clone, Debug, Default)]
 pub(crate) struct CoreCompletionResponse {