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;
15#[cfg(feature = "image")]
16pub use ratatui_image::protocol::StatefulProtocol as ImageProtocol;
17
18#[cfg(not(feature = "image"))]
21pub enum ImageProtocol {}
22use vix_editor_core::actions::{
23 Delete, Duplicate, InsertText, MoveDown, MoveRight, MoveUp, SelectAll,
24};
25use vix_editor_core::code::Code;
26pub use vix_editor_core::editor::Editor as CodeEditor;
27use vix_editor_core::utils::get_lang;
28use vix_textops::sentence_starts;
31
32use vix_theme as theme;
33
34fn line_is_blank(code: &Code, row: usize) -> bool {
37 if row >= code.len_lines() {
38 return true;
39 }
40 let start = code.line_to_char(row);
41 let len = code.line_len(row);
42 code.char_slice(start, start + len)
43 .chars()
44 .all(|c: char| c.is_whitespace())
45}
46
47fn is_section_break(code: &Code, row: usize, n: usize) -> bool {
50 line_is_blank(code, row)
51 && ((row > 0 && line_is_blank(code, row - 1))
52 || (row + 1 < n && line_is_blank(code, row + 1)))
53}
54
55fn word_starts(text: &str) -> Vec<usize> {
58 let mut starts = Vec::new();
59 let mut prev_word = false;
60 for (i, c) in text.chars().enumerate() {
61 let word = is_word_char(c);
62 if word && !prev_word {
63 starts.push(i);
64 }
65 prev_word = word;
66 }
67 starts
68}
69
70#[derive(Clone, Copy)]
72pub struct SaveOptions {
73 pub trim_trailing_whitespace: bool,
75 pub ensure_final_newline: bool,
77}
78
79fn trim_trailing_whitespace(text: &str) -> String {
82 let mut out = String::with_capacity(text.len());
83 let mut lines = text.split('\n');
84 if let Some(first) = lines.next() {
85 out.push_str(first.trim_end_matches([' ', '\t']));
86 }
87 for line in lines {
88 out.push('\n');
89 out.push_str(line.trim_end_matches([' ', '\t']));
90 }
91 out
92}
93
94#[must_use]
96pub fn is_image_path(path: &Path) -> bool {
97 matches!(
98 path.extension()
99 .and_then(|e| e.to_str())
100 .map(str::to_ascii_lowercase)
101 .as_deref(),
102 Some("png" | "jpg" | "jpeg" | "gif" | "bmp" | "webp" | "ico" | "tiff" | "tif")
103 )
104}
105
106pub const SEARCH_MARK: &str = "search";
109
110fn apply_theme(ed: &mut CodeEditor) {
116 ed.set_text_style(
117 Style::default()
118 .fg(theme::region_fg(theme::Region::Editor))
119 .add_modifier(theme::region_modifiers(theme::Region::Editor)),
120 );
121 ed.set_line_number_style(theme::dim());
122 ed.set_selection_style(theme::selected());
123 ed.set_whitespace_style(theme::dim());
124 ed.set_bracket_style(theme::selected());
125
126 let syntax = theme::syntax_theme();
128 let pairs: Vec<(&str, &str)> = syntax.iter().map(|(t, hex)| (*t, hex.as_str())).collect();
129 ed.set_syntax_theme(&pairs);
130
131 let cursor = match theme::editor_cursor() {
134 Some(color) => Style::default()
135 .bg(color)
136 .fg(theme::region_bg(theme::Region::Editor)),
137 None => theme::selected(),
138 };
139 ed.set_cursor_style(Some(cursor));
140}
141
142fn make_editor(path: Option<&Path>, text: &str, flags: Flags, indent: &str) -> CodeEditor {
143 let name = path
144 .and_then(|p| p.file_name())
145 .map(|s| s.to_string_lossy().into_owned())
146 .unwrap_or_default();
147 let lang = if name.is_empty() {
148 "text".to_string()
149 } else {
150 get_lang(&name)
151 };
152 let mut ed = CodeEditor::new(&lang, text, Vec::new())
156 .or_else(|_| CodeEditor::new("text", text, Vec::new()))
157 .expect("code editor init for plain text never fails");
158 ed.show_line_numbers(flags.contains(Flags::LINE_NUMBERS));
159 ed.show_whitespace(flags.contains(Flags::SHOW_WHITESPACE));
160 ed.set_soft_wrap(flags.contains(Flags::SOFT_WRAP));
161 ed.set_indent(Some(indent.to_string()));
162 apply_theme(&mut ed);
163 ed
164}
165
166#[must_use]
174pub fn text_field(content: &str) -> CodeEditor {
175 let mut ed = CodeEditor::new("text", content, Vec::new())
176 .expect("text field init for plain text never fails");
177 ed.show_line_numbers(false);
178 apply_theme(&mut ed);
179 ed
180}
181
182#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
184pub enum FsAccess {
185 #[default]
187 Writable,
188 ReadOnly,
190}
191
192pub struct Tab {
194 pub editor: CodeEditor,
196 pub path: Option<PathBuf>,
198 pub dirty: bool,
200 pub preview: bool,
202 pub image: Option<ImageProtocol>,
204 pub read_only: bool,
206 pub fs_access: FsAccess,
210}
211
212impl Tab {
213 pub fn is_image(&self) -> bool {
215 self.image.is_some()
216 }
217
218 pub fn text(&self) -> String {
220 self.editor.get_content()
221 }
222
223 pub fn lines(&self) -> Vec<String> {
225 self.text()
226 .lines()
227 .map(std::string::ToString::to_string)
228 .collect()
229 }
230
231 pub fn title(&self) -> String {
233 let name = self.path.as_ref().and_then(|p| p.file_name()).map_or_else(
234 || "untitled".to_string(),
235 |s| s.to_string_lossy().into_owned(),
236 );
237 let icon = theme::file_icon(&name);
238 let flag = if self.dirty {
239 format!(" {}", theme::icon::FILE_DIRTY)
240 } else {
241 String::new()
242 };
243 let lock = if self.fs_access == FsAccess::ReadOnly {
244 " 🔒"
245 } else {
246 ""
247 };
248 format!("{icon} {name}{flag}{lock}")
249 }
250
251 pub fn display_path(&self) -> String {
253 self.path
254 .as_ref()
255 .map_or_else(|| "untitled".to_string(), |p| p.display().to_string())
256 }
257
258 pub fn cursor_1based(&self) -> (usize, usize) {
260 let cur = self.editor.get_cursor();
261 let code = self.editor.code_ref();
262 let row = code.char_to_line(cur);
263 let col = cur - code.line_to_char(row);
264 (row + 1, col + 1)
265 }
266
267 #[must_use]
269 pub fn line_count(&self) -> usize {
270 self.editor.code_ref().len_lines()
271 }
272}
273
274#[derive(Clone, Copy, PartialEq, Eq, Debug)]
276pub enum SplitDir {
277 Vertical,
279 Horizontal,
281}
282
283bitflags::bitflags! {
284 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
291 pub struct Flags: u8 {
292 const LINE_NUMBERS = 1 << 0;
294 const RELATIVE_LINE_NUMBERS = 1 << 1;
296 const SHOW_WHITESPACE = 1 << 2;
298 const SOFT_WRAP = 1 << 3;
300 }
301}
302
303pub struct Editor {
305 pub tabs: Vec<Tab>,
307 pub active: usize,
309 pub flags: Flags,
312 pub indent: String,
314 pub split_root: Option<crate::pane_tree::Pane>,
317 pub focused_leaf: usize,
319 pub max_text_bytes: u64,
323}
324
325impl Default for Editor {
326 fn default() -> Self {
327 Editor::new(Flags::LINE_NUMBERS, " ".to_string())
328 }
329}
330
331impl Editor {
332 #[must_use]
335 pub fn new(flags: Flags, indent: String) -> Self {
336 let mut e = Editor {
337 tabs: Vec::new(),
338 active: 0,
339 flags,
340 indent,
341 split_root: None,
342 focused_leaf: 0,
343 max_text_bytes: vix_fileops::MAX_TEXT_BYTES,
344 };
345 e.new_tab();
346 e
347 }
348
349 #[must_use]
351 pub fn active_tab(&self) -> Option<&Tab> {
352 self.tabs.get(self.active)
353 }
354
355 pub fn active_tab_mut(&mut self) -> Option<&mut Tab> {
357 self.tabs.get_mut(self.active)
358 }
359
360 #[must_use]
362 pub fn is_split(&self) -> bool {
363 self.split_root.is_some()
364 }
365
366 pub fn set_split(&mut self, dir: SplitDir) {
370 let other = (0..self.tabs.len())
371 .find(|&i| i != self.active)
372 .unwrap_or(self.active);
373 match self.split_root.as_mut() {
374 None => {
375 self.split_root = Some(crate::pane_tree::Pane::Split {
376 dir,
377 ratio: 50,
378 first: Box::new(crate::pane_tree::Pane::Leaf(self.active)),
379 second: Box::new(crate::pane_tree::Pane::Leaf(other)),
380 });
381 self.focused_leaf = 0;
382 }
383 Some(root) => {
384 root.set_leaf_tab(self.focused_leaf, self.active);
385 root.split_leaf(self.focused_leaf, dir, other);
386 }
387 }
388 }
389
390 pub fn unsplit(&mut self) {
393 let Some(root) = self.split_root.take() else {
394 return;
395 };
396 match root.remove_leaf(self.focused_leaf) {
397 Some(tree) if tree.leaf_count() > 1 => {
398 self.focused_leaf = self.focused_leaf.min(tree.leaf_count() - 1);
399 if let Some(tab) = tree.leaf_tab(self.focused_leaf) {
400 self.active = tab.min(self.tabs.len().saturating_sub(1));
401 }
402 self.split_root = Some(tree);
403 }
404 Some(tree) => {
405 if let Some(tab) = tree.leaf_tab(0) {
406 self.active = tab.min(self.tabs.len().saturating_sub(1));
407 }
408 self.split_root = None;
409 self.focused_leaf = 0;
410 }
411 None => {
412 self.split_root = None;
413 self.focused_leaf = 0;
414 }
415 }
416 }
417
418 fn sync_split(&mut self) {
421 let count = self.tabs.len();
422 if let Some(root) = self.split_root.as_mut() {
423 root.set_leaf_tab(self.focused_leaf, self.active);
424 root.clamp_tabs(count);
425 self.focused_leaf = self.focused_leaf.min(root.leaf_count().saturating_sub(1));
426 }
427 }
428
429 pub fn focus_leaf(&mut self, i: usize) {
431 self.sync_split();
432 let Some(root) = self.split_root.as_ref() else {
433 return;
434 };
435 if let Some(tab) = root.leaf_tab(i) {
436 self.focused_leaf = i;
437 self.active = tab.min(self.tabs.len().saturating_sub(1));
438 }
439 }
440
441 pub fn focus_other_pane(&mut self) {
443 self.sync_split();
444 let Some(root) = self.split_root.as_ref() else {
445 return;
446 };
447 let n = root.leaf_count();
448 if n > 0 {
449 self.focus_leaf((self.focused_leaf + 1) % n);
450 }
451 }
452
453 #[must_use]
455 pub fn focused_leaf(&self) -> usize {
456 self.focused_leaf
457 }
458
459 pub fn split_layout(&mut self, area: Rect) -> Vec<crate::pane_tree::LeafBox> {
462 self.sync_split();
463 self.split_root
464 .as_ref()
465 .map(|r| r.layout(area))
466 .unwrap_or_default()
467 }
468
469 #[must_use]
471 pub fn split_dividers(&self, area: Rect) -> Vec<(SplitDir, Rect)> {
472 self.split_root
473 .as_ref()
474 .map(|r| r.dividers(area))
475 .unwrap_or_default()
476 }
477
478 pub fn focus_pane_at(&mut self, area: Rect, col: u16, row: u16) -> bool {
481 self.sync_split();
482 let Some(leaf) = self
483 .split_root
484 .as_ref()
485 .and_then(|r| r.leaf_at(area, col, row))
486 else {
487 return false;
488 };
489 self.focus_leaf(leaf);
490 true
491 }
492
493 pub fn resize_split_at(&mut self, area: Rect, col: u16, row: u16) -> bool {
496 self.split_root
497 .as_mut()
498 .is_some_and(|r| r.resize_at(area, col, row))
499 }
500
501 pub fn new_tab(&mut self) {
503 let editor = make_editor(None, "", self.flags, &self.indent);
504 self.tabs.push(Tab {
505 editor,
506 path: None,
507 dirty: false,
508 preview: false,
509 image: None,
510 read_only: false,
511 fs_access: FsAccess::Writable,
512 });
513 self.active = self.tabs.len() - 1;
514 }
515
516 pub fn new_tab_with_content(&mut self, content: &str) {
519 let editor = make_editor(None, content, self.flags, &self.indent);
520 self.tabs.push(Tab {
521 editor,
522 path: None,
523 dirty: true,
524 preview: false,
525 image: None,
526 read_only: false,
527 fs_access: FsAccess::Writable,
528 });
529 self.active = self.tabs.len() - 1;
530 }
531
532 #[cfg(feature = "image")]
534 pub fn open_image(&mut self, path: &Path, proto: ImageProtocol) {
535 let canon = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
536 if let Some(i) = self
537 .tabs
538 .iter()
539 .position(|t| t.path.as_deref() == Some(canon.as_path()))
540 {
541 self.tabs[i].image = Some(proto);
542 self.active = i;
543 return;
544 }
545 let editor = make_editor(Some(&canon), "", self.flags, &self.indent);
546 self.tabs.push(Tab {
547 editor,
548 path: Some(canon),
549 dirty: false,
550 preview: false,
551 image: Some(proto),
552 read_only: false,
553 fs_access: FsAccess::Writable,
554 });
555 self.active = self.tabs.len() - 1;
556 }
557
558 pub fn refresh_line_numbers(&mut self) {
561 for tab in &mut self.tabs {
562 tab.editor
563 .show_line_numbers(self.flags.contains(Flags::LINE_NUMBERS));
564 tab.editor
565 .set_relative_line_numbers(self.flags.contains(Flags::RELATIVE_LINE_NUMBERS));
566 }
567 }
568
569 pub fn refresh_whitespace(&mut self) {
571 for tab in &mut self.tabs {
572 tab.editor
573 .show_whitespace(self.flags.contains(Flags::SHOW_WHITESPACE));
574 }
575 }
576
577 pub fn refresh_soft_wrap(&mut self) {
579 for tab in &mut self.tabs {
580 tab.editor
581 .set_soft_wrap(self.flags.contains(Flags::SOFT_WRAP));
582 }
583 }
584
585 pub fn refresh_theme(&mut self) {
587 for tab in &mut self.tabs {
588 apply_theme(&mut tab.editor);
589 }
590 }
591
592 pub fn open(&mut self, path: &Path, preview: bool) -> io::Result<()> {
599 self.open_with_limit(path, preview, self.max_text_bytes)
600 }
601
602 pub fn open_unbounded(&mut self, path: &Path, preview: bool) -> io::Result<()> {
608 self.open_with_limit(path, preview, vix_fileops::HARD_MAX_TEXT_BYTES)
609 }
610
611 fn open_with_limit(&mut self, path: &Path, preview: bool, max_bytes: u64) -> io::Result<()> {
612 let canon = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
613 if let Some(i) = self
614 .tabs
615 .iter()
616 .position(|t| t.path.as_deref() == Some(canon.as_path()))
617 {
618 self.active = i;
619 if !preview {
620 self.tabs[i].preview = false;
621 }
622 return Ok(());
623 }
624
625 let fs_read_only = vix_fileops::is_unwritable(&canon);
628 let (content, lossy) = vix_fileops::read_text_guarded(&canon, max_bytes)?;
629 let editor = make_editor(Some(&canon), &content, self.flags, &self.indent);
630 let tab = Tab {
631 editor,
632 path: Some(canon),
633 dirty: false,
634 preview,
635 image: None,
636 read_only: lossy,
637 fs_access: if fs_read_only {
638 FsAccess::ReadOnly
639 } else {
640 FsAccess::Writable
641 },
642 };
643
644 if preview && let Some(i) = self.tabs.iter().position(|t| t.preview) {
645 self.tabs[i] = tab;
646 self.active = i;
647 return Ok(());
648 }
649 self.tabs.push(tab);
650 self.active = self.tabs.len() - 1;
651 Ok(())
652 }
653
654 pub fn reload_clean_from_disk(&mut self) -> usize {
659 let mut reloaded = 0;
660 for tab in &mut self.tabs {
661 if tab.dirty || tab.image.is_some() {
662 continue;
663 }
664 let Some(path) = tab.path.clone() else {
665 continue;
666 };
667 let Ok(content) = fs::read_to_string(&path) else {
668 continue;
669 };
670 if content == tab.editor.get_content() {
671 continue;
672 }
673 let cursor = tab.editor.get_cursor();
674 tab.editor.set_content(&content);
675 tab.editor.set_cursor(cursor.min(content.chars().count()));
676 tab.dirty = false;
677 reloaded += 1;
678 }
679 reloaded
680 }
681
682 pub fn promote_active(&mut self) {
684 if let Some(t) = self.active_tab_mut() {
685 t.preview = false;
686 }
687 }
688
689 pub fn save_active(&mut self, opts: SaveOptions) -> io::Result<PathBuf> {
695 let idx = self.active;
696 let path = self.tabs[idx]
697 .path
698 .clone()
699 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "buffer has no path"))?;
700 self.write_to(idx, &path, opts)?;
701 Ok(path)
702 }
703
704 pub fn save_active_as(&mut self, path: PathBuf, opts: SaveOptions) -> io::Result<PathBuf> {
710 let idx = self.active;
711 self.write_to(idx, &path, opts)?;
712 self.tabs[idx].path = Some(path.clone());
713 Ok(path)
714 }
715
716 fn write_to(&mut self, idx: usize, path: &Path, opts: SaveOptions) -> io::Result<()> {
717 let mut data = self.tabs[idx].text();
718 if opts.trim_trailing_whitespace {
719 data = trim_trailing_whitespace(&data);
720 }
721 if opts.ensure_final_newline && !data.is_empty() && !data.ends_with('\n') {
722 data.push('\n');
723 }
724 vix_fileops::write_atomic(path, data.as_bytes())?;
728 self.tabs[idx].dirty = false;
729 self.tabs[idx].preview = false;
730 Ok(())
731 }
732
733 pub fn close_active(&mut self) -> Option<PathBuf> {
736 if self.tabs.is_empty() {
737 return None;
738 }
739 let closed = self.tabs.remove(self.active).path;
740 if self.tabs.is_empty() {
741 self.new_tab();
742 } else if self.active >= self.tabs.len() {
743 self.active = self.tabs.len() - 1;
744 }
745 closed
746 }
747
748 pub fn close_all(&mut self) -> Vec<PathBuf> {
751 let closed: Vec<PathBuf> = self.tabs.drain(..).filter_map(|t| t.path).collect();
752 self.active = 0;
753 self.new_tab();
754 closed
755 }
756
757 pub fn next_tab(&mut self) {
759 if !self.tabs.is_empty() {
760 self.active = (self.active + 1) % self.tabs.len();
761 }
762 }
763
764 pub fn prev_tab(&mut self) {
766 if !self.tabs.is_empty() {
767 self.active = (self.active + self.tabs.len() - 1) % self.tabs.len();
768 }
769 }
770
771 #[must_use]
773 pub fn cursor_1based(&self) -> (usize, usize) {
774 self.active_tab().map_or((1, 1), Tab::cursor_1based)
775 }
776
777 #[must_use]
779 pub fn active_line_count(&self) -> usize {
780 self.active_tab()
781 .map_or(1, |t| t.editor.code_ref().len_lines())
782 }
783
784 pub fn goto(&mut self, line: usize, col: Option<usize>, area: Rect) {
788 if let Some(t) = self.active_tab_mut() {
789 let last = t.editor.code_ref().len_lines().max(1);
790 let line = line.clamp(1, last);
791 let off = t
792 .editor
793 .code_ref()
794 .offset(line - 1, col.unwrap_or(1).saturating_sub(1));
795 t.editor.set_cursor(off);
796 t.editor.focus(&area);
797 }
798 }
799
800 pub fn cursor_line_home(&mut self) {
805 if let Some(t) = self.active_tab_mut() {
806 let cur = t.editor.get_cursor();
807 let code = t.editor.code_ref();
808 let row = code.char_to_line(cur);
809 let line_start = code.line_to_char(row);
810 let line_len = code.line_len(row);
811 let cur_col = cur - line_start;
812 let indent = code
813 .char_slice(line_start, line_start + line_len)
814 .chars()
815 .take_while(|c| *c == ' ' || *c == '\t')
816 .count();
817 let indent = if indent >= line_len { 0 } else { indent };
819 let target = if cur_col == indent { 0 } else { indent };
820 t.editor.set_cursor(line_start + target);
821 }
822 }
823
824 pub fn cursor_document_start(&mut self, area: Rect) {
827 if let Some(t) = self.active_tab_mut() {
828 t.editor.set_cursor(0);
829 t.editor.focus(&area);
830 }
831 }
832
833 pub fn cursor_document_end(&mut self, area: Rect) {
836 if let Some(t) = self.active_tab_mut() {
837 let code = t.editor.code_ref();
838 let last = code.len_lines().saturating_sub(1);
839 let off = code.line_to_char(last) + code.line_len(last);
840 t.editor.set_cursor(off);
841 t.editor.focus(&area);
842 }
843 }
844
845 pub fn cursor_line_end(&mut self) {
847 if let Some(t) = self.active_tab_mut() {
848 let cur = t.editor.get_cursor();
849 let code = t.editor.code_ref();
850 let row = code.char_to_line(cur);
851 let off = code.line_to_char(row) + code.line_len(row);
852 t.editor.set_cursor(off);
853 }
854 }
855
856 pub fn cursor_line_start(&mut self) {
859 if let Some(t) = self.active_tab_mut() {
860 let cur = t.editor.get_cursor();
861 let code = t.editor.code_ref();
862 let row = code.char_to_line(cur);
863 let off = code.line_to_char(row);
864 t.editor.set_cursor(off);
865 }
866 }
867
868 pub fn cursor_paragraph_start(&mut self, area: Rect) {
872 if let Some(t) = self.active_tab_mut() {
873 let cur = t.editor.get_cursor();
874 let off = {
875 let code = t.editor.code_ref();
876 let is_blank = |r: usize| line_is_blank(code, r);
877 let mut r = code.char_to_line(cur);
878 while r > 0 && is_blank(r) {
879 r -= 1;
880 }
881 while r > 0 && !is_blank(r - 1) {
882 r -= 1;
883 }
884 code.line_to_char(r)
885 };
886 t.editor.set_cursor(off);
887 t.editor.focus(&area);
888 }
889 }
890
891 pub fn cursor_paragraph_end(&mut self, area: Rect) {
894 if let Some(t) = self.active_tab_mut() {
895 let cur = t.editor.get_cursor();
896 let off = {
897 let code = t.editor.code_ref();
898 let n = code.len_lines().max(1);
899 let is_blank = |r: usize| line_is_blank(code, r);
900 let mut r = code.char_to_line(cur);
901 while r + 1 < n && is_blank(r) {
902 r += 1;
903 }
904 while r + 1 < n && !is_blank(r + 1) {
905 r += 1;
906 }
907 code.line_to_char(r) + code.line_len(r)
908 };
909 t.editor.set_cursor(off);
910 t.editor.focus(&area);
911 }
912 }
913
914 pub fn cursor_section_start(&mut self, area: Rect) {
918 if let Some(t) = self.active_tab_mut() {
919 let cur = t.editor.get_cursor();
920 let off = {
921 let code = t.editor.code_ref();
922 let n = code.len_lines().max(1);
923 let sep = |r: usize| is_section_break(code, r, n);
924 let mut r = code.char_to_line(cur);
925 while r > 0 && !sep(r - 1) {
926 r -= 1;
927 }
928 code.line_to_char(r)
929 };
930 t.editor.set_cursor(off);
931 t.editor.focus(&area);
932 }
933 }
934
935 pub fn cursor_section_end(&mut self, area: Rect) {
938 if let Some(t) = self.active_tab_mut() {
939 let cur = t.editor.get_cursor();
940 let off = {
941 let code = t.editor.code_ref();
942 let n = code.len_lines().max(1);
943 let sep = |r: usize| is_section_break(code, r, n);
944 let mut r = code.char_to_line(cur);
945 while r + 1 < n && !sep(r + 1) {
946 r += 1;
947 }
948 code.line_to_char(r) + code.line_len(r)
949 };
950 t.editor.set_cursor(off);
951 t.editor.focus(&area);
952 }
953 }
954
955 pub fn cursor_line_down(&mut self, area: Rect) {
957 if let Some(t) = self.active_tab_mut() {
958 t.editor.cursor_down();
959 t.editor.focus(&area);
960 }
961 }
962
963 pub fn cursor_line_up(&mut self, area: Rect) {
965 if let Some(t) = self.active_tab_mut() {
966 t.editor.cursor_up();
967 t.editor.focus(&area);
968 }
969 }
970
971 fn cursor_row(&self) -> usize {
973 self.active_tab().map_or(0, |t| {
974 let code = t.editor.code_ref();
975 code.char_to_line(t.editor.get_cursor())
976 })
977 }
978
979 #[must_use]
981 pub fn cursor_line(&self) -> usize {
982 self.cursor_row()
983 }
984
985 pub fn poll_parse(&mut self) -> bool {
988 self.active_tab_mut().is_some_and(|t| t.editor.poll_parse())
989 }
990
991 #[must_use]
993 pub fn parse_pending(&self) -> bool {
994 self.active_tab().is_some_and(|t| t.editor.parse_pending())
995 }
996
997 pub fn switch_undo_branch(&mut self) -> bool {
1000 self.active_tab_mut()
1001 .is_some_and(|t| t.editor.switch_undo_branch())
1002 }
1003
1004 #[must_use]
1007 pub fn top_visible_line(&self) -> usize {
1008 self.active_tab().map_or(0, |t| t.editor.top_line())
1009 }
1010
1011 pub fn set_word_marks(&mut self, marks: Vec<(usize, usize)>) {
1013 if let Some(t) = self.active_tab_mut() {
1014 t.editor.set_word_marks(marks);
1015 }
1016 }
1017
1018 fn block_starts(&self, section: bool) -> Vec<usize> {
1022 let Some(t) = self.active_tab() else {
1023 return Vec::new();
1024 };
1025 let code = t.editor.code_ref();
1026 let n = code.len_lines().max(1);
1027 (0..n)
1028 .filter(|&r| {
1029 if section {
1030 !is_section_break(code, r, n) && (r == 0 || is_section_break(code, r - 1, n))
1031 } else {
1032 !line_is_blank(code, r) && (r == 0 || line_is_blank(code, r - 1))
1033 }
1034 })
1035 .collect()
1036 }
1037
1038 fn goto_row(&mut self, row: usize, area: Rect) {
1040 if let Some(t) = self.active_tab_mut() {
1041 let off = t.editor.code_ref().line_to_char(row);
1042 t.editor.set_cursor(off);
1043 t.editor.focus(&area);
1044 }
1045 }
1046
1047 pub fn cursor_paragraph_next(&mut self, area: Rect) {
1049 let row = self.cursor_row();
1050 if let Some(&next) = self.block_starts(false).iter().find(|&&r| r > row) {
1051 self.goto_row(next, area);
1052 }
1053 }
1054
1055 pub fn cursor_paragraph_prev(&mut self, area: Rect) {
1057 let row = self.cursor_row();
1058 if let Some(&prev) = self.block_starts(false).iter().rev().find(|&&r| r < row) {
1059 self.goto_row(prev, area);
1060 }
1061 }
1062
1063 pub fn goto_paragraph(&mut self, n: usize, area: Rect) {
1065 if let Some(&row) = self.block_starts(false).get(n.saturating_sub(1)) {
1066 self.goto_row(row, area);
1067 }
1068 }
1069
1070 pub fn cursor_section_next(&mut self, area: Rect) {
1072 let row = self.cursor_row();
1073 if let Some(&next) = self.block_starts(true).iter().find(|&&r| r > row) {
1074 self.goto_row(next, area);
1075 }
1076 }
1077
1078 pub fn cursor_section_prev(&mut self, area: Rect) {
1080 let row = self.cursor_row();
1081 if let Some(&prev) = self.block_starts(true).iter().rev().find(|&&r| r < row) {
1082 self.goto_row(prev, area);
1083 }
1084 }
1085
1086 pub fn goto_section(&mut self, n: usize, area: Rect) {
1088 if let Some(&row) = self.block_starts(true).get(n.saturating_sub(1)) {
1089 self.goto_row(row, area);
1090 }
1091 }
1092
1093 pub fn goto_offset(&mut self, off: usize, area: Rect) {
1096 if let Some(t) = self.active_tab_mut() {
1097 t.editor.set_cursor(off);
1098 t.editor.focus(&area);
1099 }
1100 }
1101
1102 pub fn cursor_sentence_start(&mut self, area: Rect) {
1104 let Some(t) = self.active_tab() else { return };
1105 let cur = t.editor.get_cursor();
1106 let off = sentence_starts(&t.editor.get_content())
1107 .into_iter()
1108 .rev()
1109 .find(|&s| s <= cur)
1110 .unwrap_or(0);
1111 self.goto_offset(off, area);
1112 }
1113
1114 pub fn cursor_sentence_end(&mut self, area: Rect) {
1117 let Some(t) = self.active_tab() else { return };
1118 let cur = t.editor.get_cursor();
1119 let text = t.editor.get_content();
1120 let chars: Vec<char> = text.chars().collect();
1121 let off = match sentence_starts(&text).into_iter().find(|&s| s > cur) {
1122 Some(mut e) => {
1123 while e > 0 && chars.get(e - 1).is_some_and(|c| c.is_whitespace()) {
1124 e -= 1;
1125 }
1126 e
1127 }
1128 None => chars.len(),
1129 };
1130 self.goto_offset(off, area);
1131 }
1132
1133 pub fn cursor_sentence_next(&mut self, area: Rect) {
1135 let Some(t) = self.active_tab() else { return };
1136 let cur = t.editor.get_cursor();
1137 if let Some(next) = sentence_starts(&t.editor.get_content())
1138 .into_iter()
1139 .find(|&s| s > cur)
1140 {
1141 self.goto_offset(next, area);
1142 }
1143 }
1144
1145 pub fn cursor_sentence_prev(&mut self, area: Rect) {
1147 let Some(t) = self.active_tab() else { return };
1148 let cur = t.editor.get_cursor();
1149 if let Some(prev) = sentence_starts(&t.editor.get_content())
1150 .into_iter()
1151 .rev()
1152 .find(|&s| s < cur)
1153 {
1154 self.goto_offset(prev, area);
1155 }
1156 }
1157
1158 pub fn goto_sentence(&mut self, n: usize, area: Rect) {
1160 let Some(t) = self.active_tab() else { return };
1161 let starts = sentence_starts(&t.editor.get_content());
1162 if let Some(&off) = starts.get(n.saturating_sub(1)) {
1163 self.goto_offset(off, area);
1164 }
1165 }
1166
1167 pub fn cursor_word_start(&mut self, area: Rect) {
1170 let Some(t) = self.active_tab() else { return };
1171 let cur = t.editor.get_cursor();
1172 let off = word_starts(&t.editor.get_content())
1173 .into_iter()
1174 .rev()
1175 .find(|&s| s <= cur)
1176 .unwrap_or(0);
1177 self.goto_offset(off, area);
1178 }
1179
1180 pub fn cursor_word_end(&mut self, area: Rect) {
1182 let Some(t) = self.active_tab() else { return };
1183 let cur = t.editor.get_cursor();
1184 let chars: Vec<char> = t.editor.get_content().chars().collect();
1185 let mut e = cur;
1187 while e < chars.len() && !is_word_char(chars[e]) {
1188 e += 1;
1189 }
1190 while e < chars.len() && is_word_char(chars[e]) {
1191 e += 1;
1192 }
1193 self.goto_offset(e, area);
1194 }
1195
1196 pub fn cursor_word_next(&mut self, area: Rect) {
1198 let Some(t) = self.active_tab() else { return };
1199 let cur = t.editor.get_cursor();
1200 if let Some(next) = word_starts(&t.editor.get_content())
1201 .into_iter()
1202 .find(|&s| s > cur)
1203 {
1204 self.goto_offset(next, area);
1205 }
1206 }
1207
1208 pub fn cursor_word_prev(&mut self, area: Rect) {
1210 let Some(t) = self.active_tab() else { return };
1211 let cur = t.editor.get_cursor();
1212 if let Some(prev) = word_starts(&t.editor.get_content())
1213 .into_iter()
1214 .rev()
1215 .find(|&s| s < cur)
1216 {
1217 self.goto_offset(prev, area);
1218 }
1219 }
1220
1221 pub fn goto_word(&mut self, n: usize, area: Rect) {
1223 let Some(t) = self.active_tab() else { return };
1224 let starts = word_starts(&t.editor.get_content());
1225 if let Some(&off) = starts.get(n.saturating_sub(1)) {
1226 self.goto_offset(off, area);
1227 }
1228 }
1229
1230 pub fn goto_percent(&mut self, pct: usize, area: Rect) {
1232 let Some(t) = self.active_tab() else { return };
1233 let total = t.editor.code_ref().len_lines().max(1);
1234 let line = pct.min(100) * total.saturating_sub(1) / 100;
1235 self.goto(line + 1, None, area); }
1237
1238 pub fn goto_byte(&mut self, byte: usize, area: Rect) {
1240 let Some(t) = self.active_tab() else { return };
1241 let code = t.editor.code_ref();
1242 let off = code.byte_to_char(byte.min(code.len_bytes()));
1243 self.goto_offset(off, area);
1244 }
1245
1246 pub fn select_line(&mut self, area: Rect) {
1249 if let Some(t) = self.active_tab_mut() {
1250 let (start, end) = {
1251 let code = t.editor.code_ref();
1252 let row = code.char_to_line(t.editor.get_cursor());
1253 let start = code.line_to_char(row);
1254 let end = if row + 1 < code.len_lines() {
1255 code.line_to_char(row + 1)
1256 } else {
1257 start + code.line_len(row)
1258 };
1259 (start, end)
1260 };
1261 t.editor.set_selection_range(start, end);
1262 t.editor.focus(&area);
1263 }
1264 }
1265
1266 pub fn select_paragraph(&mut self, area: Rect) {
1269 if let Some(t) = self.active_tab_mut() {
1270 let (start, end) = {
1271 let code = t.editor.code_ref();
1272 let n = code.len_lines().max(1);
1273 let is_blank = |r: usize| line_is_blank(code, r);
1274 let row = code.char_to_line(t.editor.get_cursor());
1275 let mut s = row;
1276 while s > 0 && is_blank(s) {
1277 s -= 1;
1278 }
1279 while s > 0 && !is_blank(s - 1) {
1280 s -= 1;
1281 }
1282 let mut e = row;
1283 while e + 1 < n && is_blank(e) {
1284 e += 1;
1285 }
1286 while e + 1 < n && !is_blank(e + 1) {
1287 e += 1;
1288 }
1289 (
1290 code.line_to_char(s),
1291 code.line_to_char(e) + code.line_len(e),
1292 )
1293 };
1294 t.editor.set_selection_range(start, end);
1295 t.editor.focus(&area);
1296 }
1297 }
1298
1299 pub fn select_section(&mut self, area: Rect) {
1302 if let Some(t) = self.active_tab_mut() {
1303 let (start, end) = {
1304 let code = t.editor.code_ref();
1305 let n = code.len_lines().max(1);
1306 let sep = |r: usize| is_section_break(code, r, n);
1307 let row = code.char_to_line(t.editor.get_cursor());
1308 let mut s = row;
1309 while s > 0 && !sep(s - 1) {
1310 s -= 1;
1311 }
1312 let mut e = row;
1313 while e + 1 < n && !sep(e + 1) {
1314 e += 1;
1315 }
1316 (
1317 code.line_to_char(s),
1318 code.line_to_char(e) + code.line_len(e),
1319 )
1320 };
1321 t.editor.set_selection_range(start, end);
1322 t.editor.focus(&area);
1323 }
1324 }
1325
1326 pub fn insert_str(&mut self, text: &str, area: Rect) -> bool {
1330 if let Some(t) = self.active_tab_mut() {
1331 if t.is_image() || t.read_only {
1332 return false;
1333 }
1334 t.editor.apply(InsertText {
1335 text: text.to_string(),
1336 });
1337 t.editor.focus(&area);
1338 t.dirty = true;
1339 t.preview = false;
1340 return true;
1341 }
1342 false
1343 }
1344
1345 pub fn paste_str(&mut self, text: &str, area: Rect) -> bool {
1352 if let Some(t) = self.active_tab_mut() {
1353 if t.is_image() || t.read_only {
1354 return false;
1355 }
1356 t.editor.paste_text(text);
1357 t.editor.focus(&area);
1358 t.dirty = true;
1359 t.preview = false;
1360 return true;
1361 }
1362 false
1363 }
1364
1365 pub fn select_all(&mut self) {
1367 if let Some(t) = self.active_tab_mut() {
1368 t.editor.apply(SelectAll {});
1369 }
1370 }
1371
1372 pub fn duplicate_line(&mut self) {
1374 if let Some(t) = self.active_tab_mut() {
1375 if t.is_image() {
1376 return;
1377 }
1378 t.editor.apply(Duplicate {});
1379 t.dirty = true;
1380 t.preview = false;
1381 }
1382 }
1383
1384 pub fn join_lines(&mut self) {
1387 if let Some(t) = self.active_tab_mut() {
1388 if t.is_image() {
1389 return;
1390 }
1391 t.editor.join_lines();
1392 t.dirty = true;
1393 t.preview = false;
1394 }
1395 }
1396
1397 pub fn sort_lines(&mut self) {
1400 if let Some(t) = self.active_tab_mut() {
1401 if t.is_image() {
1402 return;
1403 }
1404 t.editor.sort_lines();
1405 t.dirty = true;
1406 t.preview = false;
1407 }
1408 }
1409
1410 pub fn trim_trailing_whitespace(&mut self) {
1412 self.edit_active_lines(CodeEditor::trim_trailing_whitespace);
1413 }
1414
1415 pub fn remove_duplicate_lines(&mut self) {
1417 self.edit_active_lines(CodeEditor::remove_duplicate_lines);
1418 }
1419
1420 pub fn reverse_lines(&mut self) {
1422 self.edit_active_lines(CodeEditor::reverse_lines);
1423 }
1424
1425 pub fn sort_unique(&mut self) {
1427 self.edit_active_lines(CodeEditor::sort_unique);
1428 }
1429
1430 pub fn shuffle_lines(&mut self) {
1432 self.edit_active_lines(CodeEditor::shuffle_lines);
1433 }
1434
1435 fn edit_active_lines(&mut self, f: impl FnOnce(&mut CodeEditor)) {
1437 if let Some(t) = self.active_tab_mut() {
1438 if t.is_image() {
1439 return;
1440 }
1441 f(&mut t.editor);
1442 t.dirty = true;
1443 t.preview = false;
1444 }
1445 }
1446
1447 pub fn move_line(&mut self, down: bool, area: Rect) {
1450 if let Some(t) = self.active_tab_mut() {
1451 if t.is_image() {
1452 return;
1453 }
1454 if down {
1455 t.editor.move_line_down();
1456 } else {
1457 t.editor.move_line_up();
1458 }
1459 t.editor.focus(&area);
1460 t.dirty = true;
1461 t.preview = false;
1462 }
1463 }
1464
1465 pub fn select_word(&mut self, forward: bool, area: Rect) {
1469 if let Some(t) = self.active_tab_mut() {
1470 if t.is_image() {
1471 return;
1472 }
1473 let text: Vec<char> = t.text().chars().collect();
1474 let cur = t.editor.get_cursor();
1475 let target = if forward {
1476 next_word(&text, cur)
1477 } else {
1478 prev_word(&text, cur)
1479 };
1480 if target != cur {
1481 t.editor.extend_selection(target);
1482 t.editor.set_cursor(target);
1483 t.editor.focus(&area);
1484 }
1485 }
1486 }
1487
1488 pub fn jump_matching_bracket(&mut self, area: Rect) {
1491 if let Some(t) = self.active_tab_mut()
1492 && let Some(off) = t.editor.matching_bracket_offset()
1493 {
1494 t.editor.set_cursor(off);
1495 t.editor.focus(&area);
1496 }
1497 }
1498
1499 pub fn delete_forward(&mut self) {
1506 if let Some(t) = self.active_tab_mut() {
1507 let at_end = t.editor.get_cursor() >= t.editor.code_ref().len_chars();
1508 let selecting = t.editor.get_selection().is_some_and(|s| !s.is_empty());
1509 if at_end && !selecting {
1510 return;
1511 }
1512 t.editor.apply(MoveRight { shift: false });
1513 t.editor.apply(Delete {});
1514 }
1515 }
1516
1517 pub fn page_up(&mut self, lines: usize) {
1519 if let Some(t) = self.active_tab_mut() {
1520 for _ in 0..lines.max(1) {
1521 t.editor.apply(MoveUp { shift: false });
1522 }
1523 }
1524 }
1525
1526 pub fn page_down(&mut self, lines: usize) {
1528 if let Some(t) = self.active_tab_mut() {
1529 for _ in 0..lines.max(1) {
1530 t.editor.apply(MoveDown { shift: false });
1531 }
1532 }
1533 }
1534}
1535
1536fn is_word_char(c: char) -> bool {
1538 c.is_alphanumeric() || c == '_'
1539}
1540
1541fn next_word(text: &[char], pos: usize) -> usize {
1544 let n = text.len();
1545 let mut i = pos.min(n);
1546 while i < n && !is_word_char(text[i]) {
1547 i += 1;
1548 }
1549 while i < n && is_word_char(text[i]) {
1550 i += 1;
1551 }
1552 i
1553}
1554
1555fn prev_word(text: &[char], pos: usize) -> usize {
1558 let mut i = pos.min(text.len());
1559 while i > 0 && !is_word_char(text[i - 1]) {
1560 i -= 1;
1561 }
1562 while i > 0 && is_word_char(text[i - 1]) {
1563 i -= 1;
1564 }
1565 i
1566}
1567
1568#[cfg(test)]
1569mod nav_tests {
1570 use super::{sentence_starts, word_starts};
1571
1572 #[test]
1573 fn word_starts_finds_each_word() {
1574 assert_eq!(word_starts("foo bar_baz qux"), vec![0, 5, 13]);
1576 assert_eq!(word_starts(" leading"), vec![3]);
1577 assert_eq!(word_starts("..."), [] as [usize; 0]);
1578 }
1579
1580 #[test]
1581 fn sentence_starts_splits_on_terminators() {
1582 let text = "One. Two! Three? Four.";
1583 assert_eq!(sentence_starts(text), vec![0, 5, 10, 18]);
1585 assert_eq!(sentence_starts("pi is 3.14 today"), vec![0]);
1587 }
1588}
1589
1590#[cfg(test)]
1591mod tests {
1592 use super::*;
1593
1594 #[test]
1597 fn title_shows_a_lock_only_for_filesystem_read_only_tabs() {
1598 let mut e = Editor::new(Flags::LINE_NUMBERS, " ".into());
1599 let tab = e.active_tab_mut().unwrap();
1600 assert!(!tab.title().contains('🔒'));
1601 tab.fs_access = FsAccess::ReadOnly;
1602 assert!(tab.title().contains('🔒'));
1603 tab.read_only = true;
1604 tab.read_only = false;
1605 assert_eq!(tab.fs_access, FsAccess::ReadOnly);
1606 }
1607}