mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-24 10:46:50 -06:00
Merge pull request #1920 from vedran-dobos:range-fill-formats
This commit is contained in:
@@ -338,20 +338,31 @@ fn range_fill(s: &mut Session, p: &Json) -> Result<Json> {
|
||||
let input = str_param(p, "input").unwrap_or("").to_string();
|
||||
let origin = s.doc()?.selection.active;
|
||||
edit(s, |cx| {
|
||||
let base = input_to_cell(&input, sheet, origin, &mut cx.wb)?;
|
||||
// Each cell gets what typing the input there gives: the automatic number format where
|
||||
// it's General, its own format otherwise, text where it's Text. That depends only on
|
||||
// the cell's style, so the input is parsed once per style.
|
||||
let mut by_style: std::collections::HashMap<StyleId, Option<Cell>> = std::collections::HashMap::new();
|
||||
for r in &ranges {
|
||||
if r.count() > 2_000_000 {
|
||||
return Err(bad("range.fill", "range too large"));
|
||||
}
|
||||
for at in r.iter() {
|
||||
let mut cell = base.clone().unwrap_or_default();
|
||||
let style = cx.wb.sheet(sheet).map(|sh| sh.style_id(at)).unwrap_or_default();
|
||||
let base = match by_style.get(&style) {
|
||||
Some(c) => c.clone(),
|
||||
None => {
|
||||
let c = input_to_cell(&input, sheet, at, &mut cx.wb)?;
|
||||
by_style.insert(style, c.clone());
|
||||
c
|
||||
}
|
||||
};
|
||||
let mut cell = base.unwrap_or_default();
|
||||
if let Some(f) = &cell.formula
|
||||
&& let Some(e) = f.expr()
|
||||
{
|
||||
let shifted = gridcraft_formula::adjust::shift_relative(e, at.row as i64 - origin.row as i64, at.col as i64 - origin.col as i64);
|
||||
cell.formula = Some(std::sync::Arc::new(Formula::from_expr(shifted)));
|
||||
}
|
||||
cell.style = cx.wb.sheet(sheet).and_then(|sh| sh.cell(at)).map(|c| c.style).unwrap_or(cell.style);
|
||||
cx.sheet_mut(sheet)?.set_cell(at, cell);
|
||||
cx.touch(sheet, at);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
//! Ctrl+Enter (`range.fill`) gives every cell what typing the input there gives it: a General
|
||||
//! cell gets the input's automatic number format, a formatted cell keeps its own.
|
||||
|
||||
use gridcraft_engine::Session;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
fn session() -> Session {
|
||||
let mut s = Session::new();
|
||||
s.new_workbook();
|
||||
s
|
||||
}
|
||||
|
||||
fn get(s: &mut Session, a: &str) -> Value {
|
||||
s.execute("cell.get", json!({"cell": a})).unwrap()
|
||||
}
|
||||
|
||||
fn fmt_of(s: &Session, a: &str) -> String {
|
||||
let wb = &s.doc().unwrap().wb;
|
||||
let sh = wb.active().unwrap();
|
||||
wb.styles.get(sh.style_id(gridcraft_engine::core::CellRef::parse(a).unwrap())).num_fmt.as_str().to_string()
|
||||
}
|
||||
|
||||
/// Selects `range` with A1 active and fills it with `input`, as Ctrl+Enter does.
|
||||
fn fill(s: &mut Session, range: &str, input: &str) {
|
||||
s.execute("selection.set", json!({"range": range, "active": "A1"})).unwrap();
|
||||
s.execute("range.fill", json!({"input": input})).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filled_cells_that_already_hold_a_value_get_the_automatic_format() {
|
||||
for input in ["5%", "$5", "3/4/2026", "1:30 PM", "1.5E+3"] {
|
||||
let mut s = session();
|
||||
// What typing the input into a General cell gives.
|
||||
s.execute("cell.set", json!({"cell": "C1", "input": input})).unwrap();
|
||||
let typed = get(&mut s, "C1");
|
||||
// A1 blank, A2 holding a number: both are General.
|
||||
s.execute("cell.set", json!({"cell": "A2", "input": "1"})).unwrap();
|
||||
fill(&mut s, "A1:A2", input);
|
||||
for a in ["A1", "A2"] {
|
||||
let filled = get(&mut s, a);
|
||||
assert_eq!(filled["text"], typed["text"], "{input} in {a}");
|
||||
assert_eq!(filled["value"], typed["value"], "{input} in {a}");
|
||||
assert_eq!(fmt_of(&s, a), fmt_of(&s, "C1"), "{input} in {a}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filled_cells_with_their_own_format_keep_it() {
|
||||
let mut s = session();
|
||||
s.execute("cell.set", json!({"cell": "A2", "input": "1"})).unwrap();
|
||||
s.execute("home.formatCells", json!({"range": "A2", "style": {"num_fmt": "0.000"}})).unwrap();
|
||||
// Column B is formatted but has no cells yet; row 3 is Text.
|
||||
s.execute("home.formatCells", json!({"range": "B:B", "style": {"num_fmt": "0.00"}})).unwrap();
|
||||
s.execute("home.formatCells", json!({"range": "3:3", "style": {"num_fmt": "@"}})).unwrap();
|
||||
fill(&mut s, "A1:B3", "5%");
|
||||
assert_eq!(fmt_of(&s, "A1"), "0%");
|
||||
assert_eq!(get(&mut s, "A1")["text"], "5%");
|
||||
assert_eq!(fmt_of(&s, "A2"), "0.000");
|
||||
assert_eq!(get(&mut s, "A2")["text"], "0.050");
|
||||
assert_eq!(fmt_of(&s, "B1"), "0.00");
|
||||
assert_eq!(get(&mut s, "B1")["text"], "0.05");
|
||||
// A Text cell keeps what was typed, as text.
|
||||
for a in ["A3", "B3"] {
|
||||
assert_eq!(get(&mut s, a)["type"], "text", "{a}");
|
||||
assert_eq!(get(&mut s, a)["text"], "5%", "{a}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filled_formulas_still_shift_and_get_their_date_format() {
|
||||
let mut s = session();
|
||||
s.execute("cell.set", json!({"cell": "A2", "input": "1"})).unwrap();
|
||||
fill(&mut s, "A1:A2", "=DATE(2026,3,ROW())");
|
||||
assert_eq!(get(&mut s, "A2")["formula"], "=DATE(2026,3,ROW())");
|
||||
for (a, text) in [("A1", "3/1/2026"), ("A2", "3/2/2026")] {
|
||||
assert_eq!(fmt_of(&s, a), "m/d/yyyy", "{a}");
|
||||
assert_eq!(get(&mut s, a)["text"], text, "{a}");
|
||||
}
|
||||
fill(&mut s, "A1:A2", "=C1*2");
|
||||
assert_eq!(get(&mut s, "A2")["formula"], "=C2*2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_fill_undoes_in_one_step() {
|
||||
let mut s = session();
|
||||
s.execute("cell.set", json!({"cell": "A2", "input": "1"})).unwrap();
|
||||
fill(&mut s, "A1:A2", "5%");
|
||||
s.execute("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(get(&mut s, "A1")["type"], "empty");
|
||||
assert_eq!(get(&mut s, "A2")["value"].as_f64(), Some(1.0));
|
||||
assert_eq!(fmt_of(&s, "A2"), "General");
|
||||
}
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
# UI parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (horizontal scrollbar in the sheet tab bar, #840) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-19 · **Change:** minor (Ctrl+Enter fill gives each cell the automatic number format typing gives it) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
|
||||
|
||||
Interaction, controls and feel, against Excel. Part of [target-app-parity.md](target-app-parity.md).
|
||||
**UI/UX fidelity: ~55% ready, 50–80 h** (estimated). Ribbon command coverage is measured
|
||||
@@ -161,6 +161,7 @@ scrollbar bugs 2–4 · pixel/feel pass against observed Excel 7–12.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-19 | minor | Ctrl+Enter (`range.fill`) gives each cell what typing the input there gives: a General cell gets the automatic number format (`5%`, `$5`, `3/4/2026`, `=TODAY()`) even when it already held a value, and a cell with its own format, a formatted row or column, or Text keeps it. Cells that held a value used to keep General (`5%` showed 0.05), and empty cells took the active cell's format |
|
||||
| 2026-10-12 | minor | The File view's recent list tells when each workbook was last changed, read off the UI thread |
|
||||
| 2026-10-13 | minor | Horizontal scrollbar in the sheet tab bar: drag, track click pages, frozen columns excluded from the page (#840) |
|
||||
| 2026-10-12 | minor | Ctrl+wheel zoom steps along a ladder of percentages about 20% apart that never skips 100%, gliding to each step with animations on, so a notch out undoes a notch in (it drifted: 100 → 122 → 101); Excel steps 15 points, see decisions.md |
|
||||
|
||||
Reference in New Issue
Block a user