···55## Roadmap
66- `docs/roadmap/overview.md`: High-level phased roadmap and goals (moved from RUST_REWRITE.md).
77- `docs/roadmap/gui_first.md`: Combined GUI-first kickoff + milestones for `winit + pixels` and `cls/pix`.
88-- `docs/roadmap/editor_livecoding.md`: Livecoding editor plan (TIC‑80 UI vibes): CODE + CONSOLE only.
88+ - `docs/roadmap/editor_livecoding.md`: Livecoding editor plan (TIC‑80 UI vibes): CODE + CONSOLE only.
99 - `docs/roadmap/editor_shortcuts.md`: Implementation checklist for PageUp/Down, Shift variants, Ctrl+Home/End, and block indent/outdent.
1010+ - `docs/roadmap/editor_shortcuts_phase2.md`: Plan for word navigation/edit, smart line bounds, and document selection (Ctrl/Cmd+Shift+Home/End).
1011 - `docs/roadmap/todos_code_review.md`: Rolling TODOs from code review (high/medium/low priority) with checkboxes.
1112 - `docs/roadmap/todos_from_ai_review.md`: Rolling TODOs from AI code review (2025-08-27).
1213
+58
docs/roadmap/editor_shortcuts_phase2.md
···11+# Editor Shortcuts — Phase 2 Plan (CODE View)
22+33+Status: planning (to drive implementation and tests)
44+55+This document is the Phase 2 checklist for common text editing features. It will be updated (checkboxes) as features and tests land.
66+77+## Scope (Phase 2)
88+99+- [x] Word Navigation / Edit
1010+ - [ ] Ctrl/Alt+Left/Right: move caret by word boundaries.
1111+ - [ ] Shift+Ctrl/Alt+Left/Right: extend selection by words.
1212+ - [ ] Ctrl/Alt+Backspace/Delete: delete previous/next word.
1313+ - Word boundary rules:
1414+ - Treat sequences of letters/digits/underscore as a word.
1515+ - Whitespace collapses; punctuation is its own word unit.
1616+ - Tests: movement across mixed punctuation/whitespace; multi-line behavior; delete semantics consistent (no partial trailing spaces left).
1717+1818+- [x] Smart Line Bounds (Home toggle)
1919+ - [ ] Home: toggle between first non-whitespace column and column 0.
2020+ - [ ] Shift+Home: extend selection to the toggled bound.
2121+ - Remember last toggle state per line until caret leaves the line.
2222+ - Tests: lines with/without indentation; repeated Home presses; Shift variants.
2323+2424+- [x] Document Selection (to/from bounds)
2525+ - [ ] Ctrl/Cmd+Shift+Home: select from caret to start of document.
2626+ - [ ] Ctrl/Cmd+Shift+End: select from caret to end of document.
2727+ - Tests: range correctness at arbitrary caret positions; anchor preserved.
2828+2929+## Integration Notes
3030+3131+- Platform modifiers
3232+ - When this document says “Ctrl”, it means “Cmd” on macOS and “Ctrl” on Windows/Linux.
3333+3434+- Word boundary API
3535+ - Add helpers to locate prev/next word boundary from a (line, col) pair; respect rope line limits.
3636+ - Use these for both navigation and deletion to keep behavior consistent.
3737+3838+- Selection mechanics
3939+ - Reuse `ensure_selection_anchor()` for Shift variants; otherwise `clear_selection()`.
4040+ - Deleting by word with an active selection deletes the selection (standard editor behavior).
4141+4242+- Undo/redo
4343+ - Word deletes are single operations (batch contiguous deletes if needed).
4444+4545+- Tests
4646+ - Add unit tests for each key path, including edge cases (start/end of line, start/end of document, punctuation clusters, whitespace runs).
4747+4848+## Out of Scope (Future Phases)
4949+5050+- Duplicate/delete line; move line up/down.
5151+- Comment toggling (Ctrl/Cmd+/).
5252+- Mouse selection behaviors (click/drag/double-click/triple-click).
5353+- Find/Goto/Replace.
5454+- Column selection / multi‑cursor.
5555+5656+---
5757+5858+Implementation will reference and update this checklist, checking items off as they land with tests.
+2
docs/testing/test_catalog.md
···8787- `tic80_rust/tests/e2e_headless_cli.rs`
8888 - `e2e_headless_default_cart_screenshot`: Runs the CLI binary with `--headless --screenshot` and decodes the PNG.
8989 - `e2e_headless_editor_screenshot`: Runs with `--headless --editor --screenshot` and verifies the PNG.
9090+- `tic80_rust/tests/editor_word_nav_tests.rs`: Ctrl/Alt word navigation and word deletion (left/right).
9191+- `tic80_rust/tests/editor_smart_home_doc_select_tests.rs`: Smart Home toggle and Ctrl/Cmd+Shift+Home/End document selection.
+99-2
tic80_rust/src/editor/code.rs
···263263 self.desired_col = Some(want);
264264 }
265265266266+ // Word navigation helpers -------------------------------------------------
267267+ pub fn word_left(&mut self) {
268268+ self.reset_caret_blink();
269269+ if self.caret_col == 0 {
270270+ if self.caret_line == 0 { self.desired_col = Some(self.caret_col); return; }
271271+ self.caret_line -= 1;
272272+ self.caret_col = self.line_len(self.caret_line);
273273+ }
274274+ if self.caret_col == 0 { self.desired_col = Some(self.caret_col); return; }
275275+ let line = self.rope.line(self.caret_line).to_string();
276276+ let chars: Vec<char> = line.chars().collect();
277277+ if self.caret_col > chars.len() { self.caret_col = chars.len(); }
278278+ let mut i = self.caret_col;
279279+ while i > 0 && chars[i-1].is_whitespace() { i -= 1; }
280280+ if i > 0 {
281281+ let c = chars[i-1];
282282+ let classify = |ch: char| -> u8 { if ch.is_alphanumeric() || ch == '_' {1} else if ch.is_whitespace() {0} else {2} };
283283+ let cls = classify(c);
284284+ while i > 0 && classify(chars[i-1]) == cls { i -= 1; }
285285+ }
286286+ self.caret_col = i;
287287+ self.desired_col = Some(self.caret_col);
288288+ }
289289+290290+ pub fn word_right(&mut self) {
291291+ self.reset_caret_blink();
292292+ let len = self.line_len(self.caret_line);
293293+ if self.caret_col >= len {
294294+ if self.caret_line + 1 >= self.line_count() { self.desired_col = Some(self.caret_col); return; }
295295+ self.caret_line += 1;
296296+ self.caret_col = 0;
297297+ }
298298+ let line = self.rope.line(self.caret_line).to_string();
299299+ let chars: Vec<char> = line.chars().collect();
300300+ let mut i = self.caret_col;
301301+ while i < chars.len() && chars[i].is_whitespace() { i += 1; }
302302+ if i < chars.len() {
303303+ let classify = |ch: char| -> u8 { if ch.is_alphanumeric() || ch == '_' {1} else if ch.is_whitespace() {0} else {2} };
304304+ let cls = classify(chars[i]);
305305+ while i < chars.len() && classify(chars[i]) == cls { i += 1; }
306306+ }
307307+ self.caret_col = i;
308308+ self.desired_col = Some(self.caret_col);
309309+ }
310310+311311+ #[allow(clippy::missing_panics_doc)]
312312+ pub fn delete_word_left(&mut self) {
313313+ self.reset_caret_blink();
314314+ if self.has_selection() {
315315+ let (s, e) = self.selection_range_idx().unwrap();
316316+ let _ = self.delete_range(s, e);
317317+ return;
318318+ }
319319+ let end = self.caret_char_index();
320320+ // move to previous word start
321321+ let orig_line = self.caret_line; let orig_col = self.caret_col;
322322+ self.word_left();
323323+ let start = self.caret_char_index();
324324+ if start < end {
325325+ let deleted = self.delete_range(start, end);
326326+ self.push_undo(EditKind::Delete { index: start, text: deleted });
327327+ self.clear_redo();
328328+ } else {
329329+ self.caret_line = orig_line; self.caret_col = orig_col;
330330+ }
331331+ }
332332+333333+ #[allow(clippy::missing_panics_doc)]
334334+ pub fn delete_word_right(&mut self) {
335335+ self.reset_caret_blink();
336336+ if self.has_selection() {
337337+ let (s, e) = self.selection_range_idx().unwrap();
338338+ let _ = self.delete_range(s, e);
339339+ return;
340340+ }
341341+ let start = self.caret_char_index();
342342+ let orig_line = self.caret_line; let orig_col = self.caret_col;
343343+ self.word_right();
344344+ let end = self.caret_char_index();
345345+ if start < end {
346346+ let deleted = self.delete_range(start, end);
347347+ self.push_undo(EditKind::Delete { index: start, text: deleted });
348348+ self.clear_redo();
349349+ } else {
350350+ self.caret_line = orig_line; self.caret_col = orig_col;
351351+ }
352352+ }
353353+354354+ pub fn smart_home(&mut self, _with_shift: bool) {
355355+ self.reset_caret_blink();
356356+ let line = self.rope.line(self.caret_line).to_string();
357357+ let first_non_ws = line.chars().position(|c| !c.is_whitespace()).unwrap_or(0);
358358+ if self.caret_col == first_non_ws { self.caret_col = 0; } else { self.caret_col = first_non_ws; }
359359+ self.desired_col = Some(self.caret_col);
360360+ }
266361 // Page motion by visible line count
267362 pub fn page_down(&mut self, vis: usize) {
268363 self.reset_caret_blink();
269269- let want = self.desired_col.unwrap_or(self.caret_col);
364364+ // For paging, pin desired column to current caret column to avoid using a stale value.
365365+ let want = self.caret_col;
270366 let lc = self.line_count();
271367 if lc == 0 { return; }
272368 let delta = vis.min(lc.saturating_sub(1) - self.caret_line);
···276372 }
277373 pub fn page_up(&mut self, vis: usize) {
278374 self.reset_caret_blink();
279279- let want = self.desired_col.unwrap_or(self.caret_col);
375375+ // For paging, pin desired column to current caret column to avoid using a stale value.
376376+ let want = self.caret_col;
280377 let delta = vis.min(self.caret_line);
281378 self.caret_line -= delta;
282379 self.caret_col = self.line_len(self.caret_line).min(want);