fix(records): allow adding rows in empty records view

Add helper methods to CmdContext to clarify layout state and synthetic
record presence. Update AddRecordRow to check for records mode generally
rather than requiring a synthetic record at the current cursor, which
allows adding the first row to an empty records view.

Includes a regression test for the empty records view scenario.

Co-Authored-By: fiddlerwoaroof/git-smart-commit (gemma-4-31B-it-UD-Q4_K_XL.gguf)
This commit is contained in:
Edward Langley
2026-04-14 01:26:29 -07:00
parent 8b7b45587b
commit 1cea06e14b
4 changed files with 44 additions and 11 deletions

View File

@ -48,9 +48,22 @@ pub struct CmdContext<'a> {
} }
impl<'a> CmdContext<'a> { impl<'a> CmdContext<'a> {
/// Return true when the current layout is a records-mode layout.
pub fn is_records_mode(&self) -> bool {
self.layout.is_records_mode()
}
pub fn cell_key(&self) -> Option<CellKey> { pub fn cell_key(&self) -> Option<CellKey> {
self.layout.cell_key(self.selected.0, self.selected.1) self.layout.cell_key(self.selected.0, self.selected.1)
} }
/// Return synthetic record coordinates for the current cursor, if any.
pub fn synthetic_record_at_cursor(&self) -> Option<(usize, String)> {
self.cell_key()
.as_ref()
.and_then(crate::view::synthetic_record_info)
}
pub fn row_count(&self) -> usize { pub fn row_count(&self) -> usize {
self.layout.row_count() self.layout.row_count()
} }

View File

@ -443,12 +443,7 @@ impl Cmd for AddRecordRow {
"add-record-row" "add-record-row"
} }
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> { fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
let is_records = ctx if !ctx.is_records_mode() {
.cell_key()
.as_ref()
.and_then(crate::view::synthetic_record_info)
.is_some();
if !is_records {
return vec![effect::set_status( return vec![effect::set_status(
"add-record-row only works in records mode", "add-record-row only works in records mode",
)]; )];

View File

@ -228,11 +228,7 @@ impl Cmd for EditOrDrill {
.unwrap_or(false) .unwrap_or(false)
}); });
// In records mode (synthetic key), always edit directly — no drilling. // In records mode (synthetic key), always edit directly — no drilling.
let is_synthetic = ctx let is_synthetic = ctx.synthetic_record_at_cursor().is_some();
.cell_key()
.as_ref()
.and_then(crate::view::synthetic_record_info)
.is_some();
let is_aggregated = !is_synthetic && regular_none; let is_aggregated = !is_synthetic && regular_none;
if is_aggregated { if is_aggregated {
let Some(key) = ctx.cell_key().clone() else { let Some(key) = ctx.cell_key().clone() else {

View File

@ -712,6 +712,35 @@ mod tests {
); );
} }
/// Regression: pressing `o` in an empty records view should create the
/// first synthetic row instead of only entering edit mode on empty space.
#[test]
fn add_record_row_in_empty_records_view_creates_first_row() {
let mut m = Model::new("T");
m.add_category("Region").unwrap();
m.category_mut("Region").unwrap().add_item("East");
let mut app = App::new(m, None);
app.handle_key(KeyEvent::new(KeyCode::Char('R'), KeyModifiers::NONE))
.unwrap();
assert!(app.layout.is_records_mode(), "R should enter records mode");
assert_eq!(app.layout.row_count(), 0, "fresh records view starts empty");
app.handle_key(KeyEvent::new(KeyCode::Char('o'), KeyModifiers::NONE))
.unwrap();
assert_eq!(
app.layout.row_count(),
1,
"o should create the first record row in an empty records view"
);
assert!(
matches!(app.mode, AppMode::Editing { .. }),
"o should leave the app in edit mode, got {:?}",
app.mode
);
}
#[test] #[test]
fn command_mode_buffer_cleared_on_reentry() { fn command_mode_buffer_cleared_on_reentry() {
use crossterm::event::KeyEvent; use crossterm::event::KeyEvent;