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::margin::{MarginAnnotation, MarginContent, MarginManager, MarginPosition};
21use crate::view::overlay::{Overlay, OverlayFace, OverlayManager, UnderlineStyle};
22use crate::view::popup::{
23 Popup, PopupContent, PopupKind, PopupListItem, PopupManager, PopupPosition,
24};
25use crate::view::reference_highlight_overlay::ReferenceHighlightOverlay;
26use crate::view::soft_break::SoftBreakManager;
27use crate::view::virtual_text::VirtualTextManager;
28use anyhow::Result;
29use lsp_types::FoldingRange;
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 editing_disabled: bool,
187
188 pub buffer_settings: BufferSettings,
191
192 pub reference_highlighter: ReferenceHighlighter,
194
195 pub is_composite_buffer: bool,
197
198 pub debug_highlight_mode: bool,
200
201 pub reference_highlight_overlay: ReferenceHighlightOverlay,
203
204 pub bracket_highlight_overlay: BracketHighlightOverlay,
206
207 pub semantic_tokens: Option<SemanticTokenStore>,
209
210 pub folding_ranges: Vec<FoldingRange>,
212
213 pub language: String,
216
217 pub display_name: String,
222
223 pub scrollbar_row_cache: ScrollbarRowCache,
226}
227
228#[derive(Debug, Clone, Default)]
231pub struct ScrollbarRowCache {
232 pub buffer_version: u64,
234 pub viewport_width: u16,
236 pub wrap_indent: bool,
238 pub total_visual_rows: usize,
240 pub top_byte: usize,
242 pub top_visual_row: usize,
244 pub top_view_line_offset: usize,
246 pub valid: bool,
248}
249
250impl EditorState {
251 pub fn apply_language(&mut self, detected: DetectedLanguage) {
258 self.language = detected.name;
259 self.display_name = detected.display_name;
260 self.highlighter = detected.highlighter;
261 if let Some(lang) = &detected.ts_language {
262 self.reference_highlighter.set_language(lang);
263 }
264 }
265
266 fn new_from_buffer(buffer: Buffer) -> Self {
269 let mut marker_list = MarkerList::new();
270 if !buffer.is_empty() {
271 marker_list.adjust_for_insert(0, buffer.len());
272 }
273
274 Self {
275 buffer,
276 highlighter: HighlightEngine::None,
277 indent_calculator: RefCell::new(IndentCalculator::new()),
278 overlays: OverlayManager::new(),
279 marker_list,
280 virtual_texts: VirtualTextManager::new(),
281 conceals: ConcealManager::new(),
282 soft_breaks: SoftBreakManager::new(),
283 popups: PopupManager::new(),
284 margins: MarginManager::new(),
285 primary_cursor_line_number: LineNumber::Absolute(0),
286 mode: "insert".to_string(),
287 text_properties: TextPropertyManager::new(),
288 show_cursors: true,
289 editing_disabled: false,
290 buffer_settings: BufferSettings::default(),
291 reference_highlighter: ReferenceHighlighter::new(),
292 is_composite_buffer: false,
293 debug_highlight_mode: false,
294 reference_highlight_overlay: ReferenceHighlightOverlay::new(),
295 bracket_highlight_overlay: BracketHighlightOverlay::new(),
296 semantic_tokens: None,
297 folding_ranges: Vec::new(),
298 language: "text".to_string(),
299 display_name: "Text".to_string(),
300 scrollbar_row_cache: ScrollbarRowCache::default(),
301 }
302 }
303
304 pub fn new(
305 _width: u16,
306 _height: u16,
307 large_file_threshold: usize,
308 fs: Arc<dyn FileSystem + Send + Sync>,
309 ) -> Self {
310 Self::new_from_buffer(Buffer::new(large_file_threshold, fs))
311 }
312
313 pub fn new_with_path(
316 large_file_threshold: usize,
317 fs: Arc<dyn FileSystem + Send + Sync>,
318 path: std::path::PathBuf,
319 ) -> Self {
320 Self::new_from_buffer(Buffer::new_with_path(large_file_threshold, fs, path))
321 }
322
323 pub fn set_language_from_name(&mut self, name: &str, registry: &GrammarRegistry) {
327 let detected = DetectedLanguage::from_virtual_name(name, registry);
328 tracing::debug!(
329 "Set highlighter for virtual buffer based on name: {} (backend: {}, language: {})",
330 name,
331 detected.highlighter.backend_name(),
332 detected.name
333 );
334 self.apply_language(detected);
335 }
336
337 pub fn from_file(
342 path: &std::path::Path,
343 _width: u16,
344 _height: u16,
345 large_file_threshold: usize,
346 registry: &GrammarRegistry,
347 fs: Arc<dyn FileSystem + Send + Sync>,
348 ) -> anyhow::Result<Self> {
349 let buffer = Buffer::load_from_file(path, large_file_threshold, fs)?;
350 let detected = DetectedLanguage::from_path_builtin(path, registry);
351 let mut state = Self::new_from_buffer(buffer);
352 state.apply_language(detected);
353 Ok(state)
354 }
355
356 pub fn from_file_with_languages(
364 path: &std::path::Path,
365 _width: u16,
366 _height: u16,
367 large_file_threshold: usize,
368 registry: &GrammarRegistry,
369 languages: &std::collections::HashMap<String, crate::config::LanguageConfig>,
370 fs: Arc<dyn FileSystem + Send + Sync>,
371 ) -> anyhow::Result<Self> {
372 let buffer = Buffer::load_from_file(path, large_file_threshold, fs)?;
373 let detected = DetectedLanguage::from_path(path, registry, languages);
374 let mut state = Self::new_from_buffer(buffer);
375 state.apply_language(detected);
376 Ok(state)
377 }
378
379 pub fn from_buffer_with_language(buffer: Buffer, detected: DetectedLanguage) -> Self {
384 let mut state = Self::new_from_buffer(buffer);
385 state.apply_language(detected);
386 state
387 }
388
389 fn apply_insert(
391 &mut self,
392 cursors: &mut Cursors,
393 position: usize,
394 text: &str,
395 cursor_id: crate::model::event::CursorId,
396 ) {
397 let newlines_inserted = text.matches('\n').count();
398
399 self.marker_list.adjust_for_insert(position, text.len());
401 self.margins.adjust_for_insert(position, text.len());
402
403 self.buffer.insert(position, text);
405
406 self.highlighter.notify_insert(position, text.len());
409 self.highlighter
410 .invalidate_range(position..position + text.len());
411
412 cursors.adjust_for_edit(position, 0, text.len());
417
418 if let Some(cursor) = cursors.get_mut(cursor_id) {
420 cursor.position = position + text.len();
421 cursor.clear_selection();
422 }
423
424 if cursor_id == cursors.primary_id() {
426 self.primary_cursor_line_number = match self.primary_cursor_line_number {
427 LineNumber::Absolute(line) => LineNumber::Absolute(line + newlines_inserted),
428 LineNumber::Relative {
429 line,
430 from_cached_line,
431 } => LineNumber::Relative {
432 line: line + newlines_inserted,
433 from_cached_line,
434 },
435 };
436 }
437 }
438
439 fn apply_delete(
441 &mut self,
442 cursors: &mut Cursors,
443 range: &std::ops::Range<usize>,
444 cursor_id: crate::model::event::CursorId,
445 deleted_text: &str,
446 ) {
447 let len = range.len();
448
449 let primary_newlines_removed = if cursor_id == cursors.primary_id() {
453 let cursor_pos = cursors.get(cursor_id).map_or(range.start, |c| c.position);
454 let bytes_before_cursor = cursor_pos.saturating_sub(range.start).min(len);
455 deleted_text[..bytes_before_cursor].matches('\n').count()
456 } else {
457 0
458 };
459
460 self.marker_list.adjust_for_delete(range.start, len);
462 self.margins.adjust_for_delete(range.start, len);
463
464 self.buffer.delete(range.clone());
466
467 self.highlighter.notify_delete(range.start, len);
470 self.highlighter.invalidate_range(range.clone());
471
472 cursors.adjust_for_edit(range.start, len, 0);
477
478 if let Some(cursor) = cursors.get_mut(cursor_id) {
480 cursor.position = range.start;
481 cursor.clear_selection();
482 }
483
484 if cursor_id == cursors.primary_id() && primary_newlines_removed > 0 {
486 self.primary_cursor_line_number = match self.primary_cursor_line_number {
487 LineNumber::Absolute(line) => {
488 LineNumber::Absolute(line.saturating_sub(primary_newlines_removed))
489 }
490 LineNumber::Relative {
491 line,
492 from_cached_line,
493 } => LineNumber::Relative {
494 line: line.saturating_sub(primary_newlines_removed),
495 from_cached_line,
496 },
497 };
498 }
499 }
500
501 pub fn apply(&mut self, cursors: &mut Cursors, event: &Event) {
504 match event {
505 Event::Insert {
506 position,
507 text,
508 cursor_id,
509 } => self.apply_insert(cursors, *position, text, *cursor_id),
510
511 Event::Delete {
512 range,
513 cursor_id,
514 deleted_text,
515 } => self.apply_delete(cursors, range, *cursor_id, deleted_text),
516
517 Event::MoveCursor {
518 cursor_id,
519 new_position,
520 new_anchor,
521 new_sticky_column,
522 ..
523 } => {
524 if let Some(cursor) = cursors.get_mut(*cursor_id) {
525 cursor.position = *new_position;
526 cursor.anchor = *new_anchor;
527 cursor.sticky_column = *new_sticky_column;
528 }
529
530 if *cursor_id == cursors.primary_id() {
533 self.primary_cursor_line_number =
534 match self.buffer.offset_to_position(*new_position) {
535 Some(pos) => LineNumber::Absolute(pos.line),
536 None => {
537 let estimated_line = *new_position / 80;
540 LineNumber::Absolute(estimated_line)
541 }
542 };
543 }
544 }
545
546 Event::AddCursor {
547 cursor_id,
548 position,
549 anchor,
550 } => {
551 let cursor = if let Some(anchor) = anchor {
552 Cursor::with_selection(*anchor, *position)
553 } else {
554 Cursor::new(*position)
555 };
556
557 cursors.insert_with_id(*cursor_id, cursor);
560
561 cursors.normalize();
562 }
563
564 Event::RemoveCursor { cursor_id, .. } => {
565 cursors.remove(*cursor_id);
566 }
567
568 Event::Scroll { .. } | Event::SetViewport { .. } | Event::Recenter => {
571 tracing::warn!("View event {:?} reached EditorState.apply() - should be handled by SplitViewState", event);
574 }
575
576 Event::SetAnchor {
577 cursor_id,
578 position,
579 } => {
580 if let Some(cursor) = cursors.get_mut(*cursor_id) {
583 cursor.anchor = Some(*position);
584 cursor.deselect_on_move = false;
585 }
586 }
587
588 Event::ClearAnchor { cursor_id } => {
589 if let Some(cursor) = cursors.get_mut(*cursor_id) {
592 cursor.anchor = None;
593 cursor.deselect_on_move = true;
594 cursor.clear_block_selection();
595 }
596 }
597
598 Event::ChangeMode { mode } => {
599 self.mode = mode.clone();
600 }
601
602 Event::AddOverlay {
603 namespace,
604 range,
605 face,
606 priority,
607 message,
608 extend_to_line_end,
609 url,
610 } => {
611 tracing::trace!(
612 "AddOverlay: namespace={:?}, range={:?}, face={:?}, priority={}",
613 namespace,
614 range,
615 face,
616 priority
617 );
618 let overlay_face = convert_event_face_to_overlay_face(face);
620 tracing::trace!("Converted face: {:?}", overlay_face);
621
622 let mut overlay = Overlay::with_priority(
623 &mut self.marker_list,
624 range.clone(),
625 overlay_face,
626 *priority,
627 );
628 overlay.namespace = namespace.clone();
629 overlay.message = message.clone();
630 overlay.extend_to_line_end = *extend_to_line_end;
631 overlay.url = url.clone();
632
633 let actual_range = overlay.range(&self.marker_list);
634 tracing::trace!(
635 "Created overlay with markers - actual range: {:?}, handle={:?}",
636 actual_range,
637 overlay.handle
638 );
639
640 self.overlays.add(overlay);
641 }
642
643 Event::RemoveOverlay { handle } => {
644 tracing::trace!("RemoveOverlay: handle={:?}", handle);
645 self.overlays
646 .remove_by_handle(handle, &mut self.marker_list);
647 }
648
649 Event::RemoveOverlaysInRange { range } => {
650 self.overlays.remove_in_range(range, &mut self.marker_list);
651 }
652
653 Event::ClearNamespace { namespace } => {
654 tracing::trace!("ClearNamespace: namespace={:?}", namespace);
655 self.overlays
656 .clear_namespace(namespace, &mut self.marker_list);
657 }
658
659 Event::ClearOverlays => {
660 self.overlays.clear(&mut self.marker_list);
661 }
662
663 Event::ShowPopup { popup } => {
664 let popup_obj = convert_popup_data_to_popup(popup);
665 self.popups.show_or_replace(popup_obj);
666 }
667
668 Event::HidePopup => {
669 self.popups.hide();
670 }
671
672 Event::ClearPopups => {
673 self.popups.clear();
674 }
675
676 Event::PopupSelectNext => {
677 if let Some(popup) = self.popups.top_mut() {
678 popup.select_next();
679 }
680 }
681
682 Event::PopupSelectPrev => {
683 if let Some(popup) = self.popups.top_mut() {
684 popup.select_prev();
685 }
686 }
687
688 Event::PopupPageDown => {
689 if let Some(popup) = self.popups.top_mut() {
690 popup.page_down();
691 }
692 }
693
694 Event::PopupPageUp => {
695 if let Some(popup) = self.popups.top_mut() {
696 popup.page_up();
697 }
698 }
699
700 Event::AddMarginAnnotation {
701 line,
702 position,
703 content,
704 annotation_id,
705 } => {
706 let margin_position = convert_margin_position(position);
707 let margin_content = convert_margin_content(content);
708 let annotation = if let Some(id) = annotation_id {
709 MarginAnnotation::with_id(*line, margin_position, margin_content, id.clone())
710 } else {
711 MarginAnnotation::new(*line, margin_position, margin_content)
712 };
713 self.margins.add_annotation(annotation);
714 }
715
716 Event::RemoveMarginAnnotation { annotation_id } => {
717 self.margins.remove_by_id(annotation_id);
718 }
719
720 Event::RemoveMarginAnnotationsAtLine { line, position } => {
721 let margin_position = convert_margin_position(position);
722 self.margins.remove_at_line(*line, margin_position);
723 }
724
725 Event::ClearMarginPosition { position } => {
726 let margin_position = convert_margin_position(position);
727 self.margins.clear_position(margin_position);
728 }
729
730 Event::ClearMargins => {
731 self.margins.clear_all();
732 }
733
734 Event::SetLineNumbers { enabled } => {
735 self.margins.configure_for_line_numbers(*enabled);
736 }
737
738 Event::SplitPane { .. }
741 | Event::CloseSplit { .. }
742 | Event::SetActiveSplit { .. }
743 | Event::AdjustSplitRatio { .. }
744 | Event::NextSplit
745 | Event::PrevSplit => {
746 }
748
749 Event::Batch { events, .. } => {
750 for event in events {
753 self.apply(cursors, event);
754 }
755 }
756
757 Event::BulkEdit {
758 new_snapshot,
759 new_cursors,
760 edits,
761 displaced_markers,
762 ..
763 } => {
764 if let Some(snapshot) = new_snapshot {
770 self.buffer.restore_buffer_state(snapshot);
771 }
772
773 for &(pos, del_len, ins_len) in edits {
783 if del_len > 0 && ins_len > 0 {
784 if ins_len > del_len {
786 let net = ins_len - del_len;
787 self.marker_list.adjust_for_insert(pos, net);
788 self.margins.adjust_for_insert(pos, net);
789 } else if del_len > ins_len {
790 let net = del_len - ins_len;
791 self.marker_list.adjust_for_delete(pos, net);
792 self.margins.adjust_for_delete(pos, net);
793 }
794 } else if del_len > 0 {
796 self.marker_list.adjust_for_delete(pos, del_len);
797 self.margins.adjust_for_delete(pos, del_len);
798 } else if ins_len > 0 {
799 self.marker_list.adjust_for_insert(pos, ins_len);
800 self.margins.adjust_for_insert(pos, ins_len);
801 }
802 }
803
804 if !displaced_markers.is_empty() {
809 self.restore_displaced_markers(displaced_markers);
810 }
811
812 self.virtual_texts.clear(&mut self.marker_list);
815
816 use crate::view::overlay::OverlayNamespace;
817 let namespaces = ["lsp-diagnostic", "reference-highlight", "bracket-highlight"];
818 for ns in &namespaces {
819 self.overlays.clear_namespace(
820 &OverlayNamespace::from_string(ns.to_string()),
821 &mut self.marker_list,
822 );
823 }
824
825 for (cursor_id, position, anchor) in new_cursors {
827 if let Some(cursor) = cursors.get_mut(*cursor_id) {
828 cursor.position = *position;
829 cursor.anchor = *anchor;
830 }
831 }
832
833 self.highlighter.invalidate_all();
835
836 let primary_pos = cursors.primary().position;
838 self.primary_cursor_line_number = match self.buffer.offset_to_position(primary_pos)
839 {
840 Some(pos) => crate::model::buffer::LineNumber::Absolute(pos.line),
841 None => crate::model::buffer::LineNumber::Absolute(0),
842 };
843 }
844 }
845 }
846
847 pub fn capture_displaced_markers(&self, range: &Range<usize>) -> Vec<(u64, usize)> {
850 let mut displaced = Vec::new();
851 if range.is_empty() {
852 return displaced;
853 }
854 for (marker_id, start, _end) in self.marker_list.query_range(range.start, range.end) {
855 if start > range.start && start < range.end {
856 displaced.push(
857 DisplacedMarker::Main {
858 id: marker_id.0,
859 position: start,
860 }
861 .encode(),
862 );
863 }
864 }
865 for (marker_id, start, _end) in self.margins.query_indicator_range(range.start, range.end) {
866 if start > range.start && start < range.end {
867 displaced.push(
868 DisplacedMarker::Margin {
869 id: marker_id.0,
870 position: start,
871 }
872 .encode(),
873 );
874 }
875 }
876 displaced
877 }
878
879 pub fn capture_displaced_markers_bulk(
881 &self,
882 edits: &[(usize, usize, String)],
883 ) -> Vec<(u64, usize)> {
884 let mut displaced = Vec::new();
885 for (pos, del_len, _text) in edits {
886 if *del_len > 0 {
887 displaced.extend(self.capture_displaced_markers(&(*pos..*pos + *del_len)));
888 }
889 }
890 displaced
891 }
892
893 pub fn restore_displaced_markers(&mut self, displaced: &[(u64, usize)]) {
895 for &(tagged_id, original_pos) in displaced {
896 let dm = DisplacedMarker::decode(tagged_id, original_pos);
897 match dm {
898 DisplacedMarker::Main { id, position } => {
899 self.marker_list.set_position(MarkerId(id), position);
900 }
901 DisplacedMarker::Margin { id, position } => {
902 self.margins.set_indicator_position(MarkerId(id), position);
903 }
904 }
905 }
906 }
907
908 pub fn apply_many(&mut self, cursors: &mut Cursors, events: &[Event]) {
910 for event in events {
911 self.apply(cursors, event);
912 }
913 }
914
915 pub fn on_focus_lost(&mut self) {
919 if self.popups.dismiss_transient() {
920 tracing::debug!("Dismissed transient popup on buffer focus loss");
921 }
922 }
923}
924
925fn convert_event_face_to_overlay_face(event_face: &EventOverlayFace) -> OverlayFace {
927 match event_face {
928 EventOverlayFace::Underline { color, style } => {
929 let underline_style = match style {
930 crate::model::event::UnderlineStyle::Straight => UnderlineStyle::Straight,
931 crate::model::event::UnderlineStyle::Wavy => UnderlineStyle::Wavy,
932 crate::model::event::UnderlineStyle::Dotted => UnderlineStyle::Dotted,
933 crate::model::event::UnderlineStyle::Dashed => UnderlineStyle::Dashed,
934 };
935 OverlayFace::Underline {
936 color: Color::Rgb(color.0, color.1, color.2),
937 style: underline_style,
938 }
939 }
940 EventOverlayFace::Background { color } => OverlayFace::Background {
941 color: Color::Rgb(color.0, color.1, color.2),
942 },
943 EventOverlayFace::Foreground { color } => OverlayFace::Foreground {
944 color: Color::Rgb(color.0, color.1, color.2),
945 },
946 EventOverlayFace::Style { options } => {
947 use crate::view::theme::named_color_from_str;
948 use ratatui::style::Modifier;
949
950 let mut style = Style::default();
952
953 if let Some(ref fg) = options.fg {
955 if let Some((r, g, b)) = fg.as_rgb() {
956 style = style.fg(Color::Rgb(r, g, b));
957 } else if let Some(key) = fg.as_theme_key() {
958 if let Some(color) = named_color_from_str(key) {
959 style = style.fg(color);
960 }
961 }
962 }
963
964 if let Some(ref bg) = options.bg {
966 if let Some((r, g, b)) = bg.as_rgb() {
967 style = style.bg(Color::Rgb(r, g, b));
968 } else if let Some(key) = bg.as_theme_key() {
969 if let Some(color) = named_color_from_str(key) {
970 style = style.bg(color);
971 }
972 }
973 }
974
975 let mut modifiers = Modifier::empty();
977 if options.bold {
978 modifiers |= Modifier::BOLD;
979 }
980 if options.italic {
981 modifiers |= Modifier::ITALIC;
982 }
983 if options.underline {
984 modifiers |= Modifier::UNDERLINED;
985 }
986 if options.strikethrough {
987 modifiers |= Modifier::CROSSED_OUT;
988 }
989 if !modifiers.is_empty() {
990 style = style.add_modifier(modifiers);
991 }
992
993 let fg_theme = options
995 .fg
996 .as_ref()
997 .and_then(|c| c.as_theme_key())
998 .filter(|key| named_color_from_str(key).is_none())
999 .map(String::from);
1000 let bg_theme = options
1001 .bg
1002 .as_ref()
1003 .and_then(|c| c.as_theme_key())
1004 .filter(|key| named_color_from_str(key).is_none())
1005 .map(String::from);
1006
1007 if fg_theme.is_some() || bg_theme.is_some() {
1009 OverlayFace::ThemedStyle {
1010 fallback_style: style,
1011 fg_theme,
1012 bg_theme,
1013 }
1014 } else {
1015 OverlayFace::Style { style }
1016 }
1017 }
1018 }
1019}
1020
1021pub(crate) fn convert_popup_data_to_popup(data: &PopupData) -> Popup {
1023 let content = match &data.content {
1024 crate::model::event::PopupContentData::Text(lines) => PopupContent::Text(lines.clone()),
1025 crate::model::event::PopupContentData::List { items, selected } => PopupContent::List {
1026 items: items
1027 .iter()
1028 .map(|item| PopupListItem {
1029 text: item.text.clone(),
1030 detail: item.detail.clone(),
1031 icon: item.icon.clone(),
1032 data: item.data.clone(),
1033 })
1034 .collect(),
1035 selected: *selected,
1036 },
1037 };
1038
1039 let position = match data.position {
1040 PopupPositionData::AtCursor => PopupPosition::AtCursor,
1041 PopupPositionData::BelowCursor => PopupPosition::BelowCursor,
1042 PopupPositionData::AboveCursor => PopupPosition::AboveCursor,
1043 PopupPositionData::Fixed { x, y } => PopupPosition::Fixed { x, y },
1044 PopupPositionData::Centered => PopupPosition::Centered,
1045 PopupPositionData::BottomRight => PopupPosition::BottomRight,
1046 };
1047
1048 let kind = match data.kind {
1050 crate::model::event::PopupKindHint::Completion => PopupKind::Completion,
1051 crate::model::event::PopupKindHint::List => PopupKind::List,
1052 crate::model::event::PopupKindHint::Text => PopupKind::Text,
1053 };
1054
1055 Popup {
1056 kind,
1057 title: data.title.clone(),
1058 description: data.description.clone(),
1059 transient: data.transient,
1060 content,
1061 position,
1062 width: data.width,
1063 max_height: data.max_height,
1064 bordered: data.bordered,
1065 border_style: Style::default().fg(Color::Gray),
1066 background_style: Style::default().bg(Color::Rgb(30, 30, 30)),
1067 scroll_offset: 0,
1068 text_selection: None,
1069 accept_key_hint: None,
1070 }
1071}
1072
1073fn convert_margin_position(position: &MarginPositionData) -> MarginPosition {
1075 match position {
1076 MarginPositionData::Left => MarginPosition::Left,
1077 MarginPositionData::Right => MarginPosition::Right,
1078 }
1079}
1080
1081fn convert_margin_content(content: &MarginContentData) -> MarginContent {
1083 match content {
1084 MarginContentData::Text(text) => MarginContent::Text(text.clone()),
1085 MarginContentData::Symbol { text, color } => {
1086 if let Some((r, g, b)) = color {
1087 MarginContent::colored_symbol(text.clone(), Color::Rgb(*r, *g, *b))
1088 } else {
1089 MarginContent::symbol(text.clone(), Style::default())
1090 }
1091 }
1092 MarginContentData::Empty => MarginContent::Empty,
1093 }
1094}
1095
1096impl EditorState {
1097 pub fn prepare_for_render(&mut self, top_byte: usize, height: u16) -> Result<()> {
1104 self.buffer.prepare_viewport(top_byte, height as usize)?;
1105 Ok(())
1106 }
1107
1108 pub fn get_text_range(&mut self, start: usize, end: usize) -> String {
1128 match self
1130 .buffer
1131 .get_text_range_mut(start, end.saturating_sub(start))
1132 {
1133 Ok(bytes) => String::from_utf8_lossy(&bytes).into_owned(),
1134 Err(e) => {
1135 tracing::warn!("Failed to get text range {}..{}: {}", start, end, e);
1136 String::new()
1137 }
1138 }
1139 }
1140
1141 pub fn get_line_at_offset(&mut self, offset: usize) -> Option<(usize, String)> {
1149 use crate::model::document_model::DocumentModel;
1150
1151 let mut line_start = offset;
1154 while line_start > 0 {
1155 if let Ok(text) = self.buffer.get_text_range_mut(line_start - 1, 1) {
1156 if text.first() == Some(&b'\n') {
1157 break;
1158 }
1159 line_start -= 1;
1160 } else {
1161 break;
1162 }
1163 }
1164
1165 let viewport = self
1167 .get_viewport_content(
1168 crate::model::document_model::DocumentPosition::byte(line_start),
1169 1,
1170 )
1171 .ok()?;
1172
1173 viewport
1174 .lines
1175 .first()
1176 .map(|line| (line.byte_offset, line.content.clone()))
1177 }
1178
1179 pub fn get_text_to_end_of_line(&mut self, cursor_pos: usize) -> Result<String> {
1184 use crate::model::document_model::DocumentModel;
1185
1186 let viewport = self.get_viewport_content(
1188 crate::model::document_model::DocumentPosition::byte(cursor_pos),
1189 1,
1190 )?;
1191
1192 if let Some(line) = viewport.lines.first() {
1193 let line_start = line.byte_offset;
1194 let line_end = line_start + line.content.len();
1195
1196 if cursor_pos >= line_start && cursor_pos <= line_end {
1197 let offset_in_line = cursor_pos - line_start;
1198 Ok(line.content.get(offset_in_line..).unwrap_or("").to_string())
1200 } else {
1201 Ok(String::new())
1202 }
1203 } else {
1204 Ok(String::new())
1205 }
1206 }
1207
1208 pub fn set_semantic_tokens(&mut self, store: SemanticTokenStore) {
1210 self.semantic_tokens = Some(store);
1211 }
1212
1213 pub fn clear_semantic_tokens(&mut self) {
1215 self.semantic_tokens = None;
1216 }
1217
1218 pub fn semantic_tokens_result_id(&self) -> Option<&str> {
1220 self.semantic_tokens
1221 .as_ref()
1222 .and_then(|store| store.result_id.as_deref())
1223 }
1224}
1225
1226impl DocumentModel for EditorState {
1231 fn capabilities(&self) -> DocumentCapabilities {
1232 let line_count = self.buffer.line_count();
1233 DocumentCapabilities {
1234 has_line_index: line_count.is_some(),
1235 uses_lazy_loading: false, byte_length: self.buffer.len(),
1237 approximate_line_count: line_count.unwrap_or_else(|| {
1238 self.buffer.len() / 80
1240 }),
1241 }
1242 }
1243
1244 fn get_viewport_content(
1245 &mut self,
1246 start_pos: DocumentPosition,
1247 max_lines: usize,
1248 ) -> Result<ViewportContent> {
1249 let start_offset = self.position_to_offset(start_pos)?;
1251
1252 let line_iter = self.buffer.iter_lines_from(start_offset, max_lines)?;
1255 let has_more = line_iter.has_more;
1256
1257 let lines = line_iter
1258 .map(|line_data| ViewportLine {
1259 byte_offset: line_data.byte_offset,
1260 content: line_data.content,
1261 has_newline: line_data.has_newline,
1262 approximate_line_number: line_data.line_number,
1263 })
1264 .collect();
1265
1266 Ok(ViewportContent {
1267 start_position: DocumentPosition::ByteOffset(start_offset),
1268 lines,
1269 has_more,
1270 })
1271 }
1272
1273 fn position_to_offset(&self, pos: DocumentPosition) -> Result<usize> {
1274 match pos {
1275 DocumentPosition::ByteOffset(offset) => Ok(offset),
1276 DocumentPosition::LineColumn { line, column } => {
1277 if !self.has_line_index() {
1278 anyhow::bail!("Line indexing not available for this document");
1279 }
1280 let position = crate::model::piece_tree::Position { line, column };
1282 Ok(self.buffer.position_to_offset(position))
1283 }
1284 }
1285 }
1286
1287 fn offset_to_position(&self, offset: usize) -> DocumentPosition {
1288 if self.has_line_index() {
1289 if let Some(pos) = self.buffer.offset_to_position(offset) {
1290 DocumentPosition::LineColumn {
1291 line: pos.line,
1292 column: pos.column,
1293 }
1294 } else {
1295 DocumentPosition::ByteOffset(offset)
1297 }
1298 } else {
1299 DocumentPosition::ByteOffset(offset)
1300 }
1301 }
1302
1303 fn get_range(&mut self, start: DocumentPosition, end: DocumentPosition) -> Result<String> {
1304 let start_offset = self.position_to_offset(start)?;
1305 let end_offset = self.position_to_offset(end)?;
1306
1307 if start_offset > end_offset {
1308 anyhow::bail!(
1309 "Invalid range: start offset {} > end offset {}",
1310 start_offset,
1311 end_offset
1312 );
1313 }
1314
1315 let bytes = self
1316 .buffer
1317 .get_text_range_mut(start_offset, end_offset - start_offset)?;
1318
1319 Ok(String::from_utf8_lossy(&bytes).into_owned())
1320 }
1321
1322 fn get_line_content(&mut self, line_number: usize) -> Option<String> {
1323 if !self.has_line_index() {
1324 return None;
1325 }
1326
1327 let line_start_offset = self.buffer.line_start_offset(line_number)?;
1329
1330 let mut iter = self.buffer.line_iterator(line_start_offset, 80);
1332 if let Some((_start, content)) = iter.next_line() {
1333 let has_newline = content.ends_with('\n');
1334 let line_content = if has_newline {
1335 content[..content.len() - 1].to_string()
1336 } else {
1337 content
1338 };
1339 Some(line_content)
1340 } else {
1341 None
1342 }
1343 }
1344
1345 fn get_chunk_at_offset(&mut self, offset: usize, size: usize) -> Result<(usize, String)> {
1346 let bytes = self.buffer.get_text_range_mut(offset, size)?;
1347
1348 Ok((offset, String::from_utf8_lossy(&bytes).into_owned()))
1349 }
1350
1351 fn insert(&mut self, pos: DocumentPosition, text: &str) -> Result<usize> {
1352 let offset = self.position_to_offset(pos)?;
1353 self.buffer.insert_bytes(offset, text.as_bytes().to_vec());
1354 Ok(text.len())
1355 }
1356
1357 fn delete(&mut self, start: DocumentPosition, end: DocumentPosition) -> Result<()> {
1358 let start_offset = self.position_to_offset(start)?;
1359 let end_offset = self.position_to_offset(end)?;
1360
1361 if start_offset > end_offset {
1362 anyhow::bail!(
1363 "Invalid range: start offset {} > end offset {}",
1364 start_offset,
1365 end_offset
1366 );
1367 }
1368
1369 self.buffer.delete(start_offset..end_offset);
1370 Ok(())
1371 }
1372
1373 fn replace(
1374 &mut self,
1375 start: DocumentPosition,
1376 end: DocumentPosition,
1377 text: &str,
1378 ) -> Result<()> {
1379 self.delete(start, end)?;
1381 self.insert(start, text)?;
1382 Ok(())
1383 }
1384
1385 fn find_matches(
1386 &mut self,
1387 pattern: &str,
1388 search_range: Option<(DocumentPosition, DocumentPosition)>,
1389 ) -> Result<Vec<usize>> {
1390 let (start_offset, end_offset) = if let Some((start, end)) = search_range {
1391 (
1392 self.position_to_offset(start)?,
1393 self.position_to_offset(end)?,
1394 )
1395 } else {
1396 (0, self.buffer.len())
1397 };
1398
1399 let bytes = self
1401 .buffer
1402 .get_text_range_mut(start_offset, end_offset - start_offset)?;
1403 let text = String::from_utf8_lossy(&bytes);
1404
1405 let mut matches = Vec::new();
1407 let mut search_offset = 0;
1408 while let Some(pos) = text[search_offset..].find(pattern) {
1409 matches.push(start_offset + search_offset + pos);
1410 search_offset += pos + pattern.len();
1411 }
1412
1413 Ok(matches)
1414 }
1415}
1416
1417#[derive(Clone, Debug)]
1419pub struct SemanticTokenStore {
1420 pub version: u64,
1422 pub result_id: Option<String>,
1424 pub data: Vec<u32>,
1426 pub tokens: Vec<SemanticTokenSpan>,
1428}
1429
1430#[derive(Clone, Debug)]
1432pub struct SemanticTokenSpan {
1433 pub range: Range<usize>,
1434 pub token_type: String,
1435 pub modifiers: Vec<String>,
1436}
1437
1438#[cfg(test)]
1439mod tests {
1440 use crate::model::filesystem::StdFileSystem;
1441 use std::sync::Arc;
1442
1443 fn test_fs() -> Arc<dyn crate::model::filesystem::FileSystem + Send + Sync> {
1444 Arc::new(StdFileSystem)
1445 }
1446 use super::*;
1447 use crate::model::event::CursorId;
1448
1449 #[test]
1450 fn test_state_new() {
1451 let state = EditorState::new(
1452 80,
1453 24,
1454 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1455 test_fs(),
1456 );
1457 assert!(state.buffer.is_empty());
1458 }
1459
1460 #[test]
1461 fn test_apply_insert() {
1462 let mut state = EditorState::new(
1463 80,
1464 24,
1465 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1466 test_fs(),
1467 );
1468 let mut cursors = Cursors::new();
1469 let cursor_id = cursors.primary_id();
1470
1471 state.apply(
1472 &mut cursors,
1473 &Event::Insert {
1474 position: 0,
1475 text: "hello".to_string(),
1476 cursor_id,
1477 },
1478 );
1479
1480 assert_eq!(state.buffer.to_string().unwrap(), "hello");
1481 assert_eq!(cursors.primary().position, 5);
1482 assert!(state.buffer.is_modified());
1483 }
1484
1485 #[test]
1486 fn test_apply_delete() {
1487 let mut state = EditorState::new(
1488 80,
1489 24,
1490 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1491 test_fs(),
1492 );
1493 let mut cursors = Cursors::new();
1494 let cursor_id = cursors.primary_id();
1495
1496 state.apply(
1498 &mut cursors,
1499 &Event::Insert {
1500 position: 0,
1501 text: "hello world".to_string(),
1502 cursor_id,
1503 },
1504 );
1505
1506 state.apply(
1507 &mut cursors,
1508 &Event::Delete {
1509 range: 5..11,
1510 deleted_text: " world".to_string(),
1511 cursor_id,
1512 },
1513 );
1514
1515 assert_eq!(state.buffer.to_string().unwrap(), "hello");
1516 assert_eq!(cursors.primary().position, 5);
1517 }
1518
1519 #[test]
1520 fn test_apply_move_cursor() {
1521 let mut state = EditorState::new(
1522 80,
1523 24,
1524 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1525 test_fs(),
1526 );
1527 let mut cursors = Cursors::new();
1528 let cursor_id = cursors.primary_id();
1529
1530 state.apply(
1531 &mut cursors,
1532 &Event::Insert {
1533 position: 0,
1534 text: "hello".to_string(),
1535 cursor_id,
1536 },
1537 );
1538
1539 state.apply(
1540 &mut cursors,
1541 &Event::MoveCursor {
1542 cursor_id,
1543 old_position: 5,
1544 new_position: 2,
1545 old_anchor: None,
1546 new_anchor: None,
1547 old_sticky_column: 0,
1548 new_sticky_column: 0,
1549 },
1550 );
1551
1552 assert_eq!(cursors.primary().position, 2);
1553 }
1554
1555 #[test]
1556 fn test_apply_add_cursor() {
1557 let mut state = EditorState::new(
1558 80,
1559 24,
1560 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1561 test_fs(),
1562 );
1563 let mut cursors = Cursors::new();
1564 let cursor_id = CursorId(1);
1565
1566 state.apply(
1567 &mut cursors,
1568 &Event::AddCursor {
1569 cursor_id,
1570 position: 5,
1571 anchor: None,
1572 },
1573 );
1574
1575 assert_eq!(cursors.count(), 2);
1576 }
1577
1578 #[test]
1579 fn test_apply_many() {
1580 let mut state = EditorState::new(
1581 80,
1582 24,
1583 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1584 test_fs(),
1585 );
1586 let mut cursors = Cursors::new();
1587 let cursor_id = cursors.primary_id();
1588
1589 let events = vec![
1590 Event::Insert {
1591 position: 0,
1592 text: "hello ".to_string(),
1593 cursor_id,
1594 },
1595 Event::Insert {
1596 position: 6,
1597 text: "world".to_string(),
1598 cursor_id,
1599 },
1600 ];
1601
1602 state.apply_many(&mut cursors, &events);
1603
1604 assert_eq!(state.buffer.to_string().unwrap(), "hello world");
1605 }
1606
1607 #[test]
1608 fn test_cursor_adjustment_after_insert() {
1609 let mut state = EditorState::new(
1610 80,
1611 24,
1612 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1613 test_fs(),
1614 );
1615 let mut cursors = Cursors::new();
1616 let cursor_id = cursors.primary_id();
1617
1618 state.apply(
1620 &mut cursors,
1621 &Event::AddCursor {
1622 cursor_id: CursorId(1),
1623 position: 5,
1624 anchor: None,
1625 },
1626 );
1627
1628 state.apply(
1630 &mut cursors,
1631 &Event::Insert {
1632 position: 0,
1633 text: "abc".to_string(),
1634 cursor_id,
1635 },
1636 );
1637
1638 if let Some(cursor) = cursors.get(CursorId(1)) {
1640 assert_eq!(cursor.position, 8);
1641 }
1642 }
1643
1644 mod document_model_tests {
1646 use super::*;
1647 use crate::model::document_model::{DocumentModel, DocumentPosition};
1648
1649 #[test]
1650 fn test_capabilities_small_file() {
1651 let mut state = EditorState::new(
1652 80,
1653 24,
1654 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1655 test_fs(),
1656 );
1657 state.buffer = Buffer::from_str_test("line1\nline2\nline3");
1658
1659 let caps = state.capabilities();
1660 assert!(caps.has_line_index, "Small file should have line index");
1661 assert_eq!(caps.byte_length, "line1\nline2\nline3".len());
1662 assert_eq!(caps.approximate_line_count, 3, "Should have 3 lines");
1663 }
1664
1665 #[test]
1666 fn test_position_conversions() {
1667 let mut state = EditorState::new(
1668 80,
1669 24,
1670 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1671 test_fs(),
1672 );
1673 state.buffer = Buffer::from_str_test("hello\nworld\ntest");
1674
1675 let pos1 = DocumentPosition::ByteOffset(6);
1677 let offset1 = state.position_to_offset(pos1).unwrap();
1678 assert_eq!(offset1, 6);
1679
1680 let pos2 = DocumentPosition::LineColumn { line: 1, column: 0 };
1682 let offset2 = state.position_to_offset(pos2).unwrap();
1683 assert_eq!(offset2, 6, "Line 1, column 0 should be at byte 6");
1684
1685 let converted = state.offset_to_position(6);
1687 match converted {
1688 DocumentPosition::LineColumn { line, column } => {
1689 assert_eq!(line, 1);
1690 assert_eq!(column, 0);
1691 }
1692 _ => panic!("Expected LineColumn for small file"),
1693 }
1694 }
1695
1696 #[test]
1697 fn test_get_viewport_content() {
1698 let mut state = EditorState::new(
1699 80,
1700 24,
1701 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1702 test_fs(),
1703 );
1704 state.buffer = Buffer::from_str_test("line1\nline2\nline3\nline4\nline5");
1705
1706 let content = state
1707 .get_viewport_content(DocumentPosition::ByteOffset(0), 3)
1708 .unwrap();
1709
1710 assert_eq!(content.lines.len(), 3);
1711 assert_eq!(content.lines[0].content, "line1");
1712 assert_eq!(content.lines[1].content, "line2");
1713 assert_eq!(content.lines[2].content, "line3");
1714 assert!(content.has_more);
1715 }
1716
1717 #[test]
1718 fn test_get_range() {
1719 let mut state = EditorState::new(
1720 80,
1721 24,
1722 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1723 test_fs(),
1724 );
1725 state.buffer = Buffer::from_str_test("hello world");
1726
1727 let text = state
1728 .get_range(
1729 DocumentPosition::ByteOffset(0),
1730 DocumentPosition::ByteOffset(5),
1731 )
1732 .unwrap();
1733 assert_eq!(text, "hello");
1734
1735 let text2 = state
1736 .get_range(
1737 DocumentPosition::ByteOffset(6),
1738 DocumentPosition::ByteOffset(11),
1739 )
1740 .unwrap();
1741 assert_eq!(text2, "world");
1742 }
1743
1744 #[test]
1745 fn test_get_line_content() {
1746 let mut state = EditorState::new(
1747 80,
1748 24,
1749 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1750 test_fs(),
1751 );
1752 state.buffer = Buffer::from_str_test("line1\nline2\nline3");
1753
1754 let line0 = state.get_line_content(0).unwrap();
1755 assert_eq!(line0, "line1");
1756
1757 let line1 = state.get_line_content(1).unwrap();
1758 assert_eq!(line1, "line2");
1759
1760 let line2 = state.get_line_content(2).unwrap();
1761 assert_eq!(line2, "line3");
1762 }
1763
1764 #[test]
1765 fn test_insert_delete() {
1766 let mut state = EditorState::new(
1767 80,
1768 24,
1769 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1770 test_fs(),
1771 );
1772 state.buffer = Buffer::from_str_test("hello world");
1773
1774 let bytes_inserted = state
1776 .insert(DocumentPosition::ByteOffset(6), "beautiful ")
1777 .unwrap();
1778 assert_eq!(bytes_inserted, 10);
1779 assert_eq!(state.buffer.to_string().unwrap(), "hello beautiful world");
1780
1781 state
1783 .delete(
1784 DocumentPosition::ByteOffset(6),
1785 DocumentPosition::ByteOffset(16),
1786 )
1787 .unwrap();
1788 assert_eq!(state.buffer.to_string().unwrap(), "hello world");
1789 }
1790
1791 #[test]
1792 fn test_replace() {
1793 let mut state = EditorState::new(
1794 80,
1795 24,
1796 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1797 test_fs(),
1798 );
1799 state.buffer = Buffer::from_str_test("hello world");
1800
1801 state
1802 .replace(
1803 DocumentPosition::ByteOffset(0),
1804 DocumentPosition::ByteOffset(5),
1805 "hi",
1806 )
1807 .unwrap();
1808 assert_eq!(state.buffer.to_string().unwrap(), "hi world");
1809 }
1810
1811 #[test]
1812 fn test_find_matches() {
1813 let mut state = EditorState::new(
1814 80,
1815 24,
1816 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1817 test_fs(),
1818 );
1819 state.buffer = Buffer::from_str_test("hello world hello");
1820
1821 let matches = state.find_matches("hello", None).unwrap();
1822 assert_eq!(matches.len(), 2);
1823 assert_eq!(matches[0], 0);
1824 assert_eq!(matches[1], 12);
1825 }
1826
1827 #[test]
1828 fn test_prepare_for_render() {
1829 let mut state = EditorState::new(
1830 80,
1831 24,
1832 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1833 test_fs(),
1834 );
1835 state.buffer = Buffer::from_str_test("line1\nline2\nline3\nline4\nline5");
1836
1837 state.prepare_for_render(0, 24).unwrap();
1839 }
1840
1841 #[test]
1842 fn test_helper_get_text_range() {
1843 let mut state = EditorState::new(
1844 80,
1845 24,
1846 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1847 test_fs(),
1848 );
1849 state.buffer = Buffer::from_str_test("hello world");
1850
1851 let text = state.get_text_range(0, 5);
1853 assert_eq!(text, "hello");
1854
1855 let text2 = state.get_text_range(6, 11);
1857 assert_eq!(text2, "world");
1858 }
1859
1860 #[test]
1861 fn test_helper_get_line_at_offset() {
1862 let mut state = EditorState::new(
1863 80,
1864 24,
1865 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1866 test_fs(),
1867 );
1868 state.buffer = Buffer::from_str_test("line1\nline2\nline3");
1869
1870 let (offset, content) = state.get_line_at_offset(0).unwrap();
1872 assert_eq!(offset, 0);
1873 assert_eq!(content, "line1");
1874
1875 let (offset2, content2) = state.get_line_at_offset(8).unwrap();
1877 assert_eq!(offset2, 6); assert_eq!(content2, "line2");
1879
1880 let (offset3, content3) = state.get_line_at_offset(12).unwrap();
1882 assert_eq!(offset3, 12);
1883 assert_eq!(content3, "line3");
1884 }
1885
1886 #[test]
1887 fn test_helper_get_text_to_end_of_line() {
1888 let mut state = EditorState::new(
1889 80,
1890 24,
1891 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1892 test_fs(),
1893 );
1894 state.buffer = Buffer::from_str_test("hello world\nline2");
1895
1896 let text = state.get_text_to_end_of_line(0).unwrap();
1898 assert_eq!(text, "hello world");
1899
1900 let text2 = state.get_text_to_end_of_line(6).unwrap();
1902 assert_eq!(text2, "world");
1903
1904 let text3 = state.get_text_to_end_of_line(11).unwrap();
1906 assert_eq!(text3, "");
1907
1908 let text4 = state.get_text_to_end_of_line(12).unwrap();
1910 assert_eq!(text4, "line2");
1911 }
1912 }
1913
1914 mod virtual_text_integration_tests {
1916 use super::*;
1917 use crate::view::virtual_text::VirtualTextPosition;
1918 use ratatui::style::Style;
1919
1920 #[test]
1921 fn test_virtual_text_add_and_query() {
1922 let mut state = EditorState::new(
1923 80,
1924 24,
1925 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1926 test_fs(),
1927 );
1928 state.buffer = Buffer::from_str_test("hello world");
1929
1930 if !state.buffer.is_empty() {
1932 state.marker_list.adjust_for_insert(0, state.buffer.len());
1933 }
1934
1935 let vtext_id = state.virtual_texts.add(
1937 &mut state.marker_list,
1938 5,
1939 ": string".to_string(),
1940 Style::default(),
1941 VirtualTextPosition::AfterChar,
1942 0,
1943 );
1944
1945 let results = state.virtual_texts.query_range(&state.marker_list, 0, 11);
1947 assert_eq!(results.len(), 1);
1948 assert_eq!(results[0].0, 5); assert_eq!(results[0].1.text, ": string");
1950
1951 let lookup = state.virtual_texts.build_lookup(&state.marker_list, 0, 11);
1953 assert!(lookup.contains_key(&5));
1954 assert_eq!(lookup[&5].len(), 1);
1955 assert_eq!(lookup[&5][0].text, ": string");
1956
1957 state.virtual_texts.remove(&mut state.marker_list, vtext_id);
1959 assert!(state.virtual_texts.is_empty());
1960 }
1961
1962 #[test]
1963 fn test_virtual_text_position_tracking_on_insert() {
1964 let mut state = EditorState::new(
1965 80,
1966 24,
1967 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1968 test_fs(),
1969 );
1970 state.buffer = Buffer::from_str_test("hello world");
1971
1972 if !state.buffer.is_empty() {
1974 state.marker_list.adjust_for_insert(0, state.buffer.len());
1975 }
1976
1977 let _vtext_id = state.virtual_texts.add(
1979 &mut state.marker_list,
1980 6,
1981 "/*param*/".to_string(),
1982 Style::default(),
1983 VirtualTextPosition::BeforeChar,
1984 0,
1985 );
1986
1987 let mut cursors = Cursors::new();
1989 let cursor_id = cursors.primary_id();
1990 state.apply(
1991 &mut cursors,
1992 &Event::Insert {
1993 position: 6,
1994 text: "beautiful ".to_string(),
1995 cursor_id,
1996 },
1997 );
1998
1999 let results = state.virtual_texts.query_range(&state.marker_list, 0, 30);
2001 assert_eq!(results.len(), 1);
2002 assert_eq!(results[0].0, 16); assert_eq!(results[0].1.text, "/*param*/");
2004 }
2005
2006 #[test]
2007 fn test_virtual_text_position_tracking_on_delete() {
2008 let mut state = EditorState::new(
2009 80,
2010 24,
2011 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
2012 test_fs(),
2013 );
2014 state.buffer = Buffer::from_str_test("hello beautiful world");
2015
2016 if !state.buffer.is_empty() {
2018 state.marker_list.adjust_for_insert(0, state.buffer.len());
2019 }
2020
2021 let _vtext_id = state.virtual_texts.add(
2023 &mut state.marker_list,
2024 16,
2025 ": string".to_string(),
2026 Style::default(),
2027 VirtualTextPosition::AfterChar,
2028 0,
2029 );
2030
2031 let mut cursors = Cursors::new();
2033 let cursor_id = cursors.primary_id();
2034 state.apply(
2035 &mut cursors,
2036 &Event::Delete {
2037 range: 6..16,
2038 deleted_text: "beautiful ".to_string(),
2039 cursor_id,
2040 },
2041 );
2042
2043 let results = state.virtual_texts.query_range(&state.marker_list, 0, 20);
2045 assert_eq!(results.len(), 1);
2046 assert_eq!(results[0].0, 6); assert_eq!(results[0].1.text, ": string");
2048 }
2049
2050 #[test]
2051 fn test_multiple_virtual_texts_with_priorities() {
2052 let mut state = EditorState::new(
2053 80,
2054 24,
2055 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
2056 test_fs(),
2057 );
2058 state.buffer = Buffer::from_str_test("let x = 5");
2059
2060 if !state.buffer.is_empty() {
2062 state.marker_list.adjust_for_insert(0, state.buffer.len());
2063 }
2064
2065 state.virtual_texts.add(
2067 &mut state.marker_list,
2068 5,
2069 ": i32".to_string(),
2070 Style::default(),
2071 VirtualTextPosition::AfterChar,
2072 0, );
2074
2075 state.virtual_texts.add(
2077 &mut state.marker_list,
2078 5,
2079 " /* inferred */".to_string(),
2080 Style::default(),
2081 VirtualTextPosition::AfterChar,
2082 10, );
2084
2085 let lookup = state.virtual_texts.build_lookup(&state.marker_list, 0, 10);
2087 assert!(lookup.contains_key(&5));
2088 let vtexts = &lookup[&5];
2089 assert_eq!(vtexts.len(), 2);
2090 assert_eq!(vtexts[0].text, ": i32");
2092 assert_eq!(vtexts[1].text, " /* inferred */");
2093 }
2094
2095 #[test]
2096 fn test_virtual_text_clear() {
2097 let mut state = EditorState::new(
2098 80,
2099 24,
2100 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
2101 test_fs(),
2102 );
2103 state.buffer = Buffer::from_str_test("test");
2104
2105 if !state.buffer.is_empty() {
2107 state.marker_list.adjust_for_insert(0, state.buffer.len());
2108 }
2109
2110 state.virtual_texts.add(
2112 &mut state.marker_list,
2113 0,
2114 "hint1".to_string(),
2115 Style::default(),
2116 VirtualTextPosition::BeforeChar,
2117 0,
2118 );
2119 state.virtual_texts.add(
2120 &mut state.marker_list,
2121 2,
2122 "hint2".to_string(),
2123 Style::default(),
2124 VirtualTextPosition::AfterChar,
2125 0,
2126 );
2127
2128 assert_eq!(state.virtual_texts.len(), 2);
2129
2130 state.virtual_texts.clear(&mut state.marker_list);
2132 assert!(state.virtual_texts.is_empty());
2133
2134 let results = state.virtual_texts.query_range(&state.marker_list, 0, 10);
2136 assert!(results.is_empty());
2137 }
2138 }
2139}