#3136: ask before a drag and drop replaces data

Dragging the selection's border onto cells that hold data replaced them
without asking, so dragging column B onto column A silently destroyed A.
Excel asks "There's already data here. Do you want to replace it?" first
(its "Alert before overwriting cells" option, on by default).

The drop now runs a new `range.move {range?, to, overwrite?}` command:
a cut and paste that returns a confirm question when the destination
holds data other than the moved cells; the grid shows it with OK and
Cancel, and OK runs it again with `overwrite: true`. A refused move
leaves the sheet and the clipboard as they were.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hector Villarreal
2026-11-02 23:43:02 -06:00
co-authored by Claude Opus 5.5
parent 178f3eb745
commit 5361e2c08a
6 changed files with 98 additions and 6 deletions
+28 -1
View File
@@ -1,7 +1,8 @@
use super::cell::input_to_cell;
use super::paste_special::paste_special;
use super::{
Cell, Clipboard, EngineError, Json, RangeRef, Result, Session, Value, cell_param_on, edit, json, ok, str_param, target_ranges, target_sheet,
Cell, Clipboard, EngineError, Json, RangeRef, Result, Session, Value, bad, bool_param, cell_param_on, edit, json, ok, str_param, target_ranges,
target_sheet,
};
// ---------------------------------------------------------------- clipboard
@@ -163,6 +164,32 @@ pub(super) fn paste(s: &mut Session, p: &Json) -> Result<Json> {
paste_special(s, &json!({"what": "all", "at": p.get("at").cloned().unwrap_or(Json::Null)}))
}
/// Drag and drop (dragging the selection's border): moves `range` so its top-left cell lands on
/// `to`, as Cut and Paste would. Like Excel, it asks before replacing cells that hold data (other
/// than the moved cells themselves); run it again with `overwrite: true` to replace them. A
/// refused or failed move leaves the clipboard as it was.
pub(super) fn range_move(s: &mut Session, p: &Json) -> Result<Json> {
let sheet = target_sheet(s, p)?;
let to = cell_param_on(s, p, "to", sheet)?.ok_or_else(|| bad("range.move", "missing `to`"))?;
let kept = s.clipboard.take();
let moved = cut(s, p).and_then(|_| {
let src = s.clipboard.as_ref().map(|c| c.range).unwrap_or_default();
let dest = to.offset(i64::from(src.height()) - 1, i64::from(src.width()) - 1).map(|end| RangeRef::new(to, end));
if bool_param(p, "overwrite") != Some(true)
&& let Some(dest) = dest
&& let Some(sh) = s.doc()?.wb.sheet(sheet)
&& sh.cells.iter_range(dest).any(|(c, cell)| !src.contains(c) && cell.has_content())
{
return Err(EngineError::Confirm("There's already data here. Do you want to replace it?".into()));
}
paste(s, &json!({"at": to.a1()}))
});
if moved.is_err() {
s.clipboard = kept;
}
moved
}
fn paste_text(s: &mut Session, p: &Json, text: &str) -> Result<Json> {
let sheet = target_sheet(s, p)?;
let at = cell_param_on(s, p, "at", sheet)?.unwrap_or(s.doc()?.selection.current().start);
+28
View File
@@ -353,6 +353,34 @@ fn cut_paste_that_overflows_the_grid_moves_nothing() {
assert_eq!(get(&mut s, "XFD2")["value"], json!(2.0));
}
#[test]
fn drag_and_drop_asks_before_replacing_data() {
// #3136: dragging column B's border onto column A replaced A without asking.
let mut s = Session::new();
s.new_workbook();
s.execute("range.setValues", json!({"range": "A1:B3", "values": [["Apple", "Kiwi"], ["Pear", "Plum"], ["Fig", "Lime"]]})).unwrap();
s.execute("edit.copy", json!({"range": "D9"})).unwrap();
let text = |s: &mut Session, a: &str| s.execute("cell.get", json!({"cell": a})).unwrap()["input"].clone();
let r = s.execute("range.move", json!({"range": "B:B", "to": "A1"}));
let Err(EngineError::Confirm(q)) = r else { panic!("{r:?}") };
assert_eq!(q, "There's already data here. Do you want to replace it?");
// Nothing moved, and the copy made before is still on the clipboard.
assert_eq!(text(&mut s, "A1"), json!("Apple"));
assert_eq!(text(&mut s, "B1"), json!("Kiwi"));
assert!(s.clipboard.as_ref().is_some_and(|c| !c.cut && c.range.a1() == "D9"));
// Confirmed, column B replaces column A.
s.execute("range.move", json!({"range": "B:B", "to": "A1", "overwrite": true})).unwrap();
assert_eq!(text(&mut s, "A1"), json!("Kiwi"));
assert_eq!(text(&mut s, "A3"), json!("Lime"));
assert_eq!(text(&mut s, "B1"), json!(""));
// Onto empty cells, or overlapping only the moved cells themselves, nothing is asked.
s.execute("range.move", json!({"range": "A1:A3", "to": "A2"})).unwrap();
assert_eq!(text(&mut s, "A4"), json!("Lime"));
s.execute("range.move", json!({"range": "A2:A4", "to": "C2"})).unwrap();
assert_eq!(text(&mut s, "C2"), json!("Kiwi"));
assert!(s.execute("range.move", json!({"range": "C2"})).is_err(), "`to` is required");
}
#[test]
fn replace_case_insensitive() {
assert_eq!(replace_ci("Apple apple APPLE", "apple", "pear"), "pear pear pear");