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