Extract shared footer hint wrapping in the TUI (#44354)

## What changed

Replace duplicated footer wrapping in the MCP elicitation and request-user-input overlays with `wrap_hint_rows`. Preserve whole-hint grouping, display-width and separator accounting, oversized hints, and the single empty row for empty input.

## Testing

Add unit tests for Unicode display widths, whole-hint wrapping, oversized hints at zero width, and empty input.

GitOrigin-RevId: 4d8ead0acbfa3bf9241baedb86d2476566d26ebc
This commit is contained in:
Eric Traut
2026-09-10 00:57:33 +00:00
committed by copyberry
parent 0447e4a1fd
commit e2a9ee05f4
5 changed files with 92 additions and 76 deletions
@@ -1716,44 +1716,12 @@ impl BottomPaneView for McpServerElicitationOverlay {
}
fn wrap_footer_tips(width: u16, tips: Vec<FooterTip>) -> Vec<Vec<FooterTip>> {
let max_width = width.max(1) as usize;
let separator_width = UnicodeWidthStr::width(FOOTER_SEPARATOR);
if tips.is_empty() {
return vec![Vec::new()];
}
let mut lines = Vec::new();
let mut current = Vec::new();
let mut used = 0usize;
for tip in tips {
let tip_width = UnicodeWidthStr::width(tip.text.as_str()).min(max_width);
let extra = if current.is_empty() {
tip_width
} else {
separator_width.saturating_add(tip_width)
};
if !current.is_empty() && used.saturating_add(extra) > max_width {
lines.push(current);
current = Vec::new();
used = 0;
}
if current.is_empty() {
used = tip_width;
} else {
used = used
.saturating_add(separator_width)
.saturating_add(tip_width);
}
current.push(tip);
}
if current.is_empty() {
lines.push(Vec::new());
} else {
lines.push(current);
}
lines
crate::footer_hint::wrap_hint_rows(
tips,
width,
UnicodeWidthStr::width(FOOTER_SEPARATOR),
|tip| UnicodeWidthStr::width(tip.text.as_str()),
)
}
#[cfg(test)]
@@ -655,44 +655,12 @@ impl RequestUserInputOverlay {
}
fn wrap_footer_tips(&self, width: u16, tips: Vec<FooterTip>) -> Vec<Vec<FooterTip>> {
let max_width = width.max(1) as usize;
let separator_width = UnicodeWidthStr::width(TIP_SEPARATOR);
if tips.is_empty() {
return vec![Vec::new()];
}
let mut lines: Vec<Vec<FooterTip>> = Vec::new();
let mut current: Vec<FooterTip> = Vec::new();
let mut used = 0usize;
for tip in tips {
let tip_width = UnicodeWidthStr::width(tip.text.as_str()).min(max_width);
let extra = if current.is_empty() {
tip_width
} else {
separator_width.saturating_add(tip_width)
};
if !current.is_empty() && used.saturating_add(extra) > max_width {
lines.push(current);
current = Vec::new();
used = 0;
}
if current.is_empty() {
used = tip_width;
} else {
used = used
.saturating_add(separator_width)
.saturating_add(tip_width);
}
current.push(tip);
}
if current.is_empty() {
lines.push(Vec::new());
} else {
lines.push(current);
}
lines
crate::footer_hint::wrap_hint_rows(
tips,
width,
UnicodeWidthStr::width(TIP_SEPARATOR),
|tip| UnicodeWidthStr::width(tip.text.as_str()),
)
}
pub(super) fn footer_required_height(&self, width: u16) -> u16 {
+40
View File
@@ -0,0 +1,40 @@
//! Packs footer hints into rows without separating a shortcut from its label.
//! Callers retain ownership of styling and of clipping or wrapping oversized hints.
pub(crate) fn wrap_hint_rows<T>(
hints: impl IntoIterator<Item = T>,
width: u16,
separator_width: usize,
hint_width: impl Fn(&T) -> usize,
) -> Vec<Vec<T>> {
let width = usize::from(width.max(1));
let mut rows = Vec::new();
let mut row = Vec::new();
let mut used = 0usize;
for hint in hints {
let hint_width = hint_width(&hint).min(width);
let extra = if row.is_empty() {
hint_width
} else {
separator_width.saturating_add(hint_width)
};
if !row.is_empty() && used.saturating_add(extra) > width {
rows.push(std::mem::take(&mut row));
used = 0;
}
if row.is_empty() {
used = hint_width;
} else {
used = used
.saturating_add(separator_width)
.saturating_add(hint_width);
}
row.push(hint);
}
rows.push(row);
rows
}
#[cfg(test)]
#[path = "footer_hint_tests.rs"]
mod tests;
+39
View File
@@ -0,0 +1,39 @@
use super::wrap_hint_rows;
use pretty_assertions::assert_eq;
use unicode_width::UnicodeWidthStr;
#[test]
fn wraps_whole_hints_using_display_width() {
let hints = ["↑ 上", "↓ 下", "enter select"];
assert_eq!(
wrap_hint_rows(
hints,
/*width*/ 9,
/*separator_width*/ 1,
|hint| { UnicodeWidthStr::width(*hint) }
),
vec![vec!["↑ 上", "↓ 下"], vec!["enter select"]]
);
}
#[test]
fn retains_oversized_hints_and_an_empty_row() {
assert_eq!(
wrap_hint_rows(
["ctrl+x stop", "esc back"],
/*width*/ 0,
/*separator_width*/ 3,
|hint| hint.len()
),
vec![vec!["ctrl+x stop"], vec!["esc back"]]
);
assert_eq!(
wrap_hint_rows(
Vec::<String>::new(),
/*width*/ 40,
/*separator_width*/ 3,
String::len
),
vec![Vec::<String>::new()]
);
}
+1
View File
@@ -155,6 +155,7 @@ mod ide_context;
mod inline_visualization;
pub(crate) mod insert_history;
pub use insert_history::insert_history_lines;
mod footer_hint;
mod key_hint;
mod keymap;
mod keymap_setup;