mathypad_core/core/
state.rs1use crate::expression::{evaluate_with_variables, update_line_references_in_text};
4use std::collections::HashMap;
5
6#[derive(Debug, Clone)]
9pub struct MathypadCore {
10 pub text_lines: Vec<String>,
12 pub cursor_line: usize,
14 pub cursor_col: usize,
16 pub results: Vec<Option<String>>,
18 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 pub fn new() -> Self {
37 Self::default()
38 }
39
40 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 pub fn insert_char(&mut self, c: char) {
60 if self.cursor_line < self.text_lines.len() {
61 let line = &self.text_lines[self.cursor_line];
63 let char_count = line.chars().count();
64
65 let safe_cursor_col = self.cursor_col.min(char_count);
67
68 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 pub fn delete_char(&mut self) {
89 if self.cursor_line < self.text_lines.len() {
90 if self.cursor_col > 0 {
91 let line = &mut self.text_lines[self.cursor_line];
93
94 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 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(¤t_line);
116
117 self.results.remove(self.cursor_line + 1);
119
120 self.update_line_references_for_deletion(self.cursor_line + 1);
122 self.recalculate_all();
123 }
124 }
125 }
126
127 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 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 let remaining = self.text_lines[self.cursor_line].split_off(byte_index);
148
149 self.cursor_line += 1;
151 self.text_lines.insert(self.cursor_line, remaining);
152 self.cursor_col = 0;
153
154 self.results.insert(self.cursor_line, None);
156
157 self.update_line_references_for_insertion(self.cursor_line);
159 self.recalculate_all();
160 }
161 }
162
163 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 let (result, variable_assignment) =
170 evaluate_with_variables(line_text, &self.variables, &self.results, line_index);
171
172 if let Some((var_name, var_value)) = variable_assignment {
174 self.variables.insert(var_name, var_value);
175 }
176
177 while self.results.len() <= line_index {
179 self.results.push(None);
180 }
181
182 self.results[line_index] = result;
184 }
185 }
186
187 pub fn recalculate_all(&mut self) {
189 self.variables.clear();
191
192 self.results.resize(self.text_lines.len(), None);
194
195 for i in 0..self.text_lines.len() {
197 self.update_result(i);
198 }
199 }
200
201 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 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 fn line_contains_sum_above(&self, line_text: &str) -> bool {
219 line_text.to_lowercase().contains("sum_above(")
222 }
223
224 fn update_sum_above_dependent_lines(&mut self, changed_line: usize) {
226 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 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 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 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 pub fn set_content(&mut self, content: &str) {
263 if content.is_empty() {
264 self.text_lines = vec![String::new()];
265 } else {
266 let mut lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
268
269 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 pub fn get_content(&self) -> String {
286 if self.text_lines.len() == 1 && self.text_lines[0].is_empty() {
287 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 let content_lines = &self.text_lines[..self.text_lines.len() - 1];
298 let mut result = content_lines.join("\n");
299 result.push('\n'); result
301 } else {
302 self.text_lines.join("\n")
304 }
305 }
306
307 pub fn update_content_with_line_references(&mut self, new_content: &str) {
310 let old_lines = self.text_lines.clone();
312
313 self.set_content(new_content);
315
316 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 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 self.update_line_references_for_insertion(i);
330 break;
331 }
332 }
333 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 let lines_deleted = old_line_count - new_line_count;
343 for i in 0..lines_deleted {
344 self.update_line_references_for_deletion(new_line_count + i);
347 }
348 }
349
350 self.recalculate_all();
352 }
353}