Skip to main content

visi_core/core/engine/sheet/
edit.rs

1use super::super::column::{ColumnPosition, DataColumn};
2use super::{CellRef, CellType, Direction, ResultData, Sheet, TextCellRef};
3
4/// The word surrounding `char_offset` in `text`. The idea
5/// is to implement "highlight word on double click".
6pub fn get_word_boundaries_from_str(text: &str, char_offset: usize) -> (usize, usize) {
7    if text.is_empty() {
8        return (0, 0);
9    }
10
11    let chars: Vec<char> = text.chars().collect();
12    let len = chars.len();
13    let offset = char_offset.min(len);
14
15    let is_word_char = |c: char| c.is_alphanumeric() || c == '_';
16
17    let (on_idx, on_c) = if offset < len {
18        if chars[offset].is_whitespace() && offset > 0 && is_word_char(chars[offset - 1]) {
19            (offset - 1, chars[offset - 1])
20        } else {
21            (offset, chars[offset])
22        }
23    } else if offset > 0 {
24        (offset - 1, chars[offset - 1])
25    } else {
26        return (0, 0);
27    };
28
29    if on_c.is_whitespace() {
30        let mut start = on_idx;
31        while start > 0 && chars[start - 1].is_whitespace() {
32            start -= 1;
33        }
34        let mut end = on_idx + 1;
35        while end < len && chars[end].is_whitespace() {
36            end += 1;
37        }
38        return (start, end);
39    }
40
41    if is_word_char(on_c) {
42        let mut start = on_idx;
43        while start > 0 && is_word_char(chars[start - 1]) {
44            start -= 1;
45        }
46        let mut end = on_idx + 1;
47        while end < len && is_word_char(chars[end]) {
48            end += 1;
49        }
50        return (start, end);
51    }
52
53    let mut start = on_idx;
54    while start > 0 && !is_word_char(chars[start - 1]) && !chars[start - 1].is_whitespace() {
55        start -= 1;
56    }
57    let mut end = on_idx + 1;
58    while end < len && !is_word_char(chars[end]) && !chars[end].is_whitespace() {
59        end += 1;
60    }
61    (start, end)
62}
63impl Sheet {
64    /// The computed value of a cell, or [`ResultData::None`] if it is empty
65    /// or outside the sheet's allocated grid.
66    pub fn get_result_data(&self, cell: &CellRef) -> ResultData {
67        let col = self.columns.get(cell.col);
68        if let Some(col) = col {
69            col.data.get(cell.row).unwrap_or(ResultData::None)
70        } else {
71            ResultData::None
72        }
73    }
74
75    /// The date format a formula should inherit from the cells it reads, if
76    /// any -- Excel's "date plus a number is still a date" behavior.
77    pub(super) fn inherited_date_format(&self, ast: &crate::core::parser::Expr) -> Option<String> {
78        use crate::core::parser::{Expr, Op};
79        match ast {
80            Expr::CellRef {
81                sheet, row, col, ..
82            } if sheet.is_none() => self
83                .get_cell_style(*row, *col)
84                .and_then(|s| s.num_format.clone())
85                .filter(|code| crate::core::date::is_date_code(code)),
86            Expr::BinaryOp {
87                op: Op::Add | Op::Sub,
88                left,
89                right,
90            } => {
91                let left_fmt = self.inherited_date_format(left);
92                let right_fmt = self.inherited_date_format(right);
93                match (left_fmt, right_fmt) {
94                    (Some(fmt), None) | (None, Some(fmt)) => Some(fmt),
95                    _ => None,
96                }
97            }
98            _ => None,
99        }
100    }
101
102    /// Interpolate the cell's value into a string, with
103    /// a specified cell format
104    pub fn get_display_string(&self, cell: &CellRef) -> String {
105        let value = self.get_result_data(cell);
106        let Some(code) = self
107            .get_cell_style(cell.row, cell.col)
108            .and_then(|s| s.num_format.as_deref())
109        else {
110            return value.to_string();
111        };
112        match value {
113            ResultData::Float(f) => crate::core::text::format_number_format(f, code)
114                .unwrap_or_else(|_| ResultData::Float(f).to_string()),
115            ResultData::Integer(i) => crate::core::text::format_number_format(i as f64, code)
116                .unwrap_or_else(|_| ResultData::Integer(i).to_string()),
117            ResultData::String(s) => crate::core::text::format_text_format(&s, code)
118                .unwrap_or(ResultData::String(s).to_string()),
119            other => other.to_string(),
120        }
121    }
122
123    /// Returns the intrinsic data type of a cell.
124    pub fn get_cell_type(&self, cell: &CellRef) -> CellType {
125        let col = self.columns.get(cell.col);
126        if let Some(col) = col {
127            col.cell_types
128                .get(cell.row)
129                .copied()
130                .unwrap_or(CellType::Empty)
131        } else {
132            CellType::Empty
133        }
134    }
135
136    /// Sets the intrinsic data type of a cell at (row, col).
137    pub fn set_cell_type(&mut self, row: usize, col: usize, cell_type: CellType) {
138        if let Some(column) = self.columns.get_mut(col)
139            && row < column.cell_types.len()
140        {
141            column.cell_types[row] = cell_type;
142            column.mark_dirty(row);
143        }
144    }
145
146    /// Sets the source text and explicit cell type of a particular cell.
147    pub fn set_cell_with_type(&mut self, row: usize, col: usize, src: String, cell_type: CellType) {
148        let table_clone = self.clone();
149        if let Some(column) = self.columns.get_mut(col)
150            && row < column.src.len()
151        {
152            column.src[row] = src.clone();
153            column.cell_types[row] = cell_type;
154            let compiled = crate::core::parser::compile_formula(&src, &[table_clone]);
155            column.compiled_src[row] = compiled;
156            column.mark_dirty(row);
157
158            self.uncommitted_actions
159                .push(crate::core::SheetAction::SetCellSrc {
160                    sheet_name: self.name.clone(),
161                    col,
162                    row,
163                    src,
164                });
165        }
166    }
167
168    /// Directly sets the src of a cell and marks it dirty.
169    /// Does not automatically evaluate it.
170    /// Call [`Sheet::commit`] to evaluate
171    /// updated cells.
172    pub fn set_cell_src(&mut self, row: usize, col: usize, src: String) {
173        let table_clone = self.clone();
174        if let Some(column) = self.columns.get_mut(col)
175            && row < column.src.len()
176        {
177            column.src[row] = src.clone();
178            column.cell_types[row] = CellType::Empty;
179            let compiled = crate::core::parser::compile_formula(&src, &[table_clone]);
180            column.compiled_src[row] = compiled;
181            column.mark_dirty(row);
182
183            self.uncommitted_actions
184                .push(crate::core::SheetAction::SetCellSrc {
185                    sheet_name: self.name.clone(),
186                    col,
187                    row,
188                    src,
189                });
190        }
191    }
192
193    /// Inserts text into a cell's source at a character offset, as typing
194    /// into it would, then recompiles and marks it dirty.
195    pub fn insert(&mut self, pos: TextCellRef, input: &str) {
196        let TextCellRef {
197            row,
198            col,
199            char_offset,
200        } = pos;
201        let table_clone = self.clone();
202        let existing_col = self.columns.get_mut(col);
203        match existing_col {
204            Some(existing_column) => {
205                existing_column.insert(ColumnPosition { row, char_offset }, input);
206                let src = existing_column.src[row].clone();
207                let compiled = crate::core::parser::compile_formula(&src, &[table_clone]);
208                existing_column.compiled_src[row] = compiled;
209                existing_column.mark_dirty(row);
210                self.uncommitted_actions
211                    .push(crate::core::SheetAction::SetCellSrc {
212                        sheet_name: self.name.clone(),
213                        col,
214                        row,
215                        src,
216                    });
217            }
218            None => {
219                println!("Warning: column {} does not exist", col)
220            }
221        }
222    }
223
224    /// Delete one before (like backspace)
225    pub fn delete_one_before(&mut self, pos: TextCellRef) {
226        let char_offset = pos.char_offset;
227        let start = if char_offset > 0 {
228            TextCellRef {
229                row: pos.row,
230                col: pos.col,
231                char_offset: char_offset - 1,
232            }
233        } else {
234            pos.clone()
235        };
236        let end = pos;
237        self.delete(start, end);
238    }
239
240    /// Deletes the text between two positions, recompiling and dirtying every
241    /// cell it touches.
242    pub fn delete(&mut self, start: TextCellRef, end: TextCellRef) {
243        if start.col > end.col || (start.col == end.col && start.row > end.row) {
244            return;
245        }
246        let table_clone = self.clone();
247        if start.col == end.col {
248            let start_index = start.row;
249            let end_index = end.row;
250
251            if let Some(column) = self.columns.get_mut(start.col) {
252                if start.row == end.row && start_index < column.src.len() {
253                    let src = &mut column.src[start_index];
254                    let end_offset = std::cmp::min(end.char_offset, src.len());
255                    if start.char_offset < end_offset {
256                        src.replace_range(start.char_offset..end_offset, "");
257                        column.dirty_indices.push(start_index);
258                        let updated_src = src.clone();
259                        let compiled =
260                            crate::core::parser::compile_formula(&updated_src, &[table_clone]);
261                        column.compiled_src[start_index] = compiled;
262                    }
263                } else if start_index < column.len() && end_index >= start_index {
264                    column.drain_rows(start_index..=end_index);
265                }
266            }
267        } else {
268            for col in start.col..=end.col {
269                if let Some(column) = self.columns.get_mut(col) {
270                    let start_index = if col == start.col { col } else { 0 };
271
272                    let end_index = if col == end.col {
273                        col
274                    } else {
275                        column.src.len() - 1
276                    };
277
278                    if start_index < column.len() && end_index >= start_index {
279                        column.drain_rows(start_index..=end_index);
280                    }
281                }
282            }
283        }
284    }
285
286    /// Grows the sheet by one empty row or column on the given side.
287    pub fn extend(&mut self, direction: Direction) {
288        if self.columns.is_empty() {
289            return;
290        }
291        let row_count = self.columns[0].src.len();
292        const MAX_COLS: usize = 26;
293        match direction {
294            Direction::Up => {
295                for column in &mut self.columns {
296                    column.insert_row(0);
297                }
298                self.row_heights.insert(0, None);
299                self.uncommitted_actions
300                    .push(crate::core::SheetAction::InsertRow {
301                        sheet_name: self.name.clone(),
302                        index: 0,
303                    });
304            }
305            Direction::Down => {
306                for column in &mut self.columns {
307                    column.push_row();
308                }
309                self.row_heights.push(None);
310                self.uncommitted_actions
311                    .push(crate::core::SheetAction::InsertRow {
312                        sheet_name: self.name.clone(),
313                        index: row_count,
314                    });
315            }
316            Direction::Left => {
317                if self.columns.len() < MAX_COLS {
318                    self.columns.insert(0, DataColumn::new(row_count));
319                    self.uncommitted_actions
320                        .push(crate::core::SheetAction::InsertCol {
321                            sheet_name: self.name.clone(),
322                            index: 0,
323                        });
324                }
325            }
326            Direction::Right => {
327                if self.columns.len() < MAX_COLS {
328                    self.columns.push(DataColumn::new(row_count));
329                    self.uncommitted_actions
330                        .push(crate::core::SheetAction::InsertCol {
331                            sheet_name: self.name.clone(),
332                            index: self.columns.len() - 1,
333                        });
334                }
335            }
336            Direction::None => {}
337        }
338    }
339
340    /// Ensure sheet has at least target_row+1 rows and target_col+1 columns
341    pub fn ensure_capacity(&mut self, target_row: usize, target_col: usize) {
342        let current_rows = self.row_count();
343        let needed_rows = target_row + 1;
344        let final_rows = current_rows.max(needed_rows);
345
346        while self.columns.len() <= target_col {
347            let col_idx = self.columns.len();
348            let mut col = DataColumn::new(final_rows);
349            col.name = crate::core::parser::col_idx_to_letters(col_idx);
350            self.columns.push(col);
351        }
352
353        if final_rows > current_rows {
354            for col in &mut self.columns {
355                col.resize_rows(final_rows);
356            }
357            self.row_heights.resize(final_rows, None);
358        }
359    }
360
361    /// The style set on a cell, or `None` if it has none.
362    pub fn get_cell_style(&self, row: usize, col: usize) -> Option<&crate::core::CellStyle> {
363        self.columns
364            .get(col)
365            .and_then(|column| column.styles.get(row))
366            .and_then(|opt| opt.as_ref())
367    }
368
369    /// Replaces a cell's style, growing the sheet if the cell is past its
370    /// current bounds
371    pub fn set_cell_style(&mut self, row: usize, col: usize, style: crate::core::CellStyle) {
372        self.ensure_capacity(row, col);
373        if let Some(column) = self.columns.get_mut(col)
374            && row < column.styles.len()
375        {
376            if style.is_empty() {
377                column.styles[row] = None;
378            } else {
379                column.styles[row] = Some(style);
380            }
381        }
382    }
383
384    /// Mutates a cell's style in place.
385    ///
386    /// Grows the sheet if needed; a style left empty is dropped.
387    pub fn update_cell_style<F>(&mut self, row: usize, col: usize, f: F)
388    where
389        F: FnOnce(&mut crate::core::CellStyle),
390    {
391        self.ensure_capacity(row, col);
392        if let Some(column) = self.columns.get_mut(col)
393            && row < column.styles.len()
394        {
395            let mut current = column.styles[row].clone().unwrap_or_default();
396            f(&mut current);
397            if current.is_empty() {
398                column.styles[row] = None;
399            } else {
400                column.styles[row] = Some(current);
401            }
402        }
403    }
404
405    /// Removes a cell's style
406    pub fn clear_cell_style(&mut self, row: usize, col: usize) {
407        if let Some(column) = self.columns.get_mut(col)
408            && row < column.styles.len()
409        {
410            column.styles[row] = None;
411        }
412    }
413
414    /// Returns the custom width for the zero-based column, if one is set.
415    pub fn get_column_width(&self, col: usize) -> Option<f64> {
416        self.columns.get(col).and_then(|column| column.width)
417    }
418
419    /// Sets or clears the custom width for an existing zero-based column.
420    pub fn set_column_width(&mut self, col: usize, width: Option<f64>) {
421        if let Some(column) = self.columns.get_mut(col) {
422            column.width = width.filter(|value| value.is_finite() && *value >= 0.0);
423        }
424    }
425
426    /// Returns the custom height for the zero-based row, if one is set.
427    pub fn get_row_height(&self, row: usize) -> Option<f64> {
428        self.row_heights.get(row).and_then(|height| *height)
429    }
430
431    /// Sets or clears the custom height for an existing zero-based row.
432    pub fn set_row_height(&mut self, row: usize, height: Option<f64>) {
433        if row >= self.row_count() {
434            return;
435        }
436        let row_count = self.row_count();
437        self.row_heights.resize(row_count, None);
438        self.row_heights[row] = height.filter(|value| value.is_finite() && *value >= 0.0);
439    }
440
441    /// Insert a new empty row at the specified index.
442    /// If index is >= row_count, appends at the end
443    pub fn insert_row(&mut self, index: usize) {
444        let row_count = self.row_count();
445        self.row_heights.resize(row_count, None);
446        if index >= row_count {
447            for column in &mut self.columns {
448                column.push_row();
449            }
450            self.row_heights.push(None);
451            self.uncommitted_actions
452                .push(crate::core::SheetAction::InsertRow {
453                    sheet_name: self.name.clone(),
454                    index: row_count,
455                });
456        } else {
457            for column in &mut self.columns {
458                column.insert_row(index);
459            }
460            self.row_heights.insert(index, None);
461            self.uncommitted_actions
462                .push(crate::core::SheetAction::InsertRow {
463                    sheet_name: self.name.clone(),
464                    index,
465                });
466        }
467    }
468
469    /// Deletes a row, shifting the rows below it up.
470    pub fn delete_row(&mut self, index: usize) {
471        let row_count = self.row_count();
472        if index < row_count {
473            for column in &mut self.columns {
474                column.remove_row(index);
475            }
476            if index < self.row_heights.len() {
477                self.row_heights.remove(index);
478            }
479            self.uncommitted_actions
480                .push(crate::core::SheetAction::DeleteRow {
481                    sheet_name: self.name.clone(),
482                    index,
483                });
484            self.mark_all_dirty();
485        }
486    }
487
488    /// Excel's *Insert cells, shift down* over an inclusive column band.
489    pub fn insert_cells_shift_down(
490        &mut self,
491        row: usize,
492        first_col: usize,
493        last_col: usize,
494        count: usize,
495    ) {
496        let last_col = last_col.min(self.columns.len().saturating_sub(1));
497        if count == 0 || self.columns.is_empty() || first_col > last_col {
498            return;
499        }
500        for column in &mut self.columns {
501            for _ in 0..count {
502                column.push_row();
503            }
504        }
505        let row_count = self.row_count();
506        self.row_heights.resize(row_count, None);
507        for column in &mut self.columns[first_col..=last_col] {
508            for _ in 0..count {
509                column.insert_row(row);
510                column.remove_row(column.len() - 1);
511            }
512        }
513        self.uncommitted_actions
514            .push(crate::core::SheetAction::InsertRow {
515                sheet_name: self.name.clone(),
516                index: row,
517            });
518        self.mark_all_dirty();
519    }
520
521    /// Excel's *Delete cells, shift up* over an inclusive column band; the
522    /// inverse of [`Sheet::insert_cells_shift_down`].
523    pub fn delete_cells_shift_up(
524        &mut self,
525        row: usize,
526        first_col: usize,
527        last_col: usize,
528        count: usize,
529    ) {
530        let last_col = last_col.min(self.columns.len().saturating_sub(1));
531        if count == 0 || self.columns.is_empty() || first_col > last_col || row >= self.row_count()
532        {
533            return;
534        }
535        for column in &mut self.columns[first_col..=last_col] {
536            for _ in 0..count {
537                if row < column.len() {
538                    column.remove_row(row);
539                    column.push_row();
540                }
541            }
542        }
543        self.uncommitted_actions
544            .push(crate::core::SheetAction::DeleteRow {
545                sheet_name: self.name.clone(),
546                index: row,
547            });
548        self.mark_all_dirty();
549    }
550
551    /// Deletes a column, shifting the columns to its right left.
552    pub fn delete_col(&mut self, index: usize) {
553        if index < self.columns.len() {
554            self.columns.remove(index);
555            self.uncommitted_actions
556                .push(crate::core::SheetAction::DeleteCol {
557                    sheet_name: self.name.clone(),
558                    index,
559                });
560            self.mark_all_dirty();
561        }
562    }
563
564    /// Insert a new empty column at the specified index
565    /// If index is >= columns.len(), appends at the end
566    pub fn insert_col(&mut self, index: usize) {
567        let row_count = self.row_count();
568        let new_col = DataColumn::new(row_count);
569        let col_count = self.columns.len();
570        if index >= col_count {
571            self.columns.push(new_col);
572            self.uncommitted_actions
573                .push(crate::core::SheetAction::InsertCol {
574                    sheet_name: self.name.clone(),
575                    index: col_count,
576                });
577        } else {
578            self.columns.insert(index, new_col);
579            self.uncommitted_actions
580                .push(crate::core::SheetAction::InsertCol {
581                    sheet_name: self.name.clone(),
582                    index,
583                });
584        }
585        self.mark_all_dirty();
586    }
587
588    /// The sheet's columns
589    pub fn columns(&self) -> &[DataColumn] {
590        &self.columns
591    }
592
593    /// Allocated rows
594    pub fn row_count(&self) -> usize {
595        self.columns.first().map(|c| c.src.len()).unwrap_or(0)
596    }
597
598    /// Allocated columns
599    pub fn col_count(&self) -> usize {
600        self.columns.len()
601    }
602}