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