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::ClearAnchor { cursor_id } => {
615 if let Some(cursor) = cursors.get_mut(*cursor_id) {
618 cursor.anchor = None;
619 cursor.deselect_on_move = true;
620 cursor.clear_block_selection();
621 }
622 }
623
624 Event::ChangeMode { mode } => {
625 self.mode = mode.clone();
626 }
627
628 Event::AddOverlay {
629 namespace,
630 range,
631 face,
632 priority,
633 message,
634 extend_to_line_end,
635 url,
636 } => {
637 tracing::trace!(
638 "AddOverlay: namespace={:?}, range={:?}, face={:?}, priority={}",
639 namespace,
640 range,
641 face,
642 priority
643 );
644 let overlay_face = convert_event_face_to_overlay_face(face);
646 tracing::trace!("Converted face: {:?}", overlay_face);
647
648 let mut overlay = Overlay::with_priority(
649 &mut self.marker_list,
650 range.clone(),
651 overlay_face,
652 *priority,
653 );
654 overlay.namespace = namespace.clone();
655 overlay.message = message.clone();
656 overlay.extend_to_line_end = *extend_to_line_end;
657 overlay.url = url.clone();
658
659 let actual_range = overlay.range(&self.marker_list);
660 tracing::trace!(
661 "Created overlay with markers - actual range: {:?}, handle={:?}",
662 actual_range,
663 overlay.handle
664 );
665
666 self.overlays.add(overlay);
667 }
668
669 Event::RemoveOverlay { handle } => {
670 tracing::trace!("RemoveOverlay: handle={:?}", handle);
671 self.overlays
672 .remove_by_handle(handle, &mut self.marker_list);
673 }
674
675 Event::RemoveOverlaysInRange { range } => {
676 self.overlays.remove_in_range(range, &mut self.marker_list);
677 }
678
679 Event::ClearNamespace { namespace } => {
680 tracing::trace!("ClearNamespace: namespace={:?}", namespace);
681 self.overlays
682 .clear_namespace(namespace, &mut self.marker_list);
683 }
684
685 Event::ClearOverlays => {
686 self.overlays.clear(&mut self.marker_list);
687 }
688
689 Event::ShowPopup { popup } => {
690 let popup_obj = convert_popup_data_to_popup(popup);
691 self.popups.show_or_replace(popup_obj);
692 }
693
694 Event::HidePopup => {
695 self.popups.hide();
696 }
697
698 Event::ClearPopups => {
699 self.popups.clear();
700 }
701
702 Event::PopupSelectNext => {
703 if let Some(popup) = self.popups.top_mut() {
704 popup.select_next();
705 }
706 }
707
708 Event::PopupSelectPrev => {
709 if let Some(popup) = self.popups.top_mut() {
710 popup.select_prev();
711 }
712 }
713
714 Event::PopupPageDown => {
715 if let Some(popup) = self.popups.top_mut() {
716 popup.page_down();
717 }
718 }
719
720 Event::PopupPageUp => {
721 if let Some(popup) = self.popups.top_mut() {
722 popup.page_up();
723 }
724 }
725
726 Event::AddMarginAnnotation {
727 line,
728 position,
729 content,
730 annotation_id,
731 } => {
732 let margin_position = convert_margin_position(position);
733 let margin_content = convert_margin_content(content);
734 let annotation = if let Some(id) = annotation_id {
735 MarginAnnotation::with_id(*line, margin_position, margin_content, id.clone())
736 } else {
737 MarginAnnotation::new(*line, margin_position, margin_content)
738 };
739 self.margins.add_annotation(annotation);
740 }
741
742 Event::RemoveMarginAnnotation { annotation_id } => {
743 self.margins.remove_by_id(annotation_id);
744 }
745
746 Event::RemoveMarginAnnotationsAtLine { line, position } => {
747 let margin_position = convert_margin_position(position);
748 self.margins.remove_at_line(*line, margin_position);
749 }
750
751 Event::ClearMarginPosition { position } => {
752 let margin_position = convert_margin_position(position);
753 self.margins.clear_position(margin_position);
754 }
755
756 Event::ClearMargins => {
757 self.margins.clear_all();
758 }
759
760 Event::SetLineNumbers { enabled } => {
761 self.margins.configure_for_line_numbers(*enabled);
762 }
763
764 Event::SplitPane { .. }
767 | Event::CloseSplit { .. }
768 | Event::SetActiveSplit { .. }
769 | Event::AdjustSplitRatio { .. }
770 | Event::NextSplit
771 | Event::PrevSplit => {
772 }
774
775 Event::Batch { events, .. } => {
776 for event in events {
779 self.apply(cursors, event);
780 }
781 }
782
783 Event::BulkEdit {
784 new_snapshot,
785 new_cursors,
786 edits,
787 displaced_markers,
788 ..
789 } => {
790 if let Some(snapshot) = new_snapshot {
796 self.buffer.restore_buffer_state(snapshot);
797 }
798
799 for &(pos, del_len, ins_len) in edits {
809 if del_len > 0 && ins_len > 0 {
810 if ins_len > del_len {
812 let net = ins_len - del_len;
813 self.marker_list.adjust_for_insert(pos, net);
814 self.margins.adjust_for_insert(pos, net);
815 } else if del_len > ins_len {
816 let net = del_len - ins_len;
817 self.marker_list.adjust_for_delete(pos, net);
818 self.margins.adjust_for_delete(pos, net);
819 }
820 } else if del_len > 0 {
822 self.marker_list.adjust_for_delete(pos, del_len);
823 self.margins.adjust_for_delete(pos, del_len);
824 } else if ins_len > 0 {
825 self.marker_list.adjust_for_insert(pos, ins_len);
826 self.margins.adjust_for_insert(pos, ins_len);
827 }
828 }
829
830 if !displaced_markers.is_empty() {
835 self.restore_displaced_markers(displaced_markers);
836 }
837
838 self.virtual_texts.clear(&mut self.marker_list);
841
842 use crate::view::overlay::OverlayNamespace;
843 let namespaces = ["lsp-diagnostic", "reference-highlight", "bracket-highlight"];
844 for ns in &namespaces {
845 self.overlays.clear_namespace(
846 &OverlayNamespace::from_string(ns.to_string()),
847 &mut self.marker_list,
848 );
849 }
850
851 for (cursor_id, position, anchor) in new_cursors {
853 if let Some(cursor) = cursors.get_mut(*cursor_id) {
854 cursor.position = *position;
855 cursor.anchor = *anchor;
856 }
857 }
858
859 self.highlighter.invalidate_all();
861
862 let primary_pos = cursors.primary().position;
864 self.primary_cursor_line_number = match self.buffer.offset_to_position(primary_pos)
865 {
866 Some(pos) => crate::model::buffer::LineNumber::Absolute(pos.line),
867 None => crate::model::buffer::LineNumber::Absolute(0),
868 };
869 }
870 }
871 }
872
873 pub fn capture_displaced_markers(&self, range: &Range<usize>) -> Vec<(u64, usize)> {
876 let mut displaced = Vec::new();
877 if range.is_empty() {
878 return displaced;
879 }
880 for (marker_id, start, _end) in self.marker_list.query_range(range.start, range.end) {
881 if start > range.start && start < range.end {
882 displaced.push(
883 DisplacedMarker::Main {
884 id: marker_id.0,
885 position: start,
886 }
887 .encode(),
888 );
889 }
890 }
891 for (marker_id, start, _end) in self.margins.query_indicator_range(range.start, range.end) {
892 if start > range.start && start < range.end {
893 displaced.push(
894 DisplacedMarker::Margin {
895 id: marker_id.0,
896 position: start,
897 }
898 .encode(),
899 );
900 }
901 }
902 displaced
903 }
904
905 pub fn capture_displaced_markers_bulk(
907 &self,
908 edits: &[(usize, usize, String)],
909 ) -> Vec<(u64, usize)> {
910 let mut displaced = Vec::new();
911 for (pos, del_len, _text) in edits {
912 if *del_len > 0 {
913 displaced.extend(self.capture_displaced_markers(&(*pos..*pos + *del_len)));
914 }
915 }
916 displaced
917 }
918
919 pub fn restore_displaced_markers(&mut self, displaced: &[(u64, usize)]) {
921 for &(tagged_id, original_pos) in displaced {
922 let dm = DisplacedMarker::decode(tagged_id, original_pos);
923 match dm {
924 DisplacedMarker::Main { id, position } => {
925 self.marker_list.set_position(MarkerId(id), position);
926 }
927 DisplacedMarker::Margin { id, position } => {
928 self.margins.set_indicator_position(MarkerId(id), position);
929 }
930 }
931 }
932 }
933
934 pub fn apply_many(&mut self, cursors: &mut Cursors, events: &[Event]) {
936 for event in events {
937 self.apply(cursors, event);
938 }
939 }
940
941 pub fn on_focus_lost(&mut self) {
945 if self.popups.dismiss_transient() {
946 tracing::debug!("Dismissed transient popup on buffer focus loss");
947 }
948 }
949}
950
951fn convert_event_face_to_overlay_face(event_face: &EventOverlayFace) -> OverlayFace {
953 match event_face {
954 EventOverlayFace::Underline { color, style } => {
955 let underline_style = match style {
956 crate::model::event::UnderlineStyle::Straight => UnderlineStyle::Straight,
957 crate::model::event::UnderlineStyle::Wavy => UnderlineStyle::Wavy,
958 crate::model::event::UnderlineStyle::Dotted => UnderlineStyle::Dotted,
959 crate::model::event::UnderlineStyle::Dashed => UnderlineStyle::Dashed,
960 };
961 OverlayFace::Underline {
962 color: Color::Rgb(color.0, color.1, color.2),
963 style: underline_style,
964 }
965 }
966 EventOverlayFace::Background { color } => OverlayFace::Background {
967 color: Color::Rgb(color.0, color.1, color.2),
968 },
969 EventOverlayFace::Foreground { color } => OverlayFace::Foreground {
970 color: Color::Rgb(color.0, color.1, color.2),
971 },
972 EventOverlayFace::Style { options } => {
973 use crate::view::theme::named_color_from_str;
974 use ratatui::style::Modifier;
975
976 let mut style = Style::default();
978
979 if let Some(ref fg) = options.fg {
981 if let Some((r, g, b)) = fg.as_rgb() {
982 style = style.fg(Color::Rgb(r, g, b));
983 } else if let Some(key) = fg.as_theme_key() {
984 if let Some(color) = named_color_from_str(key) {
985 style = style.fg(color);
986 }
987 }
988 }
989
990 if let Some(ref bg) = options.bg {
992 if let Some((r, g, b)) = bg.as_rgb() {
993 style = style.bg(Color::Rgb(r, g, b));
994 } else if let Some(key) = bg.as_theme_key() {
995 if let Some(color) = named_color_from_str(key) {
996 style = style.bg(color);
997 }
998 }
999 }
1000
1001 let mut modifiers = Modifier::empty();
1003 if options.bold {
1004 modifiers |= Modifier::BOLD;
1005 }
1006 if options.italic {
1007 modifiers |= Modifier::ITALIC;
1008 }
1009 if options.underline {
1010 modifiers |= Modifier::UNDERLINED;
1011 }
1012 if options.strikethrough {
1013 modifiers |= Modifier::CROSSED_OUT;
1014 }
1015 if !modifiers.is_empty() {
1016 style = style.add_modifier(modifiers);
1017 }
1018
1019 let fg_theme = options
1021 .fg
1022 .as_ref()
1023 .and_then(|c| c.as_theme_key())
1024 .filter(|key| named_color_from_str(key).is_none())
1025 .map(String::from);
1026 let bg_theme = options
1027 .bg
1028 .as_ref()
1029 .and_then(|c| c.as_theme_key())
1030 .filter(|key| named_color_from_str(key).is_none())
1031 .map(String::from);
1032
1033 if fg_theme.is_some() || bg_theme.is_some() {
1035 OverlayFace::ThemedStyle {
1036 fallback_style: style,
1037 fg_theme,
1038 bg_theme,
1039 fg_on_collision_only: options.fg_on_collision_only,
1040 }
1041 } else {
1042 OverlayFace::Style { style }
1043 }
1044 }
1045 }
1046}
1047
1048pub(crate) fn convert_popup_data_to_popup(data: &PopupData) -> Popup {
1050 let content = match &data.content {
1051 crate::model::event::PopupContentData::Text(lines) => PopupContent::Text(lines.clone()),
1052 crate::model::event::PopupContentData::List { items, selected } => PopupContent::List {
1053 items: items
1054 .iter()
1055 .map(|item| PopupListItem {
1056 text: item.text.clone(),
1057 detail: item.detail.clone(),
1058 icon: item.icon.clone(),
1059 data: item.data.clone(),
1060 disabled: false,
1061 })
1062 .collect(),
1063 selected: *selected,
1064 },
1065 };
1066
1067 let position = match data.position {
1068 PopupPositionData::AtCursor => PopupPosition::AtCursor,
1069 PopupPositionData::BelowCursor => PopupPosition::BelowCursor,
1070 PopupPositionData::AboveCursor => PopupPosition::AboveCursor,
1071 PopupPositionData::Fixed { x, y } => PopupPosition::Fixed { x, y },
1072 PopupPositionData::Centered => PopupPosition::Centered,
1073 PopupPositionData::BottomRight => PopupPosition::BottomRight,
1074 PopupPositionData::AboveStatusBarAt { x, status_row } => {
1075 PopupPosition::AboveStatusBarAt { x, status_row }
1076 }
1077 };
1078
1079 let kind = match data.kind {
1081 crate::model::event::PopupKindHint::Completion => PopupKind::Completion,
1082 crate::model::event::PopupKindHint::List => PopupKind::List,
1083 crate::model::event::PopupKindHint::Text => PopupKind::Text,
1084 };
1085
1086 let resolver = match kind {
1093 PopupKind::Completion => crate::view::popup::PopupResolver::Completion,
1094 _ => crate::view::popup::PopupResolver::None,
1095 };
1096
1097 let focused = match kind {
1109 PopupKind::Completion => true,
1113 PopupKind::List => true,
1116 PopupKind::Text => false,
1119 PopupKind::Hover | PopupKind::Action => false,
1123 };
1124
1125 Popup {
1126 kind,
1127 title: data.title.clone(),
1128 description: data.description.clone(),
1129 transient: data.transient,
1130 content,
1131 position,
1132 width: data.width,
1133 max_height: data.max_height,
1134 bordered: data.bordered,
1135 border_style: Style::default().fg(Color::Gray),
1136 background_style: Style::default().bg(Color::Rgb(30, 30, 30)),
1137 scroll_offset: 0,
1138 text_selection: None,
1139 accept_key_hint: None,
1140 resolver,
1141 focused,
1142 focus_key_hint: None,
1143 }
1144}
1145
1146fn convert_margin_position(position: &MarginPositionData) -> MarginPosition {
1148 match position {
1149 MarginPositionData::Left => MarginPosition::Left,
1150 MarginPositionData::Right => MarginPosition::Right,
1151 }
1152}
1153
1154fn convert_margin_content(content: &MarginContentData) -> MarginContent {
1156 match content {
1157 MarginContentData::Text(text) => MarginContent::Text(text.clone()),
1158 MarginContentData::Symbol { text, color } => {
1159 if let Some((r, g, b)) = color {
1160 MarginContent::colored_symbol(text.clone(), Color::Rgb(*r, *g, *b))
1161 } else {
1162 MarginContent::symbol(text.clone(), Style::default())
1163 }
1164 }
1165 MarginContentData::Empty => MarginContent::Empty,
1166 }
1167}
1168
1169impl EditorState {
1170 pub fn prepare_for_render(&mut self, top_byte: usize, height: u16) -> Result<()> {
1177 self.buffer.prepare_viewport(top_byte, height as usize)?;
1178 Ok(())
1179 }
1180
1181 pub fn collect_virtual_line_positions(&self) -> Vec<usize> {
1193 if self.virtual_texts.is_empty() {
1194 return Vec::new();
1195 }
1196 let mut v: Vec<usize> = self
1197 .virtual_texts
1198 .query_lines_in_range(&self.marker_list, 0, self.buffer.len() + 1)
1199 .into_iter()
1200 .map(|(pos, _vt)| pos)
1201 .collect();
1202 v.sort_unstable();
1203 v
1204 }
1205
1206 pub fn collect_soft_break_positions(&self) -> Vec<(usize, u16)> {
1219 if self.soft_breaks.is_empty() {
1220 return Vec::new();
1221 }
1222 self.soft_breaks
1224 .query_viewport(0, self.buffer.len() + 1, &self.marker_list)
1225 }
1226
1227 pub fn get_text_range(&mut self, start: usize, end: usize) -> String {
1247 match self
1249 .buffer
1250 .get_text_range_mut(start, end.saturating_sub(start))
1251 {
1252 Ok(bytes) => String::from_utf8_lossy(&bytes).into_owned(),
1253 Err(e) => {
1254 tracing::warn!("Failed to get text range {}..{}: {}", start, end, e);
1255 String::new()
1256 }
1257 }
1258 }
1259
1260 pub fn get_line_at_offset(&mut self, offset: usize) -> Option<(usize, String)> {
1268 use crate::model::document_model::DocumentModel;
1269
1270 let mut line_start = offset;
1273 while line_start > 0 {
1274 if let Ok(text) = self.buffer.get_text_range_mut(line_start - 1, 1) {
1275 if text.first() == Some(&b'\n') {
1276 break;
1277 }
1278 line_start -= 1;
1279 } else {
1280 break;
1281 }
1282 }
1283
1284 let viewport = self
1286 .get_viewport_content(
1287 crate::model::document_model::DocumentPosition::byte(line_start),
1288 1,
1289 )
1290 .ok()?;
1291
1292 viewport
1293 .lines
1294 .first()
1295 .map(|line| (line.byte_offset, line.content.clone()))
1296 }
1297
1298 pub fn get_text_to_end_of_line(&mut self, cursor_pos: usize) -> Result<String> {
1303 use crate::model::document_model::DocumentModel;
1304
1305 let viewport = self.get_viewport_content(
1307 crate::model::document_model::DocumentPosition::byte(cursor_pos),
1308 1,
1309 )?;
1310
1311 if let Some(line) = viewport.lines.first() {
1312 let line_start = line.byte_offset;
1313 let line_end = line_start + line.content.len();
1314
1315 if cursor_pos >= line_start && cursor_pos <= line_end {
1316 let offset_in_line = cursor_pos - line_start;
1317 Ok(line.content.get(offset_in_line..).unwrap_or("").to_string())
1319 } else {
1320 Ok(String::new())
1321 }
1322 } else {
1323 Ok(String::new())
1324 }
1325 }
1326
1327 pub fn set_semantic_tokens(&mut self, store: SemanticTokenStore) {
1329 self.semantic_tokens = Some(store);
1330 }
1331
1332 pub fn clear_semantic_tokens(&mut self) {
1334 self.semantic_tokens = None;
1335 }
1336
1337 pub fn semantic_tokens_result_id(&self) -> Option<&str> {
1339 self.semantic_tokens
1340 .as_ref()
1341 .and_then(|store| store.result_id.as_deref())
1342 }
1343}
1344
1345impl DocumentModel for EditorState {
1350 fn capabilities(&self) -> DocumentCapabilities {
1351 let line_count = self.buffer.line_count();
1352 DocumentCapabilities {
1353 has_line_index: line_count.is_some(),
1354 uses_lazy_loading: false, byte_length: self.buffer.len(),
1356 approximate_line_count: line_count.unwrap_or_else(|| {
1357 self.buffer.len() / 80
1359 }),
1360 }
1361 }
1362
1363 fn get_viewport_content(
1364 &mut self,
1365 start_pos: DocumentPosition,
1366 max_lines: usize,
1367 ) -> Result<ViewportContent> {
1368 let start_offset = self.position_to_offset(start_pos)?;
1370
1371 let line_iter = self.buffer.iter_lines_from(start_offset, max_lines)?;
1374 let has_more = line_iter.has_more;
1375
1376 let lines = line_iter
1377 .map(|line_data| ViewportLine {
1378 byte_offset: line_data.byte_offset,
1379 content: line_data.content,
1380 has_newline: line_data.has_newline,
1381 approximate_line_number: line_data.line_number,
1382 })
1383 .collect();
1384
1385 Ok(ViewportContent {
1386 start_position: DocumentPosition::ByteOffset(start_offset),
1387 lines,
1388 has_more,
1389 })
1390 }
1391
1392 fn position_to_offset(&self, pos: DocumentPosition) -> Result<usize> {
1393 match pos {
1394 DocumentPosition::ByteOffset(offset) => Ok(offset),
1395 DocumentPosition::LineColumn { line, column } => {
1396 if !self.has_line_index() {
1397 anyhow::bail!("Line indexing not available for this document");
1398 }
1399 let position = crate::model::piece_tree::Position { line, column };
1401 Ok(self.buffer.position_to_offset(position))
1402 }
1403 }
1404 }
1405
1406 fn offset_to_position(&self, offset: usize) -> DocumentPosition {
1407 if self.has_line_index() {
1408 if let Some(pos) = self.buffer.offset_to_position(offset) {
1409 DocumentPosition::LineColumn {
1410 line: pos.line,
1411 column: pos.column,
1412 }
1413 } else {
1414 DocumentPosition::ByteOffset(offset)
1416 }
1417 } else {
1418 DocumentPosition::ByteOffset(offset)
1419 }
1420 }
1421
1422 fn get_range(&mut self, start: DocumentPosition, end: DocumentPosition) -> Result<String> {
1423 let start_offset = self.position_to_offset(start)?;
1424 let end_offset = self.position_to_offset(end)?;
1425
1426 if start_offset > end_offset {
1427 anyhow::bail!(
1428 "Invalid range: start offset {} > end offset {}",
1429 start_offset,
1430 end_offset
1431 );
1432 }
1433
1434 let bytes = self
1435 .buffer
1436 .get_text_range_mut(start_offset, end_offset - start_offset)?;
1437
1438 Ok(String::from_utf8_lossy(&bytes).into_owned())
1439 }
1440
1441 fn get_line_content(&mut self, line_number: usize) -> Option<String> {
1442 if !self.has_line_index() {
1443 return None;
1444 }
1445
1446 let line_start_offset = self.buffer.line_start_offset(line_number)?;
1448
1449 let mut iter = self.buffer.line_iterator(line_start_offset, 80);
1451 if let Some((_start, content)) = iter.next_line() {
1452 let has_newline = content.ends_with('\n');
1453 let line_content = if has_newline {
1454 content[..content.len() - 1].to_string()
1455 } else {
1456 content
1457 };
1458 Some(line_content)
1459 } else {
1460 None
1461 }
1462 }
1463
1464 fn get_chunk_at_offset(&mut self, offset: usize, size: usize) -> Result<(usize, String)> {
1465 let bytes = self.buffer.get_text_range_mut(offset, size)?;
1466
1467 Ok((offset, String::from_utf8_lossy(&bytes).into_owned()))
1468 }
1469
1470 fn insert(&mut self, pos: DocumentPosition, text: &str) -> Result<usize> {
1471 let offset = self.position_to_offset(pos)?;
1472 self.buffer.insert_bytes(offset, text.as_bytes().to_vec());
1473 Ok(text.len())
1474 }
1475
1476 fn delete(&mut self, start: DocumentPosition, end: DocumentPosition) -> Result<()> {
1477 let start_offset = self.position_to_offset(start)?;
1478 let end_offset = self.position_to_offset(end)?;
1479
1480 if start_offset > end_offset {
1481 anyhow::bail!(
1482 "Invalid range: start offset {} > end offset {}",
1483 start_offset,
1484 end_offset
1485 );
1486 }
1487
1488 self.buffer.delete(start_offset..end_offset);
1489 Ok(())
1490 }
1491
1492 fn replace(
1493 &mut self,
1494 start: DocumentPosition,
1495 end: DocumentPosition,
1496 text: &str,
1497 ) -> Result<()> {
1498 self.delete(start, end)?;
1500 self.insert(start, text)?;
1501 Ok(())
1502 }
1503
1504 fn find_matches(
1505 &mut self,
1506 pattern: &str,
1507 search_range: Option<(DocumentPosition, DocumentPosition)>,
1508 ) -> Result<Vec<usize>> {
1509 let (start_offset, end_offset) = if let Some((start, end)) = search_range {
1510 (
1511 self.position_to_offset(start)?,
1512 self.position_to_offset(end)?,
1513 )
1514 } else {
1515 (0, self.buffer.len())
1516 };
1517
1518 let bytes = self
1520 .buffer
1521 .get_text_range_mut(start_offset, end_offset - start_offset)?;
1522 let text = String::from_utf8_lossy(&bytes);
1523
1524 let mut matches = Vec::new();
1526 let mut search_offset = 0;
1527 while let Some(pos) = text[search_offset..].find(pattern) {
1528 matches.push(start_offset + search_offset + pos);
1529 search_offset += pos + pattern.len();
1530 }
1531
1532 Ok(matches)
1533 }
1534}
1535
1536#[derive(Clone, Debug)]
1538pub struct SemanticTokenStore {
1539 pub version: u64,
1541 pub result_id: Option<String>,
1543 pub data: Vec<u32>,
1545 pub tokens: Vec<SemanticTokenSpan>,
1547}
1548
1549#[derive(Clone, Debug)]
1551pub struct SemanticTokenSpan {
1552 pub range: Range<usize>,
1553 pub token_type: String,
1554 pub modifiers: Vec<String>,
1555}
1556
1557#[cfg(test)]
1558mod tests {
1559 use crate::model::filesystem::StdFileSystem;
1560 use std::sync::Arc;
1561
1562 fn test_fs() -> Arc<dyn crate::model::filesystem::FileSystem + Send + Sync> {
1563 Arc::new(StdFileSystem)
1564 }
1565 use super::*;
1566 use crate::model::event::CursorId;
1567
1568 #[test]
1569 fn test_state_new() {
1570 let state = EditorState::new(
1571 80,
1572 24,
1573 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1574 test_fs(),
1575 );
1576 assert!(state.buffer.is_empty());
1577 }
1578
1579 #[test]
1580 fn test_apply_insert() {
1581 let mut state = EditorState::new(
1582 80,
1583 24,
1584 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1585 test_fs(),
1586 );
1587 let mut cursors = Cursors::new();
1588 let cursor_id = cursors.primary_id();
1589
1590 state.apply(
1591 &mut cursors,
1592 &Event::Insert {
1593 position: 0,
1594 text: "hello".to_string(),
1595 cursor_id,
1596 },
1597 );
1598
1599 assert_eq!(state.buffer.to_string().unwrap(), "hello");
1600 assert_eq!(cursors.primary().position, 5);
1601 assert!(state.buffer.is_modified());
1602 }
1603
1604 #[test]
1605 fn test_apply_delete() {
1606 let mut state = EditorState::new(
1607 80,
1608 24,
1609 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1610 test_fs(),
1611 );
1612 let mut cursors = Cursors::new();
1613 let cursor_id = cursors.primary_id();
1614
1615 state.apply(
1617 &mut cursors,
1618 &Event::Insert {
1619 position: 0,
1620 text: "hello world".to_string(),
1621 cursor_id,
1622 },
1623 );
1624
1625 state.apply(
1626 &mut cursors,
1627 &Event::Delete {
1628 range: 5..11,
1629 deleted_text: " world".to_string(),
1630 cursor_id,
1631 },
1632 );
1633
1634 assert_eq!(state.buffer.to_string().unwrap(), "hello");
1635 assert_eq!(cursors.primary().position, 5);
1636 }
1637
1638 #[test]
1639 fn test_apply_move_cursor() {
1640 let mut state = EditorState::new(
1641 80,
1642 24,
1643 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1644 test_fs(),
1645 );
1646 let mut cursors = Cursors::new();
1647 let cursor_id = cursors.primary_id();
1648
1649 state.apply(
1650 &mut cursors,
1651 &Event::Insert {
1652 position: 0,
1653 text: "hello".to_string(),
1654 cursor_id,
1655 },
1656 );
1657
1658 state.apply(
1659 &mut cursors,
1660 &Event::MoveCursor {
1661 cursor_id,
1662 old_position: 5,
1663 new_position: 2,
1664 old_anchor: None,
1665 new_anchor: None,
1666 old_sticky_column: 0,
1667 new_sticky_column: 0,
1668 },
1669 );
1670
1671 assert_eq!(cursors.primary().position, 2);
1672 }
1673
1674 #[test]
1675 fn test_apply_add_cursor() {
1676 let mut state = EditorState::new(
1677 80,
1678 24,
1679 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1680 test_fs(),
1681 );
1682 let mut cursors = Cursors::new();
1683 let cursor_id = CursorId(1);
1684
1685 state.apply(
1686 &mut cursors,
1687 &Event::AddCursor {
1688 cursor_id,
1689 position: 5,
1690 anchor: None,
1691 },
1692 );
1693
1694 assert_eq!(cursors.count(), 2);
1695 }
1696
1697 #[test]
1698 fn test_apply_many() {
1699 let mut state = EditorState::new(
1700 80,
1701 24,
1702 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1703 test_fs(),
1704 );
1705 let mut cursors = Cursors::new();
1706 let cursor_id = cursors.primary_id();
1707
1708 let events = vec![
1709 Event::Insert {
1710 position: 0,
1711 text: "hello ".to_string(),
1712 cursor_id,
1713 },
1714 Event::Insert {
1715 position: 6,
1716 text: "world".to_string(),
1717 cursor_id,
1718 },
1719 ];
1720
1721 state.apply_many(&mut cursors, &events);
1722
1723 assert_eq!(state.buffer.to_string().unwrap(), "hello world");
1724 }
1725
1726 #[test]
1727 fn test_cursor_adjustment_after_insert() {
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 state.apply(
1739 &mut cursors,
1740 &Event::AddCursor {
1741 cursor_id: CursorId(1),
1742 position: 5,
1743 anchor: None,
1744 },
1745 );
1746
1747 state.apply(
1749 &mut cursors,
1750 &Event::Insert {
1751 position: 0,
1752 text: "abc".to_string(),
1753 cursor_id,
1754 },
1755 );
1756
1757 if let Some(cursor) = cursors.get(CursorId(1)) {
1759 assert_eq!(cursor.position, 8);
1760 }
1761 }
1762
1763 mod document_model_tests {
1765 use super::*;
1766 use crate::model::document_model::{DocumentModel, DocumentPosition};
1767
1768 #[test]
1769 fn test_capabilities_small_file() {
1770 let mut state = EditorState::new(
1771 80,
1772 24,
1773 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1774 test_fs(),
1775 );
1776 state.buffer = Buffer::from_str_test("line1\nline2\nline3");
1777
1778 let caps = state.capabilities();
1779 assert!(caps.has_line_index, "Small file should have line index");
1780 assert_eq!(caps.byte_length, "line1\nline2\nline3".len());
1781 assert_eq!(caps.approximate_line_count, 3, "Should have 3 lines");
1782 }
1783
1784 #[test]
1785 fn test_position_conversions() {
1786 let mut state = EditorState::new(
1787 80,
1788 24,
1789 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1790 test_fs(),
1791 );
1792 state.buffer = Buffer::from_str_test("hello\nworld\ntest");
1793
1794 let pos1 = DocumentPosition::ByteOffset(6);
1796 let offset1 = state.position_to_offset(pos1).unwrap();
1797 assert_eq!(offset1, 6);
1798
1799 let pos2 = DocumentPosition::LineColumn { line: 1, column: 0 };
1801 let offset2 = state.position_to_offset(pos2).unwrap();
1802 assert_eq!(offset2, 6, "Line 1, column 0 should be at byte 6");
1803
1804 let converted = state.offset_to_position(6);
1806 match converted {
1807 DocumentPosition::LineColumn { line, column } => {
1808 assert_eq!(line, 1);
1809 assert_eq!(column, 0);
1810 }
1811 _ => panic!("Expected LineColumn for small file"),
1812 }
1813 }
1814
1815 #[test]
1816 fn test_get_viewport_content() {
1817 let mut state = EditorState::new(
1818 80,
1819 24,
1820 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1821 test_fs(),
1822 );
1823 state.buffer = Buffer::from_str_test("line1\nline2\nline3\nline4\nline5");
1824
1825 let content = state
1826 .get_viewport_content(DocumentPosition::ByteOffset(0), 3)
1827 .unwrap();
1828
1829 assert_eq!(content.lines.len(), 3);
1830 assert_eq!(content.lines[0].content, "line1");
1831 assert_eq!(content.lines[1].content, "line2");
1832 assert_eq!(content.lines[2].content, "line3");
1833 assert!(content.has_more);
1834 }
1835
1836 #[test]
1837 fn test_get_range() {
1838 let mut state = EditorState::new(
1839 80,
1840 24,
1841 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1842 test_fs(),
1843 );
1844 state.buffer = Buffer::from_str_test("hello world");
1845
1846 let text = state
1847 .get_range(
1848 DocumentPosition::ByteOffset(0),
1849 DocumentPosition::ByteOffset(5),
1850 )
1851 .unwrap();
1852 assert_eq!(text, "hello");
1853
1854 let text2 = state
1855 .get_range(
1856 DocumentPosition::ByteOffset(6),
1857 DocumentPosition::ByteOffset(11),
1858 )
1859 .unwrap();
1860 assert_eq!(text2, "world");
1861 }
1862
1863 #[test]
1864 fn test_get_line_content() {
1865 let mut state = EditorState::new(
1866 80,
1867 24,
1868 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1869 test_fs(),
1870 );
1871 state.buffer = Buffer::from_str_test("line1\nline2\nline3");
1872
1873 let line0 = state.get_line_content(0).unwrap();
1874 assert_eq!(line0, "line1");
1875
1876 let line1 = state.get_line_content(1).unwrap();
1877 assert_eq!(line1, "line2");
1878
1879 let line2 = state.get_line_content(2).unwrap();
1880 assert_eq!(line2, "line3");
1881 }
1882
1883 #[test]
1884 fn test_insert_delete() {
1885 let mut state = EditorState::new(
1886 80,
1887 24,
1888 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1889 test_fs(),
1890 );
1891 state.buffer = Buffer::from_str_test("hello world");
1892
1893 let bytes_inserted = state
1895 .insert(DocumentPosition::ByteOffset(6), "beautiful ")
1896 .unwrap();
1897 assert_eq!(bytes_inserted, 10);
1898 assert_eq!(state.buffer.to_string().unwrap(), "hello beautiful world");
1899
1900 state
1902 .delete(
1903 DocumentPosition::ByteOffset(6),
1904 DocumentPosition::ByteOffset(16),
1905 )
1906 .unwrap();
1907 assert_eq!(state.buffer.to_string().unwrap(), "hello world");
1908 }
1909
1910 #[test]
1911 fn test_replace() {
1912 let mut state = EditorState::new(
1913 80,
1914 24,
1915 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1916 test_fs(),
1917 );
1918 state.buffer = Buffer::from_str_test("hello world");
1919
1920 state
1921 .replace(
1922 DocumentPosition::ByteOffset(0),
1923 DocumentPosition::ByteOffset(5),
1924 "hi",
1925 )
1926 .unwrap();
1927 assert_eq!(state.buffer.to_string().unwrap(), "hi world");
1928 }
1929
1930 #[test]
1931 fn test_find_matches() {
1932 let mut state = EditorState::new(
1933 80,
1934 24,
1935 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1936 test_fs(),
1937 );
1938 state.buffer = Buffer::from_str_test("hello world hello");
1939
1940 let matches = state.find_matches("hello", None).unwrap();
1941 assert_eq!(matches.len(), 2);
1942 assert_eq!(matches[0], 0);
1943 assert_eq!(matches[1], 12);
1944 }
1945
1946 #[test]
1947 fn test_prepare_for_render() {
1948 let mut state = EditorState::new(
1949 80,
1950 24,
1951 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1952 test_fs(),
1953 );
1954 state.buffer = Buffer::from_str_test("line1\nline2\nline3\nline4\nline5");
1955
1956 state.prepare_for_render(0, 24).unwrap();
1958 }
1959
1960 #[test]
1961 fn test_helper_get_text_range() {
1962 let mut state = EditorState::new(
1963 80,
1964 24,
1965 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1966 test_fs(),
1967 );
1968 state.buffer = Buffer::from_str_test("hello world");
1969
1970 let text = state.get_text_range(0, 5);
1972 assert_eq!(text, "hello");
1973
1974 let text2 = state.get_text_range(6, 11);
1976 assert_eq!(text2, "world");
1977 }
1978
1979 #[test]
1980 fn test_helper_get_line_at_offset() {
1981 let mut state = EditorState::new(
1982 80,
1983 24,
1984 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1985 test_fs(),
1986 );
1987 state.buffer = Buffer::from_str_test("line1\nline2\nline3");
1988
1989 let (offset, content) = state.get_line_at_offset(0).unwrap();
1991 assert_eq!(offset, 0);
1992 assert_eq!(content, "line1");
1993
1994 let (offset2, content2) = state.get_line_at_offset(8).unwrap();
1996 assert_eq!(offset2, 6); assert_eq!(content2, "line2");
1998
1999 let (offset3, content3) = state.get_line_at_offset(12).unwrap();
2001 assert_eq!(offset3, 12);
2002 assert_eq!(content3, "line3");
2003 }
2004
2005 #[test]
2006 fn test_helper_get_text_to_end_of_line() {
2007 let mut state = EditorState::new(
2008 80,
2009 24,
2010 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
2011 test_fs(),
2012 );
2013 state.buffer = Buffer::from_str_test("hello world\nline2");
2014
2015 let text = state.get_text_to_end_of_line(0).unwrap();
2017 assert_eq!(text, "hello world");
2018
2019 let text2 = state.get_text_to_end_of_line(6).unwrap();
2021 assert_eq!(text2, "world");
2022
2023 let text3 = state.get_text_to_end_of_line(11).unwrap();
2025 assert_eq!(text3, "");
2026
2027 let text4 = state.get_text_to_end_of_line(12).unwrap();
2029 assert_eq!(text4, "line2");
2030 }
2031 }
2032
2033 mod virtual_text_integration_tests {
2035 use super::*;
2036 use crate::view::virtual_text::VirtualTextPosition;
2037 use ratatui::style::Style;
2038
2039 #[test]
2040 fn test_virtual_text_add_and_query() {
2041 let mut state = EditorState::new(
2042 80,
2043 24,
2044 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
2045 test_fs(),
2046 );
2047 state.buffer = Buffer::from_str_test("hello world");
2048
2049 if !state.buffer.is_empty() {
2051 state.marker_list.adjust_for_insert(0, state.buffer.len());
2052 }
2053
2054 let vtext_id = state.virtual_texts.add(
2056 &mut state.marker_list,
2057 5,
2058 ": string".to_string(),
2059 Style::default(),
2060 VirtualTextPosition::AfterChar,
2061 0,
2062 );
2063
2064 let results = state.virtual_texts.query_range(&state.marker_list, 0, 11);
2066 assert_eq!(results.len(), 1);
2067 assert_eq!(results[0].0, 5); assert_eq!(results[0].1.text, ": string");
2069
2070 let lookup = state.virtual_texts.build_lookup(&state.marker_list, 0, 11);
2072 assert!(lookup.contains_key(&5));
2073 assert_eq!(lookup[&5].len(), 1);
2074 assert_eq!(lookup[&5][0].text, ": string");
2075
2076 state.virtual_texts.remove(&mut state.marker_list, vtext_id);
2078 assert!(state.virtual_texts.is_empty());
2079 }
2080
2081 #[test]
2082 fn test_virtual_text_position_tracking_on_insert() {
2083 let mut state = EditorState::new(
2084 80,
2085 24,
2086 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
2087 test_fs(),
2088 );
2089 state.buffer = Buffer::from_str_test("hello world");
2090
2091 if !state.buffer.is_empty() {
2093 state.marker_list.adjust_for_insert(0, state.buffer.len());
2094 }
2095
2096 let _vtext_id = state.virtual_texts.add(
2098 &mut state.marker_list,
2099 6,
2100 "/*param*/".to_string(),
2101 Style::default(),
2102 VirtualTextPosition::BeforeChar,
2103 0,
2104 );
2105
2106 let mut cursors = Cursors::new();
2108 let cursor_id = cursors.primary_id();
2109 state.apply(
2110 &mut cursors,
2111 &Event::Insert {
2112 position: 6,
2113 text: "beautiful ".to_string(),
2114 cursor_id,
2115 },
2116 );
2117
2118 let results = state.virtual_texts.query_range(&state.marker_list, 0, 30);
2120 assert_eq!(results.len(), 1);
2121 assert_eq!(results[0].0, 16); assert_eq!(results[0].1.text, "/*param*/");
2123 }
2124
2125 #[test]
2126 fn test_virtual_text_position_tracking_on_delete() {
2127 let mut state = EditorState::new(
2128 80,
2129 24,
2130 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
2131 test_fs(),
2132 );
2133 state.buffer = Buffer::from_str_test("hello beautiful world");
2134
2135 if !state.buffer.is_empty() {
2137 state.marker_list.adjust_for_insert(0, state.buffer.len());
2138 }
2139
2140 let _vtext_id = state.virtual_texts.add(
2142 &mut state.marker_list,
2143 16,
2144 ": string".to_string(),
2145 Style::default(),
2146 VirtualTextPosition::AfterChar,
2147 0,
2148 );
2149
2150 let mut cursors = Cursors::new();
2152 let cursor_id = cursors.primary_id();
2153 state.apply(
2154 &mut cursors,
2155 &Event::Delete {
2156 range: 6..16,
2157 deleted_text: "beautiful ".to_string(),
2158 cursor_id,
2159 },
2160 );
2161
2162 let results = state.virtual_texts.query_range(&state.marker_list, 0, 20);
2164 assert_eq!(results.len(), 1);
2165 assert_eq!(results[0].0, 6); assert_eq!(results[0].1.text, ": string");
2167 }
2168
2169 #[test]
2170 fn test_multiple_virtual_texts_with_priorities() {
2171 let mut state = EditorState::new(
2172 80,
2173 24,
2174 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
2175 test_fs(),
2176 );
2177 state.buffer = Buffer::from_str_test("let x = 5");
2178
2179 if !state.buffer.is_empty() {
2181 state.marker_list.adjust_for_insert(0, state.buffer.len());
2182 }
2183
2184 state.virtual_texts.add(
2186 &mut state.marker_list,
2187 5,
2188 ": i32".to_string(),
2189 Style::default(),
2190 VirtualTextPosition::AfterChar,
2191 0, );
2193
2194 state.virtual_texts.add(
2196 &mut state.marker_list,
2197 5,
2198 " /* inferred */".to_string(),
2199 Style::default(),
2200 VirtualTextPosition::AfterChar,
2201 10, );
2203
2204 let lookup = state.virtual_texts.build_lookup(&state.marker_list, 0, 10);
2206 assert!(lookup.contains_key(&5));
2207 let vtexts = &lookup[&5];
2208 assert_eq!(vtexts.len(), 2);
2209 assert_eq!(vtexts[0].text, ": i32");
2211 assert_eq!(vtexts[1].text, " /* inferred */");
2212 }
2213
2214 #[test]
2215 fn test_virtual_text_clear() {
2216 let mut state = EditorState::new(
2217 80,
2218 24,
2219 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
2220 test_fs(),
2221 );
2222 state.buffer = Buffer::from_str_test("test");
2223
2224 if !state.buffer.is_empty() {
2226 state.marker_list.adjust_for_insert(0, state.buffer.len());
2227 }
2228
2229 state.virtual_texts.add(
2231 &mut state.marker_list,
2232 0,
2233 "hint1".to_string(),
2234 Style::default(),
2235 VirtualTextPosition::BeforeChar,
2236 0,
2237 );
2238 state.virtual_texts.add(
2239 &mut state.marker_list,
2240 2,
2241 "hint2".to_string(),
2242 Style::default(),
2243 VirtualTextPosition::AfterChar,
2244 0,
2245 );
2246
2247 assert_eq!(state.virtual_texts.len(), 2);
2248
2249 state.virtual_texts.clear(&mut state.marker_list);
2251 assert!(state.virtual_texts.is_empty());
2252
2253 let results = state.virtual_texts.query_range(&state.marker_list, 0, 10);
2255 assert!(results.is_empty());
2256 }
2257 }
2258}