From 67a5cb273ef40ff3190900bd2e2d2df6241d41e4 Mon Sep 17 00:00:00 2001 From: ".CooCoo" Date: Thu, 12 Nov 2026 14:27:13 -0800 Subject: [PATCH] Tables: table commands are disabled with the caret in footnote text (#2706) Footnote text is addressed as a cell, so with the insertion point in a footnote the table commands (Insert Row, Delete Column, Merge Cells, Table Options, Select Table, table styles) were enabled and then failed with "no table 18446744073709551615". They are now disabled with "place the insertion point in a table", as the cell commands already were. The caret's footnote offset is no longer looked up in the main story either, where it could land on a table's paragraph. table.get and the other commands that take any selection report the same message instead of the footnote address. A call that names a table still runs. --- ROADMAP.md | 1 + crates/engine/src/cmd/table/target.rs | 5 ++- crates/engine/src/tests/table.rs | 45 +++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/ROADMAP.md b/ROADMAP.md index ee0f4e022..c06cfceaa 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -400,6 +400,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m - **2026-11-12** · Type shortcuts: Justify All Lines ⇧⌘F, Insert White Space › Em Space ⇧⌘M and En Space ⇧⌘N (shown in the menu, enabled at an insertion point), Paragraph Rules… ⌥⌘J (shown in the Paragraph panel menu). On the Mac a menu-bar insert (Em/En Space, Current Page Number) no longer types into the document while a text field has focus, a dialog or Quick Apply is open, or an input method is composing. An alignment the paragraphs already have records no undo step (#1138). No readiness percentage change. - **2026-11-12** · A placed JPEG drawn at a half, a quarter or an eighth of its pixel size or less is decoded at that size straight from its compressed blocks, so the canvas, page thumbnails and loaded-graphic thumbnails no longer decode and keep the full-size picture for it. CMYK JPEGs and the full size still decode in full. No readiness percentage change. - **2026-11-12** · A decoded placed image becomes a screen pixmap in its own buffer, premultiplied in place, and opaque pixels are left as they are: 120 ms becomes 7 ms for a 24-megapixel opaque photo, with the same pixels. No readiness percentage change. +- **2026-11-12** · With the insertion point in footnote text, the table commands (Insert Row, Delete Column, Merge Cells, Table Options, Select Table, table styles) are disabled, as they are outside a table, instead of failing with "no table 18446744073709551615". A call that names a table still runs. No readiness percentage change. - **2026-11-12** · The status bar's preflight count and the Preflight panel take their issues from a check on a background worker, which checks the newest state of the document and shows the issues last found meanwhile. Moving or resizing a frame and typing no longer wait for a check of the whole document on every drag step and keystroke. No readiness percentage change. - **2026-11-12** · Object › Text Frame Options… (Cmd+B) opens with the Type tool in a frame, so inset spacing and vertical justification can be changed while typing; it changes the frame that shows the insertion point, or every frame that holds part of the selected text (#2396). No readiness percentage change. diff --git a/crates/engine/src/cmd/table/target.rs b/crates/engine/src/cmd/table/target.rs index 0240aff23..a939a09dd 100644 --- a/crates/engine/src/cmd/table/target.rs +++ b/crates/engine/src/cmd/table/target.rs @@ -8,10 +8,12 @@ use super::{ pub(super) fn in_table(s: &Session) -> std::result::Result<(), String> { has_doc(s)?; let st = s.active().ok_or("no document open")?; - if st.selection.cells.is_some() || st.selection.text.is_some_and(|t| t.cell.is_some()) { + // Footnote text is addressed as a cell too, but isn't in a table. + if st.selection.cells.is_some() || st.selection.text.is_some_and(|t| t.cell.is_some_and(|c| c.footnote_id().is_none())) { return Ok(()); } if let Some(t) = st.selection.text + && t.cell.is_none() && st.doc.story(t.story).is_some_and(|x| x.table_at(t.focus).is_some()) { return Ok(()); @@ -81,6 +83,7 @@ pub(super) fn target(s: &Session, p: &Value, cmd: &str) -> Result { (c.story, c.table, Some(c.range)) } else if let Some(t) = st.selection.text { match t.cell { + Some(c) if c.footnote_id().is_some() => return Err(bad(cmd, "place the insertion point in a table")), Some(c) => (t.story, c.table, Some(CellRange::cell(c.row, c.col))), None => { let tid = doc.story(t.story).and_then(|x| x.table_at(t.focus)).ok_or_else(|| bad(cmd, "the insertion point is not in a table"))?; diff --git a/crates/engine/src/tests/table.rs b/crates/engine/src/tests/table.rs index 7626005f4..fd5323ffb 100644 --- a/crates/engine/src/tests/table.rs +++ b/crates/engine/src/tests/table.rs @@ -643,6 +643,51 @@ fn a_named_table_edit_keeps_a_footnote_caret() { check(&s); } +/// A caret in footnote text is not in a table: the table commands that need one are disabled +/// there, also when the caret's offset in the footnote falls on a table's paragraph in the main +/// story, and a call that names a table still runs. +#[test] +fn table_commands_are_disabled_in_footnote_text() { + let in_table = ["table.insertRow", "table.deleteColumn", "table.merge", "table.options", "table.selectTable", "style.table.apply"]; + let info = |s: &Session, id: &str| crate::cmd::find_command(id).unwrap().info(s); + for on_table in [false, true] { + let (mut s, sid, _) = session_with_frame(); + let tid = if on_table { + s.execute("story.setText", &json!({"story": sid, "text": "Hello"})).unwrap(); + s.execute("text.select", &json!({"story": sid, "anchor": 0, "focus": 0})).unwrap(); + let tid = s.execute("table.insert", &json!({"rows": 2, "cols": 2})).unwrap()["table"].as_u64().unwrap(); + let end = s.doc().unwrap().doc.story(designcraft_doc::StoryId(sid)).unwrap().text.len(); + s.execute("text.select", &json!({"story": sid, "anchor": end, "focus": end})).unwrap(); + s.execute("footnote.insert", &json!({})).unwrap(); + tid + } else { + let tid = s.execute("table.insert", &json!({"rows": 2, "cols": 2})).unwrap()["table"].as_u64().unwrap(); + s.execute("text.select", &json!({"story": sid, "anchor": 0, "focus": 0})).unwrap(); + s.execute("footnote.insert", &json!({"text": "Note"})).unwrap(); + tid + }; + let before = s.doc().unwrap().selection.clone(); + let caret = before.text.unwrap(); + assert!(caret.cell.and_then(|c| c.footnote_id()).is_some(), "the caret is in the footnote"); + let story = s.doc().unwrap().doc.story(designcraft_doc::StoryId(sid)).unwrap(); + assert_eq!(story.table_at(caret.focus).is_some(), on_table, "focus {}", caret.focus); + for id in in_table { + let i = info(&s, id); + assert!(!i.enabled, "{id} {on_table}"); + assert_eq!(i.disabled_reason.as_deref(), Some("place the insertion point in a table"), "{id} {on_table}"); + assert!(matches!(s.execute(id, &json!({})), Err(crate::EngineError::Disabled(..))), "{id} {on_table}"); + } + // A query that takes any selection and names no table is told where to put the caret. + let error = s.execute("table.get", &json!({})).unwrap_err().to_string(); + assert!(error.contains("place the insertion point in a table"), "{on_table}: {error}"); + assert_eq!(s.doc().unwrap().selection, before); + s.execute("table.insertRow", &json!({"table": tid})).unwrap(); + assert_eq!(s.execute("table.get", &json!({"table": tid})).unwrap()["rows"].as_array().unwrap().len(), 3); + assert_eq!(s.doc().unwrap().selection, before); + check(&s); + } +} + /// With nothing selected, the table commands stay disabled for menus and for calls that name no /// table. #[test]