basalt_tui/
note_editor.rs

1mod editor;
2mod state;
3mod text_buffer;
4
5/// Provides Markdown parser that supports Obsidian flavor.
6/// Obsidian flavor is a combination of different flavors and a few differences.
7///
8/// Namely `CommonMark` and `GitHub Flavored Markdown`. More info
9/// [here](https://help.obsidian.md/Editing+and+formatting/Obsidian+Flavored+Markdown).
10///
11/// NOTE: Current iteration does not handle Obsidian flavor, unless it is covered by
12/// pulldown-cmark. Part of Obsidian flavor is for example use of any character inside tasks to
13/// mark them as completed `- [?] Completed`.
14///
15/// This crate uses [`pulldown_cmark`] to parse the markdown and enable the applicable features. This
16/// crate uses own intermediate types to provide the parsed markdown nodes.
17/// pub mod markdown;
18pub mod markdown_parser;
19
20pub use editor::Editor;
21use ratatui::{
22    crossterm::event::{KeyCode, KeyEvent, KeyModifiers},
23    layout::Size,
24};
25pub use state::{EditorState, View};
26pub use text_buffer::TextBuffer;
27
28use crate::{
29    app::{calc_scroll_amount, ActivePane, Message as AppMessage, ScrollAmount},
30    explorer,
31    note_editor::state::EditMode,
32    outline,
33};
34
35#[derive(Clone, Debug, PartialEq)]
36pub enum Message {
37    Save,
38    SwitchPaneNext,
39    SwitchPanePrevious,
40    ToggleExplorer,
41    ToggleOutline,
42    ToggleView,
43    EditView,
44    ReadView,
45    Exit,
46    KeyEvent(KeyEvent),
47    CursorUp,
48    CursorLeft,
49    CursorRight,
50    CursorWordForward,
51    CursorWordBackward,
52    CursorDown,
53    ScrollUp(ScrollAmount),
54    ScrollDown(ScrollAmount),
55    SetRow(usize),
56    Delete,
57}
58
59pub fn update<'a>(
60    message: &Message,
61    screen_size: Size,
62    state: &mut EditorState,
63) -> Option<AppMessage<'a>> {
64    match message {
65        Message::CursorLeft => state.cursor_left(),
66        Message::CursorRight => state.cursor_right(),
67        Message::CursorWordForward => state.cursor_word_forward(),
68        Message::CursorWordBackward => state.cursor_word_backward(),
69        Message::Delete => state.delete_char(),
70        Message::SetRow(row) => state.set_row(*row),
71
72        Message::CursorUp => {
73            state.cursor_up();
74            return Some(AppMessage::Outline(outline::Message::SelectAt(
75                state.current_row,
76            )));
77        }
78        Message::CursorDown => {
79            state.cursor_down();
80            return Some(AppMessage::Outline(outline::Message::SelectAt(
81                state.current_row,
82            )));
83        }
84        _ => {}
85    };
86
87    match state.view {
88        View::Edit(..) => match message {
89            Message::ToggleView => state.set_view(View::Read),
90            Message::ScrollUp(_) => state.cursor_up(),
91            Message::ScrollDown(_) => state.cursor_down(),
92            Message::KeyEvent(key) => {
93                state.edit((*key).into());
94
95                return Some(AppMessage::UpdateSelectedNoteContent((
96                    state.content().to_string(),
97                    None,
98                )));
99            }
100            Message::Exit => {
101                state.exit_insert();
102                state.set_view(View::Read);
103
104                return Some(AppMessage::UpdateSelectedNoteContent((
105                    state.content().to_string(),
106                    Some(state.nodes().to_vec()),
107                )));
108            }
109            _ => {}
110        },
111        View::Read => match message {
112            Message::ToggleView => state.set_view(View::Edit(EditMode::Source)),
113            Message::EditView => state.set_view(View::Edit(EditMode::Source)),
114            Message::ReadView => state.set_view(View::Read),
115            Message::Exit => state.set_view(View::Read),
116
117            Message::SetRow(row) => state.set_row(*row),
118            Message::ScrollUp(scroll_amount) => {
119                state.scroll_up(calc_scroll_amount(scroll_amount, screen_size.height.into()));
120            }
121            Message::ScrollDown(scroll_amount) => {
122                state.scroll_down(calc_scroll_amount(scroll_amount, screen_size.height.into()));
123            }
124            Message::ToggleExplorer => {
125                return Some(AppMessage::Explorer(explorer::Message::Toggle));
126            }
127            Message::ToggleOutline => {
128                return Some(AppMessage::Outline(outline::Message::Toggle));
129            }
130            Message::SwitchPaneNext => {
131                state.set_active(false);
132                return Some(AppMessage::SetActivePane(ActivePane::Outline));
133            }
134            Message::SwitchPanePrevious => {
135                state.set_active(false);
136                return Some(AppMessage::SetActivePane(ActivePane::Explorer));
137            }
138            Message::Save => {
139                state.save();
140                return Some(AppMessage::UpdateSelectedNoteContent((
141                    state.content().to_string(),
142                    None,
143                )));
144            }
145            _ => {}
146        },
147    }
148
149    None
150}
151
152pub fn handle_editing_event(key: &KeyEvent) -> Option<Message> {
153    match key.code {
154        KeyCode::Up => Some(Message::CursorUp),
155        KeyCode::Down => Some(Message::CursorDown),
156        KeyCode::Esc => Some(Message::Exit),
157        KeyCode::Backspace => Some(Message::Delete),
158        KeyCode::Char('e') if key.modifiers.contains(KeyModifiers::CONTROL) => {
159            Some(Message::ToggleView)
160        }
161        _ => Some(Message::KeyEvent(*key)),
162    }
163}