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 vix_fileops::write_atomic(path, data.as_bytes())?;
729 self.tabs[idx].dirty = false;
730 self.tabs[idx].preview = false;
731 Ok(())
732 }
733
734 pub fn close_active(&mut self) -> Option<PathBuf> {
737 if self.tabs.is_empty() {
738 return None;
739 }
740 let closed = self.tabs.remove(self.active).path;
741 if self.tabs.is_empty() {
742 self.new_tab();
743 } else if self.active >= self.tabs.len() {
744 self.active = self.tabs.len() - 1;
745 }
746 closed
747 }
748
749 pub fn close_all(&mut self) -> Vec<PathBuf> {
752 let closed: Vec<PathBuf> = self.tabs.drain(..).filter_map(|t| t.path).collect();
753 self.active = 0;
754 self.new_tab();
755 closed
756 }
757
758 pub fn next_tab(&mut self) {
760 if !self.tabs.is_empty() {
761 self.active = (self.active + 1) % self.tabs.len();
762 }
763 }
764
765 pub fn prev_tab(&mut self) {
767 if !self.tabs.is_empty() {
768 self.active = (self.active + self.tabs.len() - 1) % self.tabs.len();
769 }
770 }
771
772 #[must_use]
774 pub fn cursor_1based(&self) -> (usize, usize) {
775 self.active_tab().map_or((1, 1), Tab::cursor_1based)
776 }
777
778 #[must_use]
780 pub fn active_line_count(&self) -> usize {
781 self.active_tab()
782 .map_or(1, |t| t.editor.code_ref().len_lines())
783 }
784
785 pub fn goto(&mut self, line: usize, col: Option<usize>, area: Rect) {
789 if let Some(t) = self.active_tab_mut() {
790 let last = t.editor.code_ref().len_lines().max(1);
791 let line = line.clamp(1, last);
792 let off = t
793 .editor
794 .code_ref()
795 .offset(line - 1, col.unwrap_or(1).saturating_sub(1));
796 t.editor.set_cursor(off);
797 t.editor.focus(&area);
798 }
799 }
800
801 pub fn cursor_line_home(&mut self) {
806 if let Some(t) = self.active_tab_mut() {
807 let cur = t.editor.get_cursor();
808 let code = t.editor.code_ref();
809 let row = code.char_to_line(cur);
810 let line_start = code.line_to_char(row);
811 let line_len = code.line_len(row);
812 let cur_col = cur - line_start;
813 let indent = code
814 .char_slice(line_start, line_start + line_len)
815 .chars()
816 .take_while(|c| *c == ' ' || *c == '\t')
817 .count();
818 let indent = if indent >= line_len { 0 } else { indent };
820 let target = if cur_col == indent { 0 } else { indent };
821 t.editor.set_cursor(line_start + target);
822 }
823 }
824
825 pub fn cursor_document_start(&mut self, area: Rect) {
828 if let Some(t) = self.active_tab_mut() {
829 t.editor.set_cursor(0);
830 t.editor.focus(&area);
831 }
832 }
833
834 pub fn cursor_document_end(&mut self, area: Rect) {
837 if let Some(t) = self.active_tab_mut() {
838 let code = t.editor.code_ref();
839 let last = code.len_lines().saturating_sub(1);
840 let off = code.line_to_char(last) + code.line_len(last);
841 t.editor.set_cursor(off);
842 t.editor.focus(&area);
843 }
844 }
845
846 pub fn cursor_line_end(&mut self) {
848 if let Some(t) = self.active_tab_mut() {
849 let cur = t.editor.get_cursor();
850 let code = t.editor.code_ref();
851 let row = code.char_to_line(cur);
852 let off = code.line_to_char(row) + code.line_len(row);
853 t.editor.set_cursor(off);
854 }
855 }
856
857 pub fn cursor_line_start(&mut self) {
860 if let Some(t) = self.active_tab_mut() {
861 let cur = t.editor.get_cursor();
862 let code = t.editor.code_ref();
863 let row = code.char_to_line(cur);
864 let off = code.line_to_char(row);
865 t.editor.set_cursor(off);
866 }
867 }
868
869 pub fn cursor_paragraph_start(&mut self, area: Rect) {
873 if let Some(t) = self.active_tab_mut() {
874 let cur = t.editor.get_cursor();
875 let off = {
876 let code = t.editor.code_ref();
877 let is_blank = |r: usize| line_is_blank(code, r);
878 let mut r = code.char_to_line(cur);
879 while r > 0 && is_blank(r) {
880 r -= 1;
881 }
882 while r > 0 && !is_blank(r - 1) {
883 r -= 1;
884 }
885 code.line_to_char(r)
886 };
887 t.editor.set_cursor(off);
888 t.editor.focus(&area);
889 }
890 }
891
892 pub fn cursor_paragraph_end(&mut self, area: Rect) {
895 if let Some(t) = self.active_tab_mut() {
896 let cur = t.editor.get_cursor();
897 let off = {
898 let code = t.editor.code_ref();
899 let n = code.len_lines().max(1);
900 let is_blank = |r: usize| line_is_blank(code, r);
901 let mut r = code.char_to_line(cur);
902 while r + 1 < n && is_blank(r) {
903 r += 1;
904 }
905 while r + 1 < n && !is_blank(r + 1) {
906 r += 1;
907 }
908 code.line_to_char(r) + code.line_len(r)
909 };
910 t.editor.set_cursor(off);
911 t.editor.focus(&area);
912 }
913 }
914
915 pub fn cursor_section_start(&mut self, area: Rect) {
919 if let Some(t) = self.active_tab_mut() {
920 let cur = t.editor.get_cursor();
921 let off = {
922 let code = t.editor.code_ref();
923 let n = code.len_lines().max(1);
924 let sep = |r: usize| is_section_break(code, r, n);
925 let mut r = code.char_to_line(cur);
926 while r > 0 && !sep(r - 1) {
927 r -= 1;
928 }
929 code.line_to_char(r)
930 };
931 t.editor.set_cursor(off);
932 t.editor.focus(&area);
933 }
934 }
935
936 pub fn cursor_section_end(&mut self, area: Rect) {
939 if let Some(t) = self.active_tab_mut() {
940 let cur = t.editor.get_cursor();
941 let off = {
942 let code = t.editor.code_ref();
943 let n = code.len_lines().max(1);
944 let sep = |r: usize| is_section_break(code, r, n);
945 let mut r = code.char_to_line(cur);
946 while r + 1 < n && !sep(r + 1) {
947 r += 1;
948 }
949 code.line_to_char(r) + code.line_len(r)
950 };
951 t.editor.set_cursor(off);
952 t.editor.focus(&area);
953 }
954 }
955
956 pub fn cursor_line_down(&mut self, area: Rect) {
958 if let Some(t) = self.active_tab_mut() {
959 t.editor.cursor_down();
960 t.editor.focus(&area);
961 }
962 }
963
964 pub fn cursor_line_up(&mut self, area: Rect) {
966 if let Some(t) = self.active_tab_mut() {
967 t.editor.cursor_up();
968 t.editor.focus(&area);
969 }
970 }
971
972 fn cursor_row(&self) -> usize {
974 self.active_tab().map_or(0, |t| {
975 let code = t.editor.code_ref();
976 code.char_to_line(t.editor.get_cursor())
977 })
978 }
979
980 #[must_use]
982 pub fn cursor_line(&self) -> usize {
983 self.cursor_row()
984 }
985
986 pub fn poll_parse(&mut self) -> bool {
989 self.active_tab_mut().is_some_and(|t| t.editor.poll_parse())
990 }
991
992 #[must_use]
994 pub fn parse_pending(&self) -> bool {
995 self.active_tab().is_some_and(|t| t.editor.parse_pending())
996 }
997
998 pub fn switch_undo_branch(&mut self) -> bool {
1001 self.active_tab_mut()
1002 .is_some_and(|t| t.editor.switch_undo_branch())
1003 }
1004
1005 #[must_use]
1008 pub fn top_visible_line(&self) -> usize {
1009 self.active_tab().map_or(0, |t| t.editor.top_line())
1010 }
1011
1012 pub fn set_word_marks(&mut self, marks: Vec<(usize, usize)>) {
1014 if let Some(t) = self.active_tab_mut() {
1015 t.editor.set_word_marks(marks);
1016 }
1017 }
1018
1019 fn block_starts(&self, section: bool) -> Vec<usize> {
1023 let Some(t) = self.active_tab() else {
1024 return Vec::new();
1025 };
1026 let code = t.editor.code_ref();
1027 let n = code.len_lines().max(1);
1028 (0..n)
1029 .filter(|&r| {
1030 if section {
1031 !is_section_break(code, r, n) && (r == 0 || is_section_break(code, r - 1, n))
1032 } else {
1033 !line_is_blank(code, r) && (r == 0 || line_is_blank(code, r - 1))
1034 }
1035 })
1036 .collect()
1037 }
1038
1039 fn goto_row(&mut self, row: usize, area: Rect) {
1041 if let Some(t) = self.active_tab_mut() {
1042 let off = t.editor.code_ref().line_to_char(row);
1043 t.editor.set_cursor(off);
1044 t.editor.focus(&area);
1045 }
1046 }
1047
1048 pub fn cursor_paragraph_next(&mut self, area: Rect) {
1050 let row = self.cursor_row();
1051 if let Some(&next) = self.block_starts(false).iter().find(|&&r| r > row) {
1052 self.goto_row(next, area);
1053 }
1054 }
1055
1056 pub fn cursor_paragraph_prev(&mut self, area: Rect) {
1058 let row = self.cursor_row();
1059 if let Some(&prev) = self.block_starts(false).iter().rev().find(|&&r| r < row) {
1060 self.goto_row(prev, area);
1061 }
1062 }
1063
1064 pub fn goto_paragraph(&mut self, n: usize, area: Rect) {
1066 if let Some(&row) = self.block_starts(false).get(n.saturating_sub(1)) {
1067 self.goto_row(row, area);
1068 }
1069 }
1070
1071 pub fn cursor_section_next(&mut self, area: Rect) {
1073 let row = self.cursor_row();
1074 if let Some(&next) = self.block_starts(true).iter().find(|&&r| r > row) {
1075 self.goto_row(next, area);
1076 }
1077 }
1078
1079 pub fn cursor_section_prev(&mut self, area: Rect) {
1081 let row = self.cursor_row();
1082 if let Some(&prev) = self.block_starts(true).iter().rev().find(|&&r| r < row) {
1083 self.goto_row(prev, area);
1084 }
1085 }
1086
1087 pub fn goto_section(&mut self, n: usize, area: Rect) {
1089 if let Some(&row) = self.block_starts(true).get(n.saturating_sub(1)) {
1090 self.goto_row(row, area);
1091 }
1092 }
1093
1094 pub fn goto_offset(&mut self, off: usize, area: Rect) {
1097 if let Some(t) = self.active_tab_mut() {
1098 t.editor.set_cursor(off);
1099 t.editor.focus(&area);
1100 }
1101 }
1102
1103 pub fn cursor_sentence_start(&mut self, area: Rect) {
1105 let Some(t) = self.active_tab() else { return };
1106 let cur = t.editor.get_cursor();
1107 let off = sentence_starts(&t.editor.get_content())
1108 .into_iter()
1109 .rev()
1110 .find(|&s| s <= cur)
1111 .unwrap_or(0);
1112 self.goto_offset(off, area);
1113 }
1114
1115 pub fn cursor_sentence_end(&mut self, area: Rect) {
1118 let Some(t) = self.active_tab() else { return };
1119 let cur = t.editor.get_cursor();
1120 let text = t.editor.get_content();
1121 let chars: Vec<char> = text.chars().collect();
1122 let off = match sentence_starts(&text).into_iter().find(|&s| s > cur) {
1123 Some(mut e) => {
1124 while e > 0 && chars.get(e - 1).is_some_and(|c| c.is_whitespace()) {
1125 e -= 1;
1126 }
1127 e
1128 }
1129 None => chars.len(),
1130 };
1131 self.goto_offset(off, area);
1132 }
1133
1134 pub fn cursor_sentence_next(&mut self, area: Rect) {
1136 let Some(t) = self.active_tab() else { return };
1137 let cur = t.editor.get_cursor();
1138 if let Some(next) = sentence_starts(&t.editor.get_content())
1139 .into_iter()
1140 .find(|&s| s > cur)
1141 {
1142 self.goto_offset(next, area);
1143 }
1144 }
1145
1146 pub fn cursor_sentence_prev(&mut self, area: Rect) {
1148 let Some(t) = self.active_tab() else { return };
1149 let cur = t.editor.get_cursor();
1150 if let Some(prev) = sentence_starts(&t.editor.get_content())
1151 .into_iter()
1152 .rev()
1153 .find(|&s| s < cur)
1154 {
1155 self.goto_offset(prev, area);
1156 }
1157 }
1158
1159 pub fn goto_sentence(&mut self, n: usize, area: Rect) {
1161 let Some(t) = self.active_tab() else { return };
1162 let starts = sentence_starts(&t.editor.get_content());
1163 if let Some(&off) = starts.get(n.saturating_sub(1)) {
1164 self.goto_offset(off, area);
1165 }
1166 }
1167
1168 pub fn cursor_word_start(&mut self, area: Rect) {
1171 let Some(t) = self.active_tab() else { return };
1172 let cur = t.editor.get_cursor();
1173 let off = word_starts(&t.editor.get_content())
1174 .into_iter()
1175 .rev()
1176 .find(|&s| s <= cur)
1177 .unwrap_or(0);
1178 self.goto_offset(off, area);
1179 }
1180
1181 pub fn cursor_word_end(&mut self, area: Rect) {
1183 let Some(t) = self.active_tab() else { return };
1184 let cur = t.editor.get_cursor();
1185 let chars: Vec<char> = t.editor.get_content().chars().collect();
1186 let mut e = cur;
1188 while e < chars.len() && !is_word_char(chars[e]) {
1189 e += 1;
1190 }
1191 while e < chars.len() && is_word_char(chars[e]) {
1192 e += 1;
1193 }
1194 self.goto_offset(e, area);
1195 }
1196
1197 pub fn cursor_word_next(&mut self, area: Rect) {
1199 let Some(t) = self.active_tab() else { return };
1200 let cur = t.editor.get_cursor();
1201 if let Some(next) = word_starts(&t.editor.get_content())
1202 .into_iter()
1203 .find(|&s| s > cur)
1204 {
1205 self.goto_offset(next, area);
1206 }
1207 }
1208
1209 pub fn cursor_word_prev(&mut self, area: Rect) {
1211 let Some(t) = self.active_tab() else { return };
1212 let cur = t.editor.get_cursor();
1213 if let Some(prev) = word_starts(&t.editor.get_content())
1214 .into_iter()
1215 .rev()
1216 .find(|&s| s < cur)
1217 {
1218 self.goto_offset(prev, area);
1219 }
1220 }
1221
1222 pub fn goto_word(&mut self, n: usize, area: Rect) {
1224 let Some(t) = self.active_tab() else { return };
1225 let starts = word_starts(&t.editor.get_content());
1226 if let Some(&off) = starts.get(n.saturating_sub(1)) {
1227 self.goto_offset(off, area);
1228 }
1229 }
1230
1231 pub fn goto_percent(&mut self, pct: usize, area: Rect) {
1233 let Some(t) = self.active_tab() else { return };
1234 let total = t.editor.code_ref().len_lines().max(1);
1235 let line = pct.min(100) * total.saturating_sub(1) / 100;
1236 self.goto(line + 1, None, area); }
1238
1239 pub fn goto_byte(&mut self, byte: usize, area: Rect) {
1241 let Some(t) = self.active_tab() else { return };
1242 let code = t.editor.code_ref();
1243 let off = code.byte_to_char(byte.min(code.len_bytes()));
1244 self.goto_offset(off, area);
1245 }
1246
1247 pub fn select_line(&mut self, area: Rect) {
1250 if let Some(t) = self.active_tab_mut() {
1251 let (start, end) = {
1252 let code = t.editor.code_ref();
1253 let row = code.char_to_line(t.editor.get_cursor());
1254 let start = code.line_to_char(row);
1255 let end = if row + 1 < code.len_lines() {
1256 code.line_to_char(row + 1)
1257 } else {
1258 start + code.line_len(row)
1259 };
1260 (start, end)
1261 };
1262 t.editor.set_selection_range(start, end);
1263 t.editor.focus(&area);
1264 }
1265 }
1266
1267 pub fn select_paragraph(&mut self, area: Rect) {
1270 if let Some(t) = self.active_tab_mut() {
1271 let (start, end) = {
1272 let code = t.editor.code_ref();
1273 let n = code.len_lines().max(1);
1274 let is_blank = |r: usize| line_is_blank(code, r);
1275 let row = code.char_to_line(t.editor.get_cursor());
1276 let mut s = row;
1277 while s > 0 && is_blank(s) {
1278 s -= 1;
1279 }
1280 while s > 0 && !is_blank(s - 1) {
1281 s -= 1;
1282 }
1283 let mut e = row;
1284 while e + 1 < n && is_blank(e) {
1285 e += 1;
1286 }
1287 while e + 1 < n && !is_blank(e + 1) {
1288 e += 1;
1289 }
1290 (
1291 code.line_to_char(s),
1292 code.line_to_char(e) + code.line_len(e),
1293 )
1294 };
1295 t.editor.set_selection_range(start, end);
1296 t.editor.focus(&area);
1297 }
1298 }
1299
1300 pub fn select_section(&mut self, area: Rect) {
1303 if let Some(t) = self.active_tab_mut() {
1304 let (start, end) = {
1305 let code = t.editor.code_ref();
1306 let n = code.len_lines().max(1);
1307 let sep = |r: usize| is_section_break(code, r, n);
1308 let row = code.char_to_line(t.editor.get_cursor());
1309 let mut s = row;
1310 while s > 0 && !sep(s - 1) {
1311 s -= 1;
1312 }
1313 let mut e = row;
1314 while e + 1 < n && !sep(e + 1) {
1315 e += 1;
1316 }
1317 (
1318 code.line_to_char(s),
1319 code.line_to_char(e) + code.line_len(e),
1320 )
1321 };
1322 t.editor.set_selection_range(start, end);
1323 t.editor.focus(&area);
1324 }
1325 }
1326
1327 pub fn insert_str(&mut self, text: &str, area: Rect) -> bool {
1331 if let Some(t) = self.active_tab_mut() {
1332 if t.is_image() || t.read_only {
1333 return false;
1334 }
1335 t.editor.apply(InsertText {
1336 text: text.to_string(),
1337 });
1338 t.editor.focus(&area);
1339 t.dirty = true;
1340 t.preview = false;
1341 return true;
1342 }
1343 false
1344 }
1345
1346 pub fn select_all(&mut self) {
1348 if let Some(t) = self.active_tab_mut() {
1349 t.editor.apply(SelectAll {});
1350 }
1351 }
1352
1353 pub fn duplicate_line(&mut self) {
1355 if let Some(t) = self.active_tab_mut() {
1356 if t.is_image() {
1357 return;
1358 }
1359 t.editor.apply(Duplicate {});
1360 t.dirty = true;
1361 t.preview = false;
1362 }
1363 }
1364
1365 pub fn join_lines(&mut self) {
1368 if let Some(t) = self.active_tab_mut() {
1369 if t.is_image() {
1370 return;
1371 }
1372 t.editor.join_lines();
1373 t.dirty = true;
1374 t.preview = false;
1375 }
1376 }
1377
1378 pub fn sort_lines(&mut self) {
1381 if let Some(t) = self.active_tab_mut() {
1382 if t.is_image() {
1383 return;
1384 }
1385 t.editor.sort_lines();
1386 t.dirty = true;
1387 t.preview = false;
1388 }
1389 }
1390
1391 pub fn trim_trailing_whitespace(&mut self) {
1393 self.edit_active_lines(CodeEditor::trim_trailing_whitespace);
1394 }
1395
1396 pub fn remove_duplicate_lines(&mut self) {
1398 self.edit_active_lines(CodeEditor::remove_duplicate_lines);
1399 }
1400
1401 pub fn reverse_lines(&mut self) {
1403 self.edit_active_lines(CodeEditor::reverse_lines);
1404 }
1405
1406 pub fn sort_unique(&mut self) {
1408 self.edit_active_lines(CodeEditor::sort_unique);
1409 }
1410
1411 pub fn shuffle_lines(&mut self) {
1413 self.edit_active_lines(CodeEditor::shuffle_lines);
1414 }
1415
1416 fn edit_active_lines(&mut self, f: impl FnOnce(&mut CodeEditor)) {
1418 if let Some(t) = self.active_tab_mut() {
1419 if t.is_image() {
1420 return;
1421 }
1422 f(&mut t.editor);
1423 t.dirty = true;
1424 t.preview = false;
1425 }
1426 }
1427
1428 pub fn move_line(&mut self, down: bool, area: Rect) {
1431 if let Some(t) = self.active_tab_mut() {
1432 if t.is_image() {
1433 return;
1434 }
1435 if down {
1436 t.editor.move_line_down();
1437 } else {
1438 t.editor.move_line_up();
1439 }
1440 t.editor.focus(&area);
1441 t.dirty = true;
1442 t.preview = false;
1443 }
1444 }
1445
1446 pub fn select_word(&mut self, forward: bool, area: Rect) {
1450 if let Some(t) = self.active_tab_mut() {
1451 if t.is_image() {
1452 return;
1453 }
1454 let text: Vec<char> = t.text().chars().collect();
1455 let cur = t.editor.get_cursor();
1456 let target = if forward {
1457 next_word(&text, cur)
1458 } else {
1459 prev_word(&text, cur)
1460 };
1461 if target != cur {
1462 t.editor.extend_selection(target);
1463 t.editor.set_cursor(target);
1464 t.editor.focus(&area);
1465 }
1466 }
1467 }
1468
1469 pub fn jump_matching_bracket(&mut self, area: Rect) {
1472 if let Some(t) = self.active_tab_mut()
1473 && let Some(off) = t.editor.matching_bracket_offset()
1474 {
1475 t.editor.set_cursor(off);
1476 t.editor.focus(&area);
1477 }
1478 }
1479
1480 pub fn delete_forward(&mut self) {
1482 if let Some(t) = self.active_tab_mut() {
1483 t.editor.apply(MoveRight { shift: false });
1484 t.editor.apply(Delete {});
1485 }
1486 }
1487
1488 pub fn page_up(&mut self, lines: usize) {
1490 if let Some(t) = self.active_tab_mut() {
1491 for _ in 0..lines.max(1) {
1492 t.editor.apply(MoveUp { shift: false });
1493 }
1494 }
1495 }
1496
1497 pub fn page_down(&mut self, lines: usize) {
1499 if let Some(t) = self.active_tab_mut() {
1500 for _ in 0..lines.max(1) {
1501 t.editor.apply(MoveDown { shift: false });
1502 }
1503 }
1504 }
1505}
1506
1507fn is_word_char(c: char) -> bool {
1509 c.is_alphanumeric() || c == '_'
1510}
1511
1512fn next_word(text: &[char], pos: usize) -> usize {
1515 let n = text.len();
1516 let mut i = pos.min(n);
1517 while i < n && !is_word_char(text[i]) {
1518 i += 1;
1519 }
1520 while i < n && is_word_char(text[i]) {
1521 i += 1;
1522 }
1523 i
1524}
1525
1526fn prev_word(text: &[char], pos: usize) -> usize {
1529 let mut i = pos.min(text.len());
1530 while i > 0 && !is_word_char(text[i - 1]) {
1531 i -= 1;
1532 }
1533 while i > 0 && is_word_char(text[i - 1]) {
1534 i -= 1;
1535 }
1536 i
1537}
1538
1539#[cfg(test)]
1540mod nav_tests {
1541 use super::{sentence_starts, word_starts};
1542
1543 #[test]
1544 fn word_starts_finds_each_word() {
1545 assert_eq!(word_starts("foo bar_baz qux"), vec![0, 5, 13]);
1547 assert_eq!(word_starts(" leading"), vec![3]);
1548 assert!(word_starts("...").is_empty());
1549 }
1550
1551 #[test]
1552 fn sentence_starts_splits_on_terminators() {
1553 let text = "One. Two! Three? Four.";
1554 assert_eq!(sentence_starts(text), vec![0, 5, 10, 18]);
1556 assert_eq!(sentence_starts("pi is 3.14 today"), vec![0]);
1558 }
1559}