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