feat(tui): Phase 2 Issue List + MR List screens
Implement state, action, and view layers for both list screens: - Issue List: keyset pagination, snapshot fence, filter DSL, label aggregation - MR List: mirrors Issue pattern with draft/reviewer/target branch filters - Migration 027: covering indexes for TUI list screen queries - Updated Msg types to use typed Page structs instead of raw Vec<Row> - 303 tests passing, clippy clean Beads: bd-3ei1, bd-2kr0, bd-3pm2
This commit is contained in:
@@ -60,9 +60,10 @@ impl NavigationStack {
|
||||
self.forward_stack.clear();
|
||||
|
||||
// Record significant hops in jump list (vim behavior):
|
||||
// truncate any forward entries beyond jump_index, then append.
|
||||
// Keep entries up to and including the current position, discard
|
||||
// any forward entries beyond it, then append the new destination.
|
||||
if self.current.is_detail_or_entity() {
|
||||
self.jump_list.truncate(self.jump_index);
|
||||
self.jump_list.truncate(self.jump_index.saturating_add(1));
|
||||
self.jump_list.push(self.current.clone());
|
||||
self.jump_index = self.jump_list.len();
|
||||
}
|
||||
@@ -90,23 +91,37 @@ impl NavigationStack {
|
||||
|
||||
/// Jump backward through the jump list (vim Ctrl+O).
|
||||
///
|
||||
/// Only visits detail/entity screens.
|
||||
/// Only visits detail/entity screens. Skips entries matching the
|
||||
/// current screen so the first press always produces a visible change.
|
||||
pub fn jump_back(&mut self) -> Option<&Screen> {
|
||||
if self.jump_index == 0 {
|
||||
return None;
|
||||
while self.jump_index > 0 {
|
||||
self.jump_index -= 1;
|
||||
if let Some(target) = self.jump_list.get(self.jump_index).cloned()
|
||||
&& target != self.current
|
||||
{
|
||||
self.current = target;
|
||||
return Some(&self.current);
|
||||
}
|
||||
}
|
||||
self.jump_index -= 1;
|
||||
self.jump_list.get(self.jump_index)
|
||||
None
|
||||
}
|
||||
|
||||
/// Jump forward through the jump list (vim Ctrl+I).
|
||||
///
|
||||
/// Skips entries matching the current screen.
|
||||
pub fn jump_forward(&mut self) -> Option<&Screen> {
|
||||
if self.jump_index >= self.jump_list.len() {
|
||||
return None;
|
||||
while self.jump_index < self.jump_list.len() {
|
||||
if let Some(target) = self.jump_list.get(self.jump_index).cloned() {
|
||||
self.jump_index += 1;
|
||||
if target != self.current {
|
||||
self.current = target;
|
||||
return Some(&self.current);
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let screen = self.jump_list.get(self.jump_index)?;
|
||||
self.jump_index += 1;
|
||||
Some(screen)
|
||||
None
|
||||
}
|
||||
|
||||
/// Reset to a single screen, clearing all history.
|
||||
@@ -246,24 +261,21 @@ mod tests {
|
||||
nav.push(Screen::MrList);
|
||||
nav.push(mr.clone());
|
||||
|
||||
// jump_index is at 2 (past the end of 2 items)
|
||||
let prev = nav.jump_back();
|
||||
assert_eq!(prev, Some(&mr));
|
||||
|
||||
// Current is MrDetail. jump_list = [IssueDetail, MrDetail], index = 2.
|
||||
// First jump_back skips MrDetail (== current) and lands on IssueDetail.
|
||||
let prev = nav.jump_back();
|
||||
assert_eq!(prev, Some(&issue));
|
||||
assert!(nav.is_at(&issue));
|
||||
|
||||
// at beginning
|
||||
// Already at beginning of jump list.
|
||||
assert!(nav.jump_back().is_none());
|
||||
|
||||
// forward
|
||||
let next = nav.jump_forward();
|
||||
assert_eq!(next, Some(&issue));
|
||||
|
||||
// jump_forward skips IssueDetail (== current) and lands on MrDetail.
|
||||
let next = nav.jump_forward();
|
||||
assert_eq!(next, Some(&mr));
|
||||
assert!(nav.is_at(&mr));
|
||||
|
||||
// at end
|
||||
// At end of jump list.
|
||||
assert!(nav.jump_forward().is_none());
|
||||
}
|
||||
|
||||
@@ -274,10 +286,9 @@ mod tests {
|
||||
nav.push(Screen::IssueDetail(EntityKey::issue(1, 2)));
|
||||
nav.push(Screen::IssueDetail(EntityKey::issue(1, 3)));
|
||||
|
||||
// jump back twice
|
||||
// jump back twice — lands on issue(1,1), jump_index = 0
|
||||
nav.jump_back();
|
||||
nav.jump_back();
|
||||
// jump_index = 1, pointing at issue 2
|
||||
|
||||
// new detail push truncates forward entries
|
||||
nav.push(Screen::MrDetail(EntityKey::mr(1, 99)));
|
||||
|
||||
Reference in New Issue
Block a user