1use iced::Color;
7use iced::advanced::text::{
8 Alignment, Paragraph, Renderer as TextRenderer, Text,
9};
10use iced::widget::operation::{RelativeOffset, snap_to};
11use iced::widget::{Id, canvas};
12use std::cell::{Cell, RefCell};
13use std::cmp::Ordering as CmpOrdering;
14use std::collections::{BTreeMap, HashSet};
15use std::ops::Range;
16use std::rc::Rc;
17use std::sync::atomic::{AtomicU64, Ordering};
18#[cfg(not(target_arch = "wasm32"))]
19use std::time::Instant;
20use syntect::highlighting::HighlightState;
21use syntect::parsing::ParseState;
22use unicode_width::UnicodeWidthChar;
23
24use crate::i18n::Translations;
25use crate::text_buffer::TextBuffer;
26use crate::theme::Style;
27pub use history::CommandHistory;
28
29#[cfg(target_arch = "wasm32")]
30use web_time::Instant;
31
32static EDITOR_ID_COUNTER: AtomicU64 = AtomicU64::new(1);
34
35static FOCUSED_EDITOR_ID: AtomicU64 = AtomicU64::new(0);
37
38mod canvas_impl;
40mod clipboard;
41pub mod command;
42mod context_menu;
43mod cursor;
44pub(crate) mod cursor_set;
45pub mod folding;
46mod goto_line;
47mod goto_line_dialog;
48pub mod history;
49pub mod ime_requester;
50pub mod lsp;
51#[cfg(all(feature = "lsp-process", not(target_arch = "wasm32")))]
52pub mod lsp_process;
53mod search;
54mod search_dialog;
55mod selection;
56mod update;
57mod view;
58mod vim;
59mod wrapping;
60
61pub use context_menu::{ContextMenuEntry, ContextMenuItem};
62pub use vim::VimMode;
63
64#[doc(hidden)]
70#[cfg(feature = "bench")]
71pub mod bench_support {
72 pub use super::canvas_impl::highlight_line_spans;
73 pub use super::folding::compute_foldable_regions;
74 pub use super::search::find_matches;
75 pub use super::wrapping::WrappingCalculator;
76 pub use crate::text_buffer::TextBuffer;
77
78 pub struct IncrementalEditBenchmark {
80 editor: super::CodeEditor,
81 }
82
83 impl IncrementalEditBenchmark {
84 pub fn new(content: &str, line: usize, column: usize) -> Self {
87 let mut editor = super::CodeEditor::new(content, "rs")
88 .with_wrap_column(Some(80));
89 editor.request_focus();
90 editor.has_canvas_focus = true;
91 editor.focus_locked = false;
92 editor.cursors.primary_mut().position = (line, column);
93 let _ = editor.visual_lines_cached(800.0);
94 Self { editor }
95 }
96
97 pub fn insert_and_backspace(&mut self) -> u64 {
100 let _ = self.editor.update(&super::Message::CharacterInput('x'));
101 let _ = self.editor.update(&super::Message::Backspace);
102 if self.editor.is_grouping {
103 self.editor.history.end_group();
104 self.editor.is_grouping = false;
105 }
106 self.editor.buffer_revision
107 }
108 }
109
110 struct NoopLspClient;
111
112 impl super::lsp::LspClient for NoopLspClient {}
113
114 pub struct IncrementalLspEditBenchmark {
116 editor: super::CodeEditor,
117 }
118
119 impl IncrementalLspEditBenchmark {
120 pub fn new(content: &str, line: usize, column: usize) -> Self {
122 let mut editor = super::CodeEditor::new(content, "rs")
123 .with_wrap_column(Some(80));
124 editor.attach_lsp(
125 Box::new(NoopLspClient),
126 super::lsp::LspDocument::new("file:///benchmark.rs", "rust"),
127 );
128 editor.request_focus();
129 editor.has_canvas_focus = true;
130 editor.focus_locked = false;
131 editor.cursors.primary_mut().position = (line, column);
132 let _ = editor.visual_lines_cached(800.0);
133 Self { editor }
134 }
135
136 pub fn insert_and_backspace(&mut self) -> u64 {
139 let _ = self.editor.update(&super::Message::CharacterInput('x'));
140 let _ = self.editor.update(&super::Message::Backspace);
141 if self.editor.is_grouping {
142 self.editor.history.end_group();
143 self.editor.is_grouping = false;
144 }
145 self.editor.buffer_revision
146 }
147 }
148
149 pub struct IncrementalNoWrapEditBenchmark {
151 editor: super::CodeEditor,
152 }
153
154 impl IncrementalNoWrapEditBenchmark {
155 pub fn new(content: &str, line: usize, column: usize) -> Self {
157 let mut editor = super::CodeEditor::new(content, "rs");
158 editor.set_wrap_enabled(false);
159 editor.request_focus();
160 editor.has_canvas_focus = true;
161 editor.focus_locked = false;
162 editor.cursors.primary_mut().position = (line, column);
163 let _ = editor.visual_lines_cached(800.0);
164 let _ = editor.max_content_width();
165 Self { editor }
166 }
167
168 pub fn insert_and_backspace(&mut self) -> u64 {
171 let _ = self.editor.update(&super::Message::CharacterInput('x'));
172 let _ = self.editor.update(&super::Message::Backspace);
173 if self.editor.is_grouping {
174 self.editor.history.end_group();
175 self.editor.is_grouping = false;
176 }
177 self.editor.buffer_revision
178 }
179 }
180
181 pub struct IncrementalSearchEditBenchmark {
183 editor: super::CodeEditor,
184 }
185
186 impl IncrementalSearchEditBenchmark {
187 pub fn new(
189 content: &str,
190 query: &str,
191 line: usize,
192 column: usize,
193 ) -> Self {
194 let mut editor = super::CodeEditor::new(content, "rs")
195 .with_wrap_column(Some(80));
196 editor.search_state.open_search();
197 editor.search_state.set_query(query.to_owned(), &editor.buffer);
198 editor.request_focus();
199 editor.has_canvas_focus = true;
200 editor.focus_locked = false;
201 editor.cursors.primary_mut().position = (line, column);
202 let _ = editor.visual_lines_cached(800.0);
203 Self { editor }
204 }
205
206 pub fn insert_and_backspace(&mut self) -> u64 {
208 let _ = self.editor.update(&super::Message::CharacterInput('x'));
209 let _ = self.editor.update(&super::Message::Backspace);
210 if self.editor.is_grouping {
211 self.editor.history.end_group();
212 self.editor.is_grouping = false;
213 }
214 self.editor.buffer_revision
215 }
216 }
217
218 pub fn calculate_visual_line_range_len(
221 calculator: &WrappingCalculator,
222 buffer: &TextBuffer,
223 viewport_width: f32,
224 gutter_width: f32,
225 start_line: usize,
226 end_line: usize,
227 ) -> usize {
228 calculator
229 .calculate_visual_lines_range(
230 buffer,
231 viewport_width,
232 gutter_width,
233 &std::collections::HashSet::new(),
234 start_line..end_line,
235 )
236 .len()
237 }
238}
239
240pub(crate) const FONT_SIZE: f32 = 14.0;
242pub(crate) const LINE_HEIGHT: f32 = 20.0;
243pub(crate) const CHAR_WIDTH: f32 = 8.4; pub(crate) const TAB_WIDTH: usize = 4;
245pub(crate) const GUTTER_WIDTH: f32 = 45.0;
246pub(crate) const FOLD_MARGIN_WIDTH: f32 = 14.0;
249pub(crate) const CURSOR_BLINK_INTERVAL: std::time::Duration =
250 std::time::Duration::from_millis(530);
251
252pub(crate) fn measure_char_width(
264 c: char,
265 full_char_width: f32,
266 char_width: f32,
267) -> f32 {
268 if c == '\t' {
269 return char_width * TAB_WIDTH as f32;
270 }
271 match c.width() {
272 Some(w) if w > 1 => full_char_width,
273 Some(_) => char_width,
274 None => 0.0,
275 }
276}
277
278pub(crate) fn measure_text_width(
294 text: &str,
295 full_char_width: f32,
296 char_width: f32,
297) -> f32 {
298 text.chars()
299 .map(|c| measure_char_width(c, full_char_width, char_width))
300 .sum()
301}
302
303pub(crate) const EPSILON: f32 = 0.001;
305pub(crate) const CACHE_WINDOW_MARGIN_MULTIPLIER: usize = 2;
312pub(crate) const HIGHLIGHT_LINES_PER_FRAME: usize = 2_000;
320
321pub(crate) fn compare_floats(a: f32, b: f32) -> CmpOrdering {
334 if (a - b).abs() < EPSILON {
335 CmpOrdering::Equal
336 } else if a > b {
337 CmpOrdering::Greater
338 } else {
339 CmpOrdering::Less
340 }
341}
342
343#[derive(Debug, Clone)]
344pub(crate) struct ImePreedit {
345 pub(crate) content: String,
346 pub(crate) selection: Option<Range<usize>>,
347}
348
349pub(crate) struct LspEditSnapshot {
354 pub(crate) start_line: usize,
355 pub(crate) old_end_exclusive: usize,
356 pub(crate) old_line_count: usize,
357 pub(crate) old_end: lsp::LspPosition,
358}
359
360pub struct CodeEditor {
362 pub(crate) editor_id: u64,
364 pub(crate) buffer: TextBuffer,
366 pub(crate) cursors: cursor_set::CursorSet,
368 pub(crate) horizontal_scroll_offset: f32,
370 pub(crate) style: Style,
372 pub(crate) syntax: String,
374 pub(crate) last_blink: Instant,
376 pub(crate) cursor_visible: bool,
378 pub(crate) is_dragging: bool,
380 pub(crate) content_cache: canvas::Cache,
389 pub(crate) overlay_cache: canvas::Cache,
401 pub(crate) scrollable_id: Id,
403 pub(crate) horizontal_scrollable_id: Id,
405 pub(crate) max_content_width_cache: RefCell<Option<MaxContentWidthCache>>,
407 pub(crate) viewport_scroll: f32,
409 pub(crate) viewport_height: f32,
411 pub(crate) viewport_width: f32,
413 pub(crate) history: CommandHistory,
415 pub(crate) is_grouping: bool,
417 pub(crate) wrap_enabled: bool,
419 pub(crate) auto_indent_enabled: bool,
421 pub(crate) indent_style: IndentStyle,
423 pub(crate) wrap_column: Option<usize>,
425 pub(crate) folding_enabled: bool,
427 pub(crate) collapsed_folds: HashSet<usize>,
429 pub(crate) fold_revision: u64,
434 pub(crate) foldable_regions_cache:
436 RefCell<Option<(u64, Rc<Vec<folding::FoldRegion>>)>>,
437 pub(crate) search_state: search::SearchState,
439 custom_context_menu_entries: Vec<ContextMenuEntry>,
441 default_context_menu_enabled: bool,
443 reveal_in_file_manager_enabled: bool,
445 pub(crate) goto_line_state: goto_line::GotoLineState,
447 vim_enabled: bool,
449 pub(crate) vim_state: vim::VimState,
451 pub(crate) translations: Translations,
453 pub(crate) search_replace_enabled: bool,
455 pub(crate) line_numbers_enabled: bool,
457 pub(crate) show_whitespace: bool,
459 pub(crate) lsp_enabled: bool,
461 pub(crate) lsp_client: Option<Box<dyn lsp::LspClient>>,
463 pub(crate) lsp_document: Option<lsp::LspDocument>,
465 pub(crate) lsp_pending_changes: Vec<lsp::LspTextChange>,
467 pub(crate) lsp_shadow_text: String,
469 pub(crate) lsp_shadow_is_current: bool,
471 pub(crate) lsp_synced_line_count: usize,
473 pub(crate) lsp_synced_last_line_len: usize,
475 pub(crate) lsp_edit_snapshot: Option<LspEditSnapshot>,
477 pub(crate) lsp_auto_flush: bool,
479 pub(crate) has_canvas_focus: bool,
481 pub(crate) focus_locked: bool,
483 pub(crate) show_cursor: bool,
485 pub(crate) modifiers: Cell<iced::keyboard::Modifiers>,
490 pub(crate) last_click: Cell<Option<(Instant, iced::Point, u8)>>,
493 pub(crate) font: iced::Font,
495 pub(crate) ime_preedit: Option<ImePreedit>,
497 pub(crate) font_size: f32,
499 pub(crate) full_char_width: f32,
501 pub(crate) line_height: f32,
503 pub(crate) char_width: f32,
505 pub(crate) last_first_visible_line: usize,
510 pub(crate) cache_window_start_line: usize,
512 pub(crate) cache_window_end_line: usize,
514 pub(crate) buffer_revision: u64,
521 visual_lines_cache: RefCell<Option<VisualLinesCache>>,
527 pub(crate) highlight_cache: RefCell<Option<HighlightCache>>,
536 pub(crate) highlight_lines_remaining: Cell<usize>,
539 pub(crate) pre_edit_line: usize,
545 pub(crate) pre_edit_last_line: usize,
550}
551
552#[derive(Clone, Copy, PartialEq, Eq)]
553struct VisualLinesKey {
554 buffer_revision: u64,
555 viewport_width_bits: u32,
559 gutter_width_bits: u32,
560 wrap_enabled: bool,
561 wrap_column: Option<usize>,
562 folding_enabled: bool,
563 fold_revision: u64,
564 full_char_width_bits: u32,
565 char_width_bits: u32,
566}
567
568struct VisualLinesCache {
569 key: VisualLinesKey,
570 visual_lines: Rc<Vec<wrapping::VisualLine>>,
571 buffer_line_count: usize,
572}
573
574pub(crate) struct MaxContentWidthCache {
576 revision: u64,
577 line_widths: Vec<f32>,
578 width_counts: BTreeMap<u32, usize>,
579}
580
581impl MaxContentWidthCache {
582 fn add_width(&mut self, width: f32) {
583 *self.width_counts.entry(width.to_bits()).or_insert(0) += 1;
584 }
585
586 fn remove_width(&mut self, width: f32) {
587 let bits = width.to_bits();
588 let remove_entry = if let Some(count) = self.width_counts.get_mut(&bits)
589 {
590 *count = count.saturating_sub(1);
591 *count == 0
592 } else {
593 false
594 };
595 if remove_entry {
596 self.width_counts.remove(&bits);
597 }
598 }
599
600 fn max_width(&self) -> f32 {
601 self.width_counts
602 .last_key_value()
603 .map_or(0.0, |(bits, _)| f32::from_bits(*bits))
604 }
605}
606
607struct CachedHighlightLine {
614 spans: Rc<Vec<(Color, String)>>,
616 parse_state: ParseState,
618 highlight_state: HighlightState,
620}
621
622pub(crate) struct HighlightCache {
630 syntax: String,
632 lines: Vec<CachedHighlightLine>,
634}
635
636impl HighlightCache {
637 pub(crate) fn new(syntax: String) -> Self {
643 Self { syntax, lines: Vec::new() }
644 }
645
646 pub(crate) fn syntax(&self) -> &str {
648 &self.syntax
649 }
650
651 pub(crate) fn valid_len(&self) -> usize {
653 self.lines.len()
654 }
655
656 pub(crate) fn spans(
662 &self,
663 logical_line: usize,
664 ) -> Option<Rc<Vec<(Color, String)>>> {
665 self.lines.get(logical_line).map(|line| Rc::clone(&line.spans))
666 }
667
668 pub(crate) fn resume_state(&self) -> Option<(ParseState, HighlightState)> {
674 self.lines.last().map(|line| {
675 (line.parse_state.clone(), line.highlight_state.clone())
676 })
677 }
678
679 pub(crate) fn push_line(
687 &mut self,
688 spans: Rc<Vec<(Color, String)>>,
689 parse_state: ParseState,
690 highlight_state: HighlightState,
691 ) {
692 self.lines.push(CachedHighlightLine {
693 spans,
694 parse_state,
695 highlight_state,
696 });
697 }
698
699 pub(crate) fn truncate(&mut self, line: usize) {
706 self.lines.truncate(line);
707 }
708}
709
710#[derive(Debug, Clone)]
712pub enum Message {
713 CharacterInput(char),
715 VimKey(char),
717 ToggleVimMode,
719 WriteRequested,
721 Backspace,
723 Delete,
725 Enter,
727 Tab,
729 ArrowKey(ArrowDirection, bool),
731 MouseClick(iced::Point),
733 MouseDrag(iced::Point),
735 MouseHover(iced::Point),
737 MouseRelease,
739 DoubleClick(iced::Point),
741 TripleClick(iced::Point),
743 ContextMenuRequested(iced::Point),
745 CustomContextMenuAction(String),
747 RevealInFileManager,
749 Cut,
751 Copy,
753 Paste(String),
755 DeleteSelection,
757 SelectAll,
759 Tick,
761 PageUp,
763 PageDown,
765 Home(bool),
767 End(bool),
769 CtrlHome,
771 CtrlEnd,
773 GotoPosition(usize, usize),
775 OpenGotoLine,
777 CloseGotoLine,
779 GotoLineChanged(String),
781 SubmitGotoLine,
783 Scrolled(iced::widget::scrollable::Viewport),
785 HorizontalScrolled(iced::widget::scrollable::Viewport),
787 Undo,
789 Redo,
791 OpenSearch,
793 OpenSearchReplace,
795 CloseSearch,
797 SearchQueryChanged(String),
799 ReplaceQueryChanged(String),
801 ToggleCaseSensitive,
803 FindNext,
805 FindPrevious,
807 ReplaceNext,
809 ReplaceAll,
811 SearchDialogTab,
813 SearchDialogShiftTab,
815 FocusNavigationTab,
817 FocusNavigationShiftTab,
819 CanvasFocusGained,
821 CanvasFocusLost,
823 JumpClick(iced::Point),
827 ImeOpened,
829 ImePreedit(String, Option<Range<usize>>),
831 ImeCommit(String),
833 ImeClosed,
835 AltClick(iced::Point),
837 AddCursorAbove,
839 AddCursorBelow,
841 SelectNextOccurrence,
843 ToggleFold(usize),
845 ToggleFoldAtCursor,
847 FoldAll,
849 UnfoldAll,
851 MoveLineUp,
853 MoveLineDown,
855 DuplicateLineUp,
857 DuplicateLineDown,
859 ToggleComment,
861}
862
863#[derive(Debug, Clone, Copy, PartialEq, Eq)]
867pub enum IndentStyle {
868 Spaces(u8),
870 Tab,
872}
873
874impl IndentStyle {
875 pub const ALL: [IndentStyle; 4] = [
877 IndentStyle::Spaces(2),
878 IndentStyle::Spaces(4),
879 IndentStyle::Spaces(8),
880 IndentStyle::Tab,
881 ];
882}
883
884impl std::fmt::Display for IndentStyle {
885 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
886 match self {
887 IndentStyle::Spaces(1) => write!(f, "1 space"),
888 IndentStyle::Spaces(n) => write!(f, "{n} spaces"),
889 IndentStyle::Tab => write!(f, "Tab"),
890 }
891 }
892}
893
894#[derive(Debug, Clone, Copy)]
896pub enum ArrowDirection {
897 Up,
898 Down,
899 Left,
900 Right,
901}
902
903impl CodeEditor {
904 pub fn new(content: &str, syntax: &str) -> Self {
915 let editor_id = EDITOR_ID_COUNTER.fetch_add(1, Ordering::Relaxed);
917
918 if editor_id == 1 {
920 FOCUSED_EDITOR_ID.store(editor_id, Ordering::Relaxed);
921 }
922
923 let mut editor = Self {
924 editor_id,
925 buffer: TextBuffer::new(content),
926 cursors: cursor_set::CursorSet::new((0, 0)),
927 horizontal_scroll_offset: 0.0,
928 style: crate::theme::from_iced_theme(&iced::Theme::TokyoNightStorm),
929 syntax: syntax.to_string(),
930 last_blink: Instant::now(),
931 cursor_visible: true,
932 is_dragging: false,
933 content_cache: canvas::Cache::default(),
934 overlay_cache: canvas::Cache::default(),
935 scrollable_id: Id::unique(),
936 horizontal_scrollable_id: Id::unique(),
937 max_content_width_cache: RefCell::new(None),
938 viewport_scroll: 0.0,
939 viewport_height: 600.0, viewport_width: 800.0, history: CommandHistory::new(100),
942 is_grouping: false,
943 wrap_enabled: true,
944 auto_indent_enabled: true,
945 indent_style: IndentStyle::Spaces(4),
946 wrap_column: None,
947 folding_enabled: true,
948 collapsed_folds: HashSet::new(),
949 fold_revision: 0,
950 foldable_regions_cache: RefCell::new(None),
951 search_state: search::SearchState::new(),
952 custom_context_menu_entries: Vec::new(),
953 default_context_menu_enabled: true,
954 reveal_in_file_manager_enabled: false,
955 goto_line_state: goto_line::GotoLineState::new(),
956 vim_enabled: false,
957 vim_state: vim::VimState::default(),
958 translations: Translations::default(),
959 search_replace_enabled: true,
960 line_numbers_enabled: true,
961 show_whitespace: true,
962 lsp_enabled: true,
963 lsp_client: None,
964 lsp_document: None,
965 lsp_pending_changes: Vec::new(),
966 lsp_shadow_text: String::new(),
967 lsp_shadow_is_current: true,
968 lsp_synced_line_count: 1,
969 lsp_synced_last_line_len: 0,
970 lsp_edit_snapshot: None,
971 lsp_auto_flush: true,
972 has_canvas_focus: false,
973 focus_locked: false,
974 show_cursor: false,
975 modifiers: Cell::new(iced::keyboard::Modifiers::default()),
976 last_click: Cell::new(None),
977 font: iced::Font::MONOSPACE,
978 ime_preedit: None,
979 font_size: FONT_SIZE,
980 full_char_width: CHAR_WIDTH * 2.0,
981 line_height: LINE_HEIGHT,
982 char_width: CHAR_WIDTH,
983 last_first_visible_line: 0,
987 cache_window_start_line: 0,
988 cache_window_end_line: 0,
989 buffer_revision: 0,
990 visual_lines_cache: RefCell::new(None),
991 highlight_cache: RefCell::new(None),
992 highlight_lines_remaining: Cell::new(usize::MAX),
993 pre_edit_line: 0,
994 pre_edit_last_line: 0,
995 };
996
997 editor.recalculate_char_dimensions(false);
999
1000 editor
1001 }
1002
1003 pub fn set_custom_context_menu_entries(
1005 &mut self,
1006 entries: Vec<ContextMenuEntry>,
1007 ) {
1008 self.custom_context_menu_entries = entries;
1009 }
1010
1011 #[must_use]
1013 pub fn with_custom_context_menu_entries(
1014 mut self,
1015 entries: Vec<ContextMenuEntry>,
1016 ) -> Self {
1017 self.set_custom_context_menu_entries(entries);
1018 self
1019 }
1020
1021 pub fn custom_context_menu_entries(&self) -> &[ContextMenuEntry] {
1023 &self.custom_context_menu_entries
1024 }
1025
1026 pub fn set_default_context_menu_enabled(&mut self, enabled: bool) {
1028 self.default_context_menu_enabled = enabled;
1029 }
1030
1031 #[must_use]
1033 pub fn with_default_context_menu_enabled(mut self, enabled: bool) -> Self {
1034 self.set_default_context_menu_enabled(enabled);
1035 self
1036 }
1037
1038 pub fn default_context_menu_enabled(&self) -> bool {
1040 self.default_context_menu_enabled
1041 }
1042
1043 pub fn set_reveal_in_file_manager_enabled(&mut self, enabled: bool) {
1045 self.reveal_in_file_manager_enabled = enabled;
1046 }
1047
1048 #[must_use]
1050 pub fn with_reveal_in_file_manager_enabled(
1051 mut self,
1052 enabled: bool,
1053 ) -> Self {
1054 self.set_reveal_in_file_manager_enabled(enabled);
1055 self
1056 }
1057
1058 pub fn reveal_in_file_manager_enabled(&self) -> bool {
1060 self.reveal_in_file_manager_enabled
1061 }
1062
1063 pub fn set_font(&mut self, font: iced::Font) {
1069 self.font = font;
1070 self.recalculate_char_dimensions(false);
1071 }
1072
1073 pub fn set_font_size(&mut self, size: f32, auto_adjust_line_height: bool) {
1083 self.font_size = size;
1084 self.recalculate_char_dimensions(auto_adjust_line_height);
1085 }
1086
1087 fn recalculate_char_dimensions(&mut self, auto_adjust_line_height: bool) {
1089 self.char_width = self.measure_single_char_width("a");
1090 self.full_char_width = self.measure_single_char_width("汉");
1092
1093 if self.char_width.is_infinite() {
1095 self.char_width = self.font_size / 2.0; }
1097
1098 if self.full_char_width.is_infinite() {
1099 self.full_char_width = self.font_size;
1100 }
1101
1102 if auto_adjust_line_height {
1103 let line_height_ratio = LINE_HEIGHT / FONT_SIZE;
1104 self.line_height = self.font_size * line_height_ratio;
1105 }
1106
1107 self.content_cache.clear();
1108 self.overlay_cache.clear();
1109 *self.max_content_width_cache.borrow_mut() = None;
1110 }
1111
1112 fn measure_single_char_width(&self, content: &str) -> f32 {
1114 let text = Text {
1115 content,
1116 font: self.font,
1117 size: iced::Pixels(self.font_size),
1118 line_height: iced::advanced::text::LineHeight::default(),
1119 bounds: iced::Size::new(f32::INFINITY, f32::INFINITY),
1120 align_x: Alignment::Left,
1121 align_y: iced::alignment::Vertical::Top,
1122 shaping: iced::advanced::text::Shaping::Advanced,
1123 wrapping: iced::advanced::text::Wrapping::default(),
1124 };
1125 let p = <iced::Renderer as TextRenderer>::Paragraph::with_text(text);
1126 p.min_width()
1127 }
1128
1129 pub fn font_size(&self) -> f32 {
1135 self.font_size
1136 }
1137
1138 pub fn char_width(&self) -> f32 {
1144 self.char_width
1145 }
1146
1147 pub fn full_char_width(&self) -> f32 {
1153 self.full_char_width
1154 }
1155
1156 pub fn measure_text_width(&self, text: &str) -> f32 {
1158 measure_text_width(text, self.full_char_width, self.char_width)
1159 }
1160
1161 pub fn set_line_height(&mut self, height: f32) {
1167 self.line_height = height;
1168 self.content_cache.clear();
1169 self.overlay_cache.clear();
1170 }
1171
1172 pub fn line_height(&self) -> f32 {
1178 self.line_height
1179 }
1180
1181 pub fn viewport_height(&self) -> f32 {
1183 self.viewport_height
1184 }
1185
1186 pub fn viewport_width(&self) -> f32 {
1188 self.viewport_width
1189 }
1190
1191 pub fn viewport_scroll(&self) -> f32 {
1193 self.viewport_scroll
1194 }
1195
1196 pub fn content(&self) -> String {
1202 self.buffer.to_string()
1203 }
1204
1205 pub fn set_vim_enabled(&mut self, enabled: bool) {
1210 if self.is_grouping {
1211 self.history.end_group();
1212 self.is_grouping = false;
1213 }
1214 self.vim_enabled = enabled;
1215 self.vim_state.enter_clean_normal_mode();
1216 self.cursors.remove_all_but_primary();
1217 let position = if enabled {
1218 self.vim_normal_position(self.cursors.primary_position())
1219 } else {
1220 self.cursors.primary_position()
1221 };
1222 self.cursors.set_single(position);
1223 self.is_dragging = false;
1224 self.overlay_cache.clear();
1225 }
1226
1227 #[must_use]
1229 pub fn with_vim_enabled(mut self, enabled: bool) -> Self {
1230 self.set_vim_enabled(enabled);
1231 self
1232 }
1233
1234 pub fn vim_enabled(&self) -> bool {
1236 self.vim_enabled
1237 }
1238
1239 pub fn vim_mode(&self) -> Option<VimMode> {
1241 self.vim_enabled.then(|| self.vim_state.mode())
1242 }
1243
1244 #[must_use]
1266 pub fn with_viewport_height(mut self, height: f32) -> Self {
1267 self.viewport_height = height;
1268 self
1269 }
1270
1271 pub fn set_theme(&mut self, style: Style) {
1286 self.style = style;
1287 self.content_cache.clear();
1288 self.overlay_cache.clear();
1289 }
1290
1291 pub fn set_language(&mut self, language: crate::i18n::Language) {
1309 self.translations.set_language(language);
1310 self.overlay_cache.clear();
1311 }
1312
1313 pub fn language(&self) -> crate::i18n::Language {
1328 self.translations.language()
1329 }
1330
1331 pub fn attach_lsp(
1341 &mut self,
1342 mut client: Box<dyn lsp::LspClient>,
1343 mut document: lsp::LspDocument,
1344 ) {
1345 if !self.lsp_enabled {
1346 return;
1347 }
1348 document.version = 1;
1349 let text = self.buffer.to_string();
1350 client.did_open(&document, &text);
1351 self.lsp_client = Some(client);
1352 self.lsp_document = Some(document);
1353 self.lsp_shadow_text = text;
1354 self.lsp_shadow_is_current = true;
1355 self.update_lsp_synced_extent();
1356 self.lsp_edit_snapshot = None;
1357 self.lsp_pending_changes.clear();
1358 }
1359
1360 pub fn lsp_open_document(&mut self, mut document: lsp::LspDocument) {
1369 let Some(client) = self.lsp_client.as_mut() else { return };
1370 if let Some(current) = self.lsp_document.as_ref() {
1371 client.did_close(current);
1372 }
1373 document.version = 1;
1374 let text = self.buffer.to_string();
1375 client.did_open(&document, &text);
1376 self.lsp_document = Some(document);
1377 self.lsp_shadow_text = text;
1378 self.lsp_shadow_is_current = true;
1379 self.update_lsp_synced_extent();
1380 self.lsp_edit_snapshot = None;
1381 self.lsp_pending_changes.clear();
1382 }
1383
1384 pub fn detach_lsp(&mut self) {
1388 if let (Some(client), Some(document)) =
1389 (self.lsp_client.as_mut(), self.lsp_document.as_ref())
1390 {
1391 client.did_close(document);
1392 }
1393 self.lsp_client = None;
1394 self.lsp_document = None;
1395 self.lsp_shadow_text = String::new();
1396 self.lsp_shadow_is_current = true;
1397 self.lsp_synced_line_count = 1;
1398 self.lsp_synced_last_line_len = 0;
1399 self.lsp_edit_snapshot = None;
1400 self.lsp_pending_changes.clear();
1401 }
1402
1403 pub fn lsp_did_save(&mut self) {
1405 if let (Some(client), Some(document)) =
1406 (self.lsp_client.as_mut(), self.lsp_document.as_ref())
1407 {
1408 let text = self.buffer.to_string();
1409 client.did_save(document, &text);
1410 }
1411 }
1412
1413 pub fn lsp_request_hover(&mut self) {
1415 let position = self.lsp_position_from_cursor();
1416 if let (Some(client), Some(document)) =
1417 (self.lsp_client.as_mut(), self.lsp_document.as_ref())
1418 {
1419 client.request_hover(document, position);
1420 }
1421 }
1422
1423 pub fn lsp_request_hover_at(&mut self, point: iced::Point) -> bool {
1428 let Some(position) = self.lsp_position_from_point(point) else {
1429 return false;
1430 };
1431 if let (Some(client), Some(document)) =
1432 (self.lsp_client.as_mut(), self.lsp_document.as_ref())
1433 {
1434 client.request_hover(document, position);
1435 return true;
1436 }
1437 false
1438 }
1439
1440 pub fn lsp_request_hover_at_position(
1444 &mut self,
1445 position: lsp::LspPosition,
1446 ) -> bool {
1447 if let (Some(client), Some(document)) =
1448 (self.lsp_client.as_mut(), self.lsp_document.as_ref())
1449 {
1450 client.request_hover(document, position);
1451 return true;
1452 }
1453 false
1454 }
1455
1456 pub fn lsp_position_at_point(
1458 &self,
1459 point: iced::Point,
1460 ) -> Option<lsp::LspPosition> {
1461 self.lsp_position_from_point(point)
1462 }
1463
1464 pub fn lsp_hover_anchor_at_point(
1470 &self,
1471 point: iced::Point,
1472 ) -> Option<(lsp::LspPosition, iced::Point)> {
1473 let (line, col) = self.calculate_cursor_from_point(point)?;
1474 let line_content = self.buffer.line(line);
1475 let anchor_col = Self::word_start_in_line(line_content, col);
1476 let anchor_point =
1477 self.point_from_position(line, anchor_col).unwrap_or(point);
1478 let line = u32::try_from(line).unwrap_or(u32::MAX);
1479 let character = u32::try_from(anchor_col).unwrap_or(u32::MAX);
1480 Some((lsp::LspPosition { line, character }, anchor_point))
1481 }
1482
1483 pub fn lsp_request_completion(&mut self) {
1485 let position = self.lsp_position_from_cursor();
1486 if let (Some(client), Some(document)) =
1487 (self.lsp_client.as_mut(), self.lsp_document.as_ref())
1488 {
1489 client.request_completion(document, position);
1490 }
1491 }
1492
1493 pub fn lsp_flush_pending_changes(&mut self) {
1498 if self.lsp_pending_changes.is_empty() {
1499 return;
1500 }
1501
1502 if let (Some(client), Some(document)) =
1503 (self.lsp_client.as_mut(), self.lsp_document.as_mut())
1504 {
1505 let changes = std::mem::take(&mut self.lsp_pending_changes);
1506 document.version = document.version.saturating_add(1);
1507 client.did_change(document, &changes);
1508 }
1509 }
1510
1511 pub fn set_lsp_auto_flush(&mut self, auto_flush: bool) {
1513 self.lsp_auto_flush = auto_flush;
1514 }
1515
1516 pub fn request_focus(&self) {
1534 FOCUSED_EDITOR_ID.store(self.editor_id, Ordering::Relaxed);
1535 }
1536
1537 pub fn is_focused(&self) -> bool {
1557 FOCUSED_EDITOR_ID.load(Ordering::Relaxed) == self.editor_id
1558 }
1559
1560 pub fn reset(&mut self, content: &str) -> iced::Task<Message> {
1589 self.buffer = TextBuffer::new(content);
1590 self.cursors.set_single((0, 0));
1591 self.vim_state.reset();
1592 self.horizontal_scroll_offset = 0.0;
1593 self.is_dragging = false;
1594 self.viewport_scroll = 0.0;
1595 self.history = CommandHistory::new(100);
1596 self.is_grouping = false;
1597 self.last_blink = Instant::now();
1598 self.cursor_visible = true;
1599 self.content_cache = canvas::Cache::default();
1600 self.overlay_cache = canvas::Cache::default();
1601 self.buffer_revision = self.buffer_revision.wrapping_add(1);
1602 *self.visual_lines_cache.borrow_mut() = None;
1603 self.pre_edit_line = 0;
1605 self.pre_edit_last_line = usize::MAX;
1606 self.invalidate_highlight_from(0);
1607 self.enqueue_lsp_change();
1608
1609 snap_to(self.scrollable_id.clone(), RelativeOffset::START)
1611 }
1612
1613 pub(crate) fn reset_cursor_blink(&mut self) {
1615 self.last_blink = Instant::now();
1616 self.cursor_visible = true;
1617 }
1618
1619 fn lsp_position_from_cursor(&self) -> lsp::LspPosition {
1621 let pos = self.cursors.primary_position();
1622 let line = u32::try_from(pos.0).unwrap_or(u32::MAX);
1623 let character = u32::try_from(pos.1).unwrap_or(u32::MAX);
1624 lsp::LspPosition { line, character }
1625 }
1626
1627 fn lsp_position_from_point(
1629 &self,
1630 point: iced::Point,
1631 ) -> Option<lsp::LspPosition> {
1632 let (line, col) = self.calculate_cursor_from_point(point)?;
1633 let line = u32::try_from(line).unwrap_or(u32::MAX);
1634 let character = u32::try_from(col).unwrap_or(u32::MAX);
1635 Some(lsp::LspPosition { line, character })
1636 }
1637
1638 fn point_from_position(
1640 &self,
1641 line: usize,
1642 col: usize,
1643 ) -> Option<iced::Point> {
1644 let visual_lines = self.visual_lines_cached(self.viewport_width);
1645 let visual_index = wrapping::WrappingCalculator::logical_to_visual(
1646 &visual_lines,
1647 line,
1648 col,
1649 )?;
1650 let visual_line = &visual_lines[visual_index];
1651 let line_content = self.buffer.line(visual_line.logical_line);
1652 let prefix_len = col.saturating_sub(visual_line.start_col);
1653 let prefix_text: String = line_content
1654 .chars()
1655 .skip(visual_line.start_col)
1656 .take(prefix_len)
1657 .collect();
1658 let x = self.gutter_width()
1659 + 5.0
1660 + measure_text_width(
1661 &prefix_text,
1662 self.full_char_width,
1663 self.char_width,
1664 );
1665 let y = visual_index as f32 * self.line_height;
1666 Some(iced::Point::new(x, y))
1667 }
1668
1669 pub(crate) fn word_start_in_line(line: &str, col: usize) -> usize {
1673 let chars: Vec<char> = line.chars().collect();
1674 if chars.is_empty() {
1675 return 0;
1676 }
1677 let mut idx = col.min(chars.len());
1678 if idx == chars.len() {
1679 idx = idx.saturating_sub(1);
1680 }
1681 if !Self::is_word_char(chars[idx]) {
1682 if idx > 0 && Self::is_word_char(chars[idx - 1]) {
1683 idx -= 1;
1684 } else {
1685 return col.min(chars.len());
1686 }
1687 }
1688 while idx > 0 && Self::is_word_char(chars[idx - 1]) {
1689 idx -= 1;
1690 }
1691 idx
1692 }
1693
1694 pub(crate) fn word_end_in_line(line: &str, col: usize) -> usize {
1696 let chars: Vec<char> = line.chars().collect();
1697 if chars.is_empty() {
1698 return 0;
1699 }
1700 let mut idx = col.min(chars.len());
1701 if idx == chars.len() {
1702 idx = idx.saturating_sub(1);
1703 }
1704
1705 if !Self::is_word_char(chars[idx]) {
1707 if idx > 0 && Self::is_word_char(chars[idx - 1]) {
1708 return idx;
1713 } else {
1714 return col.min(chars.len());
1716 }
1717 }
1718
1719 while idx < chars.len() && Self::is_word_char(chars[idx]) {
1721 idx += 1;
1722 }
1723 idx
1724 }
1725
1726 pub(crate) fn is_word_char(ch: char) -> bool {
1728 ch == '_' || ch.is_alphanumeric()
1729 }
1730
1731 fn enqueue_lsp_change(&mut self) {
1735 if self.lsp_document.is_none() {
1736 return;
1737 }
1738
1739 let new_text = self.buffer.to_string();
1740 let change = if self.lsp_shadow_is_current {
1741 lsp::compute_text_change(&self.lsp_shadow_text, &new_text)
1742 } else {
1743 let end_line = self.lsp_synced_line_count.saturating_sub(1);
1744 Some(lsp::LspTextChange {
1745 range: lsp::LspRange {
1746 start: lsp::LspPosition { line: 0, character: 0 },
1747 end: lsp::LspPosition {
1748 line: u32::try_from(end_line).unwrap_or(u32::MAX),
1749 character: u32::try_from(self.lsp_synced_last_line_len)
1750 .unwrap_or(u32::MAX),
1751 },
1752 },
1753 text: new_text.clone(),
1754 })
1755 };
1756 if let Some(change) = change {
1757 self.lsp_pending_changes.push(change);
1758 }
1759 self.lsp_shadow_text = new_text;
1760 self.lsp_shadow_is_current = true;
1761 self.update_lsp_synced_extent();
1762 if self.lsp_auto_flush {
1763 self.lsp_flush_pending_changes();
1764 }
1765 }
1766
1767 pub(crate) fn enqueue_incremental_lsp_change(&mut self) {
1771 if self.lsp_document.is_none() {
1772 self.lsp_edit_snapshot = None;
1773 return;
1774 }
1775
1776 let Some(snapshot) = self.lsp_edit_snapshot.take() else {
1777 self.enqueue_lsp_change();
1778 return;
1779 };
1780
1781 let new_line_count = self.buffer.line_count();
1782 let start_line =
1783 snapshot.start_line.min(new_line_count.saturating_sub(1));
1784 let new_end_exclusive = if new_line_count >= snapshot.old_line_count {
1785 snapshot
1786 .old_end_exclusive
1787 .saturating_add(new_line_count - snapshot.old_line_count)
1788 .min(new_line_count)
1789 } else {
1790 snapshot
1791 .old_end_exclusive
1792 .saturating_sub(snapshot.old_line_count - new_line_count)
1793 .max(start_line.saturating_add(1))
1794 .min(new_line_count)
1795 };
1796 let text =
1797 self.buffer.line_range_to_string(start_line, new_end_exclusive);
1798 self.lsp_pending_changes.push(lsp::LspTextChange {
1799 range: lsp::LspRange {
1800 start: lsp::LspPosition {
1801 line: u32::try_from(snapshot.start_line)
1802 .unwrap_or(u32::MAX),
1803 character: 0,
1804 },
1805 end: snapshot.old_end,
1806 },
1807 text,
1808 });
1809
1810 self.lsp_shadow_text = String::new();
1814 self.lsp_shadow_is_current = false;
1815 self.update_lsp_synced_extent();
1816 if self.lsp_auto_flush {
1817 self.lsp_flush_pending_changes();
1818 }
1819 }
1820
1821 fn update_lsp_synced_extent(&mut self) {
1824 self.lsp_synced_line_count = self.buffer.line_count();
1825 self.lsp_synced_last_line_len =
1826 self.buffer.line_len(self.lsp_synced_line_count.saturating_sub(1));
1827 }
1828
1829 pub(crate) fn refresh_search_matches_if_needed(&mut self) {
1835 if self.search_matches_visible() && !self.search_state.query.is_empty()
1836 {
1837 let start_line = self.pre_edit_line.saturating_sub(1);
1838 let old_end_exclusive = self.pre_edit_last_line.saturating_add(2);
1839 self.search_state.update_matches_after_edit(
1840 &self.buffer,
1841 start_line,
1842 old_end_exclusive,
1843 );
1844
1845 self.search_state
1847 .select_match_near_cursor(self.cursors.primary_position());
1848 }
1849 }
1850
1851 pub(crate) fn search_matches_visible(&self) -> bool {
1852 self.search_state.is_open
1853 || (self.vim_enabled && self.vim_state.last_search().is_some())
1854 }
1855
1856 pub fn is_modified(&self) -> bool {
1862 self.history.is_modified()
1863 }
1864
1865 pub fn mark_saved(&mut self) {
1869 self.history.mark_saved();
1870 }
1871
1872 pub fn can_undo(&self) -> bool {
1874 self.history.can_undo()
1875 }
1876
1877 pub fn can_redo(&self) -> bool {
1879 self.history.can_redo()
1880 }
1881
1882 pub fn set_wrap_enabled(&mut self, enabled: bool) {
1900 if self.wrap_enabled != enabled {
1901 self.wrap_enabled = enabled;
1902 if enabled {
1903 self.horizontal_scroll_offset = 0.0;
1904 }
1905 self.content_cache.clear();
1906 self.overlay_cache.clear();
1907 }
1908 }
1909
1910 pub fn wrap_enabled(&self) -> bool {
1916 self.wrap_enabled
1917 }
1918
1919 pub fn set_show_whitespace(&mut self, enabled: bool) {
1938 if self.show_whitespace != enabled {
1939 self.show_whitespace = enabled;
1940 self.content_cache.clear();
1941 }
1942 }
1943
1944 pub fn show_whitespace(&self) -> bool {
1946 self.show_whitespace
1947 }
1948
1949 pub fn set_folding_enabled(&mut self, enabled: bool) {
1968 if self.folding_enabled != enabled {
1969 self.folding_enabled = enabled;
1970 self.bump_fold_revision();
1971 }
1972 }
1973
1974 pub fn folding_enabled(&self) -> bool {
1976 self.folding_enabled
1977 }
1978
1979 pub fn is_folded(&self, header_line: usize) -> bool {
1981 self.collapsed_folds.contains(&header_line)
1982 }
1983
1984 pub fn toggle_fold(&mut self, header_line: usize) {
1995 let regions = self.foldable_regions();
1996 if !folding::is_fold_header(®ions, header_line) {
1997 return; }
1999
2000 if self.collapsed_folds.contains(&header_line) {
2001 self.collapsed_folds.remove(&header_line);
2002 } else {
2003 self.collapsed_folds.insert(header_line);
2004 }
2005 self.after_fold_change();
2006 }
2007
2008 pub fn toggle_fold_at(&mut self, line: usize) {
2020 let regions = self.foldable_regions();
2021 let header = regions
2022 .iter()
2023 .filter(|r| r.start_line <= line && line <= r.end_line)
2024 .map(|r| r.start_line)
2025 .max();
2026 if let Some(header) = header {
2027 if self.collapsed_folds.contains(&header) {
2028 self.collapsed_folds.remove(&header);
2029 } else {
2030 self.collapsed_folds.insert(header);
2031 }
2032 self.after_fold_change();
2033 }
2034 }
2035
2036 pub fn fold_at(&mut self, line: usize) {
2046 let regions = self.foldable_regions();
2047 let header = regions
2049 .iter()
2050 .filter(|r| r.start_line <= line && line <= r.end_line)
2051 .map(|r| r.start_line)
2052 .max();
2053 if let Some(header) = header
2054 && self.collapsed_folds.insert(header)
2055 {
2056 self.after_fold_change();
2057 }
2058 }
2059
2060 pub fn unfold_at(&mut self, line: usize) {
2068 let regions = self.foldable_regions();
2069 let header = regions
2070 .iter()
2071 .filter(|r| {
2072 r.start_line <= line
2073 && line <= r.end_line
2074 && self.collapsed_folds.contains(&r.start_line)
2075 })
2076 .map(|r| r.start_line)
2077 .max();
2078 if let Some(header) = header
2079 && self.collapsed_folds.remove(&header)
2080 {
2081 self.after_fold_change();
2082 }
2083 }
2084
2085 pub fn fold_all(&mut self) {
2087 let regions = self.foldable_regions();
2088 let mut changed = false;
2089 for region in regions.iter() {
2090 changed |= self.collapsed_folds.insert(region.start_line);
2091 }
2092 if changed {
2093 self.after_fold_change();
2094 }
2095 }
2096
2097 pub fn unfold_all(&mut self) {
2099 if !self.collapsed_folds.is_empty() {
2100 self.collapsed_folds.clear();
2101 self.after_fold_change();
2102 }
2103 }
2104
2105 fn after_fold_change(&mut self) {
2108 let hidden = self.hidden_lines_set();
2109 self.move_cursors_out_of_hidden(&hidden);
2110 self.bump_fold_revision();
2111 }
2112
2113 fn move_cursors_out_of_hidden(&mut self, hidden: &HashSet<usize>) {
2116 if hidden.is_empty() {
2117 return;
2118 }
2119 for cursor in self.cursors.as_mut_slice() {
2120 let mut line = cursor.position.0;
2121 while line > 0 && hidden.contains(&line) {
2122 line -= 1;
2123 }
2124 if line != cursor.position.0 {
2125 cursor.position = (line, 0);
2126 }
2127 }
2128 self.cursors.sort_and_merge();
2129 }
2130
2131 fn bump_fold_revision(&mut self) {
2133 self.fold_revision = self.fold_revision.wrapping_add(1);
2134 self.content_cache.clear();
2135 self.overlay_cache.clear();
2136 }
2137
2138 pub(crate) fn foldable_regions(&self) -> Rc<Vec<folding::FoldRegion>> {
2143 if !self.folding_enabled {
2144 return Rc::new(Vec::new());
2145 }
2146
2147 let mut cache = self.foldable_regions_cache.borrow_mut();
2148 if let Some((revision, regions)) = cache.as_ref()
2149 && *revision == self.buffer_revision
2150 {
2151 return regions.clone();
2152 }
2153
2154 let regions = Rc::new(folding::compute_foldable_regions(&self.buffer));
2155 *cache = Some((self.buffer_revision, regions.clone()));
2156 regions
2157 }
2158
2159 pub(crate) fn hidden_lines_set(&self) -> HashSet<usize> {
2163 if !self.folding_enabled || self.collapsed_folds.is_empty() {
2164 return HashSet::new();
2165 }
2166 let regions = self.foldable_regions();
2167 folding::hidden_lines(®ions, &self.collapsed_folds)
2168 }
2169
2170 pub fn set_auto_indent_enabled(&mut self, enabled: bool) {
2180 self.auto_indent_enabled = enabled;
2181 }
2182
2183 pub fn auto_indent_enabled(&self) -> bool {
2189 self.auto_indent_enabled
2190 }
2191
2192 pub fn set_indent_style(&mut self, style: IndentStyle) {
2198 self.indent_style = style;
2199 }
2200
2201 pub fn indent_style(&self) -> IndentStyle {
2207 self.indent_style
2208 }
2209
2210 pub fn set_search_replace_enabled(&mut self, enabled: bool) {
2228 self.search_replace_enabled = enabled;
2229 if !enabled && self.search_state.is_open {
2230 self.search_state.close();
2231 }
2232 }
2233
2234 pub fn search_replace_enabled(&self) -> bool {
2240 self.search_replace_enabled
2241 }
2242
2243 pub fn set_lsp_enabled(&mut self, enabled: bool) {
2259 self.lsp_enabled = enabled;
2260 if !enabled {
2261 self.detach_lsp();
2262 }
2263 }
2264
2265 pub fn lsp_enabled(&self) -> bool {
2269 self.lsp_enabled
2270 }
2271
2272 pub fn syntax(&self) -> &str {
2284 &self.syntax
2285 }
2286
2287 pub fn open_search_dialog(&mut self) -> iced::Task<Message> {
2296 self.update(&Message::OpenSearch)
2297 }
2298
2299 pub fn open_search_replace_dialog(&mut self) -> iced::Task<Message> {
2308 self.update(&Message::OpenSearchReplace)
2309 }
2310
2311 pub fn close_search_dialog(&mut self) -> iced::Task<Message> {
2317 self.update(&Message::CloseSearch)
2318 }
2319
2320 pub fn open_goto_line_dialog(&mut self) -> iced::Task<Message> {
2324 self.update(&Message::OpenGotoLine)
2325 }
2326
2327 pub fn close_goto_line_dialog(&mut self) -> iced::Task<Message> {
2329 self.update(&Message::CloseGotoLine)
2330 }
2331
2332 #[must_use]
2351 pub fn with_wrap_enabled(mut self, enabled: bool) -> Self {
2352 self.wrap_enabled = enabled;
2353 self
2354 }
2355
2356 #[must_use]
2371 pub fn with_folding_enabled(mut self, enabled: bool) -> Self {
2372 self.folding_enabled = enabled;
2373 self
2374 }
2375
2376 #[must_use]
2394 pub fn with_wrap_column(mut self, column: Option<usize>) -> Self {
2395 self.wrap_column = column;
2396 self
2397 }
2398
2399 pub fn set_line_numbers_enabled(&mut self, enabled: bool) {
2417 if self.line_numbers_enabled != enabled {
2418 self.line_numbers_enabled = enabled;
2419 self.content_cache.clear();
2420 self.overlay_cache.clear();
2421 }
2422 }
2423
2424 pub fn line_numbers_enabled(&self) -> bool {
2430 self.line_numbers_enabled
2431 }
2432
2433 #[must_use]
2452 pub fn with_line_numbers_enabled(mut self, enabled: bool) -> Self {
2453 self.line_numbers_enabled = enabled;
2454 self
2455 }
2456
2457 pub(crate) fn gutter_width(&self) -> f32 {
2463 self.line_number_gutter_width() + self.fold_margin_width()
2464 }
2465
2466 pub(crate) fn line_number_gutter_width(&self) -> f32 {
2468 if self.line_numbers_enabled { GUTTER_WIDTH } else { 0.0 }
2469 }
2470
2471 pub(crate) fn fold_margin_width(&self) -> f32 {
2474 if self.folding_enabled { FOLD_MARGIN_WIDTH } else { 0.0 }
2475 }
2476
2477 pub fn lose_focus(&mut self) {
2494 self.has_canvas_focus = false;
2495 self.show_cursor = false;
2496 self.ime_preedit = None;
2497 }
2498
2499 pub fn reset_focus_lock(&mut self) {
2515 self.focus_locked = false;
2516 }
2517
2518 pub fn cursor_screen_position(&self) -> Option<iced::Point> {
2539 let pos = self.cursors.primary_position();
2540 self.point_from_position(pos.0, pos.1)
2541 }
2542
2543 pub fn cursor_position(&self) -> (usize, usize) {
2562 self.cursors.primary_position()
2563 }
2564
2565 pub(crate) fn max_content_width(&self) -> f32 {
2574 let mut cache = self.max_content_width_cache.borrow_mut();
2575 if cache
2576 .as_ref()
2577 .is_none_or(|existing| existing.revision != self.buffer_revision)
2578 {
2579 let line_widths: Vec<f32> = (0..self.buffer.line_count())
2580 .map(|line| {
2581 measure_text_width(
2582 self.buffer.line(line),
2583 self.full_char_width,
2584 self.char_width,
2585 )
2586 })
2587 .collect();
2588 let mut width_counts = BTreeMap::new();
2589 for width in &line_widths {
2590 *width_counts.entry(width.to_bits()).or_insert(0) += 1;
2591 }
2592 *cache = Some(MaxContentWidthCache {
2593 revision: self.buffer_revision,
2594 line_widths,
2595 width_counts,
2596 });
2597 }
2598
2599 let gutter = self.gutter_width();
2600 let max_line_width =
2601 cache.as_ref().map_or(0.0, MaxContentWidthCache::max_width);
2602
2603 gutter + 5.0 + max_line_width + 20.0
2605 }
2606
2607 pub(crate) fn visual_lines_cached(
2625 &self,
2626 viewport_width: f32,
2627 ) -> Rc<Vec<wrapping::VisualLine>> {
2628 let key = VisualLinesKey {
2629 buffer_revision: self.buffer_revision,
2630 viewport_width_bits: viewport_width.to_bits(),
2631 gutter_width_bits: self.gutter_width().to_bits(),
2632 wrap_enabled: self.wrap_enabled,
2633 wrap_column: self.wrap_column,
2634 folding_enabled: self.folding_enabled,
2635 fold_revision: self.fold_revision,
2636 full_char_width_bits: self.full_char_width.to_bits(),
2637 char_width_bits: self.char_width.to_bits(),
2638 };
2639
2640 let mut cache = self.visual_lines_cache.borrow_mut();
2641 if let Some(existing) = cache.as_ref()
2642 && existing.key == key
2643 {
2644 return existing.visual_lines.clone();
2645 }
2646
2647 let hidden = self.hidden_lines_set();
2648 let wrapping_calc = wrapping::WrappingCalculator::new(
2649 self.wrap_enabled,
2650 self.wrap_column,
2651 self.full_char_width,
2652 self.char_width,
2653 );
2654 let visual_lines = wrapping_calc.calculate_visual_lines(
2655 &self.buffer,
2656 viewport_width,
2657 self.gutter_width(),
2658 &hidden,
2659 );
2660 let visual_lines = Rc::new(visual_lines);
2661
2662 *cache = Some(VisualLinesCache {
2663 key,
2664 visual_lines: visual_lines.clone(),
2665 buffer_line_count: self.buffer.line_count(),
2666 });
2667 visual_lines
2668 }
2669
2670 pub(crate) fn refresh_visual_lines_after_edit(
2677 &self,
2678 previous_revision: u64,
2679 ) {
2680 if !self.collapsed_folds.is_empty() {
2681 *self.visual_lines_cache.borrow_mut() = None;
2682 return;
2683 }
2684
2685 let mut cache_guard = self.visual_lines_cache.borrow_mut();
2686 let Some(cache) = cache_guard.as_mut() else { return };
2687 if cache.key.buffer_revision != previous_revision {
2688 *cache_guard = None;
2689 return;
2690 }
2691
2692 let same_layout = cache.key.gutter_width_bits
2693 == self.gutter_width().to_bits()
2694 && cache.key.wrap_enabled == self.wrap_enabled
2695 && cache.key.wrap_column == self.wrap_column
2696 && cache.key.folding_enabled == self.folding_enabled
2697 && cache.key.fold_revision == self.fold_revision
2698 && cache.key.full_char_width_bits == self.full_char_width.to_bits()
2699 && cache.key.char_width_bits == self.char_width.to_bits();
2700 if !same_layout {
2701 *cache_guard = None;
2702 return;
2703 }
2704
2705 let old_line_count = cache.buffer_line_count;
2706 let new_line_count = self.buffer.line_count();
2707 let start_line =
2708 self.pre_edit_line.saturating_sub(1).min(old_line_count);
2709 let old_end_line =
2710 self.pre_edit_last_line.saturating_add(2).min(old_line_count);
2711 let new_end_line = if new_line_count >= old_line_count {
2712 old_end_line
2713 .saturating_add(new_line_count - old_line_count)
2714 .min(new_line_count)
2715 } else {
2716 old_end_line
2717 .saturating_sub(old_line_count - new_line_count)
2718 .max(start_line)
2719 .min(new_line_count)
2720 };
2721
2722 let prefix_end = cache
2723 .visual_lines
2724 .partition_point(|visual| visual.logical_line < start_line);
2725 let suffix_start = cache
2726 .visual_lines
2727 .partition_point(|visual| visual.logical_line < old_end_line);
2728
2729 let wrapping_calc = wrapping::WrappingCalculator::new(
2730 self.wrap_enabled,
2731 self.wrap_column,
2732 self.full_char_width,
2733 self.char_width,
2734 );
2735 let changed_visual_lines = wrapping_calc.calculate_visual_lines_range(
2736 &self.buffer,
2737 f32::from_bits(cache.key.viewport_width_bits),
2738 f32::from_bits(cache.key.gutter_width_bits),
2739 &HashSet::new(),
2740 start_line..new_end_line,
2741 );
2742
2743 let old_segment_count = suffix_start.saturating_sub(prefix_end);
2744 let new_segment_count = changed_visual_lines.len();
2745 let visual_lines = Rc::make_mut(&mut cache.visual_lines);
2746
2747 if new_line_count == old_line_count
2751 && old_segment_count == new_segment_count
2752 {
2753 visual_lines[prefix_end..suffix_start]
2754 .clone_from_slice(&changed_visual_lines);
2755 } else {
2756 visual_lines.splice(prefix_end..suffix_start, changed_visual_lines);
2757
2758 let shifted_suffix_start = prefix_end + new_segment_count;
2759 for visual in &mut visual_lines[shifted_suffix_start..] {
2760 visual.logical_line = if new_line_count >= old_line_count {
2761 visual
2762 .logical_line
2763 .saturating_add(new_line_count - old_line_count)
2764 } else {
2765 visual
2766 .logical_line
2767 .saturating_sub(old_line_count - new_line_count)
2768 };
2769 }
2770 }
2771
2772 cache.key.buffer_revision = self.buffer_revision;
2773 cache.buffer_line_count = new_line_count;
2774 }
2775
2776 pub(crate) fn refresh_max_content_width_after_edit(
2781 &self,
2782 previous_revision: u64,
2783 ) {
2784 let mut cache_guard = self.max_content_width_cache.borrow_mut();
2785 let Some(cache) = cache_guard.as_mut() else { return };
2786 if cache.revision != previous_revision {
2787 *cache_guard = None;
2788 return;
2789 }
2790
2791 let old_line_count = cache.line_widths.len();
2792 let new_line_count = self.buffer.line_count();
2793 let start_line =
2794 self.pre_edit_line.saturating_sub(1).min(old_line_count);
2795 let old_end_line =
2796 self.pre_edit_last_line.saturating_add(2).min(old_line_count);
2797 if start_line == 0 && old_end_line == old_line_count {
2798 *cache_guard = None;
2799 return;
2800 }
2801
2802 let new_end_line = if new_line_count >= old_line_count {
2803 old_end_line
2804 .saturating_add(new_line_count - old_line_count)
2805 .min(new_line_count)
2806 } else {
2807 old_end_line
2808 .saturating_sub(old_line_count - new_line_count)
2809 .max(start_line)
2810 .min(new_line_count)
2811 };
2812 let old_widths = cache.line_widths[start_line..old_end_line].to_vec();
2813 let new_widths: Vec<f32> = (start_line..new_end_line)
2814 .map(|line| {
2815 measure_text_width(
2816 self.buffer.line(line),
2817 self.full_char_width,
2818 self.char_width,
2819 )
2820 })
2821 .collect();
2822
2823 for width in old_widths {
2824 cache.remove_width(width);
2825 }
2826 for width in &new_widths {
2827 cache.add_width(*width);
2828 }
2829 cache.line_widths.splice(start_line..old_end_line, new_widths);
2830 cache.revision = self.buffer_revision;
2831 }
2832
2833 pub fn lsp_request_definition(&mut self) {
2838 let position = self.lsp_position_from_cursor();
2839 if let (Some(client), Some(document)) =
2840 (self.lsp_client.as_mut(), self.lsp_document.as_ref())
2841 {
2842 client.request_definition(document, position);
2843 }
2844 }
2845
2846 pub fn lsp_request_definition_at(&mut self, point: iced::Point) -> bool {
2856 let Some(position) = self.lsp_position_from_point(point) else {
2857 return false;
2858 };
2859 if let (Some(client), Some(document)) =
2860 (self.lsp_client.as_mut(), self.lsp_document.as_ref())
2861 {
2862 client.request_definition(document, position);
2863 return true;
2864 }
2865 false
2866 }
2867}
2868
2869#[cfg(test)]
2870mod tests {
2871 use super::*;
2872 use std::cell::RefCell;
2873 use std::rc::Rc;
2874
2875 #[test]
2876 fn test_custom_context_menu_configuration() {
2877 let custom_entries = vec![
2878 ContextMenuEntry::item("format", "Format document")
2879 .with_shortcut("Shift+Alt+F"),
2880 ContextMenuEntry::separator(),
2881 ContextMenuEntry::Item(
2882 ContextMenuItem::new("rename", "Rename symbol")
2883 .with_enabled(false),
2884 ),
2885 ];
2886
2887 let editor = CodeEditor::new("", "rs")
2888 .with_custom_context_menu_entries(custom_entries.clone())
2889 .with_default_context_menu_enabled(false);
2890
2891 assert_eq!(editor.custom_context_menu_entries(), custom_entries);
2892 assert!(!editor.default_context_menu_enabled());
2893
2894 let default_editor = CodeEditor::new("", "rs");
2895 assert!(default_editor.custom_context_menu_entries().is_empty());
2896 assert!(default_editor.default_context_menu_enabled());
2897 }
2898
2899 #[test]
2900 fn test_reveal_in_file_manager_configuration() {
2901 let mut editor = CodeEditor::new("", "rs");
2902 assert!(!editor.reveal_in_file_manager_enabled());
2903
2904 editor.set_reveal_in_file_manager_enabled(true);
2905 assert!(editor.reveal_in_file_manager_enabled());
2906
2907 let editor =
2908 CodeEditor::new("", "rs").with_reveal_in_file_manager_enabled(true);
2909 assert!(editor.reveal_in_file_manager_enabled());
2910 }
2911
2912 #[test]
2913 fn vim_disabled_by_default() {
2914 let editor = CodeEditor::new("unchanged", "rs");
2915
2916 assert!(!editor.vim_enabled());
2917 assert_eq!(editor.vim_mode(), None);
2918 assert_eq!(editor.content(), "unchanged");
2919 assert!(!editor.can_undo());
2920 assert!(!editor.can_redo());
2921 }
2922
2923 #[test]
2924 fn vim_enable_enters_clean_normal_mode() {
2925 let mut editor = CodeEditor::new("unchanged", "rs");
2926 assert_eq!(editor.vim_state.parse_key('9'), None);
2927 assert_eq!(editor.vim_state.parse_key('d'), None);
2928
2929 editor.set_vim_enabled(true);
2930
2931 assert!(editor.vim_enabled());
2932 assert_eq!(editor.vim_mode(), Some(VimMode::Normal));
2933 assert_eq!(
2934 editor.vim_state.parse_key('l'),
2935 Some(vim::VimAction::Motion {
2936 motion: vim::VimMotion::Right,
2937 count: 1,
2938 explicit_count: false,
2939 })
2940 );
2941 assert_eq!(editor.content(), "unchanged");
2942 assert!(!editor.can_undo());
2943 assert!(!editor.can_redo());
2944 }
2945
2946 #[test]
2947 fn vim_disable_clears_pending_state() {
2948 let mut editor =
2949 CodeEditor::new("unchanged", "rs").with_vim_enabled(true);
2950 assert_eq!(editor.vim_state.parse_key('4'), None);
2951 assert_eq!(editor.vim_state.parse_key('d'), None);
2952
2953 editor.set_vim_enabled(false);
2954 assert!(!editor.vim_enabled());
2955 assert_eq!(editor.vim_mode(), None);
2956
2957 editor.set_vim_enabled(true);
2958 assert_eq!(
2959 editor.vim_state.parse_key('w'),
2960 Some(vim::VimAction::Motion {
2961 motion: vim::VimMotion::WordForward,
2962 count: 1,
2963 explicit_count: false,
2964 })
2965 );
2966 assert_eq!(editor.content(), "unchanged");
2967 assert!(!editor.can_undo());
2968 assert!(!editor.can_redo());
2969 }
2970
2971 #[test]
2972 fn vim_reset_clears_pending_state() {
2973 let mut editor = CodeEditor::new("before", "rs").with_vim_enabled(true);
2974 assert_eq!(editor.vim_state.parse_key('3'), None);
2975 assert_eq!(editor.vim_state.parse_key('g'), None);
2976
2977 let _ = editor.reset("after");
2978
2979 assert_eq!(editor.vim_mode(), Some(VimMode::Normal));
2980 assert_eq!(editor.vim_state.parse_key('g'), None);
2981 assert_eq!(
2982 editor.vim_state.parse_key('g'),
2983 Some(vim::VimAction::Motion {
2984 motion: vim::VimMotion::DocumentStart,
2985 count: 1,
2986 explicit_count: false,
2987 })
2988 );
2989 assert_eq!(editor.content(), "after");
2990 assert!(!editor.can_undo());
2991 assert!(!editor.can_redo());
2992 }
2993
2994 #[test]
2995 fn test_compare_floats() {
2996 assert_eq!(
2998 compare_floats(1.0, 1.0),
2999 CmpOrdering::Equal,
3000 "Exact equality"
3001 );
3002 assert_eq!(
3003 compare_floats(1.0, 1.0 + 0.0001),
3004 CmpOrdering::Equal,
3005 "Within epsilon (positive)"
3006 );
3007 assert_eq!(
3008 compare_floats(1.0, 1.0 - 0.0001),
3009 CmpOrdering::Equal,
3010 "Within epsilon (negative)"
3011 );
3012
3013 assert_eq!(
3015 compare_floats(1.0 + 0.002, 1.0),
3016 CmpOrdering::Greater,
3017 "Definitely greater"
3018 );
3019 assert_eq!(
3020 compare_floats(1.0011, 1.0),
3021 CmpOrdering::Greater,
3022 "Just above epsilon"
3023 );
3024
3025 assert_eq!(
3027 compare_floats(1.0, 1.0 + 0.002),
3028 CmpOrdering::Less,
3029 "Definitely less"
3030 );
3031 assert_eq!(
3032 compare_floats(1.0, 1.0011),
3033 CmpOrdering::Less,
3034 "Just below negative epsilon"
3035 );
3036 }
3037
3038 #[test]
3039 fn test_measure_text_width_ascii() {
3040 let text = "abc";
3042 let width = measure_text_width(text, FONT_SIZE, CHAR_WIDTH);
3043 let expected = CHAR_WIDTH * 3.0;
3044 assert_eq!(
3045 compare_floats(width, expected),
3046 CmpOrdering::Equal,
3047 "Width mismatch for ASCII"
3048 );
3049 }
3050
3051 #[test]
3052 fn test_measure_text_width_cjk() {
3053 let text = "你好";
3057 let width = measure_text_width(text, FONT_SIZE, CHAR_WIDTH);
3058 let expected = FONT_SIZE * 2.0;
3059 assert_eq!(
3060 compare_floats(width, expected),
3061 CmpOrdering::Equal,
3062 "Width mismatch for CJK"
3063 );
3064 }
3065
3066 #[test]
3067 fn test_measure_text_width_mixed() {
3068 let text = "Hi你好";
3071 let width = measure_text_width(text, FONT_SIZE, CHAR_WIDTH);
3072 let expected = CHAR_WIDTH * 2.0 + FONT_SIZE * 2.0;
3073 assert_eq!(
3074 compare_floats(width, expected),
3075 CmpOrdering::Equal,
3076 "Width mismatch for mixed content"
3077 );
3078 }
3079
3080 #[test]
3081 fn test_measure_text_width_control_chars() {
3082 let text = "\t\n";
3085 let width = measure_text_width(text, FONT_SIZE, CHAR_WIDTH);
3086 let expected = CHAR_WIDTH * TAB_WIDTH as f32;
3087 assert_eq!(
3088 compare_floats(width, expected),
3089 CmpOrdering::Equal,
3090 "Width mismatch for control chars"
3091 );
3092 }
3093
3094 #[test]
3095 fn test_measure_text_width_empty() {
3096 let text = "";
3097 let width = measure_text_width(text, FONT_SIZE, CHAR_WIDTH);
3098 assert!(
3099 (width - 0.0).abs() < f32::EPSILON,
3100 "Width should be 0 for empty string"
3101 );
3102 }
3103
3104 #[test]
3105 fn test_measure_text_width_emoji() {
3106 let text = "👋";
3108 let width = measure_text_width(text, FONT_SIZE, CHAR_WIDTH);
3109 let expected = FONT_SIZE;
3110 assert_eq!(
3111 compare_floats(width, expected),
3112 CmpOrdering::Equal,
3113 "Width mismatch for emoji"
3114 );
3115 }
3116
3117 #[test]
3118 fn test_measure_text_width_korean() {
3119 let text = "안녕하세요";
3123 let width = measure_text_width(text, FONT_SIZE, CHAR_WIDTH);
3124 let expected = FONT_SIZE * 5.0;
3125 assert_eq!(
3126 compare_floats(width, expected),
3127 CmpOrdering::Equal,
3128 "Width mismatch for Korean"
3129 );
3130 }
3131
3132 #[test]
3133 fn test_measure_text_width_japanese() {
3134 let text_hiragana = "こんにちは";
3139 let width_hiragana =
3140 measure_text_width(text_hiragana, FONT_SIZE, CHAR_WIDTH);
3141 let expected_hiragana = FONT_SIZE * 5.0;
3142 assert_eq!(
3143 compare_floats(width_hiragana, expected_hiragana),
3144 CmpOrdering::Equal,
3145 "Width mismatch for Hiragana"
3146 );
3147
3148 let text_katakana = "カタカナ";
3149 let width_katakana =
3150 measure_text_width(text_katakana, FONT_SIZE, CHAR_WIDTH);
3151 let expected_katakana = FONT_SIZE * 4.0;
3152 assert_eq!(
3153 compare_floats(width_katakana, expected_katakana),
3154 CmpOrdering::Equal,
3155 "Width mismatch for Katakana"
3156 );
3157
3158 let text_kanji = "漢字";
3159 let width_kanji = measure_text_width(text_kanji, FONT_SIZE, CHAR_WIDTH);
3160 let expected_kanji = FONT_SIZE * 2.0;
3161 assert_eq!(
3162 compare_floats(width_kanji, expected_kanji),
3163 CmpOrdering::Equal,
3164 "Width mismatch for Kanji"
3165 );
3166 }
3167
3168 #[test]
3169 fn test_set_font_size() {
3170 let mut editor = CodeEditor::new("", "rs");
3171
3172 assert!((editor.font_size() - 14.0).abs() < f32::EPSILON);
3174 assert!((editor.line_height() - 20.0).abs() < f32::EPSILON);
3175
3176 editor.set_font_size(28.0, true);
3178 assert!((editor.font_size() - 28.0).abs() < f32::EPSILON);
3179 assert_eq!(
3181 compare_floats(editor.line_height(), 40.0),
3182 CmpOrdering::Equal
3183 );
3184
3185 editor.set_line_height(50.0);
3188 editor.set_font_size(14.0, false);
3190 assert!((editor.font_size() - 14.0).abs() < f32::EPSILON);
3191 assert_eq!(
3193 compare_floats(editor.line_height(), 50.0),
3194 CmpOrdering::Equal
3195 );
3196 assert!(editor.char_width > 0.0);
3200 assert!((editor.char_width - CHAR_WIDTH).abs() < 0.5);
3201 }
3202
3203 #[test]
3204 fn test_measure_single_char_width() {
3205 let editor = CodeEditor::new("", "rs");
3206
3207 let width_a = editor.measure_single_char_width("a");
3209 assert!(width_a > 0.0, "Width of 'a' should be positive");
3210
3211 let width_cjk = editor.measure_single_char_width("汉");
3213 assert!(width_cjk > 0.0, "Width of '汉' should be positive");
3214
3215 assert!(
3216 width_cjk > width_a,
3217 "Width of '汉' should be greater than 'a'"
3218 );
3219
3220 assert!(width_cjk >= width_a * 1.5);
3223 }
3224
3225 #[test]
3226 fn test_set_line_height() {
3227 let mut editor = CodeEditor::new("", "rs");
3228
3229 assert!((editor.line_height() - LINE_HEIGHT).abs() < f32::EPSILON);
3231
3232 editor.set_line_height(35.0);
3234 assert!((editor.line_height() - 35.0).abs() < f32::EPSILON);
3235
3236 assert!((editor.font_size() - FONT_SIZE).abs() < f32::EPSILON);
3238 }
3239
3240 #[test]
3241 fn test_visual_lines_cached_reuses_cache_for_same_key() {
3242 let editor = CodeEditor::new("a\nb\nc", "rs");
3243
3244 let first = editor.visual_lines_cached(800.0);
3245 let second = editor.visual_lines_cached(800.0);
3246
3247 assert!(
3248 Rc::ptr_eq(&first, &second),
3249 "visual_lines_cached should reuse the cached Rc for identical keys"
3250 );
3251 }
3252
3253 #[derive(Default)]
3254 struct TestLspClient {
3255 changes: Rc<RefCell<Vec<Vec<lsp::LspTextChange>>>>,
3256 }
3257
3258 impl lsp::LspClient for TestLspClient {
3259 fn did_change(
3260 &mut self,
3261 _document: &lsp::LspDocument,
3262 changes: &[lsp::LspTextChange],
3263 ) {
3264 self.changes.borrow_mut().push(changes.to_vec());
3265 }
3266 }
3267
3268 #[test]
3269 fn test_word_start_in_line() {
3270 let line = "foo_bar baz";
3271 assert_eq!(CodeEditor::word_start_in_line(line, 0), 0);
3272 assert_eq!(CodeEditor::word_start_in_line(line, 2), 0);
3273 assert_eq!(CodeEditor::word_start_in_line(line, 4), 0);
3274 assert_eq!(CodeEditor::word_start_in_line(line, 7), 0);
3275 assert_eq!(CodeEditor::word_start_in_line(line, 9), 8);
3276 }
3277
3278 #[test]
3279 fn test_enqueue_lsp_change_auto_flush() {
3280 let changes = Rc::new(RefCell::new(Vec::new()));
3281 let client = TestLspClient { changes: Rc::clone(&changes) };
3282 let mut editor = CodeEditor::new("hello", "rs");
3283 editor.attach_lsp(
3284 Box::new(client),
3285 lsp::LspDocument::new("file:///test.rs", "rust"),
3286 );
3287 editor.set_lsp_auto_flush(true);
3288
3289 editor.buffer.insert_char(0, 5, '!');
3290 editor.enqueue_lsp_change();
3291
3292 let changes = changes.borrow();
3293 assert_eq!(changes.len(), 1);
3294 assert_eq!(changes[0].len(), 1);
3295 let change = &changes[0][0];
3296 assert_eq!(change.text, "!");
3297 assert_eq!(change.range.start.line, 0);
3298 assert_eq!(change.range.start.character, 5);
3299 assert_eq!(change.range.end.line, 0);
3300 assert_eq!(change.range.end.character, 5);
3301 }
3302
3303 #[test]
3304 fn test_editor_update_sends_bounded_incremental_lsp_change() {
3305 let changes = Rc::new(RefCell::new(Vec::new()));
3306 let client = TestLspClient { changes: Rc::clone(&changes) };
3307 let content = (0..10)
3308 .map(|line| format!("line{line}"))
3309 .collect::<Vec<_>>()
3310 .join("\n");
3311 let mut editor = CodeEditor::new(&content, "rs");
3312 editor.attach_lsp(
3313 Box::new(client),
3314 lsp::LspDocument::new("file:///large.rs", "rust"),
3315 );
3316 editor.request_focus();
3317 editor.has_canvas_focus = true;
3318 editor.focus_locked = false;
3319 editor.cursors.primary_mut().position = (5, 2);
3320
3321 let _ = editor.update(&Message::CharacterInput('X'));
3322
3323 let changes = changes.borrow();
3324 assert_eq!(changes.len(), 1);
3325 assert_eq!(changes[0].len(), 1);
3326 let change = &changes[0][0];
3327 assert_eq!(change.range.start.line, 4);
3328 assert_eq!(change.range.start.character, 0);
3329 assert_eq!(change.range.end.line, 7);
3330 assert_eq!(change.range.end.character, 0);
3331 assert_eq!(change.text, "line4\nliXne5\nline6\n");
3332 assert!(!editor.lsp_shadow_is_current);
3333 assert!(editor.lsp_shadow_text.is_empty());
3334 }
3335
3336 #[test]
3337 fn test_visual_lines_cached_changes_on_viewport_width_change() {
3338 let editor = CodeEditor::new("a\nb\nc", "rs");
3339
3340 let first = editor.visual_lines_cached(800.0);
3341 let second = editor.visual_lines_cached(801.0);
3342
3343 assert!(
3344 !Rc::ptr_eq(&first, &second),
3345 "visual_lines_cached should recompute when viewport width changes"
3346 );
3347 }
3348
3349 #[test]
3350 fn test_visual_lines_cached_changes_on_buffer_revision_change() {
3351 let mut editor = CodeEditor::new("a\nb\nc", "rs");
3352
3353 let first = editor.visual_lines_cached(800.0);
3354 editor.buffer_revision = editor.buffer_revision.wrapping_add(1);
3355 let second = editor.visual_lines_cached(800.0);
3356
3357 assert!(
3358 !Rc::ptr_eq(&first, &second),
3359 "visual_lines_cached should recompute when buffer_revision changes"
3360 );
3361 }
3362
3363 #[test]
3364 fn test_max_content_width_increases_with_longer_lines() {
3365 let short = CodeEditor::new("ab", "rs");
3366 let long =
3367 CodeEditor::new("abcdefghijklmnopqrstuvwxyz0123456789", "rs");
3368
3369 assert!(
3370 long.max_content_width() > short.max_content_width(),
3371 "Longer lines should produce a greater max_content_width"
3372 );
3373 }
3374
3375 #[test]
3376 fn test_max_content_width_cached_by_revision() {
3377 let mut editor = CodeEditor::new("hello", "rs");
3378 let w1 = editor.max_content_width();
3379
3380 let w2 = editor.max_content_width();
3382 assert!(
3383 (w1 - w2).abs() < f32::EPSILON,
3384 "Repeated calls with same revision should return identical value"
3385 );
3386
3387 editor.buffer_revision = editor.buffer_revision.wrapping_add(1);
3389 editor.buffer = crate::text_buffer::TextBuffer::new(
3391 "hello world with extra content",
3392 );
3393 let w3 = editor.max_content_width();
3394 assert!(
3395 w3 > w1,
3396 "After revision bump with longer content, width should increase"
3397 );
3398 }
3399
3400 #[test]
3401 fn test_max_content_width_cache_updates_incrementally_after_newline() {
3402 let mut editor =
3403 CodeEditor::new("short\nthis is the longest line\ntail", "rs");
3404 editor.set_wrap_enabled(false);
3405 editor.request_focus();
3406 editor.has_canvas_focus = true;
3407 editor.focus_locked = false;
3408 editor.cursors.primary_mut().position = (1, 7);
3409 let _ = editor.max_content_width();
3410
3411 let _ = editor.update(&Message::Enter);
3412 let incremental = editor.max_content_width();
3413 let expected = CodeEditor::new(&editor.content(), "rs");
3414
3415 assert!(
3416 (incremental - expected.max_content_width()).abs() < f32::EPSILON
3417 );
3418 let cache = editor.max_content_width_cache.borrow();
3419 assert_eq!(
3420 cache.as_ref().map(|cache| cache.line_widths.len()),
3421 Some(editor.buffer.line_count())
3422 );
3423 assert_eq!(
3424 cache.as_ref().map(|cache| cache.revision),
3425 Some(editor.buffer_revision)
3426 );
3427 }
3428
3429 #[test]
3430 fn test_syntax_getter() {
3431 let editor = CodeEditor::new("", "lua");
3432 assert_eq!(editor.syntax(), "lua");
3433 }
3434
3435 fn folding_editor() -> CodeEditor {
3438 CodeEditor::new(
3439 "fn main() {\n let x = 1;\n if x > 0 {\n print();\n }\n}",
3440 "rs",
3441 )
3442 }
3443
3444 #[test]
3445 fn test_folding_enabled_by_default() {
3446 let editor = CodeEditor::new("fn main() {}", "rs");
3447 assert!(editor.folding_enabled());
3448 }
3449
3450 #[test]
3451 fn test_foldable_regions_detected() {
3452 let editor = folding_editor();
3453 let regions = editor.foldable_regions();
3454 assert_eq!(
3455 *regions,
3456 vec![
3457 folding::FoldRegion::new(0, 4),
3458 folding::FoldRegion::new(2, 3)
3459 ]
3460 );
3461 }
3462
3463 #[test]
3464 fn test_toggle_fold_hides_and_shows_lines() {
3465 let mut editor = folding_editor();
3466 let width = editor.viewport_width;
3467 let total = editor.visual_lines_cached(width).len();
3468
3469 editor.toggle_fold(0);
3470 assert!(editor.is_folded(0));
3471 assert_eq!(editor.visual_lines_cached(width).len(), 2);
3473
3474 editor.toggle_fold(0);
3475 assert!(!editor.is_folded(0));
3476 assert_eq!(editor.visual_lines_cached(width).len(), total);
3477 }
3478
3479 #[test]
3480 fn test_toggle_fold_ignores_non_header() {
3481 let mut editor = folding_editor();
3482 editor.toggle_fold(3); assert!(!editor.is_folded(3));
3484 assert!(editor.collapsed_folds.is_empty());
3485 }
3486
3487 #[test]
3488 fn test_fold_at_picks_innermost_region() {
3489 let mut editor = folding_editor();
3490 editor.fold_at(3);
3492 assert!(editor.is_folded(2));
3493 assert!(!editor.is_folded(0));
3494 assert_eq!(editor.hidden_lines_set(), [3].into_iter().collect());
3495 }
3496
3497 #[test]
3498 fn test_unfold_at_expands_innermost_region() {
3499 let mut editor = folding_editor();
3500 editor.fold_at(3);
3501 editor.unfold_at(2);
3502 assert!(!editor.is_folded(2));
3503 assert!(editor.hidden_lines_set().is_empty());
3504 }
3505
3506 #[test]
3507 fn test_toggle_fold_at_cursor_folds_then_unfolds() {
3508 let mut editor = folding_editor();
3509 editor.toggle_fold_at(3);
3511 assert!(editor.is_folded(2));
3512
3513 editor.toggle_fold_at(2);
3515 assert!(!editor.is_folded(2));
3516 }
3517
3518 #[test]
3519 fn test_toggle_fold_at_ignores_unfoldable_line() {
3520 let mut editor = CodeEditor::new("a\nb\nc", "rs");
3521 editor.toggle_fold_at(1);
3522 assert!(editor.collapsed_folds.is_empty());
3523 }
3524
3525 #[test]
3526 fn test_fold_all_and_unfold_all() {
3527 let mut editor = folding_editor();
3528 editor.fold_all();
3529 assert!(editor.is_folded(0));
3530 assert!(editor.is_folded(2));
3531 assert_eq!(
3533 editor.hidden_lines_set(),
3534 [1, 2, 3, 4].into_iter().collect()
3535 );
3536
3537 editor.unfold_all();
3538 assert!(editor.collapsed_folds.is_empty());
3539 assert!(editor.hidden_lines_set().is_empty());
3540 }
3541
3542 #[test]
3543 fn test_fold_moves_cursor_out_of_hidden_lines() {
3544 let mut editor = folding_editor();
3545 editor.cursors.set_single((3, 2));
3546 editor.fold_all();
3547 assert_eq!(editor.cursors.primary_position(), (0, 0));
3549 }
3550
3551 #[test]
3552 fn test_disabled_folding_yields_no_regions() {
3553 let mut editor = folding_editor();
3554 editor.set_folding_enabled(false);
3555 assert!(editor.foldable_regions().is_empty());
3556 editor.collapsed_folds.insert(0);
3558 assert!(editor.hidden_lines_set().is_empty());
3559 }
3560}