1use std::{
2 cell::RefCell,
3 cmp::{max, min},
4 collections::HashMap,
5 rc::Rc,
6};
7
8use crate::{EditorClipboard, BORDER_PADDING_SIZE, MIN_TEXTAREA_HEIGHT};
9use crate::{MarkdownRenderer, ORANGE};
10use anyhow;
11use anyhow::Result;
12use rand::Rng;
13use ratatui::{
14 layout::{Constraint, Direction, Layout, Rect},
15 style::{Color, Style},
16 text::Text,
17 widgets::{Block, Borders, Paragraph, Wrap},
18 Frame,
19};
20use std::collections::HashSet;
21use tui_textarea::TextArea;
22
23const RENDER_CACHE_SIZE: usize = 100;
24
25struct MarkdownCache {
26 cache: HashMap<String, Text<'static>>,
27 renderer: MarkdownRenderer,
28}
29
30impl MarkdownCache {
31 fn new() -> Self {
32 MarkdownCache {
33 cache: HashMap::with_capacity(RENDER_CACHE_SIZE),
34 renderer: MarkdownRenderer::new(),
35 }
36 }
37
38 fn get_or_render(&mut self, content: &str, title: &str, width: usize) -> Result<Text<'static>> {
39 let cache_key = format!("{}:{}", title, content);
40 if let Some(cached) = self.cache.get(&cache_key) {
41 return Ok(cached.clone());
42 }
43
44 let content = format!("{}\n", content);
45
46 let rendered = self
47 .renderer
48 .render_markdown(content, title.to_string(), width)?;
49
50 if self.cache.len() >= RENDER_CACHE_SIZE {
51 if let Some(old_key) = self.cache.keys().next().cloned() {
52 self.cache.remove(&old_key);
53 }
54 }
55
56 self.cache.insert(cache_key, rendered.clone());
57 Ok(rendered)
58 }
59}
60
61pub struct ScrollableTextArea {
62 pub textareas: Vec<TextArea<'static>>,
63 pub titles: Vec<String>,
64 pub scroll: usize,
65 pub focused_index: usize,
66 pub edit_mode: bool,
67 pub full_screen_mode: bool,
68 pub viewport_height: u16,
69 pub start_sel: usize,
70 markdown_cache: Rc<RefCell<MarkdownCache>>,
71}
72
73impl Default for ScrollableTextArea {
74 fn default() -> Self {
75 Self::new()
76 }
77}
78
79impl ScrollableTextArea {
80 pub fn new() -> Self {
81 ScrollableTextArea {
82 textareas: Vec::with_capacity(10),
83 titles: Vec::with_capacity(10),
84 scroll: 0,
85 focused_index: 0,
86 edit_mode: false,
87 full_screen_mode: false,
88 viewport_height: 0,
89 start_sel: 0,
90 markdown_cache: Rc::new(RefCell::new(MarkdownCache::new())),
91 }
92 }
93
94 pub fn toggle_full_screen(&mut self) {
95 self.full_screen_mode = !self.full_screen_mode;
96 if self.full_screen_mode {
97 self.edit_mode = false;
98 self.scroll = 0
99 }
100 }
101
102 pub fn change_title(&mut self, new_title: String) {
103 let unique_title = self.generate_unique_title(new_title);
104 if self.focused_index < self.titles.len() {
105 self.titles[self.focused_index] = unique_title;
106 }
107 }
108
109 fn generate_unique_title(&self, base_title: String) -> String {
110 if !self.titles.contains(&base_title) {
111 return base_title;
112 }
113
114 let existing_titles: HashSet<String> = self.titles.iter().cloned().collect();
115 let mut rng = rand::thread_rng();
116 let mut new_title = base_title.clone();
117 let mut counter = 1;
118
119 while existing_titles.contains(&new_title) {
120 if counter <= 5 {
121 new_title = format!("{} {}", base_title, counter);
122 } else {
123 new_title = format!("{} {}", base_title, rng.gen_range(100..1000));
124 }
125 counter += 1;
126 }
127
128 new_title
129 }
130
131 pub fn add_textarea(&mut self, textarea: TextArea<'static>, title: String) {
132 let new_index = if self.textareas.is_empty() {
133 0
134 } else {
135 self.focused_index + 1
136 };
137
138 let unique_title = self.generate_unique_title(title);
139 self.textareas.insert(new_index, textarea);
140 self.titles.insert(new_index, unique_title);
141 self.focused_index = new_index;
142 self.adjust_scroll_to_focused();
143 }
144
145 pub fn copy_textarea_contents(&self) -> Result<()> {
146 if let Some(textarea) = self.textareas.get(self.focused_index) {
147 let content = textarea.lines().join("\n");
148 let mut ctx = EditorClipboard::new()
149 .map_err(|e| anyhow::anyhow!("Failed to create clipboard context: {}", e))?;
150 ctx.set_contents(content)
151 .map_err(|e| anyhow::anyhow!("Failed to set clipboard contents: {}", e))?;
152 }
153 Ok(())
154 }
155
156 pub fn jump_to_textarea(&mut self, index: usize) {
157 if index < self.textareas.len() {
158 self.focused_index = index;
159 self.adjust_scroll_to_focused();
160 }
161 }
162
163 pub fn remove_textarea(&mut self, index: usize) {
164 if index < self.textareas.len() {
165 self.textareas.remove(index);
166 self.titles.remove(index);
167 if self.focused_index >= self.textareas.len() {
168 self.focused_index = self.textareas.len().saturating_sub(1);
169 }
170 self.scroll = self.scroll.min(self.focused_index);
171 }
172 }
173
174 pub fn move_focus(&mut self, direction: isize) {
175 let new_index = self.focused_index as isize + direction;
176 if new_index >= (self.textareas.len()) as isize {
177 self.focused_index = 0;
178 } else if new_index < 0 {
179 self.focused_index = self.textareas.len() - 1;
180 } else {
181 self.focused_index = new_index as usize;
182 }
183 self.adjust_scroll_to_focused();
184 }
185
186 pub fn adjust_scroll_to_focused(&mut self) {
187 if self.focused_index < self.scroll {
188 self.scroll = self.focused_index;
189 } else {
190 let mut height_sum = 0;
191 for i in self.scroll..=self.focused_index {
192 let textarea_height =
193 self.textareas[i].lines().len().max(MIN_TEXTAREA_HEIGHT) + BORDER_PADDING_SIZE;
194 height_sum += textarea_height;
195
196 if height_sum > self.viewport_height as usize {
197 self.scroll = i;
198 break;
199 }
200 }
201 }
202
203 while self.calculate_height_to_focused() > self.viewport_height
204 && self.scroll < self.focused_index
205 {
206 self.scroll += 1;
207 }
208 }
209
210 pub fn calculate_height_to_focused(&self) -> u16 {
211 self.textareas[self.scroll..=self.focused_index]
212 .iter()
213 .map(|ta| (ta.lines().len().max(MIN_TEXTAREA_HEIGHT) + BORDER_PADDING_SIZE) as u16)
214 .sum()
215 }
216
217 pub fn initialize_scroll(&mut self) {
218 self.scroll = 0;
219 self.focused_index = 0;
220 }
221
222 pub fn copy_focused_textarea_contents(&self) -> anyhow::Result<()> {
223 if let Some(textarea) = self.textareas.get(self.focused_index) {
224 let content = textarea.lines().join("\n");
225 let mut ctx = EditorClipboard::new().unwrap();
226 ctx.set_contents(content).unwrap();
227 }
228 Ok(())
229 }
230
231 pub fn copy_selection_contents(&mut self) -> anyhow::Result<()> {
232 if let Some(textarea) = self.textareas.get(self.focused_index) {
233 let all_lines = textarea.lines();
234 let (cur_row, _) = textarea.cursor();
235 let min_row = min(cur_row, self.start_sel);
236 let max_row = max(cur_row, self.start_sel);
237
238 if max_row <= all_lines.len() {
239 let content = all_lines[min_row..max_row].join("\n");
240 let mut ctx = EditorClipboard::new().unwrap();
241 ctx.set_contents(content).unwrap();
242 }
243 }
244 self.start_sel = 0;
246 Ok(())
247 }
248
249 fn render_full_screen_edit(&mut self, f: &mut Frame, area: Rect) {
250 let textarea = &mut self.textareas[self.focused_index];
251 let title = &self.titles[self.focused_index];
252
253 let block = Block::default()
254 .title(title.clone())
255 .borders(Borders::ALL)
256 .border_style(Style::default().fg(ORANGE));
257
258 let edit_style = Style::default().fg(Color::White).bg(Color::Black);
259 let cursor_style = Style::default().fg(Color::White).bg(ORANGE);
260
261 textarea.set_block(block);
262 textarea.set_style(edit_style);
263 textarea.set_cursor_style(cursor_style);
264 textarea.set_selection_style(Style::default().bg(Color::Red));
265 f.render_widget(textarea.widget(), area);
266 }
267
268 pub fn render(&mut self, f: &mut Frame, area: Rect) -> Result<()> {
269 self.viewport_height = area.height;
270
271 if self.full_screen_mode {
272 if self.edit_mode {
273 self.render_full_screen_edit(f, area);
274 } else {
275 self.render_full_screen(f, area)?;
276 }
277 } else {
278 let mut remaining_height = area.height;
279 let mut visible_textareas = Vec::with_capacity(self.textareas.len());
280
281 for (i, textarea) in self.textareas.iter_mut().enumerate().skip(self.scroll) {
282 if remaining_height == 0 {
283 break;
284 }
285
286 let content_height = (textarea.lines().len() + BORDER_PADDING_SIZE) as u16;
287 let is_focused = i == self.focused_index;
288 let is_editing = is_focused && self.edit_mode;
289
290 let height = if is_editing {
291 remaining_height
292 } else {
293 content_height
294 .min(remaining_height)
295 .max(MIN_TEXTAREA_HEIGHT as u16)
296 };
297
298 visible_textareas.push((i, textarea, height));
299 remaining_height = remaining_height.saturating_sub(height);
300
301 if is_editing {
302 break;
303 }
304 }
305
306 let chunks = Layout::default()
307 .direction(Direction::Vertical)
308 .constraints(
309 visible_textareas
310 .iter()
311 .map(|(_, _, height)| Constraint::Length(*height))
312 .collect::<Vec<_>>(),
313 )
314 .split(area);
315
316 for ((i, textarea, _), chunk) in visible_textareas.into_iter().zip(chunks.iter()) {
317 let title = &self.titles[i];
318 let is_focused = i == self.focused_index;
319 let is_editing = is_focused && self.edit_mode;
320
321 let style = if is_focused {
322 if is_editing {
323 Style::default().fg(Color::White).bg(Color::Black)
324 } else {
325 Style::default().fg(Color::Black).bg(Color::DarkGray)
326 }
327 } else {
328 Style::default().fg(Color::White).bg(Color::Reset)
329 };
330
331 let block = Block::default()
332 .title(title.to_owned())
333 .borders(Borders::ALL)
334 .border_style(Style::default().fg(ORANGE))
335 .style(style);
336
337 if is_editing {
338 textarea.set_block(block);
339 textarea.set_style(style);
340 textarea.set_cursor_style(Style::default().fg(Color::White).bg(ORANGE));
341 f.render_widget(textarea.widget(), *chunk);
342 } else {
343 let content = textarea.lines().join("\n");
344 let rendered_markdown = self.markdown_cache.borrow_mut().get_or_render(
345 &content,
346 title,
347 f.size().width as usize - BORDER_PADDING_SIZE,
348 )?;
349 let paragraph = Paragraph::new(rendered_markdown)
350 .block(block)
351 .wrap(Wrap { trim: true });
352 f.render_widget(paragraph, *chunk);
353 }
354 }
355 }
356
357 Ok(())
358 }
359
360 pub fn handle_scroll(&mut self, direction: isize) {
361 if !self.full_screen_mode {
362 return;
363 }
364
365 let current_height = self.textareas[self.focused_index].lines().len();
366 let is_scrolling_down = direction > 0;
367 let is_at_last_textarea = self.focused_index == self.textareas.len() - 1;
368 let is_at_first_textarea = self.focused_index == 0;
369
370 if is_scrolling_down {
372 let can_scroll_further = self.scroll < current_height.saturating_sub(1);
373 let can_move_to_next = !is_at_last_textarea;
374
375 if can_scroll_further {
376 self.scroll += 1;
377 } else if can_move_to_next {
378 self.focused_index += 1;
379 self.scroll = 0;
380 }
381 return;
382 }
383
384 let can_scroll_up = self.scroll > 0;
386 let can_move_to_previous = !is_at_first_textarea;
387
388 if can_scroll_up {
389 self.scroll -= 1;
390 } else if can_move_to_previous {
391 self.focused_index -= 1;
392 let prev_height = self.textareas[self.focused_index].lines().len();
393 self.scroll = prev_height.saturating_sub(1);
394 }
395 }
396
397 fn render_full_screen(&mut self, f: &mut Frame, area: Rect) -> Result<()> {
398 let textarea = &mut self.textareas[self.focused_index];
399 textarea.set_selection_style(Style::default().bg(Color::Red));
400 let title = &self.titles[self.focused_index];
401
402 let block = Block::default()
403 .title(title.clone())
404 .borders(Borders::ALL)
405 .border_style(Style::default().fg(ORANGE));
406
407 let content = textarea.lines().join("\n");
408 let rendered_markdown = self.markdown_cache.borrow_mut().get_or_render(
409 &content,
410 title,
411 f.size().width as usize - BORDER_PADDING_SIZE,
412 )?;
413
414 let paragraph = Paragraph::new(rendered_markdown)
415 .block(block)
416 .wrap(Wrap { trim: true })
417 .scroll((self.scroll as u16, 0));
418
419 f.render_widget(paragraph, area);
420 Ok(())
421 }
422}
423
424#[cfg(test)]
425mod tests {
426 use super::*;
427
428 fn create_test_textarea() -> ScrollableTextArea {
429 ScrollableTextArea {
430 textareas: Vec::new(),
431 titles: Vec::new(),
432 scroll: 0,
433 focused_index: 0,
434 edit_mode: false,
435 full_screen_mode: false,
436 viewport_height: 0,
437 start_sel: 0,
438 markdown_cache: Rc::new(RefCell::new(MarkdownCache::new())),
439 }
440 }
441
442 #[test]
443 fn test_add_textarea() {
444 let mut sta = create_test_textarea();
445 sta.add_textarea(TextArea::default(), "Test".to_string());
446 assert_eq!(sta.textareas.len(), 1);
447 assert_eq!(sta.titles.len(), 1);
448 assert_eq!(sta.focused_index, 0);
449 }
450
451 #[test]
452 fn test_move_focus() {
453 let mut sta = create_test_textarea();
454 sta.add_textarea(TextArea::default(), "Test1".to_string());
455 assert_eq!(sta.focused_index, 0);
456 sta.add_textarea(TextArea::default(), "Test2".to_string());
457
458 assert_eq!(sta.focused_index, 1);
459 sta.move_focus(1);
460 assert_eq!(sta.focused_index, 0);
461 sta.move_focus(-1);
462 assert_eq!(sta.focused_index, 1);
463 }
464
465 #[test]
466 fn test_remove_textarea() {
467 let mut sta = create_test_textarea();
468 sta.add_textarea(TextArea::default(), "Test1".to_string());
469 sta.add_textarea(TextArea::default(), "Test2".to_string());
470 sta.remove_textarea(0);
471 assert_eq!(sta.textareas.len(), 1);
472 assert_eq!(sta.titles.len(), 1);
473 assert_eq!(sta.titles[0], "Test2");
474 }
475
476 #[test]
477 fn test_change_title() {
478 let mut sta = create_test_textarea();
479 sta.add_textarea(TextArea::default(), "Test".to_string());
480 sta.change_title("New Title".to_string());
481 assert_eq!(sta.titles[0], "New Title");
482 }
483
484 #[test]
485 fn test_toggle_full_screen() {
486 let mut sta = create_test_textarea();
487 assert!(!sta.full_screen_mode);
488 sta.toggle_full_screen();
489 assert!(sta.full_screen_mode);
490 assert!(!sta.edit_mode);
491 }
492
493 #[test]
494 fn test_copy_textarea_contents() {
495 let mut sta = create_test_textarea();
496 let mut textarea = TextArea::default();
497 textarea.insert_str("Test content");
498 sta.add_textarea(textarea, "Test".to_string());
499
500 let result = sta.copy_textarea_contents();
501
502 match result {
503 Ok(_) => println!("Clipboard operation succeeded"),
504 Err(e) => {
505 let error_message = e.to_string();
506 assert!(
507 error_message.contains("clipboard") || error_message.contains("display"),
508 "Unexpected error: {}",
509 error_message
510 );
511 }
512 }
513 }
514
515 #[test]
516 fn test_jump_to_textarea() {
517 let mut sta = create_test_textarea();
518 sta.add_textarea(TextArea::default(), "Test1".to_string());
519 sta.add_textarea(TextArea::default(), "Test2".to_string());
520 sta.jump_to_textarea(1);
521 assert_eq!(sta.focused_index, 1);
522 }
523}