Compare commits

..
Author SHA1 Message Date
Dylan M. Taylor 55ff9d9f54 refactor: split complex functions for Sonar S3776
Extract helpers from auto_sum (selection and single-cell guess), update_cursor
(drag, hover, freeze, and header-edge cursors) and read_styles (dxf reader).
No behaviour change.
2026-10-25 21:29:13 -04:00
6 changed files with 187 additions and 243 deletions
+53 -67
View File
@@ -927,70 +927,6 @@ impl Host for PassHost<'_> {
} }
} }
/// Pops one finished component off Tarjan's stack, labelling its nodes with `comp_index`. The
/// component is everything up to and including `root`.
fn pop_component(stack: &mut Vec<usize>, on_stack: &mut [bool], comp: &mut [usize], comp_index: usize, root: usize) {
while let Some(w) = stack.pop() {
on_stack[w] = false;
comp[w] = comp_index;
if w == root {
break;
}
}
}
/// Strongly connected components of a graph given as adjacency lists (Tarjan, iterative): each
/// node's component index, and the number of nodes in each component.
fn components(edges: &[Vec<usize>]) -> (Vec<usize>, Vec<usize>) {
let n = edges.len();
let (mut low, mut num) = (vec![0usize; n], vec![usize::MAX; n]);
let (mut on_stack, mut stack, mut comp) = (vec![false; n], Vec::new(), vec![usize::MAX; n]);
let (mut next, mut comps) = (0usize, 0usize);
for root in 0..n {
if num[root] != usize::MAX {
continue;
}
let mut call: Vec<(usize, usize)> = vec![(root, 0)];
num[root] = next;
low[root] = next;
next += 1;
stack.push(root);
on_stack[root] = true;
while let Some(top) = call.last_mut() {
let v = top.0;
if let Some(&w) = edges.get(v).and_then(|e| e.get(top.1)) {
top.1 += 1;
if num[w] == usize::MAX {
num[w] = next;
low[w] = next;
next += 1;
stack.push(w);
on_stack[w] = true;
call.push((w, 0));
} else if on_stack[w] {
low[v] = low[v].min(num[w]);
}
continue;
}
call.pop();
if let Some(&(u, _)) = call.last() {
low[u] = low[u].min(low[v]);
}
if low[v] == num[v] {
pop_component(&mut stack, &mut on_stack, &mut comp, comps, v);
comps += 1;
}
}
}
let mut size = vec![0usize; comps];
for &c in &comp {
if let Some(s) = size.get_mut(c) {
*s += 1;
}
}
(comp, size)
}
impl Calc { impl Calc {
pub fn new() -> Self { pub fn new() -> Self {
Self::default() Self::default()
@@ -1031,7 +967,6 @@ impl Calc {
if circ.is_empty() { if circ.is_empty() {
return Vec::new(); return Vec::new();
} }
let n = circ.len();
let index: HashMap<Key, usize> = circ.iter().enumerate().map(|(i, k)| (*k, i)).collect(); let index: HashMap<Key, usize> = circ.iter().enumerate().map(|(i, k)| (*k, i)).collect();
// Edges from a cell to the `#CIRC!` formulas that read it; loops are the same either way. // Edges from a cell to the `#CIRC!` formulas that read it; loops are the same either way.
let mut deps = Vec::new(); let mut deps = Vec::new();
@@ -1046,8 +981,59 @@ impl Calc {
e e
}) })
.collect(); .collect();
// Loop members: nodes in a component of more than one, or with an edge to themselves. // Tarjan's strongly connected components, iterative.
let (comp, size) = components(&edges); let n = circ.len();
let (mut low, mut num) = (vec![0usize; n], vec![usize::MAX; n]);
let (mut on_stack, mut stack, mut comp) = (vec![false; n], Vec::new(), vec![usize::MAX; n]);
let (mut next, mut comps) = (0usize, 0usize);
for root in 0..n {
if num[root] != usize::MAX {
continue;
}
let mut call: Vec<(usize, usize)> = vec![(root, 0)];
num[root] = next;
low[root] = next;
next += 1;
stack.push(root);
on_stack[root] = true;
while let Some(top) = call.last_mut() {
let v = top.0;
if let Some(&w) = edges.get(v).and_then(|e| e.get(top.1)) {
top.1 += 1;
if num[w] == usize::MAX {
num[w] = next;
low[w] = next;
next += 1;
stack.push(w);
on_stack[w] = true;
call.push((w, 0));
} else if on_stack[w] {
low[v] = low[v].min(num[w]);
}
continue;
}
call.pop();
if let Some(&(u, _)) = call.last() {
low[u] = low[u].min(low[v]);
}
if low[v] == num[v] {
while let Some(w) = stack.pop() {
on_stack[w] = false;
comp[w] = comps;
if w == v {
break;
}
}
comps += 1;
}
}
}
let mut size = vec![0usize; comps];
for &c in &comp {
if let Some(s) = size.get_mut(c) {
*s += 1;
}
}
let on_loop = |i: usize| comp.get(i).and_then(|c| size.get(*c)).is_some_and(|s| *s > 1) || edges.get(i).is_some_and(|e| e.contains(&i)); let on_loop = |i: usize| comp.get(i).and_then(|c| size.get(*c)).is_some_and(|s| *s > 1) || edges.get(i).is_some_and(|e| e.contains(&i));
// A formula with dynamic references (INDIRECT, OFFSET) may close a loop the graph can't see. // A formula with dynamic references (INDIRECT, OFFSET) may close a loop the graph can't see.
let mut out: Vec<Key> = let mut out: Vec<Key> =
+25 -29
View File
@@ -1454,34 +1454,6 @@ fn show_levels(s: &mut Session, p: &Json) -> Result<Json> {
/// Data ▸ Outline ▸ Settings: whether summary rows sit below their detail and summary columns /// Data ▸ Outline ▸ Settings: whether summary rows sit below their detail and summary columns
/// right of it. A changed direction moves each collapsed group's mark (+) to its new summary line. /// right of it. A changed direction moves each collapsed group's mark (+) to its new summary line.
/// Moves the collapsed marks of a line axis to the summary line on its new side (`on` is the new
/// setting, `was_before` the old one). A line that nested groups share keeps its mark on the outermost
/// group whose detail is all hidden, or the outermost when none is.
fn move_marks(map: &mut std::collections::BTreeMap<u32, LineInfo>, was_before: bool, on: bool) {
let mut marked: Vec<outline::OutlineGroup> =
outline::groups_with(map, was_before).into_iter().filter(|g| g.has_summary() && map.get(&g.summary()).is_some_and(|e| e.collapsed)).collect();
// A mark on a line that nested groups share moves with the outermost of them whose
// detail is all hidden, or with the outermost when none is.
let mut owner: std::collections::BTreeMap<u32, (bool, u8)> = std::collections::BTreeMap::new();
for g in &marked {
let key = (!g.collapsed, g.level);
let best = owner.entry(g.summary()).or_insert(key);
*best = (*best).min(key);
}
marked.retain(|g| owner.get(&g.summary()) == Some(&(!g.collapsed, g.level)));
for g in &marked {
if let Some(e) = map.get_mut(&g.summary()) {
e.collapsed = false;
}
}
for g in marked {
let g = outline::OutlineGroup { summary_before: !on, ..g };
if g.has_summary() {
map.entry(g.summary()).or_default().collapsed = true;
}
}
}
fn outline_settings(s: &mut Session, p: &Json) -> Result<Json> { fn outline_settings(s: &mut Session, p: &Json) -> Result<Json> {
let si = target_sheet(s, p)?; let si = target_sheet(s, p)?;
edit(s, |cx| { edit(s, |cx| {
@@ -1497,7 +1469,31 @@ fn outline_settings(s: &mut Session, p: &Json) -> Result<Json> {
if was_before != on { if was_before != on {
continue; continue;
} }
move_marks(if rows { &mut sh.rows } else { &mut sh.cols }, was_before, on); let map = if rows { &mut sh.rows } else { &mut sh.cols };
let mut marked: Vec<outline::OutlineGroup> = outline::groups_with(map, was_before)
.into_iter()
.filter(|g| g.has_summary() && map.get(&g.summary()).is_some_and(|e| e.collapsed))
.collect();
// A mark on a line that nested groups share moves with the outermost of them whose
// detail is all hidden, or with the outermost when none is.
let mut owner: std::collections::BTreeMap<u32, (bool, u8)> = std::collections::BTreeMap::new();
for g in &marked {
let key = (!g.collapsed, g.level);
let best = owner.entry(g.summary()).or_insert(key);
*best = (*best).min(key);
}
marked.retain(|g| owner.get(&g.summary()) == Some(&(!g.collapsed, g.level)));
for g in &marked {
if let Some(e) = map.get_mut(&g.summary()) {
e.collapsed = false;
}
}
for g in marked {
let g = outline::OutlineGroup { summary_before: !on, ..g };
if g.has_summary() {
map.entry(g.summary()).or_default().collapsed = true;
}
}
} }
Ok(json!({"summaryBelow": sh.summary_below, "summaryRight": sh.summary_right})) Ok(json!({"summaryBelow": sh.summary_below, "summaryRight": sh.summary_right}))
}) })
+25 -40
View File
@@ -957,35 +957,6 @@ pub fn export_as(app: &mut SheetApp, fmt: &str) -> bool {
fn options(app: &mut SheetApp, ui: &mut Ui) { fn options(app: &mut SheetApp, ui: &mut Ui) {
let lang = app.ui.language; let lang = app.ui.language;
section(ui, lang, "Options|General"); section(ui, lang, "Options|General");
general_options(app, ui, lang);
ui.add_space(6.0);
let mut formula_bar = app.session.prefs.formula_bar;
if ui.checkbox(&mut formula_bar, lang.tr("Show formula bar")).changed() {
app.run_or_alert("view.formulaBar", json!({"on": formula_bar}));
}
ui.add_space(18.0);
section(ui, lang, "Options|Editing");
let mut autocomplete = app.ui.autocomplete;
if ui.checkbox(&mut autocomplete, lang.tr("Enable AutoComplete for cell values")).changed() {
app.run_or_alert("options.autoComplete", json!({"on": autocomplete}));
}
ui.add_space(18.0);
section(ui, lang, "Options|Save");
let mut autosave = app.ui.autosave;
if ui.checkbox(&mut autosave, lang.tr("Save .xlsx workbooks automatically after every change")).changed() {
app.toggle_autosave();
}
let Some(calc) = app.session.active().map(|d| d.wb.calc.clone()) else { return };
ui.add_space(18.0);
section(ui, lang, "Formulas");
ui.label(lang.tr("Workbook calculation"));
calc_options(app, ui, lang, &calc);
ui.add_space(6.0);
dim(ui, lang.tr("Calculation settings are saved with the workbook."));
}
/// The display theme and interface language controls of the General section.
fn general_options(app: &mut SheetApp, ui: &mut Ui, lang: Language) {
egui::Grid::new("backstage_options_general").num_columns(2).spacing(vec2(18.0, 10.0)).show(ui, |ui| { egui::Grid::new("backstage_options_general").num_columns(2).spacing(vec2(18.0, 10.0)).show(ui, |ui| {
let label = ui.label(lang.tr("Display theme")); let label = ui.label(lang.tr("Display theme"));
let mode = app.ui.theme_mode(); let mode = app.ui.theme_mode();
@@ -1022,10 +993,27 @@ fn general_options(app: &mut SheetApp, ui: &mut Ui, lang: Language) {
.labelled_by(label.id); .labelled_by(label.id);
ui.end_row(); ui.end_row();
}); });
} ui.add_space(6.0);
let mut formula_bar = app.session.prefs.formula_bar;
/// The calculation mode, and iterative calculation, of the active workbook. if ui.checkbox(&mut formula_bar, lang.tr("Show formula bar")).changed() {
fn calc_options(app: &mut SheetApp, ui: &mut Ui, lang: Language, calc: &gridcraft_engine::model::CalcSettings) { app.run_or_alert("view.formulaBar", json!({"on": formula_bar}));
}
ui.add_space(18.0);
section(ui, lang, "Options|Editing");
let mut autocomplete = app.ui.autocomplete;
if ui.checkbox(&mut autocomplete, lang.tr("Enable AutoComplete for cell values")).changed() {
app.run_or_alert("options.autoComplete", json!({"on": autocomplete}));
}
ui.add_space(18.0);
section(ui, lang, "Options|Save");
let mut autosave = app.ui.autosave;
if ui.checkbox(&mut autosave, lang.tr("Save .xlsx workbooks automatically after every change")).changed() {
app.toggle_autosave();
}
let Some(calc) = app.session.active().map(|d| d.wb.calc.clone()) else { return };
ui.add_space(18.0);
section(ui, lang, "Formulas");
ui.label(lang.tr("Workbook calculation"));
use gridcraft_engine::model::CalcMode; use gridcraft_engine::model::CalcMode;
for (mode, id, label) in [ for (mode, id, label) in [
(CalcMode::Automatic, "automatic", "Automatic"), (CalcMode::Automatic, "automatic", "Automatic"),
@@ -1042,13 +1030,6 @@ fn calc_options(app: &mut SheetApp, ui: &mut Ui, lang: Language, calc: &gridcraf
app.run_or_alert("formulas.calculationOptions", json!({"iterative": iterative})); app.run_or_alert("formulas.calculationOptions", json!({"iterative": iterative}));
} }
// Maximum Iterations and Maximum Change apply only while iteration is on, as in Excel. // Maximum Iterations and Maximum Change apply only while iteration is on, as in Excel.
iteration_options(app, ui, lang, calc, iterative);
// TODO: add "Set precision as displayed" here once `formulas.calculationOptions` takes
// `precisionAsDisplayed` (PR #1366); the model already stores it.
}
/// Maximum Iterations and Maximum Change, live only while iterative calculation is on.
fn iteration_options(app: &mut SheetApp, ui: &mut Ui, lang: Language, calc: &gridcraft_engine::model::CalcSettings, iterative: bool) {
ui.add_enabled_ui(iterative, |ui| { ui.add_enabled_ui(iterative, |ui| {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.add_space(24.0); ui.add_space(24.0);
@@ -1089,6 +1070,10 @@ fn iteration_options(app: &mut SheetApp, ui: &mut Ui, lang: Language, calc: &gri
}); });
}); });
}); });
// TODO: add "Set precision as displayed" here once `formulas.calculationOptions` takes
// `precisionAsDisplayed` (PR #1366); the model already stores it.
ui.add_space(6.0);
dim(ui, lang.tr("Calculation settings are saved with the workbook."));
} }
/// Refuses a typed setting with a message, leaving the workbook as it was. The Enter that /// Refuses a typed setting with a message, leaving the workbook as it was. The Enter that
+22 -27
View File
@@ -78,32 +78,6 @@ pub fn highlight(text: &str, lang: Lang) -> Vec<(Range<usize>, Token)> {
} }
} }
/// End (exclusive, in chars) of the Python string literal opening with quote `c` at char `i`.
fn python_string_end(chars: &[(usize, char)], i: usize, c: char) -> usize {
let triple = chars.get(i + 1).is_some_and(|(_, d)| *d == c) && chars.get(i + 2).is_some_and(|(_, d)| *d == c);
let mut j = if triple { i + 3 } else { i + 1 };
loop {
match chars.get(j) {
None => break,
Some((_, '\\')) => j += 2,
Some((_, '\n')) if !triple => break,
Some((_, d)) if *d == c => {
if !triple {
j += 1;
break;
}
if chars.get(j + 1).is_some_and(|(_, d)| *d == c) && chars.get(j + 2).is_some_and(|(_, d)| *d == c) {
j += 3;
break;
}
j += 1;
}
Some(_) => j += 1,
}
}
j.min(chars.len())
}
fn python(text: &str) -> Vec<(Range<usize>, Token)> { fn python(text: &str) -> Vec<(Range<usize>, Token)> {
let chars: Vec<(usize, char)> = text.char_indices().collect(); let chars: Vec<(usize, char)> = text.char_indices().collect();
let end_of = |i: usize| chars.get(i).map(|(b, _)| *b).unwrap_or(text.len()); let end_of = |i: usize| chars.get(i).map(|(b, _)| *b).unwrap_or(text.len());
@@ -118,7 +92,28 @@ fn python(text: &str) -> Vec<(Range<usize>, Token)> {
out.push((start..end_of(j), Token::Comment)); out.push((start..end_of(j), Token::Comment));
i = j; i = j;
} else if c == '"' || c == '\'' { } else if c == '"' || c == '\'' {
let j = python_string_end(&chars, i, c); let triple = chars.get(i + 1).is_some_and(|(_, d)| *d == c) && chars.get(i + 2).is_some_and(|(_, d)| *d == c);
let mut j = if triple { i + 3 } else { i + 1 };
loop {
match chars.get(j) {
None => break,
Some((_, '\\')) => j += 2,
Some((_, '\n')) if !triple => break,
Some((_, d)) if *d == c => {
if !triple {
j += 1;
break;
}
if chars.get(j + 1).is_some_and(|(_, d)| *d == c) && chars.get(j + 2).is_some_and(|(_, d)| *d == c) {
j += 3;
break;
}
j += 1;
}
Some(_) => j += 1,
}
}
let j = j.min(chars.len());
out.push((start..end_of(j), Token::String)); out.push((start..end_of(j), Token::String));
i = j; i = j;
} else if c.is_ascii_digit() || (c == '.' && chars.get(i + 1).is_some_and(|(_, d)| d.is_ascii_digit())) { } else if c.is_ascii_digit() || (c == '.' && chars.get(i + 1).is_some_and(|(_, d)| d.is_ascii_digit())) {
+24 -30
View File
@@ -165,35 +165,6 @@ impl Dir<'_> {
} }
/// The band's buttons and brackets for this frame. /// The band's buttons and brackets for this frame.
/// The bracket for one group's detail, and its tick, on the lane `across`; `button` is the group's
/// summary button when it is on screen.
fn group_bracket(out: &mut Layout, dir: &Dir, grp: &OutlineGroup, button: Option<f32>, across: f32, side: f32) {
let first = dir.first_visible(grp.start, grp.end);
let Some((first_line, (a0, _))) = first else { return };
if grp.summary_before {
// From the button down (or across) to the last detail line, with the tick there.
let a0 = button.map_or(a0, |mid| mid + side / 2.0);
let end = dir.start(grp.end) + dir.size(grp.end);
let a1 = end.min(dir.hi);
if a1 <= a0 {
return;
}
let tick = (end <= dir.hi).then(|| [dir.point(a1 - 0.5, across), dir.point(a1 - 0.5, across + side / 2.0)]);
out.brackets.push(Bracket { line: [dir.point(a0, across), dir.point(a1 - 0.5, across)], tick });
return;
}
let a1 = match button {
Some(mid) => mid - side / 2.0,
None => dir.start(grp.summary()).min(dir.hi),
};
if a1 <= a0 {
return;
}
let start_shown = first_line == grp.start && (a0 - dir.start(grp.start)).abs() < 0.5;
let tick = start_shown.then(|| [dir.point(a0 + 0.5, across), dir.point(a0 + 0.5, across + side / 2.0)]);
out.brackets.push(Bracket { line: [dir.point(a0 + 0.5, across), dir.point(a1, across)], tick });
}
pub fn layout(geo: &Geo, sh: &Sheet) -> Layout { pub fn layout(geo: &Geo, sh: &Sheet) -> Layout {
let mut out = Layout::default(); let mut out = Layout::default();
let zoom = (geo.z / DISPLAY_SCALE).max(0.1); let zoom = (geo.z / DISPLAY_SCALE).max(0.1);
@@ -241,7 +212,30 @@ pub fn layout(geo: &Geo, sh: &Sheet) -> Layout {
continue; continue;
} }
// The bracket, over the part of the detail that is on screen. // The bracket, over the part of the detail that is on screen.
group_bracket(&mut out, dir, grp, button, across, side); let first = dir.first_visible(grp.start, grp.end);
let Some((first_line, (a0, _))) = first else { continue };
if grp.summary_before {
// From the button down (or across) to the last detail line, with the tick there.
let a0 = button.map_or(a0, |mid| mid + side / 2.0);
let end = dir.start(grp.end) + dir.size(grp.end);
let a1 = end.min(dir.hi);
if a1 <= a0 {
continue;
}
let tick = (end <= dir.hi).then(|| [dir.point(a1 - 0.5, across), dir.point(a1 - 0.5, across + side / 2.0)]);
out.brackets.push(Bracket { line: [dir.point(a0, across), dir.point(a1 - 0.5, across)], tick });
continue;
}
let a1 = match button {
Some(mid) => mid - side / 2.0,
None => dir.start(grp.summary()).min(dir.hi),
};
if a1 <= a0 {
continue;
}
let start_shown = first_line == grp.start && (a0 - dir.start(grp.start)).abs() < 0.5;
let tick = start_shown.then(|| [dir.point(a0 + 0.5, across), dir.point(a0 + 0.5, across + side / 2.0)]);
out.brackets.push(Bracket { line: [dir.point(a0 + 0.5, across), dir.point(a1, across)], tick });
} }
} }
out out
+38 -50
View File
@@ -116,55 +116,6 @@ pub fn catalog_keys(source: &str) -> BTreeSet<String> {
keys keys
} }
/// The character a `\u{...}` escape stands for, if it names a valid code point.
fn unicode_escape(chars: &mut std::iter::Peekable<std::str::CharIndices<'_>>) -> Option<char> {
let mut hex = String::new();
for (_, c) in chars.by_ref() {
match c {
'}' => break,
'{' => {}
c => hex.push(c),
}
}
u32::from_str_radix(&hex, 16).ok().and_then(char::from_u32)
}
/// The value of a string literal whose opening quote was just read, and the byte offset past
/// its closing quote (`len`, the source's length, when it is never closed).
fn string_literal(chars: &mut std::iter::Peekable<std::str::CharIndices<'_>>, len: usize) -> (String, usize) {
let mut value = String::new();
let mut end = len;
while let Some((j, c)) = chars.next() {
match c {
'"' => {
end = j + 1;
break;
}
'\\' => match chars.next().map(|(_, c)| c) {
Some('n') => value.push('\n'),
Some('t') => value.push('\t'),
Some('r') => value.push('\r'),
Some('0') => value.push('\0'),
Some(c @ ('\\' | '"' | '\'')) => value.push(c),
Some('u') => {
if let Some(c) = unicode_escape(chars) {
value.push(c);
}
}
// A line continuation skips the newline and the next line's indentation.
Some('\n') => {
while chars.peek().is_some_and(|(_, c)| c.is_whitespace()) {
chars.next();
}
}
_ => {}
},
c => value.push(c),
}
}
(value, end)
}
/// Every string literal outside `//` comments: its decoded value, and the byte range from its /// Every string literal outside `//` comments: its decoded value, and the byte range from its
/// opening quote to just past its closing one. /// opening quote to just past its closing one.
fn literals(source: &str) -> Vec<(String, usize, usize)> { fn literals(source: &str) -> Vec<(String, usize, usize)> {
@@ -182,7 +133,44 @@ fn literals(source: &str) -> Vec<(String, usize, usize)> {
if c != '"' { if c != '"' {
continue; continue;
} }
let (value, end) = string_literal(&mut chars, source.len()); let mut value = String::new();
let mut end = source.len();
while let Some((j, c)) = chars.next() {
match c {
'"' => {
end = j + 1;
break;
}
'\\' => match chars.next().map(|(_, c)| c) {
Some('n') => value.push('\n'),
Some('t') => value.push('\t'),
Some('r') => value.push('\r'),
Some('0') => value.push('\0'),
Some(c @ ('\\' | '"' | '\'')) => value.push(c),
Some('u') => {
let mut hex = String::new();
for (_, c) in chars.by_ref() {
match c {
'}' => break,
'{' => {}
c => hex.push(c),
}
}
if let Some(c) = u32::from_str_radix(&hex, 16).ok().and_then(char::from_u32) {
value.push(c);
}
}
// A line continuation skips the newline and the next line's indentation.
Some('\n') => {
while chars.peek().is_some_and(|(_, c)| c.is_whitespace()) {
chars.next();
}
}
_ => {}
},
c => value.push(c),
}
}
out.push((value, i, end)); out.push((value, i, end));
} }
out out