style: title and pad floating input windows

This commit is contained in:
Valdemar Lorenzen
2026-10-12 12:55:47 +02:00
parent 52915ffacf
commit 684ba46a2c
3 changed files with 84 additions and 30 deletions
+2
View File
@@ -8,6 +8,8 @@
- [x] diff view: add character-wise change highlight (enabled by default). - [x] diff view: add character-wise change highlight (enabled by default).
- [x] add tabs: tab support, editor action to open a buffer in a new tab, default tab navigation is `ctrl+option+h/l`, close tab action. - [x] add tabs: tab support, editor action to open a buffer in a new tab, default tab navigation is `ctrl+option+h/l`, close tab action.
- per-tab pane layouts, views, explorers, and comparisons; shared buffers, tab bar, and guarded close/force-close actions. - per-tab pane layouts, views, explorers, and comparisons; shared buffers, tab bar, and guarded close/force-close actions.
- [ ] make diff selector close on escape.
- [ ] `:` currently overrides the diff selector input (and maybe other input fields) and spawns the command input on top. this is of course not intended behavior and should be fixed.
- [ ] diff view: lsp actions should act as they normally would. if an lsp action would open another file, then the file should be opened in another tab outside the diff view - if the referenced file/symbol is part of the changes in the diff, it should just be opened in the diff view. - [ ] diff view: lsp actions should act as they normally would. if an lsp action would open another file, then the file should be opened in another tab outside the diff view - if the referenced file/symbol is part of the changes in the diff, it should just be opened in the diff view.
- this will require tab support. - this will require tab support.
- [ ] diff view: change diff view modes; choose between split and unified diff view (split by default). - [ ] diff view: change diff view modes; choose between split and unified diff view (split by default).
+2 -2
View File
@@ -142,9 +142,9 @@ Submitting an empty search repeats the last query. Esc cancels without moving th
## Commands, saving, and pasted input ## Commands, saving, and pasted input
`:` opens compact floating command input; `/` opens the same kind of input for buffer text search. Enter submits and Escape cancels using the existing modal rules. Long Unicode input scrolls horizontally without changing buffer text. Input is bounded by the workspace, above configured bars, and temporarily hides operation notifications; there is no permanent prompt/footer row. `:` opens compact floating command input titled **Command**; `/` opens input titled **Search** for buffer text search. Titles sit at the top-left border, replacing the displayed leading `:` / `/`. Both input fields have one column of padding on each side. Enter submits and Escape cancels using the existing modal rules. Long Unicode input scrolls horizontally without changing buffer text. Input is bounded by the workspace, above configured bars, and temporarily hides operation notifications; there is no permanent prompt/footer row. Tiny windows omit borders/titles or padding when needed to keep input usable.
The latest reported operation result appears in a non-focusable popup at the workspace's bottom-right, just above the bottom bar when visible. It remains until dismissed or replaced, with no timer. In Normal mode, the first Escape dismisses it without collapsing carets; a subsequent Escape retains ordinary Normal behavior. Existing modal windows, focused explorers/diff selectors, and pending prefixes retain Escape priority. New operation messages reopen the popup; routine successful buffer switches do not duplicate the buffer/path information already shown in the bar. The latest reported operation result appears in a non-focusable popup at the workspace's bottom-right, just above the bottom bar when visible, with one column of inner padding on each side when space permits. It remains until dismissed or replaced, with no timer. In Normal mode, the first Escape dismisses it without collapsing carets; a subsequent Escape retains ordinary Normal behavior. Existing modal windows, focused explorers/diff selectors, and pending prefixes retain Escape priority. New operation messages reopen the popup; routine successful buffer switches do not duplicate the buffer/path information already shown in the bar.
Command-capable core actions derive canonical Ex names by removing `core.`: `core.quit` gives `:quit`, `core.save` gives `:save`, and `core.diff.close` gives `:diff.close`. Native editing and Insert-only completion/snippet actions remain action bindings, not additional Ex commands. Command-capable core actions derive canonical Ex names by removing `core.`: `core.quit` gives `:quit`, `core.save` gives `:save`, and `core.diff.close` gives `:diff.close`. Native editing and Insert-only completion/snippet actions remain action bindings, not additional Ex commands.
+78 -26
View File
@@ -1848,27 +1848,31 @@ fn draw_floating_input(frame: &mut Frame, editor: &Editor, area: Rect) -> Option
height, height,
); );
let bordered = panel.width >= 4 && panel.height >= 3; let bordered = panel.width >= 4 && panel.height >= 3;
let block = Block::default() let padding = u16::from(panel.width.saturating_sub(2 * u16::from(bordered)) >= 3);
let mut block = Block::default()
.borders(if bordered { .borders(if bordered {
Borders::ALL Borders::ALL
} else { } else {
Borders::NONE Borders::NONE
}) })
.border_style(Style::default().fg(Color::Cyan)); .border_style(Style::default().fg(Color::Cyan))
.padding(ratatui::widgets::Padding::horizontal(padding));
if bordered {
block = block.title(if editor.mode() == Mode::Search {
" Search "
} else {
" Command "
});
}
let content = block.inner(panel); let content = block.inner(panel);
frame.render_widget(Clear, panel); frame.render_widget(Clear, panel);
frame.render_widget(block, panel); frame.render_widget(block, panel);
let prefix = if editor.mode() == Mode::Search { let command = editor.command_line();
'/' let column = display_column(command, usize::MAX);
} else {
':'
};
let command = format!("{prefix}{}", editor.command_line());
let column = display_column(&command, usize::MAX);
let width = usize::from(content.width); let width = usize::from(content.width);
let offset = column.saturating_sub(width.saturating_sub(1)); let offset = column.saturating_sub(width.saturating_sub(1));
frame.render_widget( frame.render_widget(
Paragraph::new(visible_line(&command, offset, width)), Paragraph::new(visible_line(command, offset, width)),
content, content,
); );
frame.set_cursor_position((content.x + (column - offset) as u16, content.y)); frame.set_cursor_position((content.x + (column - offset) as u16, content.y));
@@ -1898,13 +1902,19 @@ fn draw_notification(frame: &mut Frame, editor: &Editor, area: Rect) -> Option<R
} else { } else {
message message
}; };
let width = (UnicodeWidthStr::width(message.as_ref()).saturating_add(2)) let width = (UnicodeWidthStr::width(message.as_ref()).saturating_add(if area.height >= 3 {
4
} else {
2
}))
.min(60) .min(60)
.min(usize::from(area.width)) as u16; .min(usize::from(area.width)) as u16;
let bordered = width >= 4 && area.height >= 3; let bordered = width >= 4 && area.height >= 3;
let border = 2 * u16::from(bordered); let border = 2 * u16::from(bordered);
let padding = u16::from(width.saturating_sub(border) >= 3);
let paragraph = Paragraph::new(message).wrap(Wrap { trim: false }); let paragraph = Paragraph::new(message).wrap(Wrap { trim: false });
let height = (paragraph.line_count(width - border).min(6) as u16 + border).min(area.height); let height = (paragraph.line_count(width - border - 2 * padding).min(6) as u16 + border)
.min(area.height);
let panel = Rect::new(area.right() - width, area.bottom() - height, width, height); let panel = Rect::new(area.right() - width, area.bottom() - height, width, height);
let block = Block::default() let block = Block::default()
.borders(if bordered { .borders(if bordered {
@@ -1912,7 +1922,8 @@ fn draw_notification(frame: &mut Frame, editor: &Editor, area: Rect) -> Option<R
} else { } else {
Borders::NONE Borders::NONE
}) })
.border_style(Style::default().fg(Color::DarkGray)); .border_style(Style::default().fg(Color::DarkGray))
.padding(ratatui::widgets::Padding::horizontal(padding));
let content = block.inner(panel); let content = block.inner(panel);
frame.render_widget(Clear, panel); frame.render_widget(Clear, panel);
frame.render_widget(block, panel); frame.render_widget(block, panel);
@@ -5582,6 +5593,58 @@ mod tests {
} }
} }
#[test]
fn floating_fields_use_border_titles_and_inner_horizontal_padding() {
for (prefix, title) in [(':', "Command"), ('/', "Search")] {
let mut editor = Editor::open(None, vec![]).unwrap();
editor.handle(Input::Char(prefix));
for ch in "needle".chars() {
editor.handle(Input::Char(ch));
}
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(80, 12)).unwrap();
let mut panel = Rect::default();
terminal
.draw(|frame| {
panel = draw_floating_input(frame, &editor, frame.area()).unwrap();
})
.unwrap();
assert!(
pane_screen(&terminal, Rect::new(panel.x, panel.y, panel.width, 1)).contains(title)
);
let buffer = terminal.backend().buffer();
assert_eq!(buffer[(panel.x + 1, panel.y + 1)].symbol(), " ");
assert_eq!(buffer[(panel.x + 2, panel.y + 1)].symbol(), "n");
assert_eq!(buffer[(panel.right() - 2, panel.y + 1)].symbol(), " ");
assert_eq!(
terminal.get_cursor_position().unwrap(),
Position::new(panel.x + 8, panel.y + 1)
);
assert_eq!(
pane_screen(
&terminal,
Rect::new(panel.x + 1, panel.y + 1, panel.width - 2, 1)
)
.trim(),
"needle"
);
}
let mut editor = Editor::open(None, vec![]).unwrap();
editor.set_message("operation complete");
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(80, 12)).unwrap();
let mut panel = Rect::default();
terminal
.draw(|frame| {
panel = draw_notification(frame, &editor, frame.area()).unwrap();
})
.unwrap();
let buffer = terminal.backend().buffer();
assert_eq!(buffer[(panel.x + 1, panel.y + 1)].symbol(), " ");
assert_eq!(buffer[(panel.x + 2, panel.y + 1)].symbol(), "o");
assert_eq!(buffer[(panel.right() - 2, panel.y + 1)].symbol(), " ");
assert_eq!(panel.right(), 80);
assert_eq!(panel.bottom(), 12);
}
#[test] #[test]
fn floating_inputs_share_workspace_geometry_across_documents_splits_and_diffs() { fn floating_inputs_share_workspace_geometry_across_documents_splits_and_diffs() {
for context in ["document", "split", "diff", "split-diff", "selector"] { for context in ["document", "split", "diff", "split-diff", "selector"] {
@@ -6357,7 +6420,6 @@ mod tests {
if prompting { if prompting {
let input = views.panels.input.unwrap(); let input = views.panels.input.unwrap();
assert!(input.contains(cursor)); assert!(input.contains(cursor));
assert!(pane_screen(&terminal, input).contains(':'));
} else { } else {
assert!(viewport.contains(cursor)); assert!(viewport.contains(cursor));
assert!(pane_screen(&terminal, viewport).contains("DOCUMENT")); assert!(pane_screen(&terminal, viewport).contains("DOCUMENT"));
@@ -6456,7 +6518,6 @@ mod tests {
.unwrap(); .unwrap();
assert_eq!(views.rects[0].1, before); assert_eq!(views.rects[0].1, before);
let input = views.panels.input.unwrap(); let input = views.panels.input.unwrap();
assert!(pane_screen(&terminal, input).contains(':'));
assert!(input.contains(terminal.get_cursor_position().unwrap())); assert!(input.contains(terminal.get_cursor_position().unwrap()));
assert_eq!(pane_screen(&terminal, Rect::new(0, 11, 240, 1)), bottom); assert_eq!(pane_screen(&terminal, Rect::new(0, 11, 240, 1)), bottom);
} }
@@ -7921,7 +7982,6 @@ mod tests {
terminal terminal
.draw(|frame| views.draw(frame, &mut editor)) .draw(|frame| views.draw(frame, &mut editor))
.unwrap(); .unwrap();
assert!(screen_text(&terminal).contains(":diff.close"));
editor.handle(Input::Enter); editor.handle(Input::Enter);
assert!(editor.diff().is_none()); assert!(editor.diff().is_none());
assert_eq!(editor.lines(), ["// live comment"]); assert_eq!(editor.lines(), ["// live comment"]);
@@ -12327,7 +12387,6 @@ end
) )
}) })
.unwrap(); .unwrap();
assert!(screen_text(&terminal).contains("/saved search"));
} }
#[test] #[test]
@@ -12976,9 +13035,8 @@ end
.unwrap(); .unwrap();
let buffer = terminal.backend().buffer(); let buffer = terminal.backend().buffer();
let input = panels.input.unwrap(); let input = panels.input.unwrap();
let content = Rect::new(input.x + 1, input.y + 1, input.width - 2, 1); let content = Rect::new(input.x + 2, input.y + 1, input.width - 4, 1);
assert_eq!(buffer[(content.x, content.y)].symbol(), "界"); assert_eq!(buffer[(content.x, content.y)].symbol(), "界");
assert_eq!(buffer[(content.x + 2, content.y)].symbol(), "界");
editor.handle(Input::Backspace); editor.handle(Input::Backspace);
editor.handle(Input::Backspace); editor.handle(Input::Backspace);
editor.handle(Input::Char('\t')); editor.handle(Input::Char('\t'));
@@ -12995,19 +13053,13 @@ end
) )
}) })
.unwrap(); .unwrap();
let query = format!("/{}", editor.command_line());
let column = display_column(&query, usize::MAX);
let offset = column.saturating_sub(usize::from(content.width).saturating_sub(1));
assert!( assert!(
panels panels
.input .input
.unwrap() .unwrap()
.contains(terminal.get_cursor_position().unwrap()) .contains(terminal.get_cursor_position().unwrap())
); );
assert_eq!( assert!(content.contains(terminal.get_cursor_position().unwrap()));
terminal.get_cursor_position().unwrap(),
Position::new(content.x + (column - offset) as u16, content.y),
);
} }
#[test] #[test]