1use crate::model::buffer::{Buffer, LineNumber};
2use crate::model::cursor::{Cursor, Cursors};
3use crate::model::document_model::{
4 DocumentCapabilities, DocumentModel, DocumentPosition, ViewportContent, ViewportLine,
5};
6use crate::model::event::{
7 Event, MarginContentData, MarginPositionData, OverlayFace as EventOverlayFace, PopupData,
8 PopupPositionData,
9};
10use crate::model::filesystem::FileSystem;
11use crate::model::marker::{MarkerId, MarkerList};
12use crate::primitives::detected_language::DetectedLanguage;
13use crate::primitives::grammar::GrammarRegistry;
14use crate::primitives::highlight_engine::HighlightEngine;
15use crate::primitives::indent::IndentCalculator;
16use crate::primitives::reference_highlighter::ReferenceHighlighter;
17use crate::primitives::text_property::TextPropertyManager;
18use crate::view::bracket_highlight_overlay::BracketHighlightOverlay;
19use crate::view::conceal::ConcealManager;
20use crate::view::folding::LspFoldRanges;
21use crate::view::margin::{MarginAnnotation, MarginContent, MarginManager, MarginPosition};
22use crate::view::overlay::{Overlay, OverlayFace, OverlayManager, UnderlineStyle};
23use crate::view::popup::{
24 Popup, PopupContent, PopupKind, PopupListItem, PopupManager, PopupPosition,
25};
26use crate::view::reference_highlight_overlay::ReferenceHighlightOverlay;
27use crate::view::soft_break::SoftBreakManager;
28use crate::view::virtual_text::VirtualTextManager;
29use anyhow::Result;
30use ratatui::style::{Color, Style};
31use std::cell::RefCell;
32use std::ops::Range;
33use std::sync::Arc;
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
39pub enum DisplacedMarker {
40 Main { id: u64, position: usize },
42 Margin { id: u64, position: usize },
44}
45
46impl DisplacedMarker {
47 pub fn encode(&self) -> (u64, usize) {
49 match self {
50 Self::Main { id, position } => (*id, *position),
51 Self::Margin { id, position } => (*id | (1u64 << 63), *position),
52 }
53 }
54
55 pub fn decode(tagged_id: u64, position: usize) -> Self {
57 if (tagged_id >> 63) == 1 {
58 Self::Margin {
59 id: tagged_id & !(1u64 << 63),
60 position,
61 }
62 } else {
63 Self::Main {
64 id: tagged_id,
65 position,
66 }
67 }
68 }
69}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
73pub enum ViewMode {
74 Source,
76 PageView,
79}
80
81#[derive(Debug, Clone)]
92pub struct BufferSettings {
93 pub whitespace: crate::config::WhitespaceVisibility,
96
97 pub use_tabs: bool,
100
101 pub tab_size: usize,
105
106 pub auto_close: bool,
109
110 pub auto_surround: bool,
113
114 pub word_characters: String,
117}
118
119impl Default for BufferSettings {
120 fn default() -> Self {
121 Self {
122 whitespace: crate::config::WhitespaceVisibility::default(),
123 use_tabs: false,
124 tab_size: 4,
125 auto_close: true,
126 auto_surround: true,
127 word_characters: String::new(),
128 }
129 }
130}
131
132pub struct EditorState {
138 pub buffer: Buffer,
140
141 pub highlighter: HighlightEngine,
143
144 pub indent_calculator: RefCell<IndentCalculator>,
146
147 pub overlays: OverlayManager,
149
150 pub marker_list: MarkerList,
152
153 pub virtual_texts: VirtualTextManager,
155
156 pub conceals: ConcealManager,
158
159 pub soft_breaks: SoftBreakManager,
161
162 pub popups: PopupManager,
164
165 pub margins: MarginManager,
167
168 pub primary_cursor_line_number: LineNumber,
171
172 pub mode: String,
174
175 pub text_properties: TextPropertyManager,
178
179 pub show_cursors: bool,
182
183 pub cursor_visibility_locked: bool,
189
190 pub editing_disabled: bool,
194
195 pub scrollable: bool,
199
200 pub buffer_settings: BufferSettings,
203
204 pub reference_highlighter: ReferenceHighlighter,
206
207 pub is_composite_buffer: bool,
209
210 pub debug_highlight_mode: bool,
212
213 pub reference_highlight_overlay: ReferenceHighlightOverlay,
215
216 pub bracket_highlight_overlay: BracketHighlightOverlay,
218
219 pub semantic_tokens: Option<SemanticTokenStore>,
221
222 pub folding_ranges: LspFoldRanges,
226
227 pub language: String,
230
231 pub display_name: String,
236
237 pub line_wrap_cache: crate::view::line_wrap_cache::LineWrapCache,
244
245 pub visual_row_index: crate::view::visual_row_index::VisualRowIndex,
254}
255
256impl EditorState {
257 pub fn apply_language(&mut self, detected: DetectedLanguage) {
264 self.language = detected.name;
265 self.display_name = detected.display_name;
266 self.highlighter = detected.highlighter;
267 if let Some(lang) = &detected.ts_language {
268 self.reference_highlighter.set_language(lang);
269 }
270 }
271
272 fn new_from_buffer(buffer: Buffer) -> Self {
275 let mut marker_list = MarkerList::new();
276 if !buffer.is_empty() {
277 marker_list.adjust_for_insert(0, buffer.len());
278 }
279
280 Self {
281 buffer,
282 highlighter: HighlightEngine::None,
283 indent_calculator: RefCell::new(IndentCalculator::new()),
284 overlays: OverlayManager::new(),
285 marker_list,
286 virtual_texts: VirtualTextManager::new(),
287 conceals: ConcealManager::new(),
288 soft_breaks: SoftBreakManager::new(),
289 popups: PopupManager::new(),
290 margins: MarginManager::new(),
291 primary_cursor_line_number: LineNumber::Absolute(0),
292 mode: "insert".to_string(),
293 text_properties: TextPropertyManager::new(),
294 show_cursors: true,
295 cursor_visibility_locked: false,
296 editing_disabled: false,
297 scrollable: true,
298 buffer_settings: BufferSettings::default(),
299 reference_highlighter: ReferenceHighlighter::new(),
300 is_composite_buffer: false,
301 debug_highlight_mode: false,
302 reference_highlight_overlay: ReferenceHighlightOverlay::new(),
303 bracket_highlight_overlay: BracketHighlightOverlay::new(),
304 semantic_tokens: None,
305 folding_ranges: LspFoldRanges::new(),
306 language: "text".to_string(),
307 display_name: "Text".to_string(),
308 line_wrap_cache: crate::view::line_wrap_cache::LineWrapCache::default(),
309 visual_row_index: crate::view::visual_row_index::VisualRowIndex::default(),
310 }
311 }
312
313 pub fn new(
314 _width: u16,
315 _height: u16,
316 large_file_threshold: usize,
317 fs: Arc<dyn FileSystem + Send + Sync>,
318 ) -> Self {
319 Self::new_from_buffer(Buffer::new(large_file_threshold, fs))
320 }
321
322 pub fn new_with_path(
325 large_file_threshold: usize,
326 fs: Arc<dyn FileSystem + Send + Sync>,
327 path: std::path::PathBuf,
328 ) -> Self {
329 Self::new_from_buffer(Buffer::new_with_path(large_file_threshold, fs, path))
330 }
331
332 pub fn set_language_from_name(&mut self, name: &str, registry: &GrammarRegistry) {
336 let detected = DetectedLanguage::from_virtual_name(name, registry);
337 tracing::debug!(
338 "Set highlighter for virtual buffer based on name: {} (backend: {}, language: {})",
339 name,
340 detected.highlighter.backend_name(),
341 detected.name
342 );
343 self.apply_language(detected);
344 }
345
346 pub fn from_file(
351 path: &std::path::Path,
352 _width: u16,
353 _height: u16,
354 large_file_threshold: usize,
355 registry: &GrammarRegistry,
356 fs: Arc<dyn FileSystem + Send + Sync>,
357 ) -> anyhow::Result<Self> {
358 let buffer = Buffer::load_from_file(path, large_file_threshold, fs)?;
359 let first_line = buffer.first_line_lossy();
360 let detected = registry
361 .find_by_path(path, first_line.as_deref())
362 .map(|entry| DetectedLanguage::from_entry(entry, registry))
363 .unwrap_or_else(DetectedLanguage::plain_text);
364 let mut state = Self::new_from_buffer(buffer);
365 state.apply_language(detected);
366 Ok(state)
367 }
368
369 pub fn from_file_with_languages(
377 path: &std::path::Path,
378 _width: u16,
379 _height: u16,
380 large_file_threshold: usize,
381 registry: &GrammarRegistry,
382 languages: &std::collections::HashMap<String, crate::config::LanguageConfig>,
383 fs: Arc<dyn FileSystem + Send + Sync>,
384 ) -> anyhow::Result<Self> {
385 let buffer = Buffer::load_from_file(path, large_file_threshold, fs)?;
386 let first_line = buffer.first_line_lossy();
387 let detected =
388 DetectedLanguage::from_path(path, first_line.as_deref(), registry, languages);
389 let mut state = Self::new_from_buffer(buffer);
390 state.apply_language(detected);
391 Ok(state)
392 }
393
394 pub fn from_buffer_with_language(buffer: Buffer, detected: DetectedLanguage) -> Self {
399 let mut state = Self::new_from_buffer(buffer);
400 state.apply_language(detected);
401 state
402 }
403
404 fn apply_insert(
406 &mut self,
407 cursors: &mut Cursors,
408 position: usize,
409 text: &str,
410 cursor_id: crate::model::event::CursorId,
411 ) {
412 let newlines_inserted = text.matches('\n').count();
413
414 self.marker_list.adjust_for_insert(position, text.len());
416 self.margins.adjust_for_insert(position, text.len());
417
418 self.buffer.insert(position, text);
420
421 self.highlighter.notify_insert(position, text.len());
424 self.highlighter
425 .invalidate_range(position..position + text.len());
426
427 cursors.adjust_for_edit(position, 0, text.len());
432
433 if let Some(cursor) = cursors.get_mut(cursor_id) {
435 cursor.position = position + text.len();
436 cursor.clear_selection();
437 }
438
439 if cursor_id == cursors.primary_id() {
441 self.primary_cursor_line_number = match self.primary_cursor_line_number {
442 LineNumber::Absolute(line) => LineNumber::Absolute(line + newlines_inserted),
443 LineNumber::Relative {
444 line,
445 from_cached_line,
446 } => LineNumber::Relative {
447 line: line + newlines_inserted,
448 from_cached_line,
449 },
450 };
451 }
452 }
453
454 fn apply_delete(
456 &mut self,
457 cursors: &mut Cursors,
458 range: &std::ops::Range<usize>,
459 cursor_id: crate::model::event::CursorId,
460 deleted_text: &str,
461 ) {
462 let len = range.len();
463
464 let primary_newlines_removed = if cursor_id == cursors.primary_id() {
468 let cursor_pos = cursors.get(cursor_id).map_or(range.start, |c| c.position);
469 let bytes_before_cursor = cursor_pos
470 .saturating_sub(range.start)
471 .min(len)
472 .min(deleted_text.len());
473 deleted_text[..bytes_before_cursor].matches('\n').count()
474 } else {
475 0
476 };
477
478 self.virtual_texts
484 .remove_in_range(&mut self.marker_list, range.start, range.end);
485
486 self.marker_list.adjust_for_delete(range.start, len);
488 self.margins.adjust_for_delete(range.start, len);
489
490 self.buffer.delete(range.clone());
492
493 self.highlighter.notify_delete(range.start, len);
496 self.highlighter.invalidate_range(range.clone());
497
498 cursors.adjust_for_edit(range.start, len, 0);
503
504 if let Some(cursor) = cursors.get_mut(cursor_id) {
506 cursor.position = range.start;
507 cursor.clear_selection();
508 }
509
510 if cursor_id == cursors.primary_id() && primary_newlines_removed > 0 {
512 self.primary_cursor_line_number = match self.primary_cursor_line_number {
513 LineNumber::Absolute(line) => {
514 LineNumber::Absolute(line.saturating_sub(primary_newlines_removed))
515 }
516 LineNumber::Relative {
517 line,
518 from_cached_line,
519 } => LineNumber::Relative {
520 line: line.saturating_sub(primary_newlines_removed),
521 from_cached_line,
522 },
523 };
524 }
525 }
526
527 pub fn apply(&mut self, cursors: &mut Cursors, event: &Event) {
530 match event {
531 Event::Insert {
532 position,
533 text,
534 cursor_id,
535 } => self.apply_insert(cursors, *position, text, *cursor_id),
536
537 Event::Delete {
538 range,
539 cursor_id,
540 deleted_text,
541 } => self.apply_delete(cursors, range, *cursor_id, deleted_text),
542
543 Event::MoveCursor {
544 cursor_id,
545 new_position,
546 new_anchor,
547 new_sticky_column,
548 ..
549 } => {
550 if let Some(cursor) = cursors.get_mut(*cursor_id) {
551 cursor.position = *new_position;
552 cursor.anchor = *new_anchor;
553 cursor.sticky_column = *new_sticky_column;
554 }
555
556 if *cursor_id == cursors.primary_id() {
559 self.primary_cursor_line_number =
560 match self.buffer.offset_to_position(*new_position) {
561 Some(pos) => LineNumber::Absolute(pos.line),
562 None => {
563 let estimated_line = *new_position / 80;
566 LineNumber::Absolute(estimated_line)
567 }
568 };
569 }
570 }
571
572 Event::AddCursor {
573 cursor_id,
574 position,
575 anchor,
576 } => {
577 let cursor = if let Some(anchor) = anchor {
578 Cursor::with_selection(*anchor, *position)
579 } else {
580 Cursor::new(*position)
581 };
582
583 cursors.insert_with_id(*cursor_id, cursor);
586
587 cursors.normalize();
588 }
589
590 Event::RemoveCursor { cursor_id, .. } => {
591 cursors.remove(*cursor_id);
592 }
593
594 Event::Scroll { .. } | Event::SetViewport { .. } | Event::Recenter => {
597 tracing::warn!("View event {:?} reached EditorState.apply() - should be handled by SplitViewState", event);
600 }
601
602 Event::SetAnchor {
603 cursor_id,
604 position,
605 } => {
606 if let Some(cursor) = cursors.get_mut(*cursor_id) {
609 cursor.anchor = Some(*position);
610 cursor.deselect_on_move = false;
611 }
612 }
613
614 Event::CancelAnchor { cursor_id } => {
615 if let Some(cursor) = cursors.get_mut(*cursor_id) {
618 cursor.deselect_on_move = true;
619 }
620 }
621
622 Event::ClearAnchor { cursor_id } => {
623 if let Some(cursor) = cursors.get_mut(*cursor_id) {
626 cursor.anchor = None;
627 cursor.deselect_on_move = true;
628 cursor.clear_block_selection();
629 }
630 }
631
632 Event::ChangeMode { mode } => {
633 self.mode = mode.clone();
634 }
635
636 Event::AddOverlay {
637 namespace,
638 range,
639 face,
640 priority,
641 message,
642 extend_to_line_end,
643 url,
644 } => {
645 tracing::trace!(
646 "AddOverlay: namespace={:?}, range={:?}, face={:?}, priority={}",
647 namespace,
648 range,
649 face,
650 priority
651 );
652 let overlay_face = convert_event_face_to_overlay_face(face);
654 tracing::trace!("Converted face: {:?}", overlay_face);
655
656 let mut overlay = Overlay::with_priority(
657 &mut self.marker_list,
658 range.clone(),
659 overlay_face,
660 *priority,
661 );
662 overlay.namespace = namespace.clone();
663 overlay.message = message.clone();
664 overlay.extend_to_line_end = *extend_to_line_end;
665 overlay.url = url.clone();
666
667 let actual_range = overlay.range(&self.marker_list);
668 tracing::trace!(
669 "Created overlay with markers - actual range: {:?}, handle={:?}",
670 actual_range,
671 overlay.handle
672 );
673
674 self.overlays.add(overlay);
675 }
676
677 Event::RemoveOverlay { handle } => {
678 tracing::trace!("RemoveOverlay: handle={:?}", handle);
679 self.overlays
680 .remove_by_handle(handle, &mut self.marker_list);
681 }
682
683 Event::RemoveOverlaysInRange { range } => {
684 self.overlays.remove_in_range(range, &mut self.marker_list);
685 }
686
687 Event::ClearNamespace { namespace } => {
688 tracing::trace!("ClearNamespace: namespace={:?}", namespace);
689 self.overlays
690 .clear_namespace(namespace, &mut self.marker_list);
691 }
692
693 Event::ClearOverlays => {
694 self.overlays.clear(&mut self.marker_list);
695 }
696
697 Event::ShowPopup { popup } => {
698 use crate::view::theme::{default_popup_bg, default_popup_border_fg};
706 let popup_obj = convert_popup_data_to_popup(
707 popup,
708 default_popup_bg().into(),
709 default_popup_border_fg().into(),
710 );
711 self.popups.show_or_replace(popup_obj);
712 }
713
714 Event::HidePopup => {
715 self.popups.hide();
716 }
717
718 Event::ClearPopups => {
719 self.popups.clear();
720 }
721
722 Event::PopupSelectNext => {
723 if let Some(popup) = self.popups.top_mut() {
724 popup.select_next();
725 }
726 }
727
728 Event::PopupSelectPrev => {
729 if let Some(popup) = self.popups.top_mut() {
730 popup.select_prev();
731 }
732 }
733
734 Event::PopupPageDown => {
735 if let Some(popup) = self.popups.top_mut() {
736 popup.page_down();
737 }
738 }
739
740 Event::PopupPageUp => {
741 if let Some(popup) = self.popups.top_mut() {
742 popup.page_up();
743 }
744 }
745
746 Event::AddMarginAnnotation {
747 line,
748 position,
749 content,
750 annotation_id,
751 } => {
752 let margin_position = convert_margin_position(position);
753 let margin_content = convert_margin_content(content);
754 let annotation = if let Some(id) = annotation_id {
755 MarginAnnotation::with_id(*line, margin_position, margin_content, id.clone())
756 } else {
757 MarginAnnotation::new(*line, margin_position, margin_content)
758 };
759 self.margins.add_annotation(annotation);
760 }
761
762 Event::RemoveMarginAnnotation { annotation_id } => {
763 self.margins.remove_by_id(annotation_id);
764 }
765
766 Event::RemoveMarginAnnotationsAtLine { line, position } => {
767 let margin_position = convert_margin_position(position);
768 self.margins.remove_at_line(*line, margin_position);
769 }
770
771 Event::ClearMarginPosition { position } => {
772 let margin_position = convert_margin_position(position);
773 self.margins.clear_position(margin_position);
774 }
775
776 Event::ClearMargins => {
777 self.margins.clear_all();
778 }
779
780 Event::SetLineNumbers { enabled } => {
781 self.margins.configure_for_line_numbers(*enabled);
782 }
783
784 Event::SplitPane { .. }
787 | Event::CloseSplit { .. }
788 | Event::SetActiveSplit { .. }
789 | Event::AdjustSplitRatio { .. }
790 | Event::NextSplit
791 | Event::PrevSplit => {
792 }
794
795 Event::Batch { events, .. } => {
796 for event in events {
799 self.apply(cursors, event);
800 }
801 }
802
803 Event::BulkEdit {
804 new_snapshot,
805 new_cursors,
806 edits,
807 displaced_markers,
808 ..
809 } => {
810 if let Some(snapshot) = new_snapshot {
816 self.buffer.restore_buffer_state(snapshot);
817 }
818
819 for &(pos, del_len, ins_len) in edits {
829 if del_len > 0 && ins_len > 0 {
830 if ins_len > del_len {
832 let net = ins_len - del_len;
833 self.marker_list.adjust_for_insert(pos, net);
834 self.margins.adjust_for_insert(pos, net);
835 } else if del_len > ins_len {
836 let net = del_len - ins_len;
837 self.marker_list.adjust_for_delete(pos, net);
838 self.margins.adjust_for_delete(pos, net);
839 }
840 } else if del_len > 0 {
842 self.marker_list.adjust_for_delete(pos, del_len);
843 self.margins.adjust_for_delete(pos, del_len);
844 } else if ins_len > 0 {
845 self.marker_list.adjust_for_insert(pos, ins_len);
846 self.margins.adjust_for_insert(pos, ins_len);
847 }
848 }
849
850 if !displaced_markers.is_empty() {
855 self.restore_displaced_markers(displaced_markers);
856 }
857
858 self.virtual_texts.clear(&mut self.marker_list);
861
862 use crate::view::overlay::OverlayNamespace;
863 let namespaces = ["lsp-diagnostic", "reference-highlight", "bracket-highlight"];
864 for ns in &namespaces {
865 self.overlays.clear_namespace(
866 &OverlayNamespace::from_string(ns.to_string()),
867 &mut self.marker_list,
868 );
869 }
870
871 for (cursor_id, position, anchor) in new_cursors {
873 if let Some(cursor) = cursors.get_mut(*cursor_id) {
874 cursor.position = *position;
875 cursor.anchor = *anchor;
876 }
877 }
878
879 self.highlighter.invalidate_all();
881
882 let primary_pos = cursors.primary().position;
884 self.primary_cursor_line_number = match self.buffer.offset_to_position(primary_pos)
885 {
886 Some(pos) => crate::model::buffer::LineNumber::Absolute(pos.line),
887 None => crate::model::buffer::LineNumber::Absolute(0),
888 };
889 }
890 }
891 }
892
893 pub fn capture_displaced_markers(&self, range: &Range<usize>) -> Vec<(u64, usize)> {
896 let mut displaced = Vec::new();
897 if range.is_empty() {
898 return displaced;
899 }
900 for (marker_id, start, _end) in self.marker_list.query_range(range.start, range.end) {
901 if start > range.start && start < range.end {
902 displaced.push(
903 DisplacedMarker::Main {
904 id: marker_id.0,
905 position: start,
906 }
907 .encode(),
908 );
909 }
910 }
911 for (marker_id, start, _end) in self.margins.query_indicator_range(range.start, range.end) {
912 if start > range.start && start < range.end {
913 displaced.push(
914 DisplacedMarker::Margin {
915 id: marker_id.0,
916 position: start,
917 }
918 .encode(),
919 );
920 }
921 }
922 displaced
923 }
924
925 pub fn capture_displaced_markers_bulk(
927 &self,
928 edits: &[(usize, usize, String)],
929 ) -> Vec<(u64, usize)> {
930 let mut displaced = Vec::new();
931 for (pos, del_len, _text) in edits {
932 if *del_len > 0 {
933 displaced.extend(self.capture_displaced_markers(&(*pos..*pos + *del_len)));
934 }
935 }
936 displaced
937 }
938
939 pub fn restore_displaced_markers(&mut self, displaced: &[(u64, usize)]) {
941 for &(tagged_id, original_pos) in displaced {
942 let dm = DisplacedMarker::decode(tagged_id, original_pos);
943 match dm {
944 DisplacedMarker::Main { id, position } => {
945 self.marker_list.set_position(MarkerId(id), position);
946 }
947 DisplacedMarker::Margin { id, position } => {
948 self.margins.set_indicator_position(MarkerId(id), position);
949 }
950 }
951 }
952 }
953
954 pub fn apply_many(&mut self, cursors: &mut Cursors, events: &[Event]) {
956 for event in events {
957 self.apply(cursors, event);
958 }
959 }
960
961 pub fn on_focus_lost(&mut self) {
965 if self.popups.dismiss_transient() {
966 tracing::debug!("Dismissed transient popup on buffer focus loss");
967 }
968 }
969}
970
971fn convert_event_face_to_overlay_face(event_face: &EventOverlayFace) -> OverlayFace {
973 match event_face {
974 EventOverlayFace::Underline { color, style } => {
975 let underline_style = match style {
976 crate::model::event::UnderlineStyle::Straight => UnderlineStyle::Straight,
977 crate::model::event::UnderlineStyle::Wavy => UnderlineStyle::Wavy,
978 crate::model::event::UnderlineStyle::Dotted => UnderlineStyle::Dotted,
979 crate::model::event::UnderlineStyle::Dashed => UnderlineStyle::Dashed,
980 };
981 OverlayFace::Underline {
982 color: Color::Rgb(color.0, color.1, color.2),
983 style: underline_style,
984 }
985 }
986 EventOverlayFace::Background { color } => OverlayFace::Background {
987 color: Color::Rgb(color.0, color.1, color.2),
988 },
989 EventOverlayFace::Foreground { color } => OverlayFace::Foreground {
990 color: Color::Rgb(color.0, color.1, color.2),
991 },
992 EventOverlayFace::Style { options } => {
993 use crate::view::theme::named_color_from_str;
994 use ratatui::style::Modifier;
995
996 let mut style = Style::default();
998
999 if let Some(ref fg) = options.fg {
1001 if let Some((r, g, b)) = fg.as_rgb() {
1002 style = style.fg(Color::Rgb(r, g, b));
1003 } else if let Some(key) = fg.as_theme_key() {
1004 if let Some(color) = named_color_from_str(key) {
1005 style = style.fg(color);
1006 }
1007 }
1008 }
1009
1010 if let Some(ref bg) = options.bg {
1012 if let Some((r, g, b)) = bg.as_rgb() {
1013 style = style.bg(Color::Rgb(r, g, b));
1014 } else if let Some(key) = bg.as_theme_key() {
1015 if let Some(color) = named_color_from_str(key) {
1016 style = style.bg(color);
1017 }
1018 }
1019 }
1020
1021 let mut modifiers = Modifier::empty();
1023 if options.bold {
1024 modifiers |= Modifier::BOLD;
1025 }
1026 if options.italic {
1027 modifiers |= Modifier::ITALIC;
1028 }
1029 if options.underline {
1030 modifiers |= Modifier::UNDERLINED;
1031 }
1032 if options.strikethrough {
1033 modifiers |= Modifier::CROSSED_OUT;
1034 }
1035 if !modifiers.is_empty() {
1036 style = style.add_modifier(modifiers);
1037 }
1038
1039 let fg_theme = options
1041 .fg
1042 .as_ref()
1043 .and_then(|c| c.as_theme_key())
1044 .filter(|key| named_color_from_str(key).is_none())
1045 .map(String::from);
1046 let bg_theme = options
1047 .bg
1048 .as_ref()
1049 .and_then(|c| c.as_theme_key())
1050 .filter(|key| named_color_from_str(key).is_none())
1051 .map(String::from);
1052
1053 if fg_theme.is_some() || bg_theme.is_some() {
1055 OverlayFace::ThemedStyle {
1056 fallback_style: style,
1057 fg_theme,
1058 bg_theme,
1059 fg_on_collision_only: options.fg_on_collision_only,
1060 }
1061 } else {
1062 OverlayFace::Style { style }
1063 }
1064 }
1065 }
1066}
1067
1068pub(crate) fn convert_popup_data_to_popup(
1075 data: &PopupData,
1076 popup_bg: Color,
1077 popup_border_fg: Color,
1078) -> Popup {
1079 let content = match &data.content {
1080 crate::model::event::PopupContentData::Text(lines) => PopupContent::Text(lines.clone()),
1081 crate::model::event::PopupContentData::List { items, selected } => PopupContent::List {
1082 items: items
1083 .iter()
1084 .map(|item| PopupListItem {
1085 text: item.text.clone(),
1086 detail: item.detail.clone(),
1087 icon: item.icon.clone(),
1088 data: item.data.clone(),
1089 disabled: false,
1090 })
1091 .collect(),
1092 selected: *selected,
1093 },
1094 };
1095
1096 let position = match data.position {
1097 PopupPositionData::AtCursor => PopupPosition::AtCursor,
1098 PopupPositionData::BelowCursor => PopupPosition::BelowCursor,
1099 PopupPositionData::AboveCursor => PopupPosition::AboveCursor,
1100 PopupPositionData::Fixed { x, y } => PopupPosition::Fixed { x, y },
1101 PopupPositionData::Centered => PopupPosition::Centered,
1102 PopupPositionData::BottomRight => PopupPosition::BottomRight,
1103 PopupPositionData::AboveStatusBarAt { x, status_row } => {
1104 PopupPosition::AboveStatusBarAt { x, status_row }
1105 }
1106 };
1107
1108 let kind = match data.kind {
1110 crate::model::event::PopupKindHint::Completion => PopupKind::Completion,
1111 crate::model::event::PopupKindHint::List => PopupKind::List,
1112 crate::model::event::PopupKindHint::Text => PopupKind::Text,
1113 };
1114
1115 let resolver = match kind {
1122 PopupKind::Completion => crate::view::popup::PopupResolver::Completion,
1123 _ => crate::view::popup::PopupResolver::None,
1124 };
1125
1126 let focused = match kind {
1138 PopupKind::Completion => true,
1142 PopupKind::List => true,
1145 PopupKind::Text => false,
1148 PopupKind::Hover | PopupKind::Action => false,
1152 };
1153
1154 Popup {
1155 kind,
1156 title: data.title.clone(),
1157 description: data.description.clone(),
1158 transient: data.transient,
1159 content,
1160 position,
1161 width: data.width,
1162 max_height: data.max_height,
1163 bordered: data.bordered,
1164 border_style: Style::default().fg(popup_border_fg),
1165 background_style: Style::default().bg(popup_bg),
1166 scroll_offset: 0,
1167 text_selection: None,
1168 accept_key_hint: None,
1169 resolver,
1170 focused,
1171 focus_key_hint: None,
1172 }
1173}
1174
1175fn convert_margin_position(position: &MarginPositionData) -> MarginPosition {
1177 match position {
1178 MarginPositionData::Left => MarginPosition::Left,
1179 MarginPositionData::Right => MarginPosition::Right,
1180 }
1181}
1182
1183fn convert_margin_content(content: &MarginContentData) -> MarginContent {
1185 match content {
1186 MarginContentData::Text(text) => MarginContent::Text(text.clone()),
1187 MarginContentData::Symbol { text, color } => {
1188 if let Some((r, g, b)) = color {
1189 MarginContent::colored_symbol(text.clone(), Color::Rgb(*r, *g, *b))
1190 } else {
1191 MarginContent::symbol(text.clone(), Style::default())
1192 }
1193 }
1194 MarginContentData::Empty => MarginContent::Empty,
1195 }
1196}
1197
1198impl EditorState {
1199 pub fn prepare_for_render(&mut self, top_byte: usize, height: u16) -> Result<()> {
1206 self.buffer.prepare_viewport(top_byte, height as usize)?;
1207 Ok(())
1208 }
1209
1210 pub fn collect_virtual_line_positions(&self) -> Vec<usize> {
1222 if self.virtual_texts.is_empty() {
1223 return Vec::new();
1224 }
1225 let mut v: Vec<usize> = self
1226 .virtual_texts
1227 .query_lines_in_range(&self.marker_list, 0, self.buffer.len() + 1)
1228 .into_iter()
1229 .map(|(pos, _vt)| pos)
1230 .collect();
1231 v.sort_unstable();
1232 v
1233 }
1234
1235 pub fn collect_soft_break_positions(&self) -> Vec<(usize, u16)> {
1248 if self.soft_breaks.is_empty() {
1249 return Vec::new();
1250 }
1251 self.soft_breaks
1253 .query_viewport(0, self.buffer.len() + 1, &self.marker_list)
1254 }
1255
1256 pub fn get_text_range(&mut self, start: usize, end: usize) -> String {
1276 match self
1278 .buffer
1279 .get_text_range_mut(start, end.saturating_sub(start))
1280 {
1281 Ok(bytes) => String::from_utf8_lossy(&bytes).into_owned(),
1282 Err(e) => {
1283 tracing::warn!("Failed to get text range {}..{}: {}", start, end, e);
1284 String::new()
1285 }
1286 }
1287 }
1288
1289 pub fn get_line_at_offset(&mut self, offset: usize) -> Option<(usize, String)> {
1297 use crate::model::document_model::DocumentModel;
1298
1299 let mut line_start = offset;
1302 while line_start > 0 {
1303 if let Ok(text) = self.buffer.get_text_range_mut(line_start - 1, 1) {
1304 if text.first() == Some(&b'\n') {
1305 break;
1306 }
1307 line_start -= 1;
1308 } else {
1309 break;
1310 }
1311 }
1312
1313 let viewport = self
1315 .get_viewport_content(
1316 crate::model::document_model::DocumentPosition::byte(line_start),
1317 1,
1318 )
1319 .ok()?;
1320
1321 viewport
1322 .lines
1323 .first()
1324 .map(|line| (line.byte_offset, line.content.clone()))
1325 }
1326
1327 pub fn get_text_to_end_of_line(&mut self, cursor_pos: usize) -> Result<String> {
1332 use crate::model::document_model::DocumentModel;
1333
1334 let viewport = self.get_viewport_content(
1336 crate::model::document_model::DocumentPosition::byte(cursor_pos),
1337 1,
1338 )?;
1339
1340 if let Some(line) = viewport.lines.first() {
1341 let line_start = line.byte_offset;
1342 let line_end = line_start + line.content.len();
1343
1344 if cursor_pos >= line_start && cursor_pos <= line_end {
1345 let offset_in_line = cursor_pos - line_start;
1346 Ok(line.content.get(offset_in_line..).unwrap_or("").to_string())
1348 } else {
1349 Ok(String::new())
1350 }
1351 } else {
1352 Ok(String::new())
1353 }
1354 }
1355
1356 pub fn set_semantic_tokens(&mut self, store: SemanticTokenStore) {
1358 self.semantic_tokens = Some(store);
1359 }
1360
1361 pub fn clear_semantic_tokens(&mut self) {
1363 self.semantic_tokens = None;
1364 }
1365
1366 pub fn semantic_tokens_result_id(&self) -> Option<&str> {
1368 self.semantic_tokens
1369 .as_ref()
1370 .and_then(|store| store.result_id.as_deref())
1371 }
1372}
1373
1374impl DocumentModel for EditorState {
1379 fn capabilities(&self) -> DocumentCapabilities {
1380 let line_count = self.buffer.line_count();
1381 DocumentCapabilities {
1382 has_line_index: line_count.is_some(),
1383 uses_lazy_loading: false, byte_length: self.buffer.len(),
1385 approximate_line_count: line_count.unwrap_or_else(|| {
1386 self.buffer.len() / 80
1388 }),
1389 }
1390 }
1391
1392 fn get_viewport_content(
1393 &mut self,
1394 start_pos: DocumentPosition,
1395 max_lines: usize,
1396 ) -> Result<ViewportContent> {
1397 let start_offset = self.position_to_offset(start_pos)?;
1399
1400 let line_iter = self.buffer.iter_lines_from(start_offset, max_lines)?;
1403 let has_more = line_iter.has_more;
1404
1405 let lines = line_iter
1406 .map(|line_data| ViewportLine {
1407 byte_offset: line_data.byte_offset,
1408 content: line_data.content,
1409 has_newline: line_data.has_newline,
1410 approximate_line_number: line_data.line_number,
1411 })
1412 .collect();
1413
1414 Ok(ViewportContent {
1415 start_position: DocumentPosition::ByteOffset(start_offset),
1416 lines,
1417 has_more,
1418 })
1419 }
1420
1421 fn position_to_offset(&self, pos: DocumentPosition) -> Result<usize> {
1422 match pos {
1423 DocumentPosition::ByteOffset(offset) => Ok(offset),
1424 DocumentPosition::LineColumn { line, column } => {
1425 if !self.has_line_index() {
1426 anyhow::bail!("Line indexing not available for this document");
1427 }
1428 let position = crate::model::piece_tree::Position { line, column };
1430 Ok(self.buffer.position_to_offset(position))
1431 }
1432 }
1433 }
1434
1435 fn offset_to_position(&self, offset: usize) -> DocumentPosition {
1436 if self.has_line_index() {
1437 if let Some(pos) = self.buffer.offset_to_position(offset) {
1438 DocumentPosition::LineColumn {
1439 line: pos.line,
1440 column: pos.column,
1441 }
1442 } else {
1443 DocumentPosition::ByteOffset(offset)
1445 }
1446 } else {
1447 DocumentPosition::ByteOffset(offset)
1448 }
1449 }
1450
1451 fn get_range(&mut self, start: DocumentPosition, end: DocumentPosition) -> Result<String> {
1452 let start_offset = self.position_to_offset(start)?;
1453 let end_offset = self.position_to_offset(end)?;
1454
1455 if start_offset > end_offset {
1456 anyhow::bail!(
1457 "Invalid range: start offset {} > end offset {}",
1458 start_offset,
1459 end_offset
1460 );
1461 }
1462
1463 let bytes = self
1464 .buffer
1465 .get_text_range_mut(start_offset, end_offset - start_offset)?;
1466
1467 Ok(String::from_utf8_lossy(&bytes).into_owned())
1468 }
1469
1470 fn get_line_content(&mut self, line_number: usize) -> Option<String> {
1471 if !self.has_line_index() {
1472 return None;
1473 }
1474
1475 let line_start_offset = self.buffer.line_start_offset(line_number)?;
1477
1478 let mut iter = self.buffer.line_iterator(line_start_offset, 80);
1480 if let Some((_start, content)) = iter.next_line() {
1481 let has_newline = content.ends_with('\n');
1482 let line_content = if has_newline {
1483 content[..content.len() - 1].to_string()
1484 } else {
1485 content
1486 };
1487 Some(line_content)
1488 } else {
1489 None
1490 }
1491 }
1492
1493 fn get_chunk_at_offset(&mut self, offset: usize, size: usize) -> Result<(usize, String)> {
1494 let bytes = self.buffer.get_text_range_mut(offset, size)?;
1495
1496 Ok((offset, String::from_utf8_lossy(&bytes).into_owned()))
1497 }
1498
1499 fn insert(&mut self, pos: DocumentPosition, text: &str) -> Result<usize> {
1500 let offset = self.position_to_offset(pos)?;
1501 self.buffer.insert_bytes(offset, text.as_bytes().to_vec());
1502 Ok(text.len())
1503 }
1504
1505 fn delete(&mut self, start: DocumentPosition, end: DocumentPosition) -> Result<()> {
1506 let start_offset = self.position_to_offset(start)?;
1507 let end_offset = self.position_to_offset(end)?;
1508
1509 if start_offset > end_offset {
1510 anyhow::bail!(
1511 "Invalid range: start offset {} > end offset {}",
1512 start_offset,
1513 end_offset
1514 );
1515 }
1516
1517 self.buffer.delete(start_offset..end_offset);
1518 Ok(())
1519 }
1520
1521 fn replace(
1522 &mut self,
1523 start: DocumentPosition,
1524 end: DocumentPosition,
1525 text: &str,
1526 ) -> Result<()> {
1527 self.delete(start, end)?;
1529 self.insert(start, text)?;
1530 Ok(())
1531 }
1532
1533 fn find_matches(
1534 &mut self,
1535 pattern: &str,
1536 search_range: Option<(DocumentPosition, DocumentPosition)>,
1537 ) -> Result<Vec<usize>> {
1538 let (start_offset, end_offset) = if let Some((start, end)) = search_range {
1539 (
1540 self.position_to_offset(start)?,
1541 self.position_to_offset(end)?,
1542 )
1543 } else {
1544 (0, self.buffer.len())
1545 };
1546
1547 let bytes = self
1549 .buffer
1550 .get_text_range_mut(start_offset, end_offset - start_offset)?;
1551 let text = String::from_utf8_lossy(&bytes);
1552
1553 let mut matches = Vec::new();
1555 let mut search_offset = 0;
1556 while let Some(pos) = text[search_offset..].find(pattern) {
1557 matches.push(start_offset + search_offset + pos);
1558 search_offset += pos + pattern.len();
1559 }
1560
1561 Ok(matches)
1562 }
1563}
1564
1565#[derive(Clone, Debug)]
1567pub struct SemanticTokenStore {
1568 pub version: u64,
1570 pub result_id: Option<String>,
1572 pub data: Vec<u32>,
1574 pub tokens: Vec<SemanticTokenSpan>,
1576}
1577
1578#[derive(Clone, Debug)]
1580pub struct SemanticTokenSpan {
1581 pub range: Range<usize>,
1582 pub token_type: String,
1583 pub modifiers: Vec<String>,
1584}
1585
1586#[cfg(test)]
1587mod tests {
1588 use crate::model::filesystem::StdFileSystem;
1589 use std::sync::Arc;
1590
1591 fn test_fs() -> Arc<dyn crate::model::filesystem::FileSystem + Send + Sync> {
1592 Arc::new(StdFileSystem)
1593 }
1594 use super::*;
1595 use crate::model::event::CursorId;
1596
1597 #[test]
1598 fn test_state_new() {
1599 let state = EditorState::new(
1600 80,
1601 24,
1602 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1603 test_fs(),
1604 );
1605 assert!(state.buffer.is_empty());
1606 }
1607
1608 #[test]
1609 fn test_apply_insert() {
1610 let mut state = EditorState::new(
1611 80,
1612 24,
1613 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1614 test_fs(),
1615 );
1616 let mut cursors = Cursors::new();
1617 let cursor_id = cursors.primary_id();
1618
1619 state.apply(
1620 &mut cursors,
1621 &Event::Insert {
1622 position: 0,
1623 text: "hello".to_string(),
1624 cursor_id,
1625 },
1626 );
1627
1628 assert_eq!(state.buffer.to_string().unwrap(), "hello");
1629 assert_eq!(cursors.primary().position, 5);
1630 assert!(state.buffer.is_modified());
1631 }
1632
1633 #[test]
1634 fn test_apply_delete() {
1635 let mut state = EditorState::new(
1636 80,
1637 24,
1638 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1639 test_fs(),
1640 );
1641 let mut cursors = Cursors::new();
1642 let cursor_id = cursors.primary_id();
1643
1644 state.apply(
1646 &mut cursors,
1647 &Event::Insert {
1648 position: 0,
1649 text: "hello world".to_string(),
1650 cursor_id,
1651 },
1652 );
1653
1654 state.apply(
1655 &mut cursors,
1656 &Event::Delete {
1657 range: 5..11,
1658 deleted_text: " world".to_string(),
1659 cursor_id,
1660 },
1661 );
1662
1663 assert_eq!(state.buffer.to_string().unwrap(), "hello");
1664 assert_eq!(cursors.primary().position, 5);
1665 }
1666
1667 #[test]
1668 fn test_apply_move_cursor() {
1669 let mut state = EditorState::new(
1670 80,
1671 24,
1672 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1673 test_fs(),
1674 );
1675 let mut cursors = Cursors::new();
1676 let cursor_id = cursors.primary_id();
1677
1678 state.apply(
1679 &mut cursors,
1680 &Event::Insert {
1681 position: 0,
1682 text: "hello".to_string(),
1683 cursor_id,
1684 },
1685 );
1686
1687 state.apply(
1688 &mut cursors,
1689 &Event::MoveCursor {
1690 cursor_id,
1691 old_position: 5,
1692 new_position: 2,
1693 old_anchor: None,
1694 new_anchor: None,
1695 old_sticky_column: 0,
1696 new_sticky_column: 0,
1697 },
1698 );
1699
1700 assert_eq!(cursors.primary().position, 2);
1701 }
1702
1703 #[test]
1704 fn test_apply_add_cursor() {
1705 let mut state = EditorState::new(
1706 80,
1707 24,
1708 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1709 test_fs(),
1710 );
1711 let mut cursors = Cursors::new();
1712 let cursor_id = CursorId(1);
1713
1714 state.apply(
1715 &mut cursors,
1716 &Event::AddCursor {
1717 cursor_id,
1718 position: 5,
1719 anchor: None,
1720 },
1721 );
1722
1723 assert_eq!(cursors.count(), 2);
1724 }
1725
1726 #[test]
1727 fn test_apply_many() {
1728 let mut state = EditorState::new(
1729 80,
1730 24,
1731 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1732 test_fs(),
1733 );
1734 let mut cursors = Cursors::new();
1735 let cursor_id = cursors.primary_id();
1736
1737 let events = vec![
1738 Event::Insert {
1739 position: 0,
1740 text: "hello ".to_string(),
1741 cursor_id,
1742 },
1743 Event::Insert {
1744 position: 6,
1745 text: "world".to_string(),
1746 cursor_id,
1747 },
1748 ];
1749
1750 state.apply_many(&mut cursors, &events);
1751
1752 assert_eq!(state.buffer.to_string().unwrap(), "hello world");
1753 }
1754
1755 #[test]
1756 fn test_cursor_adjustment_after_insert() {
1757 let mut state = EditorState::new(
1758 80,
1759 24,
1760 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1761 test_fs(),
1762 );
1763 let mut cursors = Cursors::new();
1764 let cursor_id = cursors.primary_id();
1765
1766 state.apply(
1768 &mut cursors,
1769 &Event::AddCursor {
1770 cursor_id: CursorId(1),
1771 position: 5,
1772 anchor: None,
1773 },
1774 );
1775
1776 state.apply(
1778 &mut cursors,
1779 &Event::Insert {
1780 position: 0,
1781 text: "abc".to_string(),
1782 cursor_id,
1783 },
1784 );
1785
1786 if let Some(cursor) = cursors.get(CursorId(1)) {
1788 assert_eq!(cursor.position, 8);
1789 }
1790 }
1791
1792 mod document_model_tests {
1794 use super::*;
1795 use crate::model::document_model::{DocumentModel, DocumentPosition};
1796
1797 #[test]
1798 fn test_capabilities_small_file() {
1799 let mut state = EditorState::new(
1800 80,
1801 24,
1802 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1803 test_fs(),
1804 );
1805 state.buffer = Buffer::from_str_test("line1\nline2\nline3");
1806
1807 let caps = state.capabilities();
1808 assert!(caps.has_line_index, "Small file should have line index");
1809 assert_eq!(caps.byte_length, "line1\nline2\nline3".len());
1810 assert_eq!(caps.approximate_line_count, 3, "Should have 3 lines");
1811 }
1812
1813 #[test]
1814 fn test_position_conversions() {
1815 let mut state = EditorState::new(
1816 80,
1817 24,
1818 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1819 test_fs(),
1820 );
1821 state.buffer = Buffer::from_str_test("hello\nworld\ntest");
1822
1823 let pos1 = DocumentPosition::ByteOffset(6);
1825 let offset1 = state.position_to_offset(pos1).unwrap();
1826 assert_eq!(offset1, 6);
1827
1828 let pos2 = DocumentPosition::LineColumn { line: 1, column: 0 };
1830 let offset2 = state.position_to_offset(pos2).unwrap();
1831 assert_eq!(offset2, 6, "Line 1, column 0 should be at byte 6");
1832
1833 let converted = state.offset_to_position(6);
1835 match converted {
1836 DocumentPosition::LineColumn { line, column } => {
1837 assert_eq!(line, 1);
1838 assert_eq!(column, 0);
1839 }
1840 _ => panic!("Expected LineColumn for small file"),
1841 }
1842 }
1843
1844 #[test]
1845 fn test_get_viewport_content() {
1846 let mut state = EditorState::new(
1847 80,
1848 24,
1849 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1850 test_fs(),
1851 );
1852 state.buffer = Buffer::from_str_test("line1\nline2\nline3\nline4\nline5");
1853
1854 let content = state
1855 .get_viewport_content(DocumentPosition::ByteOffset(0), 3)
1856 .unwrap();
1857
1858 assert_eq!(content.lines.len(), 3);
1859 assert_eq!(content.lines[0].content, "line1");
1860 assert_eq!(content.lines[1].content, "line2");
1861 assert_eq!(content.lines[2].content, "line3");
1862 assert!(content.has_more);
1863 }
1864
1865 #[test]
1866 fn test_get_range() {
1867 let mut state = EditorState::new(
1868 80,
1869 24,
1870 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1871 test_fs(),
1872 );
1873 state.buffer = Buffer::from_str_test("hello world");
1874
1875 let text = state
1876 .get_range(
1877 DocumentPosition::ByteOffset(0),
1878 DocumentPosition::ByteOffset(5),
1879 )
1880 .unwrap();
1881 assert_eq!(text, "hello");
1882
1883 let text2 = state
1884 .get_range(
1885 DocumentPosition::ByteOffset(6),
1886 DocumentPosition::ByteOffset(11),
1887 )
1888 .unwrap();
1889 assert_eq!(text2, "world");
1890 }
1891
1892 #[test]
1893 fn test_get_line_content() {
1894 let mut state = EditorState::new(
1895 80,
1896 24,
1897 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1898 test_fs(),
1899 );
1900 state.buffer = Buffer::from_str_test("line1\nline2\nline3");
1901
1902 let line0 = state.get_line_content(0).unwrap();
1903 assert_eq!(line0, "line1");
1904
1905 let line1 = state.get_line_content(1).unwrap();
1906 assert_eq!(line1, "line2");
1907
1908 let line2 = state.get_line_content(2).unwrap();
1909 assert_eq!(line2, "line3");
1910 }
1911
1912 #[test]
1913 fn test_insert_delete() {
1914 let mut state = EditorState::new(
1915 80,
1916 24,
1917 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1918 test_fs(),
1919 );
1920 state.buffer = Buffer::from_str_test("hello world");
1921
1922 let bytes_inserted = state
1924 .insert(DocumentPosition::ByteOffset(6), "beautiful ")
1925 .unwrap();
1926 assert_eq!(bytes_inserted, 10);
1927 assert_eq!(state.buffer.to_string().unwrap(), "hello beautiful world");
1928
1929 state
1931 .delete(
1932 DocumentPosition::ByteOffset(6),
1933 DocumentPosition::ByteOffset(16),
1934 )
1935 .unwrap();
1936 assert_eq!(state.buffer.to_string().unwrap(), "hello world");
1937 }
1938
1939 #[test]
1940 fn test_replace() {
1941 let mut state = EditorState::new(
1942 80,
1943 24,
1944 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1945 test_fs(),
1946 );
1947 state.buffer = Buffer::from_str_test("hello world");
1948
1949 state
1950 .replace(
1951 DocumentPosition::ByteOffset(0),
1952 DocumentPosition::ByteOffset(5),
1953 "hi",
1954 )
1955 .unwrap();
1956 assert_eq!(state.buffer.to_string().unwrap(), "hi world");
1957 }
1958
1959 #[test]
1960 fn test_find_matches() {
1961 let mut state = EditorState::new(
1962 80,
1963 24,
1964 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1965 test_fs(),
1966 );
1967 state.buffer = Buffer::from_str_test("hello world hello");
1968
1969 let matches = state.find_matches("hello", None).unwrap();
1970 assert_eq!(matches.len(), 2);
1971 assert_eq!(matches[0], 0);
1972 assert_eq!(matches[1], 12);
1973 }
1974
1975 #[test]
1976 fn test_prepare_for_render() {
1977 let mut state = EditorState::new(
1978 80,
1979 24,
1980 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1981 test_fs(),
1982 );
1983 state.buffer = Buffer::from_str_test("line1\nline2\nline3\nline4\nline5");
1984
1985 state.prepare_for_render(0, 24).unwrap();
1987 }
1988
1989 #[test]
1990 fn test_helper_get_text_range() {
1991 let mut state = EditorState::new(
1992 80,
1993 24,
1994 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1995 test_fs(),
1996 );
1997 state.buffer = Buffer::from_str_test("hello world");
1998
1999 let text = state.get_text_range(0, 5);
2001 assert_eq!(text, "hello");
2002
2003 let text2 = state.get_text_range(6, 11);
2005 assert_eq!(text2, "world");
2006 }
2007
2008 #[test]
2009 fn test_helper_get_line_at_offset() {
2010 let mut state = EditorState::new(
2011 80,
2012 24,
2013 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
2014 test_fs(),
2015 );
2016 state.buffer = Buffer::from_str_test("line1\nline2\nline3");
2017
2018 let (offset, content) = state.get_line_at_offset(0).unwrap();
2020 assert_eq!(offset, 0);
2021 assert_eq!(content, "line1");
2022
2023 let (offset2, content2) = state.get_line_at_offset(8).unwrap();
2025 assert_eq!(offset2, 6); assert_eq!(content2, "line2");
2027
2028 let (offset3, content3) = state.get_line_at_offset(12).unwrap();
2030 assert_eq!(offset3, 12);
2031 assert_eq!(content3, "line3");
2032 }
2033
2034 #[test]
2035 fn test_helper_get_text_to_end_of_line() {
2036 let mut state = EditorState::new(
2037 80,
2038 24,
2039 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
2040 test_fs(),
2041 );
2042 state.buffer = Buffer::from_str_test("hello world\nline2");
2043
2044 let text = state.get_text_to_end_of_line(0).unwrap();
2046 assert_eq!(text, "hello world");
2047
2048 let text2 = state.get_text_to_end_of_line(6).unwrap();
2050 assert_eq!(text2, "world");
2051
2052 let text3 = state.get_text_to_end_of_line(11).unwrap();
2054 assert_eq!(text3, "");
2055
2056 let text4 = state.get_text_to_end_of_line(12).unwrap();
2058 assert_eq!(text4, "line2");
2059 }
2060 }
2061
2062 mod virtual_text_integration_tests {
2064 use super::*;
2065 use crate::view::virtual_text::VirtualTextPosition;
2066 use ratatui::style::Style;
2067
2068 #[test]
2069 fn test_virtual_text_add_and_query() {
2070 let mut state = EditorState::new(
2071 80,
2072 24,
2073 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
2074 test_fs(),
2075 );
2076 state.buffer = Buffer::from_str_test("hello world");
2077
2078 if !state.buffer.is_empty() {
2080 state.marker_list.adjust_for_insert(0, state.buffer.len());
2081 }
2082
2083 let vtext_id = state.virtual_texts.add(
2085 &mut state.marker_list,
2086 5,
2087 ": string".to_string(),
2088 Style::default(),
2089 VirtualTextPosition::AfterChar,
2090 0,
2091 );
2092
2093 let results = state.virtual_texts.query_range(&state.marker_list, 0, 11);
2095 assert_eq!(results.len(), 1);
2096 assert_eq!(results[0].0, 5); assert_eq!(results[0].1.text, ": string");
2098
2099 let lookup = state.virtual_texts.build_lookup(&state.marker_list, 0, 11);
2101 assert!(lookup.contains_key(&5));
2102 assert_eq!(lookup[&5].len(), 1);
2103 assert_eq!(lookup[&5][0].text, ": string");
2104
2105 state.virtual_texts.remove(&mut state.marker_list, vtext_id);
2107 assert!(state.virtual_texts.is_empty());
2108 }
2109
2110 #[test]
2111 fn test_virtual_text_position_tracking_on_insert() {
2112 let mut state = EditorState::new(
2113 80,
2114 24,
2115 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
2116 test_fs(),
2117 );
2118 state.buffer = Buffer::from_str_test("hello world");
2119
2120 if !state.buffer.is_empty() {
2122 state.marker_list.adjust_for_insert(0, state.buffer.len());
2123 }
2124
2125 let _vtext_id = state.virtual_texts.add(
2127 &mut state.marker_list,
2128 6,
2129 "/*param*/".to_string(),
2130 Style::default(),
2131 VirtualTextPosition::BeforeChar,
2132 0,
2133 );
2134
2135 let mut cursors = Cursors::new();
2137 let cursor_id = cursors.primary_id();
2138 state.apply(
2139 &mut cursors,
2140 &Event::Insert {
2141 position: 6,
2142 text: "beautiful ".to_string(),
2143 cursor_id,
2144 },
2145 );
2146
2147 let results = state.virtual_texts.query_range(&state.marker_list, 0, 30);
2149 assert_eq!(results.len(), 1);
2150 assert_eq!(results[0].0, 16); assert_eq!(results[0].1.text, "/*param*/");
2152 }
2153
2154 #[test]
2155 fn test_virtual_text_position_tracking_on_delete() {
2156 let mut state = EditorState::new(
2157 80,
2158 24,
2159 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
2160 test_fs(),
2161 );
2162 state.buffer = Buffer::from_str_test("hello beautiful world");
2163
2164 if !state.buffer.is_empty() {
2166 state.marker_list.adjust_for_insert(0, state.buffer.len());
2167 }
2168
2169 let _vtext_id = state.virtual_texts.add(
2171 &mut state.marker_list,
2172 16,
2173 ": string".to_string(),
2174 Style::default(),
2175 VirtualTextPosition::AfterChar,
2176 0,
2177 );
2178
2179 let mut cursors = Cursors::new();
2181 let cursor_id = cursors.primary_id();
2182 state.apply(
2183 &mut cursors,
2184 &Event::Delete {
2185 range: 6..16,
2186 deleted_text: "beautiful ".to_string(),
2187 cursor_id,
2188 },
2189 );
2190
2191 let results = state.virtual_texts.query_range(&state.marker_list, 0, 20);
2193 assert_eq!(results.len(), 1);
2194 assert_eq!(results[0].0, 6); assert_eq!(results[0].1.text, ": string");
2196 }
2197
2198 #[test]
2199 fn test_multiple_virtual_texts_with_priorities() {
2200 let mut state = EditorState::new(
2201 80,
2202 24,
2203 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
2204 test_fs(),
2205 );
2206 state.buffer = Buffer::from_str_test("let x = 5");
2207
2208 if !state.buffer.is_empty() {
2210 state.marker_list.adjust_for_insert(0, state.buffer.len());
2211 }
2212
2213 state.virtual_texts.add(
2215 &mut state.marker_list,
2216 5,
2217 ": i32".to_string(),
2218 Style::default(),
2219 VirtualTextPosition::AfterChar,
2220 0, );
2222
2223 state.virtual_texts.add(
2225 &mut state.marker_list,
2226 5,
2227 " /* inferred */".to_string(),
2228 Style::default(),
2229 VirtualTextPosition::AfterChar,
2230 10, );
2232
2233 let lookup = state.virtual_texts.build_lookup(&state.marker_list, 0, 10);
2235 assert!(lookup.contains_key(&5));
2236 let vtexts = &lookup[&5];
2237 assert_eq!(vtexts.len(), 2);
2238 assert_eq!(vtexts[0].text, ": i32");
2240 assert_eq!(vtexts[1].text, " /* inferred */");
2241 }
2242
2243 #[test]
2244 fn test_virtual_text_clear() {
2245 let mut state = EditorState::new(
2246 80,
2247 24,
2248 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
2249 test_fs(),
2250 );
2251 state.buffer = Buffer::from_str_test("test");
2252
2253 if !state.buffer.is_empty() {
2255 state.marker_list.adjust_for_insert(0, state.buffer.len());
2256 }
2257
2258 state.virtual_texts.add(
2260 &mut state.marker_list,
2261 0,
2262 "hint1".to_string(),
2263 Style::default(),
2264 VirtualTextPosition::BeforeChar,
2265 0,
2266 );
2267 state.virtual_texts.add(
2268 &mut state.marker_list,
2269 2,
2270 "hint2".to_string(),
2271 Style::default(),
2272 VirtualTextPosition::AfterChar,
2273 0,
2274 );
2275
2276 assert_eq!(state.virtual_texts.len(), 2);
2277
2278 state.virtual_texts.clear(&mut state.marker_list);
2280 assert!(state.virtual_texts.is_empty());
2281
2282 let results = state.virtual_texts.query_range(&state.marker_list, 0, 10);
2284 assert!(results.is_empty());
2285 }
2286 }
2287}