1#![deny(missing_docs)]
2
3use std::{
4 collections::{HashMap, HashSet},
5 io::Cursor,
6};
7
8use csv::{ReaderBuilder, WriterBuilder};
9use serde::{Deserialize, Serialize};
10
11use crate::{
12 cf_types::ConditionalFormatting,
13 expressions::types::{Area, CellReferenceIndex},
14 model::CellStructure,
15 types::{ArrayKind, Cell, Link, Style},
16 UserModel,
17};
18
19use crate::user_model::history::Diff;
20
21pub type ClipboardData = HashMap<i32, HashMap<i32, ClipboardCell>>;
23
24pub type ClipboardTuple = (i32, i32, i32, i32);
25
26#[derive(Serialize, Deserialize)]
27pub struct ClipboardCell {
28 text: String,
29 is_spill: bool,
30 style: Style,
31 #[serde(default)]
34 link: Option<Link>,
35}
36
37#[derive(Serialize, Deserialize)]
38pub struct Clipboard {
39 pub(crate) csv: String,
40 pub(crate) data: ClipboardData,
41 pub(crate) sheet: u32,
42 pub(crate) range: (i32, i32, i32, i32),
43}
44
45impl<'a> UserModel<'a> {
46 pub fn copy_to_clipboard(&self) -> Result<Clipboard, String> {
48 let selected_area = self.get_selected_view();
49 let sheet = selected_area.sheet;
50 let mut wtr = WriterBuilder::new().delimiter(b'\t').from_writer(vec![]);
51
52 let mut data = HashMap::new();
53 let [row_start, column_start, row_end, column_end] = selected_area.range;
54 let dimension = self.model.workbook.worksheet(sheet)?.dimension();
55 let row_end = row_end.min(dimension.max_row).max(row_start);
56 let column_end = column_end.min(dimension.max_column).max(column_start);
57 for row in row_start..=row_end {
58 let mut data_row = HashMap::new();
59 let mut text_row = Vec::new();
60 for column in column_start..=column_end {
61 let text = self.get_formatted_cell_value(sheet, row, column)?;
62 let content = self.get_cell_content(sheet, row, column)?;
63 let style = self.model.get_style_for_cell(sheet, row, column)?;
64 let is_spill = matches!(
65 self.model.get_cell_structure(sheet, row, column)?,
66 CellStructure::SpillArray { .. } | CellStructure::SpillDynamic { .. }
67 );
68 let link = self.model.get_cell_link(sheet, row, column)?;
69 data_row.insert(
70 column,
71 ClipboardCell {
72 text: content,
73 is_spill,
74 style,
75 link,
76 },
77 );
78 text_row.push(text);
79 }
80 wtr.write_record(text_row)
81 .map_err(|e| format!("Error while processing csv: {e}"))?;
82 data.insert(row, data_row);
83 }
84
85 let csv = String::from_utf8(
86 wtr.into_inner()
87 .map_err(|e| format!("Processing error: '{e}'"))?,
88 )
89 .map_err(|e| format!("Error converting from utf8: '{e}'"))?;
90
91 Ok(Clipboard {
92 csv: csv.trim().to_string(),
93 data,
94 sheet,
95 range: (row_start, column_start, row_end, column_end),
96 })
97 }
98
99 pub fn paste_from_clipboard(
101 &mut self,
102 source_sheet: u32,
103 source_range: ClipboardTuple,
104 clipboard: &ClipboardData,
105 is_cut: bool,
106 ) -> Result<(), String> {
107 let mut diff_list = Vec::new();
108 let view = self.get_selected_view();
109 let (source_first_row, source_first_column, source_last_row, source_last_column) =
110 source_range;
111 let sheet = view.sheet;
112 let [selected_row, selected_column, _, _] = view.range;
113 let mut max_row = selected_row;
114 let mut max_column = selected_column;
115 let area = &Area {
116 sheet,
117 row: source_first_row,
118 column: source_first_column,
119 width: source_last_column - source_first_column + 1,
120 height: source_last_row - source_first_row + 1,
121 };
122 let target_area = &Area {
123 sheet,
124 row: selected_row,
125 column: selected_column,
126 width: source_last_column - source_first_column + 1,
127 height: source_last_row - source_first_row + 1,
128 };
129
130 let mut seen_cells = HashSet::new();
131 let mut changes = Vec::new();
133 for (source_row, data_row) in clipboard {
134 let delta_row = source_row - source_first_row;
135 let target_row = selected_row + delta_row;
136 max_row = max_row.max(target_row);
137 for (source_column, value) in data_row {
138 let delta_column = source_column - source_first_column;
139 let target_column = selected_column + delta_column;
140 max_column = max_column.max(target_column);
141
142 if value.is_spill {
143 let old_style =
145 self.model
146 .get_cell_style_or_none(sheet, target_row, target_column)?;
147 changes.push((
148 target_row,
149 target_column,
150 None,
151 old_style,
152 None,
153 value.style.clone(),
154 ));
155 seen_cells.insert((target_row, target_column));
156 continue;
157 }
158
159 let source = &CellReferenceIndex {
165 sheet,
166 column: *source_column,
167 row: *source_row,
168 };
169 let target = &CellReferenceIndex {
170 sheet,
171 column: target_column,
172 row: target_row,
173 };
174 let new_value = if is_cut {
175 self.model
176 .move_cell_value_to_area(&value.text, source, target, area)?
177 } else {
178 self.model
179 .extend_copied_value(&value.text, source, target)?
180 };
181
182 let old_value = self
183 .model
184 .workbook
185 .worksheet(sheet)?
186 .cell(target_row, target_column)
187 .cloned();
188
189 let old_style =
190 self.model
191 .get_cell_style_or_none(sheet, target_row, target_column)?;
192 changes.push((
193 target_row,
194 target_column,
195 old_value.clone(),
196 old_style.clone(),
197 Some(new_value.clone()),
198 value.style.clone(),
199 ));
200 seen_cells.insert((target_row, target_column));
201 }
202 }
203 diff_list.extend(self.range_link_diffs(target_area)?);
205 self.model.range_clear_contents(target_area)?;
207 for (target_row, target_column, old_value, old_style, new_value, style) in changes {
209 if let Some(ref v) = new_value {
210 self.model
211 .set_user_input(sheet, target_row, target_column, v.clone())?;
212 diff_list.push(Diff::SetCellValue {
213 sheet,
214 row: target_row,
215 column: target_column,
216 new_value: v.clone(),
217 old_value: Box::new(old_value),
218 });
219 }
220 self.model
221 .set_cell_style(sheet, target_row, target_column, &style)?;
222
223 diff_list.push(Diff::SetCellStyle {
224 sheet,
225 row: target_row,
226 column: target_column,
227 old_value: Box::new(old_style),
228 new_value: Box::new(style),
229 });
230 }
231 for (source_row, data_row) in clipboard {
234 let target_row = selected_row + (source_row - source_first_row);
235 for (source_column, value) in data_row {
236 let target_column = selected_column + (source_column - source_first_column);
237 let old_link = self.model.get_cell_link(sheet, target_row, target_column)?;
238 if old_link == value.link {
239 continue;
240 }
241 match &value.link {
242 Some(link) => {
243 self.model
244 .set_cell_link(sheet, target_row, target_column, link.clone())?
245 }
246 None => self
247 .model
248 .delete_cell_link(sheet, target_row, target_column)?,
249 }
250 diff_list.push(Diff::SetCellLink {
251 sheet,
252 row: target_row,
253 column: target_column,
254 old_value: Box::new(old_link),
255 new_value: Box::new(value.link.clone()),
256 });
257 }
258 }
259 if is_cut {
260 for row in source_first_row..=source_last_row {
261 for column in source_first_column..=source_last_column {
262 if (source_sheet == sheet) && seen_cells.contains(&(row, column)) {
263 continue;
264 }
265 let old_value = self
266 .model
267 .workbook
268 .worksheet(source_sheet)?
269 .cell(row, column)
270 .cloned();
271
272 diff_list.push(Diff::RangeClearContents {
273 sheet: source_sheet,
274 row,
275 column,
276 width: 1,
277 height: 1,
278 old_value: vec![vec![old_value.clone()]],
279 });
280
281 let old_link = self.model.get_cell_link(source_sheet, row, column)?;
283 if let Some(old_link) = old_link {
284 self.model.delete_cell_link(source_sheet, row, column)?;
285 diff_list.push(Diff::SetCellLink {
286 sheet: source_sheet,
287 row,
288 column,
289 old_value: Box::new(Some(old_link)),
290 new_value: Box::new(None),
291 });
292 }
293
294 let spill_dims = match &old_value {
299 Some(Cell::ArrayFormula {
300 kind: ArrayKind::Dynamic,
301 r,
302 ..
303 }) => Some(*r),
304 _ => None,
305 };
306 if let Some((spill_w, spill_h)) = spill_dims {
307 let ws = self.model.workbook.worksheet_mut(source_sheet)?;
308 for sr in row..row + spill_h {
309 for sc in column..column + spill_w {
310 if (source_sheet == sheet) && seen_cells.contains(&(sr, sc)) {
311 continue;
312 }
313 let _ = ws.cell_clear_contents(sr, sc);
314 }
315 }
316 } else {
317 let area = Area {
318 sheet: source_sheet,
319 row,
320 column,
321 width: 1,
322 height: 1,
323 };
324 self.model.range_clear_contents(&area)?;
325 }
326 let old_style = self
327 .model
328 .get_cell_style_or_none(source_sheet, row, column)?;
329 let default_style = Style::default();
330 self.model
331 .set_cell_style(source_sheet, row, column, &default_style)?;
332 diff_list.push(Diff::SetCellStyle {
333 sheet: source_sheet,
334 row,
335 column,
336 old_value: Box::new(old_style),
337 new_value: Box::new(default_style),
338 });
339 }
340 }
341 let ext_area = Area {
344 sheet: source_sheet,
345 row: source_first_row,
346 column: source_first_column,
347 width: source_last_column - source_first_column + 1,
348 height: source_last_row - source_first_row + 1,
349 };
350 let ext_updates = self.model.get_external_formula_updates_for_cut(
351 &ext_area,
352 selected_row,
353 selected_column,
354 )?;
355 for (ext_sheet, ext_row, ext_col, new_formula) in ext_updates {
356 let old_cell = self
357 .model
358 .workbook
359 .worksheet(ext_sheet)?
360 .cell(ext_row, ext_col)
361 .cloned();
362 self.model
363 .set_user_input(ext_sheet, ext_row, ext_col, new_formula.clone())?;
364 diff_list.push(Diff::SetCellValue {
365 sheet: ext_sheet,
366 row: ext_row,
367 column: ext_col,
368 new_value: new_formula,
369 old_value: Box::new(old_cell),
370 });
371 }
372 let dn_updates = self.model.get_defined_name_updates_for_cut(
374 &ext_area,
375 selected_row,
376 selected_column,
377 );
378 for (dn_name, dn_scope, old_formula, new_formula) in dn_updates {
379 diff_list.push(Diff::UpdateDefinedName {
380 name: dn_name.clone(),
381 scope: dn_scope,
382 old_formula: old_formula.clone(),
383 new_name: dn_name.clone(),
384 new_scope: dn_scope,
385 new_formula: new_formula.clone(),
386 });
387 self.model.update_defined_name(
388 &dn_name,
389 dn_scope,
390 &dn_name,
391 dn_scope,
392 &new_formula,
393 )?;
394 }
395 let cf_updates = self.model.get_conditional_formatting_updates_for_cut(
397 &ext_area,
398 selected_row,
399 selected_column,
400 )?;
401 for (cf_sheet, cf_idx, new_range, new_rule) in cf_updates {
402 let old_cf = self
403 .model
404 .workbook
405 .worksheet(cf_sheet)?
406 .conditional_formatting
407 .get(cf_idx)
408 .ok_or_else(|| format!("CF index {cf_idx} not found"))?
409 .clone();
410 {
411 let ws = self.model.workbook.worksheet_mut(cf_sheet)?;
412 ws.conditional_formatting[cf_idx].range = new_range.clone();
413 ws.conditional_formatting[cf_idx].cf_rule = new_rule.clone();
414 }
415 diff_list.push(Diff::UpdateConditionalFormatting {
416 sheet: cf_sheet,
417 index: cf_idx as u32,
418 old_range: old_cf.range,
419 old_rule: Box::new(old_cf.cf_rule),
420 old_priority: old_cf.priority,
421 new_range,
422 new_rule: Box::new(new_rule),
423 });
424 }
425 } else {
426 let cf_copies = self.model.get_cf_rules_to_copy(
428 source_sheet,
429 source_first_row,
430 source_first_column,
431 source_last_row,
432 source_last_column,
433 selected_row,
434 selected_column,
435 );
436 for (new_range, new_rule) in cf_copies {
437 let priority = self
438 .model
439 .workbook
440 .worksheet(sheet)?
441 .conditional_formatting
442 .iter()
443 .map(|cf| cf.priority)
444 .max()
445 .map(|m| m + 1)
446 .unwrap_or(1);
447 self.model
448 .workbook
449 .worksheet_mut(sheet)?
450 .conditional_formatting
451 .push(ConditionalFormatting {
452 range: new_range.clone(),
453 cf_rule: new_rule.clone(),
454 priority,
455 });
456 diff_list.push(Diff::AddConditionalFormatting {
457 sheet,
458 range: new_range,
459 rule: Box::new(new_rule),
460 priority,
461 });
462 }
463 }
464 self.push_diff_list(diff_list);
465 self.set_selected_range(selected_row, selected_column, max_row, max_column)?;
467 self.evaluate_if_not_paused();
468 Ok(())
469 }
470
471 pub fn paste_csv_string(&mut self, area: &Area, csv: &str) -> Result<(), String> {
473 let sheet = area.sheet;
474
475 let mut records: Vec<Vec<String>> = Vec::new();
477 let mut max_width: i32 = 0;
478 let csv_reader = Cursor::new(csv);
479 let mut reader = ReaderBuilder::new()
480 .delimiter(b'\t')
481 .has_headers(false)
482 .from_reader(csv_reader);
483 for r in reader.records().flatten() {
484 let row_data: Vec<String> = r.iter().map(|v| v.to_string()).collect();
485 max_width = max_width.max(row_data.len() as i32);
486 records.push(row_data);
487 }
488 if records.is_empty() {
489 return Ok(());
490 }
491
492 let paste_area = Area {
494 sheet,
495 row: area.row,
496 column: area.column,
497 width: max_width,
498 height: records.len() as i32,
499 };
500
501 let mut old_values: HashMap<(i32, i32), Option<Cell>> = HashMap::new();
503 {
504 let ws = self.model.workbook.worksheet(sheet)?;
505 for r in area.row..area.row + records.len() as i32 {
506 for c in area.column..area.column + max_width {
507 old_values.insert((r, c), ws.cell(r, c).cloned());
508 }
509 }
510 }
511
512 let mut diff_list = self.range_link_diffs(&paste_area)?;
514 self.model.range_clear_contents(&paste_area)?;
515
516 let mut row = area.row;
518 let mut last_column = area.column;
519 for row_data in &records {
520 let mut column = area.column;
521 for value in row_data {
522 let old_value = old_values.remove(&(row, column)).unwrap_or(None);
523 diff_list.push(Diff::SetCellValue {
524 sheet,
525 row,
526 column,
527 new_value: value.to_string(),
528 old_value: Box::new(old_value),
529 });
530 self.set_user_input_with_link_diffs(
532 sheet,
533 row,
534 column,
535 value.to_string(),
536 &mut diff_list,
537 )?;
538 column += 1;
539 }
540 last_column = last_column.max(column - 1);
541 row += 1;
542 }
543 self.push_diff_list(diff_list);
544 self.set_selected_range(area.row, area.column, row - 1, last_column)?;
546 self.evaluate_if_not_paused();
547 Ok(())
548 }
549}