editor: Add alignment setting to code completion context menu detail text (#45892)

legs and Danilo Leal created

Closes #5154

Release Notes:

- Added a setting (`completion_detail_alignment`) to change the detail
text alignment in code completion context menus.

<table>
<tr>
 <td>Right Alignment
 <td>Left Alignment
<tr>
 <td>
<img width="802" height="427" alt="image"
src="https://github.com/user-attachments/assets/46202f3d-32cc-4431-aebd-aa3c52d84993"
/>
 <td>
<img width="783" height="427" alt="image"
src="https://github.com/user-attachments/assets/d26559d2-c433-4bf7-a302-e683bd5fa9f7"
/>
</table>

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>

Change summary

assets/settings/default.json                   |  2 
crates/editor/src/code_context_menus.rs        | 78 ++++++++++++++++++-
crates/editor/src/editor.rs                    |  4 
crates/editor/src/editor_settings.rs           |  9 +
crates/settings/src/settings_content/editor.rs | 26 ++++++
crates/settings/src/vscode_import.rs           |  1 
crates/settings_ui/src/page_data.rs            | 15 +++
crates/settings_ui/src/settings_ui.rs          |  1 
8 files changed, 127 insertions(+), 9 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -284,6 +284,8 @@
   // 4. Never show the scrollbar:
   //    "never" (default)
   "completion_menu_scrollbar": "never",
+  // Whether to align detail text in code completions context menus left or right.
+  "completion_detail_alignment": "left",
   // Show method signatures in the editor, when inside parentheses.
   "auto_signature_help": false,
   // Whether to show the signature help after completion or a bracket pair inserted.

crates/editor/src/code_context_menus.rs 🔗

@@ -43,7 +43,7 @@ use crate::{
 };
 use crate::{CodeActionSource, EditorSettings};
 use collections::{HashSet, VecDeque};
-use settings::{Settings, SnippetSortOrder};
+use settings::{CompletionDetailAlignment, Settings, SnippetSortOrder};
 
 pub const MENU_GAP: Pixels = px(4.);
 pub const MENU_ASIDE_X_PADDING: Pixels = px(16.);
@@ -786,6 +786,8 @@ impl CompletionsMenu {
         cx: &mut Context<Editor>,
     ) -> AnyElement {
         let show_completion_documentation = self.show_completion_documentation;
+        let completion_detail_alignment =
+            EditorSettings::get_global(cx).completion_detail_alignment;
         let widest_completion_ix = if self.display_options.dynamic_width {
             let completions = self.completions.borrow();
             let widest_completion_ix = self
@@ -839,6 +841,7 @@ impl CompletionsMenu {
                         };
 
                         let filter_start = completion.label.filter_range.start;
+
                         let highlights = gpui::combine_highlights(
                             mat.ranges().map(|range| {
                                 (
@@ -880,8 +883,58 @@ impl CompletionsMenu {
                             }),
                         );
 
-                        let completion_label = StyledText::new(completion.label.text.clone())
-                            .with_default_highlights(&style.text, highlights);
+                        let highlights: Vec<_> = highlights.collect();
+
+                        let filter_range = &completion.label.filter_range;
+                        let full_text = &completion.label.text;
+
+                        let main_text: String = full_text[filter_range.clone()].to_string();
+                        let main_highlights: Vec<_> = highlights
+                            .iter()
+                            .filter_map(|(range, highlight)| {
+                                if range.end <= filter_range.start
+                                    || range.start >= filter_range.end
+                                {
+                                    return None;
+                                }
+                                let clamped_start =
+                                    range.start.max(filter_range.start) - filter_range.start;
+                                let clamped_end =
+                                    range.end.min(filter_range.end) - filter_range.start;
+                                Some((clamped_start..clamped_end, (*highlight)))
+                            })
+                            .collect();
+                        let main_label = StyledText::new(main_text)
+                            .with_default_highlights(&style.text, main_highlights);
+
+                        let suffix_text: String = full_text[filter_range.end..].to_string();
+                        let suffix_highlights: Vec<_> = highlights
+                            .iter()
+                            .filter_map(|(range, highlight)| {
+                                if range.end <= filter_range.end {
+                                    return None;
+                                }
+                                let shifted_start = range.start.saturating_sub(filter_range.end);
+                                let shifted_end = range.end - filter_range.end;
+                                Some((shifted_start..shifted_end, (*highlight)))
+                            })
+                            .collect();
+                        let suffix_label = if !suffix_text.is_empty() {
+                            Some(
+                                StyledText::new(suffix_text)
+                                    .with_default_highlights(&style.text, suffix_highlights),
+                            )
+                        } else {
+                            None
+                        };
+
+                        let left_aligned_suffix =
+                            matches!(completion_detail_alignment, CompletionDetailAlignment::Left);
+
+                        let right_aligned_suffix = matches!(
+                            completion_detail_alignment,
+                            CompletionDetailAlignment::Right,
+                        );
 
                         let documentation_label = match documentation {
                             Some(CompletionDocumentation::SingleLine(text))
@@ -942,7 +995,24 @@ impl CompletionsMenu {
                                         }
                                     }))
                                     .start_slot::<AnyElement>(start_slot)
-                                    .child(h_flex().overflow_hidden().child(completion_label))
+                                    .child(
+                                        h_flex()
+                                            .min_w_0()
+                                            .w_full()
+                                            .when(left_aligned_suffix, |this| this.justify_start())
+                                            .when(right_aligned_suffix, |this| {
+                                                this.justify_between()
+                                            })
+                                            .child(
+                                                div()
+                                                    .flex_none()
+                                                    .whitespace_nowrap()
+                                                    .child(main_label),
+                                            )
+                                            .when_some(suffix_label, |this, suffix| {
+                                                this.child(div().truncate().child(suffix))
+                                            }),
+                                    )
                                     .end_slot::<Label>(documentation_label),
                             )
                     })

