From 6939d95caee24b5298cff9a6cede4adabf7b43e1 Mon Sep 17 00:00:00 2001 From: Marco Arnulfo Date: Mon, 12 Oct 2026 22:53:10 +0200 Subject: [PATCH] Formula bar: hide _xludf. like Excel, keep it when the edit is confirmed unchanged (#726) --- crates/ui-egui/src/formula_locale.rs | 44 +++++++++++++++++++++++--- crates/ui-egui/tests/formula_locale.rs | 20 +++++++++++- 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/crates/ui-egui/src/formula_locale.rs b/crates/ui-egui/src/formula_locale.rs index ad0725e..6e833b9 100644 --- a/crates/ui-egui/src/formula_locale.rs +++ b/crates/ui-egui/src/formula_locale.rs @@ -9,6 +9,14 @@ pub(crate) fn canonical( sheet: usize, cell: gridcraft_engine::core::CellRef, ) -> Result { + // The editor hid the `_xludf.` prefixes of this cell's formula; confirming it unchanged keeps + // the user-defined calls instead of binding them to the built-ins of the same name. + if let Some(stored) = wb.sheet(sheet).and_then(|sh| sh.cell(cell)).filter(|c| c.formula.is_some()).map(|c| c.input_text()) + && has_udf_prefix(&stored) + && text == display(&stored, locale, wb, sheet) + { + return Ok(stored); + } if locale == FormulaLocale::En || wb.sheet(sheet).is_some_and(|sh| wb.styles.get(sh.style_id(cell)).num_fmt.as_str() == "@") { return Ok(text.to_string()); } @@ -40,16 +48,42 @@ pub(crate) fn canonical( } pub(crate) fn display(text: &str, locale: FormulaLocale, wb: &Workbook, sheet: usize) -> String { - // English (and every language without a formula table) shows the stored text untouched, - // and skips a parse per frame. + // English (and every language without a formula table) shows the stored text, `_xludf.` + // prefixes aside, and skips a parse per frame. if locale == FormulaLocale::En { - return text.to_string(); + return hide_udf_prefix(text, locale); } let Some(body) = text.strip_prefix('=') else { return text.to_string() }; - formula::parse(body) + let shown = formula::parse(body) .and_then(|expr| formula::print_input(&expr, locale, |name| wb.name(name, sheet).is_some() || wb.table(name).is_some())) .map(|body| format!("={body}")) - .unwrap_or_else(|_| text.to_string()) + .unwrap_or_else(|_| text.to_string()); + hide_udf_prefix(&shown, locale) +} + +const UDF_PREFIX: &str = "_xludf."; + +fn has_udf_prefix(text: &str) -> bool { + text.to_ascii_lowercase().contains(UDF_PREFIX) +} + +/// Excel shows a user-defined call such as `_xludf.EOMONTH()` as `=EOMONTH()`. Only function +/// names lose the prefix: text literals and names keep it. +fn hide_udf_prefix(text: &str, locale: FormulaLocale) -> String { + let Some(body) = text.strip_prefix('=').filter(|b| has_udf_prefix(b)) else { return text.to_string() }; + let Ok(tokens) = formula::lexer::tokenize_locale(body, locale) else { return text.to_string() }; + let mut out = String::from("="); + let mut from = 0; + for t in tokens { + if matches!(t.tok, formula::lexer::Tok::Func(_)) + && body.get(t.start..t.start + UDF_PREFIX.len()).is_some_and(|p| p.eq_ignore_ascii_case(UDF_PREFIX)) + { + out.push_str(body.get(from..t.start).unwrap_or("")); + from = t.start + UDF_PREFIX.len(); + } + } + out.push_str(body.get(from..).unwrap_or("")); + out } // Descriptions/signatures for the supported Spanish function table; untranslated functions diff --git a/crates/ui-egui/tests/formula_locale.rs b/crates/ui-egui/tests/formula_locale.rs index fe21218..8558693 100644 --- a/crates/ui-egui/tests/formula_locale.rs +++ b/crates/ui-egui/tests/formula_locale.rs @@ -1,7 +1,7 @@ //! Language selection must affect UI entry, never the canonical engine/file boundary. use gridcraft_engine::{ Session, - core::{CellRef, Value}, + core::{CellError, CellRef, Value}, }; use gridcraft_ui_egui::{SheetApp, UiState, i18n::Language}; use serde_json::json; @@ -201,3 +201,21 @@ fn czech_interface_retains_its_canonical_formula_fallback() { let restored: UiState = serde_json::from_str(&serde_json::to_string(&app.ui).unwrap()).unwrap(); assert_eq!(restored.language, Language::Cs); } + +#[test] +fn user_defined_prefix_is_hidden_while_editing_and_kept_on_commit() { + // Excel shows `_xludf.EOMONTH()` as `=EOMONTH()`; the cell still holds the user-defined call + // (#726), and committing the edit unchanged must not turn it into the built-in. + let mut app = app(); + app.run("cell.set", json!({"cell":"A1","input":"=_xludf.EOMONTH()&_xludf.CONCAT(\"_xludf.x\")"})).unwrap(); + app.begin_edit(None, true); + assert_eq!(app.editor.as_ref().unwrap().text, "=EOMONTH()&CONCAT(\"_xludf.x\")"); + assert!(app.commit_edit(0, 0, false, false)); + assert_eq!(formula(&app), "_xludf.EOMONTH()&_xludf.CONCAT(\"_xludf.x\")"); + assert_eq!(app.session.active().unwrap().wb.active().unwrap().value(CellRef::new(0, 0)), Value::Error(CellError::Name)); + app.run("app.language.set", json!({"language":"es"})).unwrap(); + app.begin_edit(None, true); + assert_eq!(app.editor.as_ref().unwrap().text, "=EOMONTH()&CONCAT(\"_xludf.x\")"); + assert!(app.commit_edit(0, 0, false, false)); + assert_eq!(formula(&app), "_xludf.EOMONTH()&_xludf.CONCAT(\"_xludf.x\")"); +}