1use crate::model::buffer::{Buffer, LineNumber};
2use crate::model::cursor::{Cursor, Cursors};
3use crate::model::document_model::{
4 DocumentCapabilities, DocumentModel, DocumentPosition, ViewportContent, ViewportLine,
5};
6use crate::model::event::{
7 Event, MarginContentData, MarginPositionData, OverlayFace as EventOverlayFace, PopupData,
8 PopupPositionData,
9};
10use crate::model::filesystem::FileSystem;
11use crate::model::marker::{MarkerId, MarkerList};
12use crate::primitives::detected_language::DetectedLanguage;
13use crate::primitives::grammar::GrammarRegistry;
14use crate::primitives::highlight_engine::HighlightEngine;
15use crate::primitives::indent::IndentCalculator;
16use crate::primitives::reference_highlighter::ReferenceHighlighter;
17use crate::primitives::text_property::TextPropertyManager;
18use crate::view::bracket_highlight_overlay::BracketHighlightOverlay;
19use crate::view::conceal::ConcealManager;
20use crate::view::folding::LspFoldRanges;
21use crate::view::margin::{MarginAnnotation, MarginContent, MarginManager, MarginPosition};
22use crate::view::overlay::{Overlay, OverlayFace, OverlayManager, UnderlineStyle};
23use crate::view::popup::{
24 Popup, PopupContent, PopupKind, PopupListItem, PopupManager, PopupPosition,
25};
26use crate::view::reference_highlight_overlay::ReferenceHighlightOverlay;
27use crate::view::soft_break::SoftBreakManager;
28use crate::view::virtual_text::VirtualTextManager;
29use anyhow::Result;
30use ratatui::style::{Color, Style};
31use std::cell::RefCell;
32use std::ops::Range;
33use std::sync::Arc;
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
39pub enum DisplacedMarker {
40 Main { id: u64, position: usize },
42 Margin { id: u64, position: usize },
44}
45
46impl DisplacedMarker {
47 pub fn encode(&self) -> (u64, usize) {
49 match self {
50 Self::Main { id, position } => (*id, *position),
51 Self::Margin { id, position } => (*id | (1u64 << 63), *position),
52 }
53 }
54
55 pub fn decode(tagged_id: u64, position: usize) -> Self {
57 if (tagged_id >> 63) == 1 {
58 Self::Margin {
59 id: tagged_id & !(1u64 << 63),
60 position,
61 }
62 } else {
63 Self::Main {
64 id: tagged_id,
65 position,
66 }
67 }
68 }
69}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
73pub enum ViewMode {
74 Source,
76 PageView,
79}
80
81#[derive(Debug, Clone)]
92pub struct BufferSettings {
93 pub whitespace: crate::config::WhitespaceVisibility,
96
97 pub use_tabs: bool,
100
101 pub tab_size: usize,
105
106 pub auto_close: bool,
109
110 pub auto_surround: bool,
113
114 pub word_characters: String,
117}
118
119impl Default for BufferSettings {
120 fn default() -> Self {
121 Self {
122 whitespace: crate::config::WhitespaceVisibility::default(),
123 use_tabs: false,
124 tab_size: 4,
125 auto_close: true,
126 auto_surround: true,
127 word_characters: String::new(),
128 }
129 }
130}
131
132pub struct EditorState {
138 pub buffer: Buffer,
140
141 pub highlighter: HighlightEngine,
143
144 pub indent_calculator: RefCell<IndentCalculator>,
146
147 pub overlays: OverlayManager,
149
150 pub marker_list: MarkerList,
152
153 pub virtual_texts: VirtualTextManager,
155
156 pub conceals: ConcealManager,
158
159 pub soft_breaks: SoftBreakManager,
161
162 pub popups: PopupManager,
164
165 pub margins: MarginManager,
167
168 pub primary_cursor_line_number: LineNumber,
171
172 pub mode: String,
174
175 pub text_properties: TextPropertyManager,
178
179 pub show_cursors: bool,
182
183 pub editing_disabled: bool,
187
188 pub scrollable: bool,
192
193 pub buffer_settings: BufferSettings,
196
197 pub reference_highlighter: ReferenceHighlighter,
199
200 pub is_composite_buffer: bool,
202
203 pub debug_highlight_mode: bool,
205
206 pub reference_highlight_overlay: ReferenceHighlightOverlay,
208
209 pub bracket_highlight_overlay: BracketHighlightOverlay,
211
212 pub semantic_tokens: Option<SemanticTokenStore>,
214
215 pub folding_ranges: LspFoldRanges,
219
220 pub language: String,
223
224 pub display_name: String,
229
230 pub line_wrap_cache: crate::view::line_wrap_cache::LineWrapCache,
237
238 pub visual_row_index: crate::view::visual_row_index::VisualRowIndex,
247}
248
249impl EditorState {
250 pub fn apply_language(&mut self, detected: DetectedLanguage) {
257 self.language = detected.name;
258 self.display_name = detected.display_name;
259 self.highlighter = detected.highlighter;
260 if let Some(lang) = &detected.ts_language {
261 self.reference_highlighter.set_language(lang);
262 }
263 }
264
265 fn new_from_buffer(buffer: Buffer) -> Self {
268 let mut marker_list = MarkerList::new();
269 if !buffer.is_empty() {
270 marker_list.adjust_for_insert(0, buffer.len());
271 }
272
273 Self {
274 buffer,
275 highlighter: HighlightEngine::None,
276 indent_calculator: RefCell::new(IndentCalculator::new()),
277 overlays: OverlayManager::new(),
278 marker_list,
279 virtual_texts: VirtualTextManager::new(),
280 conceals: ConcealManager::new(),
281 soft_breaks: SoftBreakManager::new(),
282 popups: PopupManager::new(),
283 margins: MarginManager::new(),
284 primary_cursor_line_number: LineNumber::Absolute(0),
285 mode: "insert".to_string(),
286 text_properties: TextPropertyManager::new(),
287 show_cursors: true,
288 editing_disabled: false,
289 scrollable: true,
290 buffer_settings: BufferSettings::default(),
291 reference_highlighter: ReferenceHighlighter::new(),
292 is_composite_buffer: false,
293 debug_highlight_mode: false,
294 reference_highlight_overlay: ReferenceHighlightOverlay::new(),
295 bracket_highlight_overlay: BracketHighlightOverlay::new(),
296 semantic_tokens: None,
297 folding_ranges: LspFoldRanges::new(),
298 language: "text".to_string(),
299 display_name: "Text".to_string(),
300 line_wrap_cache: crate::view::line_wrap_cache::LineWrapCache::default(),
301 visual_row_index: crate::view::visual_row_index::VisualRowIndex::default(),
302 }
303 }
304
305 pub fn new(
306 _width: u16,
307 _height: u16,
308 large_file_threshold: usize,
309 fs: Arc<dyn FileSystem + Send + Sync>,
310 ) -> Self {
311 Self::new_from_buffer(Buffer::new(large_file_threshold, fs))
312 }
313
314 pub fn new_with_path(
317 large_file_threshold: usize,
318 fs: Arc<dyn FileSystem + Send + Sync>,
319 path: std::path::PathBuf,
320 ) -> Self {
321 Self::new_from_buffer(Buffer::new_with_path(large_file_threshold, fs, path))
322 }
323
324 pub fn set_language_from_name(&mut self, name: &str, registry: &GrammarRegistry) {
328 let detected = DetectedLanguage::from_virtual_name(name, registry);
329 tracing::debug!(
330 "Set highlighter for virtual buffer based on name: {} (backend: {}, language: {})",
331 name,
332 detected.highlighter.backend_name(),
333 detected.name
334 );
335 self.apply_language(detected);
336 }
337
338 pub fn from_file(
343 path: &std::path::Path,
344 _width: u16,
345 _height: u16,
346 large_file_threshold: usize,
347 registry: &GrammarRegistry,
348 fs: Arc<dyn FileSystem + Send + Sync>,
349 ) -> anyhow::Result<Self> {
350 let buffer = Buffer::load_from_file(path, large_file_threshold, fs)?;
351 let first_line = buffer.first_line_lossy();
352 let detected = registry
353 .find_by_path(path, first_line.as_deref())
354 .map(|entry| DetectedLanguage::from_entry(entry, registry))
355 .unwrap_or_else(DetectedLanguage::plain_text);
356 let mut state = Self::new_from_buffer(buffer);
357 state.apply_language(detected);
358 Ok(state)
359 }
360
361 pub fn from_file_with_languages(
369 path: &std::path::Path,
370 _width: u16,
371 _height: u16,
372 large_file_threshold: usize,
373 registry: &GrammarRegistry,
374 languages: &std::collections::HashMap<String, crate::config::LanguageConfig>,
375 fs: Arc<dyn FileSystem + Send + Sync>,
376 ) -> anyhow::Result<Self> {
377 let buffer = Buffer::load_from_file(path, large_file_threshold, fs)?;
378 let first_line = buffer.first_line_lossy();
379 let detected =
380 DetectedLanguage::from_path(path, first_line.as_deref(), registry, languages);
381 let mut state = Self::new_from_buffer(buffer);
382 state.apply_language(detected);
383 Ok(state)
384 }
385
386 pub fn from_buffer_with_language(buffer: Buffer, detected: DetectedLanguage) -> Self {
391 let mut state = Self::new_from_buffer(buffer);
392 state.apply_language(detected);
393 state
394 }
395
396 fn apply_insert(
398 &mut self,
399 cursors: &mut Cursors,
400 position: usize,
401 text: &str,
402 cursor_id: crate::model::event::CursorId,
403 ) {
404 let newlines_inserted = text.matches('\n').count();
405
406 self.marker_list.adjust_for_insert(position, text.len());
408 self.margins.adjust_for_insert(position, text.len());
409
410 self.buffer.insert(position, text);
412
413 self.highlighter.notify_insert(position, text.len());
416 self.highlighter
417 .invalidate_range(position..position + text.len());
418
419 cursors.adjust_for_edit(position, 0, text.len());
424
425 if let Some(cursor) = cursors.get_mut(cursor_id) {
427 cursor.position = position + text.len();
428 cursor.clear_selection();
429 }
430
431 if cursor_id == cursors.primary_id() {
433 self.primary_cursor_line_number = match self.primary_cursor_line_number {
434 LineNumber::Absolute(line) => LineNumber::Absolute(line + newlines_inserted),
435 LineNumber::Relative {
436 line,
437 from_cached_line,
438 } => LineNumber::Relative {
439 line: line + newlines_inserted,
440 from_cached_line,
441 },
442 };
443 }
444 }
445
446 fn apply_delete(
448 &mut self,
449 cursors: &mut Cursors,
450 range: &std::ops::Range<usize>,
451 cursor_id: crate::model::event::CursorId,
452 deleted_text: &str,
453 ) {
454 let len = range.len();
455
456 let primary_newlines_removed = if cursor_id == cursors.primary_id() {
460 let cursor_pos = cursors.get(cursor_id).map_or(range.start, |c| c.position);
461 let bytes_before_cursor = cursor_pos
462 .saturating_sub(range.start)
463 .min(len)
464 .min(deleted_text.len());
465 deleted_text[..bytes_before_cursor].matches('\n').count()
466 } else {
467 0
468 };
469
470 self.virtual_texts
476 .remove_in_range(&mut self.marker_list, range.start, range.end);
477
478 self.marker_list.adjust_for_delete(range.start, len);
480 self.margins.adjust_for_delete(range.start, len);
481
482 self.buffer.delete(range.clone());
484
485 self.highlighter.notify_delete(range.start, len);
488 self.highlighter.invalidate_range(range.clone());
489
490 cursors.adjust_for_edit(range.start, len, 0);
495
496 if let Some(cursor) = cursors.get_mut(cursor_id) {
498 cursor.position = range.start;
499 cursor.clear_selection();
500 }
501
502 if cursor_id == cursors.primary_id() && primary_newlines_removed > 0 {
504 self.primary_cursor_line_number = match self.primary_cursor_line_number {
505 LineNumber::Absolute(line) => {
506 LineNumber::Absolute(line.saturating_sub(primary_newlines_removed))
507 }
508 LineNumber::Relative {
509 line,
510 from_cached_line,
511 } => LineNumber::Relative {
512 line: line.saturating_sub(primary_newlines_removed),
513 from_cached_line,
514 },
515 };
516 }
517 }
518
519 pub fn apply(&mut self, cursors: &mut Cursors, event: &Event) {
522 match event {
523 Event::Insert {
524 position,
525 text,
526 cursor_id,
527 } => self.apply_insert(cursors, *position, text, *cursor_id),
528
529 Event::Delete {
530 range,
531 cursor_id,
532 deleted_text,
533 } => self.apply_delete(cursors, range, *cursor_id, deleted_text),
534
535 Event::MoveCursor {
536 cursor_id,
537 new_position,
538 new_anchor,
539 new_sticky_column,
540 ..
541 } => {
542 if let Some(cursor) = cursors.get_mut(*cursor_id) {
543 cursor.position = *new_position;
544 cursor.anchor = *new_anchor;
545 cursor.sticky_column = *new_sticky_column;
546 }
547
548 if *cursor_id == cursors.primary_id() {
551 self.primary_cursor_line_number =
552 match self.buffer.offset_to_position(*new_position) {
553 Some(pos) => LineNumber::Absolute(pos.line),
554 None => {
555 let estimated_line = *new_position / 80;
558 LineNumber::Absolute(estimated_line)
559 }
560 };
561 }
562 }
563
564 Event::AddCursor {
565 cursor_id,
566 position,
567 anchor,
568 } => {
569 let cursor = if let Some(anchor) = anchor {
570 Cursor::with_selection(*anchor, *position)
571 } else {
572 Cursor::new(*position)
573 };
574
575 cursors.insert_with_id(*cursor_id, cursor);
578
579 cursors.normalize();
580 }
581
582 Event::RemoveCursor { cursor_id, .. } => {
583 cursors.remove(*cursor_id);
584 }
585
586 Event::Scroll { .. } | Event::SetViewport { .. } | Event::Recenter => {
589 tracing::warn!("View event {:?} reached EditorState.apply() - should be handled by SplitViewState", event);
592 }
593
594 Event::SetAnchor {
595 cursor_id,
596 position,
597 } => {
598 if let Some(cursor) = cursors.get_mut(*cursor_id) {
601 cursor.anchor = Some(*position);
602 cursor.deselect_on_move = false;
603 }
604 }
605
606 Event::ClearAnchor { cursor_id } => {
607 if let Some(cursor) = cursors.get_mut(*cursor_id) {
610 cursor.anchor = None;
611 cursor.deselect_on_move = true;
612 cursor.clear_block_selection();
613 }
614 }
615
616 Event::ChangeMode { mode } => {
617 self.mode = mode.clone();
618 }
619
620 Event::AddOverlay {
621 namespace,
622 range,
623 face,
624 priority,
625 message,
626 extend_to_line_end,
627 url,
628 } => {
629 tracing::trace!(
630 "AddOverlay: namespace={:?}, range={:?}, face={:?}, priority={}",
631 namespace,
632 range,
633 face,
634 priority
635 );
636 let overlay_face = convert_event_face_to_overlay_face(face);
638 tracing::trace!("Converted face: {:?}", overlay_face);
639
640 let mut overlay = Overlay::with_priority(
641 &mut self.marker_list,
642 range.clone(),
643 overlay_face,
644 *priority,
645 );
646 overlay.namespace = namespace.clone();
647 overlay.message = message.clone();
648 overlay.extend_to_line_end = *extend_to_line_end;
649 overlay.url = url.clone();
650
651 let actual_range = overlay.range(&self.marker_list);
652 tracing::trace!(
653 "Created overlay with markers - actual range: {:?}, handle={:?}",
654 actual_range,
655 overlay.handle
656 );
657
658 self.overlays.add(overlay);
659 }
660
661 Event::RemoveOverlay { handle } => {
662 tracing::trace!("RemoveOverlay: handle={:?}", handle);
663 self.overlays
664 .remove_by_handle(handle, &mut self.marker_list);
665 }
666
667 Event::RemoveOverlaysInRange { range } => {
668 self.overlays.remove_in_range(range, &mut self.marker_list);
669 }
670
671 Event::ClearNamespace { namespace } => {
672 tracing::trace!("ClearNamespace: namespace={:?}", namespace);
673 self.overlays
674 .clear_namespace(namespace, &mut self.marker_list);
675 }
676
677 Event::ClearOverlays => {
678 self.overlays.clear(&mut self.marker_list);
679 }
680
681 Event::ShowPopup { popup } => {
682 let popup_obj = convert_popup_data_to_popup(popup);
683 self.popups.show_or_replace(popup_obj);
684 }
685
686 Event::HidePopup => {
687 self.popups.hide();
688 }
689
690 Event::ClearPopups => {
691 self.popups.clear();
692 }
693
694 Event::PopupSelectNext => {
695 if let Some(popup) = self.popups.top_mut() {
696 popup.select_next();
697 }
698 }
699
700 Event::PopupSelectPrev => {
701 if let Some(popup) = self.popups.top_mut() {
702 popup.select_prev();
703 }
704 }
705
706 Event::PopupPageDown => {
707 if let Some(popup) = self.popups.top_mut() {
708 popup.page_down();
709 }
710 }
711
712 Event::PopupPageUp => {
713 if let Some(popup) = self.popups.top_mut() {
714 popup.page_up();
715 }
716 }
717
718 Event::AddMarginAnnotation {
719 line,
720 position,
721 content,
722 annotation_id,
723 } => {
724 let margin_position = convert_margin_position(position);
725 let margin_content = convert_margin_content(content);
726 let annotation = if let Some(id) = annotation_id {
727 MarginAnnotation::with_id(*line, margin_position, margin_content, id.clone())
728 } else {
729 MarginAnnotation::new(*line, margin_position, margin_content)
730 };
731 self.margins.add_annotation(annotation);
732 }
733
734 Event::RemoveMarginAnnotation { annotation_id } => {
735 self.margins.remove_by_id(annotation_id);
736 }
737
738 Event::RemoveMarginAnnotationsAtLine { line, position } => {
739 let margin_position = convert_margin_position(position);
740 self.margins.remove_at_line(*line, margin_position);
741 }
742
743 Event::ClearMarginPosition { position } => {
744 let margin_position = convert_margin_position(position);
745 self.margins.clear_position(margin_position);
746 }
747
748 Event::ClearMargins => {
749 self.margins.clear_all();
750 }
751
752 Event::SetLineNumbers { enabled } => {
753 self.margins.configure_for_line_numbers(*enabled);
754 }
755
756 Event::SplitPane { .. }
759 | Event::CloseSplit { .. }
760 | Event::SetActiveSplit { .. }
761 | Event::AdjustSplitRatio { .. }
762 | Event::NextSplit
763 | Event::PrevSplit => {
764 }
766
767 Event::Batch { events, .. } => {
768 for event in events {
771 self.apply(cursors, event);
772 }
773 }
774
775 Event::BulkEdit {
776 new_snapshot,
777 new_cursors,
778 edits,
779 displaced_markers,
780 ..
781 } => {
782 if let Some(snapshot) = new_snapshot {
788 self.buffer.restore_buffer_state(snapshot);
789 }
790
791 for &(pos, del_len, ins_len) in edits {
801 if del_len > 0 && ins_len > 0 {
802 if ins_len > del_len {
804 let net = ins_len - del_len;
805 self.marker_list.adjust_for_insert(pos, net);
806 self.margins.adjust_for_insert(pos, net);
807 } else if del_len > ins_len {
808 let net = del_len - ins_len;
809 self.marker_list.adjust_for_delete(pos, net);
810 self.margins.adjust_for_delete(pos, net);
811 }
812 } else if del_len > 0 {
814 self.marker_list.adjust_for_delete(pos, del_len);
815 self.margins.adjust_for_delete(pos, del_len);
816 } else if ins_len > 0 {
817 self.marker_list.adjust_for_insert(pos, ins_len);
818 self.margins.adjust_for_insert(pos, ins_len);
819 }
820 }
821
822 if !displaced_markers.is_empty() {
827 self.restore_displaced_markers(displaced_markers);
828 }
829
830 self.virtual_texts.clear(&mut self.marker_list);
833
834 use crate::view::overlay::OverlayNamespace;
835 let namespaces = ["lsp-diagnostic", "reference-highlight", "bracket-highlight"];
836 for ns in &namespaces {
837 self.overlays.clear_namespace(
838 &OverlayNamespace::from_string(ns.to_string()),
839 &mut self.marker_list,
840 );
841 }
842
843 for (cursor_id, position, anchor) in new_cursors {
845 if let Some(cursor) = cursors.get_mut(*cursor_id) {
846 cursor.position = *position;
847 cursor.anchor = *anchor;
848 }
849 }
850
851 self.highlighter.invalidate_all();
853
854 let primary_pos = cursors.primary().position;
856 self.primary_cursor_line_number = match self.buffer.offset_to_position(primary_pos)
857 {
858 Some(pos) => crate::model::buffer::LineNumber::Absolute(pos.line),
859 None => crate::model::buffer::LineNumber::Absolute(0),
860 };
861 }
862 }
863 }
864
865 pub fn capture_displaced_markers(&self, range: &Range<usize>) -> Vec<(u64, usize)> {
868 let mut displaced = Vec::new();
869 if range.is_empty() {
870 return displaced;
871 }
872 for (marker_id, start, _end) in self.marker_list.query_range(range.start, range.end) {
873 if start > range.start && start < range.end {
874 displaced.push(
875 DisplacedMarker::Main {
876 id: marker_id.0,
877 position: start,
878 }
879 .encode(),
880 );
881 }
882 }
883 for (marker_id, start, _end) in self.margins.query_indicator_range(range.start, range.end) {
884 if start > range.start && start < range.end {
885 displaced.push(
886 DisplacedMarker::Margin {
887 id: marker_id.0,
888 position: start,
889 }
890 .encode(),
891 );
892 }
893 }
894 displaced
895 }
896
897 pub fn capture_displaced_markers_bulk(
899 &self,
900 edits: &[(usize, usize, String)],
901 ) -> Vec<(u64, usize)> {
902 let mut displaced = Vec::new();
903 for (pos, del_len, _text) in edits {
904 if *del_len > 0 {
905 displaced.extend(self.capture_displaced_markers(&(*pos..*pos + *del_len)));
906 }
907 }
908 displaced
909 }
910
911 pub fn restore_displaced_markers(&mut self, displaced: &[(u64, usize)]) {
913 for &(tagged_id, original_pos) in displaced {
914 let dm = DisplacedMarker::decode(tagged_id, original_pos);
915 match dm {
916 DisplacedMarker::Main { id, position } => {
917 self.marker_list.set_position(MarkerId(id), position);
918 }
919 DisplacedMarker::Margin { id, position } => {
920 self.margins.set_indicator_position(MarkerId(id), position);
921 }
922 }
923 }
924 }
925
926 pub fn apply_many(&mut self, cursors: &mut Cursors, events: &[Event]) {
928 for event in events {
929 self.apply(cursors, event);
930 }
931 }
932
933 pub fn on_focus_lost(&mut self) {
937 if self.popups.dismiss_transient() {
938 tracing::debug!("Dismissed transient popup on buffer focus loss");
939 }
940 }
941}
942
943fn convert_event_face_to_overlay_face(event_face: &EventOverlayFace) -> OverlayFace {
945 match event_face {
946 EventOverlayFace::Underline { color, style } => {
947 let underline_style = match style {
948 crate::model::event::UnderlineStyle::Straight => UnderlineStyle::Straight,
949 crate::model::event::UnderlineStyle::Wavy => UnderlineStyle::Wavy,
950 crate::model::event::UnderlineStyle::Dotted => UnderlineStyle::Dotted,
951 crate::model::event::UnderlineStyle::Dashed => UnderlineStyle::Dashed,
952 };
953 OverlayFace::Underline {
954 color: Color::Rgb(color.0, color.1, color.2),
955 style: underline_style,
956 }
957 }
958 EventOverlayFace::Background { color } => OverlayFace::Background {
959 color: Color::Rgb(color.0, color.1, color.2),
960 },
961 EventOverlayFace::Foreground { color } => OverlayFace::Foreground {
962 color: Color::Rgb(color.0, color.1, color.2),
963 },
964 EventOverlayFace::Style { options } => {
965 use crate::view::theme::named_color_from_str;
966 use ratatui::style::Modifier;
967
968 let mut style = Style::default();
970
971 if let Some(ref fg) = options.fg {
973 if let Some((r, g, b)) = fg.as_rgb() {
974 style = style.fg(Color::Rgb(r, g, b));
975 } else if let Some(key) = fg.as_theme_key() {
976 if let Some(color) = named_color_from_str(key) {
977 style = style.fg(color);
978 }
979 }
980 }
981
982 if let Some(ref bg) = options.bg {
984 if let Some((r, g, b)) = bg.as_rgb() {
985 style = style.bg(Color::Rgb(r, g, b));
986 } else if let Some(key) = bg.as_theme_key() {
987 if let Some(color) = named_color_from_str(key) {
988 style = style.bg(color);
989 }
990 }
991 }
992
993 let mut modifiers = Modifier::empty();
995 if options.bold {
996 modifiers |= Modifier::BOLD;
997 }
998 if options.italic {
999 modifiers |= Modifier::ITALIC;
1000 }
1001 if options.underline {
1002 modifiers |= Modifier::UNDERLINED;
1003 }
1004 if options.strikethrough {
1005 modifiers |= Modifier::CROSSED_OUT;
1006 }
1007 if !modifiers.is_empty() {
1008 style = style.add_modifier(modifiers);
1009 }
1010
1011 let fg_theme = options
1013 .fg
1014 .as_ref()
1015 .and_then(|c| c.as_theme_key())
1016 .filter(|key| named_color_from_str(key).is_none())
1017 .map(String::from);
1018 let bg_theme = options
1019 .bg
1020 .as_ref()
1021 .and_then(|c| c.as_theme_key())
1022 .filter(|key| named_color_from_str(key).is_none())
1023 .map(String::from);
1024
1025 if fg_theme.is_some() || bg_theme.is_some() {
1027 OverlayFace::ThemedStyle {
1028 fallback_style: style,
1029 fg_theme,
1030 bg_theme,
1031 }
1032 } else {
1033 OverlayFace::Style { style }
1034 }
1035 }
1036 }
1037}
1038
1039pub(crate) fn convert_popup_data_to_popup(data: &PopupData) -> Popup {
1041 let content = match &data.content {
1042 crate::model::event::PopupContentData::Text(lines) => PopupContent::Text(lines.clone()),
1043 crate::model::event::PopupContentData::List { items, selected } => PopupContent::List {
1044 items: items
1045 .iter()
1046 .map(|item| PopupListItem {
1047 text: item.text.clone(),
1048 detail: item.detail.clone(),
1049 icon: item.icon.clone(),
1050 data: item.data.clone(),
1051 disabled: false,
1052 })
1053 .collect(),
1054 selected: *selected,
1055 },
1056 };
1057
1058 let position = match data.position {
1059 PopupPositionData::AtCursor => PopupPosition::AtCursor,
1060 PopupPositionData::BelowCursor => PopupPosition::BelowCursor,
1061 PopupPositionData::AboveCursor => PopupPosition::AboveCursor,
1062 PopupPositionData::Fixed { x, y } => PopupPosition::Fixed { x, y },
1063 PopupPositionData::Centered => PopupPosition::Centered,
1064 PopupPositionData::BottomRight => PopupPosition::BottomRight,
1065 PopupPositionData::AboveStatusBarAt { x, status_row } => {
1066 PopupPosition::AboveStatusBarAt { x, status_row }
1067 }
1068 };
1069
1070 let kind = match data.kind {
1072 crate::model::event::PopupKindHint::Completion => PopupKind::Completion,
1073 crate::model::event::PopupKindHint::List => PopupKind::List,
1074 crate::model::event::PopupKindHint::Text => PopupKind::Text,
1075 };
1076
1077 let resolver = match kind {
1084 PopupKind::Completion => crate::view::popup::PopupResolver::Completion,
1085 _ => crate::view::popup::PopupResolver::None,
1086 };
1087
1088 let focused = match kind {
1100 PopupKind::Completion => true,
1104 PopupKind::List => true,
1107 PopupKind::Text => false,
1110 PopupKind::Hover | PopupKind::Action => false,
1114 };
1115
1116 Popup {
1117 kind,
1118 title: data.title.clone(),
1119 description: data.description.clone(),
1120 transient: data.transient,
1121 content,
1122 position,
1123 width: data.width,
1124 max_height: data.max_height,
1125 bordered: data.bordered,
1126 border_style: Style::default().fg(Color::Gray),
1127 background_style: Style::default().bg(Color::Rgb(30, 30, 30)),
1128 scroll_offset: 0,
1129 text_selection: None,
1130 accept_key_hint: None,
1131 resolver,
1132 focused,
1133 focus_key_hint: None,
1134 }
1135}
1136
1137fn convert_margin_position(position: &MarginPositionData) -> MarginPosition {
1139 match position {
1140 MarginPositionData::Left => MarginPosition::Left,
1141 MarginPositionData::Right => MarginPosition::Right,
1142 }
1143}
1144
1145fn convert_margin_content(content: &MarginContentData) -> MarginContent {
1147 match content {
1148 MarginContentData::Text(text) => MarginContent::Text(text.clone()),
1149 MarginContentData::Symbol { text, color } => {
1150 if let Some((r, g, b)) = color {
1151 MarginContent::colored_symbol(text.clone(), Color::Rgb(*r, *g, *b))
1152 } else {
1153 MarginContent::symbol(text.clone(), Style::default())
1154 }
1155 }
1156 MarginContentData::Empty => MarginContent::Empty,
1157 }
1158}
1159
1160impl EditorState {
1161 pub fn prepare_for_render(&mut self, top_byte: usize, height: u16) -> Result<()> {
1168 self.buffer.prepare_viewport(top_byte, height as usize)?;
1169 Ok(())
1170 }
1171
1172 pub fn collect_virtual_line_positions(&self) -> Vec<usize> {
1184 if self.virtual_texts.is_empty() {
1185 return Vec::new();
1186 }
1187 let mut v: Vec<usize> = self
1188 .virtual_texts
1189 .query_lines_in_range(&self.marker_list, 0, self.buffer.len() + 1)
1190 .into_iter()
1191 .map(|(pos, _vt)| pos)
1192 .collect();
1193 v.sort_unstable();
1194 v
1195 }
1196
1197 pub fn collect_soft_break_positions(&self) -> Vec<(usize, u16)> {
1210 if self.soft_breaks.is_empty() {
1211 return Vec::new();
1212 }
1213 self.soft_breaks
1215 .query_viewport(0, self.buffer.len() + 1, &self.marker_list)
1216 }
1217
1218 pub fn get_text_range(&mut self, start: usize, end: usize) -> String {
1238 match self
1240 .buffer
1241 .get_text_range_mut(start, end.saturating_sub(start))
1242 {
1243 Ok(bytes) => String::from_utf8_lossy(&bytes).into_owned(),
1244 Err(e) => {
1245 tracing::warn!("Failed to get text range {}..{}: {}", start, end, e);
1246 String::new()
1247 }
1248 }
1249 }
1250
1251 pub fn get_line_at_offset(&mut self, offset: usize) -> Option<(usize, String)> {
1259 use crate::model::document_model::DocumentModel;
1260
1261 let mut line_start = offset;
1264 while line_start > 0 {
1265 if let Ok(text) = self.buffer.get_text_range_mut(line_start - 1, 1) {
1266 if text.first() == Some(&b'\n') {
1267 break;
1268 }
1269 line_start -= 1;
1270 } else {
1271 break;
1272 }
1273 }
1274
1275 let viewport = self
1277 .get_viewport_content(
1278 crate::model::document_model::DocumentPosition::byte(line_start),
1279 1,
1280 )
1281 .ok()?;
1282
1283 viewport
1284 .lines
1285 .first()
1286 .map(|line| (line.byte_offset, line.content.clone()))
1287 }
1288
1289 pub fn get_text_to_end_of_line(&mut self, cursor_pos: usize) -> Result<String> {
1294 use crate::model::document_model::DocumentModel;
1295
1296 let viewport = self.get_viewport_content(
1298 crate::model::document_model::DocumentPosition::byte(cursor_pos),
1299 1,
1300 )?;
1301
1302 if let Some(line) = viewport.lines.first() {
1303 let line_start = line.byte_offset;
1304 let line_end = line_start + line.content.len();
1305
1306 if cursor_pos >= line_start && cursor_pos <= line_end {
1307 let offset_in_line = cursor_pos - line_start;
1308 Ok(line.content.get(offset_in_line..).unwrap_or("").to_string())
1310 } else {
1311 Ok(String::new())
1312 }
1313 } else {
1314 Ok(String::new())
1315 }
1316 }
1317
1318 pub fn set_semantic_tokens(&mut self, store: SemanticTokenStore) {
1320 self.semantic_tokens = Some(store);
1321 }
1322
1323 pub fn clear_semantic_tokens(&mut self) {
1325 self.semantic_tokens = None;
1326 }
1327
1328 pub fn semantic_tokens_result_id(&self) -> Option<&str> {
1330 self.semantic_tokens
1331 .as_ref()
1332 .and_then(|store| store.result_id.as_deref())
1333 }
1334}
1335
1336impl DocumentModel for EditorState {
1341 fn capabilities(&self) -> DocumentCapabilities {
1342 let line_count = self.buffer.line_count();
1343 DocumentCapabilities {
1344 has_line_index: line_count.is_some(),
1345 uses_lazy_loading: false, byte_length: self.buffer.len(),
1347 approximate_line_count: line_count.unwrap_or_else(|| {
1348 self.buffer.len() / 80
1350 }),
1351 }
1352 }
1353
1354 fn get_viewport_content(
1355 &mut self,
1356 start_pos: DocumentPosition,
1357 max_lines: usize,
1358 ) -> Result<ViewportContent> {
1359 let start_offset = self.position_to_offset(start_pos)?;
1361
1362 let line_iter = self.buffer.iter_lines_from(start_offset, max_lines)?;
1365 let has_more = line_iter.has_more;
1366
1367 let lines = line_iter
1368 .map(|line_data| ViewportLine {
1369 byte_offset: line_data.byte_offset,
1370 content: line_data.content,
1371 has_newline: line_data.has_newline,
1372 approximate_line_number: line_data.line_number,
1373 })
1374 .collect();
1375
1376 Ok(ViewportContent {
1377 start_position: DocumentPosition::ByteOffset(start_offset),
1378 lines,
1379 has_more,
1380 })
1381 }
1382
1383 fn position_to_offset(&self, pos: DocumentPosition) -> Result<usize> {
1384 match pos {
1385 DocumentPosition::ByteOffset(offset) => Ok(offset),
1386 DocumentPosition::LineColumn { line, column } => {
1387 if !self.has_line_index() {
1388 anyhow::bail!("Line indexing not available for this document");
1389 }
1390 let position = crate::model::piece_tree::Position { line, column };
1392 Ok(self.buffer.position_to_offset(position))
1393 }
1394 }
1395 }
1396
1397 fn offset_to_position(&self, offset: usize) -> DocumentPosition {
1398 if self.has_line_index() {
1399 if let Some(pos) = self.buffer.offset_to_position(offset) {
1400 DocumentPosition::LineColumn {
1401 line: pos.line,
1402 column: pos.column,
1403 }
1404 } else {
1405 DocumentPosition::ByteOffset(offset)
1407 }
1408 } else {
1409 DocumentPosition::ByteOffset(offset)
1410 }
1411 }
1412
1413 fn get_range(&mut self, start: DocumentPosition, end: DocumentPosition) -> Result<String> {
1414 let start_offset = self.position_to_offset(start)?;
1415 let end_offset = self.position_to_offset(end)?;
1416
1417 if start_offset > end_offset {
1418 anyhow::bail!(
1419 "Invalid range: start offset {} > end offset {}",
1420 start_offset,
1421 end_offset
1422 );
1423 }
1424
1425 let bytes = self
1426 .buffer
1427 .get_text_range_mut(start_offset, end_offset - start_offset)?;
1428
1429 Ok(String::from_utf8_lossy(&bytes).into_owned())
1430 }
1431
1432 fn get_line_content(&mut self, line_number: usize) -> Option<String> {
1433 if !self.has_line_index() {
1434 return None;
1435 }
1436
1437 let line_start_offset = self.buffer.line_start_offset(line_number)?;
1439
1440 let mut iter = self.buffer.line_iterator(line_start_offset, 80);
1442 if let Some((_start, content)) = iter.next_line() {
1443 let has_newline = content.ends_with('\n');
1444 let line_content = if has_newline {
1445 content[..content.len() - 1].to_string()
1446 } else {
1447 content
1448 };
1449 Some(line_content)
1450 } else {
1451 None
1452 }
1453 }
1454
1455 fn get_chunk_at_offset(&mut self, offset: usize, size: usize) -> Result<(usize, String)> {
1456 let bytes = self.buffer.get_text_range_mut(offset, size)?;
1457
1458 Ok((offset, String::from_utf8_lossy(&bytes).into_owned()))
1459 }
1460
1461 fn insert(&mut self, pos: DocumentPosition, text: &str) -> Result<usize> {
1462 let offset = self.position_to_offset(pos)?;
1463 self.buffer.insert_bytes(offset, text.as_bytes().to_vec());
1464 Ok(text.len())
1465 }
1466
1467 fn delete(&mut self, start: DocumentPosition, end: DocumentPosition) -> Result<()> {
1468 let start_offset = self.position_to_offset(start)?;
1469 let end_offset = self.position_to_offset(end)?;
1470
1471 if start_offset > end_offset {
1472 anyhow::bail!(
1473 "Invalid range: start offset {} > end offset {}",
1474 start_offset,
1475 end_offset
1476 );
1477 }
1478
1479 self.buffer.delete(start_offset..end_offset);
1480 Ok(())
1481 }
1482
1483 fn replace(
1484 &mut self,
1485 start: DocumentPosition,
1486 end: DocumentPosition,
1487 text: &str,
1488 ) -> Result<()> {
1489 self.delete(start, end)?;
1491 self.insert(start, text)?;
1492 Ok(())
1493 }
1494
1495 fn find_matches(
1496 &mut self,
1497 pattern: &str,
1498 search_range: Option<(DocumentPosition, DocumentPosition)>,
1499 ) -> Result<Vec<usize>> {
1500 let (start_offset, end_offset) = if let Some((start, end)) = search_range {
1501 (
1502 self.position_to_offset(start)?,
1503 self.position_to_offset(end)?,
1504 )
1505 } else {
1506 (0, self.buffer.len())
1507 };
1508
1509 let bytes = self
1511 .buffer
1512 .get_text_range_mut(start_offset, end_offset - start_offset)?;
1513 let text = String::from_utf8_lossy(&bytes);
1514
1515 let mut matches = Vec::new();
1517 let mut search_offset = 0;
1518 while let Some(pos) = text[search_offset..].find(pattern) {
1519 matches.push(start_offset + search_offset + pos);
1520 search_offset += pos + pattern.len();
1521 }
1522
1523 Ok(matches)
1524 }
1525}
1526
1527#[derive(Clone, Debug)]
1529pub struct SemanticTokenStore {
1530 pub version: u64,
1532 pub result_id: Option<String>,
1534 pub data: Vec<u32>,
1536 pub tokens: Vec<SemanticTokenSpan>,
1538}
1539
1540#[derive(Clone, Debug)]
1542pub struct SemanticTokenSpan {
1543 pub range: Range<usize>,
1544 pub token_type: String,
1545 pub modifiers: Vec<String>,
1546}
1547
1548#[cfg(test)]
1549mod tests {
1550 use crate::model::filesystem::StdFileSystem;
1551 use std::sync::Arc;
1552
1553 fn test_fs() -> Arc<dyn crate::model::filesystem::FileSystem + Send + Sync> {
1554 Arc::new(StdFileSystem)
1555 }
1556 use super::*;
1557 use crate::model::event::CursorId;
1558
1559 #[test]
1560 fn test_state_new() {
1561 let state = EditorState::new(
1562 80,
1563 24,
1564 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1565 test_fs(),
1566 );
1567 assert!(state.buffer.is_empty());
1568 }
1569
1570 #[test]
1571 fn test_apply_insert() {
1572 let mut state = EditorState::new(
1573 80,
1574 24,
1575 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1576 test_fs(),
1577 );
1578 let mut cursors = Cursors::new();
1579 let cursor_id = cursors.primary_id();
1580
1581 state.apply(
1582 &mut cursors,
1583 &Event::Insert {
1584 position: 0,
1585 text: "hello".to_string(),
1586 cursor_id,
1587 },
1588 );
1589
1590 assert_eq!(state.buffer.to_string().unwrap(), "hello");
1591 assert_eq!(cursors.primary().position, 5);
1592 assert!(state.buffer.is_modified());
1593 }
1594
1595 #[test]
1596 fn test_apply_delete() {
1597 let mut state = EditorState::new(
1598 80,
1599 24,
1600 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1601 test_fs(),
1602 );
1603 let mut cursors = Cursors::new();
1604 let cursor_id = cursors.primary_id();
1605
1606 state.apply(
1608 &mut cursors,
1609 &Event::Insert {
1610 position: 0,
1611 text: "hello world".to_string(),
1612 cursor_id,
1613 },
1614 );
1615
1616 state.apply(
1617 &mut cursors,
1618 &Event::Delete {
1619 range: 5..11,
1620 deleted_text: " world".to_string(),
1621 cursor_id,
1622 },
1623 );
1624
1625 assert_eq!(state.buffer.to_string().unwrap(), "hello");
1626 assert_eq!(cursors.primary().position, 5);
1627 }
1628
1629 #[test]
1630 fn test_apply_move_cursor() {
1631 let mut state = EditorState::new(
1632 80,
1633 24,
1634 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1635 test_fs(),
1636 );
1637 let mut cursors = Cursors::new();
1638 let cursor_id = cursors.primary_id();
1639
1640 state.apply(
1641 &mut cursors,
1642 &Event::Insert {
1643 position: 0,
1644 text: "hello".to_string(),
1645 cursor_id,
1646 },
1647 );
1648
1649 state.apply(
1650 &mut cursors,
1651 &Event::MoveCursor {
1652 cursor_id,
1653 old_position: 5,
1654 new_position: 2,
1655 old_anchor: None,
1656 new_anchor: None,
1657 old_sticky_column: 0,
1658 new_sticky_column: 0,
1659 },
1660 );
1661
1662 assert_eq!(cursors.primary().position, 2);
1663 }
1664
1665 #[test]
1666 fn test_apply_add_cursor() {
1667 let mut state = EditorState::new(
1668 80,
1669 24,
1670 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1671 test_fs(),
1672 );
1673 let mut cursors = Cursors::new();
1674 let cursor_id = CursorId(1);
1675
1676 state.apply(
1677 &mut cursors,
1678 &Event::AddCursor {
1679 cursor_id,
1680 position: 5,
1681 anchor: None,
1682 },
1683 );
1684
1685 assert_eq!(cursors.count(), 2);
1686 }
1687
1688 #[test]
1689 fn test_apply_many() {
1690 let mut state = EditorState::new(
1691 80,
1692 24,
1693 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1694 test_fs(),
1695 );
1696 let mut cursors = Cursors::new();
1697 let cursor_id = cursors.primary_id();
1698
1699 let events = vec![
1700 Event::Insert {
1701 position: 0,
1702 text: "hello ".to_string(),
1703 cursor_id,
1704 },
1705 Event::Insert {
1706 position: 6,
1707 text: "world".to_string(),
1708 cursor_id,
1709 },
1710 ];
1711
1712 state.apply_many(&mut cursors, &events);
1713
1714 assert_eq!(state.buffer.to_string().unwrap(), "hello world");
1715 }
1716
1717 #[test]
1718 fn test_cursor_adjustment_after_insert() {
1719 let mut state = EditorState::new(
1720 80,
1721 24,
1722 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1723 test_fs(),
1724 );
1725 let mut cursors = Cursors::new();
1726 let cursor_id = cursors.primary_id();
1727
1728 state.apply(
1730 &mut cursors,
1731 &Event::AddCursor {
1732 cursor_id: CursorId(1),
1733 position: 5,
1734 anchor: None,
1735 },
1736 );
1737
1738 state.apply(
1740 &mut cursors,
1741 &Event::Insert {
1742 position: 0,
1743 text: "abc".to_string(),
1744 cursor_id,
1745 },
1746 );
1747
1748 if let Some(cursor) = cursors.get(CursorId(1)) {
1750 assert_eq!(cursor.position, 8);
1751 }
1752 }
1753
1754 mod document_model_tests {
1756 use super::*;
1757 use crate::model::document_model::{DocumentModel, DocumentPosition};
1758
1759 #[test]
1760 fn test_capabilities_small_file() {
1761 let mut state = EditorState::new(
1762 80,
1763 24,
1764 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1765 test_fs(),
1766 );
1767 state.buffer = Buffer::from_str_test("line1\nline2\nline3");
1768
1769 let caps = state.capabilities();
1770 assert!(caps.has_line_index, "Small file should have line index");
1771 assert_eq!(caps.byte_length, "line1\nline2\nline3".len());
1772 assert_eq!(caps.approximate_line_count, 3, "Should have 3 lines");
1773 }
1774
1775 #[test]
1776 fn test_position_conversions() {
1777 let mut state = EditorState::new(
1778 80,
1779 24,
1780 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1781 test_fs(),
1782 );
1783 state.buffer = Buffer::from_str_test("hello\nworld\ntest");
1784
1785 let pos1 = DocumentPosition::ByteOffset(6);
1787 let offset1 = state.position_to_offset(pos1).unwrap();
1788 assert_eq!(offset1, 6);
1789
1790 let pos2 = DocumentPosition::LineColumn { line: 1, column: 0 };
1792 let offset2 = state.position_to_offset(pos2).unwrap();
1793 assert_eq!(offset2, 6, "Line 1, column 0 should be at byte 6");
1794
1795 let converted = state.offset_to_position(6);
1797 match converted {
1798 DocumentPosition::LineColumn { line, column } => {
1799 assert_eq!(line, 1);
1800 assert_eq!(column, 0);
1801 }
1802 _ => panic!("Expected LineColumn for small file"),
1803 }
1804 }
1805
1806 #[test]
1807 fn test_get_viewport_content() {
1808 let mut state = EditorState::new(
1809 80,
1810 24,
1811 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1812 test_fs(),
1813 );
1814 state.buffer = Buffer::from_str_test("line1\nline2\nline3\nline4\nline5");
1815
1816 let content = state
1817 .get_viewport_content(DocumentPosition::ByteOffset(0), 3)
1818 .unwrap();
1819
1820 assert_eq!(content.lines.len(), 3);
1821 assert_eq!(content.lines[0].content, "line1");
1822 assert_eq!(content.lines[1].content, "line2");
1823 assert_eq!(content.lines[2].content, "line3");
1824 assert!(content.has_more);
1825 }
1826
1827 #[test]
1828 fn test_get_range() {
1829 let mut state = EditorState::new(
1830 80,
1831 24,
1832 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1833 test_fs(),
1834 );
1835 state.buffer = Buffer::from_str_test("hello world");
1836
1837 let text = state
1838 .get_range(
1839 DocumentPosition::ByteOffset(0),
1840 DocumentPosition::ByteOffset(5),
1841 )
1842 .unwrap();
1843 assert_eq!(text, "hello");
1844
1845 let text2 = state
1846 .get_range(
1847 DocumentPosition::ByteOffset(6),
1848 DocumentPosition::ByteOffset(11),
1849 )
1850 .unwrap();
1851 assert_eq!(text2, "world");
1852 }
1853
1854 #[test]
1855 fn test_get_line_content() {
1856 let mut state = EditorState::new(
1857 80,
1858 24,
1859 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1860 test_fs(),
1861 );
1862 state.buffer = Buffer::from_str_test("line1\nline2\nline3");
1863
1864 let line0 = state.get_line_content(0).unwrap();
1865 assert_eq!(line0, "line1");
1866
1867 let line1 = state.get_line_content(1).unwrap();
1868 assert_eq!(line1, "line2");
1869
1870 let line2 = state.get_line_content(2).unwrap();
1871 assert_eq!(line2, "line3");
1872 }
1873
1874 #[test]
1875 fn test_insert_delete() {
1876 let mut state = EditorState::new(
1877 80,
1878 24,
1879 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1880 test_fs(),
1881 );
1882 state.buffer = Buffer::from_str_test("hello world");
1883
1884 let bytes_inserted = state
1886 .insert(DocumentPosition::ByteOffset(6), "beautiful ")
1887 .unwrap();
1888 assert_eq!(bytes_inserted, 10);
1889 assert_eq!(state.buffer.to_string().unwrap(), "hello beautiful world");
1890
1891 state
1893 .delete(
1894 DocumentPosition::ByteOffset(6),
1895 DocumentPosition::ByteOffset(16),
1896 )
1897 .unwrap();
1898 assert_eq!(state.buffer.to_string().unwrap(), "hello world");
1899 }
1900
1901 #[test]
1902 fn test_replace() {
1903 let mut state = EditorState::new(
1904 80,
1905 24,
1906 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1907 test_fs(),
1908 );
1909 state.buffer = Buffer::from_str_test("hello world");
1910
1911 state
1912 .replace(
1913 DocumentPosition::ByteOffset(0),
1914 DocumentPosition::ByteOffset(5),
1915 "hi",
1916 )
1917 .unwrap();
1918 assert_eq!(state.buffer.to_string().unwrap(), "hi world");
1919 }
1920
1921 #[test]
1922 fn test_find_matches() {
1923 let mut state = EditorState::new(
1924 80,
1925 24,
1926 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1927 test_fs(),
1928 );
1929 state.buffer = Buffer::from_str_test("hello world hello");
1930
1931 let matches = state.find_matches("hello", None).unwrap();
1932 assert_eq!(matches.len(), 2);
1933 assert_eq!(matches[0], 0);
1934 assert_eq!(matches[1], 12);
1935 }
1936
1937 #[test]
1938 fn test_prepare_for_render() {
1939 let mut state = EditorState::new(
1940 80,
1941 24,
1942 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1943 test_fs(),
1944 );
1945 state.buffer = Buffer::from_str_test("line1\nline2\nline3\nline4\nline5");
1946
1947 state.prepare_for_render(0, 24).unwrap();
1949 }
1950
1951 #[test]
1952 fn test_helper_get_text_range() {
1953 let mut state = EditorState::new(
1954 80,
1955 24,
1956 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1957 test_fs(),
1958 );
1959 state.buffer = Buffer::from_str_test("hello world");
1960
1961 let text = state.get_text_range(0, 5);
1963 assert_eq!(text, "hello");
1964
1965 let text2 = state.get_text_range(6, 11);
1967 assert_eq!(text2, "world");
1968 }
1969
1970 #[test]
1971 fn test_helper_get_line_at_offset() {
1972 let mut state = EditorState::new(
1973 80,
1974 24,
1975 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1976 test_fs(),
1977 );
1978 state.buffer = Buffer::from_str_test("line1\nline2\nline3");
1979
1980 let (offset, content) = state.get_line_at_offset(0).unwrap();
1982 assert_eq!(offset, 0);
1983 assert_eq!(content, "line1");
1984
1985 let (offset2, content2) = state.get_line_at_offset(8).unwrap();
1987 assert_eq!(offset2, 6); assert_eq!(content2, "line2");
1989
1990 let (offset3, content3) = state.get_line_at_offset(12).unwrap();
1992 assert_eq!(offset3, 12);
1993 assert_eq!(content3, "line3");
1994 }
1995
1996 #[test]
1997 fn test_helper_get_text_to_end_of_line() {
1998 let mut state = EditorState::new(
1999 80,
2000 24,
2001 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
2002 test_fs(),
2003 );
2004 state.buffer = Buffer::from_str_test("hello world\nline2");
2005
2006 let text = state.get_text_to_end_of_line(0).unwrap();
2008 assert_eq!(text, "hello world");
2009
2010 let text2 = state.get_text_to_end_of_line(6).unwrap();
2012 assert_eq!(text2, "world");
2013
2014 let text3 = state.get_text_to_end_of_line(11).unwrap();
2016 assert_eq!(text3, "");
2017
2018 let text4 = state.get_text_to_end_of_line(12).unwrap();
2020 assert_eq!(text4, "line2");
2021 }
2022 }
2023
2024 mod virtual_text_integration_tests {
2026 use super::*;
2027 use crate::view::virtual_text::VirtualTextPosition;
2028 use ratatui::style::Style;
2029
2030 #[test]
2031 fn test_virtual_text_add_and_query() {
2032 let mut state = EditorState::new(
2033 80,
2034 24,
2035 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
2036 test_fs(),
2037 );
2038 state.buffer = Buffer::from_str_test("hello world");
2039
2040 if !state.buffer.is_empty() {
2042 state.marker_list.adjust_for_insert(0, state.buffer.len());
2043 }
2044
2045 let vtext_id = state.virtual_texts.add(
2047 &mut state.marker_list,
2048 5,
2049 ": string".to_string(),
2050 Style::default(),
2051 VirtualTextPosition::AfterChar,
2052 0,
2053 );
2054
2055 let results = state.virtual_texts.query_range(&state.marker_list, 0, 11);
2057 assert_eq!(results.len(), 1);
2058 assert_eq!(results[0].0, 5); assert_eq!(results[0].1.text, ": string");
2060
2061 let lookup = state.virtual_texts.build_lookup(&state.marker_list, 0, 11);
2063 assert!(lookup.contains_key(&5));
2064 assert_eq!(lookup[&5].len(), 1);
2065 assert_eq!(lookup[&5][0].text, ": string");
2066
2067 state.virtual_texts.remove(&mut state.marker_list, vtext_id);
2069 assert!(state.virtual_texts.is_empty());
2070 }
2071
2072 #[test]
2073 fn test_virtual_text_position_tracking_on_insert() {
2074 let mut state = EditorState::new(
2075 80,
2076 24,
2077 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
2078 test_fs(),
2079 );
2080 state.buffer = Buffer::from_str_test("hello world");
2081
2082 if !state.buffer.is_empty() {
2084 state.marker_list.adjust_for_insert(0, state.buffer.len());
2085 }
2086
2087 let _vtext_id = state.virtual_texts.add(
2089 &mut state.marker_list,
2090 6,
2091 "/*param*/".to_string(),
2092 Style::default(),
2093 VirtualTextPosition::BeforeChar,
2094 0,
2095 );
2096
2097 let mut cursors = Cursors::new();
2099 let cursor_id = cursors.primary_id();
2100 state.apply(
2101 &mut cursors,
2102 &Event::Insert {
2103 position: 6,
2104 text: "beautiful ".to_string(),
2105 cursor_id,
2106 },
2107 );
2108
2109 let results = state.virtual_texts.query_range(&state.marker_list, 0, 30);
2111 assert_eq!(results.len(), 1);
2112 assert_eq!(results[0].0, 16); assert_eq!(results[0].1.text, "/*param*/");
2114 }
2115
2116 #[test]
2117 fn test_virtual_text_position_tracking_on_delete() {
2118 let mut state = EditorState::new(
2119 80,
2120 24,
2121 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
2122 test_fs(),
2123 );
2124 state.buffer = Buffer::from_str_test("hello beautiful world");
2125
2126 if !state.buffer.is_empty() {
2128 state.marker_list.adjust_for_insert(0, state.buffer.len());
2129 }
2130
2131 let _vtext_id = state.virtual_texts.add(
2133 &mut state.marker_list,
2134 16,
2135 ": string".to_string(),
2136 Style::default(),
2137 VirtualTextPosition::AfterChar,
2138 0,
2139 );
2140
2141 let mut cursors = Cursors::new();
2143 let cursor_id = cursors.primary_id();
2144 state.apply(
2145 &mut cursors,
2146 &Event::Delete {
2147 range: 6..16,
2148 deleted_text: "beautiful ".to_string(),
2149 cursor_id,
2150 },
2151 );
2152
2153 let results = state.virtual_texts.query_range(&state.marker_list, 0, 20);
2155 assert_eq!(results.len(), 1);
2156 assert_eq!(results[0].0, 6); assert_eq!(results[0].1.text, ": string");
2158 }
2159
2160 #[test]
2161 fn test_multiple_virtual_texts_with_priorities() {
2162 let mut state = EditorState::new(
2163 80,
2164 24,
2165 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
2166 test_fs(),
2167 );
2168 state.buffer = Buffer::from_str_test("let x = 5");
2169
2170 if !state.buffer.is_empty() {
2172 state.marker_list.adjust_for_insert(0, state.buffer.len());
2173 }
2174
2175 state.virtual_texts.add(
2177 &mut state.marker_list,
2178 5,
2179 ": i32".to_string(),
2180 Style::default(),
2181 VirtualTextPosition::AfterChar,
2182 0, );
2184
2185 state.virtual_texts.add(
2187 &mut state.marker_list,
2188 5,
2189 " /* inferred */".to_string(),
2190 Style::default(),
2191 VirtualTextPosition::AfterChar,
2192 10, );
2194
2195 let lookup = state.virtual_texts.build_lookup(&state.marker_list, 0, 10);
2197 assert!(lookup.contains_key(&5));
2198 let vtexts = &lookup[&5];
2199 assert_eq!(vtexts.len(), 2);
2200 assert_eq!(vtexts[0].text, ": i32");
2202 assert_eq!(vtexts[1].text, " /* inferred */");
2203 }
2204
2205 #[test]
2206 fn test_virtual_text_clear() {
2207 let mut state = EditorState::new(
2208 80,
2209 24,
2210 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
2211 test_fs(),
2212 );
2213 state.buffer = Buffer::from_str_test("test");
2214
2215 if !state.buffer.is_empty() {
2217 state.marker_list.adjust_for_insert(0, state.buffer.len());
2218 }
2219
2220 state.virtual_texts.add(
2222 &mut state.marker_list,
2223 0,
2224 "hint1".to_string(),
2225 Style::default(),
2226 VirtualTextPosition::BeforeChar,
2227 0,
2228 );
2229 state.virtual_texts.add(
2230 &mut state.marker_list,
2231 2,
2232 "hint2".to_string(),
2233 Style::default(),
2234 VirtualTextPosition::AfterChar,
2235 0,
2236 );
2237
2238 assert_eq!(state.virtual_texts.len(), 2);
2239
2240 state.virtual_texts.clear(&mut state.marker_list);
2242 assert!(state.virtual_texts.is_empty());
2243
2244 let results = state.virtual_texts.query_range(&state.marker_list, 0, 10);
2246 assert!(results.is_empty());
2247 }
2248 }
2249}