feat: configure command and search popup anchors

This commit is contained in:
Valdemar Lorenzen
2026-10-12 13:41:45 +02:00
parent 406f45c6fb
commit 6eb99a792a
11 changed files with 440 additions and 10 deletions
+139 -2
View File
@@ -33,7 +33,7 @@ use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use crate::{
chrome::{ChromeLine, SegmentConfig},
chrome::{ChromeLine, PopupAnchor, SegmentConfig},
diff::{ChangedFile, DiffState, DisplayRow, FileStatus, Focus, PaneLayout, Phase, RowKind},
editor::{
Completion, Cursor, Editor, Hover, Input, Mode, Palette, Selection, StartupDiagnostic,
@@ -1840,7 +1840,32 @@ fn draw_floating_input(frame: &mut Frame, editor: &Editor, area: Rect) -> Option
}
let width = area.width.min(72);
let height = area.height.min(3);
let panel = Rect::new(area.x, area.bottom() - height, width, height);
let anchor = if editor.mode() == Mode::Search {
editor.popup_config().search.anchor
} else {
editor.popup_config().command.anchor
};
let x = area.x
+ match anchor {
PopupAnchor::TopLeft | PopupAnchor::CenterLeft | PopupAnchor::BottomLeft => 0,
PopupAnchor::TopCenter | PopupAnchor::Center | PopupAnchor::BottomCenter => {
(area.width - width) / 2
}
PopupAnchor::TopRight | PopupAnchor::CenterRight | PopupAnchor::BottomRight => {
area.width - width
}
};
let y = area.y
+ match anchor {
PopupAnchor::TopLeft | PopupAnchor::TopCenter | PopupAnchor::TopRight => 0,
PopupAnchor::CenterLeft | PopupAnchor::Center | PopupAnchor::CenterRight => {
(area.height - height) / 2
}
PopupAnchor::BottomLeft | PopupAnchor::BottomCenter | PopupAnchor::BottomRight => {
area.height - height
}
};
let panel = Rect::new(x, y, width, height);
let bordered = panel.width >= 4 && panel.height >= 3;
let padding = u16::from(panel.width.saturating_sub(2 * u16::from(bordered)) >= 3);
let mut block = Block::default()
@@ -5638,6 +5663,118 @@ mod tests {
assert_eq!(panel.bottom(), 12);
}
#[test]
fn configured_floating_inputs_paint_each_anchor_independently_inside_the_workspace() {
let anchors = [
(PopupAnchor::TopLeft, 0, 0),
(PopupAnchor::TopCenter, 14, 0),
(PopupAnchor::TopRight, 29, 0),
(PopupAnchor::CenterLeft, 0, 5),
(PopupAnchor::Center, 14, 5),
(PopupAnchor::CenterRight, 29, 5),
(PopupAnchor::BottomLeft, 0, 10),
(PopupAnchor::BottomCenter, 14, 10),
(PopupAnchor::BottomRight, 29, 10),
];
for (index, &(command_anchor, command_x, command_y)) in anchors.iter().enumerate() {
let (search_anchor, search_x, search_y) = anchors[(index + 1) % anchors.len()];
for (prefix, title, x, y) in [
(':', "Command", command_x, command_y),
('/', "Search", search_x, search_y),
] {
let mut editor = Editor::open(None, vec![]).unwrap();
let mut popup = *editor.popup_config();
popup.command.anchor = command_anchor;
popup.search.anchor = search_anchor;
editor.set_popup_config(popup);
editor.set_message("hidden notification");
editor.handle(Input::Char(prefix));
for ch in "needle".chars() {
editor.handle(Input::Char(ch));
}
for (width, height) in [
(101, 13),
(12, 3),
(3, 2),
(2, 1),
(1, 1),
(0, 1),
(1, 0),
(0, 0),
] {
let workspace = Rect::new(5, 4, width, height);
let mut terminal =
Terminal::new(ratatui::backend::TestBackend::new(120, 24)).unwrap();
let mut panels = WheelPanels::default();
terminal
.draw(|frame| {
frame.render_widget(
Paragraph::new(vec![Line::from(".".repeat(120)); 24]),
frame.area(),
);
draw_overlays(frame, &editor, workspace, None, &mut panels);
})
.unwrap();
assert!(panels.notification.is_none());
if width == 0 || height == 0 {
assert!(panels.input.is_none());
assert_eq!(
terminal.backend().buffer()[(workspace.x, workspace.y)].symbol(),
"."
);
continue;
}
let expected = if width == 101 {
Rect::new(workspace.x + x, workspace.y + y, 72, 3)
} else {
workspace
};
let panel = panels.input.unwrap();
assert_eq!(
panel, expected,
"{prefix} {command_anchor:?} {search_anchor:?}"
);
assert_eq!(panel.intersection(workspace), panel);
let cursor = terminal.get_cursor_position().unwrap();
assert!(panel.contains(cursor));
let buffer = terminal.backend().buffer();
assert_eq!(buffer[(0, 0)].symbol(), ".");
assert_eq!(buffer[(cursor.x, cursor.y)].symbol(), " ");
if width >= 12 {
assert_eq!(buffer[(panel.x, panel.y)].symbol(), "┌");
assert_eq!(buffer[(panel.right() - 1, panel.y)].symbol(), "┐");
assert_eq!(buffer[(panel.x, panel.bottom() - 1)].symbol(), "└");
assert_eq!(
buffer[(panel.right() - 1, panel.bottom() - 1)].symbol(),
"┘"
);
assert!(
pane_screen(&terminal, Rect::new(panel.x, panel.y, panel.width, 1))
.contains(title)
);
assert_eq!(buffer[(panel.x + 1, panel.y + 1)].symbol(), " ");
assert_eq!(buffer[(panel.x + 2, panel.y + 1)].symbol(), "n");
assert_eq!(
pane_screen(
&terminal,
Rect::new(panel.x + 1, panel.y + 1, panel.width - 2, 1)
)
.trim(),
"needle"
);
assert_eq!(cursor, Position::new(panel.x + 8, panel.y + 1));
} else {
let cursor_x = u16::from(width > 1);
assert_eq!(cursor, Position::new(panel.x + cursor_x, panel.y));
if width == 2 {
assert_eq!(buffer[(panel.x, panel.y)].symbol(), "e");
}
}
}
}
}
}
#[test]
fn floating_inputs_share_workspace_geometry_across_documents_splits_and_diffs() {
for context in ["document", "split", "diff", "split-diff", "selector"] {