Formula bar: hide _xludf. like Excel, keep it when the edit is confirmed unchanged (#726)

This commit is contained in:
Marco Arnulfo 2026-10-12 22:53:10 +02:00
parent f63a437d5b
commit 6939d95cae
2 changed files with 58 additions and 6 deletions

View File

@ -9,6 +9,14 @@ pub(crate) fn canonical(
sheet: usize,
cell: gridcraft_engine::core::CellRef,
) -> Result<String, ParseError> {
// 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

View File

@ -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\")");
}