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::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, PartialEq, Eq)]
37pub enum ViewMode {
38 Source,
40 Compose,
42}
43
44#[derive(Debug, Clone)]
55pub struct BufferSettings {
56 pub whitespace: crate::config::WhitespaceVisibility,
59
60 pub use_tabs: bool,
63
64 pub tab_size: usize,
68
69 pub auto_close: bool,
72
73 pub auto_surround: bool,
76}
77
78impl Default for BufferSettings {
79 fn default() -> Self {
80 Self {
81 whitespace: crate::config::WhitespaceVisibility::default(),
82 use_tabs: false,
83 tab_size: 4,
84 auto_close: true,
85 auto_surround: true,
86 }
87 }
88}
89
90pub struct EditorState {
96 pub buffer: Buffer,
98
99 pub highlighter: HighlightEngine,
101
102 pub indent_calculator: RefCell<IndentCalculator>,
104
105 pub overlays: OverlayManager,
107
108 pub marker_list: MarkerList,
110
111 pub virtual_texts: VirtualTextManager,
113
114 pub conceals: ConcealManager,
116
117 pub soft_breaks: SoftBreakManager,
119
120 pub popups: PopupManager,
122
123 pub margins: MarginManager,
125
126 pub primary_cursor_line_number: LineNumber,
129
130 pub mode: String,
132
133 pub text_properties: TextPropertyManager,
136
137 pub show_cursors: bool,
140
141 pub editing_disabled: bool,
145
146 pub buffer_settings: BufferSettings,
149
150 pub reference_highlighter: ReferenceHighlighter,
152
153 pub is_composite_buffer: bool,
155
156 pub debug_highlight_mode: bool,
158
159 pub reference_highlight_overlay: ReferenceHighlightOverlay,
161
162 pub bracket_highlight_overlay: BracketHighlightOverlay,
164
165 pub semantic_tokens: Option<SemanticTokenStore>,
167
168 pub folding_ranges: Vec<FoldingRange>,
170
171 pub language: String,
173}
174
175impl EditorState {
176 pub fn apply_language(&mut self, detected: DetectedLanguage) {
183 self.language = detected.name;
184 self.highlighter = detected.highlighter;
185 if let Some(lang) = &detected.ts_language {
186 self.reference_highlighter.set_language(lang);
187 }
188 }
189
190 fn new_from_buffer(buffer: Buffer) -> Self {
193 let mut marker_list = MarkerList::new();
194 if !buffer.is_empty() {
195 marker_list.adjust_for_insert(0, buffer.len());
196 }
197
198 Self {
199 buffer,
200 highlighter: HighlightEngine::None,
201 indent_calculator: RefCell::new(IndentCalculator::new()),
202 overlays: OverlayManager::new(),
203 marker_list,
204 virtual_texts: VirtualTextManager::new(),
205 conceals: ConcealManager::new(),
206 soft_breaks: SoftBreakManager::new(),
207 popups: PopupManager::new(),
208 margins: MarginManager::new(),
209 primary_cursor_line_number: LineNumber::Absolute(0),
210 mode: "insert".to_string(),
211 text_properties: TextPropertyManager::new(),
212 show_cursors: true,
213 editing_disabled: false,
214 buffer_settings: BufferSettings::default(),
215 reference_highlighter: ReferenceHighlighter::new(),
216 is_composite_buffer: false,
217 debug_highlight_mode: false,
218 reference_highlight_overlay: ReferenceHighlightOverlay::new(),
219 bracket_highlight_overlay: BracketHighlightOverlay::new(),
220 semantic_tokens: None,
221 folding_ranges: Vec::new(),
222 language: "text".to_string(),
223 }
224 }
225
226 pub fn new(
227 _width: u16,
228 _height: u16,
229 large_file_threshold: usize,
230 fs: Arc<dyn FileSystem + Send + Sync>,
231 ) -> Self {
232 Self::new_from_buffer(Buffer::new(large_file_threshold, fs))
233 }
234
235 pub fn new_with_path(
238 large_file_threshold: usize,
239 fs: Arc<dyn FileSystem + Send + Sync>,
240 path: std::path::PathBuf,
241 ) -> Self {
242 Self::new_from_buffer(Buffer::new_with_path(large_file_threshold, fs, path))
243 }
244
245 pub fn set_language_from_name(&mut self, name: &str, registry: &GrammarRegistry) {
249 let detected = DetectedLanguage::from_virtual_name(name, registry);
250 tracing::debug!(
251 "Set highlighter for virtual buffer based on name: {} (backend: {}, language: {})",
252 name,
253 detected.highlighter.backend_name(),
254 detected.name
255 );
256 self.apply_language(detected);
257 }
258
259 pub fn from_file(
264 path: &std::path::Path,
265 _width: u16,
266 _height: u16,
267 large_file_threshold: usize,
268 registry: &GrammarRegistry,
269 fs: Arc<dyn FileSystem + Send + Sync>,
270 ) -> anyhow::Result<Self> {
271 let buffer = Buffer::load_from_file(path, large_file_threshold, fs)?;
272 let detected = DetectedLanguage::from_path_builtin(path, registry);
273 let mut state = Self::new_from_buffer(buffer);
274 state.apply_language(detected);
275 Ok(state)
276 }
277
278 pub fn from_file_with_languages(
286 path: &std::path::Path,
287 _width: u16,
288 _height: u16,
289 large_file_threshold: usize,
290 registry: &GrammarRegistry,
291 languages: &std::collections::HashMap<String, crate::config::LanguageConfig>,
292 fs: Arc<dyn FileSystem + Send + Sync>,
293 ) -> anyhow::Result<Self> {
294 let buffer = Buffer::load_from_file(path, large_file_threshold, fs)?;
295 let detected = DetectedLanguage::from_path(path, registry, languages);
296 let mut state = Self::new_from_buffer(buffer);
297 state.apply_language(detected);
298 Ok(state)
299 }
300
301 pub fn from_buffer_with_language(buffer: Buffer, detected: DetectedLanguage) -> Self {
306 let mut state = Self::new_from_buffer(buffer);
307 state.apply_language(detected);
308 state
309 }
310
311 fn apply_insert(
313 &mut self,
314 cursors: &mut Cursors,
315 position: usize,
316 text: &str,
317 cursor_id: crate::model::event::CursorId,
318 ) {
319 let newlines_inserted = text.matches('\n').count();
320
321 self.marker_list.adjust_for_insert(position, text.len());
323 self.margins.adjust_for_insert(position, text.len());
324
325 self.buffer.insert(position, text);
327
328 self.highlighter
330 .invalidate_range(position..position + text.len());
331
332 cursors.adjust_for_edit(position, 0, text.len());
337
338 if let Some(cursor) = cursors.get_mut(cursor_id) {
340 cursor.position = position + text.len();
341 cursor.clear_selection();
342 }
343
344 if cursor_id == cursors.primary_id() {
346 self.primary_cursor_line_number = match self.primary_cursor_line_number {
347 LineNumber::Absolute(line) => LineNumber::Absolute(line + newlines_inserted),
348 LineNumber::Relative {
349 line,
350 from_cached_line,
351 } => LineNumber::Relative {
352 line: line + newlines_inserted,
353 from_cached_line,
354 },
355 };
356 }
357 }
358
359 fn apply_delete(
361 &mut self,
362 cursors: &mut Cursors,
363 range: &std::ops::Range<usize>,
364 cursor_id: crate::model::event::CursorId,
365 deleted_text: &str,
366 ) {
367 let len = range.len();
368 let newlines_deleted = deleted_text.matches('\n').count();
369
370 self.marker_list.adjust_for_delete(range.start, len);
372 self.margins.adjust_for_delete(range.start, len);
373
374 self.buffer.delete(range.clone());
376
377 self.highlighter.invalidate_range(range.clone());
379
380 cursors.adjust_for_edit(range.start, len, 0);
385
386 if let Some(cursor) = cursors.get_mut(cursor_id) {
388 cursor.position = range.start;
389 cursor.clear_selection();
390 }
391
392 if cursor_id == cursors.primary_id() {
394 self.primary_cursor_line_number = match self.primary_cursor_line_number {
395 LineNumber::Absolute(line) => {
396 LineNumber::Absolute(line.saturating_sub(newlines_deleted))
397 }
398 LineNumber::Relative {
399 line,
400 from_cached_line,
401 } => LineNumber::Relative {
402 line: line.saturating_sub(newlines_deleted),
403 from_cached_line,
404 },
405 };
406 }
407 }
408
409 pub fn apply(&mut self, cursors: &mut Cursors, event: &Event) {
412 match event {
413 Event::Insert {
414 position,
415 text,
416 cursor_id,
417 } => self.apply_insert(cursors, *position, text, *cursor_id),
418
419 Event::Delete {
420 range,
421 cursor_id,
422 deleted_text,
423 } => self.apply_delete(cursors, range, *cursor_id, deleted_text),
424
425 Event::MoveCursor {
426 cursor_id,
427 new_position,
428 new_anchor,
429 new_sticky_column,
430 ..
431 } => {
432 if let Some(cursor) = cursors.get_mut(*cursor_id) {
433 cursor.position = *new_position;
434 cursor.anchor = *new_anchor;
435 cursor.sticky_column = *new_sticky_column;
436 }
437
438 if *cursor_id == cursors.primary_id() {
441 self.primary_cursor_line_number =
442 match self.buffer.offset_to_position(*new_position) {
443 Some(pos) => LineNumber::Absolute(pos.line),
444 None => {
445 let estimated_line = *new_position / 80;
448 LineNumber::Absolute(estimated_line)
449 }
450 };
451 }
452 }
453
454 Event::AddCursor {
455 cursor_id,
456 position,
457 anchor,
458 } => {
459 let cursor = if let Some(anchor) = anchor {
460 Cursor::with_selection(*anchor, *position)
461 } else {
462 Cursor::new(*position)
463 };
464
465 cursors.insert_with_id(*cursor_id, cursor);
468
469 cursors.normalize();
470 }
471
472 Event::RemoveCursor { cursor_id, .. } => {
473 cursors.remove(*cursor_id);
474 }
475
476 Event::Scroll { .. } | Event::SetViewport { .. } | Event::Recenter => {
479 tracing::warn!("View event {:?} reached EditorState.apply() - should be handled by SplitViewState", event);
482 }
483
484 Event::SetAnchor {
485 cursor_id,
486 position,
487 } => {
488 if let Some(cursor) = cursors.get_mut(*cursor_id) {
491 cursor.anchor = Some(*position);
492 cursor.deselect_on_move = false;
493 }
494 }
495
496 Event::ClearAnchor { cursor_id } => {
497 if let Some(cursor) = cursors.get_mut(*cursor_id) {
500 cursor.anchor = None;
501 cursor.deselect_on_move = true;
502 cursor.clear_block_selection();
503 }
504 }
505
506 Event::ChangeMode { mode } => {
507 self.mode = mode.clone();
508 }
509
510 Event::AddOverlay {
511 namespace,
512 range,
513 face,
514 priority,
515 message,
516 extend_to_line_end,
517 url,
518 } => {
519 tracing::trace!(
520 "AddOverlay: namespace={:?}, range={:?}, face={:?}, priority={}",
521 namespace,
522 range,
523 face,
524 priority
525 );
526 let overlay_face = convert_event_face_to_overlay_face(face);
528 tracing::trace!("Converted face: {:?}", overlay_face);
529
530 let mut overlay = Overlay::with_priority(
531 &mut self.marker_list,
532 range.clone(),
533 overlay_face,
534 *priority,
535 );
536 overlay.namespace = namespace.clone();
537 overlay.message = message.clone();
538 overlay.extend_to_line_end = *extend_to_line_end;
539 overlay.url = url.clone();
540
541 let actual_range = overlay.range(&self.marker_list);
542 tracing::trace!(
543 "Created overlay with markers - actual range: {:?}, handle={:?}",
544 actual_range,
545 overlay.handle
546 );
547
548 self.overlays.add(overlay);
549 }
550
551 Event::RemoveOverlay { handle } => {
552 tracing::trace!("RemoveOverlay: handle={:?}", handle);
553 self.overlays
554 .remove_by_handle(handle, &mut self.marker_list);
555 }
556
557 Event::RemoveOverlaysInRange { range } => {
558 self.overlays.remove_in_range(range, &mut self.marker_list);
559 }
560
561 Event::ClearNamespace { namespace } => {
562 tracing::trace!("ClearNamespace: namespace={:?}", namespace);
563 self.overlays
564 .clear_namespace(namespace, &mut self.marker_list);
565 }
566
567 Event::ClearOverlays => {
568 self.overlays.clear(&mut self.marker_list);
569 }
570
571 Event::ShowPopup { popup } => {
572 let popup_obj = convert_popup_data_to_popup(popup);
573 self.popups.show(popup_obj);
574 }
575
576 Event::HidePopup => {
577 self.popups.hide();
578 }
579
580 Event::ClearPopups => {
581 self.popups.clear();
582 }
583
584 Event::PopupSelectNext => {
585 if let Some(popup) = self.popups.top_mut() {
586 popup.select_next();
587 }
588 }
589
590 Event::PopupSelectPrev => {
591 if let Some(popup) = self.popups.top_mut() {
592 popup.select_prev();
593 }
594 }
595
596 Event::PopupPageDown => {
597 if let Some(popup) = self.popups.top_mut() {
598 popup.page_down();
599 }
600 }
601
602 Event::PopupPageUp => {
603 if let Some(popup) = self.popups.top_mut() {
604 popup.page_up();
605 }
606 }
607
608 Event::AddMarginAnnotation {
609 line,
610 position,
611 content,
612 annotation_id,
613 } => {
614 let margin_position = convert_margin_position(position);
615 let margin_content = convert_margin_content(content);
616 let annotation = if let Some(id) = annotation_id {
617 MarginAnnotation::with_id(*line, margin_position, margin_content, id.clone())
618 } else {
619 MarginAnnotation::new(*line, margin_position, margin_content)
620 };
621 self.margins.add_annotation(annotation);
622 }
623
624 Event::RemoveMarginAnnotation { annotation_id } => {
625 self.margins.remove_by_id(annotation_id);
626 }
627
628 Event::RemoveMarginAnnotationsAtLine { line, position } => {
629 let margin_position = convert_margin_position(position);
630 self.margins.remove_at_line(*line, margin_position);
631 }
632
633 Event::ClearMarginPosition { position } => {
634 let margin_position = convert_margin_position(position);
635 self.margins.clear_position(margin_position);
636 }
637
638 Event::ClearMargins => {
639 self.margins.clear_all();
640 }
641
642 Event::SetLineNumbers { enabled } => {
643 self.margins.configure_for_line_numbers(*enabled);
644 }
645
646 Event::SplitPane { .. }
649 | Event::CloseSplit { .. }
650 | Event::SetActiveSplit { .. }
651 | Event::AdjustSplitRatio { .. }
652 | Event::NextSplit
653 | Event::PrevSplit => {
654 }
656
657 Event::Batch { events, .. } => {
658 for event in events {
661 self.apply(cursors, event);
662 }
663 }
664
665 Event::BulkEdit {
666 new_snapshot,
667 new_cursors,
668 ..
669 } => {
670 if let Some(snapshot) = new_snapshot {
677 self.buffer.restore_buffer_state(snapshot);
678 }
679
680 for (cursor_id, position, anchor) in new_cursors {
682 if let Some(cursor) = cursors.get_mut(*cursor_id) {
683 cursor.position = *position;
684 cursor.anchor = *anchor;
685 }
686 }
687
688 self.highlighter.invalidate_all();
690
691 let primary_pos = cursors.primary().position;
693 self.primary_cursor_line_number = match self.buffer.offset_to_position(primary_pos)
694 {
695 Some(pos) => crate::model::buffer::LineNumber::Absolute(pos.line),
696 None => crate::model::buffer::LineNumber::Absolute(0),
697 };
698 }
699 }
700 }
701
702 pub fn apply_many(&mut self, cursors: &mut Cursors, events: &[Event]) {
704 for event in events {
705 self.apply(cursors, event);
706 }
707 }
708
709 pub fn on_focus_lost(&mut self) {
713 if self.popups.dismiss_transient() {
714 tracing::debug!("Dismissed transient popup on buffer focus loss");
715 }
716 }
717}
718
719fn convert_event_face_to_overlay_face(event_face: &EventOverlayFace) -> OverlayFace {
721 match event_face {
722 EventOverlayFace::Underline { color, style } => {
723 let underline_style = match style {
724 crate::model::event::UnderlineStyle::Straight => UnderlineStyle::Straight,
725 crate::model::event::UnderlineStyle::Wavy => UnderlineStyle::Wavy,
726 crate::model::event::UnderlineStyle::Dotted => UnderlineStyle::Dotted,
727 crate::model::event::UnderlineStyle::Dashed => UnderlineStyle::Dashed,
728 };
729 OverlayFace::Underline {
730 color: Color::Rgb(color.0, color.1, color.2),
731 style: underline_style,
732 }
733 }
734 EventOverlayFace::Background { color } => OverlayFace::Background {
735 color: Color::Rgb(color.0, color.1, color.2),
736 },
737 EventOverlayFace::Foreground { color } => OverlayFace::Foreground {
738 color: Color::Rgb(color.0, color.1, color.2),
739 },
740 EventOverlayFace::Style { options } => {
741 use ratatui::style::Modifier;
742
743 let mut style = Style::default();
745
746 if let Some(ref fg) = options.fg {
748 if let Some((r, g, b)) = fg.as_rgb() {
749 style = style.fg(Color::Rgb(r, g, b));
750 }
751 }
752
753 if let Some(ref bg) = options.bg {
755 if let Some((r, g, b)) = bg.as_rgb() {
756 style = style.bg(Color::Rgb(r, g, b));
757 }
758 }
759
760 let mut modifiers = Modifier::empty();
762 if options.bold {
763 modifiers |= Modifier::BOLD;
764 }
765 if options.italic {
766 modifiers |= Modifier::ITALIC;
767 }
768 if options.underline {
769 modifiers |= Modifier::UNDERLINED;
770 }
771 if options.strikethrough {
772 modifiers |= Modifier::CROSSED_OUT;
773 }
774 if !modifiers.is_empty() {
775 style = style.add_modifier(modifiers);
776 }
777
778 let fg_theme = options
780 .fg
781 .as_ref()
782 .and_then(|c| c.as_theme_key())
783 .map(String::from);
784 let bg_theme = options
785 .bg
786 .as_ref()
787 .and_then(|c| c.as_theme_key())
788 .map(String::from);
789
790 if fg_theme.is_some() || bg_theme.is_some() {
792 OverlayFace::ThemedStyle {
793 fallback_style: style,
794 fg_theme,
795 bg_theme,
796 }
797 } else {
798 OverlayFace::Style { style }
799 }
800 }
801 }
802}
803
804fn convert_popup_data_to_popup(data: &PopupData) -> Popup {
806 let content = match &data.content {
807 crate::model::event::PopupContentData::Text(lines) => PopupContent::Text(lines.clone()),
808 crate::model::event::PopupContentData::List { items, selected } => PopupContent::List {
809 items: items
810 .iter()
811 .map(|item| PopupListItem {
812 text: item.text.clone(),
813 detail: item.detail.clone(),
814 icon: item.icon.clone(),
815 data: item.data.clone(),
816 })
817 .collect(),
818 selected: *selected,
819 },
820 };
821
822 let position = match data.position {
823 PopupPositionData::AtCursor => PopupPosition::AtCursor,
824 PopupPositionData::BelowCursor => PopupPosition::BelowCursor,
825 PopupPositionData::AboveCursor => PopupPosition::AboveCursor,
826 PopupPositionData::Fixed { x, y } => PopupPosition::Fixed { x, y },
827 PopupPositionData::Centered => PopupPosition::Centered,
828 PopupPositionData::BottomRight => PopupPosition::BottomRight,
829 };
830
831 let kind = match data.kind {
833 crate::model::event::PopupKindHint::Completion => PopupKind::Completion,
834 crate::model::event::PopupKindHint::List => PopupKind::List,
835 crate::model::event::PopupKindHint::Text => PopupKind::Text,
836 };
837
838 Popup {
839 kind,
840 title: data.title.clone(),
841 description: data.description.clone(),
842 transient: data.transient,
843 content,
844 position,
845 width: data.width,
846 max_height: data.max_height,
847 bordered: data.bordered,
848 border_style: Style::default().fg(Color::Gray),
849 background_style: Style::default().bg(Color::Rgb(30, 30, 30)),
850 scroll_offset: 0,
851 text_selection: None,
852 }
853}
854
855fn convert_margin_position(position: &MarginPositionData) -> MarginPosition {
857 match position {
858 MarginPositionData::Left => MarginPosition::Left,
859 MarginPositionData::Right => MarginPosition::Right,
860 }
861}
862
863fn convert_margin_content(content: &MarginContentData) -> MarginContent {
865 match content {
866 MarginContentData::Text(text) => MarginContent::Text(text.clone()),
867 MarginContentData::Symbol { text, color } => {
868 if let Some((r, g, b)) = color {
869 MarginContent::colored_symbol(text.clone(), Color::Rgb(*r, *g, *b))
870 } else {
871 MarginContent::symbol(text.clone(), Style::default())
872 }
873 }
874 MarginContentData::Empty => MarginContent::Empty,
875 }
876}
877
878impl EditorState {
879 pub fn prepare_for_render(&mut self, top_byte: usize, height: u16) -> Result<()> {
886 self.buffer.prepare_viewport(top_byte, height as usize)?;
887 Ok(())
888 }
889
890 pub fn get_text_range(&mut self, start: usize, end: usize) -> String {
910 match self
912 .buffer
913 .get_text_range_mut(start, end.saturating_sub(start))
914 {
915 Ok(bytes) => String::from_utf8_lossy(&bytes).into_owned(),
916 Err(e) => {
917 tracing::warn!("Failed to get text range {}..{}: {}", start, end, e);
918 String::new()
919 }
920 }
921 }
922
923 pub fn get_line_at_offset(&mut self, offset: usize) -> Option<(usize, String)> {
931 use crate::model::document_model::DocumentModel;
932
933 let mut line_start = offset;
936 while line_start > 0 {
937 if let Ok(text) = self.buffer.get_text_range_mut(line_start - 1, 1) {
938 if text.first() == Some(&b'\n') {
939 break;
940 }
941 line_start -= 1;
942 } else {
943 break;
944 }
945 }
946
947 let viewport = self
949 .get_viewport_content(
950 crate::model::document_model::DocumentPosition::byte(line_start),
951 1,
952 )
953 .ok()?;
954
955 viewport
956 .lines
957 .first()
958 .map(|line| (line.byte_offset, line.content.clone()))
959 }
960
961 pub fn get_text_to_end_of_line(&mut self, cursor_pos: usize) -> Result<String> {
966 use crate::model::document_model::DocumentModel;
967
968 let viewport = self.get_viewport_content(
970 crate::model::document_model::DocumentPosition::byte(cursor_pos),
971 1,
972 )?;
973
974 if let Some(line) = viewport.lines.first() {
975 let line_start = line.byte_offset;
976 let line_end = line_start + line.content.len();
977
978 if cursor_pos >= line_start && cursor_pos <= line_end {
979 let offset_in_line = cursor_pos - line_start;
980 Ok(line.content.get(offset_in_line..).unwrap_or("").to_string())
982 } else {
983 Ok(String::new())
984 }
985 } else {
986 Ok(String::new())
987 }
988 }
989
990 pub fn set_semantic_tokens(&mut self, store: SemanticTokenStore) {
992 self.semantic_tokens = Some(store);
993 }
994
995 pub fn clear_semantic_tokens(&mut self) {
997 self.semantic_tokens = None;
998 }
999
1000 pub fn semantic_tokens_result_id(&self) -> Option<&str> {
1002 self.semantic_tokens
1003 .as_ref()
1004 .and_then(|store| store.result_id.as_deref())
1005 }
1006}
1007
1008impl DocumentModel for EditorState {
1013 fn capabilities(&self) -> DocumentCapabilities {
1014 let line_count = self.buffer.line_count();
1015 DocumentCapabilities {
1016 has_line_index: line_count.is_some(),
1017 uses_lazy_loading: false, byte_length: self.buffer.len(),
1019 approximate_line_count: line_count.unwrap_or_else(|| {
1020 self.buffer.len() / 80
1022 }),
1023 }
1024 }
1025
1026 fn get_viewport_content(
1027 &mut self,
1028 start_pos: DocumentPosition,
1029 max_lines: usize,
1030 ) -> Result<ViewportContent> {
1031 let start_offset = self.position_to_offset(start_pos)?;
1033
1034 let line_iter = self.buffer.iter_lines_from(start_offset, max_lines)?;
1037 let has_more = line_iter.has_more;
1038
1039 let lines = line_iter
1040 .map(|line_data| ViewportLine {
1041 byte_offset: line_data.byte_offset,
1042 content: line_data.content,
1043 has_newline: line_data.has_newline,
1044 approximate_line_number: line_data.line_number,
1045 })
1046 .collect();
1047
1048 Ok(ViewportContent {
1049 start_position: DocumentPosition::ByteOffset(start_offset),
1050 lines,
1051 has_more,
1052 })
1053 }
1054
1055 fn position_to_offset(&self, pos: DocumentPosition) -> Result<usize> {
1056 match pos {
1057 DocumentPosition::ByteOffset(offset) => Ok(offset),
1058 DocumentPosition::LineColumn { line, column } => {
1059 if !self.has_line_index() {
1060 anyhow::bail!("Line indexing not available for this document");
1061 }
1062 let position = crate::model::piece_tree::Position { line, column };
1064 Ok(self.buffer.position_to_offset(position))
1065 }
1066 }
1067 }
1068
1069 fn offset_to_position(&self, offset: usize) -> DocumentPosition {
1070 if self.has_line_index() {
1071 if let Some(pos) = self.buffer.offset_to_position(offset) {
1072 DocumentPosition::LineColumn {
1073 line: pos.line,
1074 column: pos.column,
1075 }
1076 } else {
1077 DocumentPosition::ByteOffset(offset)
1079 }
1080 } else {
1081 DocumentPosition::ByteOffset(offset)
1082 }
1083 }
1084
1085 fn get_range(&mut self, start: DocumentPosition, end: DocumentPosition) -> Result<String> {
1086 let start_offset = self.position_to_offset(start)?;
1087 let end_offset = self.position_to_offset(end)?;
1088
1089 if start_offset > end_offset {
1090 anyhow::bail!(
1091 "Invalid range: start offset {} > end offset {}",
1092 start_offset,
1093 end_offset
1094 );
1095 }
1096
1097 let bytes = self
1098 .buffer
1099 .get_text_range_mut(start_offset, end_offset - start_offset)?;
1100
1101 Ok(String::from_utf8_lossy(&bytes).into_owned())
1102 }
1103
1104 fn get_line_content(&mut self, line_number: usize) -> Option<String> {
1105 if !self.has_line_index() {
1106 return None;
1107 }
1108
1109 let line_start_offset = self.buffer.line_start_offset(line_number)?;
1111
1112 let mut iter = self.buffer.line_iterator(line_start_offset, 80);
1114 if let Some((_start, content)) = iter.next_line() {
1115 let has_newline = content.ends_with('\n');
1116 let line_content = if has_newline {
1117 content[..content.len() - 1].to_string()
1118 } else {
1119 content
1120 };
1121 Some(line_content)
1122 } else {
1123 None
1124 }
1125 }
1126
1127 fn get_chunk_at_offset(&mut self, offset: usize, size: usize) -> Result<(usize, String)> {
1128 let bytes = self.buffer.get_text_range_mut(offset, size)?;
1129
1130 Ok((offset, String::from_utf8_lossy(&bytes).into_owned()))
1131 }
1132
1133 fn insert(&mut self, pos: DocumentPosition, text: &str) -> Result<usize> {
1134 let offset = self.position_to_offset(pos)?;
1135 self.buffer.insert_bytes(offset, text.as_bytes().to_vec());
1136 Ok(text.len())
1137 }
1138
1139 fn delete(&mut self, start: DocumentPosition, end: DocumentPosition) -> Result<()> {
1140 let start_offset = self.position_to_offset(start)?;
1141 let end_offset = self.position_to_offset(end)?;
1142
1143 if start_offset > end_offset {
1144 anyhow::bail!(
1145 "Invalid range: start offset {} > end offset {}",
1146 start_offset,
1147 end_offset
1148 );
1149 }
1150
1151 self.buffer.delete(start_offset..end_offset);
1152 Ok(())
1153 }
1154
1155 fn replace(
1156 &mut self,
1157 start: DocumentPosition,
1158 end: DocumentPosition,
1159 text: &str,
1160 ) -> Result<()> {
1161 self.delete(start, end)?;
1163 self.insert(start, text)?;
1164 Ok(())
1165 }
1166
1167 fn find_matches(
1168 &mut self,
1169 pattern: &str,
1170 search_range: Option<(DocumentPosition, DocumentPosition)>,
1171 ) -> Result<Vec<usize>> {
1172 let (start_offset, end_offset) = if let Some((start, end)) = search_range {
1173 (
1174 self.position_to_offset(start)?,
1175 self.position_to_offset(end)?,
1176 )
1177 } else {
1178 (0, self.buffer.len())
1179 };
1180
1181 let bytes = self
1183 .buffer
1184 .get_text_range_mut(start_offset, end_offset - start_offset)?;
1185 let text = String::from_utf8_lossy(&bytes);
1186
1187 let mut matches = Vec::new();
1189 let mut search_offset = 0;
1190 while let Some(pos) = text[search_offset..].find(pattern) {
1191 matches.push(start_offset + search_offset + pos);
1192 search_offset += pos + pattern.len();
1193 }
1194
1195 Ok(matches)
1196 }
1197}
1198
1199#[derive(Clone, Debug)]
1201pub struct SemanticTokenStore {
1202 pub version: u64,
1204 pub result_id: Option<String>,
1206 pub data: Vec<u32>,
1208 pub tokens: Vec<SemanticTokenSpan>,
1210}
1211
1212#[derive(Clone, Debug)]
1214pub struct SemanticTokenSpan {
1215 pub range: Range<usize>,
1216 pub token_type: String,
1217 pub modifiers: Vec<String>,
1218}
1219
1220#[cfg(test)]
1221mod tests {
1222 use crate::model::filesystem::StdFileSystem;
1223 use std::sync::Arc;
1224
1225 fn test_fs() -> Arc<dyn crate::model::filesystem::FileSystem + Send + Sync> {
1226 Arc::new(StdFileSystem)
1227 }
1228 use super::*;
1229 use crate::model::event::CursorId;
1230
1231 #[test]
1232 fn test_state_new() {
1233 let state = EditorState::new(
1234 80,
1235 24,
1236 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1237 test_fs(),
1238 );
1239 assert!(state.buffer.is_empty());
1240 }
1241
1242 #[test]
1243 fn test_apply_insert() {
1244 let mut state = EditorState::new(
1245 80,
1246 24,
1247 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1248 test_fs(),
1249 );
1250 let mut cursors = Cursors::new();
1251 let cursor_id = cursors.primary_id();
1252
1253 state.apply(
1254 &mut cursors,
1255 &Event::Insert {
1256 position: 0,
1257 text: "hello".to_string(),
1258 cursor_id,
1259 },
1260 );
1261
1262 assert_eq!(state.buffer.to_string().unwrap(), "hello");
1263 assert_eq!(cursors.primary().position, 5);
1264 assert!(state.buffer.is_modified());
1265 }
1266
1267 #[test]
1268 fn test_apply_delete() {
1269 let mut state = EditorState::new(
1270 80,
1271 24,
1272 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1273 test_fs(),
1274 );
1275 let mut cursors = Cursors::new();
1276 let cursor_id = cursors.primary_id();
1277
1278 state.apply(
1280 &mut cursors,
1281 &Event::Insert {
1282 position: 0,
1283 text: "hello world".to_string(),
1284 cursor_id,
1285 },
1286 );
1287
1288 state.apply(
1289 &mut cursors,
1290 &Event::Delete {
1291 range: 5..11,
1292 deleted_text: " world".to_string(),
1293 cursor_id,
1294 },
1295 );
1296
1297 assert_eq!(state.buffer.to_string().unwrap(), "hello");
1298 assert_eq!(cursors.primary().position, 5);
1299 }
1300
1301 #[test]
1302 fn test_apply_move_cursor() {
1303 let mut state = EditorState::new(
1304 80,
1305 24,
1306 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1307 test_fs(),
1308 );
1309 let mut cursors = Cursors::new();
1310 let cursor_id = cursors.primary_id();
1311
1312 state.apply(
1313 &mut cursors,
1314 &Event::Insert {
1315 position: 0,
1316 text: "hello".to_string(),
1317 cursor_id,
1318 },
1319 );
1320
1321 state.apply(
1322 &mut cursors,
1323 &Event::MoveCursor {
1324 cursor_id,
1325 old_position: 5,
1326 new_position: 2,
1327 old_anchor: None,
1328 new_anchor: None,
1329 old_sticky_column: 0,
1330 new_sticky_column: 0,
1331 },
1332 );
1333
1334 assert_eq!(cursors.primary().position, 2);
1335 }
1336
1337 #[test]
1338 fn test_apply_add_cursor() {
1339 let mut state = EditorState::new(
1340 80,
1341 24,
1342 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1343 test_fs(),
1344 );
1345 let mut cursors = Cursors::new();
1346 let cursor_id = CursorId(1);
1347
1348 state.apply(
1349 &mut cursors,
1350 &Event::AddCursor {
1351 cursor_id,
1352 position: 5,
1353 anchor: None,
1354 },
1355 );
1356
1357 assert_eq!(cursors.count(), 2);
1358 }
1359
1360 #[test]
1361 fn test_apply_many() {
1362 let mut state = EditorState::new(
1363 80,
1364 24,
1365 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1366 test_fs(),
1367 );
1368 let mut cursors = Cursors::new();
1369 let cursor_id = cursors.primary_id();
1370
1371 let events = vec![
1372 Event::Insert {
1373 position: 0,
1374 text: "hello ".to_string(),
1375 cursor_id,
1376 },
1377 Event::Insert {
1378 position: 6,
1379 text: "world".to_string(),
1380 cursor_id,
1381 },
1382 ];
1383
1384 state.apply_many(&mut cursors, &events);
1385
1386 assert_eq!(state.buffer.to_string().unwrap(), "hello world");
1387 }
1388
1389 #[test]
1390 fn test_cursor_adjustment_after_insert() {
1391 let mut state = EditorState::new(
1392 80,
1393 24,
1394 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1395 test_fs(),
1396 );
1397 let mut cursors = Cursors::new();
1398 let cursor_id = cursors.primary_id();
1399
1400 state.apply(
1402 &mut cursors,
1403 &Event::AddCursor {
1404 cursor_id: CursorId(1),
1405 position: 5,
1406 anchor: None,
1407 },
1408 );
1409
1410 state.apply(
1412 &mut cursors,
1413 &Event::Insert {
1414 position: 0,
1415 text: "abc".to_string(),
1416 cursor_id,
1417 },
1418 );
1419
1420 if let Some(cursor) = cursors.get(CursorId(1)) {
1422 assert_eq!(cursor.position, 8);
1423 }
1424 }
1425
1426 mod document_model_tests {
1428 use super::*;
1429 use crate::model::document_model::{DocumentModel, DocumentPosition};
1430
1431 #[test]
1432 fn test_capabilities_small_file() {
1433 let mut state = EditorState::new(
1434 80,
1435 24,
1436 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1437 test_fs(),
1438 );
1439 state.buffer = Buffer::from_str_test("line1\nline2\nline3");
1440
1441 let caps = state.capabilities();
1442 assert!(caps.has_line_index, "Small file should have line index");
1443 assert_eq!(caps.byte_length, "line1\nline2\nline3".len());
1444 assert_eq!(caps.approximate_line_count, 3, "Should have 3 lines");
1445 }
1446
1447 #[test]
1448 fn test_position_conversions() {
1449 let mut state = EditorState::new(
1450 80,
1451 24,
1452 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1453 test_fs(),
1454 );
1455 state.buffer = Buffer::from_str_test("hello\nworld\ntest");
1456
1457 let pos1 = DocumentPosition::ByteOffset(6);
1459 let offset1 = state.position_to_offset(pos1).unwrap();
1460 assert_eq!(offset1, 6);
1461
1462 let pos2 = DocumentPosition::LineColumn { line: 1, column: 0 };
1464 let offset2 = state.position_to_offset(pos2).unwrap();
1465 assert_eq!(offset2, 6, "Line 1, column 0 should be at byte 6");
1466
1467 let converted = state.offset_to_position(6);
1469 match converted {
1470 DocumentPosition::LineColumn { line, column } => {
1471 assert_eq!(line, 1);
1472 assert_eq!(column, 0);
1473 }
1474 _ => panic!("Expected LineColumn for small file"),
1475 }
1476 }
1477
1478 #[test]
1479 fn test_get_viewport_content() {
1480 let mut state = EditorState::new(
1481 80,
1482 24,
1483 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1484 test_fs(),
1485 );
1486 state.buffer = Buffer::from_str_test("line1\nline2\nline3\nline4\nline5");
1487
1488 let content = state
1489 .get_viewport_content(DocumentPosition::ByteOffset(0), 3)
1490 .unwrap();
1491
1492 assert_eq!(content.lines.len(), 3);
1493 assert_eq!(content.lines[0].content, "line1");
1494 assert_eq!(content.lines[1].content, "line2");
1495 assert_eq!(content.lines[2].content, "line3");
1496 assert!(content.has_more);
1497 }
1498
1499 #[test]
1500 fn test_get_range() {
1501 let mut state = EditorState::new(
1502 80,
1503 24,
1504 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1505 test_fs(),
1506 );
1507 state.buffer = Buffer::from_str_test("hello world");
1508
1509 let text = state
1510 .get_range(
1511 DocumentPosition::ByteOffset(0),
1512 DocumentPosition::ByteOffset(5),
1513 )
1514 .unwrap();
1515 assert_eq!(text, "hello");
1516
1517 let text2 = state
1518 .get_range(
1519 DocumentPosition::ByteOffset(6),
1520 DocumentPosition::ByteOffset(11),
1521 )
1522 .unwrap();
1523 assert_eq!(text2, "world");
1524 }
1525
1526 #[test]
1527 fn test_get_line_content() {
1528 let mut state = EditorState::new(
1529 80,
1530 24,
1531 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1532 test_fs(),
1533 );
1534 state.buffer = Buffer::from_str_test("line1\nline2\nline3");
1535
1536 let line0 = state.get_line_content(0).unwrap();
1537 assert_eq!(line0, "line1");
1538
1539 let line1 = state.get_line_content(1).unwrap();
1540 assert_eq!(line1, "line2");
1541
1542 let line2 = state.get_line_content(2).unwrap();
1543 assert_eq!(line2, "line3");
1544 }
1545
1546 #[test]
1547 fn test_insert_delete() {
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("hello world");
1555
1556 let bytes_inserted = state
1558 .insert(DocumentPosition::ByteOffset(6), "beautiful ")
1559 .unwrap();
1560 assert_eq!(bytes_inserted, 10);
1561 assert_eq!(state.buffer.to_string().unwrap(), "hello beautiful world");
1562
1563 state
1565 .delete(
1566 DocumentPosition::ByteOffset(6),
1567 DocumentPosition::ByteOffset(16),
1568 )
1569 .unwrap();
1570 assert_eq!(state.buffer.to_string().unwrap(), "hello world");
1571 }
1572
1573 #[test]
1574 fn test_replace() {
1575 let mut state = EditorState::new(
1576 80,
1577 24,
1578 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1579 test_fs(),
1580 );
1581 state.buffer = Buffer::from_str_test("hello world");
1582
1583 state
1584 .replace(
1585 DocumentPosition::ByteOffset(0),
1586 DocumentPosition::ByteOffset(5),
1587 "hi",
1588 )
1589 .unwrap();
1590 assert_eq!(state.buffer.to_string().unwrap(), "hi world");
1591 }
1592
1593 #[test]
1594 fn test_find_matches() {
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 hello");
1602
1603 let matches = state.find_matches("hello", None).unwrap();
1604 assert_eq!(matches.len(), 2);
1605 assert_eq!(matches[0], 0);
1606 assert_eq!(matches[1], 12);
1607 }
1608
1609 #[test]
1610 fn test_prepare_for_render() {
1611 let mut state = EditorState::new(
1612 80,
1613 24,
1614 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1615 test_fs(),
1616 );
1617 state.buffer = Buffer::from_str_test("line1\nline2\nline3\nline4\nline5");
1618
1619 state.prepare_for_render(0, 24).unwrap();
1621 }
1622
1623 #[test]
1624 fn test_helper_get_text_range() {
1625 let mut state = EditorState::new(
1626 80,
1627 24,
1628 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1629 test_fs(),
1630 );
1631 state.buffer = Buffer::from_str_test("hello world");
1632
1633 let text = state.get_text_range(0, 5);
1635 assert_eq!(text, "hello");
1636
1637 let text2 = state.get_text_range(6, 11);
1639 assert_eq!(text2, "world");
1640 }
1641
1642 #[test]
1643 fn test_helper_get_line_at_offset() {
1644 let mut state = EditorState::new(
1645 80,
1646 24,
1647 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1648 test_fs(),
1649 );
1650 state.buffer = Buffer::from_str_test("line1\nline2\nline3");
1651
1652 let (offset, content) = state.get_line_at_offset(0).unwrap();
1654 assert_eq!(offset, 0);
1655 assert_eq!(content, "line1");
1656
1657 let (offset2, content2) = state.get_line_at_offset(8).unwrap();
1659 assert_eq!(offset2, 6); assert_eq!(content2, "line2");
1661
1662 let (offset3, content3) = state.get_line_at_offset(12).unwrap();
1664 assert_eq!(offset3, 12);
1665 assert_eq!(content3, "line3");
1666 }
1667
1668 #[test]
1669 fn test_helper_get_text_to_end_of_line() {
1670 let mut state = EditorState::new(
1671 80,
1672 24,
1673 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1674 test_fs(),
1675 );
1676 state.buffer = Buffer::from_str_test("hello world\nline2");
1677
1678 let text = state.get_text_to_end_of_line(0).unwrap();
1680 assert_eq!(text, "hello world");
1681
1682 let text2 = state.get_text_to_end_of_line(6).unwrap();
1684 assert_eq!(text2, "world");
1685
1686 let text3 = state.get_text_to_end_of_line(11).unwrap();
1688 assert_eq!(text3, "");
1689
1690 let text4 = state.get_text_to_end_of_line(12).unwrap();
1692 assert_eq!(text4, "line2");
1693 }
1694 }
1695
1696 mod virtual_text_integration_tests {
1698 use super::*;
1699 use crate::view::virtual_text::VirtualTextPosition;
1700 use ratatui::style::Style;
1701
1702 #[test]
1703 fn test_virtual_text_add_and_query() {
1704 let mut state = EditorState::new(
1705 80,
1706 24,
1707 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1708 test_fs(),
1709 );
1710 state.buffer = Buffer::from_str_test("hello world");
1711
1712 if !state.buffer.is_empty() {
1714 state.marker_list.adjust_for_insert(0, state.buffer.len());
1715 }
1716
1717 let vtext_id = state.virtual_texts.add(
1719 &mut state.marker_list,
1720 5,
1721 ": string".to_string(),
1722 Style::default(),
1723 VirtualTextPosition::AfterChar,
1724 0,
1725 );
1726
1727 let results = state.virtual_texts.query_range(&state.marker_list, 0, 11);
1729 assert_eq!(results.len(), 1);
1730 assert_eq!(results[0].0, 5); assert_eq!(results[0].1.text, ": string");
1732
1733 let lookup = state.virtual_texts.build_lookup(&state.marker_list, 0, 11);
1735 assert!(lookup.contains_key(&5));
1736 assert_eq!(lookup[&5].len(), 1);
1737 assert_eq!(lookup[&5][0].text, ": string");
1738
1739 state.virtual_texts.remove(&mut state.marker_list, vtext_id);
1741 assert!(state.virtual_texts.is_empty());
1742 }
1743
1744 #[test]
1745 fn test_virtual_text_position_tracking_on_insert() {
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("hello world");
1753
1754 if !state.buffer.is_empty() {
1756 state.marker_list.adjust_for_insert(0, state.buffer.len());
1757 }
1758
1759 let _vtext_id = state.virtual_texts.add(
1761 &mut state.marker_list,
1762 6,
1763 "/*param*/".to_string(),
1764 Style::default(),
1765 VirtualTextPosition::BeforeChar,
1766 0,
1767 );
1768
1769 let mut cursors = Cursors::new();
1771 let cursor_id = cursors.primary_id();
1772 state.apply(
1773 &mut cursors,
1774 &Event::Insert {
1775 position: 6,
1776 text: "beautiful ".to_string(),
1777 cursor_id,
1778 },
1779 );
1780
1781 let results = state.virtual_texts.query_range(&state.marker_list, 0, 30);
1783 assert_eq!(results.len(), 1);
1784 assert_eq!(results[0].0, 16); assert_eq!(results[0].1.text, "/*param*/");
1786 }
1787
1788 #[test]
1789 fn test_virtual_text_position_tracking_on_delete() {
1790 let mut state = EditorState::new(
1791 80,
1792 24,
1793 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1794 test_fs(),
1795 );
1796 state.buffer = Buffer::from_str_test("hello beautiful world");
1797
1798 if !state.buffer.is_empty() {
1800 state.marker_list.adjust_for_insert(0, state.buffer.len());
1801 }
1802
1803 let _vtext_id = state.virtual_texts.add(
1805 &mut state.marker_list,
1806 16,
1807 ": string".to_string(),
1808 Style::default(),
1809 VirtualTextPosition::AfterChar,
1810 0,
1811 );
1812
1813 let mut cursors = Cursors::new();
1815 let cursor_id = cursors.primary_id();
1816 state.apply(
1817 &mut cursors,
1818 &Event::Delete {
1819 range: 6..16,
1820 deleted_text: "beautiful ".to_string(),
1821 cursor_id,
1822 },
1823 );
1824
1825 let results = state.virtual_texts.query_range(&state.marker_list, 0, 20);
1827 assert_eq!(results.len(), 1);
1828 assert_eq!(results[0].0, 6); assert_eq!(results[0].1.text, ": string");
1830 }
1831
1832 #[test]
1833 fn test_multiple_virtual_texts_with_priorities() {
1834 let mut state = EditorState::new(
1835 80,
1836 24,
1837 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1838 test_fs(),
1839 );
1840 state.buffer = Buffer::from_str_test("let x = 5");
1841
1842 if !state.buffer.is_empty() {
1844 state.marker_list.adjust_for_insert(0, state.buffer.len());
1845 }
1846
1847 state.virtual_texts.add(
1849 &mut state.marker_list,
1850 5,
1851 ": i32".to_string(),
1852 Style::default(),
1853 VirtualTextPosition::AfterChar,
1854 0, );
1856
1857 state.virtual_texts.add(
1859 &mut state.marker_list,
1860 5,
1861 " /* inferred */".to_string(),
1862 Style::default(),
1863 VirtualTextPosition::AfterChar,
1864 10, );
1866
1867 let lookup = state.virtual_texts.build_lookup(&state.marker_list, 0, 10);
1869 assert!(lookup.contains_key(&5));
1870 let vtexts = &lookup[&5];
1871 assert_eq!(vtexts.len(), 2);
1872 assert_eq!(vtexts[0].text, ": i32");
1874 assert_eq!(vtexts[1].text, " /* inferred */");
1875 }
1876
1877 #[test]
1878 fn test_virtual_text_clear() {
1879 let mut state = EditorState::new(
1880 80,
1881 24,
1882 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1883 test_fs(),
1884 );
1885 state.buffer = Buffer::from_str_test("test");
1886
1887 if !state.buffer.is_empty() {
1889 state.marker_list.adjust_for_insert(0, state.buffer.len());
1890 }
1891
1892 state.virtual_texts.add(
1894 &mut state.marker_list,
1895 0,
1896 "hint1".to_string(),
1897 Style::default(),
1898 VirtualTextPosition::BeforeChar,
1899 0,
1900 );
1901 state.virtual_texts.add(
1902 &mut state.marker_list,
1903 2,
1904 "hint2".to_string(),
1905 Style::default(),
1906 VirtualTextPosition::AfterChar,
1907 0,
1908 );
1909
1910 assert_eq!(state.virtual_texts.len(), 2);
1911
1912 state.virtual_texts.clear(&mut state.marker_list);
1914 assert!(state.virtual_texts.is_empty());
1915
1916 let results = state.virtual_texts.query_range(&state.marker_list, 0, 10);
1918 assert!(results.is_empty());
1919 }
1920 }
1921}