diff --git a/crates/engine/src/cf.rs b/crates/engine/src/cf.rs index 0c0f9683e..4c9325e36 100644 --- a/crates/engine/src/cf.rs +++ b/crates/engine/src/cf.rs @@ -136,6 +136,95 @@ fn lerp(a: [u8; 3], b: [u8; 3], t: f64) -> [u8; 3] { pub use gridcraft_model::icon_count; +struct RuleEval<'a> { + wb: &'a Workbook, + sheet: usize, + cell: CellRef, + first: CellRef, + value: &'a Value, + text: &'a str, + stats: &'a RuleStats, +} + +fn simple_rule_style<'a>(ctx: &RuleEval<'_>, rule: &'a CfRule) -> Option<&'a Style> { + let RuleEval { wb, sheet, cell, first, value, text, stats } = ctx; + match rule { + CfRule::CellIs { op, a, b, style } => { + let eval = |f: &str| gridcraft_calc::evaluate(wb, *sheet, *cell, &relative(f, *first, *cell)); + let av = eval(a); + let matches = match op { + CfOperator::Between | CfOperator::NotBetween => { + let bv = b.as_deref().map(eval).unwrap_or_default(); + let (lo, hi) = if compare(&av, &bv) == std::cmp::Ordering::Greater { (bv, av) } else { (av, bv) }; + let within = compare(value, &lo) != std::cmp::Ordering::Less && compare(value, &hi) != std::cmp::Ordering::Greater; + if *op == CfOperator::Between { within } else { !within } + } + CfOperator::Equal => compare(value, &av) == std::cmp::Ordering::Equal, + CfOperator::NotEqual => compare(value, &av) != std::cmp::Ordering::Equal, + CfOperator::Greater => compare(value, &av) == std::cmp::Ordering::Greater, + CfOperator::Less => compare(value, &av) == std::cmp::Ordering::Less, + CfOperator::GreaterOrEqual => compare(value, &av) != std::cmp::Ordering::Less, + CfOperator::LessOrEqual => compare(value, &av) != std::cmp::Ordering::Greater, + }; + (matches && !value.is_error()).then_some(style.as_ref()) + } + CfRule::Expression { formula, style } => { + gridcraft_calc::evaluate(wb, *sheet, *cell, &relative(formula, *first, *cell)).to_bool().unwrap_or(false).then_some(style.as_ref()) + } + CfRule::ContainsText { text: needle, style } => { + (!value.is_error() && text.to_lowercase().contains(&needle.to_lowercase())).then_some(style.as_ref()) + } + CfRule::NotContainsText { text: needle, style } => { + (value.is_error() || !text.to_lowercase().contains(&needle.to_lowercase())).then_some(style.as_ref()) + } + CfRule::BeginsWith { text: needle, style } => { + (!value.is_error() && text.to_lowercase().starts_with(&needle.to_lowercase())).then_some(style.as_ref()) + } + CfRule::EndsWith { text: needle, style } => { + (!value.is_error() && text.to_lowercase().ends_with(&needle.to_lowercase())).then_some(style.as_ref()) + } + CfRule::Blanks { style } => (value.is_empty() || text.trim().is_empty()).then_some(style.as_ref()), + CfRule::NoBlanks { style } => (!value.is_empty() && !text.trim().is_empty()).then_some(style.as_ref()), + CfRule::Errors { style } => value.is_error().then_some(style.as_ref()), + CfRule::NoErrors { style } => (!value.is_error()).then_some(style.as_ref()), + CfRule::Duplicate { style } => { + (!value.is_empty() && stats.counts.get(&text.to_lowercase()).copied().unwrap_or(0) > 1).then_some(style.as_ref()) + } + CfRule::Unique { style } => { + (!value.is_empty() && stats.counts.get(&text.to_lowercase()).copied().unwrap_or(0) == 1).then_some(style.as_ref()) + } + _ => None, + } +} + +fn time_period_matches(wb: &Workbook, period: &str, serial: f64) -> bool { + let today = gridcraft_core::date::serial_in_system(wb.date_system, gridcraft_calc::now_serial()).floor(); + let day = serial.floor(); + let date = |serial| gridcraft_core::date::datetime_from_serial(wb.date_system, serial); + let week_start = today - date(today).map(|value| value.weekday as f64).unwrap_or(0.0); + let (year, month) = date(today).map(|value| (value.year, value.month)).unwrap_or((2026, 1)); + let year_month = date(day).map(|value| (value.year, value.month)); + let month_matches = |offset: i32| { + let shifted_month = month as i32 + offset; + let shifted_year = year + (shifted_month - 1).div_euclid(12); + let shifted_month = (shifted_month - 1).rem_euclid(12) + 1; + year_month == Some((shifted_year, shifted_month as u32)) + }; + match period { + "yesterday" => day == today - 1.0, + "today" => day == today, + "tomorrow" => day == today + 1.0, + "last7Days" => day > today - 7.0 && day <= today, + "lastWeek" => day >= week_start - 7.0 && day < week_start, + "thisWeek" => day >= week_start && day < week_start + 7.0, + "nextWeek" => day >= week_start + 7.0 && day < week_start + 14.0, + "lastMonth" => month_matches(-1), + "thisMonth" => month_matches(0), + "nextMonth" => month_matches(1), + _ => false, + } +} + impl CfCache { pub fn new() -> Self { Self::default() @@ -161,52 +250,8 @@ impl CfCache { None => &no_stats, }; let text = v.display(); - let matched_style: Option<&Style> = match &cf.rule { - CfRule::CellIs { op, a, b, style } => { - let eval = |f: &str| gridcraft_calc::evaluate(wb, sheet, c, &relative(f, first, c)); - let av = eval(a); - let ok = match op { - CfOperator::Between | CfOperator::NotBetween => { - let bv = b.as_deref().map(eval).unwrap_or_default(); - let (lo, hi) = if compare(&av, &bv) == std::cmp::Ordering::Greater { (bv, av) } else { (av, bv) }; - let inside = compare(&v, &lo) != std::cmp::Ordering::Less && compare(&v, &hi) != std::cmp::Ordering::Greater; - if *op == CfOperator::Between { inside } else { !inside } - } - CfOperator::Equal => compare(&v, &av) == std::cmp::Ordering::Equal, - CfOperator::NotEqual => compare(&v, &av) != std::cmp::Ordering::Equal, - CfOperator::Greater => compare(&v, &av) == std::cmp::Ordering::Greater, - CfOperator::Less => compare(&v, &av) == std::cmp::Ordering::Less, - CfOperator::GreaterOrEqual => compare(&v, &av) != std::cmp::Ordering::Less, - CfOperator::LessOrEqual => compare(&v, &av) != std::cmp::Ordering::Greater, - }; - // As in Excel, a blank cell compares as 0 (or ""), and an error never matches. - (ok && !v.is_error()).then_some(style.as_ref()) - } - CfRule::Expression { formula, style } => { - gridcraft_calc::evaluate(wb, sheet, c, &relative(formula, first, c)).to_bool().unwrap_or(false).then_some(style.as_ref()) - } - // As in Excel, an error value never contains, begins or ends with anything (its rule - // formulas SEARCH/LEFT/RIGHT fail on it), so it always "does not contain" (#1162). - CfRule::ContainsText { text: t, style } => { - (!v.is_error() && text.to_lowercase().contains(&t.to_lowercase())).then_some(style.as_ref()) - } - CfRule::NotContainsText { text: t, style } => { - (v.is_error() || !text.to_lowercase().contains(&t.to_lowercase())).then_some(style.as_ref()) - } - CfRule::BeginsWith { text: t, style } => { - (!v.is_error() && text.to_lowercase().starts_with(&t.to_lowercase())).then_some(style.as_ref()) - } - CfRule::EndsWith { text: t, style } => (!v.is_error() && text.to_lowercase().ends_with(&t.to_lowercase())).then_some(style.as_ref()), - CfRule::Blanks { style } => (v.is_empty() || text.trim().is_empty()).then_some(style.as_ref()), - CfRule::NoBlanks { style } => (!v.is_empty() && !text.trim().is_empty()).then_some(style.as_ref()), - CfRule::Errors { style } => v.is_error().then_some(style.as_ref()), - CfRule::NoErrors { style } => (!v.is_error()).then_some(style.as_ref()), - CfRule::Duplicate { style } => { - (!v.is_empty() && st.counts.get(&text.to_lowercase()).copied().unwrap_or(0) > 1).then_some(style.as_ref()) - } - CfRule::Unique { style } => { - (!v.is_empty() && st.counts.get(&text.to_lowercase()).copied().unwrap_or(0) == 1).then_some(style.as_ref()) - } + let context = RuleEval { wb, sheet, cell: c, first, value: &v, text: &text, stats: st }; + let matched_style = simple_rule_style(&context, &cf.rule).or_else(|| match &cf.rule { CfRule::Top10 { bottom, percent, rank, style } => match v.as_f64() { Some(n) if !st.sorted.is_empty() => { let k = if *percent { ((st.sorted.len() as f64) * (*rank as f64) / 100.0).floor().max(1.0) as usize } else { *rank as usize }; @@ -229,36 +274,8 @@ impl CfCache { None => None, }, CfRule::TimePeriod { period, style } => match v.as_f64() { - Some(n) => { - let today = gridcraft_core::date::serial_in_system(wb.date_system, gridcraft_calc::now_serial()).floor(); - let d = n.floor(); - let wd = gridcraft_core::date::datetime_from_serial(wb.date_system, today).map(|x| x.weekday as f64).unwrap_or(0.0); - let week_start = today - wd; - let (ty, tm) = - gridcraft_core::date::datetime_from_serial(wb.date_system, today).map(|x| (x.year, x.month)).unwrap_or((2026, 1)); - let ym = gridcraft_core::date::datetime_from_serial(wb.date_system, d).map(|x| (x.year, x.month)); - let month_off = |k: i32| { - let m = tm as i32 + k; - let y = ty + (m - 1).div_euclid(12); - let m = (m - 1).rem_euclid(12) + 1; - ym == Some((y, m as u32)) - }; - let ok = match period.as_str() { - "yesterday" => d == today - 1.0, - "today" => d == today, - "tomorrow" => d == today + 1.0, - "last7Days" => d > today - 7.0 && d <= today, - "lastWeek" => d >= week_start - 7.0 && d < week_start, - "thisWeek" => d >= week_start && d < week_start + 7.0, - "nextWeek" => d >= week_start + 7.0 && d < week_start + 14.0, - "lastMonth" => month_off(-1), - "thisMonth" => month_off(0), - "nextMonth" => month_off(1), - _ => false, - }; - ok.then_some(style.as_ref()) - } - None => None, + Some(n) if time_period_matches(wb, period, n) => Some(style.as_ref()), + _ => None, }, CfRule::ColorScale { stops } => { if let Some(n) = v.as_f64() { @@ -329,7 +346,8 @@ impl CfCache { } None } - }; + _ => None, + }); if let Some(st) = matched_style { any = true; // Every matching rule applies; where two set the same part, the rule with the diff --git a/crates/engine/src/cmd.rs b/crates/engine/src/cmd.rs index ef6714eea..90c8a5227 100644 --- a/crates/engine/src/cmd.rs +++ b/crates/engine/src/cmd.rs @@ -205,7 +205,6 @@ pub(crate) fn register_undoable_test_command(id: &'static str, run: Run) { } fn test_commands() -> std::sync::MutexGuard<'static, Vec<&'static CommandSpec>> { - #[allow(clippy::unwrap_used)] // a poisoned test registry has no meaningful recovery match TEST_COMMANDS.lock() { Ok(guard) => guard, Err(poisoned) => poisoned.into_inner(), @@ -349,11 +348,15 @@ pub(crate) fn edit(s: &mut Session, f: impl FnOnce(&mut Ctx) -> Result) -> }) } +fn wall_clock() -> std::time::Duration { + std::time::SystemTime::UNIX_EPOCH.elapsed().unwrap_or_default() +} + pub(crate) fn commit(d: &mut DocState, f: impl FnOnce(&mut Ctx) -> Result) -> Result { - // `GRIDCRAFT_PROFILE` timing. Read the clock only when profiling, and never on wasm: there - // `Instant::now` traps (`RuntimeError: unreachable`), which broke every edit in the web app. - #[allow(clippy::disallowed_methods)] - let t0 = if cfg!(target_arch = "wasm32") { None } else { std::env::var_os("GRIDCRAFT_PROFILE").map(|_| std::time::Instant::now()) }; + // `GRIDCRAFT_PROFILE` timing, never on wasm where the clock traps. `elapsed` on the epoch + // reads the wall clock without the (disallowed) `now`. + let t0 = if cfg!(target_arch = "wasm32") { None } else { std::env::var_os("GRIDCRAFT_PROFILE").map(|_| wall_clock()) }; + let since = |t0: std::time::Duration| wall_clock().saturating_sub(t0); let mut sel = d.selection.clone(); let mut ctx = Ctx { wb: (*d.wb).clone(), changed: Vec::new(), structural: false, sel: &mut sel, fit_rows: Vec::new(), protection_checked: false }; let r = f(&mut ctx)?; @@ -368,7 +371,7 @@ pub(crate) fn commit(d: &mut DocState, f: impl FnOnce(&mut Ctx) -> Result) fit_rows.extend(changed.iter().map(|(s, c)| (*s, c.row))); } if let Some(t0) = t0 { - eprintln!("commit: edit {:?}, {} changed", t0.elapsed(), changed.len()); + eprintln!("commit: edit {:?}, {} changed", since(t0), changed.len()); } if structural { d.calc.recalc_all(&mut wb); @@ -376,7 +379,7 @@ pub(crate) fn commit(d: &mut DocState, f: impl FnOnce(&mut Ctx) -> Result) d.calc.cells_changed(&mut wb, &changed); } if let Some(t0) = t0 { - eprintln!("commit: + recalc {:?}", t0.elapsed()); + eprintln!("commit: + recalc {:?}", since(t0)); } fit_rows.sort_unstable(); fit_rows.dedup(); @@ -384,7 +387,7 @@ pub(crate) fn commit(d: &mut DocState, f: impl FnOnce(&mut Ctx) -> Result) auto_row_height(&mut wb, si, row); } if let Some(t0) = t0 { - eprintln!("commit: + rows {:?}", t0.elapsed()); + eprintln!("commit: + rows {:?}", since(t0)); } d.wb = std::sync::Arc::new(wb); d.selection = sel; diff --git a/crates/engine/src/cmd/analysis.rs b/crates/engine/src/cmd/analysis.rs index 6225b4934..9552e95f7 100644 --- a/crates/engine/src/cmd/analysis.rs +++ b/crates/engine/src/cmd/analysis.rs @@ -14,7 +14,7 @@ use gridcraft_model::{Cell, NumFmt, Sheet}; use serde_json::{Value as Json, json}; use super::whatif::parse_range; -use super::*; +use super::{CommandSpec, EngineError, Result, Session, bad, cmd, edit, has_doc, str_param}; const CMD: &str = "data.analysisToolPak"; /// Most input cells a tool reads. diff --git a/crates/engine/src/cmd/data.rs b/crates/engine/src/cmd/data.rs index 5fa076e64..143763a5e 100644 --- a/crates/engine/src/cmd/data.rs +++ b/crates/engine/src/cmd/data.rs @@ -4,10 +4,16 @@ use std::cmp::Ordering; use std::sync::Arc; use gridcraft_core::{CellRef, RangeRef, Value, sort_compare}; -use gridcraft_model::*; +use gridcraft_model::{ + Anchor, AnchorMode, AutoFilter, Cell, CfOperator, Comment, ErrorStyle, FilterCriterion, Formula, Hyperlink, LineInfo, Sheet, Validation, + ValidationKind, Workbook, +}; use serde_json::{Value as Json, json}; -use super::*; +use super::{ + CommandSpec, EngineError, Result, Session, bad, bool_param, cell_param, cell_param_on, cmd, edit, has_doc, ok, parse_lines, split_sheet, + str_param, target_range, target_ranges, target_sheet, +}; use crate::outline; use crate::selection::current_region; diff --git a/crates/engine/src/cmd/data/filter.rs b/crates/engine/src/cmd/data/filter.rs index 71aa698f4..1b52e4537 100644 --- a/crates/engine/src/cmd/data/filter.rs +++ b/crates/engine/src/cmd/data/filter.rs @@ -1,5 +1,8 @@ use super::sort::sort_key; -use super::*; +use super::{ + AutoFilter, CellRef, EngineError, FilterCriterion, Json, Ordering, RangeRef, Result, Session, Sheet, Value, Workbook, bad, bool_param, col_param, + current_region, data_target, edit, json, +}; // ---------------------------------------------------------------- filter diff --git a/crates/engine/src/cmd/data/grouping.rs b/crates/engine/src/cmd/data/grouping.rs index 7b5a2254c..d2e183ae1 100644 --- a/crates/engine/src/cmd/data/grouping.rs +++ b/crates/engine/src/cmd/data/grouping.rs @@ -1,4 +1,7 @@ -use super::*; +use super::{ + CellRef, EngineError, Json, LineInfo, RangeRef, Result, Session, bad, bool_param, col_param, data_target, edit, json, outline, parse_lines, + str_param, target_sheet, +}; // ---------------------------------------------------------------- outline diff --git a/crates/engine/src/cmd/data/sort.rs b/crates/engine/src/cmd/data/sort.rs index 919de1734..a36527650 100644 --- a/crates/engine/src/cmd/data/sort.rs +++ b/crates/engine/src/cmd/data/sort.rs @@ -1,4 +1,7 @@ -use super::*; +use super::{ + Anchor, AnchorMode, Arc, Cell, CellRef, Comment, EngineError, Formula, Hyperlink, Json, LineInfo, Ordering, RangeRef, Result, Session, Sheet, + Value, bad, bool_param, col_param, data_range, edit, guess_header, json, ok, sort_compare, split_sheet, str_param, target_sheet, +}; #[derive(Clone)] struct SortKey { diff --git a/crates/engine/src/cmd/data/tools.rs b/crates/engine/src/cmd/data/tools.rs index 1603b2cb4..2cbca17bd 100644 --- a/crates/engine/src/cmd/data/tools.rs +++ b/crates/engine/src/cmd/data/tools.rs @@ -1,4 +1,7 @@ -use super::*; +use super::{ + Cell, CellRef, EngineError, Json, RangeRef, Result, Session, bad, bool_param, cell_param_on, col_param, data_target, edit, guess_header, json, + ok, str_param, target_range, target_sheet, +}; // ---------------------------------------------------------------- tools diff --git a/crates/engine/src/cmd/data/validation.rs b/crates/engine/src/cmd/data/validation.rs index 6d4ae73cf..ce0d7e3b8 100644 --- a/crates/engine/src/cmd/data/validation.rs +++ b/crates/engine/src/cmd/data/validation.rs @@ -1,4 +1,7 @@ -use super::*; +use super::{ + Cell, CellRef, CfOperator, EngineError, ErrorStyle, Json, RangeRef, Result, Session, Validation, ValidationKind, Value, Workbook, bad, + bool_param, cell_param, edit, json, ok, str_param, target_ranges, target_sheet, +}; // ---------------------------------------------------------------- validation diff --git a/crates/engine/src/cmd/draw.rs b/crates/engine/src/cmd/draw.rs index a86be3099..f6184509c 100644 --- a/crates/engine/src/cmd/draw.rs +++ b/crates/engine/src/cmd/draw.rs @@ -1,10 +1,10 @@ //! Draw tab: pen and eraser tools, ink strokes, Ink to Shape. use gridcraft_core::CellRef; -use gridcraft_model::*; +use gridcraft_model::{Anchor, Color, Shape, ShapeKind}; use serde_json::{Value as Json, json}; -use super::*; +use super::{CommandSpec, EngineError, Result, Session, bad, bool_param, cmd, edit, f64_param, has_doc, ok, str_param, u32_param}; pub fn specs() -> Vec { vec![ diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs index e48fe8838..f977a95ce 100644 --- a/crates/engine/src/cmd/edit.rs +++ b/crates/engine/src/cmd/edit.rs @@ -4,7 +4,10 @@ use gridcraft_core::{CellRef, MAX_ROWS, RangeRef, Value}; use gridcraft_model::{Cell, Formula, StyleId}; use serde_json::{Value as Json, json}; -use super::*; +use super::{ + CommandSpec, EngineError, Result, bad, bool_param, can_redo, can_undo, cell_param, cell_param_on, cmd, edit, f64_param, has_clipboard, has_doc, + locked_cell_error, ok, split_sheet, str_param, target_range, target_ranges, target_sheet, u32_param, +}; use crate::selection::{current_region, jump}; use crate::{Clipboard, Session}; diff --git a/crates/engine/src/cmd/edit/cell.rs b/crates/engine/src/cmd/edit/cell.rs index 62a6b38bd..c57afae97 100644 --- a/crates/engine/src/cmd/edit/cell.rs +++ b/crates/engine/src/cmd/edit/cell.rs @@ -1,4 +1,7 @@ -use super::*; +use super::{ + Cell, CellRef, EngineError, Formula, Json, MAX_ROWS, RangeRef, Result, Session, StyleId, Value, bad, bool_param, cell_param_on, edit, json, + locked_cell_error, str_param, target_range, target_ranges, target_sheet, +}; // ---------------------------------------------------------------- entry diff --git a/crates/engine/src/cmd/edit/clipboard.rs b/crates/engine/src/cmd/edit/clipboard.rs index 60c05c687..de921226e 100644 --- a/crates/engine/src/cmd/edit/clipboard.rs +++ b/crates/engine/src/cmd/edit/clipboard.rs @@ -1,6 +1,8 @@ use super::cell::input_to_cell; use super::paste_special::paste_special; -use super::*; +use super::{ + Cell, Clipboard, EngineError, Json, RangeRef, Result, Session, Value, cell_param_on, edit, json, ok, str_param, target_ranges, target_sheet, +}; // ---------------------------------------------------------------- clipboard diff --git a/crates/engine/src/cmd/edit/fill.rs b/crates/engine/src/cmd/edit/fill.rs index 84f4cd6d3..8cd1e3d88 100644 --- a/crates/engine/src/cmd/edit/fill.rs +++ b/crates/engine/src/cmd/edit/fill.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{CellRef, EngineError, Json, RangeRef, Result, Session, bad, edit, f64_param, json, str_param, target_range, target_sheet}; // ---------------------------------------------------------------- fill @@ -78,7 +78,7 @@ pub(super) fn fill_series(s: &mut Session, p: &Json) -> Result { let stop = f64_param(p, "stop"); let unit = str_param(p, "dateUnit").unwrap_or("day").to_string(); edit(s, |cx| { - crate::fill::series(cx, sheet, r, rows, &kind, step, stop, &unit)?; + crate::fill::series(cx, sheet, crate::fill::SeriesOptions { range: r, rows, kind: &kind, step, stop, unit: &unit })?; Ok(Json::Null) }) } diff --git a/crates/engine/src/cmd/edit/find.rs b/crates/engine/src/cmd/edit/find.rs index 2dcbfa886..bce0ba728 100644 --- a/crates/engine/src/cmd/edit/find.rs +++ b/crates/engine/src/cmd/edit/find.rs @@ -1,5 +1,5 @@ use super::cell::input_to_cell; -use super::*; +use super::{Cell, CellRef, EngineError, Json, Result, Session, Value, bad, bool_param, edit, json, str_param}; // ---------------------------------------------------------------- find & replace diff --git a/crates/engine/src/cmd/edit/go_to.rs b/crates/engine/src/cmd/edit/go_to.rs index b94bcd221..bc4f414db 100644 --- a/crates/engine/src/cmd/edit/go_to.rs +++ b/crates/engine/src/cmd/edit/go_to.rs @@ -1,5 +1,5 @@ use super::selection::{select_region, selection_set}; -use super::*; +use super::{CellRef, EngineError, Json, RangeRef, Result, Session, bad, bool_param, coalesce, json, split_sheet, str_param}; /// How many references the Go To dialog keeps for going back to. pub(super) const GO_TO_RECENT: usize = 10; diff --git a/crates/engine/src/cmd/edit/paste_special.rs b/crates/engine/src/cmd/edit/paste_special.rs index eba1b429e..0a6d14247 100644 --- a/crates/engine/src/cmd/edit/paste_special.rs +++ b/crates/engine/src/cmd/edit/paste_special.rs @@ -1,5 +1,76 @@ use super::clipboard::What; -use super::*; +use super::{ + Cell, CellRef, EngineError, Formula, Json, RangeRef, Result, Session, StyleId, Value, bad, bool_param, cell_param_on, edit, json, str_param, + target_sheet, +}; + +fn tiled_cells(rows: u32, cols: u32, tiles_r: u32, tiles_c: u32) -> impl Iterator + Clone { + (0..tiles_r).flat_map(move |tr| (0..tiles_c).flat_map(move |tc| (0..rows).flat_map(move |r| (0..cols).map(move |c| (tr, tc, r, c))))) +} + +fn apply_operation(op: &str, old: &Cell, new: &mut Cell) { + let operand = new.value.clone(); + if let (Ok(x), Ok(y)) = (old.value.to_number(), operand.to_number()) + && (old.value.is_number() || old.value.is_empty() || old.value.is_text()) + && (operand.is_number() || operand.is_empty()) + { + new.value = match op { + "add" => Value::number(x + y), + "subtract" => Value::number(x - y), + "multiply" => Value::number(x * y), + "divide" if y == 0.0 => Value::Error(gridcraft_core::CellError::Div0), + "divide" => Value::number(x / y), + _ => operand.clone(), + }; + } else if (old.value.is_text() || old.value.is_error()) && (operand.is_number() || operand.is_empty()) { + new.value = old.value.clone(); + } + if let Some(formula) = &old.formula + && let Ok(y) = operand.to_number() + && (operand.is_number() || operand.is_empty()) + && let Some(symbol) = operation_symbol(op) + { + let text = format!("=({}){symbol}{}", formula.text, gridcraft_core::number_to_text(y)); + new.formula = Some(std::sync::Arc::new(Formula::new(&text))); + new.value = Value::Empty; + } +} + +fn operation_symbol(op: &str) -> Option { + match op { + "add" => Some('+'), + "subtract" => Some('-'), + "multiply" => Some('*'), + "divide" => Some('/'), + _ => None, + } +} + +fn copy_merges( + cx: &mut crate::cmd::Ctx, + dest_sheet: usize, + src_sheet: &gridcraft_model::Sheet, + src: RangeRef, + at: CellRef, + what: What, + transpose: bool, +) -> Result<()> { + if !matches!(what, What::All | What::Formats) || transpose { + return Ok(()); + } + let merges: Vec = src_sheet.merges.iter().filter(|merge| src.contains_range(merge)).copied().collect(); + for merge in merges { + let row_offset = at.row as i64 - src.start.row as i64; + let col_offset = at.col as i64 - src.start.col as i64; + if let (Some(start), Some(end)) = (merge.start.offset(row_offset, col_offset), merge.end.offset(row_offset, col_offset)) { + let moved = RangeRef::new(start, end); + let sheet = cx.sheet_mut(dest_sheet)?; + sheet.merges.retain(|existing| !existing.intersects(&moved)); + sheet.merges.push(moved); + } + } + Ok(()) +} pub(super) fn paste_special(s: &mut Session, p: &Json) -> Result { let Some(clip) = s.clipboard.clone() else { return Err(EngineError::Other("Nothing to paste.".into())) }; @@ -74,209 +145,145 @@ pub(super) fn paste_special(s: &mut Session, p: &Json) -> Result { } moved_formulas = taken; } - for tr in 0..tiles_r { - for tc in 0..tiles_c { - for r in 0..src.height() { - for c in 0..src.width() { - let sc = CellRef::new(src.start.row + r, src.start.col + c); - let (dr, dc) = if transpose { (c, r) } else { (r, c) }; - let Some(dest) = at.offset((tr * h + dr) as i64, (tc * w + dc) as i64) else { continue }; - let src_cell = if cut_move { - moved_formulas.iter().find(|(k, _)| *k == sc).map(|(_, v)| v.clone()) + for (tr, tc, r, c) in tiled_cells(src.height(), src.width(), tiles_r, tiles_c) { + let sc = CellRef::new(src.start.row + r, src.start.col + c); + let (dr, dc) = if transpose { (c, r) } else { (r, c) }; + let Some(dest) = at.offset((tr * h + dr) as i64, (tc * w + dc) as i64) else { continue }; + let src_cell = if cut_move { moved_formulas.iter().find(|(k, _)| *k == sc).map(|(_, v)| v.clone()) } else { src_sheet.cell(sc).cloned() }; + let src_val = src_sheet.value(sc); + let src_picture = if cut_move { moved_pictures.get(&sc) } else { src_sheet.cell_pictures.get(&sc) }.cloned(); + if skip_blanks && src_picture.is_none() && src_val.is_empty() && src_cell.as_ref().is_none_or(|x| x.formula.is_none()) { + continue; + } + let old = cx.wb.sheet(dest_sheet).and_then(|sh| sh.cell(dest)).cloned().unwrap_or_default(); + let old_picture = cx.wb.sheet(dest_sheet).and_then(|sh| sh.cell_pictures.get(&dest)).cloned(); + let mut new_picture = old_picture.clone(); + let mut new = old.clone(); + let shift = |cell: &Cell| -> Option> { + let f = cell.formula.as_ref()?; + let e = f.expr()?; + if cut_move { + // A moved formula still reads cells on its original sheet. Qualify + // those references before the workbook-wide move adjustment below. + let e = if dest_sheet != clip.sheet { + e.map(&mut |x| match x { + gridcraft_formula::Expr::Ref(mut r) if r.sheet == gridcraft_formula::SheetSel::Current => { + r.sheet = gridcraft_formula::SheetSel::Named(src_sheet_name.clone()); + gridcraft_formula::Expr::Ref(r) + } + other => other, + }) + } else { + e + }; + let mut moved = Formula::from_expr(e); + // The array range moves with its formula. + let (dr, dc) = (dest.row as i64 - sc.row as i64, dest.col as i64 - sc.col as i64); + moved.array = f.array.and_then(|a| Some(RangeRef::new(a.start.offset(dr, dc)?, a.end.offset(dr, dc)?))); + moved.dynamic = f.dynamic; + return Some(std::sync::Arc::new(moved)); + } + let moved = gridcraft_formula::adjust::shift_relative(e, dest.row as i64 - sc.row as i64, dest.col as i64 - sc.col as i64); + Some(std::sync::Arc::new(Formula { dynamic: f.dynamic, ..Formula::from_expr(moved) })) + }; + if link { + let sheet_prefix = if dest_sheet == clip.sheet && same_doc { + String::new() + } else { + format!("{}!", gridcraft_formula::quote_sheet(&src_sheet_name)) + }; + new.formula = Some(std::sync::Arc::new(Formula::new(&format!("={sheet_prefix}{}", sc.a1())))); + new.value = Value::Empty; + new_picture = None; + } else { + let sc_cell = src_cell.clone().unwrap_or_default(); + match what { + What::All | What::AllExceptBorders => { + new_picture = src_picture.clone(); + new.formula = shift(&sc_cell); + new.value = if new.formula.is_some() { Value::Empty } else { sc_cell.value.clone() }; + let st = map_style(sc_cell.style, &mut cx.wb); + new.style = if what == What::AllExceptBorders { + let borders = cx.wb.styles.get(old.style).borders; + cx.wb.styles.derive(st, |s| s.borders = borders) } else { - src_sheet.cell(sc).cloned() + st }; - let src_val = src_sheet.value(sc); - let src_picture = if cut_move { moved_pictures.get(&sc) } else { src_sheet.cell_pictures.get(&sc) }.cloned(); - if skip_blanks && src_picture.is_none() && src_val.is_empty() && src_cell.as_ref().is_none_or(|x| x.formula.is_none()) { - continue; + } + What::Formulas | What::FormulasAndNumberFormats => { + new_picture = src_picture.clone(); + new.formula = shift(&sc_cell); + new.value = if new.formula.is_some() { Value::Empty } else { sc_cell.value.clone() }; + if what == What::FormulasAndNumberFormats { + let nf = src_wb.styles.get(sc_cell.style).num_fmt.clone(); + new.style = cx.wb.styles.derive(old.style, |s| s.num_fmt = nf); } - let old = cx.wb.sheet(dest_sheet).and_then(|sh| sh.cell(dest)).cloned().unwrap_or_default(); - let old_picture = cx.wb.sheet(dest_sheet).and_then(|sh| sh.cell_pictures.get(&dest)).cloned(); - let mut new_picture = old_picture.clone(); - let mut new = old.clone(); - let shift = |cell: &Cell| -> Option> { - let f = cell.formula.as_ref()?; - let e = f.expr()?; - if cut_move { - // A moved formula still reads cells on its original sheet. Qualify - // those references before the workbook-wide move adjustment below. - let e = if dest_sheet != clip.sheet { - e.map(&mut |x| match x { - gridcraft_formula::Expr::Ref(mut r) if r.sheet == gridcraft_formula::SheetSel::Current => { - r.sheet = gridcraft_formula::SheetSel::Named(src_sheet_name.clone()); - gridcraft_formula::Expr::Ref(r) - } - other => other, - }) - } else { - e - }; - let mut moved = Formula::from_expr(e); - // The array range moves with its formula. - let (dr, dc) = (dest.row as i64 - sc.row as i64, dest.col as i64 - sc.col as i64); - moved.array = f.array.and_then(|a| Some(RangeRef::new(a.start.offset(dr, dc)?, a.end.offset(dr, dc)?))); - moved.dynamic = f.dynamic; - return Some(std::sync::Arc::new(moved)); - } - let moved = - gridcraft_formula::adjust::shift_relative(e, dest.row as i64 - sc.row as i64, dest.col as i64 - sc.col as i64); - Some(std::sync::Arc::new(Formula { dynamic: f.dynamic, ..Formula::from_expr(moved) })) - }; - if link { - let sheet_prefix = if dest_sheet == clip.sheet && same_doc { - String::new() - } else { - format!("{}!", gridcraft_formula::quote_sheet(&src_sheet_name)) - }; - new.formula = Some(std::sync::Arc::new(Formula::new(&format!("={sheet_prefix}{}", sc.a1())))); - new.value = Value::Empty; - new_picture = None; - } else { - let sc_cell = src_cell.clone().unwrap_or_default(); - match what { - What::All | What::AllExceptBorders => { - new_picture = src_picture.clone(); - new.formula = shift(&sc_cell); - new.value = if new.formula.is_some() { Value::Empty } else { sc_cell.value.clone() }; - let st = map_style(sc_cell.style, &mut cx.wb); - new.style = if what == What::AllExceptBorders { - let borders = cx.wb.styles.get(old.style).borders; - cx.wb.styles.derive(st, |s| s.borders = borders) - } else { - st - }; - } - What::Formulas | What::FormulasAndNumberFormats => { - new_picture = src_picture.clone(); - new.formula = shift(&sc_cell); - new.value = if new.formula.is_some() { Value::Empty } else { sc_cell.value.clone() }; - if what == What::FormulasAndNumberFormats { - let nf = src_wb.styles.get(sc_cell.style).num_fmt.clone(); - new.style = cx.wb.styles.derive(old.style, |s| s.num_fmt = nf); - } - } - What::Values | What::ValuesAndNumberFormats => { - new_picture = src_picture.clone(); - new.formula = None; - new.value = src_val.clone(); - if what == What::ValuesAndNumberFormats { - let nf = src_wb.styles.get(sc_cell.style).num_fmt.clone(); - new.style = cx.wb.styles.derive(old.style, |s| s.num_fmt = nf); - } - } - What::Formats => new.style = map_style(sc_cell.style, &mut cx.wb), - What::Comments | What::Validation | What::ColumnWidths => {} - } - // Operations combine numbers with what is already there. - if op != "none" - && matches!(what, What::All | What::Values | What::ValuesAndNumberFormats | What::Formulas) - && new.formula.is_none() - { - if old_picture.is_some() || new_picture.is_some() { - new_picture = None; - new.value = Value::Error(gridcraft_core::CellError::Value); - } - let a = old.value.clone(); - let b = new.value.clone(); - // Numbers stored as text take part (multiplying by 1 turns them into - // numbers); other text and errors stay as they are. - if let (Ok(x), Ok(y)) = (a.to_number(), b.to_number()) - && (a.is_number() || a.is_empty() || a.is_text()) - && (b.is_number() || b.is_empty()) - { - new.value = match op.as_str() { - "add" => Value::number(x + y), - "subtract" => Value::number(x - y), - "multiply" => Value::number(x * y), - "divide" => { - if y == 0.0 { - Value::Error(gridcraft_core::CellError::Div0) - } else { - Value::number(x / y) - } - } - _ => b.clone(), - }; - } else if (a.is_text() || a.is_error()) && (b.is_number() || b.is_empty()) { - new.value = a; - } - // A formula already there is kept and combined with the pasted number. - if let Some(f) = &old.formula - && let Ok(y) = b.to_number() - && (b.is_number() || b.is_empty()) - && let Some(sym) = match op.as_str() { - "add" => Some('+'), - "subtract" => Some('-'), - "multiply" => Some('*'), - "divide" => Some('/'), - _ => None, - } - { - let text = format!("=({}){sym}{}", f.text, gridcraft_core::number_to_text(y)); - new.formula = Some(std::sync::Arc::new(Formula::new(&text))); - new.value = Value::Empty; - } - } + } + What::Values | What::ValuesAndNumberFormats => { + new_picture = src_picture.clone(); + new.formula = None; + new.value = src_val.clone(); + if what == What::ValuesAndNumberFormats { + let nf = src_wb.styles.get(sc_cell.style).num_fmt.clone(); + new.style = cx.wb.styles.derive(old.style, |s| s.num_fmt = nf); } - // The copied cell's validation rule replaces whatever rule the destination had. - if !link && matches!(what, What::All | What::AllExceptBorders | What::Validation) { - let rule = src_sheet.validations.iter().find(|dv| dv.ranges.iter().any(|r| r.contains(sc))).cloned(); - let sh = cx.sheet_mut(dest_sheet)?; - crate::cmd::data::remove_validation_cells(&mut sh.validations, &[RangeRef::new(dest, dest)]); - if let Some(mut dv) = rule { - // Relative references move with the rule, as pasted formulas do. - let first = dv.ranges.first().map_or(sc, |r| r.start); - dv.f1 = crate::cmd::data::move_rule_formula(&dv.f1, first, dest); - dv.f2 = dv.f2.map(|f| crate::cmd::data::move_rule_formula(&f, first, dest)); - dv.ranges = vec![RangeRef::new(dest, dest)]; - sh.validations.push(dv); - } - } - match what { - What::Comments => { - if let Some(cm) = src_sheet.comments.get(&sc).cloned() { - cx.sheet_mut(dest_sheet)?.comments.insert(dest, cm); - } - } - What::ColumnWidths => { - if let Some(info) = src_sheet.cols.get(&sc.col).copied() { - cx.sheet_mut(dest_sheet)?.cols.insert(dest.col, info); - } - } - What::All => { - if let Some(cm) = src_sheet.comments.get(&sc).cloned() { - cx.sheet_mut(dest_sheet)?.comments.insert(dest, cm); - } - if let Some(h) = src_sheet.hyperlinks.get(&sc).cloned() { - cx.sheet_mut(dest_sheet)?.hyperlinks.insert(dest, h); - } - cx.sheet_mut(dest_sheet)?.set_cell(dest, new); - } - _ => cx.sheet_mut(dest_sheet)?.set_cell(dest, new), - } - if let Some(picture) = new_picture { - cx.sheet_mut(dest_sheet)?.cell_pictures.insert(dest, picture); - } - cx.touch(dest_sheet, dest); + } + What::Formats => new.style = map_style(sc_cell.style, &mut cx.wb), + What::Comments | What::Validation | What::ColumnWidths => {} + } + // Operations combine numbers with what is already there. + if op != "none" && matches!(what, What::All | What::Values | What::ValuesAndNumberFormats | What::Formulas) && new.formula.is_none() { + if old_picture.is_some() || new_picture.is_some() { + new_picture = None; + new.value = Value::Error(gridcraft_core::CellError::Value); + } + // Numbers stored as text participate; other text and errors stay as they are. + apply_operation(&op, &old, &mut new); + } + } + // The copied cell's validation rule replaces whatever rule the destination had. + if !link && matches!(what, What::All | What::AllExceptBorders | What::Validation) { + let rule = src_sheet.validations.iter().find(|dv| dv.ranges.iter().any(|r| r.contains(sc))).cloned(); + let sh = cx.sheet_mut(dest_sheet)?; + crate::cmd::data::remove_validation_cells(&mut sh.validations, &[RangeRef::new(dest, dest)]); + if let Some(mut dv) = rule { + // Relative references move with the rule, as pasted formulas do. + let first = dv.ranges.first().map_or(sc, |r| r.start); + dv.f1 = crate::cmd::data::move_rule_formula(&dv.f1, first, dest); + dv.f2 = dv.f2.map(|f| crate::cmd::data::move_rule_formula(&f, first, dest)); + dv.ranges = vec![RangeRef::new(dest, dest)]; + sh.validations.push(dv); + } + } + match what { + What::Comments => { + if let Some(cm) = src_sheet.comments.get(&sc).cloned() { + cx.sheet_mut(dest_sheet)?.comments.insert(dest, cm); } } + What::ColumnWidths => { + if let Some(info) = src_sheet.cols.get(&sc.col).copied() { + cx.sheet_mut(dest_sheet)?.cols.insert(dest.col, info); + } + } + What::All => { + if let Some(cm) = src_sheet.comments.get(&sc).cloned() { + cx.sheet_mut(dest_sheet)?.comments.insert(dest, cm); + } + if let Some(h) = src_sheet.hyperlinks.get(&sc).cloned() { + cx.sheet_mut(dest_sheet)?.hyperlinks.insert(dest, h); + } + cx.sheet_mut(dest_sheet)?.set_cell(dest, new); + } + _ => cx.sheet_mut(dest_sheet)?.set_cell(dest, new), } + if let Some(picture) = new_picture { + cx.sheet_mut(dest_sheet)?.cell_pictures.insert(dest, picture); + } + cx.touch(dest_sheet, dest); } // Merged areas inside the copied block travel with it. - if matches!(what, What::All | What::Formats) && !transpose { - let merges: Vec = src_sheet.merges.iter().filter(|m| src.contains_range(m)).copied().collect(); - for m in merges { - let dr = at.row as i64 - src.start.row as i64; - let dc = at.col as i64 - src.start.col as i64; - if let (Some(a), Some(b)) = (m.start.offset(dr, dc), m.end.offset(dr, dc)) { - let nm = RangeRef::new(a, b); - let sh = cx.sheet_mut(dest_sheet)?; - sh.merges.retain(|x| !x.intersects(&nm)); - sh.merges.push(nm); - } - } - } + copy_merges(cx, dest_sheet, src_sheet, src, at, what, transpose)?; if cut_move { // References elsewhere to the moved block now point to its new place. let dest_name = cx.wb.sheet(dest_sheet).map(|s| s.name.clone()).unwrap_or_default(); diff --git a/crates/engine/src/cmd/edit/selection.rs b/crates/engine/src/cmd/edit/selection.rs index 95793f338..ede66a34d 100644 --- a/crates/engine/src/cmd/edit/selection.rs +++ b/crates/engine/src/cmd/edit/selection.rs @@ -1,4 +1,7 @@ -use super::*; +use super::{ + CellRef, EngineError, Json, RangeRef, Result, Session, bool_param, cell_param, current_region, json, jump, split_sheet, str_param, target_ranges, + target_sheet, +}; // ---------------------------------------------------------------- selection diff --git a/crates/engine/src/cmd/extra.rs b/crates/engine/src/cmd/extra.rs index bf21433ab..c9a1582a2 100644 --- a/crates/engine/src/cmd/extra.rs +++ b/crates/engine/src/cmd/extra.rs @@ -2,10 +2,16 @@ //! tools, Get Data, scripts (Automate), cell checkboxes, theme colours and fonts. use gridcraft_core::{CellRef, RangeRef, Value}; -use gridcraft_model::*; +use gridcraft_model::{ + Anchor, Chart, Color, DefinedName, HAlign, LegendPos, NumFmt, Shape, Sheet, SheetViewMode, TableColumn, TotalsFn, Trendline, TrendlineKind, + Workbook, +}; use serde_json::{Value as Json, json}; -use super::*; +use super::{ + CommandSpec, EngineError, Result, Session, always, anchor_mode_name, bad, bool_param, cell_param, cell_param_on, cmd, edit, f64_param, has_doc, + ok, split_sheet, str_param, target_range, target_ranges, target_sheet, u32_param, +}; mod cells; mod chart; diff --git a/crates/engine/src/cmd/extra/cells.rs b/crates/engine/src/cmd/extra/cells.rs index 5cddf0982..ff0d5b65b 100644 --- a/crates/engine/src/cmd/extra/cells.rs +++ b/crates/engine/src/cmd/extra/cells.rs @@ -1,4 +1,7 @@ -use super::*; +use super::{ + CellRef, EngineError, HAlign, Json, NumFmt, Result, Session, Value, bad, bool_param, cell_param, cell_param_on, edit, json, object_params, ok, + str_param, target_ranges, target_sheet, +}; /// Whether `c` has checkbox formatting. pub(crate) fn is_checkbox_cell(wb: &gridcraft_model::Workbook, sh: &gridcraft_model::Sheet, c: CellRef) -> bool { diff --git a/crates/engine/src/cmd/extra/chart.rs b/crates/engine/src/cmd/extra/chart.rs index 2e16befa6..b6761f1a9 100644 --- a/crates/engine/src/cmd/extra/chart.rs +++ b/crates/engine/src/cmd/extra/chart.rs @@ -1,4 +1,7 @@ -use super::*; +use super::{ + CellRef, Chart, Color, EngineError, Json, LegendPos, RangeRef, Result, Session, Trendline, TrendlineKind, bad, bool_param, cell_param_on, edit, + f64_param, json, split_sheet, str_param, u32_param, +}; fn find_chart(s: &Session, p: &Json) -> Result<(usize, usize)> { let d = s.doc()?; diff --git a/crates/engine/src/cmd/extra/data.rs b/crates/engine/src/cmd/extra/data.rs index cef840f21..9a1ae7417 100644 --- a/crates/engine/src/cmd/extra/data.rs +++ b/crates/engine/src/cmd/extra/data.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{CellRef, EngineError, Json, RangeRef, Result, Session, TableColumn, TotalsFn, Value, bad, edit, json, ok, str_param, target_range}; pub(super) fn table_resize(s: &mut Session, p: &Json) -> Result { let r = str_param(p, "range").and_then(RangeRef::parse).ok_or_else(|| bad("table.resize", "missing `range`"))?; diff --git a/crates/engine/src/cmd/extra/objects.rs b/crates/engine/src/cmd/extra/objects.rs index 59ab8447a..30725f81e 100644 --- a/crates/engine/src/cmd/extra/objects.rs +++ b/crates/engine/src/cmd/extra/objects.rs @@ -1,5 +1,7 @@ use super::chart::format_series; -use super::*; +use super::{ + Anchor, CellRef, Color, EngineError, Json, Result, Session, Shape, Sheet, anchor_mode_name, bad, bool_param, edit, json, str_param, u32_param, +}; pub(super) fn shape_fill(s: &mut Session, p: &Json) -> Result { if str_param(p, "kind") == Some("shape") { diff --git a/crates/engine/src/cmd/extra/scripts.rs b/crates/engine/src/cmd/extra/scripts.rs index 9f86837ab..f1118410f 100644 --- a/crates/engine/src/cmd/extra/scripts.rs +++ b/crates/engine/src/cmd/extra/scripts.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{Json, Result, SCRIPTS_NAME, Session, Workbook, bad, bool_param, edit, json, stash_get, stash_map, stash_set, str_param}; pub(super) fn record(s: &mut Session, p: &Json) -> Result { if bool_param(p, "stop").unwrap_or(false) || s.recording.is_some() { diff --git a/crates/engine/src/cmd/extra/window.rs b/crates/engine/src/cmd/extra/window.rs index 19e92743f..b20a00a61 100644 --- a/crates/engine/src/cmd/extra/window.rs +++ b/crates/engine/src/cmd/extra/window.rs @@ -1,4 +1,7 @@ -use super::*; +use super::{ + CellRef, EngineError, Json, Result, Session, SheetViewMode, VIEWS_NAME, WATCH_NAME, Workbook, bad, bool_param, edit, json, ok, split_sheet, + stash_get, stash_map, stash_set, str_param, target_sheet, u32_param, +}; /// Switches a sheet's view. Like zoom, the view is saved with the file but is not an edit: it /// neither records an undo step nor marks a clean workbook as changed. diff --git a/crates/engine/src/cmd/file.rs b/crates/engine/src/cmd/file.rs index 9af85b9dd..8d28c25a5 100644 --- a/crates/engine/src/cmd/file.rs +++ b/crates/engine/src/cmd/file.rs @@ -2,7 +2,7 @@ use serde_json::{Value as Json, json}; -use super::*; +use super::{CommandSpec, Result, Session, always, bad, bool_param, cmd, edit, has_doc, ok, str_param, target_sheet, u32_param}; use crate::DocState; pub fn specs() -> Vec { diff --git a/crates/engine/src/cmd/forecast.rs b/crates/engine/src/cmd/forecast.rs index b7472fd22..9bb15936b 100644 --- a/crates/engine/src/cmd/forecast.rs +++ b/crates/engine/src/cmd/forecast.rs @@ -11,7 +11,7 @@ use gridcraft_functions::Timeline; use gridcraft_model::{Anchor, Cell, Chart, ChartKind, Formula, LegendPos, Series, Sheet, StyleId, Table, TableColumn, TotalsFn}; use serde_json::{Value as Json, json}; -use super::*; +use super::{CommandSpec, EngineError, Result, Session, bad, bool_param, cmd, edit, has_doc, split_sheet, str_param}; use crate::selection::current_region; const CMD: &str = "data.forecastSheet"; diff --git a/crates/engine/src/cmd/format.rs b/crates/engine/src/cmd/format.rs index 744b6d136..69123dcfa 100644 --- a/crates/engine/src/cmd/format.rs +++ b/crates/engine/src/cmd/format.rs @@ -1,10 +1,13 @@ //! Formatting: Home › Font, Alignment, Number, Styles, Cells › Format. use gridcraft_core::{CellRef, RangeRef}; -use gridcraft_model::*; +use gridcraft_model::{BorderLine, BorderStyle, Borders, Color, Fill, HAlign, NumFmt, Style, StyleId, Theme, Underline, VAlign, VertAlign}; use serde_json::{Value as Json, json}; -use super::*; +use super::{ + CommandSpec, EngineError, Result, Session, bad, bool_param, cmd, edit, f64_param, has_doc, ok, parse_lines, split_sheet, str_param, + target_ranges, target_sheet, +}; pub fn specs() -> Vec { vec![ diff --git a/crates/engine/src/cmd/format/cell_styles.rs b/crates/engine/src/cmd/format/cell_styles.rs index 53d378202..5ad0ccc15 100644 --- a/crates/engine/src/cmd/format/cell_styles.rs +++ b/crates/engine/src/cmd/format/cell_styles.rs @@ -1,4 +1,6 @@ -use super::*; +use super::{ + ACCOUNTING, BorderLine, BorderStyle, Borders, COMMA, Color, Fill, Json, NumFmt, Result, Session, Style, Theme, apply_style, bad, str_param, +}; /// Built-in cell styles (our own palette). pub fn builtin_cell_style(name: &str, theme: &Theme) -> Option