From 833fe0da1f11f4d2d80f43e45aa4f62a58ee43c2 Mon Sep 17 00:00:00 2001 From: vangel76 Date: Wed, 14 Oct 2026 22:56:36 +0200 Subject: [PATCH] Edit: text that would parse as a number or formula keeps its quote prefix in the editor (#1362) Co-Authored-By: Claude Sonnet 5.5 --- ROADMAP.md | 1 + crates/engine/src/display.rs | 26 ++++++++++++++++++++++++++ crates/engine/src/tests.rs | 23 +++++++++++++++++++++++ 3 files changed, 50 insertions(+) diff --git a/ROADMAP.md b/ROADMAP.md index 070552d07..aa0d84c2a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -21,6 +21,7 @@ This page is the summary. The detail is in: | Number | Value | Kind | |---|---|---| +| 2026-10-14 | Editing a cell with text that would read back as a number, boolean, date or formula (entered with a leading apostrophe) shows the apostrophe in the editor and formula bar, so Enter unchanged keeps it text (#1362) | | **Feature breadth** (weighted) | **~75%** | estimated, anchored on two measurements: ribbon catalog 274 / 290 (94.5%), worksheet functions 513 / 523 (98.1%; 504 working, 96.4%) | | **Ready for real work** | **~50%** | estimated from written weights (51.6); [target-app-parity.md](docs/target-app-parity.md) | | **Mainstream practitioner** | **~50%** | estimated: typical analyst/finance user, weekly features, × 0.90 interaction × 0.92 stability × 0.88 file exchange; [method](docs/target-app-parity.md#mainstream-practitioner) | diff --git a/crates/engine/src/display.rs b/crates/engine/src/display.rs index b90e42727..5f54b7b4c 100644 --- a/crates/engine/src/display.rs +++ b/crates/engine/src/display.rs @@ -33,6 +33,25 @@ pub fn format(v: &Value, code: &str, wb: &Workbook) -> Formatted { format_value(v, &parsed(code), wb.date_system) } +/// Whether typing `t` back into a General cell would not give the text `t` again (it would parse +/// as a number, boolean, error, date or formula, or lose a leading apostrophe). Such text is shown +/// for editing with a leading `'`, as Excel does for text carrying a quote prefix, so that +/// committing the editor unchanged keeps the cell as text. +fn text_needs_quote_prefix(t: &str, sys: gridcraft_core::date::DateSystem) -> bool { + if t.is_empty() { + return false; + } + let mut chars = t.chars(); + let (first, second) = (chars.next(), chars.next()); + if matches!(first, Some('=' | '\'')) { + return true; + } + if matches!(first, Some('+' | '-')) && second.is_some_and(|c| c.is_ascii_alphabetic() || c == '(') { + return true; + } + !matches!(gridcraft_core::parse::parse_input(t, sys).value, Value::Text(ref x) if x.as_ref() == t) +} + /// What the cell editor and the formula bar start from. Same as `Cell::input_text`, except that a /// constant number in a date or time format is shown as a date (as the grid shows it), not as its /// serial. When the formatted text would not read back as the same date (for example @@ -41,6 +60,13 @@ pub fn format(v: &Value, code: &str, wb: &Workbook) -> Formatted { pub fn edit_text(wb: &Workbook, sheet: &Sheet, c: CellRef) -> String { let raw = sheet.input_text(c); let Some(cell) = sheet.cell(c) else { return raw }; + if let Value::Text(t) = &cell.value + && cell.formula.is_none() + && wb.styles.get(cell.style).num_fmt.as_str() != "@" + && text_needs_quote_prefix(t, wb.date_system) + { + return format!("'{raw}"); + } let Value::Number(n) = &cell.value else { return raw }; let code = wb.styles.get(cell.style).num_fmt.as_str(); if cell.formula.is_some() || !parsed(code).is_date() { diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index ae499a189..448802477 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -3282,3 +3282,26 @@ fn paste_special_validation_moves_relative_references() { assert!(!ok(&mut s, "C3", "5"), "the pasted rule checks C3, not A1"); assert!(ok(&mut s, "C3", "20")); } + +#[test] +fn editing_quote_prefixed_text_keeps_it_text() { + let mut s = s(); + for (i, input) in ["'00123", "'=1+1", "'TRUE", "''abc"].iter().enumerate() { + let cell = format!("A{}", i + 1); + s.execute("cell.set", json!({"cell": cell, "input": input})).unwrap(); + let at = CellRef::parse(&cell).unwrap(); + let before = v(&s, &cell); + let edit = { + let d = s.doc().unwrap(); + crate::display::edit_text(&d.wb, d.wb.active().unwrap(), at) + }; + assert_eq!(&edit, input, "edit text of {cell}"); + // Committing the editor unchanged leaves the cell as it was. + s.execute("cell.set", json!({"cell": cell, "input": edit})).unwrap(); + assert_eq!(v(&s, &cell), before, "{cell}"); + } + // Plain text is shown as typed, without a prefix. + s.execute("cell.set", json!({"cell": "B1", "input": "hello"})).unwrap(); + let d = s.doc().unwrap(); + assert_eq!(crate::display::edit_text(&d.wb, d.wb.active().unwrap(), CellRef::parse("B1").unwrap()), "hello"); +}