1pub mod explorer;
3pub mod status_line;
4pub mod theme;
5use std::time::Duration;
6
7use ratatui::prelude::*;
8pub use status_line::StatusLine;
9use synoptic::{trim, TokOpt};
10
11use self::theme::EditorTheme;
12use crate::{helper::max_col, state::EditorState, EditorMode, Index2};
13
14#[derive(Debug, Clone, Default)]
15pub struct EditorMessage {
16 message: String,
17 duration: Duration,
18}
19
20impl EditorMessage {
21 #[must_use]
23 pub fn new(message: String, duration: Duration) -> Self {
24 Self { message, duration }
25 }
26}
27
28pub struct EditorView<'a, 'b> {
29 pub(crate) state: &'a mut EditorState,
30 pub(crate) theme: EditorTheme<'b>,
31 pub(crate) message: Option<EditorMessage>,
32}
33
34impl<'a, 'b> EditorView<'a, 'b> {
35 #[must_use]
37 pub fn new(state: &'a mut EditorState) -> Self {
38 Self { state, theme: EditorTheme::default(), message: None }
39 }
40
41 #[must_use]
44 pub fn theme(mut self, theme: EditorTheme<'b>) -> Self {
45 self.theme = theme;
46 self
47 }
48
49 #[must_use]
54 pub fn message(mut self, message: Option<EditorMessage>) -> Self {
55 self.message = message;
56 self
57 }
58
59 #[must_use]
61 pub fn get_state(&'a self) -> &'a EditorState {
62 self.state
63 }
64
65 #[must_use]
67 pub fn get_state_mut(&'a mut self) -> &'a mut EditorState {
68 self.state
69 }
70 fn highlight_colour(&self, name: &str) -> Color {
71 self.theme.highlighting.get(name).unwrap_or(Color::Reset)
72 }
73}
74
75impl Widget for EditorView<'_, '_> {
76 fn render(self, area: Rect, buf: &mut Buffer) {
78 buf.set_style(area, self.theme.base);
80 let area = match &self.theme.block {
81 Some(b) => {
82 let inner_area = b.inner(area);
83 b.clone().render(area, buf);
84 inner_area
85 },
86 None => area,
87 };
88
89 let [main, status] = Layout::vertical([
91 Constraint::Min(0),
92 Constraint::Length(if self.theme.status_line.is_some() { 1 } else { 0 }),
93 ])
94 .horizontal_margin(1)
95 .areas(area);
96 let [side, _, main] = Layout::horizontal([
97 Constraint::Length(if self.theme.line_numbers_style.is_some() { 3 } else { 0 }),
98 Constraint::Length(if self.theme.line_numbers_style.is_some() { 2 } else { 0 }),
99 Constraint::Min(0),
100 ])
101 .areas(main);
102
103 let width = main.width as usize;
104 let height = main.height as usize;
105
106 let cursor = displayed_cursor(self.state);
109
110 let size = (width, height);
114 let (x_off, y_off) = self.state.view.update_offset(size, cursor);
115
116 let lines = &self.state.lines;
118 for (i, line) in lines.iter_row().skip(y_off).take(height).enumerate() {
119 let y = (main.top() as usize) as u16 + i as u16;
120 if let Some(line_numbers_style) = self.theme.line_numbers_style {
122 let line_number = (y_off + i + 1).to_string();
123 let line_number_x = side.right() - line_number.len() as u16;
124 buf.get_mut(line_number_x, y).set_symbol(&line_number).set_style(line_numbers_style);
125 }
126
127 let tokens = self.state.highlighter.line(y_off + i, &line.iter().collect());
128 let tokens = trim(&tokens, x_off);
129 let mut j = 0;
130 for token in tokens {
131 match token {
132 TokOpt::Some(text, kind) => {
133 let color = self.highlight_colour(&kind);
134 for c in text.chars() {
135 let x = (main.left() as usize) as u16 + j as u16;
136 if let Some(selection) = &self.state.selection {
137 let position = Index2::new(y_off + i, x_off + j);
138 if selection.within(&position) {
139 buf.get_mut(x, y).set_style(self.theme.selection_style);
140 }
141 }
142 if x < main.right() && y < main.bottom() {
143 buf.get_mut(x, y).set_symbol(&c.to_string()).set_style(Style::default().fg(color));
144 j += 1;
145 } else {
146 break;
147 }
148 }
149 },
150 TokOpt::None(text) => {
151 for c in text.chars() {
152 let x = (main.left() as usize) as u16 + j as u16;
153 if let Some(selection) = &self.state.selection {
154 let position = Index2::new(y_off + i, x_off + j);
155 if selection.within(&position) {
156 buf.get_mut(x, y).set_style(self.theme.selection_style);
157 }
158 }
159 if x < main.right() && y < main.bottom() {
160 buf.get_mut(x, y).set_symbol(&c.to_string());
161 j += 1;
162 } else {
163 break;
164 }
165 }
166 },
167 }
168 }
169 }
170
171 let x_cursor = (main.left() as usize) + width.min(cursor.col.saturating_sub(x_off));
174 let y_cursor = (main.top() as usize) + cursor.row.saturating_sub(y_off);
175 let cursor_cell = buf.get_mut(x_cursor as u16, y_cursor as u16).set_style(self.theme.cursor_style);
176 if let Some(symbol) = self.theme.cursor_symbol {
177 cursor_cell.set_symbol(&symbol.to_string());
178 }
179
180 if let Some(s) = self.theme.status_line {
182 s.mode(self.state.mode.name())
183 .search(if self.state.mode == EditorMode::Search {
184 Some(self.state.search.pattern.clone())
185 } else {
186 None
187 })
188 .command(if self.state.mode == EditorMode::Command { Some(self.state.command.clone()) } else { None })
189 .render(status, buf);
190 }
191 }
192}
193
194fn displayed_cursor(state: &EditorState) -> Index2 {
200 let max_col = max_col(&state.lines, &state.cursor, state.mode);
201 Index2::new(state.cursor.row, state.cursor.col.min(max_col))
202}