Skip to content

Commit fabb869

Browse files
refactor: absolute refs, FormulaError enum, sheet tabs, split state.rs
- Absolute cell references (`$A$1`): lexer/parser recognize `$` prefixes; shift_ref helpers preserve absolute components during autofill/paste. - FormulaError enum replaces String errors across the evaluator; cells now display specific Excel-style codes (`#DIV/0!`, `#REF!`, `#NAME?`, `#N/A`, `#NETWORK!`) instead of a generic `#ERROR`. - UI: dedicated sheet tab strip (ratatui Tabs) above the grid. - Split src/application/state.rs (3600 lines) into focused submodules: io, autofill, editing, clipboard, search, formatting, command.
1 parent eacb14b commit fabb869

13 files changed

Lines changed: 1972 additions & 1702 deletions

File tree

README.md

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -165,20 +165,21 @@ TSHTS supports a powerful multi-type formula system that handles both numbers an
165165
=TRIM(" spaces ") → spaces (remove leading/trailing spaces)
166166
```
167167

168-
#### String Extraction (0-based indexing)
168+
#### String Extraction (1-based, Excel-compatible)
169169
```
170170
=LEFT("Hello World", 5) → Hello (first 5 characters)
171171
=RIGHT("Hello World", 5) → World (last 5 characters)
172-
=MID("Hello World", 6, 5) → World (5 chars starting at position 6)
173-
=FIND("lo", "Hello") → 3 (position of "lo" in "Hello")
174-
=FIND("World", "Hello World") → 6 (position of "World")
172+
=MID("Hello World", 7, 5) → World (5 chars starting at position 7)
173+
=FIND("lo", "Hello") → 4 (case-sensitive; 'l' of "lo" is at position 4)
174+
=FIND("World", "Hello World") → 7
175+
=SEARCH("WORLD", "Hello World") → 7 (case-insensitive FIND)
175176
```
176177

177178
#### Advanced String Operations
178179
```
179180
=CONCAT("A", "B", "C") → ABC (concatenate multiple values)
180181
=CONCAT("Number: ", 123) → Number: 123
181-
=FIND("text", A1, 3) → Find "text" in A1 starting from position 3
182+
=FIND("text", A1, 4) → Find "text" in A1 starting from position 4
182183
```
183184

184185
### 🌐 Web Functions
@@ -217,6 +218,17 @@ TSHTS supports a powerful multi-type formula system that handles both numbers an
217218
=CONCAT(A1:A3) → Concatenate all values in range A1:A3
218219
```
219220

221+
#### Absolute vs Relative References
222+
Use `$` to lock a component so it doesn't shift when the formula is copied,
223+
pasted, or when rows/columns are inserted.
224+
```
225+
=A1 → fully relative (both row and column shift)
226+
=$A1 → column A is locked, row shifts
227+
=A$1 → row 1 is locked, column shifts
228+
=$A$1 → both locked — always the same cell
229+
=SUM($A$1:B2) → mix of absolute and relative in a range
230+
```
231+
220232
### 🔄 Type Conversion
221233
TSHTS automatically handles type conversion:
222234
- **Numeric operations**: Strings are converted to numbers (empty/invalid = 0)
@@ -249,8 +261,8 @@ TSHTS automatically handles type conversion:
249261

250262
### ⚠️ Important Notes
251263

252-
- **String Indexing**: All string functions use 0-based indexing (FIND, MID, etc.)
253-
- **Case Sensitivity**: String comparisons are case-sensitive
264+
- **String Indexing**: String functions use 1-based Excel-compatible indexing (position 1 is the first character)
265+
- **Case Sensitivity**: String comparisons and `FIND` are case-sensitive; use `SEARCH` for case-insensitive matching
254266
- **Error Handling**: Invalid operations return `#ERROR`
255267
- **Empty Strings**: `""` is considered different from empty cells
256268
- **Quotes in Strings**: Use double quotes to escape: `"Quote""Test"``Quote"Test`

