editor: Fix completion labels not being rendered completely (#56976)

PR https://github.com/zed-industries/zed/pull/45892 changed the logic
for rendering completion labels and divided rendering up into the "main"
part (consisting of the completion's `filter_range`) and "details" part
(everything after `filter_range`), but no longer renders text that comes
before the `filter_range`. This text is probably not relevant in too
many cases but makes some Rust completions unclear and ambiguous, as can
be seen in this example:

Without this change:
<img width="1262" height="313" alt="Image"
src="https://github.com/user-attachments/assets/dcdd9da4-c449-445d-b379-771b368c3778"
/>

With this change:
<img width="1328" height="376" alt="grafik"
src="https://github.com/user-attachments/assets/05fdaf95-7a18-4d50-8e3b-35be10d9a1ef"
/>

Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Closes #56973

Release Notes:

- Fix completion labels not being rendered completely

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
This commit is contained in:
davidhi7andSmit Barmase authored and GitHub committed 2026-07-06 21:22:14 +00:00
1 parent 52575c587f
commit 4a110a578e
1 file changed
+89 -36
+89 -36
View File
@@ -1,9 +1,9 @@
use crate::scroll::ScrollAmount;
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
AnyElement, Entity, Focusable, FontWeight, ListSizingBehavior, ScrollHandle, ScrollStrategy,
SharedString, Size, StrikethroughStyle, StyledText, Task, TaskExt, UniformListScrollHandle,
div, px, uniform_list,
AnyElement, Entity, Focusable, FontWeight, HighlightStyle, ListSizingBehavior, ScrollHandle,
ScrollStrategy, SharedString, Size, StrikethroughStyle, StyledText, Task, TaskExt,
UniformListScrollHandle, div, px, uniform_list,
};
use itertools::Itertools;
use language::CodeLabel;
@@ -1006,43 +1006,18 @@ impl CompletionsMenu {
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)
let ((main_text, main_highlights), (suffix_text, suffix_highlights)) =
split_completion_label(
&completion.label.text,
&completion.label.filter_range,
&highlights,
);
let main_label = StyledText::new(main_text.to_string())
.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)
StyledText::new(suffix_text.to_string())
.with_default_highlights(&style.text, suffix_highlights),
)
} else {
@@ -1691,6 +1666,42 @@ fn completion_kind_highlight_name(kind: CompletionItemKind) -> Option<&'static s
})
}
fn split_completion_label<'a>(
text: &'a str,
filter_range: &Range<usize>,
highlights: &[(Range<usize>, HighlightStyle)],
) -> (
(&'a str, Vec<(Range<usize>, HighlightStyle)>),
(&'a str, Vec<(Range<usize>, HighlightStyle)>),
) {
let (main_text, suffix_text) = text.split_at(filter_range.end);
let main_highlights = highlights
.iter()
.filter_map(|(range, highlight)| {
if range.start >= filter_range.end {
return None;
}
let clamped_end = range.end.min(filter_range.end);
Some((range.start..clamped_end, *highlight))
})
.collect();
let suffix_highlights = 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();
(
(main_text, main_highlights),
(suffix_text, suffix_highlights),
)
}
fn exact_case_match_count(query: &str, string_match: &StringMatch) -> usize {
let mut exact_matches = 0;
let mut query_chars = query.chars();
@@ -2038,3 +2049,45 @@ impl CodeActionsMenu {
)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn bold() -> HighlightStyle {
FontWeight::BOLD.into()
}
fn colored() -> HighlightStyle {
HighlightStyle {
fade_out: Some(0.5),
..Default::default()
}
}
#[test]
fn test_split_completion_label_keeps_prefix_before_filter_range() {
let ((main_text, main_highlights), (suffix_text, suffix_highlights)) =
split_completion_label("&some_str: String", &(1..9), &[(11..17, colored())]);
assert_eq!(main_text, "&some_str");
assert_eq!(suffix_text, ": String");
assert_eq!(main_highlights, vec![]);
assert_eq!(suffix_highlights, vec![(2..8, colored())]);
}
#[test]
fn test_split_completion_label_splits_boundary_spanning_highlight() {
let ((main_text, main_highlights), (suffix_text, suffix_highlights)) =
split_completion_label(
"await.as_deref_mut(&mut self)",
&(6..18),
&[(0..29, bold())],
);
assert_eq!(main_text, "await.as_deref_mut");
assert_eq!(suffix_text, "(&mut self)");
assert_eq!(main_highlights, vec![(0..18, bold())]);
assert_eq!(suffix_highlights, vec![(0..11, bold())]);
}
}