feat: align buffer picker with editor UI

This commit is contained in:
2026-10-13 16:11:10 +02:00
parent f5ba61624f
commit 9d6a0eaeca
4 changed files with 153 additions and 39 deletions
+22 -7
View File
@@ -189,6 +189,13 @@ pub struct PaletteEntry {
pub bindings: Vec<String>,
pub enabled: bool,
pub modes: Vec<Mode>,
pub buffer: Option<PaletteBuffer>,
}
pub struct PaletteBuffer {
pub id: usize,
pub active: bool,
pub dirty: bool,
}
#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
@@ -5474,16 +5481,17 @@ impl Editor {
if palette.buffers {
palette.entries.clear();
for buffer in self.buffers() {
let label = sanitize_label(format!(
"{}{}{} {}",
let label = sanitize_label(buffer.path.map_or_else(
|| "[No Name]".into(),
|path| path.to_string_lossy().into_owned(),
));
let searchable = format!(
"{}{}{} {label}",
buffer.id,
if buffer.active { "%" } else { "" },
if buffer.dirty { "+" } else { "" },
buffer
.path
.map_or_else(|| Cow::Borrowed("[No Name]"), Path::to_string_lossy),
));
let searchable = label.to_lowercase();
)
.to_lowercase();
if query
.split_whitespace()
.all(|token| searchable.contains(token))
@@ -5495,6 +5503,11 @@ impl Editor {
bindings: Vec::new(),
enabled: true,
modes: Vec::new(),
buffer: Some(PaletteBuffer {
id: buffer.id,
active: buffer.active,
dirty: buffer.dirty,
}),
});
}
}
@@ -5543,6 +5556,7 @@ impl Editor {
Action::LspDefinition | Action::LspHover | Action::LspDiagnostics
) || self.document_actions_available()),
modes: modes.to_vec(),
buffer: None,
});
}
for feature in self.features.values() {
@@ -5562,6 +5576,7 @@ impl Editor {
bindings: self.palette_bindings(None, command),
enabled: self.active.view.mode == Mode::Normal,
modes: vec![Mode::Normal],
buffer: None,
});
}
}
+127 -31
View File
@@ -4357,35 +4357,49 @@ fn draw_palette(
if area.width == 0 || area.height == 0 {
return;
}
let [_, column, _] = Layout::horizontal([
Constraint::Fill(1),
Constraint::Length(area.width.min(100)),
Constraint::Fill(1),
])
.areas(area);
let [_, panel, _] = Layout::vertical([
Constraint::Fill(1),
Constraint::Length(area.height.min(16)),
Constraint::Fill(1),
])
.areas(column);
let panel = if palette.buffers {
floating_panel(area)
} else {
let [_, column, _] = Layout::horizontal([
Constraint::Fill(1),
Constraint::Length(area.width.min(100)),
Constraint::Fill(1),
])
.areas(area);
let [_, panel, _] = Layout::vertical([
Constraint::Fill(1),
Constraint::Length(area.height.min(16)),
Constraint::Fill(1),
])
.areas(column);
panel
};
frame.render_widget(Clear, panel);
let bordered = panel.height >= 4 && panel.width >= 4;
let padding =
u16::from(palette.buffers && panel.width.saturating_sub(2 * u16::from(bordered)) >= 3);
let block = Block::default()
.borders(if bordered {
Borders::ALL
} else {
Borders::NONE
})
.title(format!(
" {} · {mode} ",
if palette.buffers {
"Buffers"
} else {
"Commands"
}
));
let content = if bordered { block.inner(panel) } else { panel };
.border_style(if palette.buffers {
Style::default().fg(Color::Cyan)
} else {
Style::default()
})
.padding(ratatui::widgets::Padding::horizontal(padding))
.title(if palette.buffers {
" Buffers ".into()
} else {
format!(" Commands · {mode} ")
});
let content = if bordered {
block.inner(panel)
} else {
panel.inner(ratatui::layout::Margin::new(padding, 0))
};
if bordered {
frame.render_widget(block, panel);
}
@@ -4398,7 +4412,7 @@ fn draw_palette(
let query = format!("> {}", palette.query);
let query_width = display_column(&query, usize::MAX);
let width = usize::from(query_area.width);
let offset = query_width.saturating_sub(width - 1);
let offset = query_width.saturating_sub(width.saturating_sub(1));
frame.render_widget(
Paragraph::new(visible_line(&query, offset, width))
.style(Style::default().fg(Color::Yellow)),
@@ -4422,6 +4436,11 @@ fn draw_palette(
};
let count = usize::from(results_area.height) / row_height;
let first = palette.selected.saturating_sub(count - 1);
let id_width = palette
.entries
.last()
.and_then(|entry| entry.buffer.as_ref())
.map_or(1, |buffer| buffer.id.ilog10() as usize + 1);
let mut lines = Vec::with_capacity(count * row_height);
for (index, entry) in palette.entries.iter().enumerate().skip(first).take(count) {
let mut style = Style::default();
@@ -4431,13 +4450,28 @@ fn draw_palette(
if index == palette.selected {
style = style.bg(Color::Blue).add_modifier(Modifier::BOLD);
}
if palette.buffers {
if let Some(buffer) = &entry.buffer {
lines.push(
Line::from(format!(
"{} {}",
if index == palette.selected { ">" } else { " " },
entry.label,
))
Line::from(vec![
Span::raw(if index == palette.selected {
"> "
} else {
" "
}),
Span::styled(
format!("{:>id_width$} ", buffer.id),
Style::default().fg(Color::DarkGray),
),
Span::styled(
if buffer.active { "%" } else { " " },
Style::default().fg(Color::Cyan),
),
Span::styled(
if buffer.dirty { "+ " } else { " " },
Style::default().fg(Color::Yellow),
),
Span::raw(&entry.label),
])
.style(style),
);
continue;
@@ -4470,9 +4504,16 @@ fn draw_palette(
frame.render_widget(Paragraph::new(lines), results_area);
}
frame.render_widget(
Paragraph::new(format!(
"{mode} · {navigation_hint} · Enter accept · Esc/Ctrl+p cancel"
)),
Paragraph::new(if palette.buffers {
format!("{navigation_hint} · Enter open · Esc/^P cancel · ^U clear")
} else {
format!("{mode} · {navigation_hint} · Enter accept · Esc/Ctrl+p cancel")
})
.style(if palette.buffers {
Style::default().fg(Color::DarkGray)
} else {
Style::default()
}),
help_area,
);
}
@@ -14419,6 +14460,61 @@ end
assert_eq!(editor.mode(), Mode::Insert);
}
#[test]
fn buffer_picker_keeps_selected_status_and_paths_visible_after_resize_and_filter() {
let mut editor = Editor::open(None, vec![]).unwrap();
for id in 2..=12 {
editor.execute(&format!("edit picker-{id}.txt"));
}
editor
.apply(vec![crate::editor::Effect::Replace("unsaved".into())])
.unwrap();
editor.execute("buffers");
editor.handle(Input::Ctrl('k'));
for (width, height) in [(80, 12), (32, 6), (1, 1), (4, 4), (80, 12)] {
let area = Rect::new(3, 2, width, height);
let mut terminal = Terminal::with_options(
ratatui::backend::TestBackend::new(90, 30),
ratatui::TerminalOptions {
viewport: ratatui::Viewport::Fixed(area),
},
)
.unwrap();
terminal
.draw(|frame| {
draw_palette(
frame,
editor.palette().unwrap(),
area,
editor.mode(),
editor.selection_navigation_hint(),
)
})
.unwrap();
assert!(area.contains(terminal.get_cursor_position().unwrap()));
if width >= 32 {
let buffer = terminal.backend().buffer();
let selected_y = (area.y..area.bottom())
.find(|&y| buffer[(area.x + 7, y)].symbol() == "%")
.unwrap();
assert_eq!(buffer[(area.x + 7, selected_y)].fg, Color::Cyan);
assert_eq!(buffer[(area.x + 8, selected_y)].symbol(), "+");
assert_eq!(buffer[(area.x + 8, selected_y)].fg, Color::Yellow);
assert_eq!(buffer[(area.x + 10, selected_y)].bg, Color::Blue);
assert!(
pane_screen(&terminal, Rect::new(area.x, selected_y, width, 1))
.contains("picker-12.txt")
);
}
}
editor.handle(Input::Char('2'));
editor.handle(Input::Ctrl('j'));
editor.handle(Input::Enter);
assert_eq!(editor.buffer_id(), 12);
assert_eq!(editor.context().text, "unsaved");
assert!(editor.dirty());
}
#[test]
fn palette_query_scrolls_graphemes_tabs_and_wide_cells_on_tiny_offset_areas() {
let editor = Editor::open(None, vec![]).unwrap();