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 cursor_visibility_locked: bool,
189
190 pub editing_disabled: bool,
194
195 pub scrollable: bool,
199
200 pub buffer_settings: BufferSettings,
203
204 pub reference_highlighter: ReferenceHighlighter,
206
207 pub is_composite_buffer: bool,
209
210 pub debug_highlight_mode: bool,
212
213 pub reference_highlight_overlay: ReferenceHighlightOverlay,
215
216 pub bracket_highlight_overlay: BracketHighlightOverlay,
218
219 pub semantic_tokens: Option<SemanticTokenStore>,
221
222 pub folding_ranges: LspFoldRanges,
226
227 pub language: String,
230
231 pub display_name: String,
236
237 pub line_wrap_cache: crate::view::line_wrap_cache::LineWrapCache,
244
245 pub visual_row_index: crate::view::visual_row_index::VisualRowIndex,
254}
255
256impl EditorState {
257 pub fn apply_language(&mut self, detected: DetectedLanguage) {
264 self.language = detected.name;
265 self.display_name = detected.display_name;
266 self.highlighter = detected.highlighter;
267 if let Some(lang) = &detected.ts_language {
268 self.reference_highlighter.set_language(lang);
269 }
270 }
271
272 fn new_from_buffer(buffer: Buffer) -> Self {
275 let mut marker_list = MarkerList::new();
276 if !buffer.is_empty() {
277 marker_list.adjust_for_insert(0, buffer.len());
278 }
279
280 Self {
281 buffer,
282 highlighter: HighlightEngine::None,
283 indent_calculator: RefCell::new(IndentCalculator::new()),
284 overlays: OverlayManager::new(),
285 marker_list,
286 virtual_texts: VirtualTextManager::new(),
287 conceals: ConcealManager::new(),
288 soft_breaks: SoftBreakManager::new(),
289 popups: PopupManager::new(),
290 margins: MarginManager::new(),
291 primary_cursor_line_number: LineNumber::Absolute(0),
292 mode: "insert".to_string(),
293 text_properties: TextPropertyManager::new(),
294 show_cursors: true,
295 cursor_visibility_locked: false,
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 line_wrap_cache: crate::view::line_wrap_cache::LineWrapCache::default(),
309 visual_row_index: crate::view::visual_row_index::VisualRowIndex::default(),
310 }
311 }
312
313 pub fn new(
314 _width: u16,
315 _height: u16,
316 large_file_threshold: usize,
317 fs: Arc<dyn FileSystem + Send + Sync>,
318 ) -> Self {
319 Self::new_from_buffer(Buffer::new(large_file_threshold, fs))
320 }
321
322 pub fn new_with_path(
325 large_file_threshold: usize,
326 fs: Arc<dyn FileSystem + Send + Sync>,
327 path: std::path::PathBuf,
328 ) -> Self {
329 Self::new_from_buffer(Buffer::new_with_path(large_file_threshold, fs, path))
330 }
331
332 pub fn set_language_from_name(&mut self, name: &str, registry: &GrammarRegistry) {
336 let detected = DetectedLanguage::from_virtual_name(name, registry);
337 tracing::debug!(
338 "Set highlighter for virtual buffer based on name: {} (backend: {}, language: {})",
339 name,
340 detected.highlighter.backend_name(),
341 detected.name
342 );
343 self.apply_language(detected);
344 }
345
346 pub fn from_file(
351 path: &std::path::Path,
352 _width: u16,
353 _height: u16,
354 large_file_threshold: usize,
355 registry: &GrammarRegistry,
356 fs: Arc<dyn FileSystem + Send + Sync>,
357 ) -> anyhow::Result<Self> {
358 let buffer = Buffer::load_from_file(path, large_file_threshold, fs)?;
359 let first_line = buffer.first_line_lossy();
360 let detected = registry
361 .find_by_path(path, first_line.as_deref())
362 .map(|entry| DetectedLanguage::from_entry(entry, registry))
363 .unwrap_or_else(DetectedLanguage::plain_text);
364 let mut state = Self::new_from_buffer(buffer);
365 state.apply_language(detected);
366 Ok(state)
367 }
368
369 pub fn from_file_with_languages(
377 path: &std::path::Path,
378 _width: u16,
379 _height: u16,
380 large_file_threshold: usize,
381 registry: &GrammarRegistry,
382 languages: &std::collections::HashMap<String, crate::config::LanguageConfig>,
383 fs: Arc<dyn FileSystem + Send + Sync>,
384 ) -> anyhow::Result<Self> {
385 let buffer = Buffer::load_from_file(path, large_file_threshold, fs)?;
386 let first_line = buffer.first_line_lossy();
387 let detected =
388 DetectedLanguage::from_path(path, first_line.as_deref(), registry, languages);
389 let mut state = Self::new_from_buffer(buffer);
390 state.apply_language(detected);
391 Ok(state)
392 }
393
394 pub fn from_file_with_languages_force_text(
402 path: &std::path::Path,
403 _width: u16,
404 _height: u16,
405 large_file_threshold: usize,
406 registry: &GrammarRegistry,
407 languages: &std::collections::HashMap<String, crate::config::LanguageConfig>,
408 fs: Arc<dyn FileSystem + Send + Sync>,
409 ) -> anyhow::Result<Self> {
410 let buffer = Buffer::load_from_file_force_text(path, large_file_threshold, fs)?;
411 let first_line = buffer.first_line_lossy();
412 let detected =
413 DetectedLanguage::from_path(path, first_line.as_deref(), registry, languages);
414 let mut state = Self::new_from_buffer(buffer);
415 state.apply_language(detected);
416 Ok(state)
417 }
418
419 pub fn from_buffer_with_language(buffer: Buffer, detected: DetectedLanguage) -> Self {
424 let mut state = Self::new_from_buffer(buffer);
425 state.apply_language(detected);
426 state
427 }
428
429 fn apply_insert(
431 &mut self,
432 cursors: &mut Cursors,
433 position: usize,
434 text: &str,
435 cursor_id: crate::model::event::CursorId,
436 ) {
437 let newlines_inserted = text.matches('\n').count();
438
439 self.marker_list.adjust_for_insert(position, text.len());
441 self.margins.adjust_for_insert(position, text.len());
442
443 self.buffer.insert(position, text);
445
446 self.highlighter.notify_insert(position, text.len());
449 self.highlighter
450 .invalidate_range(position..position + text.len());
451
452 cursors.adjust_for_edit(position, 0, text.len());
457
458 if let Some(cursor) = cursors.get_mut(cursor_id) {
460 cursor.position = position + text.len();
461 cursor.clear_selection();
462 }
463
464 if cursor_id == cursors.primary_id() {
466 self.primary_cursor_line_number = match self.primary_cursor_line_number {
467 LineNumber::Absolute(line) => LineNumber::Absolute(line + newlines_inserted),
468 LineNumber::Relative {
469 line,
470 from_cached_line,
471 } => LineNumber::Relative {
472 line: line + newlines_inserted,
473 from_cached_line,
474 },
475 };
476 }
477 }
478
479 fn apply_delete(
481 &mut self,
482 cursors: &mut Cursors,
483 range: &std::ops::Range<usize>,
484 cursor_id: crate::model::event::CursorId,
485 deleted_text: &str,
486 ) {
487 let len = range.len();
488
489 let primary_newlines_removed = if cursor_id == cursors.primary_id() {
493 let cursor_pos = cursors.get(cursor_id).map_or(range.start, |c| c.position);
494 let bytes_before_cursor = cursor_pos
495 .saturating_sub(range.start)
496 .min(len)
497 .min(deleted_text.len());
498 deleted_text[..bytes_before_cursor].matches('\n').count()
499 } else {
500 0
501 };
502
503 self.virtual_texts
509 .remove_in_range(&mut self.marker_list, range.start, range.end);
510
511 self.marker_list.adjust_for_delete(range.start, len);
513 self.margins.adjust_for_delete(range.start, len);
514
515 self.buffer.delete(range.clone());
517
518 self.highlighter.notify_delete(range.start, len);
521 self.highlighter.invalidate_range(range.clone());
522
523 cursors.adjust_for_edit(range.start, len, 0);
528
529 if let Some(cursor) = cursors.get_mut(cursor_id) {
531 cursor.position = range.start;
532 cursor.clear_selection();
533 }
534
535 if cursor_id == cursors.primary_id() && primary_newlines_removed > 0 {
537 self.primary_cursor_line_number = match self.primary_cursor_line_number {
538 LineNumber::Absolute(line) => {
539 LineNumber::Absolute(line.saturating_sub(primary_newlines_removed))
540 }
541 LineNumber::Relative {
542 line,
543 from_cached_line,
544 } => LineNumber::Relative {
545 line: line.saturating_sub(primary_newlines_removed),
546 from_cached_line,
547 },
548 };
549 }
550 }
551
552 pub fn apply(&mut self, cursors: &mut Cursors, event: &Event) {
555 match event {
556 Event::Insert {
557 position,
558 text,
559 cursor_id,
560 } => self.apply_insert(cursors, *position, text, *cursor_id),
561
562 Event::Delete {
563 range,
564 cursor_id,
565 deleted_text,
566 } => self.apply_delete(cursors, range, *cursor_id, deleted_text),
567
568 Event::MoveCursor {
569 cursor_id,
570 new_position,
571 new_anchor,
572 new_sticky_column,
573 ..
574 } => self.apply_move_cursor(
575 cursors,
576 *cursor_id,
577 *new_position,
578 *new_anchor,
579 *new_sticky_column,
580 ),
581
582 Event::AddCursor {
583 cursor_id,
584 position,
585 anchor,
586 } => Self::apply_add_cursor(cursors, *cursor_id, *position, *anchor),
587
588 Event::RemoveCursor { cursor_id, .. } => {
589 cursors.remove(*cursor_id);
590 }
591
592 Event::Scroll { .. } | Event::SetViewport { .. } | Event::Recenter => {
595 tracing::warn!("View event {:?} reached EditorState.apply() - should be handled by SplitViewState", event);
598 }
599
600 Event::SetAnchor {
601 cursor_id,
602 position,
603 } => {
604 if let Some(cursor) = cursors.get_mut(*cursor_id) {
607 cursor.anchor = Some(*position);
608 cursor.deselect_on_move = false;
609 }
610 }
611
612 Event::CancelAnchor { cursor_id } => {
613 if let Some(cursor) = cursors.get_mut(*cursor_id) {
616 cursor.deselect_on_move = true;
617 }
618 }
619
620 Event::ClearAnchor { cursor_id } => {
621 if let Some(cursor) = cursors.get_mut(*cursor_id) {
624 cursor.anchor = None;
625 cursor.deselect_on_move = true;
626 cursor.clear_block_selection();
627 }
628 }
629
630 Event::ChangeMode { mode } => {
631 self.mode = mode.clone();
632 }
633
634 Event::AddOverlay {
635 namespace,
636 range,
637 face,
638 priority,
639 message,
640 extend_to_line_end,
641 url,
642 } => self.apply_add_overlay(
643 namespace,
644 range,
645 face,
646 *priority,
647 message,
648 *extend_to_line_end,
649 url,
650 ),
651
652 Event::RemoveOverlay { handle } => {
653 tracing::trace!("RemoveOverlay: handle={:?}", handle);
654 self.overlays
655 .remove_by_handle(handle, &mut self.marker_list);
656 }
657
658 Event::RemoveOverlaysInRange { range } => {
659 self.overlays.remove_in_range(range, &mut self.marker_list);
660 }
661
662 Event::ClearNamespace { namespace } => {
663 tracing::trace!("ClearNamespace: namespace={:?}", namespace);
664 self.overlays
665 .clear_namespace(namespace, &mut self.marker_list);
666 }
667
668 Event::ClearOverlays => {
669 self.overlays.clear(&mut self.marker_list);
670 }
671
672 Event::ShowPopup { popup } => self.apply_show_popup(popup),
673
674 Event::HidePopup => {
675 self.popups.hide();
676 }
677
678 Event::ClearPopups => {
679 self.popups.clear();
680 }
681
682 Event::PopupSelectNext => {
683 if let Some(popup) = self.popups.top_mut() {
684 popup.select_next();
685 }
686 }
687
688 Event::PopupSelectPrev => {
689 if let Some(popup) = self.popups.top_mut() {
690 popup.select_prev();
691 }
692 }
693
694 Event::PopupPageDown => {
695 if let Some(popup) = self.popups.top_mut() {
696 popup.page_down();
697 }
698 }
699
700 Event::PopupPageUp => {
701 if let Some(popup) = self.popups.top_mut() {
702 popup.page_up();
703 }
704 }
705
706 Event::AddMarginAnnotation {
707 line,
708 position,
709 content,
710 annotation_id,
711 } => self.apply_add_margin_annotation(*line, position, content, annotation_id),
712
713 Event::RemoveMarginAnnotation { annotation_id } => {
714 self.margins.remove_by_id(annotation_id);
715 }
716
717 Event::RemoveMarginAnnotationsAtLine { line, position } => {
718 let margin_position = convert_margin_position(position);
719 self.margins.remove_at_line(*line, margin_position);
720 }
721
722 Event::ClearMarginPosition { position } => {
723 let margin_position = convert_margin_position(position);
724 self.margins.clear_position(margin_position);
725 }
726
727 Event::ClearMargins => {
728 self.margins.clear_all();
729 }
730
731 Event::SetLineNumbers { enabled } => {
732 self.margins.configure_for_line_numbers(*enabled);
733 }
734
735 Event::SplitPane { .. }
738 | Event::CloseSplit { .. }
739 | Event::SetActiveSplit { .. }
740 | Event::AdjustSplitRatio { .. }
741 | Event::NextSplit
742 | Event::PrevSplit => {
743 }
745
746 Event::Batch { events, .. } => {
747 for event in events {
750 self.apply(cursors, event);
751 }
752 }
753
754 Event::BulkEdit {
755 new_snapshot,
756 new_cursors,
757 edits,
758 displaced_markers,
759 ..
760 } => self.apply_bulk_edit(cursors, new_snapshot, new_cursors, edits, displaced_markers),
761 }
762 }
763
764 fn apply_move_cursor(
768 &mut self,
769 cursors: &mut Cursors,
770 cursor_id: crate::model::event::CursorId,
771 new_position: usize,
772 new_anchor: Option<usize>,
773 new_sticky_column: usize,
774 ) {
775 if let Some(cursor) = cursors.get_mut(cursor_id) {
776 cursor.position = new_position;
777 cursor.anchor = new_anchor;
778 cursor.sticky_column = new_sticky_column;
779 }
780
781 if cursor_id == cursors.primary_id() {
784 self.primary_cursor_line_number = match self.buffer.offset_to_position(new_position) {
785 Some(pos) => LineNumber::Absolute(pos.line),
786 None => {
787 LineNumber::Absolute(new_position / 80)
790 }
791 };
792 }
793 }
794
795 fn apply_add_cursor(
799 cursors: &mut Cursors,
800 cursor_id: crate::model::event::CursorId,
801 position: usize,
802 anchor: Option<usize>,
803 ) {
804 let cursor = match anchor {
805 Some(anchor) => Cursor::with_selection(anchor, position),
806 None => Cursor::new(position),
807 };
808 cursors.insert_with_id(cursor_id, cursor);
809 cursors.normalize();
810 }
811
812 fn apply_add_overlay(
814 &mut self,
815 namespace: &Option<crate::view::overlay::OverlayNamespace>,
816 range: &Range<usize>,
817 face: &EventOverlayFace,
818 priority: i32,
819 message: &Option<String>,
820 extend_to_line_end: bool,
821 url: &Option<String>,
822 ) {
823 let overlay_face = convert_event_face_to_overlay_face(face);
824 let mut overlay =
825 Overlay::with_priority(&mut self.marker_list, range.clone(), overlay_face, priority);
826 overlay.namespace = namespace.clone();
827 overlay.message = message.clone();
828 overlay.extend_to_line_end = extend_to_line_end;
829 overlay.url = url.clone();
830 self.overlays.add(overlay);
831 }
832
833 fn apply_show_popup(&mut self, popup: &PopupData) {
842 use crate::view::theme::{default_popup_bg, default_popup_border_fg};
843 let popup_obj = convert_popup_data_to_popup(
844 popup,
845 default_popup_bg().into(),
846 default_popup_border_fg().into(),
847 );
848 self.popups.show_or_replace(popup_obj);
849 }
850
851 fn apply_add_margin_annotation(
853 &mut self,
854 line: usize,
855 position: &MarginPositionData,
856 content: &MarginContentData,
857 annotation_id: &Option<String>,
858 ) {
859 let margin_position = convert_margin_position(position);
860 let margin_content = convert_margin_content(content);
861 let annotation = match annotation_id {
862 Some(id) => {
863 MarginAnnotation::with_id(line, margin_position, margin_content, id.clone())
864 }
865 None => MarginAnnotation::new(line, margin_position, margin_content),
866 };
867 self.margins.add_annotation(annotation);
868 }
869
870 fn apply_bulk_edit(
874 &mut self,
875 cursors: &mut Cursors,
876 new_snapshot: &Option<Arc<crate::model::buffer::BufferSnapshot>>,
877 new_cursors: &[(crate::model::event::CursorId, usize, Option<usize>)],
878 edits: &[(usize, usize, usize)],
879 displaced_markers: &[(u64, usize)],
880 ) {
881 if let Some(snapshot) = new_snapshot {
887 self.buffer.restore_buffer_state(snapshot);
888 }
889
890 self.replay_bulk_marker_adjustments(edits);
891
892 if !displaced_markers.is_empty() {
897 self.restore_displaced_markers(displaced_markers);
898 }
899
900 self.virtual_texts.clear(&mut self.marker_list);
903
904 use crate::view::overlay::OverlayNamespace;
905 let namespaces = ["lsp-diagnostic", "reference-highlight", "bracket-highlight"];
906 for ns in &namespaces {
907 self.overlays.clear_namespace(
908 &OverlayNamespace::from_string(ns.to_string()),
909 &mut self.marker_list,
910 );
911 }
912
913 for &(cursor_id, position, anchor) in new_cursors {
915 if let Some(cursor) = cursors.get_mut(cursor_id) {
916 cursor.position = position;
917 cursor.anchor = anchor;
918 }
919 }
920
921 self.highlighter.invalidate_all();
923
924 let primary_pos = cursors.primary().position;
926 self.primary_cursor_line_number = match self.buffer.offset_to_position(primary_pos) {
927 Some(pos) => LineNumber::Absolute(pos.line),
928 None => LineNumber::Absolute(0),
929 };
930 }
931
932 fn replay_bulk_marker_adjustments(&mut self, edits: &[(usize, usize, usize)]) {
942 for &(pos, del_len, ins_len) in edits {
943 match (del_len, ins_len) {
944 (d, i) if d > 0 && i > 0 => {
945 if i > d {
947 self.marker_list.adjust_for_insert(pos, i - d);
948 self.margins.adjust_for_insert(pos, i - d);
949 } else if d > i {
950 self.marker_list.adjust_for_delete(pos, d - i);
951 self.margins.adjust_for_delete(pos, d - i);
952 }
953 }
955 (d, _) if d > 0 => {
956 self.marker_list.adjust_for_delete(pos, d);
957 self.margins.adjust_for_delete(pos, d);
958 }
959 (_, i) if i > 0 => {
960 self.marker_list.adjust_for_insert(pos, i);
961 self.margins.adjust_for_insert(pos, i);
962 }
963 _ => {}
964 }
965 }
966 }
967
968 pub fn capture_displaced_markers(&self, range: &Range<usize>) -> Vec<(u64, usize)> {
971 let mut displaced = Vec::new();
972 if range.is_empty() {
973 return displaced;
974 }
975 for (marker_id, start, _end) in self.marker_list.query_range(range.start, range.end) {
976 if start > range.start && start < range.end {
977 displaced.push(
978 DisplacedMarker::Main {
979 id: marker_id.0,
980 position: start,
981 }
982 .encode(),
983 );
984 }
985 }
986 for (marker_id, start, _end) in self.margins.query_indicator_range(range.start, range.end) {
987 if start > range.start && start < range.end {
988 displaced.push(
989 DisplacedMarker::Margin {
990 id: marker_id.0,
991 position: start,
992 }
993 .encode(),
994 );
995 }
996 }
997 displaced
998 }
999
1000 pub fn capture_displaced_markers_bulk(
1002 &self,
1003 edits: &[(usize, usize, String)],
1004 ) -> Vec<(u64, usize)> {
1005 let mut displaced = Vec::new();
1006 for (pos, del_len, _text) in edits {
1007 if *del_len > 0 {
1008 displaced.extend(self.capture_displaced_markers(&(*pos..*pos + *del_len)));
1009 }
1010 }
1011 displaced
1012 }
1013
1014 pub fn restore_displaced_markers(&mut self, displaced: &[(u64, usize)]) {
1016 for &(tagged_id, original_pos) in displaced {
1017 let dm = DisplacedMarker::decode(tagged_id, original_pos);
1018 match dm {
1019 DisplacedMarker::Main { id, position } => {
1020 self.marker_list.set_position(MarkerId(id), position);
1021 }
1022 DisplacedMarker::Margin { id, position } => {
1023 self.margins.set_indicator_position(MarkerId(id), position);
1024 }
1025 }
1026 }
1027 }
1028
1029 pub fn apply_many(&mut self, cursors: &mut Cursors, events: &[Event]) {
1031 for event in events {
1032 self.apply(cursors, event);
1033 }
1034 }
1035
1036 pub fn on_focus_lost(&mut self) {
1040 if self.popups.dismiss_transient() {
1041 tracing::debug!("Dismissed transient popup on buffer focus loss");
1042 }
1043 }
1044}
1045
1046fn convert_event_face_to_overlay_face(event_face: &EventOverlayFace) -> OverlayFace {
1048 match event_face {
1049 EventOverlayFace::Underline { color, style } => {
1050 let underline_style = match style {
1051 crate::model::event::UnderlineStyle::Straight => UnderlineStyle::Straight,
1052 crate::model::event::UnderlineStyle::Wavy => UnderlineStyle::Wavy,
1053 crate::model::event::UnderlineStyle::Dotted => UnderlineStyle::Dotted,
1054 crate::model::event::UnderlineStyle::Dashed => UnderlineStyle::Dashed,
1055 };
1056 OverlayFace::Underline {
1057 color: Color::Rgb(color.0, color.1, color.2),
1058 style: underline_style,
1059 }
1060 }
1061 EventOverlayFace::Background { color } => OverlayFace::Background {
1062 color: Color::Rgb(color.0, color.1, color.2),
1063 },
1064 EventOverlayFace::Foreground { color } => OverlayFace::Foreground {
1065 color: Color::Rgb(color.0, color.1, color.2),
1066 },
1067 EventOverlayFace::Style { options } => {
1068 use crate::view::theme::named_color_from_str;
1069 use ratatui::style::Modifier;
1070
1071 let mut style = Style::default();
1073
1074 if let Some(ref fg) = options.fg {
1076 if let Some((r, g, b)) = fg.as_rgb() {
1077 style = style.fg(Color::Rgb(r, g, b));
1078 } else if let Some(key) = fg.as_theme_key() {
1079 if let Some(color) = named_color_from_str(key) {
1080 style = style.fg(color);
1081 }
1082 }
1083 }
1084
1085 if let Some(ref bg) = options.bg {
1087 if let Some((r, g, b)) = bg.as_rgb() {
1088 style = style.bg(Color::Rgb(r, g, b));
1089 } else if let Some(key) = bg.as_theme_key() {
1090 if let Some(color) = named_color_from_str(key) {
1091 style = style.bg(color);
1092 }
1093 }
1094 }
1095
1096 let mut modifiers = Modifier::empty();
1098 if options.bold {
1099 modifiers |= Modifier::BOLD;
1100 }
1101 if options.italic {
1102 modifiers |= Modifier::ITALIC;
1103 }
1104 if options.underline {
1105 modifiers |= Modifier::UNDERLINED;
1106 }
1107 if options.strikethrough {
1108 modifiers |= Modifier::CROSSED_OUT;
1109 }
1110 if !modifiers.is_empty() {
1111 style = style.add_modifier(modifiers);
1112 }
1113
1114 let fg_theme = options
1116 .fg
1117 .as_ref()
1118 .and_then(|c| c.as_theme_key())
1119 .filter(|key| named_color_from_str(key).is_none())
1120 .map(String::from);
1121 let bg_theme = options
1122 .bg
1123 .as_ref()
1124 .and_then(|c| c.as_theme_key())
1125 .filter(|key| named_color_from_str(key).is_none())
1126 .map(String::from);
1127
1128 if fg_theme.is_some() || bg_theme.is_some() {
1130 OverlayFace::ThemedStyle {
1131 fallback_style: style,
1132 fg_theme,
1133 bg_theme,
1134 fg_on_collision_only: options.fg_on_collision_only,
1135 }
1136 } else {
1137 OverlayFace::Style { style }
1138 }
1139 }
1140 }
1141}
1142
1143pub(crate) fn convert_popup_data_to_popup(
1150 data: &PopupData,
1151 popup_bg: Color,
1152 popup_border_fg: Color,
1153) -> Popup {
1154 let content = match &data.content {
1155 crate::model::event::PopupContentData::Text(lines) => PopupContent::Text(lines.clone()),
1156 crate::model::event::PopupContentData::List { items, selected } => PopupContent::List {
1157 items: items
1158 .iter()
1159 .map(|item| PopupListItem {
1160 text: item.text.clone(),
1161 detail: item.detail.clone(),
1162 icon: item.icon.clone(),
1163 data: item.data.clone(),
1164 disabled: false,
1165 })
1166 .collect(),
1167 selected: *selected,
1168 },
1169 };
1170
1171 let position = match data.position {
1172 PopupPositionData::AtCursor => PopupPosition::AtCursor,
1173 PopupPositionData::BelowCursor => PopupPosition::BelowCursor,
1174 PopupPositionData::AboveCursor => PopupPosition::AboveCursor,
1175 PopupPositionData::Fixed { x, y } => PopupPosition::Fixed { x, y },
1176 PopupPositionData::Centered => PopupPosition::Centered,
1177 PopupPositionData::BottomRight => PopupPosition::BottomRight,
1178 PopupPositionData::AboveStatusBarAt { x, status_row } => {
1179 PopupPosition::AboveStatusBarAt { x, status_row }
1180 }
1181 };
1182
1183 let kind = match data.kind {
1185 crate::model::event::PopupKindHint::Completion => PopupKind::Completion,
1186 crate::model::event::PopupKindHint::List => PopupKind::List,
1187 crate::model::event::PopupKindHint::Text => PopupKind::Text,
1188 };
1189
1190 let resolver = match kind {
1197 PopupKind::Completion => crate::view::popup::PopupResolver::Completion,
1198 _ => crate::view::popup::PopupResolver::None,
1199 };
1200
1201 let focused = match kind {
1213 PopupKind::Completion => true,
1217 PopupKind::List => true,
1220 PopupKind::Text => false,
1223 PopupKind::Hover | PopupKind::Action => false,
1227 };
1228
1229 Popup {
1230 kind,
1231 title: data.title.clone(),
1232 description: data.description.clone(),
1233 transient: data.transient,
1234 content,
1235 position,
1236 width: data.width,
1237 max_height: data.max_height,
1238 bordered: data.bordered,
1239 border_style: Style::default().fg(popup_border_fg),
1240 background_style: Style::default().bg(popup_bg),
1241 scroll_offset: 0,
1242 text_selection: None,
1243 accept_key_hint: None,
1244 resolver,
1245 focused,
1246 focus_key_hint: None,
1247 }
1248}
1249
1250fn convert_margin_position(position: &MarginPositionData) -> MarginPosition {
1252 match position {
1253 MarginPositionData::Left => MarginPosition::Left,
1254 MarginPositionData::Right => MarginPosition::Right,
1255 }
1256}
1257
1258fn convert_margin_content(content: &MarginContentData) -> MarginContent {
1260 match content {
1261 MarginContentData::Text(text) => MarginContent::Text(text.clone()),
1262 MarginContentData::Symbol { text, color } => {
1263 if let Some((r, g, b)) = color {
1264 MarginContent::colored_symbol(text.clone(), Color::Rgb(*r, *g, *b))
1265 } else {
1266 MarginContent::symbol(text.clone(), Style::default())
1267 }
1268 }
1269 MarginContentData::Empty => MarginContent::Empty,
1270 }
1271}
1272
1273impl EditorState {
1274 pub fn prepare_for_render(&mut self, top_byte: usize, height: u16) -> Result<()> {
1281 self.buffer.prepare_viewport(top_byte, height as usize)?;
1282 Ok(())
1283 }
1284
1285 pub fn collect_virtual_line_positions(&self) -> Vec<usize> {
1297 if self.virtual_texts.is_empty() {
1298 return Vec::new();
1299 }
1300 let mut v: Vec<usize> = self
1301 .virtual_texts
1302 .query_lines_in_range(&self.marker_list, 0, self.buffer.len() + 1)
1303 .into_iter()
1304 .map(|(pos, _vt)| pos)
1305 .collect();
1306 v.sort_unstable();
1307 v
1308 }
1309
1310 pub fn collect_soft_break_positions(&self) -> Vec<(usize, u16)> {
1323 if self.soft_breaks.is_empty() {
1324 return Vec::new();
1325 }
1326 self.soft_breaks
1328 .query_viewport(0, self.buffer.len() + 1, &self.marker_list)
1329 }
1330
1331 pub fn get_text_range(&mut self, start: usize, end: usize) -> String {
1351 match self
1353 .buffer
1354 .get_text_range_mut(start, end.saturating_sub(start))
1355 {
1356 Ok(bytes) => String::from_utf8_lossy(&bytes).into_owned(),
1357 Err(e) => {
1358 tracing::warn!("Failed to get text range {}..{}: {}", start, end, e);
1359 String::new()
1360 }
1361 }
1362 }
1363
1364 pub fn get_line_at_offset(&mut self, offset: usize) -> Option<(usize, String)> {
1372 use crate::model::document_model::DocumentModel;
1373
1374 let mut line_start = offset;
1377 while line_start > 0 {
1378 if let Ok(text) = self.buffer.get_text_range_mut(line_start - 1, 1) {
1379 if text.first() == Some(&b'\n') {
1380 break;
1381 }
1382 line_start -= 1;
1383 } else {
1384 break;
1385 }
1386 }
1387
1388 let viewport = self
1390 .get_viewport_content(
1391 crate::model::document_model::DocumentPosition::byte(line_start),
1392 1,
1393 )
1394 .ok()?;
1395
1396 viewport
1397 .lines
1398 .first()
1399 .map(|line| (line.byte_offset, line.content.clone()))
1400 }
1401
1402 pub fn get_text_to_end_of_line(&mut self, cursor_pos: usize) -> Result<String> {
1407 use crate::model::document_model::DocumentModel;
1408
1409 let viewport = self.get_viewport_content(
1411 crate::model::document_model::DocumentPosition::byte(cursor_pos),
1412 1,
1413 )?;
1414
1415 if let Some(line) = viewport.lines.first() {
1416 let line_start = line.byte_offset;
1417 let line_end = line_start + line.content.len();
1418
1419 if cursor_pos >= line_start && cursor_pos <= line_end {
1420 let offset_in_line = cursor_pos - line_start;
1421 Ok(line.content.get(offset_in_line..).unwrap_or("").to_string())
1423 } else {
1424 Ok(String::new())
1425 }
1426 } else {
1427 Ok(String::new())
1428 }
1429 }
1430
1431 pub fn set_semantic_tokens(&mut self, store: SemanticTokenStore) {
1433 self.semantic_tokens = Some(store);
1434 }
1435
1436 pub fn clear_semantic_tokens(&mut self) {
1438 self.semantic_tokens = None;
1439 }
1440
1441 pub fn semantic_tokens_result_id(&self) -> Option<&str> {
1443 self.semantic_tokens
1444 .as_ref()
1445 .and_then(|store| store.result_id.as_deref())
1446 }
1447}
1448
1449impl DocumentModel for EditorState {
1454 fn capabilities(&self) -> DocumentCapabilities {
1455 let line_count = self.buffer.line_count();
1456 DocumentCapabilities {
1457 has_line_index: line_count.is_some(),
1458 uses_lazy_loading: false, byte_length: self.buffer.len(),
1460 approximate_line_count: line_count.unwrap_or_else(|| {
1461 self.buffer.len() / 80
1463 }),
1464 }
1465 }
1466
1467 fn get_viewport_content(
1468 &mut self,
1469 start_pos: DocumentPosition,
1470 max_lines: usize,
1471 ) -> Result<ViewportContent> {
1472 let start_offset = self.position_to_offset(start_pos)?;
1474
1475 let line_iter = self.buffer.iter_lines_from(start_offset, max_lines)?;
1478 let has_more = line_iter.has_more;
1479
1480 let lines = line_iter
1481 .map(|line_data| ViewportLine {
1482 byte_offset: line_data.byte_offset,
1483 content: line_data.content,
1484 has_newline: line_data.has_newline,
1485 approximate_line_number: line_data.line_number,
1486 })
1487 .collect();
1488
1489 Ok(ViewportContent {
1490 start_position: DocumentPosition::ByteOffset(start_offset),
1491 lines,
1492 has_more,
1493 })
1494 }
1495
1496 fn position_to_offset(&self, pos: DocumentPosition) -> Result<usize> {
1497 match pos {
1498 DocumentPosition::ByteOffset(offset) => Ok(offset),
1499 DocumentPosition::LineColumn { line, column } => {
1500 if !self.has_line_index() {
1501 anyhow::bail!("Line indexing not available for this document");
1502 }
1503 let position = crate::model::piece_tree::Position { line, column };
1505 Ok(self.buffer.position_to_offset(position))
1506 }
1507 }
1508 }
1509
1510 fn offset_to_position(&self, offset: usize) -> DocumentPosition {
1511 if self.has_line_index() {
1512 if let Some(pos) = self.buffer.offset_to_position(offset) {
1513 DocumentPosition::LineColumn {
1514 line: pos.line,
1515 column: pos.column,
1516 }
1517 } else {
1518 DocumentPosition::ByteOffset(offset)
1520 }
1521 } else {
1522 DocumentPosition::ByteOffset(offset)
1523 }
1524 }
1525
1526 fn get_range(&mut self, start: DocumentPosition, end: DocumentPosition) -> Result<String> {
1527 let start_offset = self.position_to_offset(start)?;
1528 let end_offset = self.position_to_offset(end)?;
1529
1530 if start_offset > end_offset {
1531 anyhow::bail!(
1532 "Invalid range: start offset {} > end offset {}",
1533 start_offset,
1534 end_offset
1535 );
1536 }
1537
1538 let bytes = self
1539 .buffer
1540 .get_text_range_mut(start_offset, end_offset - start_offset)?;
1541
1542 Ok(String::from_utf8_lossy(&bytes).into_owned())
1543 }
1544
1545 fn get_line_content(&mut self, line_number: usize) -> Option<String> {
1546 if !self.has_line_index() {
1547 return None;
1548 }
1549
1550 let line_start_offset = self.buffer.line_start_offset(line_number)?;
1552
1553 let mut iter = self.buffer.line_iterator(line_start_offset, 80);
1555 if let Some((_start, content)) = iter.next_line() {
1556 let has_newline = content.ends_with('\n');
1557 let line_content = if has_newline {
1558 content[..content.len() - 1].to_string()
1559 } else {
1560 content
1561 };
1562 Some(line_content)
1563 } else {
1564 None
1565 }
1566 }
1567
1568 fn get_chunk_at_offset(&mut self, offset: usize, size: usize) -> Result<(usize, String)> {
1569 let bytes = self.buffer.get_text_range_mut(offset, size)?;
1570
1571 Ok((offset, String::from_utf8_lossy(&bytes).into_owned()))
1572 }
1573
1574 fn insert(&mut self, pos: DocumentPosition, text: &str) -> Result<usize> {
1575 let offset = self.position_to_offset(pos)?;
1576 self.buffer.insert_bytes(offset, text.as_bytes().to_vec());
1577 Ok(text.len())
1578 }
1579
1580 fn delete(&mut self, start: DocumentPosition, end: DocumentPosition) -> Result<()> {
1581 let start_offset = self.position_to_offset(start)?;
1582 let end_offset = self.position_to_offset(end)?;
1583
1584 if start_offset > end_offset {
1585 anyhow::bail!(
1586 "Invalid range: start offset {} > end offset {}",
1587 start_offset,
1588 end_offset
1589 );
1590 }
1591
1592 self.buffer.delete(start_offset..end_offset);
1593 Ok(())
1594 }
1595
1596 fn replace(
1597 &mut self,
1598 start: DocumentPosition,
1599 end: DocumentPosition,
1600 text: &str,
1601 ) -> Result<()> {
1602 self.delete(start, end)?;
1604 self.insert(start, text)?;
1605 Ok(())
1606 }
1607
1608 fn find_matches(
1609 &mut self,
1610 pattern: &str,
1611 search_range: Option<(DocumentPosition, DocumentPosition)>,
1612 ) -> Result<Vec<usize>> {
1613 let (start_offset, end_offset) = if let Some((start, end)) = search_range {
1614 (
1615 self.position_to_offset(start)?,
1616 self.position_to_offset(end)?,
1617 )
1618 } else {
1619 (0, self.buffer.len())
1620 };
1621
1622 let bytes = self
1624 .buffer
1625 .get_text_range_mut(start_offset, end_offset - start_offset)?;
1626 let text = String::from_utf8_lossy(&bytes);
1627
1628 let mut matches = Vec::new();
1630 let mut search_offset = 0;
1631 while let Some(pos) = text[search_offset..].find(pattern) {
1632 matches.push(start_offset + search_offset + pos);
1633 search_offset += pos + pattern.len();
1634 }
1635
1636 Ok(matches)
1637 }
1638}
1639
1640#[derive(Clone, Debug)]
1642pub struct SemanticTokenStore {
1643 pub version: u64,
1645 pub result_id: Option<String>,
1647 pub data: Vec<u32>,
1649 pub tokens: Vec<SemanticTokenSpan>,
1651}
1652
1653#[derive(Clone, Debug)]
1655pub struct SemanticTokenSpan {
1656 pub range: Range<usize>,
1657 pub token_type: String,
1658 pub modifiers: Vec<String>,
1659}
1660
1661#[cfg(test)]
1662mod tests {
1663 use crate::model::filesystem::StdFileSystem;
1664 use std::sync::Arc;
1665
1666 fn test_fs() -> Arc<dyn crate::model::filesystem::FileSystem + Send + Sync> {
1667 Arc::new(StdFileSystem)
1668 }
1669 use super::*;
1670 use crate::model::event::CursorId;
1671
1672 #[test]
1673 fn test_state_new() {
1674 let state = EditorState::new(
1675 80,
1676 24,
1677 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1678 test_fs(),
1679 );
1680 assert!(state.buffer.is_empty());
1681 }
1682
1683 #[test]
1684 fn test_apply_insert() {
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 state.apply(
1695 &mut cursors,
1696 &Event::Insert {
1697 position: 0,
1698 text: "hello".to_string(),
1699 cursor_id,
1700 },
1701 );
1702
1703 assert_eq!(state.buffer.to_string().unwrap(), "hello");
1704 assert_eq!(cursors.primary().position, 5);
1705 assert!(state.buffer.is_modified());
1706 }
1707
1708 #[test]
1709 fn test_apply_delete() {
1710 let mut state = EditorState::new(
1711 80,
1712 24,
1713 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1714 test_fs(),
1715 );
1716 let mut cursors = Cursors::new();
1717 let cursor_id = cursors.primary_id();
1718
1719 state.apply(
1721 &mut cursors,
1722 &Event::Insert {
1723 position: 0,
1724 text: "hello world".to_string(),
1725 cursor_id,
1726 },
1727 );
1728
1729 state.apply(
1730 &mut cursors,
1731 &Event::Delete {
1732 range: 5..11,
1733 deleted_text: " world".to_string(),
1734 cursor_id,
1735 },
1736 );
1737
1738 assert_eq!(state.buffer.to_string().unwrap(), "hello");
1739 assert_eq!(cursors.primary().position, 5);
1740 }
1741
1742 #[test]
1743 fn test_apply_move_cursor() {
1744 let mut state = EditorState::new(
1745 80,
1746 24,
1747 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1748 test_fs(),
1749 );
1750 let mut cursors = Cursors::new();
1751 let cursor_id = cursors.primary_id();
1752
1753 state.apply(
1754 &mut cursors,
1755 &Event::Insert {
1756 position: 0,
1757 text: "hello".to_string(),
1758 cursor_id,
1759 },
1760 );
1761
1762 state.apply(
1763 &mut cursors,
1764 &Event::MoveCursor {
1765 cursor_id,
1766 old_position: 5,
1767 new_position: 2,
1768 old_anchor: None,
1769 new_anchor: None,
1770 old_sticky_column: 0,
1771 new_sticky_column: 0,
1772 },
1773 );
1774
1775 assert_eq!(cursors.primary().position, 2);
1776 }
1777
1778 #[test]
1779 fn test_apply_add_cursor() {
1780 let mut state = EditorState::new(
1781 80,
1782 24,
1783 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1784 test_fs(),
1785 );
1786 let mut cursors = Cursors::new();
1787 let cursor_id = CursorId(1);
1788
1789 state.apply(
1790 &mut cursors,
1791 &Event::AddCursor {
1792 cursor_id,
1793 position: 5,
1794 anchor: None,
1795 },
1796 );
1797
1798 assert_eq!(cursors.count(), 2);
1799 }
1800
1801 #[test]
1802 fn test_apply_many() {
1803 let mut state = EditorState::new(
1804 80,
1805 24,
1806 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1807 test_fs(),
1808 );
1809 let mut cursors = Cursors::new();
1810 let cursor_id = cursors.primary_id();
1811
1812 let events = vec![
1813 Event::Insert {
1814 position: 0,
1815 text: "hello ".to_string(),
1816 cursor_id,
1817 },
1818 Event::Insert {
1819 position: 6,
1820 text: "world".to_string(),
1821 cursor_id,
1822 },
1823 ];
1824
1825 state.apply_many(&mut cursors, &events);
1826
1827 assert_eq!(state.buffer.to_string().unwrap(), "hello world");
1828 }
1829
1830 #[test]
1831 fn test_cursor_adjustment_after_insert() {
1832 let mut state = EditorState::new(
1833 80,
1834 24,
1835 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1836 test_fs(),
1837 );
1838 let mut cursors = Cursors::new();
1839 let cursor_id = cursors.primary_id();
1840
1841 state.apply(
1843 &mut cursors,
1844 &Event::AddCursor {
1845 cursor_id: CursorId(1),
1846 position: 5,
1847 anchor: None,
1848 },
1849 );
1850
1851 state.apply(
1853 &mut cursors,
1854 &Event::Insert {
1855 position: 0,
1856 text: "abc".to_string(),
1857 cursor_id,
1858 },
1859 );
1860
1861 if let Some(cursor) = cursors.get(CursorId(1)) {
1863 assert_eq!(cursor.position, 8);
1864 }
1865 }
1866
1867 mod document_model_tests {
1869 use super::*;
1870 use crate::model::document_model::{DocumentModel, DocumentPosition};
1871
1872 #[test]
1873 fn test_capabilities_small_file() {
1874 let mut state = EditorState::new(
1875 80,
1876 24,
1877 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1878 test_fs(),
1879 );
1880 state.buffer = Buffer::from_str_test("line1\nline2\nline3");
1881
1882 let caps = state.capabilities();
1883 assert!(caps.has_line_index, "Small file should have line index");
1884 assert_eq!(caps.byte_length, "line1\nline2\nline3".len());
1885 assert_eq!(caps.approximate_line_count, 3, "Should have 3 lines");
1886 }
1887
1888 #[test]
1889 fn test_position_conversions() {
1890 let mut state = EditorState::new(
1891 80,
1892 24,
1893 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1894 test_fs(),
1895 );
1896 state.buffer = Buffer::from_str_test("hello\nworld\ntest");
1897
1898 let pos1 = DocumentPosition::ByteOffset(6);
1900 let offset1 = state.position_to_offset(pos1).unwrap();
1901 assert_eq!(offset1, 6);
1902
1903 let pos2 = DocumentPosition::LineColumn { line: 1, column: 0 };
1905 let offset2 = state.position_to_offset(pos2).unwrap();
1906 assert_eq!(offset2, 6, "Line 1, column 0 should be at byte 6");
1907
1908 let converted = state.offset_to_position(6);
1910 match converted {
1911 DocumentPosition::LineColumn { line, column } => {
1912 assert_eq!(line, 1);
1913 assert_eq!(column, 0);
1914 }
1915 _ => panic!("Expected LineColumn for small file"),
1916 }
1917 }
1918
1919 #[test]
1920 fn test_get_viewport_content() {
1921 let mut state = EditorState::new(
1922 80,
1923 24,
1924 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1925 test_fs(),
1926 );
1927 state.buffer = Buffer::from_str_test("line1\nline2\nline3\nline4\nline5");
1928
1929 let content = state
1930 .get_viewport_content(DocumentPosition::ByteOffset(0), 3)
1931 .unwrap();
1932
1933 assert_eq!(content.lines.len(), 3);
1934 assert_eq!(content.lines[0].content, "line1");
1935 assert_eq!(content.lines[1].content, "line2");
1936 assert_eq!(content.lines[2].content, "line3");
1937 assert!(content.has_more);
1938 }
1939
1940 #[test]
1941 fn test_get_range() {
1942 let mut state = EditorState::new(
1943 80,
1944 24,
1945 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1946 test_fs(),
1947 );
1948 state.buffer = Buffer::from_str_test("hello world");
1949
1950 let text = state
1951 .get_range(
1952 DocumentPosition::ByteOffset(0),
1953 DocumentPosition::ByteOffset(5),
1954 )
1955 .unwrap();
1956 assert_eq!(text, "hello");
1957
1958 let text2 = state
1959 .get_range(
1960 DocumentPosition::ByteOffset(6),
1961 DocumentPosition::ByteOffset(11),
1962 )
1963 .unwrap();
1964 assert_eq!(text2, "world");
1965 }
1966
1967 #[test]
1968 fn test_get_line_content() {
1969 let mut state = EditorState::new(
1970 80,
1971 24,
1972 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1973 test_fs(),
1974 );
1975 state.buffer = Buffer::from_str_test("line1\nline2\nline3");
1976
1977 let line0 = state.get_line_content(0).unwrap();
1978 assert_eq!(line0, "line1");
1979
1980 let line1 = state.get_line_content(1).unwrap();
1981 assert_eq!(line1, "line2");
1982
1983 let line2 = state.get_line_content(2).unwrap();
1984 assert_eq!(line2, "line3");
1985 }
1986
1987 #[test]
1988 fn test_insert_delete() {
1989 let mut state = EditorState::new(
1990 80,
1991 24,
1992 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1993 test_fs(),
1994 );
1995 state.buffer = Buffer::from_str_test("hello world");
1996
1997 let bytes_inserted = state
1999 .insert(DocumentPosition::ByteOffset(6), "beautiful ")
2000 .unwrap();
2001 assert_eq!(bytes_inserted, 10);
2002 assert_eq!(state.buffer.to_string().unwrap(), "hello beautiful world");
2003
2004 state
2006 .delete(
2007 DocumentPosition::ByteOffset(6),
2008 DocumentPosition::ByteOffset(16),
2009 )
2010 .unwrap();
2011 assert_eq!(state.buffer.to_string().unwrap(), "hello world");
2012 }
2013
2014 #[test]
2015 fn test_replace() {
2016 let mut state = EditorState::new(
2017 80,
2018 24,
2019 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
2020 test_fs(),
2021 );
2022 state.buffer = Buffer::from_str_test("hello world");
2023
2024 state
2025 .replace(
2026 DocumentPosition::ByteOffset(0),
2027 DocumentPosition::ByteOffset(5),
2028 "hi",
2029 )
2030 .unwrap();
2031 assert_eq!(state.buffer.to_string().unwrap(), "hi world");
2032 }
2033
2034 #[test]
2035 fn test_find_matches() {
2036 let mut state = EditorState::new(
2037 80,
2038 24,
2039 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
2040 test_fs(),
2041 );
2042 state.buffer = Buffer::from_str_test("hello world hello");
2043
2044 let matches = state.find_matches("hello", None).unwrap();
2045 assert_eq!(matches.len(), 2);
2046 assert_eq!(matches[0], 0);
2047 assert_eq!(matches[1], 12);
2048 }
2049
2050 #[test]
2051 fn test_prepare_for_render() {
2052 let mut state = EditorState::new(
2053 80,
2054 24,
2055 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
2056 test_fs(),
2057 );
2058 state.buffer = Buffer::from_str_test("line1\nline2\nline3\nline4\nline5");
2059
2060 state.prepare_for_render(0, 24).unwrap();
2062 }
2063
2064 #[test]
2065 fn test_helper_get_text_range() {
2066 let mut state = EditorState::new(
2067 80,
2068 24,
2069 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
2070 test_fs(),
2071 );
2072 state.buffer = Buffer::from_str_test("hello world");
2073
2074 let text = state.get_text_range(0, 5);
2076 assert_eq!(text, "hello");
2077
2078 let text2 = state.get_text_range(6, 11);
2080 assert_eq!(text2, "world");
2081 }
2082
2083 #[test]
2084 fn test_helper_get_line_at_offset() {
2085 let mut state = EditorState::new(
2086 80,
2087 24,
2088 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
2089 test_fs(),
2090 );
2091 state.buffer = Buffer::from_str_test("line1\nline2\nline3");
2092
2093 let (offset, content) = state.get_line_at_offset(0).unwrap();
2095 assert_eq!(offset, 0);
2096 assert_eq!(content, "line1");
2097
2098 let (offset2, content2) = state.get_line_at_offset(8).unwrap();
2100 assert_eq!(offset2, 6); assert_eq!(content2, "line2");
2102
2103 let (offset3, content3) = state.get_line_at_offset(12).unwrap();
2105 assert_eq!(offset3, 12);
2106 assert_eq!(content3, "line3");
2107 }
2108
2109 #[test]
2110 fn test_helper_get_text_to_end_of_line() {
2111 let mut state = EditorState::new(
2112 80,
2113 24,
2114 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
2115 test_fs(),
2116 );
2117 state.buffer = Buffer::from_str_test("hello world\nline2");
2118
2119 let text = state.get_text_to_end_of_line(0).unwrap();
2121 assert_eq!(text, "hello world");
2122
2123 let text2 = state.get_text_to_end_of_line(6).unwrap();
2125 assert_eq!(text2, "world");
2126
2127 let text3 = state.get_text_to_end_of_line(11).unwrap();
2129 assert_eq!(text3, "");
2130
2131 let text4 = state.get_text_to_end_of_line(12).unwrap();
2133 assert_eq!(text4, "line2");
2134 }
2135 }
2136
2137 mod virtual_text_integration_tests {
2139 use super::*;
2140 use crate::view::virtual_text::VirtualTextPosition;
2141 use ratatui::style::Style;
2142
2143 #[test]
2144 fn test_virtual_text_add_and_query() {
2145 let mut state = EditorState::new(
2146 80,
2147 24,
2148 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
2149 test_fs(),
2150 );
2151 state.buffer = Buffer::from_str_test("hello world");
2152
2153 if !state.buffer.is_empty() {
2155 state.marker_list.adjust_for_insert(0, state.buffer.len());
2156 }
2157
2158 let vtext_id = state.virtual_texts.add(
2160 &mut state.marker_list,
2161 5,
2162 ": string".to_string(),
2163 Style::default(),
2164 VirtualTextPosition::AfterChar,
2165 0,
2166 );
2167
2168 let results = state.virtual_texts.query_range(&state.marker_list, 0, 11);
2170 assert_eq!(results.len(), 1);
2171 assert_eq!(results[0].0, 5); assert_eq!(results[0].1.text, ": string");
2173
2174 let lookup = state.virtual_texts.build_lookup(&state.marker_list, 0, 11);
2176 assert!(lookup.contains_key(&5));
2177 assert_eq!(lookup[&5].len(), 1);
2178 assert_eq!(lookup[&5][0].text, ": string");
2179
2180 state.virtual_texts.remove(&mut state.marker_list, vtext_id);
2182 assert!(state.virtual_texts.is_empty());
2183 }
2184
2185 #[test]
2186 fn test_virtual_text_position_tracking_on_insert() {
2187 let mut state = EditorState::new(
2188 80,
2189 24,
2190 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
2191 test_fs(),
2192 );
2193 state.buffer = Buffer::from_str_test("hello world");
2194
2195 if !state.buffer.is_empty() {
2197 state.marker_list.adjust_for_insert(0, state.buffer.len());
2198 }
2199
2200 let _vtext_id = state.virtual_texts.add(
2202 &mut state.marker_list,
2203 6,
2204 "/*param*/".to_string(),
2205 Style::default(),
2206 VirtualTextPosition::BeforeChar,
2207 0,
2208 );
2209
2210 let mut cursors = Cursors::new();
2212 let cursor_id = cursors.primary_id();
2213 state.apply(
2214 &mut cursors,
2215 &Event::Insert {
2216 position: 6,
2217 text: "beautiful ".to_string(),
2218 cursor_id,
2219 },
2220 );
2221
2222 let results = state.virtual_texts.query_range(&state.marker_list, 0, 30);
2224 assert_eq!(results.len(), 1);
2225 assert_eq!(results[0].0, 16); assert_eq!(results[0].1.text, "/*param*/");
2227 }
2228
2229 #[test]
2230 fn test_virtual_text_position_tracking_on_delete() {
2231 let mut state = EditorState::new(
2232 80,
2233 24,
2234 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
2235 test_fs(),
2236 );
2237 state.buffer = Buffer::from_str_test("hello beautiful world");
2238
2239 if !state.buffer.is_empty() {
2241 state.marker_list.adjust_for_insert(0, state.buffer.len());
2242 }
2243
2244 let _vtext_id = state.virtual_texts.add(
2246 &mut state.marker_list,
2247 16,
2248 ": string".to_string(),
2249 Style::default(),
2250 VirtualTextPosition::AfterChar,
2251 0,
2252 );
2253
2254 let mut cursors = Cursors::new();
2256 let cursor_id = cursors.primary_id();
2257 state.apply(
2258 &mut cursors,
2259 &Event::Delete {
2260 range: 6..16,
2261 deleted_text: "beautiful ".to_string(),
2262 cursor_id,
2263 },
2264 );
2265
2266 let results = state.virtual_texts.query_range(&state.marker_list, 0, 20);
2268 assert_eq!(results.len(), 1);
2269 assert_eq!(results[0].0, 6); assert_eq!(results[0].1.text, ": string");
2271 }
2272
2273 #[test]
2274 fn test_multiple_virtual_texts_with_priorities() {
2275 let mut state = EditorState::new(
2276 80,
2277 24,
2278 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
2279 test_fs(),
2280 );
2281 state.buffer = Buffer::from_str_test("let x = 5");
2282
2283 if !state.buffer.is_empty() {
2285 state.marker_list.adjust_for_insert(0, state.buffer.len());
2286 }
2287
2288 state.virtual_texts.add(
2290 &mut state.marker_list,
2291 5,
2292 ": i32".to_string(),
2293 Style::default(),
2294 VirtualTextPosition::AfterChar,
2295 0, );
2297
2298 state.virtual_texts.add(
2300 &mut state.marker_list,
2301 5,
2302 " /* inferred */".to_string(),
2303 Style::default(),
2304 VirtualTextPosition::AfterChar,
2305 10, );
2307
2308 let lookup = state.virtual_texts.build_lookup(&state.marker_list, 0, 10);
2310 assert!(lookup.contains_key(&5));
2311 let vtexts = &lookup[&5];
2312 assert_eq!(vtexts.len(), 2);
2313 assert_eq!(vtexts[0].text, ": i32");
2315 assert_eq!(vtexts[1].text, " /* inferred */");
2316 }
2317
2318 #[test]
2319 fn test_virtual_text_clear() {
2320 let mut state = EditorState::new(
2321 80,
2322 24,
2323 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
2324 test_fs(),
2325 );
2326 state.buffer = Buffer::from_str_test("test");
2327
2328 if !state.buffer.is_empty() {
2330 state.marker_list.adjust_for_insert(0, state.buffer.len());
2331 }
2332
2333 state.virtual_texts.add(
2335 &mut state.marker_list,
2336 0,
2337 "hint1".to_string(),
2338 Style::default(),
2339 VirtualTextPosition::BeforeChar,
2340 0,
2341 );
2342 state.virtual_texts.add(
2343 &mut state.marker_list,
2344 2,
2345 "hint2".to_string(),
2346 Style::default(),
2347 VirtualTextPosition::AfterChar,
2348 0,
2349 );
2350
2351 assert_eq!(state.virtual_texts.len(), 2);
2352
2353 state.virtual_texts.clear(&mut state.marker_list);
2355 assert!(state.virtual_texts.is_empty());
2356
2357 let results = state.virtual_texts.query_range(&state.marker_list, 0, 10);
2359 assert!(results.is_empty());
2360 }
2361 }
2362}