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::marker::MarkerList;
11use crate::primitives::grammar::GrammarRegistry;
12use crate::primitives::highlight_engine::HighlightEngine;
13use crate::primitives::highlighter::Language;
14use crate::primitives::indent::IndentCalculator;
15use crate::primitives::reference_highlighter::ReferenceHighlighter;
16use crate::primitives::text_property::TextPropertyManager;
17use crate::view::margin::{MarginAnnotation, MarginContent, MarginManager, MarginPosition};
18use crate::view::overlay::{Overlay, OverlayFace, OverlayManager, UnderlineStyle};
19use crate::view::popup::{Popup, PopupContent, PopupListItem, PopupManager, PopupPosition};
20use crate::view::reference_highlight_overlay::ReferenceHighlightOverlay;
21use crate::view::virtual_text::VirtualTextManager;
22use anyhow::Result;
23use ratatui::style::{Color, Style};
24use std::cell::RefCell;
25use std::ops::Range;
26
27#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum ViewMode {
30 Source,
32 Compose,
34}
35
36pub struct EditorState {
42 pub buffer: Buffer,
44
45 pub cursors: Cursors,
47
48 pub highlighter: HighlightEngine,
50
51 pub indent_calculator: RefCell<IndentCalculator>,
53
54 pub overlays: OverlayManager,
56
57 pub marker_list: MarkerList,
59
60 pub virtual_texts: VirtualTextManager,
62
63 pub popups: PopupManager,
65
66 pub margins: MarginManager,
68
69 pub primary_cursor_line_number: LineNumber,
72
73 pub mode: String,
75
76 pub text_properties: TextPropertyManager,
79
80 pub show_cursors: bool,
83
84 pub editing_disabled: bool,
88
89 pub is_composite_buffer: bool,
93
94 pub show_whitespace_tabs: bool,
97
98 pub use_tabs: bool,
101
102 pub tab_size: usize,
105
106 pub reference_highlighter: ReferenceHighlighter,
108
109 pub view_mode: ViewMode,
111
112 pub debug_highlight_mode: bool,
115
116 pub compose_width: Option<u16>,
118
119 pub compose_prev_line_numbers: Option<bool>,
121
122 pub compose_column_guides: Option<Vec<u16>>,
124
125 pub view_transform: Option<fresh_core::api::ViewTransformPayload>,
127
128 pub reference_highlight_overlay: ReferenceHighlightOverlay,
130
131 pub semantic_tokens: Option<SemanticTokenStore>,
133
134 pub language: String,
136}
137
138impl EditorState {
139 pub fn new(_width: u16, _height: u16, large_file_threshold: usize) -> Self {
144 Self {
145 buffer: Buffer::new(large_file_threshold),
146 cursors: Cursors::new(),
147 highlighter: HighlightEngine::None, indent_calculator: RefCell::new(IndentCalculator::new()),
149 overlays: OverlayManager::new(),
150 marker_list: MarkerList::new(),
151 virtual_texts: VirtualTextManager::new(),
152 popups: PopupManager::new(),
153 margins: MarginManager::new(),
154 primary_cursor_line_number: LineNumber::Absolute(0), mode: "insert".to_string(),
156 text_properties: TextPropertyManager::new(),
157 show_cursors: true,
158 editing_disabled: false,
159 is_composite_buffer: false,
160 show_whitespace_tabs: true,
161 use_tabs: false,
162 tab_size: 4, reference_highlighter: ReferenceHighlighter::new(),
164 view_mode: ViewMode::Source,
165 debug_highlight_mode: false,
166 compose_width: None,
167 compose_prev_line_numbers: None,
168 compose_column_guides: None,
169 view_transform: None,
170 reference_highlight_overlay: ReferenceHighlightOverlay::new(),
171 semantic_tokens: None,
172 language: "text".to_string(), }
174 }
175
176 pub fn set_language_from_name(&mut self, name: &str, registry: &GrammarRegistry) {
179 let cleaned_name = name.trim_matches('*');
183 let filename = if let Some(pos) = cleaned_name.rfind(':') {
184 &cleaned_name[pos + 1..]
186 } else {
187 cleaned_name
188 };
189
190 let path = std::path::Path::new(filename);
191 self.highlighter = HighlightEngine::for_file(path, registry);
192 if let Some(language) = Language::from_path(path) {
193 self.reference_highlighter.set_language(&language);
194 self.language = language.to_string();
195 } else {
196 self.language = "text".to_string();
197 }
198 tracing::debug!(
199 "Set highlighter for virtual buffer based on name: {} -> {} (backend: {}, language: {})",
200 name,
201 filename,
202 self.highlighter.backend_name(),
203 self.language
204 );
205 }
206
207 pub fn from_file(
212 path: &std::path::Path,
213 _width: u16,
214 _height: u16,
215 large_file_threshold: usize,
216 registry: &GrammarRegistry,
217 ) -> anyhow::Result<Self> {
218 let buffer = Buffer::load_from_file(path, large_file_threshold)?;
219
220 let highlighter = HighlightEngine::for_file(path, registry);
222 tracing::debug!(
223 "Created highlighter for {:?} (backend: {})",
224 path,
225 highlighter.backend_name()
226 );
227
228 let language = Language::from_path(path);
230 let mut reference_highlighter = ReferenceHighlighter::new();
231 let language_name = if let Some(lang) = &language {
232 reference_highlighter.set_language(lang);
233 lang.to_string()
234 } else {
235 "text".to_string()
236 };
237
238 let mut marker_list = MarkerList::new();
240 if !buffer.is_empty() {
241 tracing::debug!(
242 "Initializing marker list for file with {} bytes",
243 buffer.len()
244 );
245 marker_list.adjust_for_insert(0, buffer.len());
246 }
247
248 Ok(Self {
249 buffer,
250 cursors: Cursors::new(),
251 highlighter,
252 indent_calculator: RefCell::new(IndentCalculator::new()),
253 overlays: OverlayManager::new(),
254 marker_list,
255 virtual_texts: VirtualTextManager::new(),
256 popups: PopupManager::new(),
257 margins: MarginManager::new(),
258 primary_cursor_line_number: LineNumber::Absolute(0), mode: "insert".to_string(),
260 text_properties: TextPropertyManager::new(),
261 show_cursors: true,
262 editing_disabled: false,
263 is_composite_buffer: false,
264 show_whitespace_tabs: true,
265 use_tabs: false,
266 tab_size: 4, reference_highlighter,
268 view_mode: ViewMode::Source,
269 debug_highlight_mode: false,
270 compose_width: None,
271 compose_prev_line_numbers: None,
272 compose_column_guides: None,
273 view_transform: None,
274 reference_highlight_overlay: ReferenceHighlightOverlay::new(),
275 semantic_tokens: None,
276 language: language_name,
277 })
278 }
279
280 pub fn from_file_with_languages(
288 path: &std::path::Path,
289 _width: u16,
290 _height: u16,
291 large_file_threshold: usize,
292 registry: &GrammarRegistry,
293 languages: &std::collections::HashMap<String, crate::config::LanguageConfig>,
294 ) -> anyhow::Result<Self> {
295 let buffer = Buffer::load_from_file(path, large_file_threshold)?;
296
297 let highlighter = HighlightEngine::for_file_with_languages(path, registry, languages);
299 tracing::debug!(
300 "Created highlighter for {:?} (backend: {})",
301 path,
302 highlighter.backend_name()
303 );
304
305 let language = Language::from_path(path);
307 let mut reference_highlighter = ReferenceHighlighter::new();
308 let language_name = if let Some(lang) = &language {
309 reference_highlighter.set_language(lang);
310 lang.to_string()
311 } else {
312 crate::services::lsp::manager::detect_language(path, languages)
315 .unwrap_or_else(|| "text".to_string())
316 };
317
318 let mut marker_list = MarkerList::new();
320 if !buffer.is_empty() {
321 tracing::debug!(
322 "Initializing marker list for file with {} bytes",
323 buffer.len()
324 );
325 marker_list.adjust_for_insert(0, buffer.len());
326 }
327
328 Ok(Self {
329 buffer,
330 cursors: Cursors::new(),
331 highlighter,
332 indent_calculator: RefCell::new(IndentCalculator::new()),
333 overlays: OverlayManager::new(),
334 marker_list,
335 virtual_texts: VirtualTextManager::new(),
336 popups: PopupManager::new(),
337 margins: MarginManager::new(),
338 primary_cursor_line_number: LineNumber::Absolute(0), mode: "insert".to_string(),
340 text_properties: TextPropertyManager::new(),
341 show_cursors: true,
342 editing_disabled: false,
343 is_composite_buffer: false,
344 show_whitespace_tabs: true,
345 use_tabs: false,
346 tab_size: 4, reference_highlighter,
348 view_mode: ViewMode::Source,
349 debug_highlight_mode: false,
350 compose_width: None,
351 compose_prev_line_numbers: None,
352 compose_column_guides: None,
353 view_transform: None,
354 reference_highlight_overlay: ReferenceHighlightOverlay::new(),
355 semantic_tokens: None,
356 language: language_name,
357 })
358 }
359
360 fn apply_insert(
362 &mut self,
363 position: usize,
364 text: &str,
365 cursor_id: crate::model::event::CursorId,
366 ) {
367 let newlines_inserted = text.matches('\n').count();
368
369 self.marker_list.adjust_for_insert(position, text.len());
371 self.margins.adjust_for_insert(position, text.len());
372
373 self.buffer.insert(position, text);
375
376 self.highlighter
378 .invalidate_range(position..position + text.len());
379
380 self.cursors.adjust_for_edit(position, 0, text.len());
385
386 if let Some(cursor) = self.cursors.get_mut(cursor_id) {
388 cursor.position = position + text.len();
389 cursor.clear_selection();
390 }
391
392 if cursor_id == self.cursors.primary_id() {
394 self.primary_cursor_line_number = match self.primary_cursor_line_number {
395 LineNumber::Absolute(line) => LineNumber::Absolute(line + newlines_inserted),
396 LineNumber::Relative {
397 line,
398 from_cached_line,
399 } => LineNumber::Relative {
400 line: line + newlines_inserted,
401 from_cached_line,
402 },
403 };
404 }
405 }
406
407 fn apply_delete(
409 &mut self,
410 range: &std::ops::Range<usize>,
411 cursor_id: crate::model::event::CursorId,
412 deleted_text: &str,
413 ) {
414 let len = range.len();
415 let newlines_deleted = deleted_text.matches('\n').count();
416
417 self.marker_list.adjust_for_delete(range.start, len);
419 self.margins.adjust_for_delete(range.start, len);
420
421 self.buffer.delete(range.clone());
423
424 self.highlighter.invalidate_range(range.clone());
426
427 self.cursors.adjust_for_edit(range.start, len, 0);
432
433 if let Some(cursor) = self.cursors.get_mut(cursor_id) {
435 cursor.position = range.start;
436 cursor.clear_selection();
437 }
438
439 if cursor_id == self.cursors.primary_id() {
441 self.primary_cursor_line_number = match self.primary_cursor_line_number {
442 LineNumber::Absolute(line) => {
443 LineNumber::Absolute(line.saturating_sub(newlines_deleted))
444 }
445 LineNumber::Relative {
446 line,
447 from_cached_line,
448 } => LineNumber::Relative {
449 line: line.saturating_sub(newlines_deleted),
450 from_cached_line,
451 },
452 };
453 }
454 }
455
456 pub fn apply(&mut self, event: &Event) {
459 match event {
460 Event::Insert {
461 position,
462 text,
463 cursor_id,
464 } => self.apply_insert(*position, text, *cursor_id),
465
466 Event::Delete {
467 range,
468 cursor_id,
469 deleted_text,
470 } => self.apply_delete(range, *cursor_id, deleted_text),
471
472 Event::MoveCursor {
473 cursor_id,
474 new_position,
475 new_anchor,
476 new_sticky_column,
477 ..
478 } => {
479 if let Some(cursor) = self.cursors.get_mut(*cursor_id) {
480 cursor.position = *new_position;
481 cursor.anchor = *new_anchor;
482 cursor.sticky_column = *new_sticky_column;
483 }
484
485 if *cursor_id == self.cursors.primary_id() {
488 self.primary_cursor_line_number =
489 match self.buffer.offset_to_position(*new_position) {
490 Some(pos) => LineNumber::Absolute(pos.line),
491 None => {
492 let estimated_line = *new_position / 80;
495 LineNumber::Absolute(estimated_line)
496 }
497 };
498 }
499 }
500
501 Event::AddCursor {
502 cursor_id,
503 position,
504 anchor,
505 } => {
506 let cursor = if let Some(anchor) = anchor {
507 Cursor::with_selection(*anchor, *position)
508 } else {
509 Cursor::new(*position)
510 };
511
512 self.cursors.insert_with_id(*cursor_id, cursor);
515
516 self.cursors.normalize();
517 }
518
519 Event::RemoveCursor { cursor_id, .. } => {
520 self.cursors.remove(*cursor_id);
521 }
522
523 Event::Scroll { .. } | Event::SetViewport { .. } | Event::Recenter => {
526 tracing::warn!("View event {:?} reached EditorState.apply() - should be handled by SplitViewState", event);
529 }
530
531 Event::SetAnchor {
532 cursor_id,
533 position,
534 } => {
535 if let Some(cursor) = self.cursors.get_mut(*cursor_id) {
538 cursor.anchor = Some(*position);
539 cursor.deselect_on_move = false;
540 }
541 }
542
543 Event::ClearAnchor { cursor_id } => {
544 if let Some(cursor) = self.cursors.get_mut(*cursor_id) {
547 cursor.anchor = None;
548 cursor.deselect_on_move = true;
549 cursor.clear_block_selection();
550 }
551 }
552
553 Event::ChangeMode { mode } => {
554 self.mode = mode.clone();
555 }
556
557 Event::AddOverlay {
558 namespace,
559 range,
560 face,
561 priority,
562 message,
563 extend_to_line_end,
564 } => {
565 tracing::debug!(
566 "AddOverlay: namespace={:?}, range={:?}, face={:?}, priority={}",
567 namespace,
568 range,
569 face,
570 priority
571 );
572 let overlay_face = convert_event_face_to_overlay_face(face);
574 tracing::trace!("Converted face: {:?}", overlay_face);
575
576 let mut overlay = Overlay::with_priority(
577 &mut self.marker_list,
578 range.clone(),
579 overlay_face,
580 *priority,
581 );
582 overlay.namespace = namespace.clone();
583 overlay.message = message.clone();
584 overlay.extend_to_line_end = *extend_to_line_end;
585
586 let actual_range = overlay.range(&self.marker_list);
587 tracing::debug!(
588 "Created overlay with markers - actual range: {:?}, handle={:?}",
589 actual_range,
590 overlay.handle
591 );
592
593 self.overlays.add(overlay);
594 }
595
596 Event::RemoveOverlay { handle } => {
597 tracing::debug!("RemoveOverlay: handle={:?}", handle);
598 self.overlays
599 .remove_by_handle(handle, &mut self.marker_list);
600 }
601
602 Event::RemoveOverlaysInRange { range } => {
603 self.overlays.remove_in_range(range, &mut self.marker_list);
604 }
605
606 Event::ClearNamespace { namespace } => {
607 tracing::debug!("ClearNamespace: namespace={:?}", namespace);
608 self.overlays
609 .clear_namespace(namespace, &mut self.marker_list);
610 }
611
612 Event::ClearOverlays => {
613 self.overlays.clear(&mut self.marker_list);
614 }
615
616 Event::ShowPopup { popup } => {
617 let popup_obj = convert_popup_data_to_popup(popup);
618 self.popups.show(popup_obj);
619 }
620
621 Event::HidePopup => {
622 self.popups.hide();
623 }
624
625 Event::ClearPopups => {
626 self.popups.clear();
627 }
628
629 Event::PopupSelectNext => {
630 if let Some(popup) = self.popups.top_mut() {
631 popup.select_next();
632 }
633 }
634
635 Event::PopupSelectPrev => {
636 if let Some(popup) = self.popups.top_mut() {
637 popup.select_prev();
638 }
639 }
640
641 Event::PopupPageDown => {
642 if let Some(popup) = self.popups.top_mut() {
643 popup.page_down();
644 }
645 }
646
647 Event::PopupPageUp => {
648 if let Some(popup) = self.popups.top_mut() {
649 popup.page_up();
650 }
651 }
652
653 Event::AddMarginAnnotation {
654 line,
655 position,
656 content,
657 annotation_id,
658 } => {
659 let margin_position = convert_margin_position(position);
660 let margin_content = convert_margin_content(content);
661 let annotation = if let Some(id) = annotation_id {
662 MarginAnnotation::with_id(*line, margin_position, margin_content, id.clone())
663 } else {
664 MarginAnnotation::new(*line, margin_position, margin_content)
665 };
666 self.margins.add_annotation(annotation);
667 }
668
669 Event::RemoveMarginAnnotation { annotation_id } => {
670 self.margins.remove_by_id(annotation_id);
671 }
672
673 Event::RemoveMarginAnnotationsAtLine { line, position } => {
674 let margin_position = convert_margin_position(position);
675 self.margins.remove_at_line(*line, margin_position);
676 }
677
678 Event::ClearMarginPosition { position } => {
679 let margin_position = convert_margin_position(position);
680 self.margins.clear_position(margin_position);
681 }
682
683 Event::ClearMargins => {
684 self.margins.clear_all();
685 }
686
687 Event::SetLineNumbers { enabled } => {
688 self.margins.set_line_numbers(*enabled);
689 }
690
691 Event::SplitPane { .. }
694 | Event::CloseSplit { .. }
695 | Event::SetActiveSplit { .. }
696 | Event::AdjustSplitRatio { .. }
697 | Event::NextSplit
698 | Event::PrevSplit => {
699 }
701
702 Event::Batch { events, .. } => {
703 for event in events {
706 self.apply(event);
707 }
708 }
709
710 Event::BulkEdit {
711 new_tree,
712 new_cursors,
713 ..
714 } => {
715 if let Some(tree) = new_tree {
720 self.buffer.restore_piece_tree(tree);
721 }
722
723 for (cursor_id, position, anchor) in new_cursors {
725 if let Some(cursor) = self.cursors.get_mut(*cursor_id) {
726 cursor.position = *position;
727 cursor.anchor = *anchor;
728 }
729 }
730
731 self.highlighter.invalidate_all();
733
734 let primary_pos = self.cursors.primary().position;
736 self.primary_cursor_line_number = match self.buffer.offset_to_position(primary_pos)
737 {
738 Some(pos) => crate::model::buffer::LineNumber::Absolute(pos.line),
739 None => crate::model::buffer::LineNumber::Absolute(0),
740 };
741 }
742 }
743 }
744
745 pub fn apply_many(&mut self, events: &[Event]) {
747 for event in events {
748 self.apply(event);
749 }
750 }
751
752 pub fn primary_cursor(&self) -> &Cursor {
754 self.cursors.primary()
755 }
756
757 pub fn primary_cursor_mut(&mut self) -> &mut Cursor {
759 self.cursors.primary_mut()
760 }
761
762 pub fn on_focus_lost(&mut self) {
766 if self.popups.dismiss_transient() {
767 tracing::debug!("Dismissed transient popup on buffer focus loss");
768 }
769 }
770}
771
772fn convert_event_face_to_overlay_face(event_face: &EventOverlayFace) -> OverlayFace {
774 match event_face {
775 EventOverlayFace::Underline { color, style } => {
776 let underline_style = match style {
777 crate::model::event::UnderlineStyle::Straight => UnderlineStyle::Straight,
778 crate::model::event::UnderlineStyle::Wavy => UnderlineStyle::Wavy,
779 crate::model::event::UnderlineStyle::Dotted => UnderlineStyle::Dotted,
780 crate::model::event::UnderlineStyle::Dashed => UnderlineStyle::Dashed,
781 };
782 OverlayFace::Underline {
783 color: Color::Rgb(color.0, color.1, color.2),
784 style: underline_style,
785 }
786 }
787 EventOverlayFace::Background { color } => OverlayFace::Background {
788 color: Color::Rgb(color.0, color.1, color.2),
789 },
790 EventOverlayFace::Foreground { color } => OverlayFace::Foreground {
791 color: Color::Rgb(color.0, color.1, color.2),
792 },
793 EventOverlayFace::Style {
794 color,
795 bg_color,
796 bold,
797 italic,
798 underline,
799 } => {
800 use ratatui::style::Modifier;
801 let mut style = Style::default().fg(Color::Rgb(color.0, color.1, color.2));
802 if let Some(bg) = bg_color {
803 style = style.bg(Color::Rgb(bg.0, bg.1, bg.2));
804 }
805 let mut modifiers = Modifier::empty();
806 if *bold {
807 modifiers |= Modifier::BOLD;
808 }
809 if *italic {
810 modifiers |= Modifier::ITALIC;
811 }
812 if *underline {
813 modifiers |= Modifier::UNDERLINED;
814 }
815 if !modifiers.is_empty() {
816 style = style.add_modifier(modifiers);
817 }
818 OverlayFace::Style { style }
819 }
820 }
821}
822
823fn convert_popup_data_to_popup(data: &PopupData) -> Popup {
825 let content = match &data.content {
826 crate::model::event::PopupContentData::Text(lines) => PopupContent::Text(lines.clone()),
827 crate::model::event::PopupContentData::List { items, selected } => PopupContent::List {
828 items: items
829 .iter()
830 .map(|item| PopupListItem {
831 text: item.text.clone(),
832 detail: item.detail.clone(),
833 icon: item.icon.clone(),
834 data: item.data.clone(),
835 })
836 .collect(),
837 selected: *selected,
838 },
839 };
840
841 let position = match data.position {
842 PopupPositionData::AtCursor => PopupPosition::AtCursor,
843 PopupPositionData::BelowCursor => PopupPosition::BelowCursor,
844 PopupPositionData::AboveCursor => PopupPosition::AboveCursor,
845 PopupPositionData::Fixed { x, y } => PopupPosition::Fixed { x, y },
846 PopupPositionData::Centered => PopupPosition::Centered,
847 PopupPositionData::BottomRight => PopupPosition::BottomRight,
848 };
849
850 Popup {
851 title: data.title.clone(),
852 description: data.description.clone(),
853 transient: data.transient,
854 content,
855 position,
856 width: data.width,
857 max_height: data.max_height,
858 bordered: data.bordered,
859 border_style: Style::default().fg(Color::Gray),
860 background_style: Style::default().bg(Color::Rgb(30, 30, 30)),
861 scroll_offset: 0,
862 text_selection: None,
863 }
864}
865
866fn convert_margin_position(position: &MarginPositionData) -> MarginPosition {
868 match position {
869 MarginPositionData::Left => MarginPosition::Left,
870 MarginPositionData::Right => MarginPosition::Right,
871 }
872}
873
874fn convert_margin_content(content: &MarginContentData) -> MarginContent {
876 match content {
877 MarginContentData::Text(text) => MarginContent::Text(text.clone()),
878 MarginContentData::Symbol { text, color } => {
879 if let Some((r, g, b)) = color {
880 MarginContent::colored_symbol(text.clone(), Color::Rgb(*r, *g, *b))
881 } else {
882 MarginContent::symbol(text.clone(), Style::default())
883 }
884 }
885 MarginContentData::Empty => MarginContent::Empty,
886 }
887}
888
889impl EditorState {
890 pub fn prepare_for_render(&mut self, top_byte: usize, height: u16) -> Result<()> {
897 self.buffer.prepare_viewport(top_byte, height as usize)?;
898 Ok(())
899 }
900
901 pub fn get_text_range(&mut self, start: usize, end: usize) -> String {
921 match self
923 .buffer
924 .get_text_range_mut(start, end.saturating_sub(start))
925 {
926 Ok(bytes) => String::from_utf8_lossy(&bytes).into_owned(),
927 Err(e) => {
928 tracing::warn!("Failed to get text range {}..{}: {}", start, end, e);
929 String::new()
930 }
931 }
932 }
933
934 pub fn get_line_at_offset(&mut self, offset: usize) -> Option<(usize, String)> {
942 use crate::model::document_model::DocumentModel;
943
944 let mut line_start = offset;
947 while line_start > 0 {
948 if let Ok(text) = self.buffer.get_text_range_mut(line_start - 1, 1) {
949 if text.first() == Some(&b'\n') {
950 break;
951 }
952 line_start -= 1;
953 } else {
954 break;
955 }
956 }
957
958 let viewport = self
960 .get_viewport_content(
961 crate::model::document_model::DocumentPosition::byte(line_start),
962 1,
963 )
964 .ok()?;
965
966 viewport
967 .lines
968 .first()
969 .map(|line| (line.byte_offset, line.content.clone()))
970 }
971
972 pub fn get_text_to_end_of_line(&mut self, cursor_pos: usize) -> Result<String> {
977 use crate::model::document_model::DocumentModel;
978
979 let viewport = self.get_viewport_content(
981 crate::model::document_model::DocumentPosition::byte(cursor_pos),
982 1,
983 )?;
984
985 if let Some(line) = viewport.lines.first() {
986 let line_start = line.byte_offset;
987 let line_end = line_start + line.content.len();
988
989 if cursor_pos >= line_start && cursor_pos <= line_end {
990 let offset_in_line = cursor_pos - line_start;
991 Ok(line.content.get(offset_in_line..).unwrap_or("").to_string())
993 } else {
994 Ok(String::new())
995 }
996 } else {
997 Ok(String::new())
998 }
999 }
1000
1001 pub fn set_semantic_tokens(&mut self, store: SemanticTokenStore) {
1003 self.semantic_tokens = Some(store);
1004 }
1005
1006 pub fn clear_semantic_tokens(&mut self) {
1008 self.semantic_tokens = None;
1009 }
1010
1011 pub fn semantic_tokens_result_id(&self) -> Option<&str> {
1013 self.semantic_tokens
1014 .as_ref()
1015 .and_then(|store| store.result_id.as_deref())
1016 }
1017}
1018
1019impl DocumentModel for EditorState {
1024 fn capabilities(&self) -> DocumentCapabilities {
1025 let line_count = self.buffer.line_count();
1026 DocumentCapabilities {
1027 has_line_index: line_count.is_some(),
1028 uses_lazy_loading: false, byte_length: self.buffer.len(),
1030 approximate_line_count: line_count.unwrap_or_else(|| {
1031 self.buffer.len() / 80
1033 }),
1034 }
1035 }
1036
1037 fn get_viewport_content(
1038 &mut self,
1039 start_pos: DocumentPosition,
1040 max_lines: usize,
1041 ) -> Result<ViewportContent> {
1042 let start_offset = self.position_to_offset(start_pos)?;
1044
1045 let line_iter = self.buffer.iter_lines_from(start_offset, max_lines)?;
1048 let has_more = line_iter.has_more;
1049
1050 let lines = line_iter
1051 .map(|line_data| ViewportLine {
1052 byte_offset: line_data.byte_offset,
1053 content: line_data.content,
1054 has_newline: line_data.has_newline,
1055 approximate_line_number: line_data.line_number,
1056 })
1057 .collect();
1058
1059 Ok(ViewportContent {
1060 start_position: DocumentPosition::ByteOffset(start_offset),
1061 lines,
1062 has_more,
1063 })
1064 }
1065
1066 fn position_to_offset(&self, pos: DocumentPosition) -> Result<usize> {
1067 match pos {
1068 DocumentPosition::ByteOffset(offset) => Ok(offset),
1069 DocumentPosition::LineColumn { line, column } => {
1070 if !self.has_line_index() {
1071 anyhow::bail!("Line indexing not available for this document");
1072 }
1073 let position = crate::model::piece_tree::Position { line, column };
1075 Ok(self.buffer.position_to_offset(position))
1076 }
1077 }
1078 }
1079
1080 fn offset_to_position(&self, offset: usize) -> DocumentPosition {
1081 if self.has_line_index() {
1082 if let Some(pos) = self.buffer.offset_to_position(offset) {
1083 DocumentPosition::LineColumn {
1084 line: pos.line,
1085 column: pos.column,
1086 }
1087 } else {
1088 DocumentPosition::ByteOffset(offset)
1090 }
1091 } else {
1092 DocumentPosition::ByteOffset(offset)
1093 }
1094 }
1095
1096 fn get_range(&mut self, start: DocumentPosition, end: DocumentPosition) -> Result<String> {
1097 let start_offset = self.position_to_offset(start)?;
1098 let end_offset = self.position_to_offset(end)?;
1099
1100 if start_offset > end_offset {
1101 anyhow::bail!(
1102 "Invalid range: start offset {} > end offset {}",
1103 start_offset,
1104 end_offset
1105 );
1106 }
1107
1108 let bytes = self
1109 .buffer
1110 .get_text_range_mut(start_offset, end_offset - start_offset)?;
1111
1112 Ok(String::from_utf8_lossy(&bytes).into_owned())
1113 }
1114
1115 fn get_line_content(&mut self, line_number: usize) -> Option<String> {
1116 if !self.has_line_index() {
1117 return None;
1118 }
1119
1120 let line_start_offset = self.buffer.line_start_offset(line_number)?;
1122
1123 let mut iter = self.buffer.line_iterator(line_start_offset, 80);
1125 if let Some((_start, content)) = iter.next_line() {
1126 let has_newline = content.ends_with('\n');
1127 let line_content = if has_newline {
1128 content[..content.len() - 1].to_string()
1129 } else {
1130 content
1131 };
1132 Some(line_content)
1133 } else {
1134 None
1135 }
1136 }
1137
1138 fn get_chunk_at_offset(&mut self, offset: usize, size: usize) -> Result<(usize, String)> {
1139 let bytes = self.buffer.get_text_range_mut(offset, size)?;
1140
1141 Ok((offset, String::from_utf8_lossy(&bytes).into_owned()))
1142 }
1143
1144 fn insert(&mut self, pos: DocumentPosition, text: &str) -> Result<usize> {
1145 let offset = self.position_to_offset(pos)?;
1146 self.buffer.insert_bytes(offset, text.as_bytes().to_vec());
1147 Ok(text.len())
1148 }
1149
1150 fn delete(&mut self, start: DocumentPosition, end: DocumentPosition) -> Result<()> {
1151 let start_offset = self.position_to_offset(start)?;
1152 let end_offset = self.position_to_offset(end)?;
1153
1154 if start_offset > end_offset {
1155 anyhow::bail!(
1156 "Invalid range: start offset {} > end offset {}",
1157 start_offset,
1158 end_offset
1159 );
1160 }
1161
1162 self.buffer.delete(start_offset..end_offset);
1163 Ok(())
1164 }
1165
1166 fn replace(
1167 &mut self,
1168 start: DocumentPosition,
1169 end: DocumentPosition,
1170 text: &str,
1171 ) -> Result<()> {
1172 self.delete(start, end)?;
1174 self.insert(start, text)?;
1175 Ok(())
1176 }
1177
1178 fn find_matches(
1179 &mut self,
1180 pattern: &str,
1181 search_range: Option<(DocumentPosition, DocumentPosition)>,
1182 ) -> Result<Vec<usize>> {
1183 let (start_offset, end_offset) = if let Some((start, end)) = search_range {
1184 (
1185 self.position_to_offset(start)?,
1186 self.position_to_offset(end)?,
1187 )
1188 } else {
1189 (0, self.buffer.len())
1190 };
1191
1192 let bytes = self
1194 .buffer
1195 .get_text_range_mut(start_offset, end_offset - start_offset)?;
1196 let text = String::from_utf8_lossy(&bytes);
1197
1198 let mut matches = Vec::new();
1200 let mut search_offset = 0;
1201 while let Some(pos) = text[search_offset..].find(pattern) {
1202 matches.push(start_offset + search_offset + pos);
1203 search_offset += pos + pattern.len();
1204 }
1205
1206 Ok(matches)
1207 }
1208}
1209
1210#[derive(Clone, Debug)]
1212pub struct SemanticTokenStore {
1213 pub version: u64,
1215 pub result_id: Option<String>,
1217 pub data: Vec<u32>,
1219 pub tokens: Vec<SemanticTokenSpan>,
1221}
1222
1223#[derive(Clone, Debug)]
1225pub struct SemanticTokenSpan {
1226 pub range: Range<usize>,
1227 pub token_type: String,
1228 pub modifiers: Vec<String>,
1229}
1230
1231#[cfg(test)]
1232mod tests {
1233 use super::*;
1234 use crate::model::event::CursorId;
1235
1236 #[test]
1237 fn test_state_new() {
1238 let state = EditorState::new(80, 24, crate::config::LARGE_FILE_THRESHOLD_BYTES as usize);
1239 assert!(state.buffer.is_empty());
1240 assert_eq!(state.cursors.count(), 1);
1241 assert_eq!(state.cursors.primary().position, 0);
1242 }
1243
1244 #[test]
1245 fn test_apply_insert() {
1246 let mut state =
1247 EditorState::new(80, 24, crate::config::LARGE_FILE_THRESHOLD_BYTES as usize);
1248 let cursor_id = state.cursors.primary_id();
1249
1250 state.apply(&Event::Insert {
1251 position: 0,
1252 text: "hello".to_string(),
1253 cursor_id,
1254 });
1255
1256 assert_eq!(state.buffer.to_string().unwrap(), "hello");
1257 assert_eq!(state.cursors.primary().position, 5);
1258 assert!(state.buffer.is_modified());
1259 }
1260
1261 #[test]
1262 fn test_apply_delete() {
1263 let mut state =
1264 EditorState::new(80, 24, crate::config::LARGE_FILE_THRESHOLD_BYTES as usize);
1265 let cursor_id = state.cursors.primary_id();
1266
1267 state.apply(&Event::Insert {
1269 position: 0,
1270 text: "hello world".to_string(),
1271 cursor_id,
1272 });
1273
1274 state.apply(&Event::Delete {
1275 range: 5..11,
1276 deleted_text: " world".to_string(),
1277 cursor_id,
1278 });
1279
1280 assert_eq!(state.buffer.to_string().unwrap(), "hello");
1281 assert_eq!(state.cursors.primary().position, 5);
1282 }
1283
1284 #[test]
1285 fn test_apply_move_cursor() {
1286 let mut state =
1287 EditorState::new(80, 24, crate::config::LARGE_FILE_THRESHOLD_BYTES as usize);
1288 let cursor_id = state.cursors.primary_id();
1289
1290 state.apply(&Event::Insert {
1291 position: 0,
1292 text: "hello".to_string(),
1293 cursor_id,
1294 });
1295
1296 state.apply(&Event::MoveCursor {
1297 cursor_id,
1298 old_position: 5,
1299 new_position: 2,
1300 old_anchor: None,
1301 new_anchor: None,
1302 old_sticky_column: 0,
1303 new_sticky_column: 0,
1304 });
1305
1306 assert_eq!(state.cursors.primary().position, 2);
1307 }
1308
1309 #[test]
1310 fn test_apply_add_cursor() {
1311 let mut state =
1312 EditorState::new(80, 24, crate::config::LARGE_FILE_THRESHOLD_BYTES as usize);
1313 let cursor_id = CursorId(1);
1314
1315 state.apply(&Event::AddCursor {
1316 cursor_id,
1317 position: 5,
1318 anchor: None,
1319 });
1320
1321 assert_eq!(state.cursors.count(), 2);
1322 }
1323
1324 #[test]
1325 fn test_apply_many() {
1326 let mut state =
1327 EditorState::new(80, 24, crate::config::LARGE_FILE_THRESHOLD_BYTES as usize);
1328 let cursor_id = state.cursors.primary_id();
1329
1330 let events = vec![
1331 Event::Insert {
1332 position: 0,
1333 text: "hello ".to_string(),
1334 cursor_id,
1335 },
1336 Event::Insert {
1337 position: 6,
1338 text: "world".to_string(),
1339 cursor_id,
1340 },
1341 ];
1342
1343 state.apply_many(&events);
1344
1345 assert_eq!(state.buffer.to_string().unwrap(), "hello world");
1346 }
1347
1348 #[test]
1349 fn test_cursor_adjustment_after_insert() {
1350 let mut state =
1351 EditorState::new(80, 24, crate::config::LARGE_FILE_THRESHOLD_BYTES as usize);
1352 let cursor_id = state.cursors.primary_id();
1353
1354 state.apply(&Event::AddCursor {
1356 cursor_id: CursorId(1),
1357 position: 5,
1358 anchor: None,
1359 });
1360
1361 state.apply(&Event::Insert {
1363 position: 0,
1364 text: "abc".to_string(),
1365 cursor_id,
1366 });
1367
1368 if let Some(cursor) = state.cursors.get(CursorId(1)) {
1370 assert_eq!(cursor.position, 8);
1371 }
1372 }
1373
1374 mod document_model_tests {
1376 use super::*;
1377 use crate::model::document_model::{DocumentModel, DocumentPosition};
1378
1379 #[test]
1380 fn test_capabilities_small_file() {
1381 let mut state =
1382 EditorState::new(80, 24, crate::config::LARGE_FILE_THRESHOLD_BYTES as usize);
1383 state.buffer = Buffer::from_str_test("line1\nline2\nline3");
1384
1385 let caps = state.capabilities();
1386 assert!(caps.has_line_index, "Small file should have line index");
1387 assert_eq!(caps.byte_length, "line1\nline2\nline3".len());
1388 assert_eq!(caps.approximate_line_count, 3, "Should have 3 lines");
1389 }
1390
1391 #[test]
1392 fn test_position_conversions() {
1393 let mut state =
1394 EditorState::new(80, 24, crate::config::LARGE_FILE_THRESHOLD_BYTES as usize);
1395 state.buffer = Buffer::from_str_test("hello\nworld\ntest");
1396
1397 let pos1 = DocumentPosition::ByteOffset(6);
1399 let offset1 = state.position_to_offset(pos1).unwrap();
1400 assert_eq!(offset1, 6);
1401
1402 let pos2 = DocumentPosition::LineColumn { line: 1, column: 0 };
1404 let offset2 = state.position_to_offset(pos2).unwrap();
1405 assert_eq!(offset2, 6, "Line 1, column 0 should be at byte 6");
1406
1407 let converted = state.offset_to_position(6);
1409 match converted {
1410 DocumentPosition::LineColumn { line, column } => {
1411 assert_eq!(line, 1);
1412 assert_eq!(column, 0);
1413 }
1414 _ => panic!("Expected LineColumn for small file"),
1415 }
1416 }
1417
1418 #[test]
1419 fn test_get_viewport_content() {
1420 let mut state =
1421 EditorState::new(80, 24, crate::config::LARGE_FILE_THRESHOLD_BYTES as usize);
1422 state.buffer = Buffer::from_str_test("line1\nline2\nline3\nline4\nline5");
1423
1424 let content = state
1425 .get_viewport_content(DocumentPosition::ByteOffset(0), 3)
1426 .unwrap();
1427
1428 assert_eq!(content.lines.len(), 3);
1429 assert_eq!(content.lines[0].content, "line1");
1430 assert_eq!(content.lines[1].content, "line2");
1431 assert_eq!(content.lines[2].content, "line3");
1432 assert!(content.has_more);
1433 }
1434
1435 #[test]
1436 fn test_get_range() {
1437 let mut state =
1438 EditorState::new(80, 24, crate::config::LARGE_FILE_THRESHOLD_BYTES as usize);
1439 state.buffer = Buffer::from_str_test("hello world");
1440
1441 let text = state
1442 .get_range(
1443 DocumentPosition::ByteOffset(0),
1444 DocumentPosition::ByteOffset(5),
1445 )
1446 .unwrap();
1447 assert_eq!(text, "hello");
1448
1449 let text2 = state
1450 .get_range(
1451 DocumentPosition::ByteOffset(6),
1452 DocumentPosition::ByteOffset(11),
1453 )
1454 .unwrap();
1455 assert_eq!(text2, "world");
1456 }
1457
1458 #[test]
1459 fn test_get_line_content() {
1460 let mut state =
1461 EditorState::new(80, 24, crate::config::LARGE_FILE_THRESHOLD_BYTES as usize);
1462 state.buffer = Buffer::from_str_test("line1\nline2\nline3");
1463
1464 let line0 = state.get_line_content(0).unwrap();
1465 assert_eq!(line0, "line1");
1466
1467 let line1 = state.get_line_content(1).unwrap();
1468 assert_eq!(line1, "line2");
1469
1470 let line2 = state.get_line_content(2).unwrap();
1471 assert_eq!(line2, "line3");
1472 }
1473
1474 #[test]
1475 fn test_insert_delete() {
1476 let mut state =
1477 EditorState::new(80, 24, crate::config::LARGE_FILE_THRESHOLD_BYTES as usize);
1478 state.buffer = Buffer::from_str_test("hello world");
1479
1480 let bytes_inserted = state
1482 .insert(DocumentPosition::ByteOffset(6), "beautiful ")
1483 .unwrap();
1484 assert_eq!(bytes_inserted, 10);
1485 assert_eq!(state.buffer.to_string().unwrap(), "hello beautiful world");
1486
1487 state
1489 .delete(
1490 DocumentPosition::ByteOffset(6),
1491 DocumentPosition::ByteOffset(16),
1492 )
1493 .unwrap();
1494 assert_eq!(state.buffer.to_string().unwrap(), "hello world");
1495 }
1496
1497 #[test]
1498 fn test_replace() {
1499 let mut state =
1500 EditorState::new(80, 24, crate::config::LARGE_FILE_THRESHOLD_BYTES as usize);
1501 state.buffer = Buffer::from_str_test("hello world");
1502
1503 state
1504 .replace(
1505 DocumentPosition::ByteOffset(0),
1506 DocumentPosition::ByteOffset(5),
1507 "hi",
1508 )
1509 .unwrap();
1510 assert_eq!(state.buffer.to_string().unwrap(), "hi world");
1511 }
1512
1513 #[test]
1514 fn test_find_matches() {
1515 let mut state =
1516 EditorState::new(80, 24, crate::config::LARGE_FILE_THRESHOLD_BYTES as usize);
1517 state.buffer = Buffer::from_str_test("hello world hello");
1518
1519 let matches = state.find_matches("hello", None).unwrap();
1520 assert_eq!(matches.len(), 2);
1521 assert_eq!(matches[0], 0);
1522 assert_eq!(matches[1], 12);
1523 }
1524
1525 #[test]
1526 fn test_prepare_for_render() {
1527 let mut state =
1528 EditorState::new(80, 24, crate::config::LARGE_FILE_THRESHOLD_BYTES as usize);
1529 state.buffer = Buffer::from_str_test("line1\nline2\nline3\nline4\nline5");
1530
1531 state.prepare_for_render(0, 24).unwrap();
1533 }
1534
1535 #[test]
1536 fn test_helper_get_text_range() {
1537 let mut state =
1538 EditorState::new(80, 24, crate::config::LARGE_FILE_THRESHOLD_BYTES as usize);
1539 state.buffer = Buffer::from_str_test("hello world");
1540
1541 let text = state.get_text_range(0, 5);
1543 assert_eq!(text, "hello");
1544
1545 let text2 = state.get_text_range(6, 11);
1547 assert_eq!(text2, "world");
1548 }
1549
1550 #[test]
1551 fn test_helper_get_line_at_offset() {
1552 let mut state =
1553 EditorState::new(80, 24, crate::config::LARGE_FILE_THRESHOLD_BYTES as usize);
1554 state.buffer = Buffer::from_str_test("line1\nline2\nline3");
1555
1556 let (offset, content) = state.get_line_at_offset(0).unwrap();
1558 assert_eq!(offset, 0);
1559 assert_eq!(content, "line1");
1560
1561 let (offset2, content2) = state.get_line_at_offset(8).unwrap();
1563 assert_eq!(offset2, 6); assert_eq!(content2, "line2");
1565
1566 let (offset3, content3) = state.get_line_at_offset(12).unwrap();
1568 assert_eq!(offset3, 12);
1569 assert_eq!(content3, "line3");
1570 }
1571
1572 #[test]
1573 fn test_helper_get_text_to_end_of_line() {
1574 let mut state =
1575 EditorState::new(80, 24, crate::config::LARGE_FILE_THRESHOLD_BYTES as usize);
1576 state.buffer = Buffer::from_str_test("hello world\nline2");
1577
1578 let text = state.get_text_to_end_of_line(0).unwrap();
1580 assert_eq!(text, "hello world");
1581
1582 let text2 = state.get_text_to_end_of_line(6).unwrap();
1584 assert_eq!(text2, "world");
1585
1586 let text3 = state.get_text_to_end_of_line(11).unwrap();
1588 assert_eq!(text3, "");
1589
1590 let text4 = state.get_text_to_end_of_line(12).unwrap();
1592 assert_eq!(text4, "line2");
1593 }
1594 }
1595
1596 mod virtual_text_integration_tests {
1598 use super::*;
1599 use crate::view::virtual_text::VirtualTextPosition;
1600 use ratatui::style::Style;
1601
1602 #[test]
1603 fn test_virtual_text_add_and_query() {
1604 let mut state =
1605 EditorState::new(80, 24, crate::config::LARGE_FILE_THRESHOLD_BYTES as usize);
1606 state.buffer = Buffer::from_str_test("hello world");
1607
1608 if !state.buffer.is_empty() {
1610 state.marker_list.adjust_for_insert(0, state.buffer.len());
1611 }
1612
1613 let vtext_id = state.virtual_texts.add(
1615 &mut state.marker_list,
1616 5,
1617 ": string".to_string(),
1618 Style::default(),
1619 VirtualTextPosition::AfterChar,
1620 0,
1621 );
1622
1623 let results = state.virtual_texts.query_range(&state.marker_list, 0, 11);
1625 assert_eq!(results.len(), 1);
1626 assert_eq!(results[0].0, 5); assert_eq!(results[0].1.text, ": string");
1628
1629 let lookup = state.virtual_texts.build_lookup(&state.marker_list, 0, 11);
1631 assert!(lookup.contains_key(&5));
1632 assert_eq!(lookup[&5].len(), 1);
1633 assert_eq!(lookup[&5][0].text, ": string");
1634
1635 state.virtual_texts.remove(&mut state.marker_list, vtext_id);
1637 assert!(state.virtual_texts.is_empty());
1638 }
1639
1640 #[test]
1641 fn test_virtual_text_position_tracking_on_insert() {
1642 let mut state =
1643 EditorState::new(80, 24, crate::config::LARGE_FILE_THRESHOLD_BYTES as usize);
1644 state.buffer = Buffer::from_str_test("hello world");
1645
1646 if !state.buffer.is_empty() {
1648 state.marker_list.adjust_for_insert(0, state.buffer.len());
1649 }
1650
1651 let _vtext_id = state.virtual_texts.add(
1653 &mut state.marker_list,
1654 6,
1655 "/*param*/".to_string(),
1656 Style::default(),
1657 VirtualTextPosition::BeforeChar,
1658 0,
1659 );
1660
1661 let cursor_id = state.cursors.primary_id();
1663 state.apply(&Event::Insert {
1664 position: 6,
1665 text: "beautiful ".to_string(),
1666 cursor_id,
1667 });
1668
1669 let results = state.virtual_texts.query_range(&state.marker_list, 0, 30);
1671 assert_eq!(results.len(), 1);
1672 assert_eq!(results[0].0, 16); assert_eq!(results[0].1.text, "/*param*/");
1674 }
1675
1676 #[test]
1677 fn test_virtual_text_position_tracking_on_delete() {
1678 let mut state =
1679 EditorState::new(80, 24, crate::config::LARGE_FILE_THRESHOLD_BYTES as usize);
1680 state.buffer = Buffer::from_str_test("hello beautiful world");
1681
1682 if !state.buffer.is_empty() {
1684 state.marker_list.adjust_for_insert(0, state.buffer.len());
1685 }
1686
1687 let _vtext_id = state.virtual_texts.add(
1689 &mut state.marker_list,
1690 16,
1691 ": string".to_string(),
1692 Style::default(),
1693 VirtualTextPosition::AfterChar,
1694 0,
1695 );
1696
1697 let cursor_id = state.cursors.primary_id();
1699 state.apply(&Event::Delete {
1700 range: 6..16,
1701 deleted_text: "beautiful ".to_string(),
1702 cursor_id,
1703 });
1704
1705 let results = state.virtual_texts.query_range(&state.marker_list, 0, 20);
1707 assert_eq!(results.len(), 1);
1708 assert_eq!(results[0].0, 6); assert_eq!(results[0].1.text, ": string");
1710 }
1711
1712 #[test]
1713 fn test_multiple_virtual_texts_with_priorities() {
1714 let mut state =
1715 EditorState::new(80, 24, crate::config::LARGE_FILE_THRESHOLD_BYTES as usize);
1716 state.buffer = Buffer::from_str_test("let x = 5");
1717
1718 if !state.buffer.is_empty() {
1720 state.marker_list.adjust_for_insert(0, state.buffer.len());
1721 }
1722
1723 state.virtual_texts.add(
1725 &mut state.marker_list,
1726 5,
1727 ": i32".to_string(),
1728 Style::default(),
1729 VirtualTextPosition::AfterChar,
1730 0, );
1732
1733 state.virtual_texts.add(
1735 &mut state.marker_list,
1736 5,
1737 " /* inferred */".to_string(),
1738 Style::default(),
1739 VirtualTextPosition::AfterChar,
1740 10, );
1742
1743 let lookup = state.virtual_texts.build_lookup(&state.marker_list, 0, 10);
1745 assert!(lookup.contains_key(&5));
1746 let vtexts = &lookup[&5];
1747 assert_eq!(vtexts.len(), 2);
1748 assert_eq!(vtexts[0].text, ": i32");
1750 assert_eq!(vtexts[1].text, " /* inferred */");
1751 }
1752
1753 #[test]
1754 fn test_virtual_text_clear() {
1755 let mut state =
1756 EditorState::new(80, 24, crate::config::LARGE_FILE_THRESHOLD_BYTES as usize);
1757 state.buffer = Buffer::from_str_test("test");
1758
1759 if !state.buffer.is_empty() {
1761 state.marker_list.adjust_for_insert(0, state.buffer.len());
1762 }
1763
1764 state.virtual_texts.add(
1766 &mut state.marker_list,
1767 0,
1768 "hint1".to_string(),
1769 Style::default(),
1770 VirtualTextPosition::BeforeChar,
1771 0,
1772 );
1773 state.virtual_texts.add(
1774 &mut state.marker_list,
1775 2,
1776 "hint2".to_string(),
1777 Style::default(),
1778 VirtualTextPosition::AfterChar,
1779 0,
1780 );
1781
1782 assert_eq!(state.virtual_texts.len(), 2);
1783
1784 state.virtual_texts.clear(&mut state.marker_list);
1786 assert!(state.virtual_texts.is_empty());
1787
1788 let results = state.virtual_texts.query_range(&state.marker_list, 0, 10);
1790 assert!(results.is_empty());
1791 }
1792 }
1793}