mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-10 20:41:47 +00:00
formulas.recentlyUsed scanned the session journal for cell.set inputs. It missed formulas entered by range.* commands, lost them when the journal dropped its oldest entries (a large file open or a long script is enough), and started empty in every session. The Function Library's Recently Used menu and the Insert Function dialog list what it returns. - Prefs::recent_functions (a new engine module, recent.rs) holds the ten functions most recently entered in a formula, newest first. Session::execute updates it for every cell.* and range.* command it journals, from any source (the interface, the control channel, MCP or a script). It reads at most 2,000 of the formulas a command enters, starting from the last one; formulas longer than Excel's limit are skipped. formulas.recentlyUsed returns it. It never reads the journal back, so the journal's caps do not affect it. - The list is kept with the other engine preferences (prefs.json, the browser's storage on the web), so it survives a restart as Excel's does. It is read leniently: a malformed value loses only this list. A fresh install starts from common functions. The Insert Function dialog is titled "Insert Function", as in Excel (it said "Formula Builder"), and gains a working category list: Most Recently Used, All, then each category in Excel's order. A category may be given by label or by engine id, so a Function Library menu or the control channel (ui.dialog.set) opens it on one, and a new search switches to All. The dialog keeps its translated labels and its search in the interface language. Each row shows the function's name and its category in two columns; the category column starts after the widest name shown, so a long name in any font or formula language stays clear of it. A function counts as recently used once its formula is entered, not when OK is pressed. The unused "Formula Builder" translations are removed. Tests: engine unit tests for reading functions from formulas (strings, unknown names, over-long and malformed formulas), the formulas a command enters (newest first, within the budget), the ten-name list and the lenient read of prefs.json; tests/it/recently_used.rs for cell.set and range.setValues from the session, a failed command, a cleared journal and a save and reload of the preferences. UI tests for the menu's order and the dialog's title, categories, engine-id category, search and OK, and for the category column with a wide font.
119 lines
6.0 KiB
Rust
119 lines
6.0 KiB
Rust
use super::show::ok_cancel;
|
|
use super::{Dialog, Json, SheetApp, json, vec2};
|
|
|
|
/// Insert Function: search names (as the formula language spells them) and descriptions, or pick
|
|
/// a category (Most Recently Used, All or one category), then a function; each hit shows its
|
|
/// category and description. OK starts editing `=NAME(` (or appends to the formula).
|
|
pub(super) fn insert_function(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dialog, confirm: &mut bool) {
|
|
use crate::function_library::{self as lib, ALL, MOST_RECENTLY_USED};
|
|
let language = app.ui.language;
|
|
ui.horizontal(|ui| {
|
|
ui.label(language.tr("Search:"));
|
|
let r = ui.add(egui::TextEdit::singleline(&mut d.search).desired_width(300.0).hint_text(language.tr("e.g. lookup, average, date")));
|
|
r.request_focus();
|
|
});
|
|
// A new search (typed, or set through the control channel) looks through every function, as
|
|
// Excel's does.
|
|
if d.values.get("searched").and_then(Json::as_str) != Some(d.search.as_str()) {
|
|
d.list_index = 0;
|
|
if !d.search.trim().is_empty() {
|
|
d.values.insert("category".into(), json!(ALL));
|
|
}
|
|
d.values.insert("searched".into(), json!(d.search));
|
|
}
|
|
// The category may be given by label or by engine id (a Function Library menu, `ui.dialog.set`).
|
|
let saved = d.values.get("category").and_then(Json::as_str).unwrap_or(MOST_RECENTLY_USED).to_string();
|
|
let mut category = if saved.eq_ignore_ascii_case(ALL) {
|
|
ALL.to_string()
|
|
} else {
|
|
lib::category_id(&saved).map(lib::category_label).unwrap_or(MOST_RECENTLY_USED).to_string()
|
|
};
|
|
ui.horizontal(|ui| {
|
|
let label = ui.label(language.tr("Or select a category:"));
|
|
craft_ui::ComboBox::from_id_salt("insertFunctionCategory")
|
|
.width(220.0)
|
|
.selected_text(language.tr(&category))
|
|
.show_ui(ui, |ui| {
|
|
for c in [MOST_RECENTLY_USED, ALL].into_iter().chain(lib::CATEGORIES.iter().map(|(_, l)| *l)) {
|
|
ui.selectable_value(&mut category, c.to_string(), language.tr(c));
|
|
}
|
|
})
|
|
.response
|
|
.labelled_by(label.id);
|
|
});
|
|
if category != saved {
|
|
d.list_index = 0;
|
|
}
|
|
d.values.insert("category".into(), json!(category));
|
|
let locale = language.formula_locale();
|
|
// The names and descriptions searched are the ones shown, so in the interface language.
|
|
let q = d.search.trim().to_lowercase();
|
|
let matches = |f: &lib::FnEntry| {
|
|
q.is_empty()
|
|
|| locale.function_name(&f.name).to_lowercase().contains(&q)
|
|
|| language.function_description(&f.description).unwrap_or(&f.description).to_lowercase().contains(&q)
|
|
};
|
|
let hits: Vec<&lib::FnEntry> = if category == MOST_RECENTLY_USED {
|
|
lib::recent(app).iter().filter_map(|n| lib::find(n)).filter(|f| matches(f)).collect()
|
|
} else {
|
|
let id = lib::category_id(&category);
|
|
// The catalog is fixed (some 500 functions), so All lists every one.
|
|
lib::all().iter().filter(|f| id.is_none_or(|id| f.category == id) && matches(f)).collect()
|
|
};
|
|
let t = crate::theme::Tokens::get(ui.ctx());
|
|
let font = egui::TextStyle::Body.resolve(ui.style());
|
|
let rows: Vec<(&str, &str)> = hits.iter().map(|f| (locale.function_name(&f.name), language.tr(lib::category_label(&f.category)))).collect();
|
|
// The category column starts after the widest name shown, so a long name in any formula
|
|
// language or font never runs into it.
|
|
let name_w = rows.iter().map(|(n, _)| ui.painter().layout_no_wrap(n.to_string(), font.clone(), t.text).size().x).fold(0.0_f32, f32::max);
|
|
egui::ScrollArea::vertical().max_height(260.0).auto_shrink([false, false]).show(ui, |ui| {
|
|
if rows.is_empty() {
|
|
ui.label(egui::RichText::new(language.tr("No functions match.")).italics());
|
|
}
|
|
let row_h = ui.text_style_height(&egui::TextStyle::Body) + 4.0;
|
|
for (i, (name, category)) in rows.iter().enumerate() {
|
|
let (rect, r) = ui.allocate_exact_size(vec2(ui.available_width().max(name_w + 120.0), row_h), egui::Sense::click());
|
|
if i == d.list_index {
|
|
ui.painter().rect_filled(rect, 3.0, ui.visuals().selection.bg_fill);
|
|
} else if r.hovered() {
|
|
ui.painter().rect_filled(rect, 3.0, t.hover);
|
|
}
|
|
let color = if i == d.list_index { ui.visuals().selection.stroke.color } else { t.text };
|
|
ui.painter().text(rect.left_center() + vec2(6.0, 0.0), egui::Align2::LEFT_CENTER, *name, font.clone(), color);
|
|
ui.painter().text(rect.left_center() + vec2(6.0 + name_w + 16.0, 0.0), egui::Align2::LEFT_CENTER, *category, font.clone(), t.text_dim);
|
|
r.widget_info(|| egui::WidgetInfo::selected(egui::Role::Button, true, i == d.list_index, format!("{name} ({category})")));
|
|
if r.clicked() {
|
|
d.list_index = i;
|
|
}
|
|
if r.double_clicked() {
|
|
d.list_index = i;
|
|
*confirm = true;
|
|
}
|
|
}
|
|
});
|
|
if let Some(f) = hits.get(d.list_index) {
|
|
ui.separator();
|
|
ui.label(egui::RichText::new(crate::formula_locale::signature(&f.name, locale, language).unwrap_or_default()).strong());
|
|
ui.label(crate::formula_locale::description(&f.name, locale, language).unwrap_or_default());
|
|
}
|
|
let mut open = true;
|
|
ok_cancel(ui, confirm, &mut open);
|
|
// The function counts as recently used once the formula is entered (the engine notes it).
|
|
if *confirm && let Some(f) = hits.get(d.list_index) {
|
|
let n = app
|
|
.session
|
|
.active()
|
|
.map(|doc| crate::formula_locale::completion_name(&f.name, locale, &doc.wb, doc.wb.active_sheet()))
|
|
.unwrap_or(&f.name);
|
|
let cur = app.editor.as_ref().map(|e| e.text.clone());
|
|
let text = match cur {
|
|
Some(t) if t.starts_with('=') => format!("{t}{n}("),
|
|
_ => format!("={n}("),
|
|
};
|
|
app.begin_edit(Some(text), false);
|
|
}
|
|
if !open {
|
|
*confirm = true;
|
|
}
|
|
}
|