crates/editor/src/editor.rs 🔗

@@ -53,8 +53,8 @@ pub(crate) use actions::*;
 pub use display_map::{ChunkRenderer, ChunkRendererContext, DisplayPoint, FoldPlaceholder};
 pub use edit_prediction_types::Direction;
 pub use editor_settings::{
-    CurrentLineHighlight, DocumentColorsRenderMode, EditorSettings, HideMouseMode,
-    ScrollBeyondLastLine, ScrollbarAxes, SearchSettings, ShowMinimap,
+    CompletionDetailAlignment, CurrentLineHighlight, DocumentColorsRenderMode, EditorSettings,
+    HideMouseMode, ScrollBeyondLastLine, ScrollbarAxes, SearchSettings, ShowMinimap,
 };
 pub use element::{
     CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, PointForPosition,

crates/editor/src/editor_settings.rs 🔗

@@ -4,9 +4,10 @@ use gpui::App;
 use language::CursorShape;
 use project::project_settings::DiagnosticSeverity;
 pub use settings::{
-    CurrentLineHighlight, DelayMs, DisplayIn, DocumentColorsRenderMode, DoubleClickInMultibuffer,
-    GoToDefinitionFallback, HideMouseMode, MinimapThumb, MinimapThumbBorder, MultiCursorModifier,
-    ScrollBeyondLastLine, ScrollbarDiagnostics, SeedQuerySetting, ShowMinimap, SnippetSortOrder,
+    CompletionDetailAlignment, CurrentLineHighlight, DelayMs, DisplayIn, DocumentColorsRenderMode,
+    DoubleClickInMultibuffer, GoToDefinitionFallback, HideMouseMode, MinimapThumb,
+    MinimapThumbBorder, MultiCursorModifier, ScrollBeyondLastLine, ScrollbarDiagnostics,
+    SeedQuerySetting, ShowMinimap, SnippetSortOrder,
 };
 use settings::{RegisterSetting, RelativeLineNumbers, Settings};
 use ui::scrollbars::{ScrollbarVisibility, ShowScrollbar};
@@ -57,6 +58,7 @@ pub struct EditorSettings {
     pub lsp_document_colors: DocumentColorsRenderMode,
     pub minimum_contrast_for_highlights: f32,
     pub completion_menu_scrollbar: ShowScrollbar,
+    pub completion_detail_alignment: CompletionDetailAlignment,
 }
 #[derive(Debug, Clone)]
 pub struct Jupyter {
@@ -286,6 +288,7 @@ impl Settings for EditorSettings {
             lsp_document_colors: editor.lsp_document_colors.unwrap(),
             minimum_contrast_for_highlights: editor.minimum_contrast_for_highlights.unwrap().0,
             completion_menu_scrollbar: editor.completion_menu_scrollbar.map(Into::into).unwrap(),
+            completion_detail_alignment: editor.completion_detail_alignment.unwrap(),
         }
     }
 }

