Skip to main content

visi_core/core/engine/sheet/
edit.rs

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