src/application/state/autofill.rs

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
//! Autofill logic for detecting and extending patterns across a selection.
2+
3+
use crate::domain::CellData;
4+
use super::App;
5+
6+
impl App {
7+
/// Autofills the selected range based on detected pattern.
8+
///
9+
/// Analyzes the existing cells in the selection to detect a pattern,
10+
/// then fills empty cells in the selection with the continued pattern.
11+
///
12+
/// Fill direction is determined by selection shape:
13+
/// - Tall selection (rows > cols): Fill down (column-wise pattern)
14+
/// - Wide selection (cols > rows): Fill right (row-wise pattern)
15+
/// - Square: Default to fill down
16+
pub fn autofill_selection(&mut self) {
17+
if let Some(((start_row, start_col), (end_row, end_col))) = self.get_selection_range() {
18+
let num_rows = end_row - start_row + 1;
19+
let num_cols = end_col - start_col + 1;
20+
21+
let fill_down = num_rows >= num_cols;
22+
23+
let mut changes = Vec::new();
24+
let mut pattern_desc = String::new();
25+
26+
if fill_down {
27+
for col in start_col..=end_col {
28+
let (filled, desc) = self.autofill_column(start_row, end_row, col);
29+
changes.extend(filled);
30+
if pattern_desc.is_empty() && !desc.is_empty() {
31+
pattern_desc = desc;
32+
}
33+
}
34+
} else {
35+
for row in start_row..=end_row {
36+
let (filled, desc) = self.autofill_row(row, start_col, end_col);
37+
changes.extend(filled);
38+
if pattern_desc.is_empty() && !desc.is_empty() {
39+
pattern_desc = desc;
40+
}
41+
}
42+
}
43+
44+
let num_changes = changes.len();
45+
for (row, col, cell_data) in changes {
46+
self.set_cell_with_undo(row, col, cell_data);
47+
}
48+
49+
if num_changes > 0 {
50+
self.status_message = Some(format!(
51+
"Autofilled {} cells using {}",
52+
num_changes,
53+
pattern_desc
54+
));
55+
} else {
56+
self.status_message = Some("No cells to fill".to_string());
57+
}
58+
}
59+
}
60+
61+
/// Autofill a single column from start_row to end_row.
62+
fn autofill_column(&self, start_row: usize, end_row: usize, col: usize) -> (Vec<(usize, usize, CellData)>, String) {
63+
use crate::domain::services::{FormulaEvaluator, AutofillPattern};
64+
65+
let mut changes = Vec::new();
66+
67+
let mut pattern_cells: Vec<(usize, CellData)> = Vec::new();
68+
let mut target_rows: Vec<usize> = Vec::new();
69+
70+
for row in start_row..=end_row {
71+
let cell = self.workbook.current_sheet().get_cell(row, col);
72+
if !cell.value.is_empty() || cell.formula.is_some() {
73+
pattern_cells.push((row, cell.clone()));
74+
} else {
75+
target_rows.push(row);
76+
}
77+
}
78+
79+
if pattern_cells.is_empty() || target_rows.is_empty() {
80+
return (changes, String::new());
81+
}
82+
83+
let has_formula = pattern_cells.iter().any(|(_, cell)| cell.formula.is_some());
84+
85+
if has_formula {
86+
let (source_row, source_cell) = pattern_cells.iter()
87+
.find(|(_, cell)| cell.formula.is_some())
88+
.unwrap();
89+
90+
let evaluator = FormulaEvaluator::new(self.workbook.current_sheet());
91+
92+
for target_row in &target_rows {
93+
let row_offset = *target_row as i32 - *source_row as i32;
94+
95+
if let Some(ref formula) = source_cell.formula {
96+
let adjusted_formula = evaluator.adjust_formula_references(formula, row_offset, 0);
97+
98+
if evaluator.would_create_circular_reference(&adjusted_formula, (*target_row, col)) {
99+
continue;
100+
}
101+
102+
let new_value = evaluator.evaluate_formula(&adjusted_formula);
103+
changes.push((*target_row, col, CellData {
104+
value: new_value,
105+
formula: Some(adjusted_formula),
106+
format: None,
107+
comment: None,
108+
}));
109+
}
110+
}
111+
112+
return (changes, "formula".to_string());
113+
}
114+
115+
let values: Vec<String> = pattern_cells.iter()
116+
.map(|(_, cell)| cell.value.clone())
117+
.collect();
118+
119+
let pattern = AutofillPattern::detect(&values);
120+
let pattern_desc = pattern.description();
121+
122+
let pattern_len = pattern_cells.len();
123+
124+
for (i, target_row) in target_rows.iter().enumerate() {
125+
let pattern_index = pattern_len + i;
126+
let generated_value = pattern.generate(pattern_index);
127+
128+
changes.push((*target_row, col, CellData {
129+
value: generated_value,
130+
formula: None,
131+
format: None,
132+
comment: None,
133+
}));
134+
}
135+
136+
(changes, pattern_desc)
137+
}
138+
139+
/// Autofill a single row from start_col to end_col.
140+
fn autofill_row(&self, row: usize, start_col: usize, end_col: usize) -> (Vec<(usize, usize, CellData)>, String) {
141+
use crate::domain::services::{FormulaEvaluator, AutofillPattern};
142+
143+
let mut changes = Vec::new();
144+
145+
let mut pattern_cells: Vec<(usize, CellData)> = Vec::new();
146+
let mut target_cols: Vec<usize> = Vec::new();
147+
148+
for col in start_col..=end_col {
149+
let cell = self.workbook.current_sheet().get_cell(row, col);
150+
if !cell.value.is_empty() || cell.formula.is_some() {
151+
pattern_cells.push((col, cell.clone()));
152+
} else {
153+
target_cols.push(col);
154+
}
155+
}
156+
157+
if pattern_cells.is_empty() || target_cols.is_empty() {
158+
return (changes, String::new());
159+
}
160+
161+
let has_formula = pattern_cells.iter().any(|(_, cell)| cell.formula.is_some());
162+
163+
if has_formula {
164+
let (source_col, source_cell) = pattern_cells.iter()
165+
.find(|(_, cell)| cell.formula.is_some())
166+
.unwrap();
167+
168+
let evaluator = FormulaEvaluator::new(self.workbook.current_sheet());
169+
170+
for target_col in &target_cols {
171+
let col_offset = *target_col as i32 - *source_col as i32;
172+
173+
if let Some(ref formula) = source_cell.formula {
174+
let adjusted_formula = evaluator.adjust_formula_references(formula, 0, col_offset);
175+
176+
if evaluator.would_create_circular_reference(&adjusted_formula, (row, *target_col)) {
177+
continue;
178+
}
179+
180+
let new_value = evaluator.evaluate_formula(&adjusted_formula);
181+
changes.push((row, *target_col, CellData {
182+
value: new_value,
183+
formula: Some(adjusted_formula),
184+
format: None,
185+
comment: None,
186+
}));
187+
}
188+
}
189+
190+
return (changes, "formula".to_string());
191+
}
192+
193+
let values: Vec<String> = pattern_cells.iter()
194+
.map(|(_, cell)| cell.value.clone())
195+
.collect();
196+
197+
let pattern = AutofillPattern::detect(&values);
198+
let pattern_desc = pattern.description();
199+
200+
let pattern_len = pattern_cells.len();
201+
202+
for (i, target_col) in target_cols.iter().enumerate() {
203+
let pattern_index = pattern_len + i;
204+
let generated_value = pattern.generate(pattern_index);
205+
206+
changes.push((row, *target_col, CellData {
207+
value: generated_value,
208+
formula: None,
209+
format: None,
210+
comment: None,
211+
}));
212+
}
213+
214+
(changes, pattern_desc)
215+
}
216+
}

0 commit comments

Comments
 (0)