mirror of
https://github.com/openai/codex.git
synced 2026-10-27 18:48:37 +00:00
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:
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
@@ -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()]
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user