editor: Fix `fade_out` styling for completion labels without highlights (#45936)

Shuhei Kadowaki and MrSubidubi created

LSP's `CompletionItemKind` is defined by the protocol and may not have a
corresponding highlight name in a language's `highlights.scm`. For
example, Julia's grammar defines `@function.call` but not `@function`,
so completions with `Method` kind cannot resolve a highlight. Since the
mapping from `CompletionItemKind` to highlight names is an internal
implementation detail that is not guaranteed to be stable, the fallback
behavior should provide consistent styling regardless of grammar
definitions.

Bundled language extensions (e.g., Rust, TypeScript) apply `fade_out` or
muted styling to the description portion of completion labels, so the
fallback path and extension-provided labels should match this behavior.

Previously, the description portion could fail to receive the expected
`fade_out` styling in several independent cases:

1. When `runs` was empty (grammar lacks the highlight name), the
`flat_map` loop never executed
2. When theme lacked a style for the `highlight_id`, early return
skipped the `fade_out` logic
3. When extensions used `Literal` spans with `highlight_name: None`,
`HighlightId::default()` was still added to `runs`, preventing
`fade_out` from being applied

The fix ensures description portions consistently receive `fade_out`
styling regardless of whether the label portion can be highlighted, both
for fallback completions and extension-provided labels.

Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...

---------

Co-authored-by: MrSubidubi <finn@zed.dev>

Change summary

crates/editor/src/editor.rs                            | 85 ++++++-----
crates/language_extension/src/extension_lsp_adapter.rs |  9 
2 files changed, 52 insertions(+), 42 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -28848,49 +28848,58 @@ pub fn styled_runs_for_code_label<'a>(
         ..Default::default()
     };
 
+    if label.runs.is_empty() {
+        let desc_start = label.filter_range.end;
+        let fade_run =
+            (desc_start < label.text.len()).then(|| (desc_start..label.text.len(), fade_out));
+        return Either::Left(fade_run.into_iter());
+    }
+
     let mut prev_end = label.filter_range.end;
-    label
-        .runs
-        .iter()
-        .enumerate()
-        .flat_map(move |(ix, (range, highlight_id))| {
-            let style = if *highlight_id == language::HighlightId::TABSTOP_INSERT_ID {
-                HighlightStyle {
-                    color: Some(local_player.cursor),
-                    ..Default::default()
-                }
-            } else if *highlight_id == language::HighlightId::TABSTOP_REPLACE_ID {
-                HighlightStyle {
-                    background_color: Some(local_player.selection),
-                    ..Default::default()
-                }
-            } else if let Some(style) = syntax_theme.get(*highlight_id).cloned() {
-                style
-            } else {
-                return Default::default();
-            };
-            let muted_style = style.highlight(fade_out);
+    Either::Right(
+        label
+            .runs
+            .iter()
+            .enumerate()
+            .flat_map(move |(ix, (range, highlight_id))| {
+                let style = if *highlight_id == language::HighlightId::TABSTOP_INSERT_ID {
+                    HighlightStyle {
+                        color: Some(local_player.cursor),
+                        ..Default::default()
+                    }
+                } else if *highlight_id == language::HighlightId::TABSTOP_REPLACE_ID {
+                    HighlightStyle {
+                        background_color: Some(local_player.selection),
+                        ..Default::default()
+                    }
+                } else if let Some(style) = syntax_theme.get(*highlight_id).cloned() {
+                    style
+                } else {
+                    return Default::default();
+                };
 
-            let mut runs = SmallVec::<[(Range<usize>, HighlightStyle); 3]>::new();
-            if range.start >= label.filter_range.end {
-                if range.start > prev_end {
-                    runs.push((prev_end..range.start, fade_out));
+                let mut runs = SmallVec::<[(Range<usize>, HighlightStyle); 3]>::new();
+                let muted_style = style.highlight(fade_out);
+                if range.start >= label.filter_range.end {
+                    if range.start > prev_end {
+                        runs.push((prev_end..range.start, fade_out));
+                    }
+                    runs.push((range.clone(), muted_style));
+                } else if range.end <= label.filter_range.end {
+                    runs.push((range.clone(), style));
+                } else {
+                    runs.push((range.start..label.filter_range.end, style));
+                    runs.push((label.filter_range.end..range.end, muted_style));
                 }
-                runs.push((range.clone(), muted_style));
-            } else if range.end <= label.filter_range.end {
-                runs.push((range.clone(), style));
-            } else {
-                runs.push((range.start..label.filter_range.end, style));
-                runs.push((label.filter_range.end..range.end, muted_style));
-            }
-            prev_end = cmp::max(prev_end, range.end);
+                prev_end = cmp::max(prev_end, range.end);
 
-            if ix + 1 == label.runs.len() && label.text.len() > prev_end {
-                runs.push((prev_end..label.text.len(), fade_out));
-            }
+                if ix + 1 == label.runs.len() && label.text.len() > prev_end {
+                    runs.push((prev_end..label.text.len(), fade_out));
+                }
 
-            runs
-        })
+                runs
+            }),
+    )
 }
 
 pub(crate) fn split_words(text: &str) -> impl std::iter::Iterator<Item = &str> + '_ {

crates/language_extension/src/extension_lsp_adapter.rs 🔗

@@ -547,15 +547,16 @@ fn build_code_label(
                 text.push_str(code_span);
             }
             extension::CodeLabelSpan::Literal(span) => {
-                let highlight_id = language
+                if let Some(highlight_id) = language
                     .grammar()
                     .zip(span.highlight_name.as_ref())
                     .and_then(|(grammar, highlight_name)| {
                         grammar.highlight_id_for_name(highlight_name)
                     })
-                    .unwrap_or_default();
-                let ix = text.len();
-                runs.push((ix..ix + span.text.len(), highlight_id));
+                {
+                    let ix = text.len();
+                    runs.push((ix..ix + span.text.len(), highlight_id));
+                }
                 text.push_str(&span.text);
             }
         }