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                    (Some(fmt), None) | (None, Some(fmt)) => Some(fmt),
119                    _ => None,
120                }
121            }
122            _ => None,
123        }
124    }
125
126    /// [AI-Agent] The cell's value as it should be shown, honoring dates and numeric number formats.
127    pub fn get_display_string(&self, cell: &CellRef) -> String {
128        let value = self.get_result_data(cell);
129        let Some(code) = self
130            .get_cell_style(cell.row, cell.col)
131            .and_then(|s| s.num_format.as_deref())
132        else {
133            return value.to_string();
134        };
135        match value {
136            ResultData::Float(f) => crate::core::text::format_number_format(f, code)
137                .unwrap_or_else(|_| ResultData::Float(f).to_string()),
138            ResultData::Integer(i) => crate::core::text::format_number_format(i as f64, code)
139                .unwrap_or_else(|_| ResultData::Integer(i).to_string()),
140            ResultData::String(s) => crate::core::text::format_text_format(&s, code)
141                .unwrap_or(ResultData::String(s).to_string()),
142            other => other.to_string(),
143        }
144    }
145
146    /// Returns the intrinsic data type of a cell.
147    pub fn get_cell_type(&self, cell: &CellRef) -> CellType {
148        let col = self.columns.get(cell.col);
149        if let Some(col) = col {
150            col.cell_types
151                .get(cell.row)
152                .copied()
153                .unwrap_or(CellType::Empty)
154        } else {
155            CellType::Empty
156        }
157    }
158
159    /// Sets the intrinsic data type of a cell at (row, col).
160    pub fn set_cell_type(&mut self, row: usize, col: usize, cell_type: CellType) {
161        if let Some(column) = self.columns.get_mut(col)
162            && row < column.cell_types.len()
163        {
164            column.cell_types[row] = cell_type;
165            column.mark_dirty(row);
166        }
167    }
168
169    /// Sets the source text and explicit cell type of a particular cell.
170    pub fn set_cell_with_type(&mut self, row: usize, col: usize, src: String, cell_type: CellType) {
171        let table_clone = self.clone();
172        if let Some(column) = self.columns.get_mut(col)
173            && row < column.src.len()
174        {
175            column.src[row] = src.clone();
176            column.cell_types[row] = cell_type;
177            let compiled = crate::core::parser::compile_formula(&src, &[table_clone]);
178            column.compiled_src[row] = compiled;
179            column.mark_dirty(row);
180
181            self.uncommitted_actions
182                .push(crate::core::SheetAction::SetCellSrc {
183                    sheet_name: self.name.clone(),
184                    col,
185                    row,
186                    src,
187                });
188        }
189    }
190
191    /// Updates the src text of a particular cell but does
192    /// not automatically evaluate. Call [`Sheet::commit`] to evaluate
193    /// updated cells.
194    /// Directly sets the src of a cell and marks it dirty.
195    pub fn set_cell_src(&mut self, row: usize, col: usize, src: String) {
196        let table_clone = self.clone();
197        if let Some(column) = self.columns.get_mut(col)
198            && row < column.src.len()
199        {
200            column.src[row] = src.clone();
201            column.cell_types[row] = CellType::Empty;
202            let compiled = crate::core::parser::compile_formula(&src, &[table_clone]);
203            column.compiled_src[row] = compiled;
204            column.mark_dirty(row);
205
206            self.uncommitted_actions
207                .push(crate::core::SheetAction::SetCellSrc {
208                    sheet_name: self.name.clone(),
209                    col,
210                    row,
211                    src,
212                });
213        }
214    }
215
216    /// Inserts text into a cell's source at a character offset, as typing
217    /// into it would, then recompiles and marks it dirty.
218    ///
219    /// This is a text edit within one cell, not a range insert; see
220    /// [`Sheet::insert_row`] and [`Sheet::insert_col`] for the structural
221    /// operations. Out-of-range positions are ignored.
222    pub fn insert(&mut self, pos: TextCellRef, input: &str) {
223        let TextCellRef {
224            row,
225            col,
226            char_offset,
227        } = pos;
228        let table_clone = self.clone();
229        let existing_col = self.columns.get_mut(col);
230        match existing_col {
231            Some(existing_column) => {
232                existing_column.insert(ColumnPosition { row, char_offset }, input);
233                let src = existing_column.src[row].clone();
234                let compiled = crate::core::parser::compile_formula(&src, &[table_clone]);
235                existing_column.compiled_src[row] = compiled;
236                existing_column.mark_dirty(row);
237                self.uncommitted_actions
238                    .push(crate::core::SheetAction::SetCellSrc {
239                        sheet_name: self.name.clone(),
240                        col,
241                        row,
242                        src,
243                    });
244            }
245            None => {
246                println!("Warning: column {} does not exist", col)
247            }
248        }
249    }
250
251    /// Delete one before (like backspace)
252    pub fn delete_one_before(&mut self, pos: TextCellRef) {
253        let char_offset = pos.char_offset;
254        let start = if char_offset > 0 {
255            TextCellRef {
256                row: pos.row,
257                col: pos.col,
258                char_offset: char_offset - 1,
259            }
260        } else {
261            pos.clone()
262        };
263        let end = pos;
264        self.delete(start, end);
265    }
266
267    /// Deletes the text between two positions, recompiling and dirtying every
268    /// cell it touches.
269    ///
270    /// Within a single cell this removes a character range; spanning cells it
271    /// truncates the first, clears those in between and trims the last.
272    /// Ignored if `end` precedes `start`.
273    pub fn delete(&mut self, start: TextCellRef, end: TextCellRef) {
274        if start.col > end.col || (start.col == end.col && start.row > end.row) {
275            return;
276        }
277        let table_clone = self.clone();
278        if start.col == end.col {
279            let start_index = start.row;
280            let end_index = end.row;
281
282            if let Some(column) = self.columns.get_mut(start.col) {
283                if start.row == end.row && start_index < column.src.len() {
284                    let src = &mut column.src[start_index];
285                    let end_offset = std::cmp::min(end.char_offset, src.len());
286                    if start.char_offset < end_offset {
287                        src.replace_range(start.char_offset..end_offset, "");
288                        column.dirty_indices.push(start_index);
289                        let updated_src = src.clone();
290                        let compiled =
291                            crate::core::parser::compile_formula(&updated_src, &[table_clone]);
292                        column.compiled_src[start_index] = compiled;
293                    }
294                } else if start_index < column.len() && end_index >= start_index {
295                    column.drain_rows(start_index..=end_index);
296                }
297            }
298        } else {
299            for col in start.col..=end.col {
300                if let Some(column) = self.columns.get_mut(col) {
301                    let start_index = if col == start.col { col } else { 0 };
302
303                    let end_index = if col == end.col {
304                        col
305                    } else {
306                        column.src.len() - 1
307                    };
308
309                    if start_index < column.len() && end_index >= start_index {
310                        column.drain_rows(start_index..=end_index);
311                    }
312                }
313            }
314        }
315    }
316
317    /// Grows the sheet by one empty row or column on the given side.
318    ///
319    /// [`Direction::None`] does nothing. Rows are unbounded, but sideways
320    /// growth stops once the sheet has 26 columns.
321    pub fn extend(&mut self, direction: Direction) {
322        if self.columns.is_empty() {
323            return;
324        }
325        let row_count = self.columns[0].src.len();
326        const MAX_COLS: usize = 26;
327        match direction {
328            Direction::Up => {
329                for column in &mut self.columns {
330                    column.insert_row(0);
331                }
332                self.row_heights.insert(0, None);
333                self.uncommitted_actions
334                    .push(crate::core::SheetAction::InsertRow {
335                        sheet_name: self.name.clone(),
336                        index: 0,
337                    });
338            }
339            Direction::Down => {
340                for column in &mut self.columns {
341                    column.push_row();
342                }
343                self.row_heights.push(None);
344                self.uncommitted_actions
345                    .push(crate::core::SheetAction::InsertRow {
346                        sheet_name: self.name.clone(),
347                        index: row_count,
348                    });
349            }
350            Direction::Left => {
351                if self.columns.len() < MAX_COLS {
352                    self.columns.insert(0, DataColumn::new(row_count));
353                    self.uncommitted_actions
354                        .push(crate::core::SheetAction::InsertCol {
355                            sheet_name: self.name.clone(),
356                            index: 0,
357                        });
358                }
359            }
360            Direction::Right => {
361                if self.columns.len() < MAX_COLS {
362                    self.columns.push(DataColumn::new(row_count));
363                    self.uncommitted_actions
364                        .push(crate::core::SheetAction::InsertCol {
365                            sheet_name: self.name.clone(),
366                            index: self.columns.len() - 1,
367                        });
368                }
369            }
370            Direction::None => {}
371        }
372    }
373
374    /// Ensure sheet has at least target_row+1 rows and target_col+1 columns
375    pub fn ensure_capacity(&mut self, target_row: usize, target_col: usize) {
376        let current_rows = self.row_count();
377        let needed_rows = target_row + 1;
378        let final_rows = current_rows.max(needed_rows);
379
380        while self.columns.len() <= target_col {
381            let col_idx = self.columns.len();
382            let mut col = DataColumn::new(final_rows);
383            col.name = crate::core::parser::col_idx_to_letters(col_idx);
384            self.columns.push(col);
385        }
386
387        if final_rows > current_rows {
388            for col in &mut self.columns {
389                col.resize_rows(final_rows);
390            }
391            self.row_heights.resize(final_rows, None);
392        }
393    }
394
395    /// The style set on a cell, or `None` if it has none.
396    ///
397    /// This is where a date cell's `num_format` lives -- the notation half of
398    /// a date, the value half being the serial in the cell.
399    pub fn get_cell_style(&self, row: usize, col: usize) -> Option<&crate::core::CellStyle> {
400        self.columns
401            .get(col)
402            .and_then(|column| column.styles.get(row))
403            .and_then(|opt| opt.as_ref())
404    }
405
406    /// Replaces a cell's style, growing the sheet if the cell is past its
407    /// current bounds. An empty style is stored as no style at all.
408    pub fn set_cell_style(&mut self, row: usize, col: usize, style: crate::core::CellStyle) {
409        self.ensure_capacity(row, col);
410        if let Some(column) = self.columns.get_mut(col)
411            && row < column.styles.len()
412        {
413            if style.is_empty() {
414                column.styles[row] = None;
415            } else {
416                column.styles[row] = Some(style);
417            }
418        }
419    }
420
421    /// Mutates a cell's style in place, starting from the default if it has
422    /// none, so one attribute can be changed without disturbing the others.
423    ///
424    /// Grows the sheet if needed; a style left empty is dropped.
425    pub fn update_cell_style<F>(&mut self, row: usize, col: usize, f: F)
426    where
427        F: FnOnce(&mut crate::core::CellStyle),
428    {
429        self.ensure_capacity(row, col);
430        if let Some(column) = self.columns.get_mut(col)
431            && row < column.styles.len()
432        {
433            let mut current = column.styles[row].clone().unwrap_or_default();
434            f(&mut current);
435            if current.is_empty() {
436                column.styles[row] = None;
437            } else {
438                column.styles[row] = Some(current);
439            }
440        }
441    }
442
443    /// Removes a cell's style. Unlike the setters, this never grows the sheet.
444    pub fn clear_cell_style(&mut self, row: usize, col: usize) {
445        if let Some(column) = self.columns.get_mut(col)
446            && row < column.styles.len()
447        {
448            column.styles[row] = None;
449        }
450    }
451
452    /// [GPT-5.5] Returns the custom width for the zero-based column, if one is set.
453    pub fn get_column_width(&self, col: usize) -> Option<f64> {
454        self.columns.get(col).and_then(|column| column.width)
455    }
456
457    /// [GPT-5.5] Sets or clears the custom width for an existing zero-based column.
458    pub fn set_column_width(&mut self, col: usize, width: Option<f64>) {
459        if let Some(column) = self.columns.get_mut(col) {
460            column.width = width.filter(|value| value.is_finite() && *value >= 0.0);
461        }
462    }
463
464    /// [GPT-5.5] Returns the custom height for the zero-based row, if one is set.
465    pub fn get_row_height(&self, row: usize) -> Option<f64> {
466        self.row_heights.get(row).and_then(|height| *height)
467    }
468
469    /// [GPT-5.5] Sets or clears the custom height for an existing zero-based row.
470    pub fn set_row_height(&mut self, row: usize, height: Option<f64>) {
471        if row >= self.row_count() {
472            return;
473        }
474        let row_count = self.row_count();
475        self.row_heights.resize(row_count, None);
476        self.row_heights[row] = height.filter(|value| value.is_finite() && *value >= 0.0);
477    }
478
479    /// Insert a new empty row at the specified index
480    /// If index is >= row_count, appends at the end
481    pub fn insert_row(&mut self, index: usize) {
482        let row_count = self.row_count();
483        self.row_heights.resize(row_count, None);
484        if index >= row_count {
485            for column in &mut self.columns {
486                column.push_row();
487            }
488            self.row_heights.push(None);
489            self.uncommitted_actions
490                .push(crate::core::SheetAction::InsertRow {
491                    sheet_name: self.name.clone(),
492                    index: row_count,
493                });
494        } else {
495            for column in &mut self.columns {
496                column.insert_row(index);
497            }
498            self.row_heights.insert(index, None);
499            self.uncommitted_actions
500                .push(crate::core::SheetAction::InsertRow {
501                    sheet_name: self.name.clone(),
502                    index,
503                });
504        }
505    }
506
507    /// Deletes a row, shifting the rows below it up.
508    ///
509    /// Removes the entry from all three parallel per-row vectors together,
510    /// which is what keeps them the same length, and rebases the dirty queue.
511    /// Out-of-range indices are ignored. Everything is marked dirty, since
512    /// formulas above the deleted row may refer to it.
513    pub fn delete_row(&mut self, index: usize) {
514        let row_count = self.row_count();
515        if index < row_count {
516            for column in &mut self.columns {
517                column.remove_row(index);
518            }
519            if index < self.row_heights.len() {
520                self.row_heights.remove(index);
521            }
522            self.uncommitted_actions
523                .push(crate::core::SheetAction::DeleteRow {
524                    sheet_name: self.name.clone(),
525                    index,
526                });
527            self.mark_all_dirty();
528        }
529    }
530
531    /// Excel's *Insert cells, shift down* over an inclusive column band.
532    ///
533    /// Unlike [`Sheet::insert_row`] this moves only `first_col..=last_col`,
534    /// leaving every other column where it is -- which is what
535    /// `ListRows.Add` actually does. Measured: adding a row to a table at
536    /// `A1:C4` moves `A8` down to `A9` but leaves `E2` alone.
537    ///
538    /// Every column keeps the same length: the sheet first grows by `count`
539    /// rows, so the rows pushed off the bottom of the band are the blank ones
540    /// just added rather than data. Everything moves through `DataColumn`'s
541    /// paired operations, so `src` / `data` / `compiled_src` / `styles` stay
542    /// aligned.
543    ///
544    /// Out-of-range bands and a zero `count` are no-ops. Formula references
545    /// are *not* rewritten here -- that is
546    /// `WorkbookManager::insert_cells_shift_down`'s job, since it spans
547    /// sheets.
548    pub fn insert_cells_shift_down(
549        &mut self,
550        row: usize,
551        first_col: usize,
552        last_col: usize,
553        count: usize,
554    ) {
555        let last_col = last_col.min(self.columns.len().saturating_sub(1));
556        if count == 0 || self.columns.is_empty() || first_col > last_col {
557            return;
558        }
559        for column in &mut self.columns {
560            for _ in 0..count {
561                column.push_row();
562            }
563        }
564        let row_count = self.row_count();
565        self.row_heights.resize(row_count, None);
566        for column in &mut self.columns[first_col..=last_col] {
567            for _ in 0..count {
568                column.insert_row(row);
569                column.remove_row(column.len() - 1);
570            }
571        }
572        self.uncommitted_actions
573            .push(crate::core::SheetAction::InsertRow {
574                sheet_name: self.name.clone(),
575                index: row,
576            });
577        self.mark_all_dirty();
578    }
579
580    /// Excel's *Delete cells, shift up* over an inclusive column band; the
581    /// inverse of [`Sheet::insert_cells_shift_down`].
582    ///
583    /// The band's rows below `row` move up and blank rows appear at its
584    /// bottom, so the sheet keeps its shape and other columns are untouched.
585    pub fn delete_cells_shift_up(
586        &mut self,
587        row: usize,
588        first_col: usize,
589        last_col: usize,
590        count: usize,
591    ) {
592        let last_col = last_col.min(self.columns.len().saturating_sub(1));
593        if count == 0 || self.columns.is_empty() || first_col > last_col || row >= self.row_count()
594        {
595            return;
596        }
597        for column in &mut self.columns[first_col..=last_col] {
598            for _ in 0..count {
599                if row < column.len() {
600                    column.remove_row(row);
601                    column.push_row();
602                }
603            }
604        }
605        self.uncommitted_actions
606            .push(crate::core::SheetAction::DeleteRow {
607                sheet_name: self.name.clone(),
608                index: row,
609            });
610        self.mark_all_dirty();
611    }
612
613    /// Deletes a column, shifting the columns to its right left.
614    ///
615    /// Out-of-range indices are ignored; everything is marked dirty.
616    pub fn delete_col(&mut self, index: usize) {
617        if index < self.columns.len() {
618            self.columns.remove(index);
619            self.uncommitted_actions
620                .push(crate::core::SheetAction::DeleteCol {
621                    sheet_name: self.name.clone(),
622                    index,
623                });
624            self.mark_all_dirty();
625        }
626    }
627
628    /// Insert a new empty column at the specified index
629    /// If index is >= columns.len(), appends at the end
630    pub fn insert_col(&mut self, index: usize) {
631        let row_count = self.row_count();
632        let new_col = DataColumn::new(row_count);
633        let col_count = self.columns.len();
634        if index >= col_count {
635            self.columns.push(new_col);
636            self.uncommitted_actions
637                .push(crate::core::SheetAction::InsertCol {
638                    sheet_name: self.name.clone(),
639                    index: col_count,
640                });
641        } else {
642            self.columns.insert(index, new_col);
643            self.uncommitted_actions
644                .push(crate::core::SheetAction::InsertCol {
645                    sheet_name: self.name.clone(),
646                    index,
647                });
648        }
649        self.mark_all_dirty();
650    }
651
652    /// The sheet's columns.
653    ///
654    /// Read-only: every column must keep the same number of rows, so growing
655    /// or replacing one from outside would desync the sheet. Use
656    /// [`Sheet::insert_col`], [`Sheet::delete_col`] and [`Sheet::extend`] to
657    /// change the shape.
658    pub fn columns(&self) -> &[DataColumn] {
659        &self.columns
660    }
661
662    /// Allocated rows, taken from the first column -- every column has the
663    /// same length.
664    pub fn row_count(&self) -> usize {
665        self.columns.first().map(|c| c.src.len()).unwrap_or(0)
666    }
667
668    /// Allocated columns.
669    pub fn col_count(&self) -> usize {
670        self.columns.len()
671    }
672}