longcipher_leptos_components/components/editor/
state.rs1use serde::{Deserialize, Serialize};
6
7use super::{
8 cursor::{Cursor, CursorPosition, CursorSet},
9 history::History,
10};
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14#[allow(clippy::struct_excessive_bools)]
15pub struct EditorConfig {
16 pub tab_size: usize,
18 pub insert_spaces: bool,
20 pub word_wrap: bool,
22 pub show_line_numbers: bool,
24 pub highlight_current_line: bool,
26 pub show_whitespace: bool,
28 pub match_brackets: bool,
30 pub auto_indent: bool,
32 pub auto_close_brackets: bool,
34 pub font_size: f32,
36 pub line_height: f32,
38 pub max_line_width: usize,
40 pub read_only: bool,
42}
43
44impl Default for EditorConfig {
45 fn default() -> Self {
46 Self {
47 tab_size: 4,
48 insert_spaces: true,
49 word_wrap: true,
50 show_line_numbers: true,
51 highlight_current_line: true,
52 show_whitespace: false,
53 match_brackets: true,
54 auto_indent: true,
55 auto_close_brackets: true,
56 font_size: 14.0,
57 line_height: 1.5,
58 max_line_width: 0,
59 read_only: false,
60 }
61 }
62}
63
64#[derive(Debug, Clone)]
66pub struct EditorState {
67 pub content: String,
69 pub cursors: CursorSet,
71 pub history: History,
73 pub config: EditorConfig,
75 pub version: u64,
77 pub is_modified: bool,
79 pub scroll_line: usize,
81 pub scroll_offset: f32,
83 pub language: Option<String>,
85}
86
87impl Default for EditorState {
88 fn default() -> Self {
89 Self {
90 content: String::new(),
91 cursors: CursorSet::new(Cursor::zero()),
92 history: History::new(),
93 config: EditorConfig::default(),
94 version: 0,
95 is_modified: false,
96 scroll_line: 0,
97 scroll_offset: 0.0,
98 language: None,
99 }
100 }
101}
102
103impl EditorState {
104 #[must_use]
106 pub fn new(content: impl Into<String>) -> Self {
107 Self {
108 content: content.into(),
109 ..Default::default()
110 }
111 }
112
113 #[must_use]
115 pub fn with_config(content: impl Into<String>, config: EditorConfig) -> Self {
116 Self {
117 content: content.into(),
118 config,
119 ..Default::default()
120 }
121 }
122
123 #[must_use]
125 pub fn content(&self) -> &str {
126 &self.content
127 }
128
129 pub fn set_content(&mut self, content: impl Into<String>) {
131 let new_content = content.into();
132 if new_content != self.content {
133 self.history
135 .push(self.content.clone(), self.cursors.clone());
136 self.content = new_content;
137 self.version += 1;
138 self.is_modified = true;
139 }
140 }
141
142 pub fn replace_content(&mut self, content: impl Into<String>) {
144 self.content = content.into();
145 self.version += 1;
146 }
147
148 #[must_use]
150 pub fn cursor_position(&self) -> CursorPosition {
151 self.cursors.primary().head
152 }
153
154 pub fn set_cursor(&mut self, position: CursorPosition) {
156 self.cursors.primary_mut().move_to(position, false);
157 }
158
159 pub fn set_cursor_with_selection(&mut self, head: CursorPosition, anchor: CursorPosition) {
161 let cursor = self.cursors.primary_mut();
162 cursor.head = head;
163 cursor.anchor = anchor;
164 }
165
166 #[must_use]
168 pub fn line_count(&self) -> usize {
169 if self.content.is_empty() {
170 1
171 } else {
172 self.content.chars().filter(|&c| c == '\n').count() + 1
173 }
174 }
175
176 #[must_use]
178 pub fn get_line(&self, index: usize) -> Option<&str> {
179 self.content.lines().nth(index)
180 }
181
182 pub fn insert(&mut self, text: &str) {
184 if self.config.read_only {
185 return;
186 }
187
188 let position = self.cursor_position();
189 if let Some(offset) = self.position_to_offset(position) {
190 self.history
191 .push(self.content.clone(), self.cursors.clone());
192
193 let cursor = self.cursors.primary();
195 if cursor.has_selection() {
196 let (start, end) = (
197 self.position_to_offset(cursor.selection_start()),
198 self.position_to_offset(cursor.selection_end()),
199 );
200 if let (Some(start), Some(end)) = (start, end) {
201 self.content =
202 format!("{}{}{}", &self.content[..start], text, &self.content[end..]);
203 let new_offset = start + text.len();
205 if let Some(new_pos) = self.offset_to_position(new_offset) {
206 self.set_cursor(new_pos);
207 }
208 }
209 } else {
210 self.content.insert_str(offset, text);
212 let new_offset = offset + text.len();
213 if let Some(new_pos) = self.offset_to_position(new_offset) {
214 self.set_cursor(new_pos);
215 }
216 }
217
218 self.version += 1;
219 self.is_modified = true;
220 }
221 }
222
223 pub fn delete_backward(&mut self) {
225 if self.config.read_only {
226 return;
227 }
228
229 let cursor = self.cursors.primary();
230 if cursor.has_selection() {
231 self.delete_selection();
232 return;
233 }
234
235 let position = cursor.head;
236 if let Some(offset) = self.position_to_offset(position) {
237 if offset == 0 {
238 return;
239 }
240
241 self.history
242 .push(self.content.clone(), self.cursors.clone());
243
244 let prev_offset = self.content[..offset]
246 .char_indices()
247 .last()
248 .map_or(0, |(i, _)| i);
249
250 self.content = format!(
251 "{}{}",
252 &self.content[..prev_offset],
253 &self.content[offset..]
254 );
255
256 if let Some(new_pos) = self.offset_to_position(prev_offset) {
257 self.set_cursor(new_pos);
258 }
259
260 self.version += 1;
261 self.is_modified = true;
262 }
263 }
264
265 pub fn delete_forward(&mut self) {
267 if self.config.read_only {
268 return;
269 }
270
271 let cursor = self.cursors.primary();
272 if cursor.has_selection() {
273 self.delete_selection();
274 return;
275 }
276
277 let position = cursor.head;
278 if let Some(offset) = self.position_to_offset(position) {
279 if offset >= self.content.len() {
280 return;
281 }
282
283 self.history
284 .push(self.content.clone(), self.cursors.clone());
285
286 let next_offset = self.content[offset..]
288 .char_indices()
289 .nth(1)
290 .map_or(self.content.len(), |(i, _)| offset + i);
291
292 self.content = format!(
293 "{}{}",
294 &self.content[..offset],
295 &self.content[next_offset..]
296 );
297
298 self.version += 1;
299 self.is_modified = true;
300 }
301 }
302
303 fn delete_selection(&mut self) {
305 let cursor = self.cursors.primary();
306 if !cursor.has_selection() {
307 return;
308 }
309
310 let start_pos = cursor.selection_start();
311 let end_pos = cursor.selection_end();
312
313 if let (Some(start), Some(end)) = (
314 self.position_to_offset(start_pos),
315 self.position_to_offset(end_pos),
316 ) {
317 self.history
318 .push(self.content.clone(), self.cursors.clone());
319
320 self.content = format!("{}{}", &self.content[..start], &self.content[end..]);
321 self.set_cursor(start_pos);
322
323 self.version += 1;
324 self.is_modified = true;
325 }
326 }
327
328 pub fn undo(&mut self) -> bool {
330 if let Some(entry) = self.history.undo(&self.content, &self.cursors) {
331 self.content = entry.content;
332 self.cursors = entry.cursors;
333 self.version += 1;
334 true
335 } else {
336 false
337 }
338 }
339
340 pub fn redo(&mut self) -> bool {
342 if let Some(entry) = self.history.redo(&self.content, &self.cursors) {
343 self.content = entry.content;
344 self.cursors = entry.cursors;
345 self.version += 1;
346 true
347 } else {
348 false
349 }
350 }
351
352 #[must_use]
354 pub fn can_undo(&self) -> bool {
355 self.history.can_undo()
356 }
357
358 #[must_use]
360 pub fn can_redo(&self) -> bool {
361 self.history.can_redo()
362 }
363
364 pub fn mark_saved(&mut self) {
366 self.is_modified = false;
367 }
368
369 #[must_use]
371 pub fn position_to_offset(&self, position: CursorPosition) -> Option<usize> {
372 let mut current_line = 0;
373 let mut offset = 0;
374
375 for (i, ch) in self.content.char_indices() {
376 if current_line == position.line {
377 let line_start = i;
378 let mut col = 0;
379 for (j, c) in self.content[line_start..].char_indices() {
380 if col == position.column {
381 return Some(line_start + j);
382 }
383 if c == '\n' {
384 break;
385 }
386 col += 1;
387 }
388 if col == position.column {
390 return Some(
391 line_start
392 + self.content[line_start..]
393 .find('\n')
394 .unwrap_or(self.content.len() - line_start),
395 );
396 }
397 return None;
398 }
399 if ch == '\n' {
400 current_line += 1;
401 }
402 offset = i + ch.len_utf8();
403 }
404
405 if current_line == position.line && position.column == 0 {
407 return Some(offset);
408 }
409
410 None
411 }
412
413 #[must_use]
415 pub fn offset_to_position(&self, offset: usize) -> Option<CursorPosition> {
416 if offset > self.content.len() {
417 return None;
418 }
419
420 let mut line = 0;
421 let mut col = 0;
422
423 for (i, ch) in self.content.char_indices() {
424 if i >= offset {
425 return Some(CursorPosition::new(line, col));
426 }
427 if ch == '\n' {
428 line += 1;
429 col = 0;
430 } else {
431 col += 1;
432 }
433 }
434
435 Some(CursorPosition::new(line, col))
437 }
438}
439
440#[cfg(test)]
441mod tests {
442 use super::*;
443
444 #[test]
445 fn test_editor_state_new() {
446 let state = EditorState::new("Hello, World!");
447 assert_eq!(state.content(), "Hello, World!");
448 assert_eq!(state.line_count(), 1);
449 assert!(!state.is_modified);
450 }
451
452 #[test]
453 fn test_insert() {
454 let mut state = EditorState::new("");
455 state.insert("Hello");
456 assert_eq!(state.content(), "Hello");
457 assert!(state.is_modified);
458 }
459
460 #[test]
461 fn test_undo_redo() {
462 let mut state = EditorState::new("initial");
463 state.set_content("modified");
464
465 assert!(state.undo());
466 assert_eq!(state.content(), "initial");
467
468 assert!(state.redo());
469 assert_eq!(state.content(), "modified");
470 }
471
472 #[test]
473 fn test_position_offset_conversion() {
474 let state = EditorState::new("hello\nworld\nfoo");
475
476 assert_eq!(state.position_to_offset(CursorPosition::new(0, 0)), Some(0));
477 assert_eq!(state.position_to_offset(CursorPosition::new(1, 0)), Some(6));
478 assert_eq!(
479 state.position_to_offset(CursorPosition::new(2, 0)),
480 Some(12)
481 );
482
483 assert_eq!(state.offset_to_position(0), Some(CursorPosition::new(0, 0)));
484 assert_eq!(state.offset_to_position(6), Some(CursorPosition::new(1, 0)));
485 }
486}