Engine: the macro recorder keeps a step's own sheet, stays bounded, and can be cancelled

Six fixes from a review of the recorder, each with a regression test (four
fail against the old recorder; the other two bound work that grew with the
input):

- A step that names another sheet with its sheet parameter (cell.set and
  about fifty other commands) was tokenized as if on the input's sheet, so a
  recorded cell.set {sheet: Two, cell: C4} ran on Two at the input's
  relative place. Such a step keeps its references, like a sheet-qualified
  one.
- Inlining recorded runs of other macros and scripts stops at the macro's
  step limit and counts the rest in dropped, instead of expanding every run
  first (a thousand runs of a long macro made millions of steps).
- macro.record {cancel: true} ends a recording without saving it: the way
  out of a recording whose macro is too large for the workbook, which could
  neither be saved nor ended.
- Formula tokenization reads each reference in the formula and matches it,
  instead of listing every column and row of the example (an example as
  large as a sheet made a million candidates per formula).
- A selection of several areas keeps its active cell as it is; a token for
  the active cell alone put it outside the areas on playback.
- Steps recorded in another workbook are counted in dropped.
This commit is contained in:
prxnvt
2026-10-23 23:56:32 -04:00
parent 67fa9f7b4e
commit bb3db53489
4 changed files with 189 additions and 45 deletions
+11 -3
View File
@@ -45,7 +45,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Record Macro",
["View", "Macros"],
None,
"{stop?: bool, relative?: bool (default true; when starting), name?, description? (when stopping; a taken name gets a suffix)} toggles recording; with relative references the selection when it starts is the example input → {recording, relative?, input?} or {recording: false, saved, steps, dropped, skipped, inputs}",
"{stop?: bool, cancel?: bool (ends a recording without saving it), relative?: bool (default true; when starting), name?, description? (when stopping; a taken name gets a suffix)} toggles recording; with relative references the selection when it starts is the example input → {recording, relative?, input?} or {recording: false, saved, steps, dropped, skipped, inputs}",
has_doc,
record
),
@@ -163,6 +163,13 @@ fn check(s: &mut Session, p: &Json) -> Result<Json> {
}
fn record(s: &mut Session, p: &Json) -> Result<Json> {
// `cancel` ends a recording without saving it: the way out of a recording whose macro
// cannot be saved (too large for the workbook, for one).
if bool_param(p, "cancel").unwrap_or(false) {
let was = s.stop_recording().is_some();
s.macro_recording = None;
return Ok(json!({"recording": false, "cancelled": was}));
}
if bool_param(p, "stop").unwrap_or(false) || s.is_recording() {
return stop_recording(s, p);
}
@@ -228,8 +235,9 @@ fn save_recording(s: &mut Session, p: &Json, rec: &crate::record::Recorder, star
None => crate::macros::next_name(&wb),
};
let mut converted = crate::macros::recorder::convert(&name, &start, &rec.steps, &wb);
// Steps past the macro limit are left out and counted with the ones the recorder dropped.
let cut = converted.mac.steps.len().saturating_sub(gridcraft_model::MAX_MACRO_STEPS);
// Steps past the macro limit, and ones recorded in another workbook, are left out and counted
// with the ones the recorder dropped.
let cut = converted.cut.saturating_add(converted.mac.steps.len().saturating_sub(gridcraft_model::MAX_MACRO_STEPS));
converted.mac.steps.truncate(gridcraft_model::MAX_MACRO_STEPS);
if let Some(desc) = str_param(p, "description") {
converted.mac.description = desc.chars().take(gridcraft_model::MAX_MACRO_TEXT).collect();