mathypad_core/core/
state.rs

1//! Core application state shared between TUI and web UI
2
3use crate::expression::{evaluate_with_variables, update_line_references_in_text};
4use std::collections::HashMap;
5
6/// Core application state containing text, results, and variables
7/// This is UI-agnostic and can be used by both TUI and web implementations
8#[derive(Debug, Clone)]
9pub struct MathypadCore {
10    /// The text content of each line
11    pub text_lines: Vec<String>,
12    /// Current cursor line position (0-indexed)
13    pub cursor_line: usize,
14    /// Current cursor column position (0-indexed, in characters)
15    pub cursor_col: usize,
16    /// Evaluation results for each line (None means no result or error)
17    pub results: Vec<Option<String>>,
18    /// Variable storage (variable_name -> value_string)
19    pub variables: HashMap<String, String>,
20}
21
22impl Default for MathypadCore {
23    fn default() -> Self {
24        Self {
25            text_lines: vec![String::new()],
26            cursor_line: 0,
27            cursor_col: 0,
28            results: vec![None],
29            variables: HashMap::new(),
30        }
31    }
32}
33
34impl MathypadCore {
35    /// Create a new empty MathypadCore instance
36    pub fn new() -> Self {
37        Self::default()
38    }
39
40    /// Create a MathypadCore from a list of text lines
41    pub fn from_lines(lines: Vec<String>) -> Self {
42        let line_count = lines.len().max(1);
43        let mut core = Self {
44            text_lines: if lines.is_empty() {
45                vec![String::new()]
46            } else {
47                lines
48            },
49            cursor_line: 0,
50            cursor_col: 0,
51            results: vec![None; line_count],
52            variables: HashMap::new(),
53        };
54        core.recalculate_all();
55        core
56    }
57
58    /// Insert a character at the current cursor position
59    pub fn insert_char(&mut self, c: char) {
60        if self.cursor_line < self.text_lines.len() {
61            // Convert cursor position from character index to byte index for insertion
62            let line = &self.text_lines[self.cursor_line];
63            let char_count = line.chars().count();
64
65            // Ensure cursor position is within bounds
66            let safe_cursor_col = self.cursor_col.min(char_count);
67
68            // Find the byte position for character insertion
69            let byte_index = if safe_cursor_col == 0 {
70                0
71            } else if safe_cursor_col >= char_count {
72                line.len()
73            } else {
74                line.char_indices()
75                    .nth(safe_cursor_col)
76                    .map(|(i, _)| i)
77                    .unwrap_or(line.len())
78            };
79
80            self.text_lines[self.cursor_line].insert(byte_index, c);
81            self.cursor_col += 1;
82            self.update_result(self.cursor_line);
83            self.update_sum_above_dependent_lines(self.cursor_line);
84        }
85    }
86
87    /// Delete the character before the cursor
88    pub fn delete_char(&mut self) {
89        if self.cursor_line < self.text_lines.len() {
90            if self.cursor_col > 0 {
91                // Delete character within the current line
92                let line = &mut self.text_lines[self.cursor_line];
93
94                // Find the byte index of the character to delete
95                let char_indices: Vec<_> = line.char_indices().collect();
96                if self.cursor_col > 0 && self.cursor_col <= char_indices.len() {
97                    let char_to_delete_idx = self.cursor_col - 1;
98                    let start_byte = char_indices[char_to_delete_idx].0;
99                    let end_byte = if char_to_delete_idx + 1 < char_indices.len() {
100                        char_indices[char_to_delete_idx + 1].0
101                    } else {
102                        line.len()
103                    };
104                    line.drain(start_byte..end_byte);
105                }
106
107                self.cursor_col -= 1;
108                self.update_result(self.cursor_line);
109                self.update_sum_above_dependent_lines(self.cursor_line);
110            } else if self.cursor_line > 0 {
111                // Delete newline - merge with previous line
112                let current_line = self.text_lines.remove(self.cursor_line);
113                self.cursor_line -= 1;
114                self.cursor_col = self.text_lines[self.cursor_line].chars().count();
115                self.text_lines[self.cursor_line].push_str(&current_line);
116
117                // Remove the corresponding result
118                self.results.remove(self.cursor_line + 1);
119
120                // Update all affected line references
121                self.update_line_references_for_deletion(self.cursor_line + 1);
122                self.recalculate_all();
123            }
124        }
125    }
126
127    /// Insert a new line at the current cursor position
128    pub fn new_line(&mut self) {
129        if self.cursor_line < self.text_lines.len() {
130            let line = &self.text_lines[self.cursor_line];
131            let char_count = line.chars().count();
132            let safe_cursor_col = self.cursor_col.min(char_count);
133
134            // Find the byte position for splitting
135            let byte_index = if safe_cursor_col == 0 {
136                0
137            } else if safe_cursor_col >= char_count {
138                line.len()
139            } else {
140                line.char_indices()
141                    .nth(safe_cursor_col)
142                    .map(|(i, _)| i)
143                    .unwrap_or(line.len())
144            };
145
146            // Split the line at the cursor position
147            let remaining = self.text_lines[self.cursor_line].split_off(byte_index);
148
149            // Insert the new line
150            self.cursor_line += 1;
151            self.text_lines.insert(self.cursor_line, remaining);
152            self.cursor_col = 0;
153
154            // Insert corresponding result placeholder
155            self.results.insert(self.cursor_line, None);
156
157            // Update line references for insertion
158            self.update_line_references_for_insertion(self.cursor_line);
159            self.recalculate_all();
160        }
161    }
162
163    /// Update the result for a specific line
164    pub fn update_result(&mut self, line_index: usize) {
165        if line_index < self.text_lines.len() {
166            let line_text = &self.text_lines[line_index];
167
168            // Evaluate the expression with current variables and other line results
169            let (result, variable_assignment) =
170                evaluate_with_variables(line_text, &self.variables, &self.results, line_index);
171
172            // Handle variable assignment if present
173            if let Some((var_name, var_value)) = variable_assignment {
174                self.variables.insert(var_name, var_value);
175            }
176
177            // Ensure results vector is large enough
178            while self.results.len() <= line_index {
179                self.results.push(None);
180            }
181
182            // Store the result
183            self.results[line_index] = result;
184        }
185    }
186
187    /// Recalculate all results and variables
188    pub fn recalculate_all(&mut self) {
189        // Clear variables and recalculate from scratch
190        self.variables.clear();
191
192        // Ensure results vector matches text lines
193        self.results.resize(self.text_lines.len(), None);
194
195        // Evaluate each line in order
196        for i in 0..self.text_lines.len() {
197            self.update_result(i);
198        }
199    }
200
201    /// Update line references after a line insertion
202    fn update_line_references_for_insertion(&mut self, inserted_at: usize) {
203        for (i, line) in self.text_lines.iter_mut().enumerate() {
204            if i != inserted_at {
205                *line = update_line_references_in_text(line, inserted_at, 1);
206            }
207        }
208    }
209
210    /// Update line references after a line deletion
211    fn update_line_references_for_deletion(&mut self, deleted_at: usize) {
212        for line in self.text_lines.iter_mut() {
213            *line = update_line_references_in_text(line, deleted_at, -1);
214        }
215    }
216
217    /// Check if a line contains a sum_above() function call
218    fn line_contains_sum_above(&self, line_text: &str) -> bool {
219        // Simple check for sum_above() - could be more sophisticated
220        // but this catches the common case
221        line_text.to_lowercase().contains("sum_above(")
222    }
223
224    /// Update all lines below the given line that contain sum_above()
225    fn update_sum_above_dependent_lines(&mut self, changed_line: usize) {
226        // Update all lines below the current line that contain sum_above()
227        for line_index in (changed_line + 1)..self.text_lines.len() {
228            if self.line_contains_sum_above(&self.text_lines[line_index]) {
229                self.update_result(line_index);
230            }
231        }
232    }
233
234    /// Move cursor to a specific position
235    pub fn move_cursor_to(&mut self, line: usize, col: usize) {
236        self.cursor_line = line.min(self.text_lines.len().saturating_sub(1));
237        if self.cursor_line < self.text_lines.len() {
238            let max_col = self.text_lines[self.cursor_line].chars().count();
239            self.cursor_col = col.min(max_col);
240        }
241    }
242
243    /// Get the current line content
244    pub fn current_line(&self) -> &str {
245        if self.cursor_line < self.text_lines.len() {
246            &self.text_lines[self.cursor_line]
247        } else {
248            ""
249        }
250    }
251
252    /// Get the result for the current line
253    pub fn current_result(&self) -> Option<&str> {
254        if self.cursor_line < self.results.len() {
255            self.results[self.cursor_line].as_deref()
256        } else {
257            None
258        }
259    }
260
261    /// Set text content from a string (splitting into lines)
262    pub fn set_content(&mut self, content: &str) {
263        if content.is_empty() {
264            self.text_lines = vec![String::new()];
265        } else {
266            // Preserve trailing newlines by checking if content ends with newline
267            let mut lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
268
269            // If content ends with newline, add an empty line to represent it
270            if content.ends_with('\n') {
271                lines.push(String::new());
272            }
273
274            self.text_lines = lines;
275        }
276
277        self.cursor_line = 0;
278        self.cursor_col = 0;
279        self.results = vec![None; self.text_lines.len()];
280        self.variables.clear();
281        self.recalculate_all();
282    }
283
284    /// Get content as a single string
285    pub fn get_content(&self) -> String {
286        if self.text_lines.len() == 1 && self.text_lines[0].is_empty() {
287            // Special case: single empty line means empty content
288            String::new()
289        } else if self.text_lines.len() > 1
290            && self
291                .text_lines
292                .last()
293                .map(|s| s.is_empty())
294                .unwrap_or(false)
295        {
296            // Multiple lines with empty last line = trailing newline
297            let content_lines = &self.text_lines[..self.text_lines.len() - 1];
298            let mut result = content_lines.join("\n");
299            result.push('\n'); // Always add trailing newline when we have multiple lines
300            result
301        } else {
302            // Normal case: just join with newlines
303            self.text_lines.join("\n")
304        }
305    }
306
307    /// Update content with line reference updating (for incremental edits)
308    /// This detects line insertions/deletions and updates references accordingly
309    pub fn update_content_with_line_references(&mut self, new_content: &str) {
310        // Get current state
311        let old_lines = self.text_lines.clone();
312
313        // Set the new content first
314        self.set_content(new_content);
315
316        // Detect what changed and update line references
317        let new_line_count = self.text_lines.len();
318        let old_line_count = old_lines.len();
319
320        if new_line_count > old_line_count {
321            // Lines were inserted - we need to figure out where
322            // For now, assume insertion happened at the end or find the first difference
323            for i in 0..old_line_count.min(new_line_count) {
324                if i >= old_lines.len()
325                    || i >= self.text_lines.len()
326                    || old_lines[i] != self.text_lines[i]
327                {
328                    // Found first difference - line was likely inserted here
329                    self.update_line_references_for_insertion(i);
330                    break;
331                }
332            }
333            // If no difference found in existing lines, insertion was at the end
334            if old_line_count > 0 && new_line_count > old_line_count {
335                let lines_added = new_line_count - old_line_count;
336                for _ in 0..lines_added {
337                    self.update_line_references_for_insertion(old_line_count);
338                }
339            }
340        } else if new_line_count < old_line_count {
341            // Lines were deleted
342            let lines_deleted = old_line_count - new_line_count;
343            for i in 0..lines_deleted {
344                // Assume deletion happened at the point where content differs
345                // For simplicity, assume deletion at the end for now
346                self.update_line_references_for_deletion(new_line_count + i);
347            }
348        }
349
350        // Recalculate everything after line reference updates
351        self.recalculate_all();
352    }
353}