mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-31 06:06:48 -06:00
Fill handle: Ctrl+drag swaps copying and extending a series (#264)
A plain drag copies a lone number and extends every other series; Ctrl+drag (Cmd on macOS) now does the opposite, as in Excel: a lone number counts up by 1, and numbers, dates, lists and `Item 1`-style text are copied. edit.autoFill gains mode "alternate" for this. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
9f896c1249
commit
1b9751ed82
@@ -79,7 +79,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"AutoFill",
|
||||
[],
|
||||
None,
|
||||
"{source: \"A1:A2\", target: \"A1:A10\", mode?: series|copy|formats|values}: the fill-handle drag",
|
||||
"{source: \"A1:A2\", target: \"A1:A10\", mode?: series|alternate|copy|formats|values}: the fill-handle drag (alternate = Ctrl+drag: a lone number counts up, any other series copies)",
|
||||
has_doc,
|
||||
auto_fill
|
||||
),
|
||||
@@ -1139,6 +1139,7 @@ fn auto_fill(s: &mut Session, p: &Json) -> Result<Json> {
|
||||
let Some(target) = str_param(p, "target").and_then(RangeRef::parse) else { return Err(bad("edit.autoFill", "missing `target`")) };
|
||||
let mode = match str_param(p, "mode").unwrap_or("series") {
|
||||
"copy" => crate::fill::FillMode::Copy,
|
||||
"alternate" => crate::fill::FillMode::Alternate,
|
||||
"formats" => crate::fill::FillMode::Formats,
|
||||
"values" => crate::fill::FillMode::ValuesOnly,
|
||||
_ => crate::fill::FillMode::Series,
|
||||
|
||||
@@ -13,6 +13,9 @@ use crate::cmd::Ctx;
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum FillMode {
|
||||
Series,
|
||||
/// The Ctrl+drag fill: the opposite of a plain drag. A lone number fills a
|
||||
/// series stepping by 1; anything a plain drag would extend is copied.
|
||||
Alternate,
|
||||
Copy,
|
||||
Formats,
|
||||
ValuesOnly,
|
||||
@@ -62,13 +65,14 @@ pub fn fill(cx: &mut Ctx, sheet: usize, src: RangeRef, target: RangeRef, mode: F
|
||||
if forward { (src.end.col + 1..=target.end.col).collect() } else { (target.start.col..src.start.col).rev().collect() };
|
||||
cols.into_iter().map(|c| CellRef::new(lane, c)).collect()
|
||||
};
|
||||
let pattern = if mode == FillMode::Series {
|
||||
Pattern::detect(
|
||||
let pattern = if matches!(mode, FillMode::Series | FillMode::Alternate) {
|
||||
let detected = Pattern::detect(
|
||||
&src_cells,
|
||||
&custom,
|
||||
cx.wb.date_system,
|
||||
cx.wb.styles.get(src_cells.first().and_then(|c| c.1.as_ref()).map(|c| c.style).unwrap_or_default()).num_fmt.as_str(),
|
||||
)
|
||||
);
|
||||
if mode == FillMode::Alternate { detected.alternate(&src_cells) } else { detected }
|
||||
} else {
|
||||
Pattern::Copy
|
||||
};
|
||||
@@ -84,7 +88,7 @@ pub fn fill(cx: &mut Ctx, sheet: usize, src: RangeRef, target: RangeRef, mode: F
|
||||
writes.push((*t, Some(existing)));
|
||||
continue;
|
||||
}
|
||||
FillMode::ValuesOnly | FillMode::Series | FillMode::Copy => {
|
||||
FillMode::ValuesOnly | FillMode::Series | FillMode::Alternate | FillMode::Copy => {
|
||||
let style = if mode == FillMode::ValuesOnly { sh.cell(*t).map(|c| c.style).unwrap_or_default() } else { cell.style };
|
||||
if let Some(f) = &cell.formula {
|
||||
if let Some(e) = f.expr() {
|
||||
@@ -231,6 +235,19 @@ impl Pattern {
|
||||
Pattern::Copy
|
||||
}
|
||||
|
||||
/// The pattern of a Ctrl+drag, which swaps copying and extending: a single
|
||||
/// number (which a plain drag copies) counts up by 1, and every series a
|
||||
/// plain drag would extend (numbers, dates, lists, `Item 1`) is copied.
|
||||
fn alternate(self, cells: &[(CellRef, Option<Cell>)]) -> Pattern {
|
||||
match (self, cells) {
|
||||
(Pattern::Copy, [(_, Some(cell))]) if cell.formula.is_none() => match cell.value.as_f64() {
|
||||
Some(n) if cell.value.is_number() => Pattern::Linear { last: n, first: n, step: 1.0 },
|
||||
_ => Pattern::Copy,
|
||||
},
|
||||
_ => Pattern::Copy,
|
||||
}
|
||||
}
|
||||
|
||||
/// The value `k` steps beyond the source (forward) or before it (backward).
|
||||
fn value(&self, k: i64, forward: bool, _len: usize) -> Option<Value> {
|
||||
let sign = if forward { 1 } else { -1 };
|
||||
|
||||
@@ -452,6 +452,39 @@ fn fill_series() {
|
||||
assert_eq!(v(&s, "C2"), Value::from("Item 10"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctrl_drag_fill_swaps_copy_and_series() {
|
||||
// Issue #264: Ctrl+drag fills a lone number as a series, and copies what a plain drag extends.
|
||||
let mut s = s();
|
||||
s.execute("cell.set", json!({"cell": "A1", "input": "7"})).unwrap();
|
||||
s.execute("edit.autoFill", json!({"source": "A1", "target": "A1:A4"})).unwrap();
|
||||
assert_eq!(v(&s, "A4"), Value::Number(7.0), "a plain drag copies a lone number");
|
||||
s.execute("edit.autoFill", json!({"source": "A1", "target": "A1:A4", "mode": "alternate"})).unwrap();
|
||||
assert_eq!(v(&s, "A2"), Value::Number(8.0));
|
||||
assert_eq!(v(&s, "A4"), Value::Number(10.0));
|
||||
// Upward too.
|
||||
s.execute("cell.set", json!({"cell": "B5", "input": "7"})).unwrap();
|
||||
s.execute("edit.autoFill", json!({"source": "B5", "target": "B3:B5", "mode": "alternate"})).unwrap();
|
||||
assert_eq!(v(&s, "B3"), Value::Number(5.0));
|
||||
// A two-number series, a list entry and text with a number are copied.
|
||||
s.execute("range.setValues", json!({"range": "C1", "values": [[1], [3]]})).unwrap();
|
||||
s.execute("edit.autoFill", json!({"source": "C1:C2", "target": "C1:C4", "mode": "alternate"})).unwrap();
|
||||
assert_eq!(v(&s, "C3"), Value::Number(1.0));
|
||||
assert_eq!(v(&s, "C4"), Value::Number(3.0));
|
||||
s.execute("cell.set", json!({"cell": "D1", "input": "Mon"})).unwrap();
|
||||
s.execute("edit.autoFill", json!({"source": "D1", "target": "D1:D3", "mode": "alternate"})).unwrap();
|
||||
assert_eq!(v(&s, "D3"), Value::from("Mon"));
|
||||
s.execute("cell.set", json!({"cell": "E1", "input": "Item 9"})).unwrap();
|
||||
s.execute("edit.autoFill", json!({"source": "E1", "target": "E1:E2", "mode": "alternate"})).unwrap();
|
||||
assert_eq!(v(&s, "E2"), Value::from("Item 9"));
|
||||
// Plain text copies either way; an empty source clears.
|
||||
s.execute("cell.set", json!({"cell": "F1", "input": "abc"})).unwrap();
|
||||
s.execute("edit.autoFill", json!({"source": "F1", "target": "F1:F2", "mode": "alternate"})).unwrap();
|
||||
assert_eq!(v(&s, "F2"), Value::from("abc"));
|
||||
s.execute("edit.autoFill", json!({"source": "G1", "target": "G1:G3", "mode": "alternate"})).unwrap();
|
||||
assert_eq!(v(&s, "G3"), Value::Empty);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_delete_cells_adjust() {
|
||||
let formula = |s: &mut Session, a: &str| s.execute("cell.get", json!({"cell": a})).unwrap()["formula"].clone();
|
||||
|
||||
Reference in New Issue
Block a user