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`, as a half-open range of
5/// character indices.
6///
7/// A "word" is a run of alphanumerics and underscores, a run of whitespace, or
8/// a run of punctuation -- so double-clicking in a formula selects a function
9/// name or a cell reference rather than the whole line. An offset at the end
10/// of the text, or one just past a word onto whitespace, selects the word to
11/// its left.
12pub fn get_word_boundaries_from_str(text: &str, char_offset: usize) -> (usize, usize) {
13    if text.is_empty() {
14        return (0, 0);
15    }
16
17    let chars: Vec<char> = text.chars().collect();
18    let len = chars.len();
19    let offset = char_offset.min(len);
20
21    let is_word_char = |c: char| c.is_alphanumeric() || c == '_';
22
23    let (on_idx, on_c) = if offset < len {
24        if chars[offset].is_whitespace() && offset > 0 && is_word_char(chars[offset - 1]) {
25            (offset - 1, chars[offset - 1])
26        } else {
27            (offset, chars[offset])
28        }
29    } else if offset > 0 {
30        (offset - 1, chars[offset - 1])
31    } else {
32        return (0, 0);
33    };
34
35    if on_c.is_whitespace() {
36        let mut start = on_idx;
37        while start > 0 && chars[start - 1].is_whitespace() {
38            start -= 1;
39        }
40        let mut end = on_idx + 1;
41        while end < len && chars[end].is_whitespace() {
42            end += 1;
43        }
44        return (start, end);
45    }
46
47    if is_word_char(on_c) {
48        let mut start = on_idx;
49        while start > 0 && is_word_char(chars[start - 1]) {
50            start -= 1;
51        }
52        let mut end = on_idx + 1;
53        while end < len && is_word_char(chars[end]) {
54            end += 1;
55        }
56        return (start, end);
57    }
58
59    let mut start = on_idx;
60    while start > 0 && !is_word_char(chars[start - 1]) && !chars[start - 1].is_whitespace() {
61        start -= 1;
62    }
63    let mut end = on_idx + 1;
64    while end < len && !is_word_char(chars[end]) && !chars[end].is_whitespace() {
65        end += 1;
66    }
67    (start, end)
68}
69impl Sheet {
70    /// The computed value of a cell, or [`ResultData::None`] if it is empty
71    /// or outside the sheet's allocated grid.
72    ///
73    /// Reflects the last [`Sheet::commit`]; a cell edited since then still
74    /// reads as its old value.
75    ///
76    /// A date reads back as the plain numeric serial it is. Rendering it in
77    /// the notation the cell carries is `Sheet::get_display_string`'s job, and
78    /// only its -- do not format a `ResultData` directly if a user will see it.
79    pub fn get_result_data(&self, cell: &CellRef) -> ResultData {
80        let col = self.columns.get(cell.col);
81        if let Some(col) = col {
82            col.data.get(cell.row).unwrap_or(ResultData::None)
83        } else {
84            ResultData::None
85        }
86    }
87
88    /// The date format a formula should inherit from the cells it reads, if
89    /// any -- Excel's "date plus a number is still a date" behavior.
90    ///
91    /// The rule is deliberately about the *operator*, not about how many
92    /// cells the formula touches, because those come apart: `=YEAR(A1)` reads
93    /// exactly one date cell and returns a year, which is emphatically not a
94    /// date. So only two shapes inherit:
95    ///
96    /// - a bare reference to a date cell (`=A1`), and
97    /// - adding or subtracting a non-date from one (`=A1+1`, `=1+A1`).
98    ///
99    /// Everything else -- a function call, a product, a difference of two
100    /// dates (which is a count of days) -- declines, leaving a plain number.
101    pub(super) fn inherited_date_format(&self, ast: &crate::core::parser::Expr) -> Option<String> {
102        use crate::core::parser::{Expr, Op};
103        match ast {
104            Expr::CellRef {
105                sheet, row, col, ..
106            } if sheet.is_none() => self
107                .get_cell_style(*row, *col)
108                .and_then(|s| s.num_format.clone())
109                .filter(|code| crate::core::date::is_date_code(code)),
110            Expr::BinaryOp {
111                op: Op::Add | Op::Sub,
112                left,
113                right,
114            } => {
115                let left_fmt = self.inherited_date_format(left);
116                let right_fmt = self.inherited_date_format(right);
117                match (left_fmt, right_fmt) {
118                    // Exactly one side is a date: the other is an offset in
119                    // days, so the result stays that date's format.
120                    (Some(fmt), None) | (None, Some(fmt)) => Some(fmt),
121                    // Neither, or both (a day count) -- no date format.
122                    _ => None,
123                }
124            }
125            _ => None,
126        }
127    }
128
129    /// The cell's value as it should be shown, honoring the cell's number
130    /// format.
131    ///
132    /// A date cell holds a plain numeric serial, exactly as in Excel, so
133    /// rendering it as a date is a display-time concern: this is the only
134    /// place that turns 46195 back into `6/22/26`. Everything that shows a
135    /// value to a user should go through here rather than formatting
136    /// [`ResultData`] directly, which knows nothing about formats.
137    pub fn get_display_string(&self, cell: &CellRef) -> String {
138        let value = self.get_result_data(cell);
139        let Some(code) = self
140            .get_cell_style(cell.row, cell.col)
141            .and_then(|s| s.num_format.as_deref())
142        else {
143            return value.to_string();
144        };
145        if !crate::core::date::is_date_code(code) {
146            return value.to_string();
147        }
148        // Only a number is a date serial; text and errors render as-is.
149        let serial = match value {
150            ResultData::Float(f) => f,
151            ResultData::Integer(i) => i as f64,
152            _ => return value.to_string(),
153        };
154        if serial < 0.0 {
155            return value.to_string();
156        }
157        crate::core::date::render_date_code(
158            crate::core::date::excel_serial_to_date(serial),
159            code,
160            crate::core::date::StringCase::Title,
161        )
162    }
163
164    /// Returns the intrinsic data type of a cell.
165    pub fn get_cell_type(&self, cell: &CellRef) -> CellType {
166        let col = self.columns.get(cell.col);
167        if let Some(col) = col {
168            col.cell_types
169                .get(cell.row)
170                .copied()
171                .unwrap_or(CellType::Empty)
172        } else {
173            CellType::Empty
174        }
175    }
176
177    /// Sets the intrinsic data type of a cell at (row, col).
178    pub fn set_cell_type(&mut self, row: usize, col: usize, cell_type: CellType) {
179        if let Some(column) = self.columns.get_mut(col)
180            && row < column.cell_types.len()
181        {
182            column.cell_types[row] = cell_type;
183            column.mark_dirty(row);
184        }
185    }
186
187    /// Sets the source text and explicit cell type of a particular cell.
188    pub fn set_cell_with_type(&mut self, row: usize, col: usize, src: String, cell_type: CellType) {
189        let table_clone = self.clone();
190        if let Some(column) = self.columns.get_mut(col)
191            && row < column.src.len()
192        {
193            column.src[row] = src.clone();
194            column.cell_types[row] = cell_type;
195            let compiled = crate::core::parser::compile_formula(&src, &[table_clone]);
196            column.compiled_src[row] = compiled;
197            column.mark_dirty(row);
198
199            self.uncommitted_actions
200                .push(crate::core::SheetAction::SetCellSrc {
201                    sheet_name: self.name.clone(),
202                    col,
203                    row,
204                    src,
205                });
206        }
207    }
208
209    /// Updates the src text of a particular cell but does
210    /// not automatically evaluate. Call [`Sheet::commit`] to evaluate
211    /// updated cells.
212    /// Directly sets the src of a cell and marks it dirty.
213    pub fn set_cell_src(&mut self, row: usize, col: usize, src: String) {
214        let table_clone = self.clone();
215        if let Some(column) = self.columns.get_mut(col)
216            && row < column.src.len()
217        {
218            column.src[row] = src.clone();
219            column.cell_types[row] = CellType::Auto;
220            let compiled = crate::core::parser::compile_formula(&src, &[table_clone]);
221            column.compiled_src[row] = compiled;
222            column.mark_dirty(row);
223
224            self.uncommitted_actions
225                .push(crate::core::SheetAction::SetCellSrc {
226                    sheet_name: self.name.clone(),
227                    col,
228                    row,
229                    src,
230                });
231        }
232    }
233
234    /// Inserts text into a cell's source at a character offset, as typing
235    /// into it would, then recompiles and marks it dirty.
236    ///
237    /// This is a text edit within one cell, not a range insert; see
238    /// [`Sheet::insert_row`] and [`Sheet::insert_col`] for the structural
239    /// operations. Out-of-range positions are ignored.
240    pub fn insert(&mut self, pos: TextCellRef, input: &str) {
241        let TextCellRef {
242            row,
243            col,
244            char_offset,
245        } = pos;
246        let table_clone = self.clone();
247        let existing_col = self.columns.get_mut(col);
248        match existing_col {
249            Some(existing_column) => {
250                existing_column.insert(ColumnPosition { row, char_offset }, input);
251                let src = existing_column.src[row].clone();
252                let compiled = crate::core::parser::compile_formula(&src, &[table_clone]);
253                existing_column.compiled_src[row] = compiled;
254                existing_column.mark_dirty(row);
255                self.uncommitted_actions
256                    .push(crate::core::SheetAction::SetCellSrc {
257                        sheet_name: self.name.clone(),
258                        col,
259                        row,
260                        src,
261                    });
262            }
263            None => {
264                println!("Warning: column {} does not exist", col)
265            }
266        }
267    }
268
269    /// Delete one before (like backspace)
270    pub fn delete_one_before(&mut self, pos: TextCellRef) {
271        let char_offset = pos.char_offset;
272        let start = if char_offset > 0 {
273            TextCellRef {
274                row: pos.row,
275                col: pos.col,
276                char_offset: char_offset - 1,
277            }
278        } else {
279            pos.clone()
280        };
281        let end = pos;
282        self.delete(start, end);
283    }
284
285    /// Deletes the text between two positions, recompiling and dirtying every
286    /// cell it touches.
287    ///
288    /// Within a single cell this removes a character range; spanning cells it
289    /// truncates the first, clears those in between and trims the last.
290    /// Ignored if `end` precedes `start`.
291    pub fn delete(&mut self, start: TextCellRef, end: TextCellRef) {
292        // Validate positions are in correct order
293        if start.col > end.col || (start.col == end.col && start.row > end.row) {
294            return;
295        }
296        let table_clone = self.clone();
297        if start.col == end.col {
298            let start_index = start.row;
299            let end_index = end.row;
300
301            if let Some(column) = self.columns.get_mut(start.col) {
302                if start.row == end.row && start_index < column.src.len() {
303                    let src = &mut column.src[start_index];
304                    let end_offset = std::cmp::min(end.char_offset, src.len());
305                    if start.char_offset < end_offset {
306                        src.replace_range(start.char_offset..end_offset, "");
307                        column.dirty_indices.push(start_index);
308                        let updated_src = src.clone();
309                        let compiled =
310                            crate::core::parser::compile_formula(&updated_src, &[table_clone]);
311                        column.compiled_src[start_index] = compiled;
312                    }
313                } else if start_index < column.len() && end_index >= start_index {
314                    column.drain_rows(start_index..=end_index);
315                }
316            }
317        } else {
318            for col in start.col..=end.col {
319                if let Some(column) = self.columns.get_mut(col) {
320                    let start_index = if col == start.col { col } else { 0 };
321
322                    let end_index = if col == end.col {
323                        col
324                    } else {
325                        column.src.len() - 1
326                    };
327
328                    if start_index < column.len() && end_index >= start_index {
329                        column.drain_rows(start_index..=end_index);
330                    }
331                }
332            }
333        }
334    }
335
336    /// Grows the sheet by one empty row or column on the given side.
337    ///
338    /// [`Direction::None`] does nothing. Rows are unbounded, but sideways
339    /// growth stops once the sheet has 26 columns.
340    pub fn extend(&mut self, direction: Direction) {
341        if self.columns.is_empty() {
342            return;
343        }
344        let row_count = self.columns[0].src.len();
345        const MAX_COLS: usize = 26;
346        match direction {
347            Direction::Up => {
348                for column in &mut self.columns {
349                    column.insert_row(0);
350                }
351                self.uncommitted_actions
352                    .push(crate::core::SheetAction::InsertRow {
353                        sheet_name: self.name.clone(),
354                        index: 0,
355                    });
356            }
357            Direction::Down => {
358                for column in &mut self.columns {
359                    column.push_row();
360                }
361                self.uncommitted_actions
362                    .push(crate::core::SheetAction::InsertRow {
363                        sheet_name: self.name.clone(),
364                        index: row_count,
365                    });
366            }
367            Direction::Left => {
368                if self.columns.len() < MAX_COLS {
369                    self.columns.insert(0, DataColumn::new(row_count));
370                    self.uncommitted_actions
371                        .push(crate::core::SheetAction::InsertCol {
372                            sheet_name: self.name.clone(),
373                            index: 0,
374                        });
375                }
376            }
377            Direction::Right => {
378                if self.columns.len() < MAX_COLS {
379                    self.columns.push(DataColumn::new(row_count));
380                    self.uncommitted_actions
381                        .push(crate::core::SheetAction::InsertCol {
382                            sheet_name: self.name.clone(),
383                            index: self.columns.len() - 1,
384                        });
385                }
386            }
387            Direction::None => {}
388        }
389    }
390
391    /// Ensure sheet has at least target_row+1 rows and target_col+1 columns
392    pub fn ensure_capacity(&mut self, target_row: usize, target_col: usize) {
393        let current_rows = self.row_count();
394        let needed_rows = target_row + 1;
395        let final_rows = current_rows.max(needed_rows);
396
397        while self.columns.len() <= target_col {
398            let col_idx = self.columns.len();
399            let mut col = DataColumn::new(final_rows);
400            col.name = crate::core::parser::col_idx_to_letters(col_idx);
401            self.columns.push(col);
402        }
403
404        if final_rows > current_rows {
405            for col in &mut self.columns {
406                col.resize_rows(final_rows);
407            }
408        }
409    }
410
411    /// The style set on a cell, or `None` if it has none.
412    ///
413    /// This is where a date cell's `num_format` lives -- the notation half of
414    /// a date, the value half being the serial in the cell.
415    pub fn get_cell_style(&self, row: usize, col: usize) -> Option<&crate::core::CellStyle> {
416        self.columns
417            .get(col)
418            .and_then(|column| column.styles.get(row))
419            .and_then(|opt| opt.as_ref())
420    }
421
422    /// Replaces a cell's style, growing the sheet if the cell is past its
423    /// current bounds. An empty style is stored as no style at all.
424    pub fn set_cell_style(&mut self, row: usize, col: usize, style: crate::core::CellStyle) {
425        self.ensure_capacity(row, col);
426        if let Some(column) = self.columns.get_mut(col)
427            && row < column.styles.len()
428        {
429            if style.is_empty() {
430                column.styles[row] = None;
431            } else {
432                column.styles[row] = Some(style);
433            }
434        }
435    }
436
437    /// Mutates a cell's style in place, starting from the default if it has
438    /// none, so one attribute can be changed without disturbing the others.
439    ///
440    /// Grows the sheet if needed; a style left empty is dropped.
441    pub fn update_cell_style<F>(&mut self, row: usize, col: usize, f: F)
442    where
443        F: FnOnce(&mut crate::core::CellStyle),
444    {
445        self.ensure_capacity(row, col);
446        if let Some(column) = self.columns.get_mut(col)
447            && row < column.styles.len()
448        {
449            let mut current = column.styles[row].clone().unwrap_or_default();
450            f(&mut current);
451            if current.is_empty() {
452                column.styles[row] = None;
453            } else {
454                column.styles[row] = Some(current);
455            }
456        }
457    }
458
459    /// Removes a cell's style. Unlike the setters, this never grows the sheet.
460    pub fn clear_cell_style(&mut self, row: usize, col: usize) {
461        if let Some(column) = self.columns.get_mut(col)
462            && row < column.styles.len()
463        {
464            column.styles[row] = None;
465        }
466    }
467
468    /// Insert a new empty row at the specified index
469    /// If index is >= row_count, appends at the end
470    pub fn insert_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.push_row();
475            }
476            self.uncommitted_actions
477                .push(crate::core::SheetAction::InsertRow {
478                    sheet_name: self.name.clone(),
479                    index: row_count,
480                });
481        } else {
482            for column in &mut self.columns {
483                column.insert_row(index);
484            }
485            self.uncommitted_actions
486                .push(crate::core::SheetAction::InsertRow {
487                    sheet_name: self.name.clone(),
488                    index,
489                });
490        }
491    }
492
493    /// Deletes a row, shifting the rows below it up.
494    ///
495    /// Removes the entry from all three parallel per-row vectors together,
496    /// which is what keeps them the same length, and rebases the dirty queue.
497    /// Out-of-range indices are ignored. Everything is marked dirty, since
498    /// formulas above the deleted row may refer to it.
499    pub fn delete_row(&mut self, index: usize) {
500        let row_count = self.row_count();
501        if index < row_count {
502            for column in &mut self.columns {
503                column.remove_row(index);
504            }
505            self.uncommitted_actions
506                .push(crate::core::SheetAction::DeleteRow {
507                    sheet_name: self.name.clone(),
508                    index,
509                });
510            self.mark_all_dirty();
511        }
512    }
513
514    /// Excel's *Insert cells, shift down* over an inclusive column band.
515    ///
516    /// Unlike [`Sheet::insert_row`] this moves only `first_col..=last_col`,
517    /// leaving every other column where it is -- which is what
518    /// `ListRows.Add` actually does. Measured: adding a row to a table at
519    /// `A1:C4` moves `A8` down to `A9` but leaves `E2` alone.
520    ///
521    /// Every column keeps the same length: the sheet first grows by `count`
522    /// rows, so the rows pushed off the bottom of the band are the blank ones
523    /// just added rather than data. Everything moves through `DataColumn`'s
524    /// paired operations, so `src` / `data` / `compiled_src` / `styles` stay
525    /// aligned.
526    ///
527    /// Out-of-range bands and a zero `count` are no-ops. Formula references
528    /// are *not* rewritten here -- that is
529    /// `WorkbookManager::insert_cells_shift_down`'s job, since it spans
530    /// sheets.
531    pub fn insert_cells_shift_down(
532        &mut self,
533        row: usize,
534        first_col: usize,
535        last_col: usize,
536        count: usize,
537    ) {
538        let last_col = last_col.min(self.columns.len().saturating_sub(1));
539        if count == 0 || self.columns.is_empty() || first_col > last_col {
540            return;
541        }
542        // Grow every column together first, so the band has somewhere to
543        // push into and the sheet stays rectangular throughout.
544        for column in &mut self.columns {
545            for _ in 0..count {
546                column.push_row();
547            }
548        }
549        for column in &mut self.columns[first_col..=last_col] {
550            for _ in 0..count {
551                column.insert_row(row);
552                // Drop the blank row the growth added, so this column ends
553                // the same length as the untouched ones.
554                column.remove_row(column.len() - 1);
555            }
556        }
557        self.uncommitted_actions
558            .push(crate::core::SheetAction::InsertRow {
559                sheet_name: self.name.clone(),
560                index: row,
561            });
562        self.mark_all_dirty();
563    }
564
565    /// Excel's *Delete cells, shift up* over an inclusive column band; the
566    /// inverse of [`Sheet::insert_cells_shift_down`].
567    ///
568    /// The band's rows below `row` move up and blank rows appear at its
569    /// bottom, so the sheet keeps its shape and other columns are untouched.
570    pub fn delete_cells_shift_up(
571        &mut self,
572        row: usize,
573        first_col: usize,
574        last_col: usize,
575        count: usize,
576    ) {
577        let last_col = last_col.min(self.columns.len().saturating_sub(1));
578        if count == 0 || self.columns.is_empty() || first_col > last_col || row >= self.row_count()
579        {
580            return;
581        }
582        for column in &mut self.columns[first_col..=last_col] {
583            for _ in 0..count {
584                if row < column.len() {
585                    column.remove_row(row);
586                    // Keep the length: the band gains a blank row at the
587                    // bottom for each one removed from the middle.
588                    column.push_row();
589                }
590            }
591        }
592        self.uncommitted_actions
593            .push(crate::core::SheetAction::DeleteRow {
594                sheet_name: self.name.clone(),
595                index: row,
596            });
597        self.mark_all_dirty();
598    }
599
600    /// Deletes a column, shifting the columns to its right left.
601    ///
602    /// Out-of-range indices are ignored; everything is marked dirty.
603    pub fn delete_col(&mut self, index: usize) {
604        if index < self.columns.len() {
605            self.columns.remove(index);
606            self.uncommitted_actions
607                .push(crate::core::SheetAction::DeleteCol {
608                    sheet_name: self.name.clone(),
609                    index,
610                });
611            self.mark_all_dirty();
612        }
613    }
614
615    /// Insert a new empty column at the specified index
616    /// If index is >= columns.len(), appends at the end
617    pub fn insert_col(&mut self, index: usize) {
618        let row_count = self.row_count();
619        let new_col = DataColumn::new(row_count);
620        let col_count = self.columns.len();
621        if index >= col_count {
622            self.columns.push(new_col);
623            self.uncommitted_actions
624                .push(crate::core::SheetAction::InsertCol {
625                    sheet_name: self.name.clone(),
626                    index: col_count,
627                });
628        } else {
629            self.columns.insert(index, new_col);
630            self.uncommitted_actions
631                .push(crate::core::SheetAction::InsertCol {
632                    sheet_name: self.name.clone(),
633                    index,
634                });
635        }
636        self.mark_all_dirty();
637    }
638
639    /// The sheet's columns.
640    ///
641    /// Read-only: every column must keep the same number of rows, so growing
642    /// or replacing one from outside would desync the sheet. Use
643    /// [`Sheet::insert_col`], [`Sheet::delete_col`] and [`Sheet::extend`] to
644    /// change the shape.
645    pub fn columns(&self) -> &[DataColumn] {
646        &self.columns
647    }
648
649    /// Allocated rows, taken from the first column -- every column has the
650    /// same length.
651    pub fn row_count(&self) -> usize {
652        self.columns.first().map(|c| c.src.len()).unwrap_or(0)
653    }
654
655    /// Allocated columns.
656    pub fn col_count(&self) -> usize {
657        self.columns.len()
658    }
659}