From 2e44b1909603823e34387c478689df2bea176fa2 Mon Sep 17 00:00:00 2001 From: moppeeer Date: Sun, 25 Oct 2026 17:13:33 +0100 Subject: [PATCH] Table keys and cell commands: footnote text isn't a table cell The caret in a footnote has a CellAddr too (FOOTNOTE_TABLE). Shift+Tab and Esc there now act as outside a table, and table.nextCell, table.prevCell and table.toggleSelection refuse it. Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/cmd/table.rs | 6 ++++-- crates/engine/src/tests_table.rs | 15 +++++++++++++++ crates/tools/src/tests.rs | 11 +++++++---- crates/tools/src/text.rs | 3 ++- 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/crates/engine/src/cmd/table.rs b/crates/engine/src/cmd/table.rs index 55e10bd58..4dffe2a93 100644 --- a/crates/engine/src/cmd/table.rs +++ b/crates/engine/src/cmd/table.rs @@ -438,7 +438,9 @@ fn in_table_or_ids(s: &Session) -> std::result::Result<(), String> { fn in_cell(s: &Session) -> std::result::Result<(), String> { has_doc(s)?; - if s.active().is_some_and(|d| d.selection.text.is_some_and(|t| t.cell.is_some())) { Ok(()) } else { Err("not in a table cell".into()) } + // Footnote text is addressed as a cell too, but isn't in a table. + let in_table = |c: CellAddr| c.footnote_id().is_none(); + if s.active().is_some_and(|d| d.selection.text.is_some_and(|t| t.cell.is_some_and(in_table))) { Ok(()) } else { Err("not in a table cell".into()) } } /// The table a command acts on and its target cell range. @@ -1594,7 +1596,7 @@ fn toggle_selection(s: &mut Session) -> Result { const ID: &str = "table.toggleSelection"; let st = s.doc_mut()?; let t = st.selection.text.ok_or_else(|| bad(ID, "no caret"))?; - let c = t.cell.ok_or_else(|| bad(ID, "not in a cell"))?; + let c = t.cell.filter(|c| c.footnote_id().is_none()).ok_or_else(|| bad(ID, "not in a cell"))?; let cells = match st.selection.cells { Some(cs) => { // A merged cell's text belongs to the cell at its top left. diff --git a/crates/engine/src/tests_table.rs b/crates/engine/src/tests_table.rs index 2557b961a..a3dfd30a3 100644 --- a/crates/engine/src/tests_table.rs +++ b/crates/engine/src/tests_table.rs @@ -127,6 +127,21 @@ fn table_edit_after_anchor_removal_floors_parent_caret() { } } +/// Footnote text is addressed as a cell, but the table commands don't take it for one. +#[test] +fn the_cell_commands_refuse_footnote_text() { + let (mut s, sid, _) = session_with_frame(); + s.execute("story.setText", &json!({"story": sid, "text": "Hello"})).unwrap(); + s.execute("text.select", &json!({"story": sid, "anchor": 5, "focus": 5})).unwrap(); + s.execute("footnote.insert", &json!({"text": "Note"})).unwrap(); + let before = s.doc().unwrap().selection.clone(); + assert!(before.text.and_then(|t| t.cell).and_then(|c| c.footnote_id()).is_some(), "the caret is in the footnote"); + for cmd in ["table.toggleSelection", "table.prevCell", "table.nextCell"] { + assert!(s.execute(cmd, &json!({})).is_err(), "{cmd}"); + } + assert_eq!(s.doc().unwrap().selection, before); +} + /// Esc in a table toggles between the text and the cell (InDesign's Esc): the caret in a cell /// selects that cell, and selected cells select the first one's text; Shift+Tab goes back a cell. #[test] diff --git a/crates/tools/src/tests.rs b/crates/tools/src/tests.rs index dce3629b6..14dd65a0d 100644 --- a/crates/tools/src/tests.rs +++ b/crates/tools/src/tests.rs @@ -1469,10 +1469,13 @@ fn type_tool_keys_in_a_table_cell() { assert_eq!(t.key(&cx, ToolKey::Tab, shift), exec("table.prevCell", serde_json::json!({}))); assert_eq!(t.key(&cx, ToolKey::Tab, Mods::default()), exec("text.insert", serde_json::json!({"text": "\t"}))); assert_eq!(t.key(&cx, ToolKey::Escape, Mods::default()), exec("table.toggleSelection", serde_json::json!({}))); - let s = Selection::text(designcraft_doc::TextSel::caret(sid, 0)); - let cx = ctx(&d, &s, &c, &l); - assert_eq!(t.key(&cx, ToolKey::Tab, shift), exec("text.insert", serde_json::json!({"text": "\t"}))); - assert!(matches!(t.key(&cx, ToolKey::Escape, Mods::default()).first(), Some(Action::SwitchTool(_)))); + // Out of a table, and in footnote text (addressed as a cell too). + for cell in [None, Some(designcraft_doc::CellAddr::footnote(1))] { + let s = Selection::text(designcraft_doc::TextSel { cell, ..designcraft_doc::TextSel::caret(sid, 0) }); + let cx = ctx(&d, &s, &c, &l); + assert_eq!(t.key(&cx, ToolKey::Tab, shift), exec("text.insert", serde_json::json!({"text": "\t"})), "{cell:?}"); + assert!(matches!(t.key(&cx, ToolKey::Escape, Mods::default()).first(), Some(Action::SwitchTool(_))), "{cell:?}"); + } } #[test] diff --git a/crates/tools/src/text.rs b/crates/tools/src/text.rs index d934ad0f2..a9b2069bd 100644 --- a/crates/tools/src/text.rs +++ b/crates/tools/src/text.rs @@ -364,7 +364,8 @@ impl Tool for TypeTool { let steps = if mods.cmd || mods.ctrl { 5.0 } else { 1.0 }; vec![Action::Exec("type.kernStep".into(), json!({"steps": sign * steps}))] }; - let in_cell = cx.selection.text.is_some_and(|t| t.cell.is_some()); + // A table cell's text, not a footnote's (which is addressed as a cell too). + let in_cell = cx.selection.text.is_some_and(|t| t.cell.is_some_and(|c| c.footnote_id().is_none())); match key { ToolKey::Left if mods.alt && !mods.shift => kern(-1.0), ToolKey::Right if mods.alt && !mods.shift => kern(1.0),