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