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::MarkerList;
12use crate::primitives::grammar::GrammarRegistry;
13use crate::primitives::highlight_engine::HighlightEngine;
14use crate::primitives::highlighter::Language;
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::margin::{MarginAnnotation, MarginContent, MarginManager, MarginPosition};
20use crate::view::overlay::{Overlay, OverlayFace, OverlayManager, UnderlineStyle};
21use crate::view::popup::{
22 Popup, PopupContent, PopupKind, PopupListItem, PopupManager, PopupPosition,
23};
24use crate::view::reference_highlight_overlay::ReferenceHighlightOverlay;
25use crate::view::virtual_text::VirtualTextManager;
26use anyhow::Result;
27use ratatui::style::{Color, Style};
28use rust_i18n::t;
29use std::cell::RefCell;
30use std::ops::Range;
31use std::sync::Arc;
32
33#[derive(Debug, Clone, PartialEq, Eq)]
35pub enum ViewMode {
36 Source,
38 Compose,
40}
41
42pub struct EditorState {
48 pub buffer: Buffer,
50
51 pub cursors: Cursors,
53
54 pub highlighter: HighlightEngine,
56
57 pub indent_calculator: RefCell<IndentCalculator>,
59
60 pub overlays: OverlayManager,
62
63 pub marker_list: MarkerList,
65
66 pub virtual_texts: VirtualTextManager,
68
69 pub popups: PopupManager,
71
72 pub margins: MarginManager,
74
75 pub primary_cursor_line_number: LineNumber,
78
79 pub mode: String,
81
82 pub text_properties: TextPropertyManager,
85
86 pub show_cursors: bool,
89
90 pub editing_disabled: bool,
94
95 pub is_composite_buffer: bool,
99
100 pub show_whitespace_tabs: bool,
103
104 pub use_tabs: bool,
107
108 pub tab_size: usize,
111
112 pub reference_highlighter: ReferenceHighlighter,
114
115 pub view_mode: ViewMode,
117
118 pub debug_highlight_mode: bool,
121
122 pub compose_width: Option<u16>,
124
125 pub compose_prev_line_numbers: Option<bool>,
127
128 pub compose_column_guides: Option<Vec<u16>>,
130
131 pub view_transform: Option<fresh_core::api::ViewTransformPayload>,
133
134 pub reference_highlight_overlay: ReferenceHighlightOverlay,
136
137 pub bracket_highlight_overlay: BracketHighlightOverlay,
139
140 pub semantic_tokens: Option<SemanticTokenStore>,
142
143 pub language: String,
145}
146
147impl EditorState {
148 pub fn new(
153 _width: u16,
154 _height: u16,
155 large_file_threshold: usize,
156 fs: Arc<dyn FileSystem + Send + Sync>,
157 ) -> Self {
158 Self {
159 buffer: Buffer::new(large_file_threshold, fs),
160 cursors: Cursors::new(),
161 highlighter: HighlightEngine::None, indent_calculator: RefCell::new(IndentCalculator::new()),
163 overlays: OverlayManager::new(),
164 marker_list: MarkerList::new(),
165 virtual_texts: VirtualTextManager::new(),
166 popups: PopupManager::new(),
167 margins: MarginManager::new(),
168 primary_cursor_line_number: LineNumber::Absolute(0), mode: "insert".to_string(),
170 text_properties: TextPropertyManager::new(),
171 show_cursors: true,
172 editing_disabled: false,
173 is_composite_buffer: false,
174 show_whitespace_tabs: true,
175 use_tabs: false,
176 tab_size: 4, reference_highlighter: ReferenceHighlighter::new(),
178 view_mode: ViewMode::Source,
179 debug_highlight_mode: false,
180 compose_width: None,
181 compose_prev_line_numbers: None,
182 compose_column_guides: None,
183 view_transform: None,
184 reference_highlight_overlay: ReferenceHighlightOverlay::new(),
185 bracket_highlight_overlay: BracketHighlightOverlay::new(),
186 semantic_tokens: None,
187 language: "text".to_string(), }
189 }
190
191 pub fn set_language_from_name(&mut self, name: &str, registry: &GrammarRegistry) {
194 let cleaned_name = name.trim_matches('*');
198 let filename = if let Some(pos) = cleaned_name.rfind(':') {
199 &cleaned_name[pos + 1..]
201 } else {
202 cleaned_name
203 };
204
205 let path = std::path::Path::new(filename);
206 self.highlighter = HighlightEngine::for_file(path, registry);
207 if let Some(language) = Language::from_path(path) {
208 self.reference_highlighter.set_language(&language);
209 self.language = language.to_string();
210 } else {
211 self.language = "text".to_string();
212 }
213 tracing::debug!(
214 "Set highlighter for virtual buffer based on name: {} -> {} (backend: {}, language: {})",
215 name,
216 filename,
217 self.highlighter.backend_name(),
218 self.language
219 );
220 }
221
222 pub fn from_file(
227 path: &std::path::Path,
228 _width: u16,
229 _height: u16,
230 large_file_threshold: usize,
231 registry: &GrammarRegistry,
232 fs: Arc<dyn FileSystem + Send + Sync>,
233 ) -> anyhow::Result<Self> {
234 let buffer = Buffer::load_from_file(path, large_file_threshold, fs)?;
235
236 let highlighter = HighlightEngine::for_file(path, registry);
237 let language = Language::from_path(path);
238 let mut reference_highlighter = ReferenceHighlighter::new();
239 let language_name = if let Some(lang) = &language {
240 reference_highlighter.set_language(lang);
241 lang.to_string()
242 } else {
243 "text".to_string()
244 };
245
246 let mut marker_list = MarkerList::new();
247 if !buffer.is_empty() {
248 marker_list.adjust_for_insert(0, buffer.len());
249 }
250
251 Ok(Self {
252 buffer,
253 cursors: Cursors::new(),
254 highlighter,
255 indent_calculator: RefCell::new(IndentCalculator::new()),
256 overlays: OverlayManager::new(),
257 marker_list,
258 virtual_texts: VirtualTextManager::new(),
259 popups: PopupManager::new(),
260 margins: MarginManager::new(),
261 primary_cursor_line_number: LineNumber::Absolute(0),
262 mode: "insert".to_string(),
263 text_properties: TextPropertyManager::new(),
264 show_cursors: true,
265 editing_disabled: false,
266 is_composite_buffer: false,
267 show_whitespace_tabs: true,
268 use_tabs: false,
269 tab_size: 4,
270 reference_highlighter,
271 view_mode: ViewMode::Source,
272 debug_highlight_mode: false,
273 compose_width: None,
274 compose_prev_line_numbers: None,
275 compose_column_guides: None,
276 view_transform: None,
277 reference_highlight_overlay: ReferenceHighlightOverlay::new(),
278 bracket_highlight_overlay: BracketHighlightOverlay::new(),
279 semantic_tokens: None,
280 language: language_name,
281 })
282 }
283
284 pub fn from_file_with_languages(
292 path: &std::path::Path,
293 _width: u16,
294 _height: u16,
295 large_file_threshold: usize,
296 registry: &GrammarRegistry,
297 languages: &std::collections::HashMap<String, crate::config::LanguageConfig>,
298 fs: Arc<dyn FileSystem + Send + Sync>,
299 ) -> anyhow::Result<Self> {
300 let buffer = Buffer::load_from_file(path, large_file_threshold, fs)?;
301
302 let highlighter = HighlightEngine::for_file_with_languages(path, registry, languages);
303
304 let language = Language::from_path(path);
305 let mut reference_highlighter = ReferenceHighlighter::new();
306 let language_name = if let Some(lang) = &language {
307 reference_highlighter.set_language(lang);
308 lang.to_string()
309 } else {
310 crate::services::lsp::manager::detect_language(path, languages)
311 .unwrap_or_else(|| "text".to_string())
312 };
313
314 let mut marker_list = MarkerList::new();
315 if !buffer.is_empty() {
316 marker_list.adjust_for_insert(0, buffer.len());
317 }
318
319 Ok(Self {
320 buffer,
321 cursors: Cursors::new(),
322 highlighter,
323 indent_calculator: RefCell::new(IndentCalculator::new()),
324 overlays: OverlayManager::new(),
325 marker_list,
326 virtual_texts: VirtualTextManager::new(),
327 popups: PopupManager::new(),
328 margins: MarginManager::new(),
329 primary_cursor_line_number: LineNumber::Absolute(0),
330 mode: "insert".to_string(),
331 text_properties: TextPropertyManager::new(),
332 show_cursors: true,
333 editing_disabled: false,
334 is_composite_buffer: false,
335 show_whitespace_tabs: true,
336 use_tabs: false,
337 tab_size: 4,
338 reference_highlighter,
339 view_mode: ViewMode::Source,
340 debug_highlight_mode: false,
341 compose_width: None,
342 compose_prev_line_numbers: None,
343 compose_column_guides: None,
344 view_transform: None,
345 reference_highlight_overlay: ReferenceHighlightOverlay::new(),
346 bracket_highlight_overlay: BracketHighlightOverlay::new(),
347 semantic_tokens: None,
348 language: language_name,
349 })
350 }
351
352 fn apply_insert(
354 &mut self,
355 position: usize,
356 text: &str,
357 cursor_id: crate::model::event::CursorId,
358 ) {
359 let newlines_inserted = text.matches('\n').count();
360
361 self.marker_list.adjust_for_insert(position, text.len());
363 self.margins.adjust_for_insert(position, text.len());
364
365 self.buffer.insert(position, text);
367
368 self.highlighter
370 .invalidate_range(position..position + text.len());
371
372 self.cursors.adjust_for_edit(position, 0, text.len());
377
378 if let Some(cursor) = self.cursors.get_mut(cursor_id) {
380 cursor.position = position + text.len();
381 cursor.clear_selection();
382 }
383
384 if cursor_id == self.cursors.primary_id() {
386 self.primary_cursor_line_number = match self.primary_cursor_line_number {
387 LineNumber::Absolute(line) => LineNumber::Absolute(line + newlines_inserted),
388 LineNumber::Relative {
389 line,
390 from_cached_line,
391 } => LineNumber::Relative {
392 line: line + newlines_inserted,
393 from_cached_line,
394 },
395 };
396 }
397 }
398
399 fn apply_delete(
401 &mut self,
402 range: &std::ops::Range<usize>,
403 cursor_id: crate::model::event::CursorId,
404 deleted_text: &str,
405 ) {
406 let len = range.len();
407 let newlines_deleted = deleted_text.matches('\n').count();
408
409 self.marker_list.adjust_for_delete(range.start, len);
411 self.margins.adjust_for_delete(range.start, len);
412
413 self.buffer.delete(range.clone());
415
416 self.highlighter.invalidate_range(range.clone());
418
419 self.cursors.adjust_for_edit(range.start, len, 0);
424
425 if let Some(cursor) = self.cursors.get_mut(cursor_id) {
427 cursor.position = range.start;
428 cursor.clear_selection();
429 }
430
431 if cursor_id == self.cursors.primary_id() {
433 self.primary_cursor_line_number = match self.primary_cursor_line_number {
434 LineNumber::Absolute(line) => {
435 LineNumber::Absolute(line.saturating_sub(newlines_deleted))
436 }
437 LineNumber::Relative {
438 line,
439 from_cached_line,
440 } => LineNumber::Relative {
441 line: line.saturating_sub(newlines_deleted),
442 from_cached_line,
443 },
444 };
445 }
446 }
447
448 pub fn apply(&mut self, event: &Event) {
451 match event {
452 Event::Insert {
453 position,
454 text,
455 cursor_id,
456 } => self.apply_insert(*position, text, *cursor_id),
457
458 Event::Delete {
459 range,
460 cursor_id,
461 deleted_text,
462 } => self.apply_delete(range, *cursor_id, deleted_text),
463
464 Event::MoveCursor {
465 cursor_id,
466 new_position,
467 new_anchor,
468 new_sticky_column,
469 ..
470 } => {
471 if let Some(cursor) = self.cursors.get_mut(*cursor_id) {
472 cursor.position = *new_position;
473 cursor.anchor = *new_anchor;
474 cursor.sticky_column = *new_sticky_column;
475 }
476
477 if *cursor_id == self.cursors.primary_id() {
480 self.primary_cursor_line_number =
481 match self.buffer.offset_to_position(*new_position) {
482 Some(pos) => LineNumber::Absolute(pos.line),
483 None => {
484 let estimated_line = *new_position / 80;
487 LineNumber::Absolute(estimated_line)
488 }
489 };
490 }
491 }
492
493 Event::AddCursor {
494 cursor_id,
495 position,
496 anchor,
497 } => {
498 let cursor = if let Some(anchor) = anchor {
499 Cursor::with_selection(*anchor, *position)
500 } else {
501 Cursor::new(*position)
502 };
503
504 self.cursors.insert_with_id(*cursor_id, cursor);
507
508 self.cursors.normalize();
509 }
510
511 Event::RemoveCursor { cursor_id, .. } => {
512 self.cursors.remove(*cursor_id);
513 }
514
515 Event::Scroll { .. } | Event::SetViewport { .. } | Event::Recenter => {
518 tracing::warn!("View event {:?} reached EditorState.apply() - should be handled by SplitViewState", event);
521 }
522
523 Event::SetAnchor {
524 cursor_id,
525 position,
526 } => {
527 if let Some(cursor) = self.cursors.get_mut(*cursor_id) {
530 cursor.anchor = Some(*position);
531 cursor.deselect_on_move = false;
532 }
533 }
534
535 Event::ClearAnchor { cursor_id } => {
536 if let Some(cursor) = self.cursors.get_mut(*cursor_id) {
539 cursor.anchor = None;
540 cursor.deselect_on_move = true;
541 cursor.clear_block_selection();
542 }
543 }
544
545 Event::ChangeMode { mode } => {
546 self.mode = mode.clone();
547 }
548
549 Event::AddOverlay {
550 namespace,
551 range,
552 face,
553 priority,
554 message,
555 extend_to_line_end,
556 } => {
557 tracing::debug!(
558 "AddOverlay: namespace={:?}, range={:?}, face={:?}, priority={}",
559 namespace,
560 range,
561 face,
562 priority
563 );
564 let overlay_face = convert_event_face_to_overlay_face(face);
566 tracing::trace!("Converted face: {:?}", overlay_face);
567
568 let mut overlay = Overlay::with_priority(
569 &mut self.marker_list,
570 range.clone(),
571 overlay_face,
572 *priority,
573 );
574 overlay.namespace = namespace.clone();
575 overlay.message = message.clone();
576 overlay.extend_to_line_end = *extend_to_line_end;
577
578 let actual_range = overlay.range(&self.marker_list);
579 tracing::debug!(
580 "Created overlay with markers - actual range: {:?}, handle={:?}",
581 actual_range,
582 overlay.handle
583 );
584
585 self.overlays.add(overlay);
586 }
587
588 Event::RemoveOverlay { handle } => {
589 tracing::debug!("RemoveOverlay: handle={:?}", handle);
590 self.overlays
591 .remove_by_handle(handle, &mut self.marker_list);
592 }
593
594 Event::RemoveOverlaysInRange { range } => {
595 self.overlays.remove_in_range(range, &mut self.marker_list);
596 }
597
598 Event::ClearNamespace { namespace } => {
599 tracing::debug!("ClearNamespace: namespace={:?}", namespace);
600 self.overlays
601 .clear_namespace(namespace, &mut self.marker_list);
602 }
603
604 Event::ClearOverlays => {
605 self.overlays.clear(&mut self.marker_list);
606 }
607
608 Event::ShowPopup { popup } => {
609 let popup_obj = convert_popup_data_to_popup(popup);
610 self.popups.show(popup_obj);
611 }
612
613 Event::HidePopup => {
614 self.popups.hide();
615 }
616
617 Event::ClearPopups => {
618 self.popups.clear();
619 }
620
621 Event::PopupSelectNext => {
622 if let Some(popup) = self.popups.top_mut() {
623 popup.select_next();
624 }
625 }
626
627 Event::PopupSelectPrev => {
628 if let Some(popup) = self.popups.top_mut() {
629 popup.select_prev();
630 }
631 }
632
633 Event::PopupPageDown => {
634 if let Some(popup) = self.popups.top_mut() {
635 popup.page_down();
636 }
637 }
638
639 Event::PopupPageUp => {
640 if let Some(popup) = self.popups.top_mut() {
641 popup.page_up();
642 }
643 }
644
645 Event::AddMarginAnnotation {
646 line,
647 position,
648 content,
649 annotation_id,
650 } => {
651 let margin_position = convert_margin_position(position);
652 let margin_content = convert_margin_content(content);
653 let annotation = if let Some(id) = annotation_id {
654 MarginAnnotation::with_id(*line, margin_position, margin_content, id.clone())
655 } else {
656 MarginAnnotation::new(*line, margin_position, margin_content)
657 };
658 self.margins.add_annotation(annotation);
659 }
660
661 Event::RemoveMarginAnnotation { annotation_id } => {
662 self.margins.remove_by_id(annotation_id);
663 }
664
665 Event::RemoveMarginAnnotationsAtLine { line, position } => {
666 let margin_position = convert_margin_position(position);
667 self.margins.remove_at_line(*line, margin_position);
668 }
669
670 Event::ClearMarginPosition { position } => {
671 let margin_position = convert_margin_position(position);
672 self.margins.clear_position(margin_position);
673 }
674
675 Event::ClearMargins => {
676 self.margins.clear_all();
677 }
678
679 Event::SetLineNumbers { enabled } => {
680 self.margins.set_line_numbers(*enabled);
681 }
682
683 Event::SplitPane { .. }
686 | Event::CloseSplit { .. }
687 | Event::SetActiveSplit { .. }
688 | Event::AdjustSplitRatio { .. }
689 | Event::NextSplit
690 | Event::PrevSplit => {
691 }
693
694 Event::Batch { events, .. } => {
695 for event in events {
698 self.apply(event);
699 }
700 }
701
702 Event::BulkEdit {
703 new_tree,
704 new_cursors,
705 ..
706 } => {
707 if let Some(tree) = new_tree {
712 self.buffer.restore_piece_tree(tree);
713 }
714
715 for (cursor_id, position, anchor) in new_cursors {
717 if let Some(cursor) = self.cursors.get_mut(*cursor_id) {
718 cursor.position = *position;
719 cursor.anchor = *anchor;
720 }
721 }
722
723 self.highlighter.invalidate_all();
725
726 let primary_pos = self.cursors.primary().position;
728 self.primary_cursor_line_number = match self.buffer.offset_to_position(primary_pos)
729 {
730 Some(pos) => crate::model::buffer::LineNumber::Absolute(pos.line),
731 None => crate::model::buffer::LineNumber::Absolute(0),
732 };
733 }
734 }
735 }
736
737 pub fn apply_many(&mut self, events: &[Event]) {
739 for event in events {
740 self.apply(event);
741 }
742 }
743
744 pub fn primary_cursor(&self) -> &Cursor {
746 self.cursors.primary()
747 }
748
749 pub fn primary_cursor_mut(&mut self) -> &mut Cursor {
751 self.cursors.primary_mut()
752 }
753
754 pub fn on_focus_lost(&mut self) {
758 if self.popups.dismiss_transient() {
759 tracing::debug!("Dismissed transient popup on buffer focus loss");
760 }
761 }
762}
763
764fn convert_event_face_to_overlay_face(event_face: &EventOverlayFace) -> OverlayFace {
766 match event_face {
767 EventOverlayFace::Underline { color, style } => {
768 let underline_style = match style {
769 crate::model::event::UnderlineStyle::Straight => UnderlineStyle::Straight,
770 crate::model::event::UnderlineStyle::Wavy => UnderlineStyle::Wavy,
771 crate::model::event::UnderlineStyle::Dotted => UnderlineStyle::Dotted,
772 crate::model::event::UnderlineStyle::Dashed => UnderlineStyle::Dashed,
773 };
774 OverlayFace::Underline {
775 color: Color::Rgb(color.0, color.1, color.2),
776 style: underline_style,
777 }
778 }
779 EventOverlayFace::Background { color } => OverlayFace::Background {
780 color: Color::Rgb(color.0, color.1, color.2),
781 },
782 EventOverlayFace::Foreground { color } => OverlayFace::Foreground {
783 color: Color::Rgb(color.0, color.1, color.2),
784 },
785 EventOverlayFace::Style { options } => {
786 use ratatui::style::Modifier;
787
788 let mut style = Style::default();
790
791 if let Some(ref fg) = options.fg {
793 if let Some((r, g, b)) = fg.as_rgb() {
794 style = style.fg(Color::Rgb(r, g, b));
795 }
796 }
797
798 if let Some(ref bg) = options.bg {
800 if let Some((r, g, b)) = bg.as_rgb() {
801 style = style.bg(Color::Rgb(r, g, b));
802 }
803 }
804
805 let mut modifiers = Modifier::empty();
807 if options.bold {
808 modifiers |= Modifier::BOLD;
809 }
810 if options.italic {
811 modifiers |= Modifier::ITALIC;
812 }
813 if options.underline {
814 modifiers |= Modifier::UNDERLINED;
815 }
816 if !modifiers.is_empty() {
817 style = style.add_modifier(modifiers);
818 }
819
820 let fg_theme = options
822 .fg
823 .as_ref()
824 .and_then(|c| c.as_theme_key())
825 .map(String::from);
826 let bg_theme = options
827 .bg
828 .as_ref()
829 .and_then(|c| c.as_theme_key())
830 .map(String::from);
831
832 if fg_theme.is_some() || bg_theme.is_some() {
834 OverlayFace::ThemedStyle {
835 fallback_style: style,
836 fg_theme,
837 bg_theme,
838 }
839 } else {
840 OverlayFace::Style { style }
841 }
842 }
843 }
844}
845
846fn convert_popup_data_to_popup(data: &PopupData) -> Popup {
848 let content = match &data.content {
849 crate::model::event::PopupContentData::Text(lines) => PopupContent::Text(lines.clone()),
850 crate::model::event::PopupContentData::List { items, selected } => PopupContent::List {
851 items: items
852 .iter()
853 .map(|item| PopupListItem {
854 text: item.text.clone(),
855 detail: item.detail.clone(),
856 icon: item.icon.clone(),
857 data: item.data.clone(),
858 })
859 .collect(),
860 selected: *selected,
861 },
862 };
863
864 let position = match data.position {
865 PopupPositionData::AtCursor => PopupPosition::AtCursor,
866 PopupPositionData::BelowCursor => PopupPosition::BelowCursor,
867 PopupPositionData::AboveCursor => PopupPosition::AboveCursor,
868 PopupPositionData::Fixed { x, y } => PopupPosition::Fixed { x, y },
869 PopupPositionData::Centered => PopupPosition::Centered,
870 PopupPositionData::BottomRight => PopupPosition::BottomRight,
871 };
872
873 let completion_title = t!("lsp.popup_completion").to_string();
875 let kind = if data.title.as_ref() == Some(&completion_title) {
876 PopupKind::Completion
877 } else {
878 match &content {
879 PopupContent::List { .. } => PopupKind::List,
880 PopupContent::Text(_) => PopupKind::Text,
881 PopupContent::Markdown(_) => PopupKind::Text,
882 PopupContent::Custom(_) => PopupKind::Text,
883 }
884 };
885
886 Popup {
887 kind,
888 title: data.title.clone(),
889 description: data.description.clone(),
890 transient: data.transient,
891 content,
892 position,
893 width: data.width,
894 max_height: data.max_height,
895 bordered: data.bordered,
896 border_style: Style::default().fg(Color::Gray),
897 background_style: Style::default().bg(Color::Rgb(30, 30, 30)),
898 scroll_offset: 0,
899 text_selection: None,
900 }
901}
902
903fn convert_margin_position(position: &MarginPositionData) -> MarginPosition {
905 match position {
906 MarginPositionData::Left => MarginPosition::Left,
907 MarginPositionData::Right => MarginPosition::Right,
908 }
909}
910
911fn convert_margin_content(content: &MarginContentData) -> MarginContent {
913 match content {
914 MarginContentData::Text(text) => MarginContent::Text(text.clone()),
915 MarginContentData::Symbol { text, color } => {
916 if let Some((r, g, b)) = color {
917 MarginContent::colored_symbol(text.clone(), Color::Rgb(*r, *g, *b))
918 } else {
919 MarginContent::symbol(text.clone(), Style::default())
920 }
921 }
922 MarginContentData::Empty => MarginContent::Empty,
923 }
924}
925
926impl EditorState {
927 pub fn prepare_for_render(&mut self, top_byte: usize, height: u16) -> Result<()> {
934 self.buffer.prepare_viewport(top_byte, height as usize)?;
935 Ok(())
936 }
937
938 pub fn get_text_range(&mut self, start: usize, end: usize) -> String {
958 match self
960 .buffer
961 .get_text_range_mut(start, end.saturating_sub(start))
962 {
963 Ok(bytes) => String::from_utf8_lossy(&bytes).into_owned(),
964 Err(e) => {
965 tracing::warn!("Failed to get text range {}..{}: {}", start, end, e);
966 String::new()
967 }
968 }
969 }
970
971 pub fn get_line_at_offset(&mut self, offset: usize) -> Option<(usize, String)> {
979 use crate::model::document_model::DocumentModel;
980
981 let mut line_start = offset;
984 while line_start > 0 {
985 if let Ok(text) = self.buffer.get_text_range_mut(line_start - 1, 1) {
986 if text.first() == Some(&b'\n') {
987 break;
988 }
989 line_start -= 1;
990 } else {
991 break;
992 }
993 }
994
995 let viewport = self
997 .get_viewport_content(
998 crate::model::document_model::DocumentPosition::byte(line_start),
999 1,
1000 )
1001 .ok()?;
1002
1003 viewport
1004 .lines
1005 .first()
1006 .map(|line| (line.byte_offset, line.content.clone()))
1007 }
1008
1009 pub fn get_text_to_end_of_line(&mut self, cursor_pos: usize) -> Result<String> {
1014 use crate::model::document_model::DocumentModel;
1015
1016 let viewport = self.get_viewport_content(
1018 crate::model::document_model::DocumentPosition::byte(cursor_pos),
1019 1,
1020 )?;
1021
1022 if let Some(line) = viewport.lines.first() {
1023 let line_start = line.byte_offset;
1024 let line_end = line_start + line.content.len();
1025
1026 if cursor_pos >= line_start && cursor_pos <= line_end {
1027 let offset_in_line = cursor_pos - line_start;
1028 Ok(line.content.get(offset_in_line..).unwrap_or("").to_string())
1030 } else {
1031 Ok(String::new())
1032 }
1033 } else {
1034 Ok(String::new())
1035 }
1036 }
1037
1038 pub fn set_semantic_tokens(&mut self, store: SemanticTokenStore) {
1040 self.semantic_tokens = Some(store);
1041 }
1042
1043 pub fn clear_semantic_tokens(&mut self) {
1045 self.semantic_tokens = None;
1046 }
1047
1048 pub fn semantic_tokens_result_id(&self) -> Option<&str> {
1050 self.semantic_tokens
1051 .as_ref()
1052 .and_then(|store| store.result_id.as_deref())
1053 }
1054}
1055
1056impl DocumentModel for EditorState {
1061 fn capabilities(&self) -> DocumentCapabilities {
1062 let line_count = self.buffer.line_count();
1063 DocumentCapabilities {
1064 has_line_index: line_count.is_some(),
1065 uses_lazy_loading: false, byte_length: self.buffer.len(),
1067 approximate_line_count: line_count.unwrap_or_else(|| {
1068 self.buffer.len() / 80
1070 }),
1071 }
1072 }
1073
1074 fn get_viewport_content(
1075 &mut self,
1076 start_pos: DocumentPosition,
1077 max_lines: usize,
1078 ) -> Result<ViewportContent> {
1079 let start_offset = self.position_to_offset(start_pos)?;
1081
1082 let line_iter = self.buffer.iter_lines_from(start_offset, max_lines)?;
1085 let has_more = line_iter.has_more;
1086
1087 let lines = line_iter
1088 .map(|line_data| ViewportLine {
1089 byte_offset: line_data.byte_offset,
1090 content: line_data.content,
1091 has_newline: line_data.has_newline,
1092 approximate_line_number: line_data.line_number,
1093 })
1094 .collect();
1095
1096 Ok(ViewportContent {
1097 start_position: DocumentPosition::ByteOffset(start_offset),
1098 lines,
1099 has_more,
1100 })
1101 }
1102
1103 fn position_to_offset(&self, pos: DocumentPosition) -> Result<usize> {
1104 match pos {
1105 DocumentPosition::ByteOffset(offset) => Ok(offset),
1106 DocumentPosition::LineColumn { line, column } => {
1107 if !self.has_line_index() {
1108 anyhow::bail!("Line indexing not available for this document");
1109 }
1110 let position = crate::model::piece_tree::Position { line, column };
1112 Ok(self.buffer.position_to_offset(position))
1113 }
1114 }
1115 }
1116
1117 fn offset_to_position(&self, offset: usize) -> DocumentPosition {
1118 if self.has_line_index() {
1119 if let Some(pos) = self.buffer.offset_to_position(offset) {
1120 DocumentPosition::LineColumn {
1121 line: pos.line,
1122 column: pos.column,
1123 }
1124 } else {
1125 DocumentPosition::ByteOffset(offset)
1127 }
1128 } else {
1129 DocumentPosition::ByteOffset(offset)
1130 }
1131 }
1132
1133 fn get_range(&mut self, start: DocumentPosition, end: DocumentPosition) -> Result<String> {
1134 let start_offset = self.position_to_offset(start)?;
1135 let end_offset = self.position_to_offset(end)?;
1136
1137 if start_offset > end_offset {
1138 anyhow::bail!(
1139 "Invalid range: start offset {} > end offset {}",
1140 start_offset,
1141 end_offset
1142 );
1143 }
1144
1145 let bytes = self
1146 .buffer
1147 .get_text_range_mut(start_offset, end_offset - start_offset)?;
1148
1149 Ok(String::from_utf8_lossy(&bytes).into_owned())
1150 }
1151
1152 fn get_line_content(&mut self, line_number: usize) -> Option<String> {
1153 if !self.has_line_index() {
1154 return None;
1155 }
1156
1157 let line_start_offset = self.buffer.line_start_offset(line_number)?;
1159
1160 let mut iter = self.buffer.line_iterator(line_start_offset, 80);
1162 if let Some((_start, content)) = iter.next_line() {
1163 let has_newline = content.ends_with('\n');
1164 let line_content = if has_newline {
1165 content[..content.len() - 1].to_string()
1166 } else {
1167 content
1168 };
1169 Some(line_content)
1170 } else {
1171 None
1172 }
1173 }
1174
1175 fn get_chunk_at_offset(&mut self, offset: usize, size: usize) -> Result<(usize, String)> {
1176 let bytes = self.buffer.get_text_range_mut(offset, size)?;
1177
1178 Ok((offset, String::from_utf8_lossy(&bytes).into_owned()))
1179 }
1180
1181 fn insert(&mut self, pos: DocumentPosition, text: &str) -> Result<usize> {
1182 let offset = self.position_to_offset(pos)?;
1183 self.buffer.insert_bytes(offset, text.as_bytes().to_vec());
1184 Ok(text.len())
1185 }
1186
1187 fn delete(&mut self, start: DocumentPosition, end: DocumentPosition) -> Result<()> {
1188 let start_offset = self.position_to_offset(start)?;
1189 let end_offset = self.position_to_offset(end)?;
1190
1191 if start_offset > end_offset {
1192 anyhow::bail!(
1193 "Invalid range: start offset {} > end offset {}",
1194 start_offset,
1195 end_offset
1196 );
1197 }
1198
1199 self.buffer.delete(start_offset..end_offset);
1200 Ok(())
1201 }
1202
1203 fn replace(
1204 &mut self,
1205 start: DocumentPosition,
1206 end: DocumentPosition,
1207 text: &str,
1208 ) -> Result<()> {
1209 self.delete(start, end)?;
1211 self.insert(start, text)?;
1212 Ok(())
1213 }
1214
1215 fn find_matches(
1216 &mut self,
1217 pattern: &str,
1218 search_range: Option<(DocumentPosition, DocumentPosition)>,
1219 ) -> Result<Vec<usize>> {
1220 let (start_offset, end_offset) = if let Some((start, end)) = search_range {
1221 (
1222 self.position_to_offset(start)?,
1223 self.position_to_offset(end)?,
1224 )
1225 } else {
1226 (0, self.buffer.len())
1227 };
1228
1229 let bytes = self
1231 .buffer
1232 .get_text_range_mut(start_offset, end_offset - start_offset)?;
1233 let text = String::from_utf8_lossy(&bytes);
1234
1235 let mut matches = Vec::new();
1237 let mut search_offset = 0;
1238 while let Some(pos) = text[search_offset..].find(pattern) {
1239 matches.push(start_offset + search_offset + pos);
1240 search_offset += pos + pattern.len();
1241 }
1242
1243 Ok(matches)
1244 }
1245}
1246
1247#[derive(Clone, Debug)]
1249pub struct SemanticTokenStore {
1250 pub version: u64,
1252 pub result_id: Option<String>,
1254 pub data: Vec<u32>,
1256 pub tokens: Vec<SemanticTokenSpan>,
1258}
1259
1260#[derive(Clone, Debug)]
1262pub struct SemanticTokenSpan {
1263 pub range: Range<usize>,
1264 pub token_type: String,
1265 pub modifiers: Vec<String>,
1266}
1267
1268#[cfg(test)]
1269mod tests {
1270 use crate::model::filesystem::StdFileSystem;
1271 use std::sync::Arc;
1272
1273 fn test_fs() -> Arc<dyn crate::model::filesystem::FileSystem + Send + Sync> {
1274 Arc::new(StdFileSystem)
1275 }
1276 use super::*;
1277 use crate::model::event::CursorId;
1278
1279 #[test]
1280 fn test_state_new() {
1281 let state = EditorState::new(
1282 80,
1283 24,
1284 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1285 test_fs(),
1286 );
1287 assert!(state.buffer.is_empty());
1288 assert_eq!(state.cursors.count(), 1);
1289 assert_eq!(state.cursors.primary().position, 0);
1290 }
1291
1292 #[test]
1293 fn test_apply_insert() {
1294 let mut state = EditorState::new(
1295 80,
1296 24,
1297 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1298 test_fs(),
1299 );
1300 let cursor_id = state.cursors.primary_id();
1301
1302 state.apply(&Event::Insert {
1303 position: 0,
1304 text: "hello".to_string(),
1305 cursor_id,
1306 });
1307
1308 assert_eq!(state.buffer.to_string().unwrap(), "hello");
1309 assert_eq!(state.cursors.primary().position, 5);
1310 assert!(state.buffer.is_modified());
1311 }
1312
1313 #[test]
1314 fn test_apply_delete() {
1315 let mut state = EditorState::new(
1316 80,
1317 24,
1318 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1319 test_fs(),
1320 );
1321 let cursor_id = state.cursors.primary_id();
1322
1323 state.apply(&Event::Insert {
1325 position: 0,
1326 text: "hello world".to_string(),
1327 cursor_id,
1328 });
1329
1330 state.apply(&Event::Delete {
1331 range: 5..11,
1332 deleted_text: " world".to_string(),
1333 cursor_id,
1334 });
1335
1336 assert_eq!(state.buffer.to_string().unwrap(), "hello");
1337 assert_eq!(state.cursors.primary().position, 5);
1338 }
1339
1340 #[test]
1341 fn test_apply_move_cursor() {
1342 let mut state = EditorState::new(
1343 80,
1344 24,
1345 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1346 test_fs(),
1347 );
1348 let cursor_id = state.cursors.primary_id();
1349
1350 state.apply(&Event::Insert {
1351 position: 0,
1352 text: "hello".to_string(),
1353 cursor_id,
1354 });
1355
1356 state.apply(&Event::MoveCursor {
1357 cursor_id,
1358 old_position: 5,
1359 new_position: 2,
1360 old_anchor: None,
1361 new_anchor: None,
1362 old_sticky_column: 0,
1363 new_sticky_column: 0,
1364 });
1365
1366 assert_eq!(state.cursors.primary().position, 2);
1367 }
1368
1369 #[test]
1370 fn test_apply_add_cursor() {
1371 let mut state = EditorState::new(
1372 80,
1373 24,
1374 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1375 test_fs(),
1376 );
1377 let cursor_id = CursorId(1);
1378
1379 state.apply(&Event::AddCursor {
1380 cursor_id,
1381 position: 5,
1382 anchor: None,
1383 });
1384
1385 assert_eq!(state.cursors.count(), 2);
1386 }
1387
1388 #[test]
1389 fn test_apply_many() {
1390 let mut state = EditorState::new(
1391 80,
1392 24,
1393 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1394 test_fs(),
1395 );
1396 let cursor_id = state.cursors.primary_id();
1397
1398 let events = vec![
1399 Event::Insert {
1400 position: 0,
1401 text: "hello ".to_string(),
1402 cursor_id,
1403 },
1404 Event::Insert {
1405 position: 6,
1406 text: "world".to_string(),
1407 cursor_id,
1408 },
1409 ];
1410
1411 state.apply_many(&events);
1412
1413 assert_eq!(state.buffer.to_string().unwrap(), "hello world");
1414 }
1415
1416 #[test]
1417 fn test_cursor_adjustment_after_insert() {
1418 let mut state = EditorState::new(
1419 80,
1420 24,
1421 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1422 test_fs(),
1423 );
1424 let cursor_id = state.cursors.primary_id();
1425
1426 state.apply(&Event::AddCursor {
1428 cursor_id: CursorId(1),
1429 position: 5,
1430 anchor: None,
1431 });
1432
1433 state.apply(&Event::Insert {
1435 position: 0,
1436 text: "abc".to_string(),
1437 cursor_id,
1438 });
1439
1440 if let Some(cursor) = state.cursors.get(CursorId(1)) {
1442 assert_eq!(cursor.position, 8);
1443 }
1444 }
1445
1446 mod document_model_tests {
1448 use super::*;
1449 use crate::model::document_model::{DocumentModel, DocumentPosition};
1450
1451 #[test]
1452 fn test_capabilities_small_file() {
1453 let mut state = EditorState::new(
1454 80,
1455 24,
1456 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1457 test_fs(),
1458 );
1459 state.buffer = Buffer::from_str_test("line1\nline2\nline3");
1460
1461 let caps = state.capabilities();
1462 assert!(caps.has_line_index, "Small file should have line index");
1463 assert_eq!(caps.byte_length, "line1\nline2\nline3".len());
1464 assert_eq!(caps.approximate_line_count, 3, "Should have 3 lines");
1465 }
1466
1467 #[test]
1468 fn test_position_conversions() {
1469 let mut state = EditorState::new(
1470 80,
1471 24,
1472 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1473 test_fs(),
1474 );
1475 state.buffer = Buffer::from_str_test("hello\nworld\ntest");
1476
1477 let pos1 = DocumentPosition::ByteOffset(6);
1479 let offset1 = state.position_to_offset(pos1).unwrap();
1480 assert_eq!(offset1, 6);
1481
1482 let pos2 = DocumentPosition::LineColumn { line: 1, column: 0 };
1484 let offset2 = state.position_to_offset(pos2).unwrap();
1485 assert_eq!(offset2, 6, "Line 1, column 0 should be at byte 6");
1486
1487 let converted = state.offset_to_position(6);
1489 match converted {
1490 DocumentPosition::LineColumn { line, column } => {
1491 assert_eq!(line, 1);
1492 assert_eq!(column, 0);
1493 }
1494 _ => panic!("Expected LineColumn for small file"),
1495 }
1496 }
1497
1498 #[test]
1499 fn test_get_viewport_content() {
1500 let mut state = EditorState::new(
1501 80,
1502 24,
1503 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1504 test_fs(),
1505 );
1506 state.buffer = Buffer::from_str_test("line1\nline2\nline3\nline4\nline5");
1507
1508 let content = state
1509 .get_viewport_content(DocumentPosition::ByteOffset(0), 3)
1510 .unwrap();
1511
1512 assert_eq!(content.lines.len(), 3);
1513 assert_eq!(content.lines[0].content, "line1");
1514 assert_eq!(content.lines[1].content, "line2");
1515 assert_eq!(content.lines[2].content, "line3");
1516 assert!(content.has_more);
1517 }
1518
1519 #[test]
1520 fn test_get_range() {
1521 let mut state = EditorState::new(
1522 80,
1523 24,
1524 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1525 test_fs(),
1526 );
1527 state.buffer = Buffer::from_str_test("hello world");
1528
1529 let text = state
1530 .get_range(
1531 DocumentPosition::ByteOffset(0),
1532 DocumentPosition::ByteOffset(5),
1533 )
1534 .unwrap();
1535 assert_eq!(text, "hello");
1536
1537 let text2 = state
1538 .get_range(
1539 DocumentPosition::ByteOffset(6),
1540 DocumentPosition::ByteOffset(11),
1541 )
1542 .unwrap();
1543 assert_eq!(text2, "world");
1544 }
1545
1546 #[test]
1547 fn test_get_line_content() {
1548 let mut state = EditorState::new(
1549 80,
1550 24,
1551 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1552 test_fs(),
1553 );
1554 state.buffer = Buffer::from_str_test("line1\nline2\nline3");
1555
1556 let line0 = state.get_line_content(0).unwrap();
1557 assert_eq!(line0, "line1");
1558
1559 let line1 = state.get_line_content(1).unwrap();
1560 assert_eq!(line1, "line2");
1561
1562 let line2 = state.get_line_content(2).unwrap();
1563 assert_eq!(line2, "line3");
1564 }
1565
1566 #[test]
1567 fn test_insert_delete() {
1568 let mut state = EditorState::new(
1569 80,
1570 24,
1571 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1572 test_fs(),
1573 );
1574 state.buffer = Buffer::from_str_test("hello world");
1575
1576 let bytes_inserted = state
1578 .insert(DocumentPosition::ByteOffset(6), "beautiful ")
1579 .unwrap();
1580 assert_eq!(bytes_inserted, 10);
1581 assert_eq!(state.buffer.to_string().unwrap(), "hello beautiful world");
1582
1583 state
1585 .delete(
1586 DocumentPosition::ByteOffset(6),
1587 DocumentPosition::ByteOffset(16),
1588 )
1589 .unwrap();
1590 assert_eq!(state.buffer.to_string().unwrap(), "hello world");
1591 }
1592
1593 #[test]
1594 fn test_replace() {
1595 let mut state = EditorState::new(
1596 80,
1597 24,
1598 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1599 test_fs(),
1600 );
1601 state.buffer = Buffer::from_str_test("hello world");
1602
1603 state
1604 .replace(
1605 DocumentPosition::ByteOffset(0),
1606 DocumentPosition::ByteOffset(5),
1607 "hi",
1608 )
1609 .unwrap();
1610 assert_eq!(state.buffer.to_string().unwrap(), "hi world");
1611 }
1612
1613 #[test]
1614 fn test_find_matches() {
1615 let mut state = EditorState::new(
1616 80,
1617 24,
1618 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1619 test_fs(),
1620 );
1621 state.buffer = Buffer::from_str_test("hello world hello");
1622
1623 let matches = state.find_matches("hello", None).unwrap();
1624 assert_eq!(matches.len(), 2);
1625 assert_eq!(matches[0], 0);
1626 assert_eq!(matches[1], 12);
1627 }
1628
1629 #[test]
1630 fn test_prepare_for_render() {
1631 let mut state = EditorState::new(
1632 80,
1633 24,
1634 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1635 test_fs(),
1636 );
1637 state.buffer = Buffer::from_str_test("line1\nline2\nline3\nline4\nline5");
1638
1639 state.prepare_for_render(0, 24).unwrap();
1641 }
1642
1643 #[test]
1644 fn test_helper_get_text_range() {
1645 let mut state = EditorState::new(
1646 80,
1647 24,
1648 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1649 test_fs(),
1650 );
1651 state.buffer = Buffer::from_str_test("hello world");
1652
1653 let text = state.get_text_range(0, 5);
1655 assert_eq!(text, "hello");
1656
1657 let text2 = state.get_text_range(6, 11);
1659 assert_eq!(text2, "world");
1660 }
1661
1662 #[test]
1663 fn test_helper_get_line_at_offset() {
1664 let mut state = EditorState::new(
1665 80,
1666 24,
1667 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1668 test_fs(),
1669 );
1670 state.buffer = Buffer::from_str_test("line1\nline2\nline3");
1671
1672 let (offset, content) = state.get_line_at_offset(0).unwrap();
1674 assert_eq!(offset, 0);
1675 assert_eq!(content, "line1");
1676
1677 let (offset2, content2) = state.get_line_at_offset(8).unwrap();
1679 assert_eq!(offset2, 6); assert_eq!(content2, "line2");
1681
1682 let (offset3, content3) = state.get_line_at_offset(12).unwrap();
1684 assert_eq!(offset3, 12);
1685 assert_eq!(content3, "line3");
1686 }
1687
1688 #[test]
1689 fn test_helper_get_text_to_end_of_line() {
1690 let mut state = EditorState::new(
1691 80,
1692 24,
1693 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1694 test_fs(),
1695 );
1696 state.buffer = Buffer::from_str_test("hello world\nline2");
1697
1698 let text = state.get_text_to_end_of_line(0).unwrap();
1700 assert_eq!(text, "hello world");
1701
1702 let text2 = state.get_text_to_end_of_line(6).unwrap();
1704 assert_eq!(text2, "world");
1705
1706 let text3 = state.get_text_to_end_of_line(11).unwrap();
1708 assert_eq!(text3, "");
1709
1710 let text4 = state.get_text_to_end_of_line(12).unwrap();
1712 assert_eq!(text4, "line2");
1713 }
1714 }
1715
1716 mod virtual_text_integration_tests {
1718 use super::*;
1719 use crate::view::virtual_text::VirtualTextPosition;
1720 use ratatui::style::Style;
1721
1722 #[test]
1723 fn test_virtual_text_add_and_query() {
1724 let mut state = EditorState::new(
1725 80,
1726 24,
1727 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1728 test_fs(),
1729 );
1730 state.buffer = Buffer::from_str_test("hello world");
1731
1732 if !state.buffer.is_empty() {
1734 state.marker_list.adjust_for_insert(0, state.buffer.len());
1735 }
1736
1737 let vtext_id = state.virtual_texts.add(
1739 &mut state.marker_list,
1740 5,
1741 ": string".to_string(),
1742 Style::default(),
1743 VirtualTextPosition::AfterChar,
1744 0,
1745 );
1746
1747 let results = state.virtual_texts.query_range(&state.marker_list, 0, 11);
1749 assert_eq!(results.len(), 1);
1750 assert_eq!(results[0].0, 5); assert_eq!(results[0].1.text, ": string");
1752
1753 let lookup = state.virtual_texts.build_lookup(&state.marker_list, 0, 11);
1755 assert!(lookup.contains_key(&5));
1756 assert_eq!(lookup[&5].len(), 1);
1757 assert_eq!(lookup[&5][0].text, ": string");
1758
1759 state.virtual_texts.remove(&mut state.marker_list, vtext_id);
1761 assert!(state.virtual_texts.is_empty());
1762 }
1763
1764 #[test]
1765 fn test_virtual_text_position_tracking_on_insert() {
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 if !state.buffer.is_empty() {
1776 state.marker_list.adjust_for_insert(0, state.buffer.len());
1777 }
1778
1779 let _vtext_id = state.virtual_texts.add(
1781 &mut state.marker_list,
1782 6,
1783 "/*param*/".to_string(),
1784 Style::default(),
1785 VirtualTextPosition::BeforeChar,
1786 0,
1787 );
1788
1789 let cursor_id = state.cursors.primary_id();
1791 state.apply(&Event::Insert {
1792 position: 6,
1793 text: "beautiful ".to_string(),
1794 cursor_id,
1795 });
1796
1797 let results = state.virtual_texts.query_range(&state.marker_list, 0, 30);
1799 assert_eq!(results.len(), 1);
1800 assert_eq!(results[0].0, 16); assert_eq!(results[0].1.text, "/*param*/");
1802 }
1803
1804 #[test]
1805 fn test_virtual_text_position_tracking_on_delete() {
1806 let mut state = EditorState::new(
1807 80,
1808 24,
1809 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1810 test_fs(),
1811 );
1812 state.buffer = Buffer::from_str_test("hello beautiful world");
1813
1814 if !state.buffer.is_empty() {
1816 state.marker_list.adjust_for_insert(0, state.buffer.len());
1817 }
1818
1819 let _vtext_id = state.virtual_texts.add(
1821 &mut state.marker_list,
1822 16,
1823 ": string".to_string(),
1824 Style::default(),
1825 VirtualTextPosition::AfterChar,
1826 0,
1827 );
1828
1829 let cursor_id = state.cursors.primary_id();
1831 state.apply(&Event::Delete {
1832 range: 6..16,
1833 deleted_text: "beautiful ".to_string(),
1834 cursor_id,
1835 });
1836
1837 let results = state.virtual_texts.query_range(&state.marker_list, 0, 20);
1839 assert_eq!(results.len(), 1);
1840 assert_eq!(results[0].0, 6); assert_eq!(results[0].1.text, ": string");
1842 }
1843
1844 #[test]
1845 fn test_multiple_virtual_texts_with_priorities() {
1846 let mut state = EditorState::new(
1847 80,
1848 24,
1849 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1850 test_fs(),
1851 );
1852 state.buffer = Buffer::from_str_test("let x = 5");
1853
1854 if !state.buffer.is_empty() {
1856 state.marker_list.adjust_for_insert(0, state.buffer.len());
1857 }
1858
1859 state.virtual_texts.add(
1861 &mut state.marker_list,
1862 5,
1863 ": i32".to_string(),
1864 Style::default(),
1865 VirtualTextPosition::AfterChar,
1866 0, );
1868
1869 state.virtual_texts.add(
1871 &mut state.marker_list,
1872 5,
1873 " /* inferred */".to_string(),
1874 Style::default(),
1875 VirtualTextPosition::AfterChar,
1876 10, );
1878
1879 let lookup = state.virtual_texts.build_lookup(&state.marker_list, 0, 10);
1881 assert!(lookup.contains_key(&5));
1882 let vtexts = &lookup[&5];
1883 assert_eq!(vtexts.len(), 2);
1884 assert_eq!(vtexts[0].text, ": i32");
1886 assert_eq!(vtexts[1].text, " /* inferred */");
1887 }
1888
1889 #[test]
1890 fn test_virtual_text_clear() {
1891 let mut state = EditorState::new(
1892 80,
1893 24,
1894 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1895 test_fs(),
1896 );
1897 state.buffer = Buffer::from_str_test("test");
1898
1899 if !state.buffer.is_empty() {
1901 state.marker_list.adjust_for_insert(0, state.buffer.len());
1902 }
1903
1904 state.virtual_texts.add(
1906 &mut state.marker_list,
1907 0,
1908 "hint1".to_string(),
1909 Style::default(),
1910 VirtualTextPosition::BeforeChar,
1911 0,
1912 );
1913 state.virtual_texts.add(
1914 &mut state.marker_list,
1915 2,
1916 "hint2".to_string(),
1917 Style::default(),
1918 VirtualTextPosition::AfterChar,
1919 0,
1920 );
1921
1922 assert_eq!(state.virtual_texts.len(), 2);
1923
1924 state.virtual_texts.clear(&mut state.marker_list);
1926 assert!(state.virtual_texts.is_empty());
1927
1928 let results = state.virtual_texts.query_range(&state.marker_list, 0, 10);
1930 assert!(results.is_empty());
1931 }
1932 }
1933}