1use std::{
2 fmt,
3 fs::File,
4 io::{self, Write},
5 path::{Path, PathBuf},
6};
7
8use ratatui::layout::Size;
9
10use crate::{
11 config::Symbols,
12 note_editor::{
13 ast::{self},
14 cursor::{self, Cursor},
15 parser,
16 rich_text::RichText,
17 text_buffer::TextBuffer,
18 viewport::Viewport,
19 virtual_document::VirtualDocument,
20 },
21};
22
23#[derive(Clone, Copy, Debug, Default, PartialEq)]
24pub enum EditMode {
25 #[default]
26 Source,
28 }
32
33#[derive(Clone, Copy, Debug, Default, PartialEq)]
34pub enum View {
35 #[default]
36 Read,
37 Edit(EditMode),
38}
39
40impl fmt::Display for View {
41 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
42 match self {
43 View::Read => write!(f, "READ"),
44 View::Edit(..) => write!(f, "EDIT"),
45 }
46 }
47}
48
49#[derive(Clone, Debug, Default)]
50pub struct NoteEditorState<'a> {
51 pub content: String,
53 pub view: View,
54 pub cursor: Cursor,
55 pub ast_nodes: Vec<ast::Node>,
56 pub virtual_document: VirtualDocument<'a>,
57 pub symbols: Symbols,
58 filepath: PathBuf,
59 filename: String,
60 active: bool,
61 insert_mode: bool,
62 vim_mode: bool,
63 editor_enabled: bool,
64 modified: bool,
65 viewport: Viewport,
66 text_buffer: Option<TextBuffer>,
67 editing_block: Option<usize>,
71}
72
73impl<'a> NoteEditorState<'a> {
74 pub fn new(content: &str, filename: &str, filepath: &Path, symbols: &Symbols) -> Self {
75 let ast_nodes = parser::from_str(content);
76 let content = content.to_string();
77 Self {
78 text_buffer: None,
79 content: content.clone(),
80 view: View::Read,
81 cursor: Cursor::default(),
82 viewport: Viewport::default(),
83 symbols: symbols.clone(),
84 virtual_document: VirtualDocument::new(symbols),
85 filename: filename.to_string(),
86 filepath: filepath.to_path_buf(),
87 ast_nodes,
88 active: false,
89 insert_mode: false,
90 vim_mode: false,
91 editor_enabled: false,
92 modified: false,
93 editing_block: None,
94 }
95 }
96
97 pub fn viewport(&self) -> &Viewport {
98 &self.viewport
99 }
100
101 pub fn is_editing(&self) -> bool {
102 matches!(self.view, View::Edit(..))
103 }
104
105 pub fn insert_mode(&self) -> bool {
106 self.insert_mode
107 }
108
109 pub fn set_insert_mode(&mut self, mode: bool) {
110 self.insert_mode = mode;
111 }
112
113 pub fn vim_mode(&self) -> bool {
114 self.vim_mode
115 }
116
117 pub fn set_vim_mode(&mut self, mode: bool) {
118 self.vim_mode = mode;
119 }
120
121 pub fn editor_enabled(&self) -> bool {
122 self.editor_enabled
123 }
124
125 pub fn set_editor_enabled(&mut self, enabled: bool) {
126 self.editor_enabled = enabled;
127 }
128
129 pub fn text_buffer(&self) -> Option<&TextBuffer> {
130 self.text_buffer.as_ref()
131 }
132
133 pub fn enter_insert(&mut self, block_idx: usize) {
134 self.commit_text_buffer();
136
137 self.editing_block = Some(block_idx);
138 if let Some(node) = self.ast_nodes.get(block_idx) {
139 let source_range = node.source_range();
140 if let Some(content) = self.content.get(source_range.clone()) {
141 self.text_buffer = Some(TextBuffer::new(content, source_range.clone()));
142 }
143 } else if self.content.is_empty() {
144 let empty_node = ast::Node::Paragraph {
147 text: RichText::empty(),
148 source_range: 0..0,
149 };
150 self.text_buffer = Some(TextBuffer::new("", empty_node.source_range().clone()));
151 self.ast_nodes.push(empty_node);
152 }
153 }
154
155 pub fn commit_text_buffer(&mut self) -> bool {
158 if let Some(buffer) = self.text_buffer() {
159 let new_content = buffer.write(&self.content);
160 if self.content != new_content {
161 self.content = new_content;
162 self.ast_nodes = parser::from_str(&self.content);
163 self.modified = true;
164 return true;
165 }
166 }
167 false
168 }
169
170 pub fn exit_insert(&mut self) {
171 if matches!(self.view, View::Read) {
172 return;
173 }
174
175 self.commit_text_buffer();
176 self.text_buffer = None;
177 self.editing_block = None;
178 }
179
180 pub fn set_filename(&mut self, name: &str) {
181 self.filename = name.to_string();
182 }
183
184 pub fn set_filepath(&mut self, path: &Path) {
185 self.filepath = path.to_path_buf();
186 }
187
188 pub fn insert_char(&mut self, c: char) {
189 if let Some(buffer) = &mut self.text_buffer {
190 let insertion_offset = self.cursor.source_offset();
191 buffer.insert_char(c, insertion_offset);
192
193 let char_byte_len = c.len_utf8();
195 self.shift_source_ranges(insertion_offset, char_byte_len as isize);
196
197 self.update_layout();
198
199 self.cursor.update(
201 cursor::Message::Jump(insertion_offset + char_byte_len),
202 self.virtual_document.lines(),
203 &self.text_buffer,
204 );
205
206 self.ensure_cursor_visible();
207 }
208 }
209
210 pub fn delete_char(&mut self) {
211 if let Some(buffer) = &mut self.text_buffer {
212 if buffer.source_range.start == self.cursor.source_offset() {
213 } else {
218 let deletion_offset = self.cursor.source_offset();
219 if let Some(char_byte_len) = buffer.delete_char(deletion_offset) {
220 self.shift_source_ranges(deletion_offset, -(char_byte_len as isize));
222
223 self.update_layout();
224
225 let new_cursor_pos = deletion_offset.saturating_sub(char_byte_len);
227 self.cursor.update(
228 cursor::Message::Jump(new_cursor_pos),
229 self.virtual_document.lines(),
230 &self.text_buffer,
231 );
232
233 self.ensure_cursor_visible();
234 }
235 }
236 }
237 }
238
239 pub fn active(&self) -> bool {
240 self.active
241 }
242
243 pub fn current_block(&self) -> usize {
244 *self
245 .virtual_document
246 .line_to_block()
247 .get(self.cursor.virtual_row())
248 .unwrap_or(&0)
249 }
250
251 pub fn set_view(&mut self, view: View) {
252 let block_idx = self.current_block();
253
254 self.view = view;
255
256 use cursor::Message::*;
257
258 match self.view {
259 View::Read => {
260 self.exit_insert();
261 self.update_layout();
262 self.cursor.update(
263 SwitchMode(cursor::CursorMode::Read),
264 self.virtual_document.lines(),
265 &None,
266 );
267 }
268 View::Edit(..) => {
269 self.enter_insert(block_idx);
270 self.update_layout();
271 self.cursor.update(
272 SwitchMode(cursor::CursorMode::Edit),
273 self.virtual_document.lines(),
274 &self.text_buffer,
275 );
276 }
277 }
278 }
279
280 pub fn resize_viewport(&mut self, size: Size) {
281 if self.viewport.size_changed(size) {
282 use cursor::Message::*;
283
284 let current_block_idx = self.editing_block;
285
286 self.virtual_document.layout(
287 &self.filename,
288 &self.content,
289 &self.view,
290 current_block_idx,
291 &self.ast_nodes,
292 size.width.into(),
293 self.text_buffer.clone(),
294 );
295
296 self.viewport.resize(size);
297
298 self.cursor.update(
299 Jump(self.cursor.source_offset()),
300 self.virtual_document.lines(),
301 &self.text_buffer,
302 );
303
304 self.ensure_cursor_visible();
305 }
306 }
307
308 fn ensure_cursor_visible(&mut self) {
312 let cursor_row = self.cursor.virtual_row() as i32;
313 let viewport_top = self.viewport.top() as i32;
314 let viewport_bottom = self.viewport.bottom() as i32;
315 let meta_len = self.virtual_document.meta().len() as i32;
316
317 let effective_bottom = viewport_bottom.saturating_sub(meta_len);
318
319 if cursor_row < viewport_top {
320 let scroll_offset = cursor_row - viewport_top;
321 self.viewport.scroll_by((scroll_offset, 0));
322 } else if cursor_row >= effective_bottom {
323 let scroll_offset = cursor_row - effective_bottom + 1;
324 self.viewport.scroll_by((scroll_offset, 0));
325 }
326 }
327
328 pub fn set_active(&mut self, active: bool) {
329 self.active = active;
330 }
331
332 pub fn modified(&self) -> bool {
333 self.modified || self.text_buffer().is_some_and(|buffer| buffer.modified)
334 }
335
336 pub fn cursor_word_forward(&mut self) {
337 use cursor::Message::*;
338
339 self.cursor.update(
340 MoveWordForward,
341 self.virtual_document.lines(),
342 &self.text_buffer,
343 );
344
345 self.ensure_cursor_visible();
346 }
347
348 pub fn cursor_word_backward(&mut self) {
349 use cursor::Message::*;
350
351 self.cursor.update(
352 MoveWordBackward,
353 self.virtual_document.lines(),
354 &self.text_buffer,
355 );
356
357 self.ensure_cursor_visible();
358 }
359
360 pub fn cursor_left(&mut self, amount: usize) {
361 use cursor::Message::*;
362
363 self.cursor.update(
364 MoveLeft(amount),
365 self.virtual_document.lines(),
366 &self.text_buffer,
367 );
368
369 self.ensure_cursor_visible();
370 }
371
372 pub fn cursor_right(&mut self, amount: usize) {
373 use cursor::Message::*;
374
375 self.cursor.update(
376 MoveRight(amount),
377 self.virtual_document.lines(),
378 &self.text_buffer,
379 );
380
381 self.ensure_cursor_visible();
382 }
383
384 pub fn cursor_to_end(&mut self) {
385 let last_block = self.virtual_document.blocks().len().saturating_sub(1);
386 self.cursor_jump(last_block);
387 self.cursor_down(usize::MAX);
390 }
391
392 pub fn cursor_jump(&mut self, idx: usize) {
393 let prev_block_idx = self.current_block();
394
395 if let Some(block) = self.virtual_document.blocks().get(idx) {
396 self.cursor.update(
397 cursor::Message::Jump(block.source_range.start),
398 self.virtual_document.lines(),
399 &self.text_buffer,
400 );
401 }
402
403 self.relayout_on_block_change(prev_block_idx);
404 self.ensure_cursor_visible();
405 }
406
407 pub fn update_layout(&mut self) {
408 use cursor::Message::*;
409
410 if matches!(self.view, View::Edit(..)) && self.text_buffer.is_none() {
414 let block_idx = self.current_block();
415 self.enter_insert(block_idx);
416 self.cursor.update(
417 SwitchMode(cursor::CursorMode::Edit),
418 self.virtual_document.lines(),
419 &self.text_buffer,
420 );
421 }
422
423 let current_block_idx = self.editing_block;
424
425 self.virtual_document.layout(
426 &self.filename,
427 &self.content,
428 &self.view,
429 current_block_idx,
430 &self.ast_nodes,
431 self.viewport.area().width.into(),
432 self.text_buffer.clone(),
433 );
434
435 self.cursor.update(
436 Jump(self.cursor.source_offset()),
437 self.virtual_document.lines(),
438 &self.text_buffer,
439 );
440 }
441
442 pub fn cursor_up(&mut self, amount: usize) {
443 let prev_block_idx = self.current_block();
444
445 self.cursor.update(
446 cursor::Message::MoveUp(amount),
447 self.virtual_document.lines(),
448 &self.text_buffer,
449 );
450
451 self.relayout_on_block_change(prev_block_idx);
452 self.ensure_cursor_visible();
453 }
454
455 pub fn cursor_down(&mut self, amount: usize) {
456 let prev_block_idx = self.current_block();
457
458 self.cursor.update(
459 cursor::Message::MoveDown(amount),
460 self.virtual_document.lines(),
461 &self.text_buffer,
462 );
463
464 self.relayout_on_block_change(prev_block_idx);
465 self.ensure_cursor_visible();
466 }
467
468 fn relayout_on_block_change(&mut self, prev_block_idx: usize) {
479 if !matches!(self.view, View::Edit(..)) {
480 return;
481 }
482
483 let target_block_idx = self.current_block();
484 if target_block_idx == prev_block_idx {
485 return;
486 }
487
488 let adjacent = prev_block_idx.abs_diff(target_block_idx) == 1;
489 let moved_up = target_block_idx < prev_block_idx;
490 let use_end = adjacent && moved_up;
491
492 let target_offset = self.ast_nodes.get(target_block_idx).map(|node| {
493 let range = node.source_range();
494 if use_end {
495 range.end.saturating_sub(1).max(range.start)
496 } else {
497 range.start
498 }
499 });
500
501 self.enter_insert(target_block_idx);
502
503 self.virtual_document.layout(
504 &self.filename,
505 &self.content,
506 &self.view,
507 self.editing_block,
508 &self.ast_nodes,
509 self.viewport.area().width.into(),
510 self.text_buffer.clone(),
511 );
512
513 if let Some(offset) = target_offset {
514 self.cursor.update(
515 cursor::Message::Jump(offset),
516 self.virtual_document.lines(),
517 &self.text_buffer,
518 );
519 }
520 }
521
522 pub fn save_to_file(&mut self) -> io::Result<()> {
523 if self.modified() {
524 let mut file = File::create(&self.filepath)?;
525 file.write_all(self.content.as_bytes())?;
526 self.modified = false;
527 }
528 Ok(())
529 }
530
531 fn shift_source_ranges(&mut self, offset: usize, shift: isize) {
533 self.shift_nodes(offset, shift);
534 }
535
536 fn shift_nodes(&mut self, offset: usize, shift: isize) {
542 let shift_value = |v: usize| v.checked_add_signed(shift).unwrap_or(0);
543
544 let nodes = self
546 .ast_nodes
547 .iter_mut()
548 .filter(|node| node.source_range().end > offset);
549
550 nodes.for_each(|node| {
551 let range = node.source_range();
552 let shifted_range = if range.start > offset {
553 shift_value(range.start)..shift_value(range.end)
554 } else {
555 range.start..shift_value(range.end)
556 };
557 node.set_source_range(shifted_range);
558 });
559 }
560}
561
562#[cfg(test)]
563mod tests {
564 use super::*;
565 use ratatui::layout::Size;
566 use std::path::Path;
567
568 fn assert_cursor_visible(state: &NoteEditorState, context: &str) {
569 let cursor_row = state.cursor.virtual_row() as i32;
570 let top = state.viewport().top() as i32;
571 let bottom = state.viewport().bottom() as i32;
572 assert!(
573 cursor_row >= top && cursor_row < bottom,
574 "{context}: cursor row {cursor_row} outside viewport [{top}, {bottom})",
575 );
576 }
577
578 #[test]
579 fn test_viewport_scrolls_with_cursor_in_edit_mode() {
580 let content = "# Title\n\nLine 1\n\nLine 2\n\nLine 3\n\nLine 4\n\nLine 5\n";
581
582 let mut state =
583 NoteEditorState::new(content, "test", Path::new("test.md"), &Symbols::unicode());
584 state.resize_viewport(Size::new(40, 4));
585
586 state.cursor_down(2);
587 state.set_view(View::Edit(EditMode::Source));
588
589 state.insert_char('\n');
590 state.insert_char('\n');
591 state.insert_char('\n');
592 state.insert_char('\n');
593 assert_cursor_visible(&state, "after insert_char");
594
595 state.cursor_right(20);
596 assert_cursor_visible(&state, "after cursor_right");
597
598 state.cursor_left(20);
599 assert_cursor_visible(&state, "after cursor_left");
600
601 state.cursor_down(5);
602 assert_cursor_visible(&state, "after cursor_down");
603
604 state.cursor_up(5);
605 assert_cursor_visible(&state, "after cursor_up");
606 }
607}