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 ─────────────────────────────────────────────────────
|
// ── Mode change commands ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[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.
|
/// Typewriter-style advance: move down, wrap to top of next column at bottom.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct EnterAdvance {
|
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.
|
/// Commit a formula from the formula edit buffer.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct CommitFormula;
|
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(
|
r.register(
|
||||||
&EnterAdvance { cursor: CursorState::default() },
|
&EnterAdvance { cursor: CursorState::default() },
|
||||||
|_| {
|
|_| {
|
||||||
@ -2817,6 +2910,7 @@ pub fn default_registry() -> CmdRegistry {
|
|||||||
Box::new(DeleteFormulaAtCursor)
|
Box::new(DeleteFormulaAtCursor)
|
||||||
});
|
});
|
||||||
r.register_nullary(|| Box::new(AddRecordRow));
|
r.register_nullary(|| Box::new(AddRecordRow));
|
||||||
|
r.register_nullary(|| Box::new(OpenRecordRow));
|
||||||
r.register_nullary(|| Box::new(TogglePruneEmpty));
|
r.register_nullary(|| Box::new(TogglePruneEmpty));
|
||||||
r.register_nullary(|| Box::new(ToggleRecordsMode));
|
r.register_nullary(|| Box::new(ToggleRecordsMode));
|
||||||
r.register_nullary(|| Box::new(CycleAxisAtCursor));
|
r.register_nullary(|| Box::new(CycleAxisAtCursor));
|
||||||
@ -2870,35 +2964,36 @@ pub fn default_registry() -> CmdRegistry {
|
|||||||
&CommitCellEdit {
|
&CommitCellEdit {
|
||||||
key: CellKey::new(vec![]),
|
key: CellKey::new(vec![]),
|
||||||
value: String::new(),
|
value: String::new(),
|
||||||
records_edit: None,
|
|
||||||
},
|
},
|
||||||
|args| {
|
|args| {
|
||||||
// parse: commit-cell-edit <value> <Cat/Item>...
|
|
||||||
if args.len() < 2 {
|
if args.len() < 2 {
|
||||||
return Err("commit-cell-edit requires a value and coords".into());
|
return Err("commit-cell-edit requires a value and coords".into());
|
||||||
}
|
}
|
||||||
Ok(Box::new(CommitCellEdit {
|
Ok(Box::new(CommitCellEdit {
|
||||||
key: parse_cell_key_from_args(&args[1..]),
|
key: parse_cell_key_from_args(&args[1..]),
|
||||||
value: args[0].clone(),
|
value: args[0].clone(),
|
||||||
records_edit: None,
|
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
|_args, ctx| {
|
|_args, ctx| {
|
||||||
let value = read_buffer(ctx, "edit");
|
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")?;
|
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,
|
key,
|
||||||
value,
|
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('G'), none, "jump-last-row");
|
||||||
normal.bind(KeyCode::Char('0'), none, "jump-first-col");
|
normal.bind(KeyCode::Char('0'), none, "jump-first-col");
|
||||||
normal.bind(KeyCode::Char('$'), none, "jump-last-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
|
// Scroll
|
||||||
normal.bind_args(KeyCode::Char('d'), ctrl, "scroll-rows", vec!["5".into()]);
|
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::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
|
// Cell operations
|
||||||
normal.bind(KeyCode::Char('x'), none, "clear-cell");
|
normal.bind(KeyCode::Char('x'), none, "clear-cell");
|
||||||
@ -357,7 +361,7 @@ impl KeymapSet {
|
|||||||
// Drill into aggregated cell / view history / add row
|
// Drill into aggregated cell / view history / add row
|
||||||
normal.bind(KeyCode::Char('>'), none, "drill-into-cell");
|
normal.bind(KeyCode::Char('>'), none, "drill-into-cell");
|
||||||
normal.bind(KeyCode::Char('<'), none, "view-back");
|
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
|
// Records mode toggle and prune toggle
|
||||||
normal.bind(KeyCode::Char('R'), none, "toggle-records-mode");
|
normal.bind(KeyCode::Char('R'), none, "toggle-records-mode");
|
||||||
@ -560,6 +564,7 @@ impl KeymapSet {
|
|||||||
let mut ed = Keymap::new();
|
let mut ed = Keymap::new();
|
||||||
ed.bind_args(KeyCode::Esc, none, "enter-mode", vec!["normal".into()]);
|
ed.bind_args(KeyCode::Esc, none, "enter-mode", vec!["normal".into()]);
|
||||||
ed.bind(KeyCode::Enter, none, "commit-cell-edit");
|
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_args(KeyCode::Backspace, none, "pop-char", vec!["edit".into()]);
|
||||||
ed.bind_any_char("append-char", vec!["edit".into()]);
|
ed.bind_any_char("append-char", vec!["edit".into()]);
|
||||||
set.insert(ModeKey::Editing, Arc::new(ed));
|
set.insert(ModeKey::Editing, Arc::new(ed));
|
||||||
|
|||||||
@ -253,7 +253,7 @@ impl App {
|
|||||||
pub fn hint_text(&self) -> &'static str {
|
pub fn hint_text(&self) -> &'static str {
|
||||||
match &self.mode {
|
match &self.mode {
|
||||||
AppMode::Normal => "hjkl:nav i:edit R:records P:prune F/C/V:panels T:tiles [:]:page >:drill ::cmd",
|
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::FormulaPanel => "n:new d:delete jk:nav Esc:back",
|
||||||
AppMode::FormulaEdit { .. } => "Enter:save Esc:cancel — type: Name = expression",
|
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",
|
AppMode::CategoryPanel => "jk:nav Space:cycle-axis n:new-cat a:add-items d:delete Esc:back",
|
||||||
|
|||||||
Reference in New Issue
Block a user