From d0c7f98269a25975b3fd5c2d8e28d4bb4647142c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sat, 26 Sep 2026 03:57:27 -0400 Subject: [PATCH] T21a round 2 fixes - a field notes its selection when a key goes down, so undo of typing selects what the typing replaced - reading order skips table cells merged away, so Up and Down across blocks land on cells that show --- TASKS.md | 3 ++- src/documents/blocks/edit.rs | 14 ++++++++++++- src/documents/blocks/keys.rs | 11 +++++++++- src/documents/blocks/mod.rs | 3 +++ src/documents/blocks/view.rs | 1 + src/documents/tests.rs | 39 ++++++++++++++++++++++++++++++++++++ 6 files changed, 68 insertions(+), 3 deletions(-) diff --git a/TASKS.md b/TASKS.md index 19f3e3a..e6204cb 100644 --- a/TASKS.md +++ b/TASKS.md @@ -248,4 +248,5 @@ Per-component lines live in `tasks/`. Tags there: | T20 | 1 | FAIL | the demo picked past memory's end; an empty gutter had no height to press; a breakpoint's box also opened it; zoomed ticks lost their places; docs promised bound keys and batch actions | | T20 | 2 | PASS | | | T21a | 1 | FAIL | Enter kept a selection; undo of a split left focus on a gone field; italic inside bold ate the bold; keys went into merged cells; undo kept a changed kind's highlighter | -| T21a | 2 | — | pending review | +| T21a | 2 | FAIL | undo of typing lost the selection it replaced; Up from below a table landed in a hidden merged cell | +| T21a | 3 | — | pending review | diff --git a/src/documents/blocks/edit.rs b/src/documents/blocks/edit.rs index 86cf1b5..4070fac 100644 --- a/src/documents/blocks/edit.rs +++ b/src/documents/blocks/edit.rs @@ -162,14 +162,26 @@ impl BlockEditor { /// Records what changed as one undo step, a burst of typing as one, and tells the owner. pub(super) fn settle(&mut self, typing: bool, cx: &mut Context) { let now = self.snapshot(cx); + let typed_from = self.typed_from.take(); if now.blocks != self.current.blocks { - let before = std::mem::replace(&mut self.current, now); + let mut before = std::mem::replace(&mut self.current, now); + if typed_from.is_some() { + before.caret = typed_from; + } self.history.record(before, typing); cx.emit(BlockEvent::Changed); cx.notify(); } } + /// Where a key went down, before the field acts on it: the start a typing burst's undo goes back to. + pub(crate) fn note_key(&mut self, key: u64, field: usize, cx: &Context) { + let selection = self.blocks[self.index(key)].fields[field] + .read(cx) + .selection(); + self.typed_from = Some((key, field, selection)); + } + /// Records the document before a change to its blocks. pub(crate) fn before_change(&mut self, cx: &mut Context) { self.settle(true, cx); diff --git a/src/documents/blocks/keys.rs b/src/documents/blocks/keys.rs index eb1a64a..41e3cfb 100644 --- a/src/documents/blocks/keys.rs +++ b/src/documents/blocks/keys.rs @@ -18,7 +18,16 @@ impl BlockEditor { BlockKind::Toggle(false) => 1, _ => block.fields.len(), }; - (0..shown).map(move |field| (block.key, field)) + let hidden: Vec = match &block.kind { + BlockKind::Table { columns, merged } => merged + .iter() + .map(|(row, column)| row * columns + column) + .collect(), + _ => Vec::new(), + }; + (0..shown) + .filter(move |field| !hidden.contains(field)) + .map(move |field| (block.key, field)) }) .collect() } diff --git a/src/documents/blocks/mod.rs b/src/documents/blocks/mod.rs index f1ca478..b94d60e 100644 --- a/src/documents/blocks/mod.rs +++ b/src/documents/blocks/mod.rs @@ -76,6 +76,8 @@ pub struct BlockEditor { current: Snapshot, /// The field that last held focus, as block and field. caret: Option<(u64, usize)>, + /// The selection a key went down on, before its field changed. + typed_from: Option<(u64, usize, std::ops::Range)>, pub(crate) people: Vec, pub(crate) diagram: Option, _synced: Subscription, @@ -95,6 +97,7 @@ impl BlockEditor { caret: None, }, caret: None, + typed_from: None, people: Vec::new(), diagram: None, _synced: synced, diff --git a/src/documents/blocks/view.rs b/src/documents/blocks/view.rs index b4b8cb8..c090621 100644 --- a/src/documents/blocks/view.rs +++ b/src/documents/blocks/view.rs @@ -114,6 +114,7 @@ impl BlockEditor { cx.stop_propagation(); editor.on_submit(key, window, cx); })) + .capture_key_down(cx.listener(move |editor, _, _, cx| editor.note_key(key, ix, cx))) .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) .child(field.clone()); let slash = move |query: &str, at: usize, caret: usize| -> (Vec, Pick) { diff --git a/src/documents/tests.rs b/src/documents/tests.rs index a2f98a0..4670958 100644 --- a/src/documents/tests.rs +++ b/src/documents/tests.rs @@ -305,3 +305,42 @@ fn undoing_a_kind_change_brings_back_its_styling(cx: &mut TestAppContext) { ); assert!(bold, "the paragraph styles its markdown again"); } + +#[gpui::test] +fn undo_of_typing_restores_the_selection_it_replaced(cx: &mut TestAppContext) { + let (editor, cx) = open(vec![BlockData::new(BlockKind::Paragraph, ["abcdef"])], cx); + select(&editor, 0, 0, 2..4, cx); + cx.simulate_keystrokes("x"); + settle(cx); + assert_eq!(blocks(&editor, cx), [text(BlockKind::Paragraph, "abxef")]); + cx.simulate_keystrokes("cmd-z"); + settle(cx); + let selection = cx.update(|_, cx| editor.read(cx).blocks[0].fields[0].read(cx).selection()); + assert_eq!(blocks(&editor, cx), [text(BlockKind::Paragraph, "abcdef")]); + assert_eq!(selection, 2..4, "undo selects what the typing replaced"); +} + +#[gpui::test] +fn up_into_a_table_skips_its_hidden_cells(cx: &mut TestAppContext) { + let table = BlockKind::Table { + columns: 2, + merged: vec![(1, 1)], + }; + let (editor, cx) = open( + vec![ + BlockData::new(table, ["A", "B", "c", ""]), + BlockData::new(BlockKind::Paragraph, ["after"]), + ], + cx, + ); + caret(&editor, 1, 0, cx); + cx.simulate_keystrokes("up"); + settle(cx); + let key = cx.update(|_, cx| editor.read(cx).blocks[0].key); + let focused = cx.update(|window, cx| editor.read(cx).focused(window, cx)); + assert_eq!( + focused, + Some((key, 2)), + "up lands on the cell that shows in the last row" + ); +}