From 3050260e68200496cd5451c3c45341bd379ee7cf Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Sun, 16 Aug 2026 02:38:54 +0800 Subject: [PATCH] fix(completion): a description with a newline broke its menu row MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `elide` exists to make a completion description fit one row, and it only ever bounded the length. A newline defeats the row at any length: gpui breaks on it whatever the row says, so the description grows a second line and its tail paints over whatever the menu is floating above. This is shipped data, not a hypothetical. 122 of the specs in `assets/completions` carry a newline in a description, and the break is early enough to land inside the budget the menu hands out: ten of them break before column 30. `go build -race` opens "Enable data race detection." and breaks at column 27. Folding in `elide` rather than at the call site, because every caller of it wants one line by definition — and because the early return for text already under budget was the path that handed a newline straight back untouched, which a fix at the call site would have had to remember. One cluster in, one cluster out, so the budget still measures what is drawn. Found by finishing the enumeration 1c203cc started, this time by asking which *data source* a drawn string comes from rather than which module draws it. Completion descriptions come from spec files, and 97 of those ship in this repo. --- src/terminal/view.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/terminal/view.rs b/src/terminal/view.rs index 8b3af50f..8269c246 100644 --- a/src/terminal/view.rs +++ b/src/terminal/view.rs @@ -6561,7 +6561,16 @@ fn description_budget(cell_width: f32, label_cells: usize, menu_w: f32) -> usize /// explains: a completion description is arbitrary text from a shell plugin, /// and cutting a cluster in half renders as a character the description never /// contained. +/// +/// The line breaks go first, for the same reason and a nearer one: this exists +/// to make a description fit a row, and a newline defeats that at any length. +/// 122 of the specs shipped in `assets/completions` carry one — `go build +/// -race` opens "Enable data race detection." and breaks at column 27, inside +/// any budget this menu hands out — so without the fold the row grows and its +/// tail paints over whatever the menu is floating above. Folding is one +/// cluster for one cluster, so the budget still measures what is drawn. fn elide(text: &str, budget: usize) -> String { + let text = one_line(text); let clusters: Vec<&str> = text.graphemes(true).collect(); if clusters.len() <= budget { return text.to_string(); @@ -8060,6 +8069,21 @@ mod tests { assert_eq!(elide("abcd", 4), "abcd"); } + #[test] + fn a_description_that_carries_a_line_break_still_draws_on_one_row() { + // Verbatim from `assets/completions/go.json`, which is one of 122 + // shipped specs whose descriptions contain a newline. Nothing about + // the budget saves this: the break is at column 27. + let out = elide("Enable data race detection.\nSee more at docs.", 40); + assert!(!out.contains('\n'), "{out:?}"); + assert!(out.starts_with("Enable data race detection.↵"), "{out:?}"); + + // Under budget is the case that used to slip through untouched — the + // early return handed the text straight back, newline and all. + let short = elide("a\nb", 40); + assert_eq!(short, "a↵b"); + } + #[test] fn an_elided_description_spends_its_budget_on_whole_clusters() { use unicode_segmentation::UnicodeSegmentation as _;