fix alignment in slash command popup (#3937)

This commit is contained in:
Jeremy Rose
2025-09-19 12:08:04 -07:00
committed by GitHub
parent 9b18875a42
commit ff389dc52f
5 changed files with 60 additions and 56 deletions

View File

@@ -95,32 +95,10 @@ impl CommandPopup {
/// Determine the preferred height of the popup for a given width. /// Determine the preferred height of the popup for a given width.
/// Accounts for wrapped descriptions so that long tooltips don't overflow. /// Accounts for wrapped descriptions so that long tooltips don't overflow.
pub(crate) fn calculate_required_height(&self, width: u16) -> u16 { pub(crate) fn calculate_required_height(&self, width: u16) -> u16 {
use super::selection_popup_common::GenericDisplayRow;
use super::selection_popup_common::measure_rows_height; use super::selection_popup_common::measure_rows_height;
let matches = self.filtered(); let rows = self.rows_from_matches(self.filtered());
let rows_all: Vec<GenericDisplayRow> = if matches.is_empty() {
Vec::new()
} else {
matches
.into_iter()
.map(|(item, indices, _)| match item {
CommandItem::Builtin(cmd) => GenericDisplayRow {
name: format!("/{}", cmd.command()),
match_indices: indices.map(|v| v.into_iter().map(|i| i + 1).collect()),
is_current: false,
description: Some(cmd.description().to_string()),
},
CommandItem::UserPrompt(i) => GenericDisplayRow {
name: format!("/{}", self.prompts[i].name),
match_indices: indices.map(|v| v.into_iter().map(|i| i + 1).collect()),
is_current: false,
description: Some("send saved prompt".to_string()),
},
})
.collect()
};
measure_rows_height(&rows_all, &self.state, MAX_POPUP_ROWS, width) measure_rows_height(&rows, &self.state, MAX_POPUP_ROWS, width)
} }
/// Compute fuzzy-filtered matches over built-in commands and user prompts, /// Compute fuzzy-filtered matches over built-in commands and user prompts,
@@ -172,6 +150,32 @@ impl CommandPopup {
self.filtered().into_iter().map(|(c, _, _)| c).collect() self.filtered().into_iter().map(|(c, _, _)| c).collect()
} }
fn rows_from_matches(
&self,
matches: Vec<(CommandItem, Option<Vec<usize>>, i32)>,
) -> Vec<GenericDisplayRow> {
matches
.into_iter()
.map(|(item, indices, _)| {
let (name, description) = match item {
CommandItem::Builtin(cmd) => {
(format!("/{}", cmd.command()), cmd.description().to_string())
}
CommandItem::UserPrompt(i) => (
format!("/{}", self.prompts[i].name),
"send saved prompt".to_string(),
),
};
GenericDisplayRow {
name,
match_indices: indices.map(|v| v.into_iter().map(|i| i + 1).collect()),
is_current: false,
description: Some(description),
}
})
.collect()
}
/// Move the selection cursor one step up. /// Move the selection cursor one step up.
pub(crate) fn move_up(&mut self) { pub(crate) fn move_up(&mut self) {
let len = self.filtered_items().len(); let len = self.filtered_items().len();
@@ -198,32 +202,11 @@ impl CommandPopup {
impl WidgetRef for CommandPopup { impl WidgetRef for CommandPopup {
fn render_ref(&self, area: Rect, buf: &mut Buffer) { fn render_ref(&self, area: Rect, buf: &mut Buffer) {
let matches = self.filtered(); let rows = self.rows_from_matches(self.filtered());
let rows_all: Vec<GenericDisplayRow> = if matches.is_empty() {
Vec::new()
} else {
matches
.into_iter()
.map(|(item, indices, _)| match item {
CommandItem::Builtin(cmd) => GenericDisplayRow {
name: format!("/{}", cmd.command()),
match_indices: indices.map(|v| v.into_iter().map(|i| i + 1).collect()),
is_current: false,
description: Some(cmd.description().to_string()),
},
CommandItem::UserPrompt(i) => GenericDisplayRow {
name: format!("/{}", self.prompts[i].name),
match_indices: indices.map(|v| v.into_iter().map(|i| i + 1).collect()),
is_current: false,
description: Some("send saved prompt".to_string()),
},
})
.collect()
};
render_rows( render_rows(
area, area,
buf, buf,
&rows_all, &rows,
&self.state, &self.state,
MAX_POPUP_ROWS, MAX_POPUP_ROWS,
false, false,

View File

@@ -15,6 +15,7 @@ use ratatui::widgets::Paragraph;
use ratatui::widgets::Widget; use ratatui::widgets::Widget;
use super::scroll_state::ScrollState; use super::scroll_state::ScrollState;
use crate::ui_consts::LIVE_PREFIX_COLS;
/// A generic representation of a display row for selection popups. /// A generic representation of a display row for selection popups.
pub(crate) struct GenericDisplayRow { pub(crate) struct GenericDisplayRow {
@@ -99,14 +100,34 @@ pub(crate) fn render_rows(
.border_style(Style::default().add_modifier(Modifier::DIM)); .border_style(Style::default().add_modifier(Modifier::DIM));
block.render(area, buf); block.render(area, buf);
// Content renders to the right of the border. // Content renders to the right of the border with the same live prefix
// padding used by the composer so the popup aligns with the input text.
let prefix_cols = LIVE_PREFIX_COLS;
let content_area = Rect { let content_area = Rect {
x: area.x.saturating_add(1), x: area.x.saturating_add(prefix_cols),
y: area.y, y: area.y,
width: area.width.saturating_sub(1), width: area.width.saturating_sub(prefix_cols),
height: area.height, height: area.height,
}; };
// Clear the padding column(s) so stale characters never peek between the
// border and the popup contents.
let padding_cols = prefix_cols.saturating_sub(1);
if padding_cols > 0 {
let pad_start = area.x.saturating_add(1);
let pad_end = pad_start
.saturating_add(padding_cols)
.min(area.x.saturating_add(area.width));
let pad_bottom = area.y.saturating_add(area.height);
for x in pad_start..pad_end {
for y in area.y..pad_bottom {
if let Some(cell) = buf.cell_mut((x, y)) {
cell.set_symbol(" ");
}
}
}
}
if rows_all.is_empty() { if rows_all.is_empty() {
if content_area.height > 0 { if content_area.height > 0 {
let para = Paragraph::new(Line::from(empty_message.dim().italic())); let para = Paragraph::new(Line::from(empty_message.dim().italic()));