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 f9de6a0d 220440c1

+199 -13
+2
AGENTS.md
··· 168 168 - Editor layout tightened: small-font baseline, 7 px line pitch, gutter=18 px with 1 px gap; first row starts immediately below the bar; removed extra paddings. 169 169 - Fixed new-text baseline misalignment (was rendered 1 px too low); unified small-font rendering across normal/selected/caret glyphs. 170 170 - Tests and docs updated; clippy/tests green. 171 + - Keyboard shortcuts (Phase 1): implemented PageUp/PageDown (+Shift), Ctrl/Cmd+Home/End, and block indent/outdent (Tab/Shift+Tab with selection; Shift+Tab outdents current line). Added tests and a planning doc. 172 + - Plan doc: `docs/roadmap/editor_shortcuts.md` with checkboxes (updated). 171 173 - 2025-09-01: 172 174 - Fixed editor selection drop shadow to avoid interior horizontal seams for multi-line selections. 173 175 - Implemented per-row bottom shadow segmentation by subtracting next-row overlap; right-edge rule preserved.
+4 -4
docs/roadmap/editor_shortcuts.md
··· 6 6 7 7 ## Scope (Phase 1) 8 8 9 - - [ ] Page Up / Page Down (plain) 9 + - [x] Page Up / Page Down (plain) 10 10 - Move caret up/down by visible line count (viewport height / 7), clamped. 11 11 - Maintain a virtual column (desired x) across ragged lines. 12 12 - Ensure caret remains visible by adjusting `scroll_line`. 13 13 - Tests: basic motion, clamping, viewport re-centering/visibility. 14 14 15 - - [ ] Page Up / Page Down with Shift 15 + - [x] Page Up / Page Down with Shift 16 16 - Same movement but extend selection from anchor. 17 17 - If no active selection, set anchor at caret before moving. 18 18 - Tests: selection spans exactly N rows; correct range regardless of direction. 19 19 20 - - [ ] Ctrl+Home / Ctrl+End 20 + - [x] Ctrl+Home / Ctrl+End 21 21 - Ctrl+Home → caret to (0,0); Ctrl+End → caret to (last line, `line_len(last)`). 22 22 - Adjust viewport so caret is visible. 23 23 - Tests: motion to start/end; clamping; viewport visibility. 24 24 25 - - [ ] Block Indent / Outdent (Tab / Shift+Tab) 25 + - [x] Block Indent / Outdent (Tab / Shift+Tab) 26 26 - When selection spans lines: 27 27 - Tab → insert one leading space on each selected line. 28 28 - Shift+Tab → remove one leading space when present.
+2
docs/specs/implementation_status.md
··· 82 82 - CODE view: rope‑backed buffer; editable; gutter; selection highlight; TIC‑style caret (cursor color index 2 + shadow); 6 px small font on a 7 px pitch. 83 83 - Layout: gutter 18 px wide (3 digits), 1 px gap before code; first row starts directly under the toolbar; no extra top/bottom padding. 84 84 - Selection: per‑cell 7×7 shadow+fill; outer-perimeter shadow only; no inter‑line seams. 85 + - Navigation shortcuts: PageUp/PageDown (with Shift), Ctrl/Cmd+Home/End. 86 + - Editing shortcuts: Block indent/outdent with Tab/Shift+Tab on multi-line selections; Shift+Tab outdents current line when no selection. 85 87 - Navigation: arrows, Home/End; auto‑scroll keeps caret visible. 86 88 - Editing: insert chars/newline/tab (1 space), backspace/delete with line joins at SOL/EOL. 87 89 - Selection/Clipboard: Shift+arrows; Select All (Cmd/Ctrl+A); copy/cut/paste via OS clipboard (Cmd/Ctrl+C/V/X).
+3
docs/testing/test_catalog.md
··· 77 77 - `tic80_rust/tests/editor_selection_undo_tests.rs`: selection replace/cut/paste and undo/redo cycles; select-all. 78 78 - `tic80_rust/tests/editor_selection_shadow_tests.rs`: multi-line selection renders without interior bottom seams; right-edge shadow is exactly 7 px tall. 79 79 - `tic80_rust/tests/editor_selection_align_tests.rs`: selection top aligns with caret box (small font baseline at 6 px, pitch 7 px). 80 + - `tic80_rust/tests/editor_page_nav_tests.rs`: PageUp/Down movement by visible lines; Shift variants extend selection. 81 + - `tic80_rust/tests/editor_ctrl_home_end_tests.rs`: Ctrl/Cmd+Home/End jump to document bounds. 82 + - `tic80_rust/tests/editor_indent_outdent_tests.rs`: Block indent/outdent adds/removes a leading space per selected line. 80 83 81 84 ## Screenshot Tests 82 85 - `tic80_rust/tests/screenshot_smoke.rs`
+98 -7
tic80_rust/src/editor/code.rs
··· 14 14 pub caret_col: usize, 15 15 pub scroll_line: usize, 16 16 pub scroll_col: usize, 17 + // Desired horizontal column preserved across vertical/page moves 18 + desired_col: Option<usize>, 17 19 // Selection anchor as (line, col) if active 18 20 sel_anchor: Option<(usize, usize)>, 19 21 // Undo/redo stacks (each EditOp can be a batch of atomic edits) ··· 44 46 caret_col: 0, 45 47 scroll_line: 0, 46 48 scroll_col: 0, 49 + desired_col: None, 47 50 sel_anchor: None, 48 51 undo: Vec::new(), 49 52 redo: Vec::new(), ··· 212 215 } 213 216 } 214 217 218 + // Read-only access to rope for tests 219 + #[must_use] 220 + #[allow(clippy::missing_const_for_fn)] 221 + pub fn rope(&self) -> &Rope { &self.rope } 222 + 215 223 pub fn move_left(&mut self) { 216 224 self.reset_caret_blink(); 217 225 if self.caret_col > 0 { ··· 220 228 self.caret_line -= 1; 221 229 self.caret_col = self.line_len(self.caret_line); 222 230 } 231 + self.desired_col = Some(self.caret_col); 223 232 } 224 233 225 234 pub fn move_right(&mut self) { ··· 231 240 self.caret_line += 1; 232 241 self.caret_col = 0; 233 242 } 243 + self.desired_col = Some(self.caret_col); 234 244 } 235 245 236 246 pub fn move_up(&mut self) { 237 247 self.reset_caret_blink(); 248 + let want = self.desired_col.unwrap_or(self.caret_col); 238 249 if self.caret_line > 0 { 239 250 self.caret_line -= 1; 240 - let len = self.line_len(self.caret_line); 241 - if self.caret_col > len { 242 - self.caret_col = len; 243 - } 251 + self.caret_col = self.line_len(self.caret_line).min(want); 244 252 } 253 + self.desired_col = Some(want); 245 254 } 246 255 247 256 pub fn move_down(&mut self) { 248 257 self.reset_caret_blink(); 258 + let want = self.desired_col.unwrap_or(self.caret_col); 249 259 if self.caret_line + 1 < self.line_count() { 250 260 self.caret_line += 1; 251 - let len = self.line_len(self.caret_line); 252 - if self.caret_col > len { 253 - self.caret_col = len; 261 + self.caret_col = self.line_len(self.caret_line).min(want); 262 + } 263 + self.desired_col = Some(want); 264 + } 265 + 266 + // Page motion by visible line count 267 + pub fn page_down(&mut self, vis: usize) { 268 + self.reset_caret_blink(); 269 + let want = self.desired_col.unwrap_or(self.caret_col); 270 + let lc = self.line_count(); 271 + if lc == 0 { return; } 272 + let delta = vis.min(lc.saturating_sub(1) - self.caret_line); 273 + self.caret_line += delta; 274 + self.caret_col = self.line_len(self.caret_line).min(want); 275 + self.desired_col = Some(want); 276 + } 277 + pub fn page_up(&mut self, vis: usize) { 278 + self.reset_caret_blink(); 279 + let want = self.desired_col.unwrap_or(self.caret_col); 280 + let delta = vis.min(self.caret_line); 281 + self.caret_line -= delta; 282 + self.caret_col = self.line_len(self.caret_line).min(want); 283 + self.desired_col = Some(want); 284 + } 285 + 286 + pub fn doc_home(&mut self) { 287 + self.reset_caret_blink(); 288 + self.caret_line = 0; 289 + self.caret_col = 0; 290 + self.desired_col = Some(0); 291 + } 292 + pub fn doc_end(&mut self) { 293 + self.reset_caret_blink(); 294 + if self.line_count() == 0 { self.caret_line = 0; self.caret_col = 0; return; } 295 + self.caret_line = self.line_count().saturating_sub(1); 296 + self.caret_col = self.line_len(self.caret_line); 297 + self.desired_col = Some(self.caret_col); 298 + } 299 + 300 + // Block indentation: selection required for indent; outdent also supports single line when no selection 301 + pub fn block_indent(&mut self) { 302 + if let Some((s, e)) = self.selection_range_idx() { 303 + let mut op = EditOp { ops: Vec::new() }; 304 + let start_line = self.rope.char_to_line(s); 305 + let end_line = if e > 0 { self.rope.char_to_line(e - 1) } else { self.rope.char_to_line(e) }; 306 + for l in start_line..=end_line { 307 + let idx = self.rope.line_to_char(l); 308 + self.rope.insert(idx, " "); 309 + op.ops.push(EditKind::Insert { index: idx, text: " ".to_string() }); 310 + if self.caret_line == l { self.caret_col = self.caret_col.saturating_add(1); } 311 + if let Some((al, ac)) = self.sel_anchor { if al == l { self.sel_anchor = Some((al, ac.saturating_add(1))); } } 312 + } 313 + self.undo.push(op); 314 + self.clear_redo(); 315 + } 316 + } 317 + pub fn block_outdent(&mut self) { 318 + if let Some((s, e)) = self.selection_range_idx() { 319 + let mut op = EditOp { ops: Vec::new() }; 320 + let start_line = self.rope.char_to_line(s); 321 + let end_line = if e > 0 { self.rope.char_to_line(e - 1) } else { self.rope.char_to_line(e) }; 322 + for l in start_line..=end_line { 323 + let idx = self.rope.line_to_char(l); 324 + // Remove leading space if any 325 + if self.rope.line(l).chars().next() == Some(' ') { 326 + self.rope.remove(idx..=idx); 327 + op.ops.push(EditKind::Delete { index: idx, text: " ".to_string() }); 328 + if self.caret_line == l { self.caret_col = self.caret_col.saturating_sub(1); } 329 + if let Some((al, ac)) = self.sel_anchor { if al == l { self.sel_anchor = Some((al, ac.saturating_sub(1))); } } 330 + } 331 + } 332 + self.undo.push(op); 333 + self.clear_redo(); 334 + } else { 335 + // No selection: outdent current line if possible 336 + let l = self.caret_line; 337 + let idx = self.rope.line_to_char(l); 338 + if self.rope.line(l).chars().next() == Some(' ') { 339 + let mut op = EditOp { ops: Vec::new() }; 340 + self.rope.remove(idx..=idx); 341 + op.ops.push(EditKind::Delete { index: idx, text: " ".to_string() }); 342 + self.undo.push(op); 343 + self.caret_col = self.caret_col.saturating_sub(1); 344 + self.clear_redo(); 254 345 } 255 346 } 256 347 }
+8 -2
tic80_rust/src/main.rs
··· 412 412 if ch == '\n' { 413 413 cb.insert_newline(); 414 414 } else if ch == '\t' { 415 - cb.insert_tab(); 415 + // handled in KeyboardInput for block indent/outdent 416 416 } else if !ch.is_control() { 417 417 cb.insert_char(ch); 418 418 } ··· 466 466 } 467 467 // Navigation and editing 468 468 match key { 469 + VirtualKeyCode::PageUp => { if shift { cb.ensure_selection_anchor(); } else { cb.clear_selection(); } cb.page_up(18); }, 470 + VirtualKeyCode::PageDown => { if shift { cb.ensure_selection_anchor(); } else { cb.clear_selection(); } cb.page_down(18); }, 471 + VirtualKeyCode::Home if ctrl || cmd => { cb.doc_home(); }, 472 + VirtualKeyCode::End if ctrl || cmd => { cb.doc_end(); }, 469 473 VirtualKeyCode::Left => { if shift { cb.ensure_selection_anchor(); } else { cb.clear_selection(); } cb.move_left(); }, 470 474 VirtualKeyCode::Right => { if shift { cb.ensure_selection_anchor(); } else { cb.clear_selection(); } cb.move_right(); }, 471 475 VirtualKeyCode::Up => { if shift { cb.ensure_selection_anchor(); } else { cb.clear_selection(); } cb.move_up(); }, ··· 473 477 VirtualKeyCode::Back => { cb.backspace(); }, 474 478 VirtualKeyCode::Delete => { cb.delete_forward(); }, 475 479 VirtualKeyCode::Return => { cb.insert_newline(); }, 476 - VirtualKeyCode::Tab => { cb.insert_tab(); }, 480 + VirtualKeyCode::Tab => { 481 + if shift { cb.block_outdent(); } else if cb.has_selection() { cb.block_indent(); } else { cb.insert_tab(); } 482 + }, 477 483 VirtualKeyCode::Home => { if shift { cb.ensure_selection_anchor(); } else { cb.clear_selection(); } cb.home(); }, 478 484 VirtualKeyCode::End => { if shift { cb.ensure_selection_anchor(); } else { cb.clear_selection(); } cb.end(); }, 479 485 _ => {}
+15
tic80_rust/tests/editor_ctrl_home_end_tests.rs
··· 1 + use tic80_rust::editor::code::CodeBuffer; 2 + 3 + #[test] 4 + fn ctrl_home_end_move_to_bounds() { 5 + let text = "abc\ndef\nlast"; 6 + let mut cb = CodeBuffer::from_text(text); 7 + cb.caret_line = 1; 8 + cb.caret_col = 2; 9 + cb.doc_home(); 10 + assert_eq!((cb.caret_line, cb.caret_col), (0, 0)); 11 + cb.doc_end(); 12 + assert_eq!(cb.caret_line, cb.line_count() - 1); 13 + assert_eq!(cb.caret_col, cb.line_len(cb.caret_line)); 14 + } 15 +
+22
tic80_rust/tests/editor_indent_outdent_tests.rs
··· 1 + use tic80_rust::editor::code::CodeBuffer; 2 + 3 + #[test] 4 + fn block_indent_and_outdent_selection() { 5 + let text = "one\ntwo\nthree\n"; 6 + let mut cb = CodeBuffer::from_text(text); 7 + // Select from start of line 0 to end of line 2 (inclusive) 8 + cb.caret_line = 0; cb.caret_col = 0; cb.ensure_selection_anchor(); 9 + cb.caret_line = 2; cb.caret_col = cb.line_len(2); 10 + 11 + // Indent selection 12 + cb.block_indent(); 13 + assert!(cb.rope().line(0).to_string().starts_with(" ")); 14 + assert!(cb.rope().line(1).to_string().starts_with(" ")); 15 + assert!(cb.rope().line(2).to_string().starts_with(" ")); 16 + 17 + // Outdent selection 18 + cb.block_outdent(); 19 + assert!(!cb.rope().line(0).to_string().starts_with(" ")); 20 + assert!(!cb.rope().line(1).to_string().starts_with(" ")); 21 + assert!(!cb.rope().line(2).to_string().starts_with(" ")); 22 + }
+45
tic80_rust/tests/editor_page_nav_tests.rs
··· 1 + use tic80_rust::editor::code::CodeBuffer; 2 + 3 + fn make_lines(n: usize) -> String { 4 + let mut s = String::new(); 5 + for i in 0..n { let _ = i; s.push_str("x\n"); } 6 + s 7 + } 8 + 9 + #[test] 10 + fn page_down_moves_by_visible_lines() { 11 + let text = make_lines(50); 12 + let mut cb = CodeBuffer::from_text(&text); 13 + cb.caret_line = 0; 14 + let vis = 10usize; 15 + cb.page_down(vis); 16 + assert_eq!(cb.caret_line, 10); 17 + cb.page_down(vis); 18 + assert_eq!(cb.caret_line, 20); 19 + } 20 + 21 + #[test] 22 + fn page_up_clamps_at_top() { 23 + let text = make_lines(5); 24 + let mut cb = CodeBuffer::from_text(&text); 25 + cb.caret_line = 2; 26 + cb.page_up(10); 27 + assert_eq!(cb.caret_line, 0); 28 + } 29 + 30 + #[test] 31 + fn shift_page_extends_selection() { 32 + let text = make_lines(40); 33 + let mut cb = CodeBuffer::from_text(&text); 34 + cb.caret_line = 5; 35 + cb.caret_col = 1; 36 + cb.ensure_selection_anchor(); 37 + cb.page_down(10); 38 + // Expect selection spanning from original caret line to new caret line 39 + let (s, e) = cb.selection_range_idx().expect("selection after shift+page"); 40 + let s_line = cb.rope().char_to_line(s); 41 + let e_line = cb.rope().char_to_line(e - 1); 42 + assert_eq!(s_line, 5); 43 + assert_eq!(e_line, 15); 44 + } 45 +