1use std::any::Any;
2use std::path::Path;
3
4use anyhow::Result;
5use crossterm::event::{KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
6use ratatui::buffer::Buffer;
7use ratatui::layout::Rect;
8use ratatui::style::Style;
9use ratatui::text::Line;
10use ratatui::widgets::{Block, Paragraph, Widget};
11use typ_buffer::{
12 EditKind, Position, SearchQuery, Selection, Selections, TextBuffer, display_to_grapheme_col,
13 grapheme_to_display_col,
14};
15use typ_core::{KeyChord, Panel, PanelEvent, RenderContext};
16
17pub mod actions;
18pub mod render;
19
20pub(crate) const TAB_WIDTH: usize = 4;
21
22pub struct EditorPanel {
23 pub(crate) buffer: TextBuffer,
24 pub(crate) selections: Selections,
27 pub(crate) top_line: usize,
28 pub(crate) left_col: usize,
31 pub(crate) goal_col: Option<usize>,
34 pub(crate) height: usize,
35 pub(crate) width: usize,
38 drag_anchor: Option<Position>,
41 last_click: Option<Position>,
44}
45
46impl EditorPanel {
47 #[allow(clippy::should_implement_trait)]
50 pub fn from_str(s: &str) -> Self {
51 Self::new(TextBuffer::from_str(s))
52 }
53
54 pub fn from_path(path: &Path) -> Result<Self> {
55 Ok(Self::new(TextBuffer::from_path(path)?))
56 }
57
58 fn new(buffer: TextBuffer) -> Self {
59 Self {
60 buffer,
61 selections: Selections::default(),
62 top_line: 0,
63 left_col: 0,
64 goal_col: None,
65 height: 0,
66 width: 0,
67 drag_anchor: None,
68 last_click: None,
69 }
70 }
71
72 pub fn selections(&self) -> &Selections {
73 &self.selections
74 }
75
76 pub fn cursor(&self) -> Position {
78 self.selections.primary().head
79 }
80
81 #[doc(hidden)]
84 pub fn set_selections_for_test(&mut self, list: Vec<Selection>) {
85 assert!(!list.is_empty(), "selections are never empty");
86 let mut selections = Selections::single(list[0]);
87 for selection in &list[1..] {
88 selections.push(*selection);
89 }
90 self.selections = selections;
91 }
92
93 pub fn top_line(&self) -> usize {
94 self.top_line
95 }
96
97 pub fn left_col(&self) -> usize {
98 self.left_col
99 }
100
101 pub fn save(&mut self) -> Result<()> {
102 self.buffer.save()
103 }
104
105 pub fn line_text(&self, line: usize) -> String {
107 self.buffer.line_text(line)
108 }
109
110 pub(crate) fn set_caret(&mut self, at: Position) {
116 self.buffer.undo_boundary();
120 self.selections.set_single(Selection::caret(at));
121 self.goal_col = None;
122 }
123
124 fn text_area(area: Rect) -> Rect {
126 Block::bordered().inner(area)
127 }
128
129 pub(crate) fn line_grapheme_count(&self, line: usize) -> usize {
130 self.buffer.line_grapheme_count(line)
131 }
132
133 pub(crate) fn last_line(&self) -> usize {
134 self.buffer.line_count().saturating_sub(1)
135 }
136
137 pub(crate) fn scroll_to_cursor(&mut self) {
139 let cursor = self.cursor();
140
141 if self.height > 0 {
142 if cursor.line < self.top_line {
143 self.top_line = cursor.line;
144 } else if cursor.line >= self.top_line + self.height {
145 self.top_line = cursor.line - self.height + 1;
146 }
147 }
148
149 if self.width > 0 {
150 let col = self.cursor_display_col(cursor);
151 if col < self.left_col {
152 self.left_col = col;
153 } else if col >= self.left_col + self.width {
154 self.left_col = col + 1 - self.width;
158 }
159 }
160 }
161
162 fn cursor_display_col(&self, cursor: Position) -> usize {
164 self.buffer.with_line_str(cursor.line, |line| {
165 grapheme_to_display_col(line, cursor.col, TAB_WIDTH)
166 })
167 }
168
169 pub(crate) fn page(&self) -> usize {
172 self.height.max(1)
173 }
174
175 pub fn buffer_find_all(&self, query: &SearchQuery) -> Vec<Selection> {
186 self.buffer.find_all(query)
187 }
188
189 pub fn select_range(&mut self, selection: Selection) {
191 self.selections.set_single(selection);
192 self.goal_col = None;
193 self.scroll_to_cursor();
194 }
195
196 pub fn replace_all(&mut self, query: &SearchQuery, replacement: &str) -> usize {
198 let hits = self.buffer.find_all(query);
199 if hits.is_empty() {
200 return 0;
201 }
202
203 self.buffer
206 .begin_edit_group(EditKind::Other, &self.selections);
207 for hit in hits.iter().rev() {
210 let (start, end) = hit.range();
211 self.buffer.replace_range(start, end, replacement);
212 }
213 self.buffer.end_edit_group();
214
215 self.clamp_selections();
216 hits.len()
217 }
218
219 fn clamp_selections(&mut self) {
226 let last_line = self.last_line();
227 let buffer = &self.buffer;
228 let clamp = |p: Position| {
229 let line = p.line.min(last_line);
230 Position {
231 line,
232 col: p.col.min(buffer.line_grapheme_count(line)),
233 }
234 };
235 let clamped: Vec<Selection> = self
236 .selections
237 .iter()
238 .map(|s| Selection {
239 anchor: clamp(s.anchor),
240 head: clamp(s.head),
241 })
242 .collect();
243 self.set_selections(clamped);
244 self.goal_col = None;
245 }
246}
247
248impl Panel for EditorPanel {
249 fn name(&self) -> &'static str {
250 "editor"
251 }
252
253 fn title(&self) -> String {
254 let name = self
255 .buffer
256 .path()
257 .and_then(|p| p.file_name())
258 .and_then(|n| n.to_str())
259 .unwrap_or("untitled")
260 .to_string();
261 if self.buffer.is_dirty() {
262 format!("{name} *")
263 } else {
264 name
265 }
266 }
267
268 fn render(&mut self, area: Rect, buf: &mut Buffer, ctx: &RenderContext) {
269 let border = if ctx.is_focused {
270 ctx.theme.border_focused
271 } else {
272 ctx.theme.border
273 };
274 let block = Block::bordered()
275 .border_style(Style::default().fg(border))
276 .title(self.title());
277 let inner = block.inner(area);
278 block.render(area, buf);
279
280 self.height = inner.height as usize;
281 self.width = inner.width as usize;
282 let end = (self.top_line + self.height).min(self.buffer.line_count());
283 let selections: Vec<Selection> = self.selections.iter().copied().collect();
284 let left_col = self.left_col;
285 let lines: Vec<Line> = (self.top_line..end)
286 .map(|i| {
287 self.buffer.with_line_str(i, |text| {
288 crate::render::styled_line(text, i, left_col, TAB_WIDTH, &selections, ctx.theme)
289 })
290 })
291 .collect();
292 Paragraph::new(lines)
293 .style(Style::default().fg(ctx.theme.fg).bg(ctx.theme.bg))
294 .render(inner, buf);
295 }
296
297 fn apply_action(&mut self, action: typ_core::Action) -> Option<Vec<PanelEvent>> {
298 self.perform(action)
299 }
300
301 fn cursor_position(&self, panel_area: Rect) -> Option<(u16, u16)> {
302 let inner = Self::text_area(panel_area);
303 let cursor = self.cursor();
304 let row = cursor.line.checked_sub(self.top_line)?;
305 if row >= inner.height as usize {
306 return None;
307 }
308 let col = self.cursor_display_col(cursor).checked_sub(self.left_col)?;
312 if col >= inner.width as usize {
313 return None;
314 }
315 Some((inner.x + col as u16, inner.y + row as u16))
316 }
317
318 fn handle_key(&mut self, _chord: KeyChord) -> Vec<PanelEvent> {
326 Vec::new()
327 }
328
329 fn handle_mouse(&mut self, event: MouseEvent, panel_area: Rect) -> Vec<PanelEvent> {
330 let at = |panel: &Self, event: &MouseEvent| {
331 let inner = Self::text_area(panel_area);
332 let row = event.row.saturating_sub(inner.y) as usize;
333 let col = event.column.saturating_sub(inner.x) as usize + panel.left_col;
336 let line = (panel.top_line + row).min(panel.last_line());
337 Position {
338 line,
339 col: panel
340 .buffer
341 .with_line_str(line, |text| display_to_grapheme_col(text, col, TAB_WIDTH)),
342 }
343 };
344
345 match event.kind {
346 MouseEventKind::Down(MouseButton::Left) => {
347 let position = at(self, &event);
348
349 if event.modifiers.contains(KeyModifiers::ALT) {
350 self.selections.push(Selection::caret(position));
353 self.last_click = Some(position);
354 self.drag_anchor = Some(position);
355 return vec![PanelEvent::NeedsRedraw];
356 }
357
358 if self.last_click == Some(position) {
359 let text = self.buffer.line_text(position.line);
365 if let Some((start, end)) = typ_buffer::word_at(&text, position.col) {
366 self.selections.set_single(Selection {
367 anchor: Position {
368 line: position.line,
369 col: start,
370 },
371 head: Position {
372 line: position.line,
373 col: end,
374 },
375 });
376 self.drag_anchor = None;
377 self.goal_col = None;
378 return vec![PanelEvent::NeedsRedraw];
379 }
380 }
381
382 self.set_caret(position);
383 self.drag_anchor = Some(position);
384 self.last_click = Some(position);
385 vec![PanelEvent::NeedsRedraw]
386 }
387
388 MouseEventKind::Drag(MouseButton::Left) => {
389 let Some(anchor) = self.drag_anchor else {
390 return Vec::new();
393 };
394 let head = at(self, &event);
395 self.selections.set_single(Selection { anchor, head });
396 self.goal_col = None;
397 vec![PanelEvent::NeedsRedraw]
398 }
399
400 MouseEventKind::Up(MouseButton::Left) => {
401 self.drag_anchor = None;
402 Vec::new()
403 }
404
405 _ => Vec::new(),
406 }
407 }
408
409 fn handle_scroll(&mut self, delta: i32, _panel_area: Rect) -> Vec<PanelEvent> {
410 let max_top = self.buffer.line_count().saturating_sub(self.height.max(1));
411 self.top_line = (self.top_line as i64 + delta as i64).clamp(0, max_top as i64) as usize;
412 vec![PanelEvent::NeedsRedraw]
413 }
414
415 fn needs_close_confirmation(&self) -> Option<String> {
416 self.buffer
417 .is_dirty()
418 .then(|| "Unsaved changes. Close anyway?".to_string())
419 }
420
421 fn as_any(&self) -> &dyn Any {
422 self
423 }
424 fn as_any_mut(&mut self) -> &mut dyn Any {
425 self
426 }
427}