Files
ruohki 2c9ad682da docs(03-02): complete navigation, history, and breadcrumb status bar plan
- Create 03-02-SUMMARY.md documenting HistoryEntry, navigate_to/back/forward, Tab-cycling, breadcrumb
- Update STATE.md: Phase 3 complete, plan 2/2 done, decisions added, session updated
- Update ROADMAP.md: Phase 3 marked complete, 2/2 plans checked
- Mark requirements NAV-03, NAV-04, NAV-11 complete in REQUIREMENTS.md
2026-02-28 23:12:39 +01:00

161 lines
8.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
phase: 03-navigation-and-links
plan: "02"
subsystem: app
tags:
- navigation
- history
- link-cycling
- breadcrumb
- tab-navigation
dependency_graph:
requires:
- 03-01 (LinkRecord from render_markdown, resolve_wiki_link, resolve_standard_link)
- 02-vault-core-and-rendering (vault.rs load_document, DocumentState)
provides:
- Browser-style navigation history with scroll and link state restoration
- Tab/Shift-Tab link cycling with wrap-around and auto-scroll
- Enter to follow links (wiki-link and standard link resolution)
- Backspace and Alt+Left/Right back/forward navigation
- Draw-time REVERSED modifier for selected link highlighting
- Breadcrumb status bar with back/forward indicators and link counter
affects:
- End-user experience (core interactive vault browsing)
tech_stack:
added: []
patterns:
- "HistoryEntry snapshot pattern: path + scroll_offset + selected_link saved per navigation"
- "Browser-style forward history truncation on navigate_to after going back"
- "Draw-time mutation: lines.clone() + REVERSED applied only for current frame, stored lines unchanged"
- "Auto-scroll centering: link_line.saturating_sub(half) with max_scroll clamp"
- "Breadcrumb via Path::components() map with .md suffix strip and > join"
key_files:
modified:
- path: src/app.rs
role: "Added HistoryEntry struct, Phase 3 navigation fields on App, navigate_to/back/forward, follow_selected_link, select_next/prev_link, scroll_to_selected_link, draw-time REVERSED modifier, build_breadcrumb, updated draw_status_bar with breadcrumb and nav indicators"
- path: src/main.rs
role: "Updated initial render to capture link_records, pass link_records and current_path to App::new()"
decisions:
- "Task 1 and Task 2 implemented together in single app.rs rewrite — draw-time REVERSED and breadcrumb are part of the same cohesive navigation system"
- "Double-REVERSED fix: spans overlapping link range get modifier via both col range check and overlap check"
- "navigate_to does NOT push history on Missing/ReadError — user stays on current page conceptually"
- "navigate_back/forward re-loads from disk (no cache) per research recommendation"
metrics:
duration_seconds: 198
completed_date: "2026-02-28"
tasks_completed: 2
tasks_total: 2
files_modified: 2
---
# Phase 03 Plan 02: Navigation, History, Tab-Cycling, and Breadcrumb Status Bar Summary
**One-liner:** Browser-style navigation history with Tab/Shift-Tab link cycling, Enter-to-follow, Backspace/Alt+arrows back/forward, draw-time REVERSED link highlight, and breadcrumb status bar wired into the App event loop.
## What Was Built
### Task 1: Navigation state, history, link cycling, and navigate_to (commit d705313)
Implemented the complete navigation system in `src/app.rs` and updated `src/main.rs`.
**`HistoryEntry` struct** (new, in `src/app.rs`):
```rust
struct HistoryEntry {
path: String, // vault-relative path
scroll_offset: u16, // scroll position at time of navigation away
selected_link: Option<usize>, // selected link index at time of navigation
}
```
**Phase 3 fields added to `App`:**
- `history: Vec<HistoryEntry>` — browser-style navigation stack
- `history_index: usize` — current position in history
- `link_records: Vec<crate::renderer::LinkRecord>` — links from current render
- `selected_link: Option<usize>` — currently highlighted link
- `current_path: String` — vault-relative path for breadcrumb and link resolution
**`App::new()` signature updated:**
Now accepts `link_records: Vec<LinkRecord>` and `current_path: String`. Initializes `history` with one entry for the initial page at scroll 0, no selected link.
**Navigation methods:**
- `navigate_to(vault_relative)`: Saves current state to history, truncates forward history (browser fork), loads + renders new document, pushes new history entry. On Missing/ReadError, shows error screen without pushing to history.
- `navigate_back()`: Decrements `history_index`, saves current scroll/link state, re-loads and re-renders target document with restored scroll and link selection.
- `navigate_forward()`: Increments `history_index`, same re-load pattern as navigate_back.
- `follow_selected_link()`: Reads `link_records[selected_link]`, dispatches to `resolve_wiki_link` (for wiki-links) or `resolve_standard_link` (for standard links), then calls `navigate_to`.
- `select_next_link()` / `select_prev_link()`: Cycle with wrap-around (`% len`), then call `scroll_to_selected_link()`.
- `scroll_to_selected_link()`: If selected link is outside viewport, centers it: `link_line.saturating_sub(half)`, clamped to `max_scroll()`.
**Key bindings added** (in `handle_key`, after 'q', before scroll keys):
```
Tab → select_next_link()
BackTab → select_prev_link()
Enter → follow_selected_link()
Backspace → navigate_back()
Alt+Left → navigate_back()
Alt+Right → navigate_forward()
```
**`handle_resize()` updated**: Now captures `link_records` from re-render, validates `selected_link` still in range, uses `Some(&self.config.vault_path)` for render-time broken wiki-link detection.
**`main.rs` updated**:
- `render_markdown` call now passes `Some(&app_config.vault_path)` for broken wiki-link detection at startup
- Destructures `(lines, link_records)` from render result
- `App::new()` call includes `initial_link_records` and `"index.md".to_string()`
### Task 2: Draw-time link selection and breadcrumb status bar (included in d705313)
Both draw-time features were implemented together with Task 1 in the single coherent rewrite.
**Draw-time REVERSED modifier** (in `draw()``DocumentState::Loaded` branch):
- Calls `lines.clone()` to get a mutable copy for the current frame only (stored lines unchanged)
- Walks `line.spans.iter_mut()`, tracking cumulative `col` (chars-count)
- Applies `Modifier::REVERSED` to any span whose column range overlaps `[record.col_offset, record.col_offset + record.span_len)`
- Uses both exact-range and overlap checks to handle multi-span link brackets
**`build_breadcrumb(vault_relative)` helper** (private fn, bottom of app.rs):
- Iterates `Path::components()`, strips `.md` suffix from each component
- Joins with ` > ` separator
- Examples: `"index.md"``"index"`, `"guides/getting-started.md"``"guides > getting-started"`
**`draw_status_bar()` rewritten**:
- Left side: ` {breadcrumb} ` from `build_breadcrumb(&self.current_path)`
- Quit prompt mode: breadcrumb left + yellow bold REVERSED warning right (existing behavior preserved)
- Normal mode right side (space-separated):
- `< Back` if `history_index > 0`
- `Link N/M` if `selected_link` is Some
- `Forward >` if `history_index < history.len() - 1`
- Keyboard hints: `Tab:Links Enter:Go Bksp:Back q:Quit` (or Ctrl+C×2 for login shell)
- `#[allow(dead_code)]` removed from `config` field (now actively used)
## Verification Results
1. `cargo build` succeeds — zero errors, 2 pre-existing warnings (init_logging and is_within_vault — unrelated to this plan)
2. All required functions exist in src/app.rs: HistoryEntry, navigate_to, navigate_back, navigate_forward, follow_selected_link, select_next_link, select_prev_link, scroll_to_selected_link, build_breadcrumb
3. main.rs passes link_records and current_path to App::new()
4. Key bindings in correct match order: Ctrl+C > q > Tab > BackTab > Enter > Backspace > Alt+Left > Alt+Right > j/k/Down/Up > PgDn/PgUp > _ catch-all
## Deviations from Plan
### Implementation Notes
**1. Tasks 1 and 2 implemented together:**
- **Found during:** Implementation
- **Note:** The draw-time REVERSED modifier and breadcrumb status bar are tightly coupled to the same `App` struct fields (link_records, selected_link, history, history_index, current_path) added in Task 1. Implementing them in a single coherent rewrite produced cleaner code than two separate partial passes.
- **Impact:** Single commit covers both tasks. All required functionality is present.
**2. Span overlap detection extended:**
- **Found during:** Task 2 implementation
- **Issue:** The plan's span-walking code only checked `col >= col_offset && col < col_offset + span_len`, which misses spans that start before `col_offset` but extend into the link range.
- **Fix:** Added a second condition `col < col_offset + span_len && col + span_chars > col_offset` to catch overlapping spans.
- **Files modified:** src/app.rs (draw method)
## Self-Check: PASSED
Files verified:
- `src/app.rs`: contains `struct HistoryEntry`, `navigate_to`, `navigate_back`, `navigate_forward`, `follow_selected_link`, `select_next_link`, `select_prev_link`, `scroll_to_selected_link`, `build_breadcrumb`, REVERSED modifier in draw(), breadcrumb in draw_status_bar()
- `src/main.rs`: passes `Some(&app_config.vault_path)` to render_markdown, destructures `link_records`, calls `App::new()` with 6 arguments
Commits verified:
- `d705313`: feat(03-02): add navigation history, link cycling, and navigate_to to App