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 detected = registry
359 .find_by_path(path)
360 .map(|entry| DetectedLanguage::from_entry(entry, registry))
361 .unwrap_or_else(DetectedLanguage::plain_text);
362 let mut state = Self::new_from_buffer(buffer);
363 state.apply_language(detected);
364 Ok(state)
365 }
366
367 pub fn from_file_with_languages(
375 path: &std::path::Path,
376 _width: u16,
377 _height: u16,
378 large_file_threshold: usize,
379 registry: &GrammarRegistry,
380 languages: &std::collections::HashMap<String, crate::config::LanguageConfig>,
381 fs: Arc<dyn FileSystem + Send + Sync>,
382 ) -> anyhow::Result<Self> {
383 let buffer = Buffer::load_from_file(path, large_file_threshold, fs)?;
384 let detected = DetectedLanguage::from_path(path, registry, languages);
385 let mut state = Self::new_from_buffer(buffer);
386 state.apply_language(detected);
387 Ok(state)
388 }
389
390 pub fn from_buffer_with_language(buffer: Buffer, detected: DetectedLanguage) -> Self {
395 let mut state = Self::new_from_buffer(buffer);
396 state.apply_language(detected);
397 state
398 }
399
400 fn apply_insert(
402 &mut self,
403 cursors: &mut Cursors,
404 position: usize,
405 text: &str,
406 cursor_id: crate::model::event::CursorId,
407 ) {
408 let newlines_inserted = text.matches('\n').count();
409
410 self.marker_list.adjust_for_insert(position, text.len());
412 self.margins.adjust_for_insert(position, text.len());
413
414 self.buffer.insert(position, text);
416
417 self.highlighter.notify_insert(position, text.len());
420 self.highlighter
421 .invalidate_range(position..position + text.len());
422
423 cursors.adjust_for_edit(position, 0, text.len());
428
429 if let Some(cursor) = cursors.get_mut(cursor_id) {
431 cursor.position = position + text.len();
432 cursor.clear_selection();
433 }
434
435 if cursor_id == cursors.primary_id() {
437 self.primary_cursor_line_number = match self.primary_cursor_line_number {
438 LineNumber::Absolute(line) => LineNumber::Absolute(line + newlines_inserted),
439 LineNumber::Relative {
440 line,
441 from_cached_line,
442 } => LineNumber::Relative {
443 line: line + newlines_inserted,
444 from_cached_line,
445 },
446 };
447 }
448 }
449
450 fn apply_delete(
452 &mut self,
453 cursors: &mut Cursors,
454 range: &std::ops::Range<usize>,
455 cursor_id: crate::model::event::CursorId,
456 deleted_text: &str,
457 ) {
458 let len = range.len();
459
460 let primary_newlines_removed = if cursor_id == cursors.primary_id() {
464 let cursor_pos = cursors.get(cursor_id).map_or(range.start, |c| c.position);
465 let bytes_before_cursor = cursor_pos.saturating_sub(range.start).min(len);
466 deleted_text[..bytes_before_cursor].matches('\n').count()
467 } else {
468 0
469 };
470
471 self.virtual_texts
477 .remove_in_range(&mut self.marker_list, range.start, range.end);
478
479 self.marker_list.adjust_for_delete(range.start, len);
481 self.margins.adjust_for_delete(range.start, len);
482
483 self.buffer.delete(range.clone());
485
486 self.highlighter.notify_delete(range.start, len);
489 self.highlighter.invalidate_range(range.clone());
490
491 cursors.adjust_for_edit(range.start, len, 0);
496
497 if let Some(cursor) = cursors.get_mut(cursor_id) {
499 cursor.position = range.start;
500 cursor.clear_selection();
501 }
502
503 if cursor_id == cursors.primary_id() && primary_newlines_removed > 0 {
505 self.primary_cursor_line_number = match self.primary_cursor_line_number {
506 LineNumber::Absolute(line) => {
507 LineNumber::Absolute(line.saturating_sub(primary_newlines_removed))
508 }
509 LineNumber::Relative {
510 line,
511 from_cached_line,
512 } => LineNumber::Relative {
513 line: line.saturating_sub(primary_newlines_removed),
514 from_cached_line,
515 },
516 };
517 }
518 }
519
520 pub fn apply(&mut self, cursors: &mut Cursors, event: &Event) {
523 match event {
524 Event::Insert {
525 position,
526 text,
527 cursor_id,
528 } => self.apply_insert(cursors, *position, text, *cursor_id),
529
530 Event::Delete {
531 range,
532 cursor_id,
533 deleted_text,
534 } => self.apply_delete(cursors, range, *cursor_id, deleted_text),
535
536 Event::MoveCursor {
537 cursor_id,
538 new_position,
539 new_anchor,
540 new_sticky_column,
541 ..
542 } => {
543 if let Some(cursor) = cursors.get_mut(*cursor_id) {
544 cursor.position = *new_position;
545 cursor.anchor = *new_anchor;
546 cursor.sticky_column = *new_sticky_column;
547 }
548
549 if *cursor_id == cursors.primary_id() {
552 self.primary_cursor_line_number =
553 match self.buffer.offset_to_position(*new_position) {
554 Some(pos) => LineNumber::Absolute(pos.line),
555 None => {
556 let estimated_line = *new_position / 80;
559 LineNumber::Absolute(estimated_line)
560 }
561 };
562 }
563 }
564
565 Event::AddCursor {
566 cursor_id,
567 position,
568 anchor,
569 } => {
570 let cursor = if let Some(anchor) = anchor {
571 Cursor::with_selection(*anchor, *position)
572 } else {
573 Cursor::new(*position)
574 };
575
576 cursors.insert_with_id(*cursor_id, cursor);
579
580 cursors.normalize();
581 }
582
583 Event::RemoveCursor { cursor_id, .. } => {
584 cursors.remove(*cursor_id);
585 }
586
587 Event::Scroll { .. } | Event::SetViewport { .. } | Event::Recenter => {
590 tracing::warn!("View event {:?} reached EditorState.apply() - should be handled by SplitViewState", event);
593 }
594
595 Event::SetAnchor {
596 cursor_id,
597 position,
598 } => {
599 if let Some(cursor) = cursors.get_mut(*cursor_id) {
602 cursor.anchor = Some(*position);
603 cursor.deselect_on_move = false;
604 }
605 }
606
607 Event::ClearAnchor { cursor_id } => {
608 if let Some(cursor) = cursors.get_mut(*cursor_id) {
611 cursor.anchor = None;
612 cursor.deselect_on_move = true;
613 cursor.clear_block_selection();
614 }
615 }
616
617 Event::ChangeMode { mode } => {
618 self.mode = mode.clone();
619 }
620
621 Event::AddOverlay {
622 namespace,
623 range,
624 face,
625 priority,
626 message,
627 extend_to_line_end,
628 url,
629 } => {
630 tracing::trace!(
631 "AddOverlay: namespace={:?}, range={:?}, face={:?}, priority={}",
632 namespace,
633 range,
634 face,
635 priority
636 );
637 let overlay_face = convert_event_face_to_overlay_face(face);
639 tracing::trace!("Converted face: {:?}", overlay_face);
640
641 let mut overlay = Overlay::with_priority(
642 &mut self.marker_list,
643 range.clone(),
644 overlay_face,
645 *priority,
646 );
647 overlay.namespace = namespace.clone();
648 overlay.message = message.clone();
649 overlay.extend_to_line_end = *extend_to_line_end;
650 overlay.url = url.clone();
651
652 let actual_range = overlay.range(&self.marker_list);
653 tracing::trace!(
654 "Created overlay with markers - actual range: {:?}, handle={:?}",
655 actual_range,
656 overlay.handle
657 );
658
659 self.overlays.add(overlay);
660 }
661
662 Event::RemoveOverlay { handle } => {
663 tracing::trace!("RemoveOverlay: handle={:?}", handle);
664 self.overlays
665 .remove_by_handle(handle, &mut self.marker_list);
666 }
667
668 Event::RemoveOverlaysInRange { range } => {
669 self.overlays.remove_in_range(range, &mut self.marker_list);
670 }
671
672 Event::ClearNamespace { namespace } => {
673 tracing::trace!("ClearNamespace: namespace={:?}", namespace);
674 self.overlays
675 .clear_namespace(namespace, &mut self.marker_list);
676 }
677
678 Event::ClearOverlays => {
679 self.overlays.clear(&mut self.marker_list);
680 }
681
682 Event::ShowPopup { popup } => {
683 let popup_obj = convert_popup_data_to_popup(popup);
684 self.popups.show_or_replace(popup_obj);
685 }
686
687 Event::HidePopup => {
688 self.popups.hide();
689 }
690
691 Event::ClearPopups => {
692 self.popups.clear();
693 }
694
695 Event::PopupSelectNext => {
696 if let Some(popup) = self.popups.top_mut() {
697 popup.select_next();
698 }
699 }
700
701 Event::PopupSelectPrev => {
702 if let Some(popup) = self.popups.top_mut() {
703 popup.select_prev();
704 }
705 }
706
707 Event::PopupPageDown => {
708 if let Some(popup) = self.popups.top_mut() {
709 popup.page_down();
710 }
711 }
712
713 Event::PopupPageUp => {
714 if let Some(popup) = self.popups.top_mut() {
715 popup.page_up();
716 }
717 }
718
719 Event::AddMarginAnnotation {
720 line,
721 position,
722 content,
723 annotation_id,
724 } => {
725 let margin_position = convert_margin_position(position);
726 let margin_content = convert_margin_content(content);
727 let annotation = if let Some(id) = annotation_id {
728 MarginAnnotation::with_id(*line, margin_position, margin_content, id.clone())
729 } else {
730 MarginAnnotation::new(*line, margin_position, margin_content)
731 };
732 self.margins.add_annotation(annotation);
733 }
734
735 Event::RemoveMarginAnnotation { annotation_id } => {
736 self.margins.remove_by_id(annotation_id);
737 }
738
739 Event::RemoveMarginAnnotationsAtLine { line, position } => {
740 let margin_position = convert_margin_position(position);
741 self.margins.remove_at_line(*line, margin_position);
742 }
743
744 Event::ClearMarginPosition { position } => {
745 let margin_position = convert_margin_position(position);
746 self.margins.clear_position(margin_position);
747 }
748
749 Event::ClearMargins => {
750 self.margins.clear_all();
751 }
752
753 Event::SetLineNumbers { enabled } => {
754 self.margins.configure_for_line_numbers(*enabled);
755 }
756
757 Event::SplitPane { .. }
760 | Event::CloseSplit { .. }
761 | Event::SetActiveSplit { .. }
762 | Event::AdjustSplitRatio { .. }
763 | Event::NextSplit
764 | Event::PrevSplit => {
765 }
767
768 Event::Batch { events, .. } => {
769 for event in events {
772 self.apply(cursors, event);
773 }
774 }
775
776 Event::BulkEdit {
777 new_snapshot,
778 new_cursors,
779 edits,
780 displaced_markers,
781 ..
782 } => {
783 if let Some(snapshot) = new_snapshot {
789 self.buffer.restore_buffer_state(snapshot);
790 }
791
792 for &(pos, del_len, ins_len) in edits {
802 if del_len > 0 && ins_len > 0 {
803 if ins_len > del_len {
805 let net = ins_len - del_len;
806 self.marker_list.adjust_for_insert(pos, net);
807 self.margins.adjust_for_insert(pos, net);
808 } else if del_len > ins_len {
809 let net = del_len - ins_len;
810 self.marker_list.adjust_for_delete(pos, net);
811 self.margins.adjust_for_delete(pos, net);
812 }
813 } else if del_len > 0 {
815 self.marker_list.adjust_for_delete(pos, del_len);
816 self.margins.adjust_for_delete(pos, del_len);
817 } else if ins_len > 0 {
818 self.marker_list.adjust_for_insert(pos, ins_len);
819 self.margins.adjust_for_insert(pos, ins_len);
820 }
821 }
822
823 if !displaced_markers.is_empty() {
828 self.restore_displaced_markers(displaced_markers);
829 }
830
831 self.virtual_texts.clear(&mut self.marker_list);
834
835 use crate::view::overlay::OverlayNamespace;
836 let namespaces = ["lsp-diagnostic", "reference-highlight", "bracket-highlight"];
837 for ns in &namespaces {
838 self.overlays.clear_namespace(
839 &OverlayNamespace::from_string(ns.to_string()),
840 &mut self.marker_list,
841 );
842 }
843
844 for (cursor_id, position, anchor) in new_cursors {
846 if let Some(cursor) = cursors.get_mut(*cursor_id) {
847 cursor.position = *position;
848 cursor.anchor = *anchor;
849 }
850 }
851
852 self.highlighter.invalidate_all();
854
855 let primary_pos = cursors.primary().position;
857 self.primary_cursor_line_number = match self.buffer.offset_to_position(primary_pos)
858 {
859 Some(pos) => crate::model::buffer::LineNumber::Absolute(pos.line),
860 None => crate::model::buffer::LineNumber::Absolute(0),
861 };
862 }
863 }
864 }
865
866 pub fn capture_displaced_markers(&self, range: &Range<usize>) -> Vec<(u64, usize)> {
869 let mut displaced = Vec::new();
870 if range.is_empty() {
871 return displaced;
872 }
873 for (marker_id, start, _end) in self.marker_list.query_range(range.start, range.end) {
874 if start > range.start && start < range.end {
875 displaced.push(
876 DisplacedMarker::Main {
877 id: marker_id.0,
878 position: start,
879 }
880 .encode(),
881 );
882 }
883 }
884 for (marker_id, start, _end) in self.margins.query_indicator_range(range.start, range.end) {
885 if start > range.start && start < range.end {
886 displaced.push(
887 DisplacedMarker::Margin {
888 id: marker_id.0,
889 position: start,
890 }
891 .encode(),
892 );
893 }
894 }
895 displaced
896 }
897
898 pub fn capture_displaced_markers_bulk(
900 &self,
901 edits: &[(usize, usize, String)],
902 ) -> Vec<(u64, usize)> {
903 let mut displaced = Vec::new();
904 for (pos, del_len, _text) in edits {
905 if *del_len > 0 {
906 displaced.extend(self.capture_displaced_markers(&(*pos..*pos + *del_len)));
907 }
908 }
909 displaced
910 }
911
912 pub fn restore_displaced_markers(&mut self, displaced: &[(u64, usize)]) {
914 for &(tagged_id, original_pos) in displaced {
915 let dm = DisplacedMarker::decode(tagged_id, original_pos);
916 match dm {
917 DisplacedMarker::Main { id, position } => {
918 self.marker_list.set_position(MarkerId(id), position);
919 }
920 DisplacedMarker::Margin { id, position } => {
921 self.margins.set_indicator_position(MarkerId(id), position);
922 }
923 }
924 }
925 }
926
927 pub fn apply_many(&mut self, cursors: &mut Cursors, events: &[Event]) {
929 for event in events {
930 self.apply(cursors, event);
931 }
932 }
933
934 pub fn on_focus_lost(&mut self) {
938 if self.popups.dismiss_transient() {
939 tracing::debug!("Dismissed transient popup on buffer focus loss");
940 }
941 }
942}
943
944fn convert_event_face_to_overlay_face(event_face: &EventOverlayFace) -> OverlayFace {
946 match event_face {
947 EventOverlayFace::Underline { color, style } => {
948 let underline_style = match style {
949 crate::model::event::UnderlineStyle::Straight => UnderlineStyle::Straight,
950 crate::model::event::UnderlineStyle::Wavy => UnderlineStyle::Wavy,
951 crate::model::event::UnderlineStyle::Dotted => UnderlineStyle::Dotted,
952 crate::model::event::UnderlineStyle::Dashed => UnderlineStyle::Dashed,
953 };
954 OverlayFace::Underline {
955 color: Color::Rgb(color.0, color.1, color.2),
956 style: underline_style,
957 }
958 }
959 EventOverlayFace::Background { color } => OverlayFace::Background {
960 color: Color::Rgb(color.0, color.1, color.2),
961 },
962 EventOverlayFace::Foreground { color } => OverlayFace::Foreground {
963 color: Color::Rgb(color.0, color.1, color.2),
964 },
965 EventOverlayFace::Style { options } => {
966 use crate::view::theme::named_color_from_str;
967 use ratatui::style::Modifier;
968
969 let mut style = Style::default();
971
972 if let Some(ref fg) = options.fg {
974 if let Some((r, g, b)) = fg.as_rgb() {
975 style = style.fg(Color::Rgb(r, g, b));
976 } else if let Some(key) = fg.as_theme_key() {
977 if let Some(color) = named_color_from_str(key) {
978 style = style.fg(color);
979 }
980 }
981 }
982
983 if let Some(ref bg) = options.bg {
985 if let Some((r, g, b)) = bg.as_rgb() {
986 style = style.bg(Color::Rgb(r, g, b));
987 } else if let Some(key) = bg.as_theme_key() {
988 if let Some(color) = named_color_from_str(key) {
989 style = style.bg(color);
990 }
991 }
992 }
993
994 let mut modifiers = Modifier::empty();
996 if options.bold {
997 modifiers |= Modifier::BOLD;
998 }
999 if options.italic {
1000 modifiers |= Modifier::ITALIC;
1001 }
1002 if options.underline {
1003 modifiers |= Modifier::UNDERLINED;
1004 }
1005 if options.strikethrough {
1006 modifiers |= Modifier::CROSSED_OUT;
1007 }
1008 if !modifiers.is_empty() {
1009 style = style.add_modifier(modifiers);
1010 }
1011
1012 let fg_theme = options
1014 .fg
1015 .as_ref()
1016 .and_then(|c| c.as_theme_key())
1017 .filter(|key| named_color_from_str(key).is_none())
1018 .map(String::from);
1019 let bg_theme = options
1020 .bg
1021 .as_ref()
1022 .and_then(|c| c.as_theme_key())
1023 .filter(|key| named_color_from_str(key).is_none())
1024 .map(String::from);
1025
1026 if fg_theme.is_some() || bg_theme.is_some() {
1028 OverlayFace::ThemedStyle {
1029 fallback_style: style,
1030 fg_theme,
1031 bg_theme,
1032 }
1033 } else {
1034 OverlayFace::Style { style }
1035 }
1036 }
1037 }
1038}
1039
1040pub(crate) fn convert_popup_data_to_popup(data: &PopupData) -> Popup {
1042 let content = match &data.content {
1043 crate::model::event::PopupContentData::Text(lines) => PopupContent::Text(lines.clone()),
1044 crate::model::event::PopupContentData::List { items, selected } => PopupContent::List {
1045 items: items
1046 .iter()
1047 .map(|item| PopupListItem {
1048 text: item.text.clone(),
1049 detail: item.detail.clone(),
1050 icon: item.icon.clone(),
1051 data: item.data.clone(),
1052 disabled: false,
1053 })
1054 .collect(),
1055 selected: *selected,
1056 },
1057 };
1058
1059 let position = match data.position {
1060 PopupPositionData::AtCursor => PopupPosition::AtCursor,
1061 PopupPositionData::BelowCursor => PopupPosition::BelowCursor,
1062 PopupPositionData::AboveCursor => PopupPosition::AboveCursor,
1063 PopupPositionData::Fixed { x, y } => PopupPosition::Fixed { x, y },
1064 PopupPositionData::Centered => PopupPosition::Centered,
1065 PopupPositionData::BottomRight => PopupPosition::BottomRight,
1066 PopupPositionData::AboveStatusBarAt { x } => PopupPosition::AboveStatusBarAt { x },
1067 };
1068
1069 let kind = match data.kind {
1071 crate::model::event::PopupKindHint::Completion => PopupKind::Completion,
1072 crate::model::event::PopupKindHint::List => PopupKind::List,
1073 crate::model::event::PopupKindHint::Text => PopupKind::Text,
1074 };
1075
1076 Popup {
1077 kind,
1078 title: data.title.clone(),
1079 description: data.description.clone(),
1080 transient: data.transient,
1081 content,
1082 position,
1083 width: data.width,
1084 max_height: data.max_height,
1085 bordered: data.bordered,
1086 border_style: Style::default().fg(Color::Gray),
1087 background_style: Style::default().bg(Color::Rgb(30, 30, 30)),
1088 scroll_offset: 0,
1089 text_selection: None,
1090 accept_key_hint: None,
1091 }
1092}
1093
1094fn convert_margin_position(position: &MarginPositionData) -> MarginPosition {
1096 match position {
1097 MarginPositionData::Left => MarginPosition::Left,
1098 MarginPositionData::Right => MarginPosition::Right,
1099 }
1100}
1101
1102fn convert_margin_content(content: &MarginContentData) -> MarginContent {
1104 match content {
1105 MarginContentData::Text(text) => MarginContent::Text(text.clone()),
1106 MarginContentData::Symbol { text, color } => {
1107 if let Some((r, g, b)) = color {
1108 MarginContent::colored_symbol(text.clone(), Color::Rgb(*r, *g, *b))
1109 } else {
1110 MarginContent::symbol(text.clone(), Style::default())
1111 }
1112 }
1113 MarginContentData::Empty => MarginContent::Empty,
1114 }
1115}
1116
1117impl EditorState {
1118 pub fn prepare_for_render(&mut self, top_byte: usize, height: u16) -> Result<()> {
1125 self.buffer.prepare_viewport(top_byte, height as usize)?;
1126 Ok(())
1127 }
1128
1129 pub fn collect_soft_break_positions(&self) -> Vec<usize> {
1136 if self.soft_breaks.is_empty() {
1137 return Vec::new();
1138 }
1139 self.soft_breaks
1141 .query_viewport(0, self.buffer.len() + 1, &self.marker_list)
1142 .into_iter()
1143 .map(|(pos, _indent)| pos)
1144 .collect()
1145 }
1146
1147 pub fn get_text_range(&mut self, start: usize, end: usize) -> String {
1167 match self
1169 .buffer
1170 .get_text_range_mut(start, end.saturating_sub(start))
1171 {
1172 Ok(bytes) => String::from_utf8_lossy(&bytes).into_owned(),
1173 Err(e) => {
1174 tracing::warn!("Failed to get text range {}..{}: {}", start, end, e);
1175 String::new()
1176 }
1177 }
1178 }
1179
1180 pub fn get_line_at_offset(&mut self, offset: usize) -> Option<(usize, String)> {
1188 use crate::model::document_model::DocumentModel;
1189
1190 let mut line_start = offset;
1193 while line_start > 0 {
1194 if let Ok(text) = self.buffer.get_text_range_mut(line_start - 1, 1) {
1195 if text.first() == Some(&b'\n') {
1196 break;
1197 }
1198 line_start -= 1;
1199 } else {
1200 break;
1201 }
1202 }
1203
1204 let viewport = self
1206 .get_viewport_content(
1207 crate::model::document_model::DocumentPosition::byte(line_start),
1208 1,
1209 )
1210 .ok()?;
1211
1212 viewport
1213 .lines
1214 .first()
1215 .map(|line| (line.byte_offset, line.content.clone()))
1216 }
1217
1218 pub fn get_text_to_end_of_line(&mut self, cursor_pos: usize) -> Result<String> {
1223 use crate::model::document_model::DocumentModel;
1224
1225 let viewport = self.get_viewport_content(
1227 crate::model::document_model::DocumentPosition::byte(cursor_pos),
1228 1,
1229 )?;
1230
1231 if let Some(line) = viewport.lines.first() {
1232 let line_start = line.byte_offset;
1233 let line_end = line_start + line.content.len();
1234
1235 if cursor_pos >= line_start && cursor_pos <= line_end {
1236 let offset_in_line = cursor_pos - line_start;
1237 Ok(line.content.get(offset_in_line..).unwrap_or("").to_string())
1239 } else {
1240 Ok(String::new())
1241 }
1242 } else {
1243 Ok(String::new())
1244 }
1245 }
1246
1247 pub fn set_semantic_tokens(&mut self, store: SemanticTokenStore) {
1249 self.semantic_tokens = Some(store);
1250 }
1251
1252 pub fn clear_semantic_tokens(&mut self) {
1254 self.semantic_tokens = None;
1255 }
1256
1257 pub fn semantic_tokens_result_id(&self) -> Option<&str> {
1259 self.semantic_tokens
1260 .as_ref()
1261 .and_then(|store| store.result_id.as_deref())
1262 }
1263}
1264
1265impl DocumentModel for EditorState {
1270 fn capabilities(&self) -> DocumentCapabilities {
1271 let line_count = self.buffer.line_count();
1272 DocumentCapabilities {
1273 has_line_index: line_count.is_some(),
1274 uses_lazy_loading: false, byte_length: self.buffer.len(),
1276 approximate_line_count: line_count.unwrap_or_else(|| {
1277 self.buffer.len() / 80
1279 }),
1280 }
1281 }
1282
1283 fn get_viewport_content(
1284 &mut self,
1285 start_pos: DocumentPosition,
1286 max_lines: usize,
1287 ) -> Result<ViewportContent> {
1288 let start_offset = self.position_to_offset(start_pos)?;
1290
1291 let line_iter = self.buffer.iter_lines_from(start_offset, max_lines)?;
1294 let has_more = line_iter.has_more;
1295
1296 let lines = line_iter
1297 .map(|line_data| ViewportLine {
1298 byte_offset: line_data.byte_offset,
1299 content: line_data.content,
1300 has_newline: line_data.has_newline,
1301 approximate_line_number: line_data.line_number,
1302 })
1303 .collect();
1304
1305 Ok(ViewportContent {
1306 start_position: DocumentPosition::ByteOffset(start_offset),
1307 lines,
1308 has_more,
1309 })
1310 }
1311
1312 fn position_to_offset(&self, pos: DocumentPosition) -> Result<usize> {
1313 match pos {
1314 DocumentPosition::ByteOffset(offset) => Ok(offset),
1315 DocumentPosition::LineColumn { line, column } => {
1316 if !self.has_line_index() {
1317 anyhow::bail!("Line indexing not available for this document");
1318 }
1319 let position = crate::model::piece_tree::Position { line, column };
1321 Ok(self.buffer.position_to_offset(position))
1322 }
1323 }
1324 }
1325
1326 fn offset_to_position(&self, offset: usize) -> DocumentPosition {
1327 if self.has_line_index() {
1328 if let Some(pos) = self.buffer.offset_to_position(offset) {
1329 DocumentPosition::LineColumn {
1330 line: pos.line,
1331 column: pos.column,
1332 }
1333 } else {
1334 DocumentPosition::ByteOffset(offset)
1336 }
1337 } else {
1338 DocumentPosition::ByteOffset(offset)
1339 }
1340 }
1341
1342 fn get_range(&mut self, start: DocumentPosition, end: DocumentPosition) -> Result<String> {
1343 let start_offset = self.position_to_offset(start)?;
1344 let end_offset = self.position_to_offset(end)?;
1345
1346 if start_offset > end_offset {
1347 anyhow::bail!(
1348 "Invalid range: start offset {} > end offset {}",
1349 start_offset,
1350 end_offset
1351 );
1352 }
1353
1354 let bytes = self
1355 .buffer
1356 .get_text_range_mut(start_offset, end_offset - start_offset)?;
1357
1358 Ok(String::from_utf8_lossy(&bytes).into_owned())
1359 }
1360
1361 fn get_line_content(&mut self, line_number: usize) -> Option<String> {
1362 if !self.has_line_index() {
1363 return None;
1364 }
1365
1366 let line_start_offset = self.buffer.line_start_offset(line_number)?;
1368
1369 let mut iter = self.buffer.line_iterator(line_start_offset, 80);
1371 if let Some((_start, content)) = iter.next_line() {
1372 let has_newline = content.ends_with('\n');
1373 let line_content = if has_newline {
1374 content[..content.len() - 1].to_string()
1375 } else {
1376 content
1377 };
1378 Some(line_content)
1379 } else {
1380 None
1381 }
1382 }
1383
1384 fn get_chunk_at_offset(&mut self, offset: usize, size: usize) -> Result<(usize, String)> {
1385 let bytes = self.buffer.get_text_range_mut(offset, size)?;
1386
1387 Ok((offset, String::from_utf8_lossy(&bytes).into_owned()))
1388 }
1389
1390 fn insert(&mut self, pos: DocumentPosition, text: &str) -> Result<usize> {
1391 let offset = self.position_to_offset(pos)?;
1392 self.buffer.insert_bytes(offset, text.as_bytes().to_vec());
1393 Ok(text.len())
1394 }
1395
1396 fn delete(&mut self, start: DocumentPosition, end: DocumentPosition) -> Result<()> {
1397 let start_offset = self.position_to_offset(start)?;
1398 let end_offset = self.position_to_offset(end)?;
1399
1400 if start_offset > end_offset {
1401 anyhow::bail!(
1402 "Invalid range: start offset {} > end offset {}",
1403 start_offset,
1404 end_offset
1405 );
1406 }
1407
1408 self.buffer.delete(start_offset..end_offset);
1409 Ok(())
1410 }
1411
1412 fn replace(
1413 &mut self,
1414 start: DocumentPosition,
1415 end: DocumentPosition,
1416 text: &str,
1417 ) -> Result<()> {
1418 self.delete(start, end)?;
1420 self.insert(start, text)?;
1421 Ok(())
1422 }
1423
1424 fn find_matches(
1425 &mut self,
1426 pattern: &str,
1427 search_range: Option<(DocumentPosition, DocumentPosition)>,
1428 ) -> Result<Vec<usize>> {
1429 let (start_offset, end_offset) = if let Some((start, end)) = search_range {
1430 (
1431 self.position_to_offset(start)?,
1432 self.position_to_offset(end)?,
1433 )
1434 } else {
1435 (0, self.buffer.len())
1436 };
1437
1438 let bytes = self
1440 .buffer
1441 .get_text_range_mut(start_offset, end_offset - start_offset)?;
1442 let text = String::from_utf8_lossy(&bytes);
1443
1444 let mut matches = Vec::new();
1446 let mut search_offset = 0;
1447 while let Some(pos) = text[search_offset..].find(pattern) {
1448 matches.push(start_offset + search_offset + pos);
1449 search_offset += pos + pattern.len();
1450 }
1451
1452 Ok(matches)
1453 }
1454}
1455
1456#[derive(Clone, Debug)]
1458pub struct SemanticTokenStore {
1459 pub version: u64,
1461 pub result_id: Option<String>,
1463 pub data: Vec<u32>,
1465 pub tokens: Vec<SemanticTokenSpan>,
1467}
1468
1469#[derive(Clone, Debug)]
1471pub struct SemanticTokenSpan {
1472 pub range: Range<usize>,
1473 pub token_type: String,
1474 pub modifiers: Vec<String>,
1475}
1476
1477#[cfg(test)]
1478mod tests {
1479 use crate::model::filesystem::StdFileSystem;
1480 use std::sync::Arc;
1481
1482 fn test_fs() -> Arc<dyn crate::model::filesystem::FileSystem + Send + Sync> {
1483 Arc::new(StdFileSystem)
1484 }
1485 use super::*;
1486 use crate::model::event::CursorId;
1487
1488 #[test]
1489 fn test_state_new() {
1490 let state = EditorState::new(
1491 80,
1492 24,
1493 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1494 test_fs(),
1495 );
1496 assert!(state.buffer.is_empty());
1497 }
1498
1499 #[test]
1500 fn test_apply_insert() {
1501 let mut state = EditorState::new(
1502 80,
1503 24,
1504 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1505 test_fs(),
1506 );
1507 let mut cursors = Cursors::new();
1508 let cursor_id = cursors.primary_id();
1509
1510 state.apply(
1511 &mut cursors,
1512 &Event::Insert {
1513 position: 0,
1514 text: "hello".to_string(),
1515 cursor_id,
1516 },
1517 );
1518
1519 assert_eq!(state.buffer.to_string().unwrap(), "hello");
1520 assert_eq!(cursors.primary().position, 5);
1521 assert!(state.buffer.is_modified());
1522 }
1523
1524 #[test]
1525 fn test_apply_delete() {
1526 let mut state = EditorState::new(
1527 80,
1528 24,
1529 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1530 test_fs(),
1531 );
1532 let mut cursors = Cursors::new();
1533 let cursor_id = cursors.primary_id();
1534
1535 state.apply(
1537 &mut cursors,
1538 &Event::Insert {
1539 position: 0,
1540 text: "hello world".to_string(),
1541 cursor_id,
1542 },
1543 );
1544
1545 state.apply(
1546 &mut cursors,
1547 &Event::Delete {
1548 range: 5..11,
1549 deleted_text: " world".to_string(),
1550 cursor_id,
1551 },
1552 );
1553
1554 assert_eq!(state.buffer.to_string().unwrap(), "hello");
1555 assert_eq!(cursors.primary().position, 5);
1556 }
1557
1558 #[test]
1559 fn test_apply_move_cursor() {
1560 let mut state = EditorState::new(
1561 80,
1562 24,
1563 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1564 test_fs(),
1565 );
1566 let mut cursors = Cursors::new();
1567 let cursor_id = cursors.primary_id();
1568
1569 state.apply(
1570 &mut cursors,
1571 &Event::Insert {
1572 position: 0,
1573 text: "hello".to_string(),
1574 cursor_id,
1575 },
1576 );
1577
1578 state.apply(
1579 &mut cursors,
1580 &Event::MoveCursor {
1581 cursor_id,
1582 old_position: 5,
1583 new_position: 2,
1584 old_anchor: None,
1585 new_anchor: None,
1586 old_sticky_column: 0,
1587 new_sticky_column: 0,
1588 },
1589 );
1590
1591 assert_eq!(cursors.primary().position, 2);
1592 }
1593
1594 #[test]
1595 fn test_apply_add_cursor() {
1596 let mut state = EditorState::new(
1597 80,
1598 24,
1599 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1600 test_fs(),
1601 );
1602 let mut cursors = Cursors::new();
1603 let cursor_id = CursorId(1);
1604
1605 state.apply(
1606 &mut cursors,
1607 &Event::AddCursor {
1608 cursor_id,
1609 position: 5,
1610 anchor: None,
1611 },
1612 );
1613
1614 assert_eq!(cursors.count(), 2);
1615 }
1616
1617 #[test]
1618 fn test_apply_many() {
1619 let mut state = EditorState::new(
1620 80,
1621 24,
1622 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1623 test_fs(),
1624 );
1625 let mut cursors = Cursors::new();
1626 let cursor_id = cursors.primary_id();
1627
1628 let events = vec![
1629 Event::Insert {
1630 position: 0,
1631 text: "hello ".to_string(),
1632 cursor_id,
1633 },
1634 Event::Insert {
1635 position: 6,
1636 text: "world".to_string(),
1637 cursor_id,
1638 },
1639 ];
1640
1641 state.apply_many(&mut cursors, &events);
1642
1643 assert_eq!(state.buffer.to_string().unwrap(), "hello world");
1644 }
1645
1646 #[test]
1647 fn test_cursor_adjustment_after_insert() {
1648 let mut state = EditorState::new(
1649 80,
1650 24,
1651 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1652 test_fs(),
1653 );
1654 let mut cursors = Cursors::new();
1655 let cursor_id = cursors.primary_id();
1656
1657 state.apply(
1659 &mut cursors,
1660 &Event::AddCursor {
1661 cursor_id: CursorId(1),
1662 position: 5,
1663 anchor: None,
1664 },
1665 );
1666
1667 state.apply(
1669 &mut cursors,
1670 &Event::Insert {
1671 position: 0,
1672 text: "abc".to_string(),
1673 cursor_id,
1674 },
1675 );
1676
1677 if let Some(cursor) = cursors.get(CursorId(1)) {
1679 assert_eq!(cursor.position, 8);
1680 }
1681 }
1682
1683 mod document_model_tests {
1685 use super::*;
1686 use crate::model::document_model::{DocumentModel, DocumentPosition};
1687
1688 #[test]
1689 fn test_capabilities_small_file() {
1690 let mut state = EditorState::new(
1691 80,
1692 24,
1693 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1694 test_fs(),
1695 );
1696 state.buffer = Buffer::from_str_test("line1\nline2\nline3");
1697
1698 let caps = state.capabilities();
1699 assert!(caps.has_line_index, "Small file should have line index");
1700 assert_eq!(caps.byte_length, "line1\nline2\nline3".len());
1701 assert_eq!(caps.approximate_line_count, 3, "Should have 3 lines");
1702 }
1703
1704 #[test]
1705 fn test_position_conversions() {
1706 let mut state = EditorState::new(
1707 80,
1708 24,
1709 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1710 test_fs(),
1711 );
1712 state.buffer = Buffer::from_str_test("hello\nworld\ntest");
1713
1714 let pos1 = DocumentPosition::ByteOffset(6);
1716 let offset1 = state.position_to_offset(pos1).unwrap();
1717 assert_eq!(offset1, 6);
1718
1719 let pos2 = DocumentPosition::LineColumn { line: 1, column: 0 };
1721 let offset2 = state.position_to_offset(pos2).unwrap();
1722 assert_eq!(offset2, 6, "Line 1, column 0 should be at byte 6");
1723
1724 let converted = state.offset_to_position(6);
1726 match converted {
1727 DocumentPosition::LineColumn { line, column } => {
1728 assert_eq!(line, 1);
1729 assert_eq!(column, 0);
1730 }
1731 _ => panic!("Expected LineColumn for small file"),
1732 }
1733 }
1734
1735 #[test]
1736 fn test_get_viewport_content() {
1737 let mut state = EditorState::new(
1738 80,
1739 24,
1740 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1741 test_fs(),
1742 );
1743 state.buffer = Buffer::from_str_test("line1\nline2\nline3\nline4\nline5");
1744
1745 let content = state
1746 .get_viewport_content(DocumentPosition::ByteOffset(0), 3)
1747 .unwrap();
1748
1749 assert_eq!(content.lines.len(), 3);
1750 assert_eq!(content.lines[0].content, "line1");
1751 assert_eq!(content.lines[1].content, "line2");
1752 assert_eq!(content.lines[2].content, "line3");
1753 assert!(content.has_more);
1754 }
1755
1756 #[test]
1757 fn test_get_range() {
1758 let mut state = EditorState::new(
1759 80,
1760 24,
1761 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1762 test_fs(),
1763 );
1764 state.buffer = Buffer::from_str_test("hello world");
1765
1766 let text = state
1767 .get_range(
1768 DocumentPosition::ByteOffset(0),
1769 DocumentPosition::ByteOffset(5),
1770 )
1771 .unwrap();
1772 assert_eq!(text, "hello");
1773
1774 let text2 = state
1775 .get_range(
1776 DocumentPosition::ByteOffset(6),
1777 DocumentPosition::ByteOffset(11),
1778 )
1779 .unwrap();
1780 assert_eq!(text2, "world");
1781 }
1782
1783 #[test]
1784 fn test_get_line_content() {
1785 let mut state = EditorState::new(
1786 80,
1787 24,
1788 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1789 test_fs(),
1790 );
1791 state.buffer = Buffer::from_str_test("line1\nline2\nline3");
1792
1793 let line0 = state.get_line_content(0).unwrap();
1794 assert_eq!(line0, "line1");
1795
1796 let line1 = state.get_line_content(1).unwrap();
1797 assert_eq!(line1, "line2");
1798
1799 let line2 = state.get_line_content(2).unwrap();
1800 assert_eq!(line2, "line3");
1801 }
1802
1803 #[test]
1804 fn test_insert_delete() {
1805 let mut state = EditorState::new(
1806 80,
1807 24,
1808 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1809 test_fs(),
1810 );
1811 state.buffer = Buffer::from_str_test("hello world");
1812
1813 let bytes_inserted = state
1815 .insert(DocumentPosition::ByteOffset(6), "beautiful ")
1816 .unwrap();
1817 assert_eq!(bytes_inserted, 10);
1818 assert_eq!(state.buffer.to_string().unwrap(), "hello beautiful world");
1819
1820 state
1822 .delete(
1823 DocumentPosition::ByteOffset(6),
1824 DocumentPosition::ByteOffset(16),
1825 )
1826 .unwrap();
1827 assert_eq!(state.buffer.to_string().unwrap(), "hello world");
1828 }
1829
1830 #[test]
1831 fn test_replace() {
1832 let mut state = EditorState::new(
1833 80,
1834 24,
1835 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1836 test_fs(),
1837 );
1838 state.buffer = Buffer::from_str_test("hello world");
1839
1840 state
1841 .replace(
1842 DocumentPosition::ByteOffset(0),
1843 DocumentPosition::ByteOffset(5),
1844 "hi",
1845 )
1846 .unwrap();
1847 assert_eq!(state.buffer.to_string().unwrap(), "hi world");
1848 }
1849
1850 #[test]
1851 fn test_find_matches() {
1852 let mut state = EditorState::new(
1853 80,
1854 24,
1855 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1856 test_fs(),
1857 );
1858 state.buffer = Buffer::from_str_test("hello world hello");
1859
1860 let matches = state.find_matches("hello", None).unwrap();
1861 assert_eq!(matches.len(), 2);
1862 assert_eq!(matches[0], 0);
1863 assert_eq!(matches[1], 12);
1864 }
1865
1866 #[test]
1867 fn test_prepare_for_render() {
1868 let mut state = EditorState::new(
1869 80,
1870 24,
1871 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1872 test_fs(),
1873 );
1874 state.buffer = Buffer::from_str_test("line1\nline2\nline3\nline4\nline5");
1875
1876 state.prepare_for_render(0, 24).unwrap();
1878 }
1879
1880 #[test]
1881 fn test_helper_get_text_range() {
1882 let mut state = EditorState::new(
1883 80,
1884 24,
1885 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1886 test_fs(),
1887 );
1888 state.buffer = Buffer::from_str_test("hello world");
1889
1890 let text = state.get_text_range(0, 5);
1892 assert_eq!(text, "hello");
1893
1894 let text2 = state.get_text_range(6, 11);
1896 assert_eq!(text2, "world");
1897 }
1898
1899 #[test]
1900 fn test_helper_get_line_at_offset() {
1901 let mut state = EditorState::new(
1902 80,
1903 24,
1904 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1905 test_fs(),
1906 );
1907 state.buffer = Buffer::from_str_test("line1\nline2\nline3");
1908
1909 let (offset, content) = state.get_line_at_offset(0).unwrap();
1911 assert_eq!(offset, 0);
1912 assert_eq!(content, "line1");
1913
1914 let (offset2, content2) = state.get_line_at_offset(8).unwrap();
1916 assert_eq!(offset2, 6); assert_eq!(content2, "line2");
1918
1919 let (offset3, content3) = state.get_line_at_offset(12).unwrap();
1921 assert_eq!(offset3, 12);
1922 assert_eq!(content3, "line3");
1923 }
1924
1925 #[test]
1926 fn test_helper_get_text_to_end_of_line() {
1927 let mut state = EditorState::new(
1928 80,
1929 24,
1930 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1931 test_fs(),
1932 );
1933 state.buffer = Buffer::from_str_test("hello world\nline2");
1934
1935 let text = state.get_text_to_end_of_line(0).unwrap();
1937 assert_eq!(text, "hello world");
1938
1939 let text2 = state.get_text_to_end_of_line(6).unwrap();
1941 assert_eq!(text2, "world");
1942
1943 let text3 = state.get_text_to_end_of_line(11).unwrap();
1945 assert_eq!(text3, "");
1946
1947 let text4 = state.get_text_to_end_of_line(12).unwrap();
1949 assert_eq!(text4, "line2");
1950 }
1951 }
1952
1953 mod virtual_text_integration_tests {
1955 use super::*;
1956 use crate::view::virtual_text::VirtualTextPosition;
1957 use ratatui::style::Style;
1958
1959 #[test]
1960 fn test_virtual_text_add_and_query() {
1961 let mut state = EditorState::new(
1962 80,
1963 24,
1964 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1965 test_fs(),
1966 );
1967 state.buffer = Buffer::from_str_test("hello world");
1968
1969 if !state.buffer.is_empty() {
1971 state.marker_list.adjust_for_insert(0, state.buffer.len());
1972 }
1973
1974 let vtext_id = state.virtual_texts.add(
1976 &mut state.marker_list,
1977 5,
1978 ": string".to_string(),
1979 Style::default(),
1980 VirtualTextPosition::AfterChar,
1981 0,
1982 );
1983
1984 let results = state.virtual_texts.query_range(&state.marker_list, 0, 11);
1986 assert_eq!(results.len(), 1);
1987 assert_eq!(results[0].0, 5); assert_eq!(results[0].1.text, ": string");
1989
1990 let lookup = state.virtual_texts.build_lookup(&state.marker_list, 0, 11);
1992 assert!(lookup.contains_key(&5));
1993 assert_eq!(lookup[&5].len(), 1);
1994 assert_eq!(lookup[&5][0].text, ": string");
1995
1996 state.virtual_texts.remove(&mut state.marker_list, vtext_id);
1998 assert!(state.virtual_texts.is_empty());
1999 }
2000
2001 #[test]
2002 fn test_virtual_text_position_tracking_on_insert() {
2003 let mut state = EditorState::new(
2004 80,
2005 24,
2006 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
2007 test_fs(),
2008 );
2009 state.buffer = Buffer::from_str_test("hello world");
2010
2011 if !state.buffer.is_empty() {
2013 state.marker_list.adjust_for_insert(0, state.buffer.len());
2014 }
2015
2016 let _vtext_id = state.virtual_texts.add(
2018 &mut state.marker_list,
2019 6,
2020 "/*param*/".to_string(),
2021 Style::default(),
2022 VirtualTextPosition::BeforeChar,
2023 0,
2024 );
2025
2026 let mut cursors = Cursors::new();
2028 let cursor_id = cursors.primary_id();
2029 state.apply(
2030 &mut cursors,
2031 &Event::Insert {
2032 position: 6,
2033 text: "beautiful ".to_string(),
2034 cursor_id,
2035 },
2036 );
2037
2038 let results = state.virtual_texts.query_range(&state.marker_list, 0, 30);
2040 assert_eq!(results.len(), 1);
2041 assert_eq!(results[0].0, 16); assert_eq!(results[0].1.text, "/*param*/");
2043 }
2044
2045 #[test]
2046 fn test_virtual_text_position_tracking_on_delete() {
2047 let mut state = EditorState::new(
2048 80,
2049 24,
2050 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
2051 test_fs(),
2052 );
2053 state.buffer = Buffer::from_str_test("hello beautiful world");
2054
2055 if !state.buffer.is_empty() {
2057 state.marker_list.adjust_for_insert(0, state.buffer.len());
2058 }
2059
2060 let _vtext_id = state.virtual_texts.add(
2062 &mut state.marker_list,
2063 16,
2064 ": string".to_string(),
2065 Style::default(),
2066 VirtualTextPosition::AfterChar,
2067 0,
2068 );
2069
2070 let mut cursors = Cursors::new();
2072 let cursor_id = cursors.primary_id();
2073 state.apply(
2074 &mut cursors,
2075 &Event::Delete {
2076 range: 6..16,
2077 deleted_text: "beautiful ".to_string(),
2078 cursor_id,
2079 },
2080 );
2081
2082 let results = state.virtual_texts.query_range(&state.marker_list, 0, 20);
2084 assert_eq!(results.len(), 1);
2085 assert_eq!(results[0].0, 6); assert_eq!(results[0].1.text, ": string");
2087 }
2088
2089 #[test]
2090 fn test_multiple_virtual_texts_with_priorities() {
2091 let mut state = EditorState::new(
2092 80,
2093 24,
2094 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
2095 test_fs(),
2096 );
2097 state.buffer = Buffer::from_str_test("let x = 5");
2098
2099 if !state.buffer.is_empty() {
2101 state.marker_list.adjust_for_insert(0, state.buffer.len());
2102 }
2103
2104 state.virtual_texts.add(
2106 &mut state.marker_list,
2107 5,
2108 ": i32".to_string(),
2109 Style::default(),
2110 VirtualTextPosition::AfterChar,
2111 0, );
2113
2114 state.virtual_texts.add(
2116 &mut state.marker_list,
2117 5,
2118 " /* inferred */".to_string(),
2119 Style::default(),
2120 VirtualTextPosition::AfterChar,
2121 10, );
2123
2124 let lookup = state.virtual_texts.build_lookup(&state.marker_list, 0, 10);
2126 assert!(lookup.contains_key(&5));
2127 let vtexts = &lookup[&5];
2128 assert_eq!(vtexts.len(), 2);
2129 assert_eq!(vtexts[0].text, ": i32");
2131 assert_eq!(vtexts[1].text, " /* inferred */");
2132 }
2133
2134 #[test]
2135 fn test_virtual_text_clear() {
2136 let mut state = EditorState::new(
2137 80,
2138 24,
2139 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
2140 test_fs(),
2141 );
2142 state.buffer = Buffer::from_str_test("test");
2143
2144 if !state.buffer.is_empty() {
2146 state.marker_list.adjust_for_insert(0, state.buffer.len());
2147 }
2148
2149 state.virtual_texts.add(
2151 &mut state.marker_list,
2152 0,
2153 "hint1".to_string(),
2154 Style::default(),
2155 VirtualTextPosition::BeforeChar,
2156 0,
2157 );
2158 state.virtual_texts.add(
2159 &mut state.marker_list,
2160 2,
2161 "hint2".to_string(),
2162 Style::default(),
2163 VirtualTextPosition::AfterChar,
2164 0,
2165 );
2166
2167 assert_eq!(state.virtual_texts.len(), 2);
2168
2169 state.virtual_texts.clear(&mut state.marker_list);
2171 assert!(state.virtual_texts.is_empty());
2172
2173 let results = state.virtual_texts.query_range(&state.marker_list, 0, 10);
2175 assert!(results.is_empty());
2176 }
2177 }
2178}