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::margin::{MarginAnnotation, MarginContent, MarginManager, MarginPosition};
21use crate::view::overlay::{Overlay, OverlayFace, OverlayManager, UnderlineStyle};
22use crate::view::popup::{
23 Popup, PopupContent, PopupKind, PopupListItem, PopupManager, PopupPosition,
24};
25use crate::view::reference_highlight_overlay::ReferenceHighlightOverlay;
26use crate::view::soft_break::SoftBreakManager;
27use crate::view::virtual_text::VirtualTextManager;
28use anyhow::Result;
29use lsp_types::FoldingRange;
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 buffer_settings: BufferSettings,
191
192 pub reference_highlighter: ReferenceHighlighter,
194
195 pub is_composite_buffer: bool,
197
198 pub debug_highlight_mode: bool,
200
201 pub reference_highlight_overlay: ReferenceHighlightOverlay,
203
204 pub bracket_highlight_overlay: BracketHighlightOverlay,
206
207 pub semantic_tokens: Option<SemanticTokenStore>,
209
210 pub folding_ranges: Vec<FoldingRange>,
212
213 pub language: String,
216
217 pub display_name: String,
222}
223
224impl EditorState {
225 pub fn apply_language(&mut self, detected: DetectedLanguage) {
232 self.language = detected.name;
233 self.display_name = detected.display_name;
234 self.highlighter = detected.highlighter;
235 if let Some(lang) = &detected.ts_language {
236 self.reference_highlighter.set_language(lang);
237 }
238 }
239
240 fn new_from_buffer(buffer: Buffer) -> Self {
243 let mut marker_list = MarkerList::new();
244 if !buffer.is_empty() {
245 marker_list.adjust_for_insert(0, buffer.len());
246 }
247
248 Self {
249 buffer,
250 highlighter: HighlightEngine::None,
251 indent_calculator: RefCell::new(IndentCalculator::new()),
252 overlays: OverlayManager::new(),
253 marker_list,
254 virtual_texts: VirtualTextManager::new(),
255 conceals: ConcealManager::new(),
256 soft_breaks: SoftBreakManager::new(),
257 popups: PopupManager::new(),
258 margins: MarginManager::new(),
259 primary_cursor_line_number: LineNumber::Absolute(0),
260 mode: "insert".to_string(),
261 text_properties: TextPropertyManager::new(),
262 show_cursors: true,
263 editing_disabled: false,
264 buffer_settings: BufferSettings::default(),
265 reference_highlighter: ReferenceHighlighter::new(),
266 is_composite_buffer: false,
267 debug_highlight_mode: false,
268 reference_highlight_overlay: ReferenceHighlightOverlay::new(),
269 bracket_highlight_overlay: BracketHighlightOverlay::new(),
270 semantic_tokens: None,
271 folding_ranges: Vec::new(),
272 language: "text".to_string(),
273 display_name: "Text".to_string(),
274 }
275 }
276
277 pub fn new(
278 _width: u16,
279 _height: u16,
280 large_file_threshold: usize,
281 fs: Arc<dyn FileSystem + Send + Sync>,
282 ) -> Self {
283 Self::new_from_buffer(Buffer::new(large_file_threshold, fs))
284 }
285
286 pub fn new_with_path(
289 large_file_threshold: usize,
290 fs: Arc<dyn FileSystem + Send + Sync>,
291 path: std::path::PathBuf,
292 ) -> Self {
293 Self::new_from_buffer(Buffer::new_with_path(large_file_threshold, fs, path))
294 }
295
296 pub fn set_language_from_name(&mut self, name: &str, registry: &GrammarRegistry) {
300 let detected = DetectedLanguage::from_virtual_name(name, registry);
301 tracing::debug!(
302 "Set highlighter for virtual buffer based on name: {} (backend: {}, language: {})",
303 name,
304 detected.highlighter.backend_name(),
305 detected.name
306 );
307 self.apply_language(detected);
308 }
309
310 pub fn from_file(
315 path: &std::path::Path,
316 _width: u16,
317 _height: u16,
318 large_file_threshold: usize,
319 registry: &GrammarRegistry,
320 fs: Arc<dyn FileSystem + Send + Sync>,
321 ) -> anyhow::Result<Self> {
322 let buffer = Buffer::load_from_file(path, large_file_threshold, fs)?;
323 let detected = DetectedLanguage::from_path_builtin(path, registry);
324 let mut state = Self::new_from_buffer(buffer);
325 state.apply_language(detected);
326 Ok(state)
327 }
328
329 pub fn from_file_with_languages(
337 path: &std::path::Path,
338 _width: u16,
339 _height: u16,
340 large_file_threshold: usize,
341 registry: &GrammarRegistry,
342 languages: &std::collections::HashMap<String, crate::config::LanguageConfig>,
343 fs: Arc<dyn FileSystem + Send + Sync>,
344 ) -> anyhow::Result<Self> {
345 let buffer = Buffer::load_from_file(path, large_file_threshold, fs)?;
346 let detected = DetectedLanguage::from_path(path, registry, languages);
347 let mut state = Self::new_from_buffer(buffer);
348 state.apply_language(detected);
349 Ok(state)
350 }
351
352 pub fn from_buffer_with_language(buffer: Buffer, detected: DetectedLanguage) -> Self {
357 let mut state = Self::new_from_buffer(buffer);
358 state.apply_language(detected);
359 state
360 }
361
362 fn apply_insert(
364 &mut self,
365 cursors: &mut Cursors,
366 position: usize,
367 text: &str,
368 cursor_id: crate::model::event::CursorId,
369 ) {
370 let newlines_inserted = text.matches('\n').count();
371
372 self.marker_list.adjust_for_insert(position, text.len());
374 self.margins.adjust_for_insert(position, text.len());
375
376 self.buffer.insert(position, text);
378
379 self.highlighter.notify_insert(position, text.len());
382 self.highlighter
383 .invalidate_range(position..position + text.len());
384
385 cursors.adjust_for_edit(position, 0, text.len());
390
391 if let Some(cursor) = cursors.get_mut(cursor_id) {
393 cursor.position = position + text.len();
394 cursor.clear_selection();
395 }
396
397 if cursor_id == cursors.primary_id() {
399 self.primary_cursor_line_number = match self.primary_cursor_line_number {
400 LineNumber::Absolute(line) => LineNumber::Absolute(line + newlines_inserted),
401 LineNumber::Relative {
402 line,
403 from_cached_line,
404 } => LineNumber::Relative {
405 line: line + newlines_inserted,
406 from_cached_line,
407 },
408 };
409 }
410 }
411
412 fn apply_delete(
414 &mut self,
415 cursors: &mut Cursors,
416 range: &std::ops::Range<usize>,
417 cursor_id: crate::model::event::CursorId,
418 deleted_text: &str,
419 ) {
420 let len = range.len();
421
422 let primary_newlines_removed = if cursor_id == cursors.primary_id() {
426 let cursor_pos = cursors.get(cursor_id).map_or(range.start, |c| c.position);
427 let bytes_before_cursor = cursor_pos.saturating_sub(range.start).min(len);
428 deleted_text[..bytes_before_cursor].matches('\n').count()
429 } else {
430 0
431 };
432
433 self.marker_list.adjust_for_delete(range.start, len);
435 self.margins.adjust_for_delete(range.start, len);
436
437 self.buffer.delete(range.clone());
439
440 self.highlighter.notify_delete(range.start, len);
443 self.highlighter.invalidate_range(range.clone());
444
445 cursors.adjust_for_edit(range.start, len, 0);
450
451 if let Some(cursor) = cursors.get_mut(cursor_id) {
453 cursor.position = range.start;
454 cursor.clear_selection();
455 }
456
457 if cursor_id == cursors.primary_id() && primary_newlines_removed > 0 {
459 self.primary_cursor_line_number = match self.primary_cursor_line_number {
460 LineNumber::Absolute(line) => {
461 LineNumber::Absolute(line.saturating_sub(primary_newlines_removed))
462 }
463 LineNumber::Relative {
464 line,
465 from_cached_line,
466 } => LineNumber::Relative {
467 line: line.saturating_sub(primary_newlines_removed),
468 from_cached_line,
469 },
470 };
471 }
472 }
473
474 pub fn apply(&mut self, cursors: &mut Cursors, event: &Event) {
477 match event {
478 Event::Insert {
479 position,
480 text,
481 cursor_id,
482 } => self.apply_insert(cursors, *position, text, *cursor_id),
483
484 Event::Delete {
485 range,
486 cursor_id,
487 deleted_text,
488 } => self.apply_delete(cursors, range, *cursor_id, deleted_text),
489
490 Event::MoveCursor {
491 cursor_id,
492 new_position,
493 new_anchor,
494 new_sticky_column,
495 ..
496 } => {
497 if let Some(cursor) = cursors.get_mut(*cursor_id) {
498 cursor.position = *new_position;
499 cursor.anchor = *new_anchor;
500 cursor.sticky_column = *new_sticky_column;
501 }
502
503 if *cursor_id == cursors.primary_id() {
506 self.primary_cursor_line_number =
507 match self.buffer.offset_to_position(*new_position) {
508 Some(pos) => LineNumber::Absolute(pos.line),
509 None => {
510 let estimated_line = *new_position / 80;
513 LineNumber::Absolute(estimated_line)
514 }
515 };
516 }
517 }
518
519 Event::AddCursor {
520 cursor_id,
521 position,
522 anchor,
523 } => {
524 let cursor = if let Some(anchor) = anchor {
525 Cursor::with_selection(*anchor, *position)
526 } else {
527 Cursor::new(*position)
528 };
529
530 cursors.insert_with_id(*cursor_id, cursor);
533
534 cursors.normalize();
535 }
536
537 Event::RemoveCursor { cursor_id, .. } => {
538 cursors.remove(*cursor_id);
539 }
540
541 Event::Scroll { .. } | Event::SetViewport { .. } | Event::Recenter => {
544 tracing::warn!("View event {:?} reached EditorState.apply() - should be handled by SplitViewState", event);
547 }
548
549 Event::SetAnchor {
550 cursor_id,
551 position,
552 } => {
553 if let Some(cursor) = cursors.get_mut(*cursor_id) {
556 cursor.anchor = Some(*position);
557 cursor.deselect_on_move = false;
558 }
559 }
560
561 Event::ClearAnchor { cursor_id } => {
562 if let Some(cursor) = cursors.get_mut(*cursor_id) {
565 cursor.anchor = None;
566 cursor.deselect_on_move = true;
567 cursor.clear_block_selection();
568 }
569 }
570
571 Event::ChangeMode { mode } => {
572 self.mode = mode.clone();
573 }
574
575 Event::AddOverlay {
576 namespace,
577 range,
578 face,
579 priority,
580 message,
581 extend_to_line_end,
582 url,
583 } => {
584 tracing::trace!(
585 "AddOverlay: namespace={:?}, range={:?}, face={:?}, priority={}",
586 namespace,
587 range,
588 face,
589 priority
590 );
591 let overlay_face = convert_event_face_to_overlay_face(face);
593 tracing::trace!("Converted face: {:?}", overlay_face);
594
595 let mut overlay = Overlay::with_priority(
596 &mut self.marker_list,
597 range.clone(),
598 overlay_face,
599 *priority,
600 );
601 overlay.namespace = namespace.clone();
602 overlay.message = message.clone();
603 overlay.extend_to_line_end = *extend_to_line_end;
604 overlay.url = url.clone();
605
606 let actual_range = overlay.range(&self.marker_list);
607 tracing::trace!(
608 "Created overlay with markers - actual range: {:?}, handle={:?}",
609 actual_range,
610 overlay.handle
611 );
612
613 self.overlays.add(overlay);
614 }
615
616 Event::RemoveOverlay { handle } => {
617 tracing::trace!("RemoveOverlay: handle={:?}", handle);
618 self.overlays
619 .remove_by_handle(handle, &mut self.marker_list);
620 }
621
622 Event::RemoveOverlaysInRange { range } => {
623 self.overlays.remove_in_range(range, &mut self.marker_list);
624 }
625
626 Event::ClearNamespace { namespace } => {
627 tracing::trace!("ClearNamespace: namespace={:?}", namespace);
628 self.overlays
629 .clear_namespace(namespace, &mut self.marker_list);
630 }
631
632 Event::ClearOverlays => {
633 self.overlays.clear(&mut self.marker_list);
634 }
635
636 Event::ShowPopup { popup } => {
637 let popup_obj = convert_popup_data_to_popup(popup);
638 self.popups.show_or_replace(popup_obj);
639 }
640
641 Event::HidePopup => {
642 self.popups.hide();
643 }
644
645 Event::ClearPopups => {
646 self.popups.clear();
647 }
648
649 Event::PopupSelectNext => {
650 if let Some(popup) = self.popups.top_mut() {
651 popup.select_next();
652 }
653 }
654
655 Event::PopupSelectPrev => {
656 if let Some(popup) = self.popups.top_mut() {
657 popup.select_prev();
658 }
659 }
660
661 Event::PopupPageDown => {
662 if let Some(popup) = self.popups.top_mut() {
663 popup.page_down();
664 }
665 }
666
667 Event::PopupPageUp => {
668 if let Some(popup) = self.popups.top_mut() {
669 popup.page_up();
670 }
671 }
672
673 Event::AddMarginAnnotation {
674 line,
675 position,
676 content,
677 annotation_id,
678 } => {
679 let margin_position = convert_margin_position(position);
680 let margin_content = convert_margin_content(content);
681 let annotation = if let Some(id) = annotation_id {
682 MarginAnnotation::with_id(*line, margin_position, margin_content, id.clone())
683 } else {
684 MarginAnnotation::new(*line, margin_position, margin_content)
685 };
686 self.margins.add_annotation(annotation);
687 }
688
689 Event::RemoveMarginAnnotation { annotation_id } => {
690 self.margins.remove_by_id(annotation_id);
691 }
692
693 Event::RemoveMarginAnnotationsAtLine { line, position } => {
694 let margin_position = convert_margin_position(position);
695 self.margins.remove_at_line(*line, margin_position);
696 }
697
698 Event::ClearMarginPosition { position } => {
699 let margin_position = convert_margin_position(position);
700 self.margins.clear_position(margin_position);
701 }
702
703 Event::ClearMargins => {
704 self.margins.clear_all();
705 }
706
707 Event::SetLineNumbers { enabled } => {
708 self.margins.configure_for_line_numbers(*enabled);
709 }
710
711 Event::SplitPane { .. }
714 | Event::CloseSplit { .. }
715 | Event::SetActiveSplit { .. }
716 | Event::AdjustSplitRatio { .. }
717 | Event::NextSplit
718 | Event::PrevSplit => {
719 }
721
722 Event::Batch { events, .. } => {
723 for event in events {
726 self.apply(cursors, event);
727 }
728 }
729
730 Event::BulkEdit {
731 new_snapshot,
732 new_cursors,
733 edits,
734 displaced_markers,
735 ..
736 } => {
737 if let Some(snapshot) = new_snapshot {
743 self.buffer.restore_buffer_state(snapshot);
744 }
745
746 for &(pos, del_len, ins_len) in edits {
756 if del_len > 0 && ins_len > 0 {
757 if ins_len > del_len {
759 let net = ins_len - del_len;
760 self.marker_list.adjust_for_insert(pos, net);
761 self.margins.adjust_for_insert(pos, net);
762 } else if del_len > ins_len {
763 let net = del_len - ins_len;
764 self.marker_list.adjust_for_delete(pos, net);
765 self.margins.adjust_for_delete(pos, net);
766 }
767 } else if del_len > 0 {
769 self.marker_list.adjust_for_delete(pos, del_len);
770 self.margins.adjust_for_delete(pos, del_len);
771 } else if ins_len > 0 {
772 self.marker_list.adjust_for_insert(pos, ins_len);
773 self.margins.adjust_for_insert(pos, ins_len);
774 }
775 }
776
777 if !displaced_markers.is_empty() {
782 self.restore_displaced_markers(displaced_markers);
783 }
784
785 self.virtual_texts.clear(&mut self.marker_list);
788
789 use crate::view::overlay::OverlayNamespace;
790 let namespaces = ["lsp-diagnostic", "reference-highlight", "bracket-highlight"];
791 for ns in &namespaces {
792 self.overlays.clear_namespace(
793 &OverlayNamespace::from_string(ns.to_string()),
794 &mut self.marker_list,
795 );
796 }
797
798 for (cursor_id, position, anchor) in new_cursors {
800 if let Some(cursor) = cursors.get_mut(*cursor_id) {
801 cursor.position = *position;
802 cursor.anchor = *anchor;
803 }
804 }
805
806 self.highlighter.invalidate_all();
808
809 let primary_pos = cursors.primary().position;
811 self.primary_cursor_line_number = match self.buffer.offset_to_position(primary_pos)
812 {
813 Some(pos) => crate::model::buffer::LineNumber::Absolute(pos.line),
814 None => crate::model::buffer::LineNumber::Absolute(0),
815 };
816 }
817 }
818 }
819
820 pub fn capture_displaced_markers(&self, range: &Range<usize>) -> Vec<(u64, usize)> {
823 let mut displaced = Vec::new();
824 if range.is_empty() {
825 return displaced;
826 }
827 for (marker_id, start, _end) in self.marker_list.query_range(range.start, range.end) {
828 if start > range.start && start < range.end {
829 displaced.push(
830 DisplacedMarker::Main {
831 id: marker_id.0,
832 position: start,
833 }
834 .encode(),
835 );
836 }
837 }
838 for (marker_id, start, _end) in self.margins.query_indicator_range(range.start, range.end) {
839 if start > range.start && start < range.end {
840 displaced.push(
841 DisplacedMarker::Margin {
842 id: marker_id.0,
843 position: start,
844 }
845 .encode(),
846 );
847 }
848 }
849 displaced
850 }
851
852 pub fn capture_displaced_markers_bulk(
854 &self,
855 edits: &[(usize, usize, String)],
856 ) -> Vec<(u64, usize)> {
857 let mut displaced = Vec::new();
858 for (pos, del_len, _text) in edits {
859 if *del_len > 0 {
860 displaced.extend(self.capture_displaced_markers(&(*pos..*pos + *del_len)));
861 }
862 }
863 displaced
864 }
865
866 pub fn restore_displaced_markers(&mut self, displaced: &[(u64, usize)]) {
868 for &(tagged_id, original_pos) in displaced {
869 let dm = DisplacedMarker::decode(tagged_id, original_pos);
870 match dm {
871 DisplacedMarker::Main { id, position } => {
872 self.marker_list.set_position(MarkerId(id), position);
873 }
874 DisplacedMarker::Margin { id, position } => {
875 self.margins.set_indicator_position(MarkerId(id), position);
876 }
877 }
878 }
879 }
880
881 pub fn apply_many(&mut self, cursors: &mut Cursors, events: &[Event]) {
883 for event in events {
884 self.apply(cursors, event);
885 }
886 }
887
888 pub fn on_focus_lost(&mut self) {
892 if self.popups.dismiss_transient() {
893 tracing::debug!("Dismissed transient popup on buffer focus loss");
894 }
895 }
896}
897
898fn convert_event_face_to_overlay_face(event_face: &EventOverlayFace) -> OverlayFace {
900 match event_face {
901 EventOverlayFace::Underline { color, style } => {
902 let underline_style = match style {
903 crate::model::event::UnderlineStyle::Straight => UnderlineStyle::Straight,
904 crate::model::event::UnderlineStyle::Wavy => UnderlineStyle::Wavy,
905 crate::model::event::UnderlineStyle::Dotted => UnderlineStyle::Dotted,
906 crate::model::event::UnderlineStyle::Dashed => UnderlineStyle::Dashed,
907 };
908 OverlayFace::Underline {
909 color: Color::Rgb(color.0, color.1, color.2),
910 style: underline_style,
911 }
912 }
913 EventOverlayFace::Background { color } => OverlayFace::Background {
914 color: Color::Rgb(color.0, color.1, color.2),
915 },
916 EventOverlayFace::Foreground { color } => OverlayFace::Foreground {
917 color: Color::Rgb(color.0, color.1, color.2),
918 },
919 EventOverlayFace::Style { options } => {
920 use crate::view::theme::named_color_from_str;
921 use ratatui::style::Modifier;
922
923 let mut style = Style::default();
925
926 if let Some(ref fg) = options.fg {
928 if let Some((r, g, b)) = fg.as_rgb() {
929 style = style.fg(Color::Rgb(r, g, b));
930 } else if let Some(key) = fg.as_theme_key() {
931 if let Some(color) = named_color_from_str(key) {
932 style = style.fg(color);
933 }
934 }
935 }
936
937 if let Some(ref bg) = options.bg {
939 if let Some((r, g, b)) = bg.as_rgb() {
940 style = style.bg(Color::Rgb(r, g, b));
941 } else if let Some(key) = bg.as_theme_key() {
942 if let Some(color) = named_color_from_str(key) {
943 style = style.bg(color);
944 }
945 }
946 }
947
948 let mut modifiers = Modifier::empty();
950 if options.bold {
951 modifiers |= Modifier::BOLD;
952 }
953 if options.italic {
954 modifiers |= Modifier::ITALIC;
955 }
956 if options.underline {
957 modifiers |= Modifier::UNDERLINED;
958 }
959 if options.strikethrough {
960 modifiers |= Modifier::CROSSED_OUT;
961 }
962 if !modifiers.is_empty() {
963 style = style.add_modifier(modifiers);
964 }
965
966 let fg_theme = options
968 .fg
969 .as_ref()
970 .and_then(|c| c.as_theme_key())
971 .filter(|key| named_color_from_str(key).is_none())
972 .map(String::from);
973 let bg_theme = options
974 .bg
975 .as_ref()
976 .and_then(|c| c.as_theme_key())
977 .filter(|key| named_color_from_str(key).is_none())
978 .map(String::from);
979
980 if fg_theme.is_some() || bg_theme.is_some() {
982 OverlayFace::ThemedStyle {
983 fallback_style: style,
984 fg_theme,
985 bg_theme,
986 }
987 } else {
988 OverlayFace::Style { style }
989 }
990 }
991 }
992}
993
994pub(crate) fn convert_popup_data_to_popup(data: &PopupData) -> Popup {
996 let content = match &data.content {
997 crate::model::event::PopupContentData::Text(lines) => PopupContent::Text(lines.clone()),
998 crate::model::event::PopupContentData::List { items, selected } => PopupContent::List {
999 items: items
1000 .iter()
1001 .map(|item| PopupListItem {
1002 text: item.text.clone(),
1003 detail: item.detail.clone(),
1004 icon: item.icon.clone(),
1005 data: item.data.clone(),
1006 })
1007 .collect(),
1008 selected: *selected,
1009 },
1010 };
1011
1012 let position = match data.position {
1013 PopupPositionData::AtCursor => PopupPosition::AtCursor,
1014 PopupPositionData::BelowCursor => PopupPosition::BelowCursor,
1015 PopupPositionData::AboveCursor => PopupPosition::AboveCursor,
1016 PopupPositionData::Fixed { x, y } => PopupPosition::Fixed { x, y },
1017 PopupPositionData::Centered => PopupPosition::Centered,
1018 PopupPositionData::BottomRight => PopupPosition::BottomRight,
1019 };
1020
1021 let kind = match data.kind {
1023 crate::model::event::PopupKindHint::Completion => PopupKind::Completion,
1024 crate::model::event::PopupKindHint::List => PopupKind::List,
1025 crate::model::event::PopupKindHint::Text => PopupKind::Text,
1026 };
1027
1028 Popup {
1029 kind,
1030 title: data.title.clone(),
1031 description: data.description.clone(),
1032 transient: data.transient,
1033 content,
1034 position,
1035 width: data.width,
1036 max_height: data.max_height,
1037 bordered: data.bordered,
1038 border_style: Style::default().fg(Color::Gray),
1039 background_style: Style::default().bg(Color::Rgb(30, 30, 30)),
1040 scroll_offset: 0,
1041 text_selection: None,
1042 accept_key_hint: None,
1043 }
1044}
1045
1046fn convert_margin_position(position: &MarginPositionData) -> MarginPosition {
1048 match position {
1049 MarginPositionData::Left => MarginPosition::Left,
1050 MarginPositionData::Right => MarginPosition::Right,
1051 }
1052}
1053
1054fn convert_margin_content(content: &MarginContentData) -> MarginContent {
1056 match content {
1057 MarginContentData::Text(text) => MarginContent::Text(text.clone()),
1058 MarginContentData::Symbol { text, color } => {
1059 if let Some((r, g, b)) = color {
1060 MarginContent::colored_symbol(text.clone(), Color::Rgb(*r, *g, *b))
1061 } else {
1062 MarginContent::symbol(text.clone(), Style::default())
1063 }
1064 }
1065 MarginContentData::Empty => MarginContent::Empty,
1066 }
1067}
1068
1069impl EditorState {
1070 pub fn prepare_for_render(&mut self, top_byte: usize, height: u16) -> Result<()> {
1077 self.buffer.prepare_viewport(top_byte, height as usize)?;
1078 Ok(())
1079 }
1080
1081 pub fn get_text_range(&mut self, start: usize, end: usize) -> String {
1101 match self
1103 .buffer
1104 .get_text_range_mut(start, end.saturating_sub(start))
1105 {
1106 Ok(bytes) => String::from_utf8_lossy(&bytes).into_owned(),
1107 Err(e) => {
1108 tracing::warn!("Failed to get text range {}..{}: {}", start, end, e);
1109 String::new()
1110 }
1111 }
1112 }
1113
1114 pub fn get_line_at_offset(&mut self, offset: usize) -> Option<(usize, String)> {
1122 use crate::model::document_model::DocumentModel;
1123
1124 let mut line_start = offset;
1127 while line_start > 0 {
1128 if let Ok(text) = self.buffer.get_text_range_mut(line_start - 1, 1) {
1129 if text.first() == Some(&b'\n') {
1130 break;
1131 }
1132 line_start -= 1;
1133 } else {
1134 break;
1135 }
1136 }
1137
1138 let viewport = self
1140 .get_viewport_content(
1141 crate::model::document_model::DocumentPosition::byte(line_start),
1142 1,
1143 )
1144 .ok()?;
1145
1146 viewport
1147 .lines
1148 .first()
1149 .map(|line| (line.byte_offset, line.content.clone()))
1150 }
1151
1152 pub fn get_text_to_end_of_line(&mut self, cursor_pos: usize) -> Result<String> {
1157 use crate::model::document_model::DocumentModel;
1158
1159 let viewport = self.get_viewport_content(
1161 crate::model::document_model::DocumentPosition::byte(cursor_pos),
1162 1,
1163 )?;
1164
1165 if let Some(line) = viewport.lines.first() {
1166 let line_start = line.byte_offset;
1167 let line_end = line_start + line.content.len();
1168
1169 if cursor_pos >= line_start && cursor_pos <= line_end {
1170 let offset_in_line = cursor_pos - line_start;
1171 Ok(line.content.get(offset_in_line..).unwrap_or("").to_string())
1173 } else {
1174 Ok(String::new())
1175 }
1176 } else {
1177 Ok(String::new())
1178 }
1179 }
1180
1181 pub fn set_semantic_tokens(&mut self, store: SemanticTokenStore) {
1183 self.semantic_tokens = Some(store);
1184 }
1185
1186 pub fn clear_semantic_tokens(&mut self) {
1188 self.semantic_tokens = None;
1189 }
1190
1191 pub fn semantic_tokens_result_id(&self) -> Option<&str> {
1193 self.semantic_tokens
1194 .as_ref()
1195 .and_then(|store| store.result_id.as_deref())
1196 }
1197}
1198
1199impl DocumentModel for EditorState {
1204 fn capabilities(&self) -> DocumentCapabilities {
1205 let line_count = self.buffer.line_count();
1206 DocumentCapabilities {
1207 has_line_index: line_count.is_some(),
1208 uses_lazy_loading: false, byte_length: self.buffer.len(),
1210 approximate_line_count: line_count.unwrap_or_else(|| {
1211 self.buffer.len() / 80
1213 }),
1214 }
1215 }
1216
1217 fn get_viewport_content(
1218 &mut self,
1219 start_pos: DocumentPosition,
1220 max_lines: usize,
1221 ) -> Result<ViewportContent> {
1222 let start_offset = self.position_to_offset(start_pos)?;
1224
1225 let line_iter = self.buffer.iter_lines_from(start_offset, max_lines)?;
1228 let has_more = line_iter.has_more;
1229
1230 let lines = line_iter
1231 .map(|line_data| ViewportLine {
1232 byte_offset: line_data.byte_offset,
1233 content: line_data.content,
1234 has_newline: line_data.has_newline,
1235 approximate_line_number: line_data.line_number,
1236 })
1237 .collect();
1238
1239 Ok(ViewportContent {
1240 start_position: DocumentPosition::ByteOffset(start_offset),
1241 lines,
1242 has_more,
1243 })
1244 }
1245
1246 fn position_to_offset(&self, pos: DocumentPosition) -> Result<usize> {
1247 match pos {
1248 DocumentPosition::ByteOffset(offset) => Ok(offset),
1249 DocumentPosition::LineColumn { line, column } => {
1250 if !self.has_line_index() {
1251 anyhow::bail!("Line indexing not available for this document");
1252 }
1253 let position = crate::model::piece_tree::Position { line, column };
1255 Ok(self.buffer.position_to_offset(position))
1256 }
1257 }
1258 }
1259
1260 fn offset_to_position(&self, offset: usize) -> DocumentPosition {
1261 if self.has_line_index() {
1262 if let Some(pos) = self.buffer.offset_to_position(offset) {
1263 DocumentPosition::LineColumn {
1264 line: pos.line,
1265 column: pos.column,
1266 }
1267 } else {
1268 DocumentPosition::ByteOffset(offset)
1270 }
1271 } else {
1272 DocumentPosition::ByteOffset(offset)
1273 }
1274 }
1275
1276 fn get_range(&mut self, start: DocumentPosition, end: DocumentPosition) -> Result<String> {
1277 let start_offset = self.position_to_offset(start)?;
1278 let end_offset = self.position_to_offset(end)?;
1279
1280 if start_offset > end_offset {
1281 anyhow::bail!(
1282 "Invalid range: start offset {} > end offset {}",
1283 start_offset,
1284 end_offset
1285 );
1286 }
1287
1288 let bytes = self
1289 .buffer
1290 .get_text_range_mut(start_offset, end_offset - start_offset)?;
1291
1292 Ok(String::from_utf8_lossy(&bytes).into_owned())
1293 }
1294
1295 fn get_line_content(&mut self, line_number: usize) -> Option<String> {
1296 if !self.has_line_index() {
1297 return None;
1298 }
1299
1300 let line_start_offset = self.buffer.line_start_offset(line_number)?;
1302
1303 let mut iter = self.buffer.line_iterator(line_start_offset, 80);
1305 if let Some((_start, content)) = iter.next_line() {
1306 let has_newline = content.ends_with('\n');
1307 let line_content = if has_newline {
1308 content[..content.len() - 1].to_string()
1309 } else {
1310 content
1311 };
1312 Some(line_content)
1313 } else {
1314 None
1315 }
1316 }
1317
1318 fn get_chunk_at_offset(&mut self, offset: usize, size: usize) -> Result<(usize, String)> {
1319 let bytes = self.buffer.get_text_range_mut(offset, size)?;
1320
1321 Ok((offset, String::from_utf8_lossy(&bytes).into_owned()))
1322 }
1323
1324 fn insert(&mut self, pos: DocumentPosition, text: &str) -> Result<usize> {
1325 let offset = self.position_to_offset(pos)?;
1326 self.buffer.insert_bytes(offset, text.as_bytes().to_vec());
1327 Ok(text.len())
1328 }
1329
1330 fn delete(&mut self, start: DocumentPosition, end: DocumentPosition) -> Result<()> {
1331 let start_offset = self.position_to_offset(start)?;
1332 let end_offset = self.position_to_offset(end)?;
1333
1334 if start_offset > end_offset {
1335 anyhow::bail!(
1336 "Invalid range: start offset {} > end offset {}",
1337 start_offset,
1338 end_offset
1339 );
1340 }
1341
1342 self.buffer.delete(start_offset..end_offset);
1343 Ok(())
1344 }
1345
1346 fn replace(
1347 &mut self,
1348 start: DocumentPosition,
1349 end: DocumentPosition,
1350 text: &str,
1351 ) -> Result<()> {
1352 self.delete(start, end)?;
1354 self.insert(start, text)?;
1355 Ok(())
1356 }
1357
1358 fn find_matches(
1359 &mut self,
1360 pattern: &str,
1361 search_range: Option<(DocumentPosition, DocumentPosition)>,
1362 ) -> Result<Vec<usize>> {
1363 let (start_offset, end_offset) = if let Some((start, end)) = search_range {
1364 (
1365 self.position_to_offset(start)?,
1366 self.position_to_offset(end)?,
1367 )
1368 } else {
1369 (0, self.buffer.len())
1370 };
1371
1372 let bytes = self
1374 .buffer
1375 .get_text_range_mut(start_offset, end_offset - start_offset)?;
1376 let text = String::from_utf8_lossy(&bytes);
1377
1378 let mut matches = Vec::new();
1380 let mut search_offset = 0;
1381 while let Some(pos) = text[search_offset..].find(pattern) {
1382 matches.push(start_offset + search_offset + pos);
1383 search_offset += pos + pattern.len();
1384 }
1385
1386 Ok(matches)
1387 }
1388}
1389
1390#[derive(Clone, Debug)]
1392pub struct SemanticTokenStore {
1393 pub version: u64,
1395 pub result_id: Option<String>,
1397 pub data: Vec<u32>,
1399 pub tokens: Vec<SemanticTokenSpan>,
1401}
1402
1403#[derive(Clone, Debug)]
1405pub struct SemanticTokenSpan {
1406 pub range: Range<usize>,
1407 pub token_type: String,
1408 pub modifiers: Vec<String>,
1409}
1410
1411#[cfg(test)]
1412mod tests {
1413 use crate::model::filesystem::StdFileSystem;
1414 use std::sync::Arc;
1415
1416 fn test_fs() -> Arc<dyn crate::model::filesystem::FileSystem + Send + Sync> {
1417 Arc::new(StdFileSystem)
1418 }
1419 use super::*;
1420 use crate::model::event::CursorId;
1421
1422 #[test]
1423 fn test_state_new() {
1424 let state = EditorState::new(
1425 80,
1426 24,
1427 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1428 test_fs(),
1429 );
1430 assert!(state.buffer.is_empty());
1431 }
1432
1433 #[test]
1434 fn test_apply_insert() {
1435 let mut state = EditorState::new(
1436 80,
1437 24,
1438 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1439 test_fs(),
1440 );
1441 let mut cursors = Cursors::new();
1442 let cursor_id = cursors.primary_id();
1443
1444 state.apply(
1445 &mut cursors,
1446 &Event::Insert {
1447 position: 0,
1448 text: "hello".to_string(),
1449 cursor_id,
1450 },
1451 );
1452
1453 assert_eq!(state.buffer.to_string().unwrap(), "hello");
1454 assert_eq!(cursors.primary().position, 5);
1455 assert!(state.buffer.is_modified());
1456 }
1457
1458 #[test]
1459 fn test_apply_delete() {
1460 let mut state = EditorState::new(
1461 80,
1462 24,
1463 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1464 test_fs(),
1465 );
1466 let mut cursors = Cursors::new();
1467 let cursor_id = cursors.primary_id();
1468
1469 state.apply(
1471 &mut cursors,
1472 &Event::Insert {
1473 position: 0,
1474 text: "hello world".to_string(),
1475 cursor_id,
1476 },
1477 );
1478
1479 state.apply(
1480 &mut cursors,
1481 &Event::Delete {
1482 range: 5..11,
1483 deleted_text: " world".to_string(),
1484 cursor_id,
1485 },
1486 );
1487
1488 assert_eq!(state.buffer.to_string().unwrap(), "hello");
1489 assert_eq!(cursors.primary().position, 5);
1490 }
1491
1492 #[test]
1493 fn test_apply_move_cursor() {
1494 let mut state = EditorState::new(
1495 80,
1496 24,
1497 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1498 test_fs(),
1499 );
1500 let mut cursors = Cursors::new();
1501 let cursor_id = cursors.primary_id();
1502
1503 state.apply(
1504 &mut cursors,
1505 &Event::Insert {
1506 position: 0,
1507 text: "hello".to_string(),
1508 cursor_id,
1509 },
1510 );
1511
1512 state.apply(
1513 &mut cursors,
1514 &Event::MoveCursor {
1515 cursor_id,
1516 old_position: 5,
1517 new_position: 2,
1518 old_anchor: None,
1519 new_anchor: None,
1520 old_sticky_column: 0,
1521 new_sticky_column: 0,
1522 },
1523 );
1524
1525 assert_eq!(cursors.primary().position, 2);
1526 }
1527
1528 #[test]
1529 fn test_apply_add_cursor() {
1530 let mut state = EditorState::new(
1531 80,
1532 24,
1533 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1534 test_fs(),
1535 );
1536 let mut cursors = Cursors::new();
1537 let cursor_id = CursorId(1);
1538
1539 state.apply(
1540 &mut cursors,
1541 &Event::AddCursor {
1542 cursor_id,
1543 position: 5,
1544 anchor: None,
1545 },
1546 );
1547
1548 assert_eq!(cursors.count(), 2);
1549 }
1550
1551 #[test]
1552 fn test_apply_many() {
1553 let mut state = EditorState::new(
1554 80,
1555 24,
1556 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1557 test_fs(),
1558 );
1559 let mut cursors = Cursors::new();
1560 let cursor_id = cursors.primary_id();
1561
1562 let events = vec![
1563 Event::Insert {
1564 position: 0,
1565 text: "hello ".to_string(),
1566 cursor_id,
1567 },
1568 Event::Insert {
1569 position: 6,
1570 text: "world".to_string(),
1571 cursor_id,
1572 },
1573 ];
1574
1575 state.apply_many(&mut cursors, &events);
1576
1577 assert_eq!(state.buffer.to_string().unwrap(), "hello world");
1578 }
1579
1580 #[test]
1581 fn test_cursor_adjustment_after_insert() {
1582 let mut state = EditorState::new(
1583 80,
1584 24,
1585 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1586 test_fs(),
1587 );
1588 let mut cursors = Cursors::new();
1589 let cursor_id = cursors.primary_id();
1590
1591 state.apply(
1593 &mut cursors,
1594 &Event::AddCursor {
1595 cursor_id: CursorId(1),
1596 position: 5,
1597 anchor: None,
1598 },
1599 );
1600
1601 state.apply(
1603 &mut cursors,
1604 &Event::Insert {
1605 position: 0,
1606 text: "abc".to_string(),
1607 cursor_id,
1608 },
1609 );
1610
1611 if let Some(cursor) = cursors.get(CursorId(1)) {
1613 assert_eq!(cursor.position, 8);
1614 }
1615 }
1616
1617 mod document_model_tests {
1619 use super::*;
1620 use crate::model::document_model::{DocumentModel, DocumentPosition};
1621
1622 #[test]
1623 fn test_capabilities_small_file() {
1624 let mut state = EditorState::new(
1625 80,
1626 24,
1627 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1628 test_fs(),
1629 );
1630 state.buffer = Buffer::from_str_test("line1\nline2\nline3");
1631
1632 let caps = state.capabilities();
1633 assert!(caps.has_line_index, "Small file should have line index");
1634 assert_eq!(caps.byte_length, "line1\nline2\nline3".len());
1635 assert_eq!(caps.approximate_line_count, 3, "Should have 3 lines");
1636 }
1637
1638 #[test]
1639 fn test_position_conversions() {
1640 let mut state = EditorState::new(
1641 80,
1642 24,
1643 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1644 test_fs(),
1645 );
1646 state.buffer = Buffer::from_str_test("hello\nworld\ntest");
1647
1648 let pos1 = DocumentPosition::ByteOffset(6);
1650 let offset1 = state.position_to_offset(pos1).unwrap();
1651 assert_eq!(offset1, 6);
1652
1653 let pos2 = DocumentPosition::LineColumn { line: 1, column: 0 };
1655 let offset2 = state.position_to_offset(pos2).unwrap();
1656 assert_eq!(offset2, 6, "Line 1, column 0 should be at byte 6");
1657
1658 let converted = state.offset_to_position(6);
1660 match converted {
1661 DocumentPosition::LineColumn { line, column } => {
1662 assert_eq!(line, 1);
1663 assert_eq!(column, 0);
1664 }
1665 _ => panic!("Expected LineColumn for small file"),
1666 }
1667 }
1668
1669 #[test]
1670 fn test_get_viewport_content() {
1671 let mut state = EditorState::new(
1672 80,
1673 24,
1674 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1675 test_fs(),
1676 );
1677 state.buffer = Buffer::from_str_test("line1\nline2\nline3\nline4\nline5");
1678
1679 let content = state
1680 .get_viewport_content(DocumentPosition::ByteOffset(0), 3)
1681 .unwrap();
1682
1683 assert_eq!(content.lines.len(), 3);
1684 assert_eq!(content.lines[0].content, "line1");
1685 assert_eq!(content.lines[1].content, "line2");
1686 assert_eq!(content.lines[2].content, "line3");
1687 assert!(content.has_more);
1688 }
1689
1690 #[test]
1691 fn test_get_range() {
1692 let mut state = EditorState::new(
1693 80,
1694 24,
1695 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1696 test_fs(),
1697 );
1698 state.buffer = Buffer::from_str_test("hello world");
1699
1700 let text = state
1701 .get_range(
1702 DocumentPosition::ByteOffset(0),
1703 DocumentPosition::ByteOffset(5),
1704 )
1705 .unwrap();
1706 assert_eq!(text, "hello");
1707
1708 let text2 = state
1709 .get_range(
1710 DocumentPosition::ByteOffset(6),
1711 DocumentPosition::ByteOffset(11),
1712 )
1713 .unwrap();
1714 assert_eq!(text2, "world");
1715 }
1716
1717 #[test]
1718 fn test_get_line_content() {
1719 let mut state = EditorState::new(
1720 80,
1721 24,
1722 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1723 test_fs(),
1724 );
1725 state.buffer = Buffer::from_str_test("line1\nline2\nline3");
1726
1727 let line0 = state.get_line_content(0).unwrap();
1728 assert_eq!(line0, "line1");
1729
1730 let line1 = state.get_line_content(1).unwrap();
1731 assert_eq!(line1, "line2");
1732
1733 let line2 = state.get_line_content(2).unwrap();
1734 assert_eq!(line2, "line3");
1735 }
1736
1737 #[test]
1738 fn test_insert_delete() {
1739 let mut state = EditorState::new(
1740 80,
1741 24,
1742 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1743 test_fs(),
1744 );
1745 state.buffer = Buffer::from_str_test("hello world");
1746
1747 let bytes_inserted = state
1749 .insert(DocumentPosition::ByteOffset(6), "beautiful ")
1750 .unwrap();
1751 assert_eq!(bytes_inserted, 10);
1752 assert_eq!(state.buffer.to_string().unwrap(), "hello beautiful world");
1753
1754 state
1756 .delete(
1757 DocumentPosition::ByteOffset(6),
1758 DocumentPosition::ByteOffset(16),
1759 )
1760 .unwrap();
1761 assert_eq!(state.buffer.to_string().unwrap(), "hello world");
1762 }
1763
1764 #[test]
1765 fn test_replace() {
1766 let mut state = EditorState::new(
1767 80,
1768 24,
1769 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1770 test_fs(),
1771 );
1772 state.buffer = Buffer::from_str_test("hello world");
1773
1774 state
1775 .replace(
1776 DocumentPosition::ByteOffset(0),
1777 DocumentPosition::ByteOffset(5),
1778 "hi",
1779 )
1780 .unwrap();
1781 assert_eq!(state.buffer.to_string().unwrap(), "hi world");
1782 }
1783
1784 #[test]
1785 fn test_find_matches() {
1786 let mut state = EditorState::new(
1787 80,
1788 24,
1789 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1790 test_fs(),
1791 );
1792 state.buffer = Buffer::from_str_test("hello world hello");
1793
1794 let matches = state.find_matches("hello", None).unwrap();
1795 assert_eq!(matches.len(), 2);
1796 assert_eq!(matches[0], 0);
1797 assert_eq!(matches[1], 12);
1798 }
1799
1800 #[test]
1801 fn test_prepare_for_render() {
1802 let mut state = EditorState::new(
1803 80,
1804 24,
1805 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1806 test_fs(),
1807 );
1808 state.buffer = Buffer::from_str_test("line1\nline2\nline3\nline4\nline5");
1809
1810 state.prepare_for_render(0, 24).unwrap();
1812 }
1813
1814 #[test]
1815 fn test_helper_get_text_range() {
1816 let mut state = EditorState::new(
1817 80,
1818 24,
1819 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1820 test_fs(),
1821 );
1822 state.buffer = Buffer::from_str_test("hello world");
1823
1824 let text = state.get_text_range(0, 5);
1826 assert_eq!(text, "hello");
1827
1828 let text2 = state.get_text_range(6, 11);
1830 assert_eq!(text2, "world");
1831 }
1832
1833 #[test]
1834 fn test_helper_get_line_at_offset() {
1835 let mut state = EditorState::new(
1836 80,
1837 24,
1838 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1839 test_fs(),
1840 );
1841 state.buffer = Buffer::from_str_test("line1\nline2\nline3");
1842
1843 let (offset, content) = state.get_line_at_offset(0).unwrap();
1845 assert_eq!(offset, 0);
1846 assert_eq!(content, "line1");
1847
1848 let (offset2, content2) = state.get_line_at_offset(8).unwrap();
1850 assert_eq!(offset2, 6); assert_eq!(content2, "line2");
1852
1853 let (offset3, content3) = state.get_line_at_offset(12).unwrap();
1855 assert_eq!(offset3, 12);
1856 assert_eq!(content3, "line3");
1857 }
1858
1859 #[test]
1860 fn test_helper_get_text_to_end_of_line() {
1861 let mut state = EditorState::new(
1862 80,
1863 24,
1864 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1865 test_fs(),
1866 );
1867 state.buffer = Buffer::from_str_test("hello world\nline2");
1868
1869 let text = state.get_text_to_end_of_line(0).unwrap();
1871 assert_eq!(text, "hello world");
1872
1873 let text2 = state.get_text_to_end_of_line(6).unwrap();
1875 assert_eq!(text2, "world");
1876
1877 let text3 = state.get_text_to_end_of_line(11).unwrap();
1879 assert_eq!(text3, "");
1880
1881 let text4 = state.get_text_to_end_of_line(12).unwrap();
1883 assert_eq!(text4, "line2");
1884 }
1885 }
1886
1887 mod virtual_text_integration_tests {
1889 use super::*;
1890 use crate::view::virtual_text::VirtualTextPosition;
1891 use ratatui::style::Style;
1892
1893 #[test]
1894 fn test_virtual_text_add_and_query() {
1895 let mut state = EditorState::new(
1896 80,
1897 24,
1898 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1899 test_fs(),
1900 );
1901 state.buffer = Buffer::from_str_test("hello world");
1902
1903 if !state.buffer.is_empty() {
1905 state.marker_list.adjust_for_insert(0, state.buffer.len());
1906 }
1907
1908 let vtext_id = state.virtual_texts.add(
1910 &mut state.marker_list,
1911 5,
1912 ": string".to_string(),
1913 Style::default(),
1914 VirtualTextPosition::AfterChar,
1915 0,
1916 );
1917
1918 let results = state.virtual_texts.query_range(&state.marker_list, 0, 11);
1920 assert_eq!(results.len(), 1);
1921 assert_eq!(results[0].0, 5); assert_eq!(results[0].1.text, ": string");
1923
1924 let lookup = state.virtual_texts.build_lookup(&state.marker_list, 0, 11);
1926 assert!(lookup.contains_key(&5));
1927 assert_eq!(lookup[&5].len(), 1);
1928 assert_eq!(lookup[&5][0].text, ": string");
1929
1930 state.virtual_texts.remove(&mut state.marker_list, vtext_id);
1932 assert!(state.virtual_texts.is_empty());
1933 }
1934
1935 #[test]
1936 fn test_virtual_text_position_tracking_on_insert() {
1937 let mut state = EditorState::new(
1938 80,
1939 24,
1940 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1941 test_fs(),
1942 );
1943 state.buffer = Buffer::from_str_test("hello world");
1944
1945 if !state.buffer.is_empty() {
1947 state.marker_list.adjust_for_insert(0, state.buffer.len());
1948 }
1949
1950 let _vtext_id = state.virtual_texts.add(
1952 &mut state.marker_list,
1953 6,
1954 "/*param*/".to_string(),
1955 Style::default(),
1956 VirtualTextPosition::BeforeChar,
1957 0,
1958 );
1959
1960 let mut cursors = Cursors::new();
1962 let cursor_id = cursors.primary_id();
1963 state.apply(
1964 &mut cursors,
1965 &Event::Insert {
1966 position: 6,
1967 text: "beautiful ".to_string(),
1968 cursor_id,
1969 },
1970 );
1971
1972 let results = state.virtual_texts.query_range(&state.marker_list, 0, 30);
1974 assert_eq!(results.len(), 1);
1975 assert_eq!(results[0].0, 16); assert_eq!(results[0].1.text, "/*param*/");
1977 }
1978
1979 #[test]
1980 fn test_virtual_text_position_tracking_on_delete() {
1981 let mut state = EditorState::new(
1982 80,
1983 24,
1984 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1985 test_fs(),
1986 );
1987 state.buffer = Buffer::from_str_test("hello beautiful world");
1988
1989 if !state.buffer.is_empty() {
1991 state.marker_list.adjust_for_insert(0, state.buffer.len());
1992 }
1993
1994 let _vtext_id = state.virtual_texts.add(
1996 &mut state.marker_list,
1997 16,
1998 ": string".to_string(),
1999 Style::default(),
2000 VirtualTextPosition::AfterChar,
2001 0,
2002 );
2003
2004 let mut cursors = Cursors::new();
2006 let cursor_id = cursors.primary_id();
2007 state.apply(
2008 &mut cursors,
2009 &Event::Delete {
2010 range: 6..16,
2011 deleted_text: "beautiful ".to_string(),
2012 cursor_id,
2013 },
2014 );
2015
2016 let results = state.virtual_texts.query_range(&state.marker_list, 0, 20);
2018 assert_eq!(results.len(), 1);
2019 assert_eq!(results[0].0, 6); assert_eq!(results[0].1.text, ": string");
2021 }
2022
2023 #[test]
2024 fn test_multiple_virtual_texts_with_priorities() {
2025 let mut state = EditorState::new(
2026 80,
2027 24,
2028 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
2029 test_fs(),
2030 );
2031 state.buffer = Buffer::from_str_test("let x = 5");
2032
2033 if !state.buffer.is_empty() {
2035 state.marker_list.adjust_for_insert(0, state.buffer.len());
2036 }
2037
2038 state.virtual_texts.add(
2040 &mut state.marker_list,
2041 5,
2042 ": i32".to_string(),
2043 Style::default(),
2044 VirtualTextPosition::AfterChar,
2045 0, );
2047
2048 state.virtual_texts.add(
2050 &mut state.marker_list,
2051 5,
2052 " /* inferred */".to_string(),
2053 Style::default(),
2054 VirtualTextPosition::AfterChar,
2055 10, );
2057
2058 let lookup = state.virtual_texts.build_lookup(&state.marker_list, 0, 10);
2060 assert!(lookup.contains_key(&5));
2061 let vtexts = &lookup[&5];
2062 assert_eq!(vtexts.len(), 2);
2063 assert_eq!(vtexts[0].text, ": i32");
2065 assert_eq!(vtexts[1].text, " /* inferred */");
2066 }
2067
2068 #[test]
2069 fn test_virtual_text_clear() {
2070 let mut state = EditorState::new(
2071 80,
2072 24,
2073 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
2074 test_fs(),
2075 );
2076 state.buffer = Buffer::from_str_test("test");
2077
2078 if !state.buffer.is_empty() {
2080 state.marker_list.adjust_for_insert(0, state.buffer.len());
2081 }
2082
2083 state.virtual_texts.add(
2085 &mut state.marker_list,
2086 0,
2087 "hint1".to_string(),
2088 Style::default(),
2089 VirtualTextPosition::BeforeChar,
2090 0,
2091 );
2092 state.virtual_texts.add(
2093 &mut state.marker_list,
2094 2,
2095 "hint2".to_string(),
2096 Style::default(),
2097 VirtualTextPosition::AfterChar,
2098 0,
2099 );
2100
2101 assert_eq!(state.virtual_texts.len(), 2);
2102
2103 state.virtual_texts.clear(&mut state.marker_list);
2105 assert!(state.virtual_texts.is_empty());
2106
2107 let results = state.virtual_texts.query_range(&state.marker_list, 0, 10);
2109 assert!(results.is_empty());
2110 }
2111 }
2112}