crates/settings/src/settings_content/editor.rs 🔗

@@ -215,6 +215,11 @@ pub struct EditorSettingsContent {
     /// 4. Never show the scrollbar:
     ///    "never" (default)
     pub completion_menu_scrollbar: Option<ShowScrollbar>,
+
+    /// Whether to align detail text in code completions context menus left or right.
+    ///
+    /// Default: left
+    pub completion_detail_alignment: Option<CompletionDetailAlignment>,
 }
 
 #[derive(
@@ -237,6 +242,27 @@ pub enum RelativeLineNumbers {
     Wrapped,
 }
 
+#[derive(
+    Debug,
+    Default,
+    Clone,
+    Copy,
+    Serialize,
+    Deserialize,
+    JsonSchema,
+    MergeFrom,
+    PartialEq,
+    Eq,
+    strum::VariantArray,
+    strum::VariantNames,
+)]
+#[serde(rename_all = "snake_case")]
+pub enum CompletionDetailAlignment {
+    #[default]
+    Left,
+    Right,
+}
+
 impl RelativeLineNumbers {
     pub fn enabled(&self) -> bool {
         match self {

crates/settings/src/vscode_import.rs 🔗

@@ -302,6 +302,7 @@ impl VsCodeSettings {
             use_smartcase_search: self.read_bool("search.smartCase"),
             vertical_scroll_margin: self.read_f32("editor.cursorSurroundingLines"),
             completion_menu_scrollbar: None,
+            completion_detail_alignment: None,
         }
     }
 

crates/settings_ui/src/page_data.rs 🔗

@@ -6974,6 +6974,21 @@ fn language_settings_data() -> Vec<SettingsPageItem> {
             metadata: None,
             files: USER,
         }),
+        SettingsPageItem::SettingItem(SettingItem {
+            title: "Completion Detail Alignment",
+            description: "Whether to align detail text in code completions context menus left or right.",
+            field: Box::new(SettingField {
+                json_path: Some("editor.completion_detail_alignment"),
+                pick: |settings_content| {
+                    settings_content.editor.completion_detail_alignment.as_ref()
+                },
+                write: |settings_content, value| {
+                    settings_content.editor.completion_detail_alignment = value;
+                },
+            }),
+            metadata: None,
+            files: USER,
+        }),
         SettingsPageItem::SectionHeader("Inlay Hints"),
         SettingsPageItem::SettingItem(SettingItem {
             title: "Enabled",

crates/settings_ui/src/settings_ui.rs 🔗

@@ -469,6 +469,7 @@ fn init_renderers(cx: &mut App) {
         .add_basic_renderer::<settings::ShowDiagnostics>(render_dropdown)
         .add_basic_renderer::<settings::WordsCompletionMode>(render_dropdown)
         .add_basic_renderer::<settings::LspInsertMode>(render_dropdown)
+        .add_basic_renderer::<settings::CompletionDetailAlignment>(render_dropdown)
         .add_basic_renderer::<settings::AlternateScroll>(render_dropdown)
         .add_basic_renderer::<settings::TerminalBlink>(render_dropdown)
         .add_basic_renderer::<settings::CursorShapeContent>(render_dropdown)