1#![warn(clippy::pedantic)]
8
9use std::fs;
10use std::io;
11use std::path::{Path, PathBuf};
12
13use ratatui::layout::Rect;
14use ratatui::style::Style;
15use ratatui_image::protocol::StatefulProtocol;
16use vix_editor_core::actions::{
17 Delete, Duplicate, InsertText, MoveDown, MoveRight, MoveUp, SelectAll,
18};
19use vix_editor_core::code::Code;
20pub use vix_editor_core::editor::Editor as CodeEditor;
21use vix_editor_core::utils::get_lang;
22
23use vix_theme as theme;
24
25fn line_is_blank(code: &Code, row: usize) -> bool {
28 if row >= code.len_lines() {
29 return true;
30 }
31 let start = code.line_to_char(row);
32 let len = code.line_len(row);
33 code.char_slice(start, start + len)
34 .chars()
35 .all(|c: char| c.is_whitespace())
36}
37
38fn is_section_break(code: &Code, row: usize, n: usize) -> bool {
41 line_is_blank(code, row)
42 && ((row > 0 && line_is_blank(code, row - 1))
43 || (row + 1 < n && line_is_blank(code, row + 1)))
44}
45
46fn word_starts(text: &str) -> Vec<usize> {
49 let mut starts = Vec::new();
50 let mut prev_word = false;
51 for (i, c) in text.chars().enumerate() {
52 let word = is_word_char(c);
53 if word && !prev_word {
54 starts.push(i);
55 }
56 prev_word = word;
57 }
58 starts
59}
60
61fn sentence_starts(text: &str) -> Vec<usize> {
65 let chars: Vec<char> = text.chars().collect();
66 let n = chars.len();
67 let mut starts = Vec::new();
68 let mut i = 0;
69 while i < n && chars[i].is_whitespace() {
70 i += 1;
71 }
72 if i < n {
73 starts.push(i);
74 }
75 while i < n {
76 if matches!(chars[i], '.' | '!' | '?') {
77 let mut j = i + 1;
78 while j < n && matches!(chars[j], '.' | '!' | '?' | '"' | '\'' | ')' | ']' | '}') {
79 j += 1;
80 }
81 if j < n && chars[j].is_whitespace() {
82 while j < n && chars[j].is_whitespace() {
83 j += 1;
84 }
85 if j < n {
86 starts.push(j);
87 }
88 i = j;
89 continue;
90 }
91 }
92 i += 1;
93 }
94 starts.dedup();
95 starts
96}
97
98#[derive(Clone, Copy)]
100pub struct SaveOptions {
101 pub trim_trailing_whitespace: bool,
103 pub ensure_final_newline: bool,
105}
106
107fn trim_trailing_whitespace(text: &str) -> String {
110 let mut out = String::with_capacity(text.len());
111 let mut lines = text.split('\n');
112 if let Some(first) = lines.next() {
113 out.push_str(first.trim_end_matches([' ', '\t']));
114 }
115 for line in lines {
116 out.push('\n');
117 out.push_str(line.trim_end_matches([' ', '\t']));
118 }
119 out
120}
121
122#[must_use]
124pub fn is_image_path(path: &Path) -> bool {
125 matches!(
126 path.extension()
127 .and_then(|e| e.to_str())
128 .map(str::to_ascii_lowercase)
129 .as_deref(),
130 Some("png" | "jpg" | "jpeg" | "gif" | "bmp" | "webp" | "ico" | "tiff" | "tif")
131 )
132}
133
134pub const SEARCH_MARK: &str = "search";
137
138fn apply_theme(ed: &mut CodeEditor) {
144 ed.set_text_style(
145 Style::default()
146 .fg(theme::region_fg(theme::Region::Editor))
147 .add_modifier(theme::region_modifiers(theme::Region::Editor)),
148 );
149 ed.set_line_number_style(theme::dim());
150 ed.set_selection_style(theme::selected());
151 ed.set_whitespace_style(theme::dim());
152 ed.set_bracket_style(theme::selected());
153
154 let syntax = theme::syntax_theme();
156 let pairs: Vec<(&str, &str)> = syntax.iter().map(|(t, hex)| (*t, hex.as_str())).collect();
157 ed.set_syntax_theme(&pairs);
158
159 let cursor = match theme::editor_cursor() {
162 Some(color) => Style::default()
163 .bg(color)
164 .fg(theme::region_bg(theme::Region::Editor)),
165 None => theme::selected(),
166 };
167 ed.set_cursor_style(Some(cursor));
168}
169
170fn make_editor(
171 path: Option<&Path>,
172 text: &str,
173 line_numbers: bool,
174 show_whitespace: bool,
175 soft_wrap: bool,
176 indent: &str,
177) -> CodeEditor {
178 let name = path
179 .and_then(|p| p.file_name())
180 .map(|s| s.to_string_lossy().into_owned())
181 .unwrap_or_default();
182 let lang = if name.is_empty() {
183 "text".to_string()
184 } else {
185 get_lang(&name)
186 };
187 let mut ed = CodeEditor::new(&lang, text, Vec::new())
191 .or_else(|_| CodeEditor::new("text", text, Vec::new()))
192 .expect("code editor init for plain text never fails");
193 ed.show_line_numbers(line_numbers);
194 ed.show_whitespace(show_whitespace);
195 ed.set_soft_wrap(soft_wrap);
196 ed.set_indent(Some(indent.to_string()));
197 apply_theme(&mut ed);
198 ed
199}
200
201#[must_use]
209pub fn text_field(content: &str) -> CodeEditor {
210 let mut ed = CodeEditor::new("text", content, Vec::new())
211 .expect("text field init for plain text never fails");
212 ed.show_line_numbers(false);
213 apply_theme(&mut ed);
214 ed
215}
216
217pub struct Tab {
219 pub editor: CodeEditor,
221 pub path: Option<PathBuf>,
223 pub dirty: bool,
225 pub preview: bool,
227 pub image: Option<StatefulProtocol>,
229 pub read_only: bool,
231}
232
233impl Tab {
234 pub fn is_image(&self) -> bool {
236 self.image.is_some()
237 }
238
239 pub fn text(&self) -> String {
241 self.editor.get_content()
242 }
243
244 pub fn lines(&self) -> Vec<String> {
246 self.text()
247 .lines()
248 .map(std::string::ToString::to_string)
249 .collect()
250 }
251
252 pub fn title(&self) -> String {
254 let name = self.path.as_ref().and_then(|p| p.file_name()).map_or_else(
255 || "untitled".to_string(),
256 |s| s.to_string_lossy().into_owned(),
257 );
258 let icon = theme::file_icon(&name);
259 let flag = if self.dirty {
260 format!(" {}", theme::icon::FILE_DIRTY)
261 } else {
262 String::new()
263 };
264 format!("{icon} {name}{flag}")
265 }
266
267 pub fn display_path(&self) -> String {
269 self.path
270 .as_ref()
271 .map_or_else(|| "untitled".to_string(), |p| p.display().to_string())
272 }
273
274 pub fn cursor_1based(&self) -> (usize, usize) {
276 let cur = self.editor.get_cursor();
277 let code = self.editor.code_ref();
278 let row = code.char_to_line(cur);
279 let col = cur - code.line_to_char(row);
280 (row + 1, col + 1)
281 }
282
283 #[must_use]
285 pub fn line_count(&self) -> usize {
286 self.editor.code_ref().len_lines()
287 }
288}
289
290#[derive(Clone, Copy, PartialEq, Eq, Debug)]
292pub enum SplitDir {
293 Vertical,
295 Horizontal,
297}
298
299#[allow(clippy::struct_excessive_bools)]
302pub struct Editor {
303 pub tabs: Vec<Tab>,
305 pub active: usize,
307 pub line_numbers: bool,
309 pub relative_line_numbers: bool,
311 pub show_whitespace: bool,
313 pub soft_wrap: bool,
315 pub indent: String,
317 pub split_root: Option<crate::pane_tree::Pane>,
320 pub focused_leaf: usize,
322}
323
324impl Default for Editor {
325 fn default() -> Self {
326 Editor::new(true, false, false, " ".to_string())
327 }
328}
329
330impl Editor {
331 #[must_use]
334 pub fn new(line_numbers: bool, show_whitespace: bool, soft_wrap: bool, indent: String) -> Self {
335 let mut e = Editor {
336 tabs: Vec::new(),
337 active: 0,
338 line_numbers,
339 relative_line_numbers: false,
340 show_whitespace,
341 soft_wrap,
342 indent,
343 split_root: None,
344 focused_leaf: 0,
345 };
346 e.new_tab();
347 e
348 }
349
350 #[must_use]
352 pub fn active_tab(&self) -> Option<&Tab> {
353 self.tabs.get(self.active)
354 }
355
356 pub fn active_tab_mut(&mut self) -> Option<&mut Tab> {
358 self.tabs.get_mut(self.active)
359 }
360
361 #[must_use]
363 pub fn is_split(&self) -> bool {
364 self.split_root.is_some()
365 }
366
367 pub fn set_split(&mut self, dir: SplitDir) {
371 let other = (0..self.tabs.len())
372 .find(|&i| i != self.active)
373 .unwrap_or(self.active);
374 match self.split_root.as_mut() {
375 None => {
376 self.split_root = Some(crate::pane_tree::Pane::Split {
377 dir,
378 ratio: 50,
379 first: Box::new(crate::pane_tree::Pane::Leaf(self.active)),
380 second: Box::new(crate::pane_tree::Pane::Leaf(other)),
381 });
382 self.focused_leaf = 0;
383 }
384 Some(root) => {
385 root.set_leaf_tab(self.focused_leaf, self.active);
386 root.split_leaf(self.focused_leaf, dir, other);
387 }
388 }
389 }
390
391 pub fn unsplit(&mut self) {
394 let Some(root) = self.split_root.take() else {
395 return;
396 };
397 match root.remove_leaf(self.focused_leaf) {
398 Some(tree) if tree.leaf_count() > 1 => {
399 self.focused_leaf = self.focused_leaf.min(tree.leaf_count() - 1);
400 if let Some(tab) = tree.leaf_tab(self.focused_leaf) {
401 self.active = tab.min(self.tabs.len().saturating_sub(1));
402 }
403 self.split_root = Some(tree);
404 }
405 Some(tree) => {
406 if let Some(tab) = tree.leaf_tab(0) {
407 self.active = tab.min(self.tabs.len().saturating_sub(1));
408 }
409 self.split_root = None;
410 self.focused_leaf = 0;
411 }
412 None => {
413 self.split_root = None;
414 self.focused_leaf = 0;
415 }
416 }
417 }
418
419 fn sync_split(&mut self) {
422 let count = self.tabs.len();
423 if let Some(root) = self.split_root.as_mut() {
424 root.set_leaf_tab(self.focused_leaf, self.active);
425 root.clamp_tabs(count);
426 self.focused_leaf = self.focused_leaf.min(root.leaf_count().saturating_sub(1));
427 }
428 }
429
430 pub fn focus_leaf(&mut self, i: usize) {
432 self.sync_split();
433 let Some(root) = self.split_root.as_ref() else {
434 return;
435 };
436 if let Some(tab) = root.leaf_tab(i) {
437 self.focused_leaf = i;
438 self.active = tab.min(self.tabs.len().saturating_sub(1));
439 }
440 }
441
442 pub fn focus_other_pane(&mut self) {
444 self.sync_split();
445 let Some(root) = self.split_root.as_ref() else {
446 return;
447 };
448 let n = root.leaf_count();
449 if n > 0 {
450 self.focus_leaf((self.focused_leaf + 1) % n);
451 }
452 }
453
454 #[must_use]
456 pub fn focused_leaf(&self) -> usize {
457 self.focused_leaf
458 }
459
460 pub fn split_layout(&mut self, area: Rect) -> Vec<crate::pane_tree::LeafBox> {
463 self.sync_split();
464 self.split_root
465 .as_ref()
466 .map(|r| r.layout(area))
467 .unwrap_or_default()
468 }
469
470 #[must_use]
472 pub fn split_dividers(&self, area: Rect) -> Vec<(SplitDir, Rect)> {
473 self.split_root
474 .as_ref()
475 .map(|r| r.dividers(area))
476 .unwrap_or_default()
477 }
478
479 pub fn focus_pane_at(&mut self, area: Rect, col: u16, row: u16) -> bool {
482 self.sync_split();
483 let Some(leaf) = self
484 .split_root
485 .as_ref()
486 .and_then(|r| r.leaf_at(area, col, row))
487 else {
488 return false;
489 };
490 self.focus_leaf(leaf);
491 true
492 }
493
494 pub fn resize_split_at(&mut self, area: Rect, col: u16, row: u16) -> bool {
497 self.split_root
498 .as_mut()
499 .is_some_and(|r| r.resize_at(area, col, row))
500 }
501
502 pub fn new_tab(&mut self) {
504 let editor = make_editor(
505 None,
506 "",
507 self.line_numbers,
508 self.show_whitespace,
509 self.soft_wrap,
510 &self.indent,
511 );
512 self.tabs.push(Tab {
513 editor,
514 path: None,
515 dirty: false,
516 preview: false,
517 image: None,
518 read_only: false,
519 });
520 self.active = self.tabs.len() - 1;
521 }
522
523 pub fn new_tab_with_content(&mut self, content: &str) {
526 let editor = make_editor(
527 None,
528 content,
529 self.line_numbers,
530 self.show_whitespace,
531 self.soft_wrap,
532 &self.indent,
533 );
534 self.tabs.push(Tab {
535 editor,
536 path: None,
537 dirty: true,
538 preview: false,
539 image: None,
540 read_only: false,
541 });
542 self.active = self.tabs.len() - 1;
543 }
544
545 pub fn open_image(&mut self, path: &Path, proto: StatefulProtocol) {
547 let canon = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
548 if let Some(i) = self
549 .tabs
550 .iter()
551 .position(|t| t.path.as_deref() == Some(canon.as_path()))
552 {
553 self.tabs[i].image = Some(proto);
554 self.active = i;
555 return;
556 }
557 let editor = make_editor(
558 Some(&canon),
559 "",
560 self.line_numbers,
561 self.show_whitespace,
562 self.soft_wrap,
563 &self.indent,
564 );
565 self.tabs.push(Tab {
566 editor,
567 path: Some(canon),
568 dirty: false,
569 preview: false,
570 image: Some(proto),
571 read_only: false,
572 });
573 self.active = self.tabs.len() - 1;
574 }
575
576 pub fn refresh_line_numbers(&mut self) {
579 for tab in &mut self.tabs {
580 tab.editor.show_line_numbers(self.line_numbers);
581 tab.editor
582 .set_relative_line_numbers(self.relative_line_numbers);
583 }
584 }
585
586 pub fn refresh_whitespace(&mut self) {
588 for tab in &mut self.tabs {
589 tab.editor.show_whitespace(self.show_whitespace);
590 }
591 }
592
593 pub fn refresh_soft_wrap(&mut self) {
595 for tab in &mut self.tabs {
596 tab.editor.set_soft_wrap(self.soft_wrap);
597 }
598 }
599
600 pub fn refresh_theme(&mut self) {
602 for tab in &mut self.tabs {
603 apply_theme(&mut tab.editor);
604 }
605 }
606
607 pub fn open(&mut self, path: &Path, preview: bool) -> io::Result<()> {
614 let canon = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
615 if let Some(i) = self
616 .tabs
617 .iter()
618 .position(|t| t.path.as_deref() == Some(canon.as_path()))
619 {
620 self.active = i;
621 if !preview {
622 self.tabs[i].preview = false;
623 }
624 return Ok(());
625 }
626
627 let content = fs::read_to_string(&canon)?;
628 let editor = make_editor(
629 Some(&canon),
630 &content,
631 self.line_numbers,
632 self.show_whitespace,
633 self.soft_wrap,
634 &self.indent,
635 );
636 let tab = Tab {
637 editor,
638 path: Some(canon),
639 dirty: false,
640 preview,
641 image: None,
642 read_only: false,
643 };
644
645 if preview && let Some(i) = self.tabs.iter().position(|t| t.preview) {
646 self.tabs[i] = tab;
647 self.active = i;
648 return Ok(());
649 }
650 self.tabs.push(tab);
651 self.active = self.tabs.len() - 1;
652 Ok(())
653 }
654
655 pub fn reload_clean_from_disk(&mut self) -> usize {
660 let mut reloaded = 0;
661 for tab in &mut self.tabs {
662 if tab.dirty || tab.image.is_some() {
663 continue;
664 }
665 let Some(path) = tab.path.clone() else {
666 continue;
667 };
668 let Ok(content) = fs::read_to_string(&path) else {
669 continue;
670 };
671 if content == tab.editor.get_content() {
672 continue;
673 }
674 let cursor = tab.editor.get_cursor();
675 tab.editor.set_content(&content);
676 tab.editor.set_cursor(cursor.min(content.chars().count()));
677 tab.dirty = false;
678 reloaded += 1;
679 }
680 reloaded
681 }
682
683 pub fn promote_active(&mut self) {
685 if let Some(t) = self.active_tab_mut() {
686 t.preview = false;
687 }
688 }
689
690 pub fn save_active(&mut self, opts: SaveOptions) -> io::Result<PathBuf> {
696 let idx = self.active;
697 let path = self.tabs[idx]
698 .path
699 .clone()
700 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "buffer has no path"))?;
701 self.write_to(idx, &path, opts)?;
702 Ok(path)
703 }
704
705 pub fn save_active_as(&mut self, path: PathBuf, opts: SaveOptions) -> io::Result<PathBuf> {
711 let idx = self.active;
712 self.write_to(idx, &path, opts)?;
713 self.tabs[idx].path = Some(path.clone());
714 Ok(path)
715 }
716
717 fn write_to(&mut self, idx: usize, path: &Path, opts: SaveOptions) -> io::Result<()> {
718 let mut data = self.tabs[idx].text();
719 if opts.trim_trailing_whitespace {
720 data = trim_trailing_whitespace(&data);
721 }
722 if opts.ensure_final_newline && !data.is_empty() && !data.ends_with('\n') {
723 data.push('\n');
724 }
725 fs::write(path, data)?;
726 self.tabs[idx].dirty = false;
727 self.tabs[idx].preview = false;
728 Ok(())
729 }
730
731 pub fn close_active(&mut self) -> Option<PathBuf> {
734 if self.tabs.is_empty() {
735 return None;
736 }
737 let closed = self.tabs.remove(self.active).path;
738 if self.tabs.is_empty() {
739 self.new_tab();
740 } else if self.active >= self.tabs.len() {
741 self.active = self.tabs.len() - 1;
742 }
743 closed
744 }
745
746 pub fn close_all(&mut self) -> Vec<PathBuf> {
749 let closed: Vec<PathBuf> = self.tabs.drain(..).filter_map(|t| t.path).collect();
750 self.active = 0;
751 self.new_tab();
752 closed
753 }
754
755 pub fn next_tab(&mut self) {
757 if !self.tabs.is_empty() {
758 self.active = (self.active + 1) % self.tabs.len();
759 }
760 }
761
762 pub fn prev_tab(&mut self) {
764 if !self.tabs.is_empty() {
765 self.active = (self.active + self.tabs.len() - 1) % self.tabs.len();
766 }
767 }
768
769 #[must_use]
771 pub fn cursor_1based(&self) -> (usize, usize) {
772 self.active_tab().map_or((1, 1), Tab::cursor_1based)
773 }
774
775 #[must_use]
777 pub fn active_line_count(&self) -> usize {
778 self.active_tab()
779 .map_or(1, |t| t.editor.code_ref().len_lines())
780 }
781
782 pub fn goto(&mut self, line: usize, col: Option<usize>, area: Rect) {
786 if let Some(t) = self.active_tab_mut() {
787 let last = t.editor.code_ref().len_lines().max(1);
788 let line = line.clamp(1, last);
789 let off = t
790 .editor
791 .code_ref()
792 .offset(line - 1, col.unwrap_or(1).saturating_sub(1));
793 t.editor.set_cursor(off);
794 t.editor.focus(&area);
795 }
796 }
797
798 pub fn cursor_line_home(&mut self) {
803 if let Some(t) = self.active_tab_mut() {
804 let cur = t.editor.get_cursor();
805 let code = t.editor.code_ref();
806 let row = code.char_to_line(cur);
807 let line_start = code.line_to_char(row);
808 let line_len = code.line_len(row);
809 let cur_col = cur - line_start;
810 let indent = code
811 .char_slice(line_start, line_start + line_len)
812 .chars()
813 .take_while(|c| *c == ' ' || *c == '\t')
814 .count();
815 let indent = if indent >= line_len { 0 } else { indent };
817 let target = if cur_col == indent { 0 } else { indent };
818 t.editor.set_cursor(line_start + target);
819 }
820 }
821
822 pub fn cursor_document_start(&mut self, area: Rect) {
825 if let Some(t) = self.active_tab_mut() {
826 t.editor.set_cursor(0);
827 t.editor.focus(&area);
828 }
829 }
830
831 pub fn cursor_document_end(&mut self, area: Rect) {
834 if let Some(t) = self.active_tab_mut() {
835 let code = t.editor.code_ref();
836 let last = code.len_lines().saturating_sub(1);
837 let off = code.line_to_char(last) + code.line_len(last);
838 t.editor.set_cursor(off);
839 t.editor.focus(&area);
840 }
841 }
842
843 pub fn cursor_line_end(&mut self) {
845 if let Some(t) = self.active_tab_mut() {
846 let cur = t.editor.get_cursor();
847 let code = t.editor.code_ref();
848 let row = code.char_to_line(cur);
849 let off = code.line_to_char(row) + code.line_len(row);
850 t.editor.set_cursor(off);
851 }
852 }
853
854 pub fn cursor_line_start(&mut self) {
857 if let Some(t) = self.active_tab_mut() {
858 let cur = t.editor.get_cursor();
859 let code = t.editor.code_ref();
860 let row = code.char_to_line(cur);
861 let off = code.line_to_char(row);
862 t.editor.set_cursor(off);
863 }
864 }
865
866 pub fn cursor_paragraph_start(&mut self, area: Rect) {
870 if let Some(t) = self.active_tab_mut() {
871 let cur = t.editor.get_cursor();
872 let off = {
873 let code = t.editor.code_ref();
874 let is_blank = |r: usize| line_is_blank(code, r);
875 let mut r = code.char_to_line(cur);
876 while r > 0 && is_blank(r) {
877 r -= 1;
878 }
879 while r > 0 && !is_blank(r - 1) {
880 r -= 1;
881 }
882 code.line_to_char(r)
883 };
884 t.editor.set_cursor(off);
885 t.editor.focus(&area);
886 }
887 }
888
889 pub fn cursor_paragraph_end(&mut self, area: Rect) {
892 if let Some(t) = self.active_tab_mut() {
893 let cur = t.editor.get_cursor();
894 let off = {
895 let code = t.editor.code_ref();
896 let n = code.len_lines().max(1);
897 let is_blank = |r: usize| line_is_blank(code, r);
898 let mut r = code.char_to_line(cur);
899 while r + 1 < n && is_blank(r) {
900 r += 1;
901 }
902 while r + 1 < n && !is_blank(r + 1) {
903 r += 1;
904 }
905 code.line_to_char(r) + code.line_len(r)
906 };
907 t.editor.set_cursor(off);
908 t.editor.focus(&area);
909 }
910 }
911
912 pub fn cursor_section_start(&mut self, area: Rect) {
916 if let Some(t) = self.active_tab_mut() {
917 let cur = t.editor.get_cursor();
918 let off = {
919 let code = t.editor.code_ref();
920 let n = code.len_lines().max(1);
921 let sep = |r: usize| is_section_break(code, r, n);
922 let mut r = code.char_to_line(cur);
923 while r > 0 && !sep(r - 1) {
924 r -= 1;
925 }
926 code.line_to_char(r)
927 };
928 t.editor.set_cursor(off);
929 t.editor.focus(&area);
930 }
931 }
932
933 pub fn cursor_section_end(&mut self, area: Rect) {
936 if let Some(t) = self.active_tab_mut() {
937 let cur = t.editor.get_cursor();
938 let off = {
939 let code = t.editor.code_ref();
940 let n = code.len_lines().max(1);
941 let sep = |r: usize| is_section_break(code, r, n);
942 let mut r = code.char_to_line(cur);
943 while r + 1 < n && !sep(r + 1) {
944 r += 1;
945 }
946 code.line_to_char(r) + code.line_len(r)
947 };
948 t.editor.set_cursor(off);
949 t.editor.focus(&area);
950 }
951 }
952
953 pub fn cursor_line_down(&mut self, area: Rect) {
955 if let Some(t) = self.active_tab_mut() {
956 t.editor.cursor_down();
957 t.editor.focus(&area);
958 }
959 }
960
961 pub fn cursor_line_up(&mut self, area: Rect) {
963 if let Some(t) = self.active_tab_mut() {
964 t.editor.cursor_up();
965 t.editor.focus(&area);
966 }
967 }
968
969 fn cursor_row(&self) -> usize {
971 self.active_tab().map_or(0, |t| {
972 let code = t.editor.code_ref();
973 code.char_to_line(t.editor.get_cursor())
974 })
975 }
976
977 #[must_use]
979 pub fn cursor_line(&self) -> usize {
980 self.cursor_row()
981 }
982
983 pub fn poll_parse(&mut self) -> bool {
986 self.active_tab_mut().is_some_and(|t| t.editor.poll_parse())
987 }
988
989 #[must_use]
991 pub fn parse_pending(&self) -> bool {
992 self.active_tab().is_some_and(|t| t.editor.parse_pending())
993 }
994
995 pub fn switch_undo_branch(&mut self) -> bool {
998 self.active_tab_mut()
999 .is_some_and(|t| t.editor.switch_undo_branch())
1000 }
1001
1002 #[must_use]
1005 pub fn top_visible_line(&self) -> usize {
1006 self.active_tab().map_or(0, |t| t.editor.top_line())
1007 }
1008
1009 pub fn set_word_marks(&mut self, marks: Vec<(usize, usize)>) {
1011 if let Some(t) = self.active_tab_mut() {
1012 t.editor.set_word_marks(marks);
1013 }
1014 }
1015
1016 fn block_starts(&self, section: bool) -> Vec<usize> {
1020 let Some(t) = self.active_tab() else {
1021 return Vec::new();
1022 };
1023 let code = t.editor.code_ref();
1024 let n = code.len_lines().max(1);
1025 (0..n)
1026 .filter(|&r| {
1027 if section {
1028 !is_section_break(code, r, n) && (r == 0 || is_section_break(code, r - 1, n))
1029 } else {
1030 !line_is_blank(code, r) && (r == 0 || line_is_blank(code, r - 1))
1031 }
1032 })
1033 .collect()
1034 }
1035
1036 fn goto_row(&mut self, row: usize, area: Rect) {
1038 if let Some(t) = self.active_tab_mut() {
1039 let off = t.editor.code_ref().line_to_char(row);
1040 t.editor.set_cursor(off);
1041 t.editor.focus(&area);
1042 }
1043 }
1044
1045 pub fn cursor_paragraph_next(&mut self, area: Rect) {
1047 let row = self.cursor_row();
1048 if let Some(&next) = self.block_starts(false).iter().find(|&&r| r > row) {
1049 self.goto_row(next, area);
1050 }
1051 }
1052
1053 pub fn cursor_paragraph_prev(&mut self, area: Rect) {
1055 let row = self.cursor_row();
1056 if let Some(&prev) = self.block_starts(false).iter().rev().find(|&&r| r < row) {
1057 self.goto_row(prev, area);
1058 }
1059 }
1060
1061 pub fn goto_paragraph(&mut self, n: usize, area: Rect) {
1063 if let Some(&row) = self.block_starts(false).get(n.saturating_sub(1)) {
1064 self.goto_row(row, area);
1065 }
1066 }
1067
1068 pub fn cursor_section_next(&mut self, area: Rect) {
1070 let row = self.cursor_row();
1071 if let Some(&next) = self.block_starts(true).iter().find(|&&r| r > row) {
1072 self.goto_row(next, area);
1073 }
1074 }
1075
1076 pub fn cursor_section_prev(&mut self, area: Rect) {
1078 let row = self.cursor_row();
1079 if let Some(&prev) = self.block_starts(true).iter().rev().find(|&&r| r < row) {
1080 self.goto_row(prev, area);
1081 }
1082 }
1083
1084 pub fn goto_section(&mut self, n: usize, area: Rect) {
1086 if let Some(&row) = self.block_starts(true).get(n.saturating_sub(1)) {
1087 self.goto_row(row, area);
1088 }
1089 }
1090
1091 pub fn goto_offset(&mut self, off: usize, area: Rect) {
1094 if let Some(t) = self.active_tab_mut() {
1095 t.editor.set_cursor(off);
1096 t.editor.focus(&area);
1097 }
1098 }
1099
1100 pub fn cursor_sentence_start(&mut self, area: Rect) {
1102 let Some(t) = self.active_tab() else { return };
1103 let cur = t.editor.get_cursor();
1104 let off = sentence_starts(&t.editor.get_content())
1105 .into_iter()
1106 .rev()
1107 .find(|&s| s <= cur)
1108 .unwrap_or(0);
1109 self.goto_offset(off, area);
1110 }
1111
1112 pub fn cursor_sentence_end(&mut self, area: Rect) {
1115 let Some(t) = self.active_tab() else { return };
1116 let cur = t.editor.get_cursor();
1117 let text = t.editor.get_content();
1118 let chars: Vec<char> = text.chars().collect();
1119 let off = match sentence_starts(&text).into_iter().find(|&s| s > cur) {
1120 Some(mut e) => {
1121 while e > 0 && chars.get(e - 1).is_some_and(|c| c.is_whitespace()) {
1122 e -= 1;
1123 }
1124 e
1125 }
1126 None => chars.len(),
1127 };
1128 self.goto_offset(off, area);
1129 }
1130
1131 pub fn cursor_sentence_next(&mut self, area: Rect) {
1133 let Some(t) = self.active_tab() else { return };
1134 let cur = t.editor.get_cursor();
1135 if let Some(next) = sentence_starts(&t.editor.get_content())
1136 .into_iter()
1137 .find(|&s| s > cur)
1138 {
1139 self.goto_offset(next, area);
1140 }
1141 }
1142
1143 pub fn cursor_sentence_prev(&mut self, area: Rect) {
1145 let Some(t) = self.active_tab() else { return };
1146 let cur = t.editor.get_cursor();
1147 if let Some(prev) = sentence_starts(&t.editor.get_content())
1148 .into_iter()
1149 .rev()
1150 .find(|&s| s < cur)
1151 {
1152 self.goto_offset(prev, area);
1153 }
1154 }
1155
1156 pub fn goto_sentence(&mut self, n: usize, area: Rect) {
1158 let Some(t) = self.active_tab() else { return };
1159 let starts = sentence_starts(&t.editor.get_content());
1160 if let Some(&off) = starts.get(n.saturating_sub(1)) {
1161 self.goto_offset(off, area);
1162 }
1163 }
1164
1165 pub fn cursor_word_start(&mut self, area: Rect) {
1168 let Some(t) = self.active_tab() else { return };
1169 let cur = t.editor.get_cursor();
1170 let off = word_starts(&t.editor.get_content())
1171 .into_iter()
1172 .rev()
1173 .find(|&s| s <= cur)
1174 .unwrap_or(0);
1175 self.goto_offset(off, area);
1176 }
1177
1178 pub fn cursor_word_end(&mut self, area: Rect) {
1180 let Some(t) = self.active_tab() else { return };
1181 let cur = t.editor.get_cursor();
1182 let chars: Vec<char> = t.editor.get_content().chars().collect();
1183 let mut e = cur;
1185 while e < chars.len() && !is_word_char(chars[e]) {
1186 e += 1;
1187 }
1188 while e < chars.len() && is_word_char(chars[e]) {
1189 e += 1;
1190 }
1191 self.goto_offset(e, area);
1192 }
1193
1194 pub fn cursor_word_next(&mut self, area: Rect) {
1196 let Some(t) = self.active_tab() else { return };
1197 let cur = t.editor.get_cursor();
1198 if let Some(next) = word_starts(&t.editor.get_content())
1199 .into_iter()
1200 .find(|&s| s > cur)
1201 {
1202 self.goto_offset(next, area);
1203 }
1204 }
1205
1206 pub fn cursor_word_prev(&mut self, area: Rect) {
1208 let Some(t) = self.active_tab() else { return };
1209 let cur = t.editor.get_cursor();
1210 if let Some(prev) = word_starts(&t.editor.get_content())
1211 .into_iter()
1212 .rev()
1213 .find(|&s| s < cur)
1214 {
1215 self.goto_offset(prev, area);
1216 }
1217 }
1218
1219 pub fn goto_word(&mut self, n: usize, area: Rect) {
1221 let Some(t) = self.active_tab() else { return };
1222 let starts = word_starts(&t.editor.get_content());
1223 if let Some(&off) = starts.get(n.saturating_sub(1)) {
1224 self.goto_offset(off, area);
1225 }
1226 }
1227
1228 pub fn goto_percent(&mut self, pct: usize, area: Rect) {
1230 let Some(t) = self.active_tab() else { return };
1231 let total = t.editor.code_ref().len_lines().max(1);
1232 let line = pct.min(100) * total.saturating_sub(1) / 100;
1233 self.goto(line + 1, None, area); }
1235
1236 pub fn goto_byte(&mut self, byte: usize, area: Rect) {
1238 let Some(t) = self.active_tab() else { return };
1239 let code = t.editor.code_ref();
1240 let off = code.byte_to_char(byte.min(code.len_bytes()));
1241 self.goto_offset(off, area);
1242 }
1243
1244 pub fn select_line(&mut self, area: Rect) {
1247 if let Some(t) = self.active_tab_mut() {
1248 let (start, end) = {
1249 let code = t.editor.code_ref();
1250 let row = code.char_to_line(t.editor.get_cursor());
1251 let start = code.line_to_char(row);
1252 let end = if row + 1 < code.len_lines() {
1253 code.line_to_char(row + 1)
1254 } else {
1255 start + code.line_len(row)
1256 };
1257 (start, end)
1258 };
1259 t.editor.set_selection_range(start, end);
1260 t.editor.focus(&area);
1261 }
1262 }
1263
1264 pub fn select_paragraph(&mut self, area: Rect) {
1267 if let Some(t) = self.active_tab_mut() {
1268 let (start, end) = {
1269 let code = t.editor.code_ref();
1270 let n = code.len_lines().max(1);
1271 let is_blank = |r: usize| line_is_blank(code, r);
1272 let row = code.char_to_line(t.editor.get_cursor());
1273 let mut s = row;
1274 while s > 0 && is_blank(s) {
1275 s -= 1;
1276 }
1277 while s > 0 && !is_blank(s - 1) {
1278 s -= 1;
1279 }
1280 let mut e = row;
1281 while e + 1 < n && is_blank(e) {
1282 e += 1;
1283 }
1284 while e + 1 < n && !is_blank(e + 1) {
1285 e += 1;
1286 }
1287 (
1288 code.line_to_char(s),
1289 code.line_to_char(e) + code.line_len(e),
1290 )
1291 };
1292 t.editor.set_selection_range(start, end);
1293 t.editor.focus(&area);
1294 }
1295 }
1296
1297 pub fn select_section(&mut self, area: Rect) {
1300 if let Some(t) = self.active_tab_mut() {
1301 let (start, end) = {
1302 let code = t.editor.code_ref();
1303 let n = code.len_lines().max(1);
1304 let sep = |r: usize| is_section_break(code, r, n);
1305 let row = code.char_to_line(t.editor.get_cursor());
1306 let mut s = row;
1307 while s > 0 && !sep(s - 1) {
1308 s -= 1;
1309 }
1310 let mut e = row;
1311 while e + 1 < n && !sep(e + 1) {
1312 e += 1;
1313 }
1314 (
1315 code.line_to_char(s),
1316 code.line_to_char(e) + code.line_len(e),
1317 )
1318 };
1319 t.editor.set_selection_range(start, end);
1320 t.editor.focus(&area);
1321 }
1322 }
1323
1324 pub fn insert_str(&mut self, text: &str, area: Rect) -> bool {
1328 if let Some(t) = self.active_tab_mut() {
1329 if t.is_image() || t.read_only {
1330 return false;
1331 }
1332 t.editor.apply(InsertText {
1333 text: text.to_string(),
1334 });
1335 t.editor.focus(&area);
1336 t.dirty = true;
1337 t.preview = false;
1338 return true;
1339 }
1340 false
1341 }
1342
1343 pub fn select_all(&mut self) {
1345 if let Some(t) = self.active_tab_mut() {
1346 t.editor.apply(SelectAll {});
1347 }
1348 }
1349
1350 pub fn duplicate_line(&mut self) {
1352 if let Some(t) = self.active_tab_mut() {
1353 if t.is_image() {
1354 return;
1355 }
1356 t.editor.apply(Duplicate {});
1357 t.dirty = true;
1358 t.preview = false;
1359 }
1360 }
1361
1362 pub fn join_lines(&mut self) {
1365 if let Some(t) = self.active_tab_mut() {
1366 if t.is_image() {
1367 return;
1368 }
1369 t.editor.join_lines();
1370 t.dirty = true;
1371 t.preview = false;
1372 }
1373 }
1374
1375 pub fn sort_lines(&mut self) {
1378 if let Some(t) = self.active_tab_mut() {
1379 if t.is_image() {
1380 return;
1381 }
1382 t.editor.sort_lines();
1383 t.dirty = true;
1384 t.preview = false;
1385 }
1386 }
1387
1388 pub fn trim_trailing_whitespace(&mut self) {
1390 self.edit_active_lines(CodeEditor::trim_trailing_whitespace);
1391 }
1392
1393 pub fn remove_duplicate_lines(&mut self) {
1395 self.edit_active_lines(CodeEditor::remove_duplicate_lines);
1396 }
1397
1398 pub fn reverse_lines(&mut self) {
1400 self.edit_active_lines(CodeEditor::reverse_lines);
1401 }
1402
1403 pub fn sort_unique(&mut self) {
1405 self.edit_active_lines(CodeEditor::sort_unique);
1406 }
1407
1408 pub fn shuffle_lines(&mut self) {
1410 self.edit_active_lines(CodeEditor::shuffle_lines);
1411 }
1412
1413 fn edit_active_lines(&mut self, f: impl FnOnce(&mut CodeEditor)) {
1415 if let Some(t) = self.active_tab_mut() {
1416 if t.is_image() {
1417 return;
1418 }
1419 f(&mut t.editor);
1420 t.dirty = true;
1421 t.preview = false;
1422 }
1423 }
1424
1425 pub fn move_line(&mut self, down: bool, area: Rect) {
1428 if let Some(t) = self.active_tab_mut() {
1429 if t.is_image() {
1430 return;
1431 }
1432 if down {
1433 t.editor.move_line_down();
1434 } else {
1435 t.editor.move_line_up();
1436 }
1437 t.editor.focus(&area);
1438 t.dirty = true;
1439 t.preview = false;
1440 }
1441 }
1442
1443 pub fn select_word(&mut self, forward: bool, area: Rect) {
1447 if let Some(t) = self.active_tab_mut() {
1448 if t.is_image() {
1449 return;
1450 }
1451 let text: Vec<char> = t.text().chars().collect();
1452 let cur = t.editor.get_cursor();
1453 let target = if forward {
1454 next_word(&text, cur)
1455 } else {
1456 prev_word(&text, cur)
1457 };
1458 if target != cur {
1459 t.editor.extend_selection(target);
1460 t.editor.set_cursor(target);
1461 t.editor.focus(&area);
1462 }
1463 }
1464 }
1465
1466 pub fn jump_matching_bracket(&mut self, area: Rect) {
1469 if let Some(t) = self.active_tab_mut()
1470 && let Some(off) = t.editor.matching_bracket_offset()
1471 {
1472 t.editor.set_cursor(off);
1473 t.editor.focus(&area);
1474 }
1475 }
1476
1477 pub fn delete_forward(&mut self) {
1479 if let Some(t) = self.active_tab_mut() {
1480 t.editor.apply(MoveRight { shift: false });
1481 t.editor.apply(Delete {});
1482 }
1483 }
1484
1485 pub fn page_up(&mut self, lines: usize) {
1487 if let Some(t) = self.active_tab_mut() {
1488 for _ in 0..lines.max(1) {
1489 t.editor.apply(MoveUp { shift: false });
1490 }
1491 }
1492 }
1493
1494 pub fn page_down(&mut self, lines: usize) {
1496 if let Some(t) = self.active_tab_mut() {
1497 for _ in 0..lines.max(1) {
1498 t.editor.apply(MoveDown { shift: false });
1499 }
1500 }
1501 }
1502}
1503
1504fn is_word_char(c: char) -> bool {
1506 c.is_alphanumeric() || c == '_'
1507}
1508
1509fn next_word(text: &[char], pos: usize) -> usize {
1512 let n = text.len();
1513 let mut i = pos.min(n);
1514 while i < n && !is_word_char(text[i]) {
1515 i += 1;
1516 }
1517 while i < n && is_word_char(text[i]) {
1518 i += 1;
1519 }
1520 i
1521}
1522
1523fn prev_word(text: &[char], pos: usize) -> usize {
1526 let mut i = pos.min(text.len());
1527 while i > 0 && !is_word_char(text[i - 1]) {
1528 i -= 1;
1529 }
1530 while i > 0 && is_word_char(text[i - 1]) {
1531 i -= 1;
1532 }
1533 i
1534}
1535
1536#[cfg(test)]
1537mod nav_tests {
1538 use super::{sentence_starts, word_starts};
1539
1540 #[test]
1541 fn word_starts_finds_each_word() {
1542 assert_eq!(word_starts("foo bar_baz qux"), vec![0, 5, 13]);
1544 assert_eq!(word_starts(" leading"), vec![3]);
1545 assert!(word_starts("...").is_empty());
1546 }
1547
1548 #[test]
1549 fn sentence_starts_splits_on_terminators() {
1550 let text = "One. Two! Three? Four.";
1551 assert_eq!(sentence_starts(text), vec![0, 5, 10, 18]);
1553 assert_eq!(sentence_starts("pi is 3.14 today"), vec![0]);
1555 }
1556}