feat: add page scrolling, open-row, and tab-advance
Add new navigation and editing capabilities to improve data entry and browsing efficiency. - Implement `PageScroll` command for PageUp/PageDown navigation. - Implement `OpenRecordRow` command (Vim-style 'o') to add a row and enter edit mode. - Implement `CommitAndAdvanceRight` command for Excel-style Tab navigation. - Add keybindings for Home, End, PageUp, PageDown, and Tab. - Update UI hints to reflect new Tab functionality. Co-Authored-By: fiddlerwoaroof/git-smart-commit (unsloth/gemma-4-31B-it-GGUF:UD-Q5_K_XL)
This commit is contained in:
@ -413,6 +413,30 @@ impl Cmd for ScrollRows {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PageScroll {
|
||||
pub direction: i32, // 1 for down, -1 for up
|
||||
pub cursor: CursorState,
|
||||
}
|
||||
impl Cmd for PageScroll {
|
||||
fn name(&self) -> &'static str {
|
||||
"page-scroll"
|
||||
}
|
||||
fn execute(&self, _ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||
let delta = (self.cursor.visible_rows as i32 * 3 / 4).max(1) * self.direction;
|
||||
let row_max = self.cursor.row_count.saturating_sub(1) as i32;
|
||||
let nr = (self.cursor.row as i32 + delta).clamp(0, row_max) as usize;
|
||||
viewport_effects(
|
||||
nr,
|
||||
self.cursor.col,
|
||||
self.cursor.row_offset,
|
||||
self.cursor.col_offset,
|
||||
self.cursor.visible_rows,
|
||||
self.cursor.visible_cols,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Mode change commands ─────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug)]
|
||||
@ -746,6 +770,26 @@ impl Cmd for AddRecordRow {
|
||||
}
|
||||
}
|
||||
|
||||
/// Vim-style 'o': add a new record row below cursor and enter edit mode.
|
||||
#[derive(Debug)]
|
||||
pub struct OpenRecordRow;
|
||||
impl Cmd for OpenRecordRow {
|
||||
fn name(&self) -> &'static str {
|
||||
"open-record-row"
|
||||
}
|
||||
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||
let is_records = ctx.cell_key.as_ref()
|
||||
.and_then(|k| crate::view::synthetic_record_info(k))
|
||||
.is_some();
|
||||
if !is_records {
|
||||
return vec![effect::set_status("open-record-row only works in records mode")];
|
||||
}
|
||||
let mut effects = AddRecordRow.execute(ctx);
|
||||
effects.push(Box::new(effect::EnterEditAtCursor));
|
||||
effects
|
||||
}
|
||||
}
|
||||
|
||||
/// Typewriter-style advance: move down, wrap to top of next column at bottom.
|
||||
#[derive(Debug)]
|
||||
pub struct EnterAdvance {
|
||||
@ -1971,6 +2015,36 @@ impl Cmd for CommitCellEdit {
|
||||
}
|
||||
}
|
||||
|
||||
/// Tab in editing: commit cell, move right, re-enter edit mode (Excel-style).
|
||||
#[derive(Debug)]
|
||||
pub struct CommitAndAdvanceRight {
|
||||
pub key: CellKey,
|
||||
pub value: String,
|
||||
pub cursor: CursorState,
|
||||
}
|
||||
impl Cmd for CommitAndAdvanceRight {
|
||||
fn name(&self) -> &'static str {
|
||||
"commit-and-advance-right"
|
||||
}
|
||||
fn execute(&self, _ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||
let mut effects: Vec<Box<dyn Effect>> = Vec::new();
|
||||
commit_cell_value(&self.key, &self.value, &mut effects);
|
||||
// Move right instead of down
|
||||
let col_max = self.cursor.col_count.saturating_sub(1);
|
||||
let nc = (self.cursor.col + 1).min(col_max);
|
||||
effects.extend(viewport_effects(
|
||||
self.cursor.row,
|
||||
nc,
|
||||
self.cursor.row_offset,
|
||||
self.cursor.col_offset,
|
||||
self.cursor.visible_rows,
|
||||
self.cursor.visible_cols,
|
||||
));
|
||||
effects.push(Box::new(effect::EnterEditAtCursor));
|
||||
effects
|
||||
}
|
||||
}
|
||||
|
||||
/// Commit a formula from the formula edit buffer.
|
||||
#[derive(Debug)]
|
||||
pub struct CommitFormula;
|
||||
@ -2555,6 +2629,25 @@ pub fn default_registry() -> CmdRegistry {
|
||||
}))
|
||||
},
|
||||
);
|
||||
r.register(
|
||||
&PageScroll { direction: 0, cursor: CursorState::default() },
|
||||
|args| {
|
||||
require_args("page-scroll", args, 1)?;
|
||||
let dir = args[0].parse::<i32>().map_err(|e| e.to_string())?;
|
||||
Ok(Box::new(PageScroll {
|
||||
direction: dir,
|
||||
cursor: CursorState::default(),
|
||||
}))
|
||||
},
|
||||
|args, ctx| {
|
||||
require_args("page-scroll", args, 1)?;
|
||||
let dir = args[0].parse::<i32>().map_err(|e| e.to_string())?;
|
||||
Ok(Box::new(PageScroll {
|
||||
direction: dir,
|
||||
cursor: CursorState::from_ctx(ctx),
|
||||
}))
|
||||
},
|
||||
);
|
||||
r.register(
|
||||
&EnterAdvance { cursor: CursorState::default() },
|
||||
|_| {
|
||||
@ -2817,6 +2910,7 @@ pub fn default_registry() -> CmdRegistry {
|
||||
Box::new(DeleteFormulaAtCursor)
|
||||
});
|
||||
r.register_nullary(|| Box::new(AddRecordRow));
|
||||
r.register_nullary(|| Box::new(OpenRecordRow));
|
||||
r.register_nullary(|| Box::new(TogglePruneEmpty));
|
||||
r.register_nullary(|| Box::new(ToggleRecordsMode));
|
||||
r.register_nullary(|| Box::new(CycleAxisAtCursor));
|
||||
@ -2870,35 +2964,36 @@ pub fn default_registry() -> CmdRegistry {
|
||||
&CommitCellEdit {
|
||||
key: CellKey::new(vec![]),
|
||||
value: String::new(),
|
||||
records_edit: None,
|
||||
},
|
||||
|args| {
|
||||
// parse: commit-cell-edit <value> <Cat/Item>...
|
||||
if args.len() < 2 {
|
||||
return Err("commit-cell-edit requires a value and coords".into());
|
||||
}
|
||||
Ok(Box::new(CommitCellEdit {
|
||||
key: parse_cell_key_from_args(&args[1..]),
|
||||
value: args[0].clone(),
|
||||
records_edit: None,
|
||||
}))
|
||||
},
|
||||
|_args, ctx| {
|
||||
let value = read_buffer(ctx, "edit");
|
||||
// In records mode, stage the edit instead of writing to the model
|
||||
if let Some(col_name) = &ctx.records_col {
|
||||
let record_idx = ctx.selected.0;
|
||||
return Ok(Box::new(CommitCellEdit {
|
||||
key: CellKey::new(vec![]), // ignored in records mode
|
||||
value,
|
||||
records_edit: Some((record_idx, col_name.clone())),
|
||||
}));
|
||||
}
|
||||
let key = ctx.cell_key.clone().ok_or("no cell at cursor")?;
|
||||
Ok(Box::new(CommitCellEdit {
|
||||
Ok(Box::new(CommitCellEdit { key, value }))
|
||||
},
|
||||
);
|
||||
r.register(
|
||||
&CommitAndAdvanceRight {
|
||||
key: CellKey::new(vec![]),
|
||||
value: String::new(),
|
||||
cursor: CursorState::default(),
|
||||
},
|
||||
|_| Err("commit-and-advance-right requires context".into()),
|
||||
|_args, ctx| {
|
||||
let value = read_buffer(ctx, "edit");
|
||||
let key = ctx.cell_key.clone().ok_or("no cell at cursor")?;
|
||||
Ok(Box::new(CommitAndAdvanceRight {
|
||||
key,
|
||||
value,
|
||||
records_edit: None,
|
||||
cursor: CursorState::from_ctx(ctx),
|
||||
}))
|
||||
},
|
||||
);
|
||||
|
||||
@ -266,10 +266,14 @@ impl KeymapSet {
|
||||
normal.bind(KeyCode::Char('G'), none, "jump-last-row");
|
||||
normal.bind(KeyCode::Char('0'), none, "jump-first-col");
|
||||
normal.bind(KeyCode::Char('$'), none, "jump-last-col");
|
||||
normal.bind(KeyCode::Home, none, "jump-first-col");
|
||||
normal.bind(KeyCode::End, none, "jump-last-col");
|
||||
|
||||
// Scroll
|
||||
normal.bind_args(KeyCode::Char('d'), ctrl, "scroll-rows", vec!["5".into()]);
|
||||
normal.bind_args(KeyCode::Char('u'), ctrl, "scroll-rows", vec!["-5".into()]);
|
||||
normal.bind_args(KeyCode::PageDown, none, "page-scroll", vec!["1".into()]);
|
||||
normal.bind_args(KeyCode::PageUp, none, "page-scroll", vec!["-1".into()]);
|
||||
|
||||
// Cell operations
|
||||
normal.bind(KeyCode::Char('x'), none, "clear-cell");
|
||||
@ -357,7 +361,7 @@ impl KeymapSet {
|
||||
// Drill into aggregated cell / view history / add row
|
||||
normal.bind(KeyCode::Char('>'), none, "drill-into-cell");
|
||||
normal.bind(KeyCode::Char('<'), none, "view-back");
|
||||
normal.bind(KeyCode::Char('o'), none, "add-record-row");
|
||||
normal.bind(KeyCode::Char('o'), none, "open-record-row");
|
||||
|
||||
// Records mode toggle and prune toggle
|
||||
normal.bind(KeyCode::Char('R'), none, "toggle-records-mode");
|
||||
@ -560,6 +564,7 @@ impl KeymapSet {
|
||||
let mut ed = Keymap::new();
|
||||
ed.bind_args(KeyCode::Esc, none, "enter-mode", vec!["normal".into()]);
|
||||
ed.bind(KeyCode::Enter, none, "commit-cell-edit");
|
||||
ed.bind(KeyCode::Tab, none, "commit-and-advance-right");
|
||||
ed.bind_args(KeyCode::Backspace, none, "pop-char", vec!["edit".into()]);
|
||||
ed.bind_any_char("append-char", vec!["edit".into()]);
|
||||
set.insert(ModeKey::Editing, Arc::new(ed));
|
||||
|
||||
@ -253,7 +253,7 @@ impl App {
|
||||
pub fn hint_text(&self) -> &'static str {
|
||||
match &self.mode {
|
||||
AppMode::Normal => "hjkl:nav i:edit R:records P:prune F/C/V:panels T:tiles [:]:page >:drill ::cmd",
|
||||
AppMode::Editing { .. } => "Enter:commit Esc:cancel",
|
||||
AppMode::Editing { .. } => "Enter:commit Tab:commit+right Esc:cancel",
|
||||
AppMode::FormulaPanel => "n:new d:delete jk:nav Esc:back",
|
||||
AppMode::FormulaEdit { .. } => "Enter:save Esc:cancel — type: Name = expression",
|
||||
AppMode::CategoryPanel => "jk:nav Space:cycle-axis n:new-cat a:add-items d:delete Esc:back",
|
||||
|
||||
Reference in New Issue
Block a user