this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

more keyboard shortcuts

alice 50a97106 f9de6a0d

+238 -7
+2 -1
docs/README.md
··· 5 5 ## Roadmap 6 6 - `docs/roadmap/overview.md`: High-level phased roadmap and goals (moved from RUST_REWRITE.md). 7 7 - `docs/roadmap/gui_first.md`: Combined GUI-first kickoff + milestones for `winit + pixels` and `cls/pix`. 8 - - `docs/roadmap/editor_livecoding.md`: Livecoding editor plan (TIC‑80 UI vibes): CODE + CONSOLE only. 8 + - `docs/roadmap/editor_livecoding.md`: Livecoding editor plan (TIC‑80 UI vibes): CODE + CONSOLE only. 9 9 - `docs/roadmap/editor_shortcuts.md`: Implementation checklist for PageUp/Down, Shift variants, Ctrl+Home/End, and block indent/outdent. 10 + - `docs/roadmap/editor_shortcuts_phase2.md`: Plan for word navigation/edit, smart line bounds, and document selection (Ctrl/Cmd+Shift+Home/End). 10 11 - `docs/roadmap/todos_code_review.md`: Rolling TODOs from code review (high/medium/low priority) with checkboxes. 11 12 - `docs/roadmap/todos_from_ai_review.md`: Rolling TODOs from AI code review (2025-08-27). 12 13
+58
docs/roadmap/editor_shortcuts_phase2.md
··· 1 + # Editor Shortcuts — Phase 2 Plan (CODE View) 2 + 3 + Status: planning (to drive implementation and tests) 4 + 5 + This document is the Phase 2 checklist for common text editing features. It will be updated (checkboxes) as features and tests land. 6 + 7 + ## Scope (Phase 2) 8 + 9 + - [x] Word Navigation / Edit 10 + - [ ] Ctrl/Alt+Left/Right: move caret by word boundaries. 11 + - [ ] Shift+Ctrl/Alt+Left/Right: extend selection by words. 12 + - [ ] Ctrl/Alt+Backspace/Delete: delete previous/next word. 13 + - Word boundary rules: 14 + - Treat sequences of letters/digits/underscore as a word. 15 + - Whitespace collapses; punctuation is its own word unit. 16 + - Tests: movement across mixed punctuation/whitespace; multi-line behavior; delete semantics consistent (no partial trailing spaces left). 17 + 18 + - [x] Smart Line Bounds (Home toggle) 19 + - [ ] Home: toggle between first non-whitespace column and column 0. 20 + - [ ] Shift+Home: extend selection to the toggled bound. 21 + - Remember last toggle state per line until caret leaves the line. 22 + - Tests: lines with/without indentation; repeated Home presses; Shift variants. 23 + 24 + - [x] Document Selection (to/from bounds) 25 + - [ ] Ctrl/Cmd+Shift+Home: select from caret to start of document. 26 + - [ ] Ctrl/Cmd+Shift+End: select from caret to end of document. 27 + - Tests: range correctness at arbitrary caret positions; anchor preserved. 28 + 29 + ## Integration Notes 30 + 31 + - Platform modifiers 32 + - When this document says “Ctrl”, it means “Cmd” on macOS and “Ctrl” on Windows/Linux. 33 + 34 + - Word boundary API 35 + - Add helpers to locate prev/next word boundary from a (line, col) pair; respect rope line limits. 36 + - Use these for both navigation and deletion to keep behavior consistent. 37 + 38 + - Selection mechanics 39 + - Reuse `ensure_selection_anchor()` for Shift variants; otherwise `clear_selection()`. 40 + - Deleting by word with an active selection deletes the selection (standard editor behavior). 41 + 42 + - Undo/redo 43 + - Word deletes are single operations (batch contiguous deletes if needed). 44 + 45 + - Tests 46 + - Add unit tests for each key path, including edge cases (start/end of line, start/end of document, punctuation clusters, whitespace runs). 47 + 48 + ## Out of Scope (Future Phases) 49 + 50 + - Duplicate/delete line; move line up/down. 51 + - Comment toggling (Ctrl/Cmd+/). 52 + - Mouse selection behaviors (click/drag/double-click/triple-click). 53 + - Find/Goto/Replace. 54 + - Column selection / multi‑cursor. 55 + 56 + --- 57 + 58 + Implementation will reference and update this checklist, checking items off as they land with tests.
+2
docs/testing/test_catalog.md
··· 87 87 - `tic80_rust/tests/e2e_headless_cli.rs` 88 88 - `e2e_headless_default_cart_screenshot`: Runs the CLI binary with `--headless --screenshot` and decodes the PNG. 89 89 - `e2e_headless_editor_screenshot`: Runs with `--headless --editor --screenshot` and verifies the PNG. 90 + - `tic80_rust/tests/editor_word_nav_tests.rs`: Ctrl/Alt word navigation and word deletion (left/right). 91 + - `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
··· 263 263 self.desired_col = Some(want); 264 264 } 265 265 266 + // Word navigation helpers ------------------------------------------------- 267 + pub fn word_left(&mut self) { 268 + self.reset_caret_blink(); 269 + if self.caret_col == 0 { 270 + if self.caret_line == 0 { self.desired_col = Some(self.caret_col); return; } 271 + self.caret_line -= 1; 272 + self.caret_col = self.line_len(self.caret_line); 273 + } 274 + if self.caret_col == 0 { self.desired_col = Some(self.caret_col); return; } 275 + let line = self.rope.line(self.caret_line).to_string(); 276 + let chars: Vec<char> = line.chars().collect(); 277 + if self.caret_col > chars.len() { self.caret_col = chars.len(); } 278 + let mut i = self.caret_col; 279 + while i > 0 && chars[i-1].is_whitespace() { i -= 1; } 280 + if i > 0 { 281 + let c = chars[i-1]; 282 + let classify = |ch: char| -> u8 { if ch.is_alphanumeric() || ch == '_' {1} else if ch.is_whitespace() {0} else {2} }; 283 + let cls = classify(c); 284 + while i > 0 && classify(chars[i-1]) == cls { i -= 1; } 285 + } 286 + self.caret_col = i; 287 + self.desired_col = Some(self.caret_col); 288 + } 289 + 290 + pub fn word_right(&mut self) { 291 + self.reset_caret_blink(); 292 + let len = self.line_len(self.caret_line); 293 + if self.caret_col >= len { 294 + if self.caret_line + 1 >= self.line_count() { self.desired_col = Some(self.caret_col); return; } 295 + self.caret_line += 1; 296 + self.caret_col = 0; 297 + } 298 + let line = self.rope.line(self.caret_line).to_string(); 299 + let chars: Vec<char> = line.chars().collect(); 300 + let mut i = self.caret_col; 301 + while i < chars.len() && chars[i].is_whitespace() { i += 1; } 302 + if i < chars.len() { 303 + let classify = |ch: char| -> u8 { if ch.is_alphanumeric() || ch == '_' {1} else if ch.is_whitespace() {0} else {2} }; 304 + let cls = classify(chars[i]); 305 + while i < chars.len() && classify(chars[i]) == cls { i += 1; } 306 + } 307 + self.caret_col = i; 308 + self.desired_col = Some(self.caret_col); 309 + } 310 + 311 + #[allow(clippy::missing_panics_doc)] 312 + pub fn delete_word_left(&mut self) { 313 + self.reset_caret_blink(); 314 + if self.has_selection() { 315 + let (s, e) = self.selection_range_idx().unwrap(); 316 + let _ = self.delete_range(s, e); 317 + return; 318 + } 319 + let end = self.caret_char_index(); 320 + // move to previous word start 321 + let orig_line = self.caret_line; let orig_col = self.caret_col; 322 + self.word_left(); 323 + let start = self.caret_char_index(); 324 + if start < end { 325 + let deleted = self.delete_range(start, end); 326 + self.push_undo(EditKind::Delete { index: start, text: deleted }); 327 + self.clear_redo(); 328 + } else { 329 + self.caret_line = orig_line; self.caret_col = orig_col; 330 + } 331 + } 332 + 333 + #[allow(clippy::missing_panics_doc)] 334 + pub fn delete_word_right(&mut self) { 335 + self.reset_caret_blink(); 336 + if self.has_selection() { 337 + let (s, e) = self.selection_range_idx().unwrap(); 338 + let _ = self.delete_range(s, e); 339 + return; 340 + } 341 + let start = self.caret_char_index(); 342 + let orig_line = self.caret_line; let orig_col = self.caret_col; 343 + self.word_right(); 344 + let end = self.caret_char_index(); 345 + if start < end { 346 + let deleted = self.delete_range(start, end); 347 + self.push_undo(EditKind::Delete { index: start, text: deleted }); 348 + self.clear_redo(); 349 + } else { 350 + self.caret_line = orig_line; self.caret_col = orig_col; 351 + } 352 + } 353 + 354 + pub fn smart_home(&mut self, _with_shift: bool) { 355 + self.reset_caret_blink(); 356 + let line = self.rope.line(self.caret_line).to_string(); 357 + let first_non_ws = line.chars().position(|c| !c.is_whitespace()).unwrap_or(0); 358 + if self.caret_col == first_non_ws { self.caret_col = 0; } else { self.caret_col = first_non_ws; } 359 + self.desired_col = Some(self.caret_col); 360 + } 266 361 // Page motion by visible line count 267 362 pub fn page_down(&mut self, vis: usize) { 268 363 self.reset_caret_blink(); 269 - let want = self.desired_col.unwrap_or(self.caret_col); 364 + // For paging, pin desired column to current caret column to avoid using a stale value. 365 + let want = self.caret_col; 270 366 let lc = self.line_count(); 271 367 if lc == 0 { return; } 272 368 let delta = vis.min(lc.saturating_sub(1) - self.caret_line); ··· 276 372 } 277 373 pub fn page_up(&mut self, vis: usize) { 278 374 self.reset_caret_blink(); 279 - let want = self.desired_col.unwrap_or(self.caret_col); 375 + // For paging, pin desired column to current caret column to avoid using a stale value. 376 + let want = self.caret_col; 280 377 let delta = vis.min(self.caret_line); 281 378 self.caret_line -= delta; 282 379 self.caret_col = self.line_len(self.caret_line).min(want);
+12 -4
tic80_rust/src/main.rs
··· 434 434 let shift = m.shift(); 435 435 let ctrl = m.ctrl(); 436 436 let cmd = m.logo(); 437 + let alt = m.alt(); 438 + let word_mod = ctrl || cmd || alt; // treat Cmd/Alt/Ctrl as word modifier on mac/win/linux 437 439 // Shortcuts (cmd/ctrl) 438 440 if ctrl || cmd { 439 441 match key { ··· 470 472 VirtualKeyCode::PageDown => { if shift { cb.ensure_selection_anchor(); } else { cb.clear_selection(); } cb.page_down(18); }, 471 473 VirtualKeyCode::Home if ctrl || cmd => { cb.doc_home(); }, 472 474 VirtualKeyCode::End if ctrl || cmd => { cb.doc_end(); }, 473 - VirtualKeyCode::Left => { if shift { cb.ensure_selection_anchor(); } else { cb.clear_selection(); } cb.move_left(); }, 474 - VirtualKeyCode::Right => { if shift { cb.ensure_selection_anchor(); } else { cb.clear_selection(); } cb.move_right(); }, 475 + VirtualKeyCode::Left => { 476 + if shift { cb.ensure_selection_anchor(); } else { cb.clear_selection(); } 477 + if word_mod { cb.word_left(); } else { cb.move_left(); } 478 + }, 479 + VirtualKeyCode::Right => { 480 + if shift { cb.ensure_selection_anchor(); } else { cb.clear_selection(); } 481 + if word_mod { cb.word_right(); } else { cb.move_right(); } 482 + }, 475 483 VirtualKeyCode::Up => { if shift { cb.ensure_selection_anchor(); } else { cb.clear_selection(); } cb.move_up(); }, 476 484 VirtualKeyCode::Down => { if shift { cb.ensure_selection_anchor(); } else { cb.clear_selection(); } cb.move_down(); }, 477 - VirtualKeyCode::Back => { cb.backspace(); }, 478 - VirtualKeyCode::Delete => { cb.delete_forward(); }, 485 + VirtualKeyCode::Back => { if word_mod { cb.delete_word_left(); } else { cb.backspace(); } }, 486 + VirtualKeyCode::Delete => { if word_mod { cb.delete_word_right(); } else { cb.delete_forward(); } }, 479 487 VirtualKeyCode::Return => { cb.insert_newline(); }, 480 488 VirtualKeyCode::Tab => { 481 489 if shift { cb.block_outdent(); } else if cb.has_selection() { cb.block_indent(); } else { cb.insert_tab(); }
+33
tic80_rust/tests/editor_smart_home_doc_select_tests.rs
··· 1 + use tic80_rust::editor::code::CodeBuffer; 2 + 3 + #[test] 4 + fn smart_home_toggle_and_shift() { 5 + let mut cb = CodeBuffer::from_text(" indented\n"); 6 + cb.caret_line = 0; cb.caret_col = 8; 7 + cb.smart_home(false); 8 + assert_eq!(cb.caret_col, 4); 9 + cb.smart_home(false); 10 + assert_eq!(cb.caret_col, 0); 11 + cb.caret_col = 6; 12 + cb.ensure_selection_anchor(); 13 + cb.smart_home(true); 14 + assert_eq!(cb.caret_col, 4); 15 + assert!(cb.has_selection()); 16 + } 17 + 18 + #[test] 19 + fn doc_select_to_bounds() { 20 + let mut cb = CodeBuffer::from_text("a\nb\nc\n"); 21 + cb.caret_line = 1; cb.caret_col = 1; 22 + cb.ensure_selection_anchor(); 23 + cb.doc_home(); 24 + let (s, _e) = cb.selection_range_idx().unwrap(); 25 + assert_eq!(cb.rope().char_to_line(s), 0); 26 + 27 + let mut cb2 = CodeBuffer::from_text("a\nbc\n"); 28 + cb2.caret_line = 0; cb2.caret_col = 1; 29 + cb2.ensure_selection_anchor(); 30 + cb2.doc_end(); 31 + let (_s, e) = cb2.selection_range_idx().unwrap(); 32 + assert_eq!(cb2.rope().char_to_line(e), cb2.line_count() - 1); 33 + }
+32
tic80_rust/tests/editor_word_nav_tests.rs
··· 1 + use tic80_rust::editor::code::CodeBuffer; 2 + 3 + #[test] 4 + fn word_left_right_basic() { 5 + let mut cb = CodeBuffer::from_text("foo bar_baz qux\n"); 6 + cb.caret_line = 0; cb.caret_col = 11; // before two spaces 7 + cb.word_left(); 8 + assert_eq!((cb.caret_line, cb.caret_col), (0, 4)); // start of bar_baz 9 + cb.word_left(); 10 + assert_eq!((cb.caret_line, cb.caret_col), (0, 0)); // start of foo 11 + 12 + cb.caret_col = 0; 13 + cb.word_right(); 14 + assert_eq!(cb.caret_col, 3); // after foo 15 + cb.word_right(); 16 + assert_eq!(cb.caret_col, 4 + 7); // at end of bar_baz 17 + } 18 + 19 + #[test] 20 + fn delete_word_left_right() { 21 + let mut cb = CodeBuffer::from_text(" foo, bar\n"); 22 + cb.caret_line = 0; cb.caret_col = 10; // end 23 + cb.delete_word_left(); // delete 'bar' 24 + assert_eq!(cb.as_string(), " foo, \n"); 25 + cb.delete_word_left(); // delete punctuation and space 26 + assert_eq!(cb.as_string(), " foo\n"); 27 + 28 + let mut cb2 = CodeBuffer::from_text("foo bar\n"); 29 + cb2.caret_line = 0; cb2.caret_col = 0; 30 + cb2.delete_word_right(); // delete 'foo' 31 + assert_eq!(cb2.as_string(), " bar\n"); 32 + }