1use futures::Stream as _;
2#[cfg(not(target_family = "wasm"))]
3use std::time::Instant;
4use std::{
5 ops::{Range, RangeInclusive},
6 pin::Pin,
7 sync::{Arc, Mutex, OnceLock},
8 task::Poll,
9 time::Duration,
10};
11#[cfg(target_family = "wasm")]
12use web_time::Instant;
13
14use gpui::{
15 App, AppContext as _, Bounds, Context, EntityId, FocusHandle, IntoElement, KeyBinding,
16 ListState, ParentElement as _, Pixels, Point, Render, SharedString, Styled as _, Task, Window,
17 prelude::FluentBuilder as _, px,
18};
19
20use crate::{
21 AutoScroll, ElementExt, TextSelection,
22 async_util::{Receiver, Sender, unbounded},
23 input::{self, SelectAll},
24 text::{
25 CodeBlockActionsFn, CodeBlockHighlighterFn, LinkClickHandlerFn, MarkdownExtensions,
26 RangeHighlight, RangeHighlightError, RenderedText, TableActionsFn, TextViewStyle,
27 document::ParsedDocument,
28 format,
29 node::{self, NodeContext},
30 range_highlight::{
31 LeafRemap, PendingReveal, RangeHighlightFrame, RenderedIndex, RevealRequest,
32 },
33 selection_adapter::TextViewSelectionAdapter,
34 stream_fade::{StreamFadeTracker, TextViewMotion},
35 },
36 v_flex,
37};
38
39const CONTEXT: &'static str = "TextView";
40const MAX_COALESCED_UPDATES_PER_PARSE: usize = 64;
42const MAX_SYNC_FULL_REPLACE_BYTES: usize = 4 * 1024;
45const STREAM_FADE_TICK: Duration = Duration::from_millis(33);
47
48pub(crate) fn init(cx: &mut App) {
49 cx.bind_keys(vec![
50 #[cfg(target_os = "macos")]
51 KeyBinding::new("cmd-c", input::Copy, Some(CONTEXT)),
52 #[cfg(not(target_os = "macos"))]
53 KeyBinding::new("ctrl-c", input::Copy, Some(CONTEXT)),
54 #[cfg(target_os = "macos")]
55 KeyBinding::new("cmd-a", input::SelectAll, Some(CONTEXT)),
56 #[cfg(not(target_os = "macos"))]
57 KeyBinding::new("ctrl-a", input::SelectAll, Some(CONTEXT)),
58 ]);
59}
60
61#[derive(Clone, Copy, PartialEq, Eq)]
63pub(super) enum TextViewFormat {
64 Markdown,
66 Html,
68}
69
70#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
74pub enum SelectionFormat {
75 #[default]
77 Plain,
78 Source,
84}
85
86#[derive(Clone, Copy)]
90pub(super) struct LineSpan {
91 pub(super) top: Pixels,
92 pub(super) bottom: Pixels,
93 pub(super) line_height: Pixels,
94}
95
96pub struct TextViewState {
98 entity_id: EntityId,
99 pub(super) focus_handle: FocusHandle,
100 pub(super) list_state: ListState,
101
102 bounds: Bounds<Pixels>,
104
105 pub(super) selectable: bool,
106 pub(super) selection_format: SelectionFormat,
107 pub(super) scrollable: bool,
108 pub(super) max_lines: Option<usize>,
109 pub(super) line_spans: Arc<Mutex<Vec<LineSpan>>>,
112 pub(super) clamped: bool,
114 pub(super) text_view_style: Arc<TextViewStyle>,
115 pub(super) code_block_actions: Option<std::sync::Arc<CodeBlockActionsFn>>,
116 pub(super) code_block_highlighter: Option<std::sync::Arc<CodeBlockHighlighterFn>>,
117 pub(super) table_actions: Option<std::sync::Arc<TableActionsFn>>,
118 pub(super) image_source: Option<std::sync::Arc<super::text_view::ImageSourceFn>>,
119 pub(super) link_click_handler: Option<std::sync::Arc<LinkClickHandlerFn>>,
120 pub(super) markdown_extensions: Arc<MarkdownExtensions>,
121
122 pub(super) is_selecting: bool,
123 pub(super) preserve_inline_selection: bool,
125 multi_click_selection: Option<TextViewMultiClickSelection>,
126 selected_text_override: Option<String>,
127 select_all: bool,
128 pub(super) auto_scroll: AutoScroll,
129 pub(super) selection_adapter: TextViewSelectionAdapter,
130
131 pub(super) parsed_content: ParsedContent,
132 pub(super) stream_fade: StreamFadeTracker,
133 fade_tick: Option<Task<()>>,
134 format: TextViewFormat,
137 text: String,
138 element_text: Option<SharedString>,
141 revision: usize,
142 committed_revision: usize,
144 full_update_revision: usize,
147 rendered_index: Arc<OnceLock<RenderedIndex>>,
149 range_highlights: Option<Arc<RangeHighlightFrame>>,
150 pub(super) pending_reveal: Option<PendingReveal>,
151 pub(super) selection_revision: usize,
152 compatible_layout_update: bool,
153 layout_text_style: Option<(gpui::TextStyle, Pixels)>,
154 parsed_error: Option<SharedString>,
155 tx: Sender<UpdateOptions>,
156 _parse_task: Task<()>,
157 _receive_task: Task<()>,
158}
159
160impl TextViewState {
161 pub fn markdown(text: &str, cx: &mut Context<Self>) -> Self {
163 Self::new(TextViewFormat::Markdown, text, cx)
164 }
165
166 pub fn html(text: &str, cx: &mut Context<Self>) -> Self {
168 Self::new(TextViewFormat::Html, text, cx)
169 }
170
171 fn new(format: TextViewFormat, text: &str, cx: &mut Context<Self>) -> Self {
173 let focus_handle = cx.focus_handle();
174 let selection_adapter = TextViewSelectionAdapter::new(cx.entity().downgrade(), cx);
175
176 let (tx, rx) = unbounded::<UpdateOptions>();
177 let (tx_result, rx_result) = unbounded::<ParsedUpdate>();
178 let _receive_task = cx.spawn({
179 async move |weak_self, cx| {
180 while let Ok(parsed_update) = rx_result.recv().await {
181 let Ok(commits) =
186 weak_self.read_with(cx, |state, _| state.commits(&parsed_update))
187 else {
188 break;
189 };
190 if !commits {
191 continue;
192 }
193 _ = weak_self.update(cx, |state, cx| {
194 state.commit_parsed_update(parsed_update, cx);
195 });
196 }
197 }
198 });
199
200 let _parse_task = cx.background_spawn(UpdateFuture::new(format, rx, tx_result));
201
202 let mut this = Self {
203 entity_id: cx.entity_id(),
204 focus_handle,
205 bounds: Bounds::default(),
206 multi_click_selection: None,
207 selected_text_override: None,
208 select_all: false,
209 selectable: false,
210 selection_format: SelectionFormat::default(),
211 scrollable: false,
212 max_lines: None,
213 line_spans: Arc::default(),
214 clamped: false,
215 list_state: ListState::new(0, gpui::ListAlignment::Top, px(1000.)).measure_all(),
220 text_view_style: Arc::default(),
221 code_block_actions: None,
222 code_block_highlighter: None,
223 table_actions: None,
224 link_click_handler: None,
225 image_source: None,
226 markdown_extensions: Arc::default(),
227 is_selecting: false,
228 preserve_inline_selection: false,
229 auto_scroll: AutoScroll::default(),
230 selection_adapter,
231 parsed_content: Default::default(),
232 stream_fade: StreamFadeTracker::default(),
233 fade_tick: None,
234 format,
235 parsed_error: None,
236 text: text.to_string(),
237 element_text: None,
238 revision: 0,
239 committed_revision: 0,
240 full_update_revision: 0,
241 rendered_index: Arc::default(),
242 range_highlights: None,
243 pending_reveal: None,
244 selection_revision: 0,
245 compatible_layout_update: false,
246 layout_text_style: None,
247 tx,
248 _parse_task,
249 _receive_task,
250 };
251 this.increment_update(&text, false, cx);
252 this
253 }
254
255 pub(crate) fn source(&self) -> SharedString {
257 self.parsed_content.document.source.clone()
258 }
259
260 pub fn selectable(mut self, selectable: bool) -> Self {
262 self.selectable = selectable;
263 self
264 }
265
266 pub fn set_selectable(&mut self, selectable: bool, cx: &mut Context<Self>) {
268 self.selectable = selectable;
269 cx.notify();
270 }
271
272 pub fn selection_format(mut self, selection_format: SelectionFormat) -> Self {
274 self.selection_format = selection_format;
275 self
276 }
277
278 pub fn set_selection_format(
280 &mut self,
281 selection_format: SelectionFormat,
282 cx: &mut Context<Self>,
283 ) {
284 self.selection_format = selection_format;
285 cx.notify();
286 }
287
288 pub fn scrollable(mut self, scrollable: bool) -> Self {
290 self.scrollable = scrollable;
291 self
292 }
293
294 pub fn set_scrollable(&mut self, scrollable: bool, cx: &mut Context<Self>) {
296 if !scrollable {
297 self.reset_selection_and_adapter(cx);
298 }
299 self.scrollable = scrollable;
300 cx.notify();
301 }
302
303 pub fn is_clamped(&self) -> bool {
306 self.clamped
307 }
308
309 pub fn set_text(&mut self, text: &str, cx: &mut Context<Self>) {
316 if self.text.as_str() == text {
317 return;
318 }
319 if self.format == TextViewFormat::Markdown
323 && self.parsed_error.is_none()
324 && !self.text.is_empty()
325 && let Some(delta) = text.strip_prefix(self.text.as_str())
326 {
327 self.push_str(delta, cx);
328 return;
329 }
330 self.stream_fade.note_replace();
331
332 self.text.clear();
333 self.text.push_str(text);
334 self.parsed_error = None;
335 self.increment_update(text, false, cx);
336 }
337
338 pub(super) fn set_element_text(&mut self, text: &SharedString, cx: &mut Context<Self>) {
343 let same_string = self.element_text.as_ref().is_some_and(|current| {
344 current.as_ptr() == text.as_ptr() && current.len() == text.len()
345 });
346 if same_string && self.text.len() == text.len() {
347 return;
348 }
349 self.element_text = Some(text.clone());
350 self.set_text(text, cx);
351 }
352
353 pub fn push_str(&mut self, new_text: &str, cx: &mut Context<Self>) {
355 if new_text.is_empty() {
356 return;
357 }
358 self.stream_fade.note_extend(self.text.len());
359 self.text.push_str(new_text);
360 self.increment_update(new_text, true, cx);
361 }
362
363 pub fn motion(mut self, motion: TextViewMotion) -> Self {
365 self.set_motion(motion);
366 self
367 }
368
369 pub fn set_motion(&mut self, motion: TextViewMotion) {
371 self.stream_fade.set_motion(motion);
372 }
373
374 pub(crate) fn set_markdown_extensions(
375 &mut self,
376 markdown_extensions: Arc<MarkdownExtensions>,
377 cx: &mut Context<Self>,
378 ) {
379 if self.markdown_extensions.revision() == markdown_extensions.revision() {
380 return;
381 }
382
383 let parser_configuration_changed = !self
384 .markdown_extensions
385 .has_same_parser_configuration(&markdown_extensions);
386 self.markdown_extensions = markdown_extensions;
387 if parser_configuration_changed && self.format == TextViewFormat::Markdown {
388 let text = self.text.clone();
389 self.increment_update(&text, false, cx);
390 }
391 }
392
393 pub fn selected_text(&self) -> String {
395 self.selected_text_in(None)
396 }
397
398 pub fn selected_source_range(&self) -> Option<Range<usize>> {
409 if self.format != TextViewFormat::Markdown {
410 return None;
411 }
412 if self.select_all {
413 return Some(0..self.source().len());
414 }
415 self.parsed_content.document.selected_source_range()
416 }
417
418 fn effective_format(&self) -> SelectionFormat {
428 match self.format {
429 TextViewFormat::Markdown => self.selection_format,
430 TextViewFormat::Html => SelectionFormat::Plain,
431 }
432 }
433
434 pub(super) fn selected_text_in(&self, blocks: Option<RangeInclusive<usize>>) -> String {
441 let format = self.effective_format();
442
443 if self.select_all {
444 if format == SelectionFormat::Source {
445 return self.source().to_string();
446 }
447
448 return self.parsed_content.document.text();
449 }
450
451 if format != SelectionFormat::Source
456 && let Some(text) = &self.selected_text_override
457 {
458 return text.clone();
459 }
460
461 self.parsed_content.document.selected_text(format, blocks)
462 }
463
464 fn invalidate_measured_heights(&self) {
480 let count = self.list_state.item_count();
481 if count > 0 {
482 self.list_state.remeasure_items(0..count);
483 }
484 }
485
486 pub fn invalidate_inline_layout(&mut self, cx: &mut Context<Self>) {
492 self.preserve_selection_for_reflow();
493 cx.notify();
494 }
495
496 pub(super) fn preserve_selection_for_reflow(&mut self) {
497 self.preserve_inline_selection = true;
498 self.compatible_layout_update = true;
499 self.invalidate_measured_heights();
500 }
501
502 fn increment_update(&mut self, text: &str, append: bool, cx: &mut Context<Self>) {
503 self.revision += 1;
504 if !append {
505 self.full_update_revision = self.revision;
506 self.selection_revision = self.selection_revision.wrapping_add(1);
507 }
508 let parse_synchronously = !append && text.len() <= MAX_SYNC_FULL_REPLACE_BYTES;
509 let update_options = UpdateOptions {
510 revision: self.revision,
511 append,
512 mode: if append {
513 ParseMode::Compatible
514 } else if parse_synchronously {
515 ParseMode::BaselineAck
516 } else {
517 ParseMode::Replace
518 },
519 pending_text: text.to_string(),
520 markdown_extensions: self.markdown_extensions.clone(),
521 };
522
523 if parse_synchronously {
527 match parse_content(self.format, ParsedContent::default(), &update_options) {
528 Ok(content) => {
529 self.reconcile_range_highlights(&content.document, self.revision, false);
530 self.stream_fade.record(
531 &self.parsed_content.document,
532 &content.document,
533 Instant::now(),
534 );
535 self.parsed_content = content;
536 self.parsed_error = None;
537 self.invalidate_measured_heights();
538 if !self.is_selecting {
539 self.reset_selection_and_adapter(cx);
540 }
541 }
542 Err(err) => {
543 self.stream_fade.discard_pending();
544 self.parsed_error = Some(err);
545 }
546 }
547 _ = self.tx.try_send(update_options);
551 cx.notify();
552 return;
553 }
554
555 _ = self.tx.try_send(update_options);
556 }
557
558 fn commits(&self, parsed_update: &ParsedUpdate) -> bool {
562 if parsed_update.revision < self.full_update_revision
563 || parsed_update.revision <= self.committed_revision
564 {
565 return false;
566 }
567 debug_assert!(!parsed_update.baseline_ack || parsed_update.full_parse);
568 !parsed_update.baseline_ack
569 }
570
571 fn commit_parsed_update(&mut self, parsed_update: ParsedUpdate, cx: &mut Context<Self>) {
580 if !self.commits(&parsed_update) {
581 return;
582 }
583
584 match parsed_update.result {
585 Ok(content) => {
586 let append = parsed_update.selection_compatible && !parsed_update.full_parse;
587 if append && self.full_update_revision <= self.committed_revision {
588 self.splice_appended_blocks(&content.document);
589 }
590 self.reconcile_range_highlights(&content.document, parsed_update.revision, append);
591 self.stream_fade.record(
592 &self.parsed_content.document,
593 &content.document,
594 Instant::now(),
595 );
596 if parsed_update.revision < self.revision {
600 self.stream_fade.note_extend(content.document.source.len());
601 }
602 self.parsed_content = content;
603 self.parsed_error = None;
604 self.compatible_layout_update = parsed_update.selection_compatible;
605 if parsed_update.full_parse {
606 self.invalidate_measured_heights();
607 }
608 }
609 Err(err) => {
610 self.stream_fade.discard_pending();
611 self.parsed_error = Some(err);
612 }
613 }
614 if !parsed_update.selection_compatible && !self.is_selecting {
618 self.reset_selection_and_adapter(cx);
619 }
620 cx.notify();
621 }
622
623 pub fn rendered_text(&self) -> RenderedText {
628 RenderedText::new(
629 self.entity_id,
630 self.committed_revision,
631 self.parsed_content.document.clone(),
632 self.rendered_index.clone(),
633 )
634 }
635
636 pub fn set_range_highlights(
652 &mut self,
653 highlights: impl IntoIterator<Item = RangeHighlight>,
654 cx: &mut Context<Self>,
655 ) -> Result<(), RangeHighlightError> {
656 if self.format != TextViewFormat::Markdown {
657 return Err(RangeHighlightError::Unsupported);
658 }
659 let text = self.rendered_text();
660 self.range_highlights = RangeHighlightFrame::new(&text, highlights)?.map(Arc::new);
661 cx.notify();
662 Ok(())
663 }
664
665 pub fn reveal_range(
688 &mut self,
689 range: Range<usize>,
690 cx: &mut Context<Self>,
691 ) -> Result<(), RangeHighlightError> {
692 if self.format != TextViewFormat::Markdown {
693 return Err(RangeHighlightError::Unsupported);
694 }
695 let text = self.rendered_text();
696 if text.is_empty() && range == (0..0) {
697 self.pending_reveal = None;
699 return Ok(());
700 }
701 let now = cx.background_executor().now();
702 self.pending_reveal = Some(
703 PendingReveal::new(&text, &range, now).ok_or(RangeHighlightError::InvalidRange(0))?,
704 );
705 cx.notify();
706 Ok(())
707 }
708
709 pub fn clear_range_highlights(&mut self, cx: &mut Context<Self>) {
711 if self.range_highlights.take().is_some() {
712 cx.notify();
713 }
714 }
715
716 fn reconcile_range_highlights(&mut self, new: &ParsedDocument, revision: usize, append: bool) {
724 let tail_only = append && self.full_update_revision <= self.committed_revision;
725 self.committed_revision = revision;
726 self.rendered_index = Arc::default();
727 if self.range_highlights.is_some() || self.pending_reveal.is_some() {
728 let remap = LeafRemap::new(&self.parsed_content.document, new, tail_only);
729 self.range_highlights = self
730 .range_highlights
731 .take()
732 .and_then(|highlights| highlights.remap(&remap))
733 .map(Arc::new);
734 self.pending_reveal = self
735 .pending_reveal
736 .take()
737 .and_then(|reveal| reveal.remap(&remap));
738 }
739 }
740
741 fn splice_appended_blocks(&self, new: &ParsedDocument) {
751 let old = &self.parsed_content.document.blocks;
752 let (old_count, new_count) = (old.len(), new.blocks.len());
753 if old_count == 0 || new_count <= old_count || self.list_state.item_count() != old_count {
754 return;
755 }
756
757 let unchanged = old
760 .iter()
761 .zip(new.blocks.iter())
762 .take(old_count - 1)
763 .take_while(|(old_block, new_block)| {
764 old_block
765 .span()
766 .is_some_and(|span| new_block.span() == Some(span))
767 })
768 .count();
769 self.list_state.remeasure_items(unchanged..old_count);
773 self.list_state
774 .splice(old_count..old_count, new_count - old_count);
775 }
776
777 pub(super) fn update_bounds(&mut self, bounds: Bounds<Pixels>, _cx: &mut App) {
779 self.bounds = bounds;
780 }
781
782 pub(super) fn block_ix_at(&self, content_y: Pixels) -> Option<usize> {
790 if !self.scrollable {
791 return None;
792 }
793
794 let origin = self.bounds.origin.y + self.scroll_offset().y;
795 let count = self.list_state.item_count();
796 let mut ix = self.list_state.logical_scroll_top().item_ix;
797 while ix < count {
798 let bounds = self.list_state.bounds_for_item(ix)?;
799 if content_y < bounds.bottom() - origin {
800 return Some(ix);
801 }
802 ix += 1;
803 }
804
805 count.checked_sub(1)
806 }
807
808 #[doc(hidden)]
809 pub fn bounds(&self) -> Bounds<Pixels> {
810 self.bounds
811 }
812
813 #[doc(hidden)]
814 pub fn list_state(&self) -> &ListState {
815 &self.list_state
816 }
817
818 #[doc(hidden)]
819 pub fn is_selecting(&self) -> bool {
820 self.is_selecting
821 }
822
823 #[doc(hidden)]
824 pub fn focus_handle(&self) -> &FocusHandle {
825 &self.focus_handle
826 }
827
828 pub(super) fn has_view_selection(&self) -> bool {
831 self.select_all
832 || self.multi_click_selection.is_some()
833 || self.selected_text_override.is_some()
834 }
835
836 pub(super) fn stop_auto_scroll(&mut self) {
837 self.auto_scroll.stop();
838 }
839
840 pub(super) fn reset_selection(&mut self) {
841 self.preserve_inline_selection = false;
842 self.multi_click_selection = None;
843 self.selected_text_override = None;
844 self.select_all = false;
845 self.is_selecting = false;
846 self.auto_scroll.stop();
847 self.parsed_content.document.clear_selection();
851 }
852
853 fn reset_selection_and_adapter(&mut self, cx: &mut App) {
854 self.reset_selection();
855 self.selection_adapter.set_local_selection(false, cx);
856 }
857
858 pub fn clear_selection(&mut self, cx: &mut Context<Self>) {
860 self.reset_selection_and_adapter(cx);
861 cx.notify();
862 }
863
864 pub(super) fn scroll_offset(&self) -> Point<Pixels> {
865 if self.scrollable {
866 self.list_state.scroll_px_offset_for_scrollbar()
867 } else {
868 Point::default()
869 }
870 }
871
872 pub fn select_all(&mut self, cx: &mut Context<Self>) {
874 self.multi_click_selection = None;
875 self.selected_text_override = None;
876 self.select_all = true;
877 self.is_selecting = false;
878 self.auto_scroll.stop();
879 self.selection_adapter.set_local_selection(true, cx);
880 cx.notify();
881 }
882
883 pub(crate) fn set_multi_click_selection(
884 &mut self,
885 pos: Point<Pixels>,
886 kind: TextViewMultiClickKind,
887 selected_text: String,
888 cx: &mut App,
889 ) {
890 self.preserve_inline_selection = false;
891 let scroll_offset = self.scroll_offset();
892 let pos = pos - self.bounds.origin - scroll_offset;
893 self.multi_click_selection = Some(TextViewMultiClickSelection {
894 pos,
895 kind,
896 line_bounds: None,
897 });
898 self.selected_text_override = Some(selected_text);
899 self.select_all = false;
900 self.is_selecting = false;
901 self.auto_scroll.stop();
902 self.selection_adapter.set_local_selection(true, cx);
903 }
904
905 pub(crate) fn set_multi_click_line(&mut self, bounds: Bounds<Pixels>, cx: &mut App) {
906 self.set_multi_click_selection(
907 bounds.center(),
908 TextViewMultiClickKind::Line,
909 String::new(),
910 cx,
911 );
912 self.selected_text_override = None;
913 let offset = self.bounds.origin + self.scroll_offset();
914 if let Some(selection) = self.multi_click_selection.as_mut() {
915 selection.line_bounds = Some(Bounds::new(bounds.origin - offset, bounds.size));
916 }
917 }
918
919 pub(super) fn set_auto_scroll(&mut self, delta: Option<Pixels>, cx: &mut Context<Self>) {
920 self.auto_scroll.set(delta, cx, |delta, state, cx| {
921 state.list_state.scroll_by(delta);
922 cx.notify();
923 });
924 }
925
926 pub(crate) fn selection_points(&self, cx: &App) -> Option<(Point<Pixels>, Point<Pixels>)> {
933 if !self.selectable {
934 return None;
935 }
936 self.selection_adapter.selection_points(cx)
937 }
938
939 pub(crate) fn has_selection(&self, cx: &App) -> bool {
940 self.has_view_selection() || self.selection_points(cx).is_some()
941 }
942
943 pub(super) fn on_action_select_all(
944 &mut self,
945 _: &SelectAll,
946 _: &mut Window,
947 cx: &mut Context<Self>,
948 ) {
949 if !self.selectable {
950 cx.propagate();
951 return;
952 }
953
954 self.select_all(cx);
955 }
956
957 pub(crate) fn is_selectable(&self) -> bool {
958 self.selectable
959 }
960
961 pub(crate) fn is_all_selected(&self) -> bool {
962 self.select_all
963 }
964
965 pub(crate) fn multi_click_selection(&self) -> Option<TextViewMultiClickSelection> {
966 let scroll_offset = self.scroll_offset();
967 self.multi_click_selection.map(|selection| {
968 let pos = selection.pos + scroll_offset + self.bounds.origin;
969 let line_bounds = selection.line_bounds.map(|bounds| {
970 Bounds::new(
971 bounds.origin + scroll_offset + self.bounds.origin,
972 bounds.size,
973 )
974 });
975 TextViewMultiClickSelection {
976 pos,
977 line_bounds,
978 ..selection
979 }
980 })
981 }
982}
983
984#[derive(Clone, Copy, Debug, PartialEq)]
985pub(crate) struct TextViewMultiClickSelection {
986 pub(crate) pos: Point<Pixels>,
987 pub(crate) kind: TextViewMultiClickKind,
988 pub(crate) line_bounds: Option<Bounds<Pixels>>,
989}
990
991#[derive(Clone, Copy, Debug, PartialEq, Eq)]
992pub(crate) enum TextViewMultiClickKind {
993 Word,
994 Paragraph,
995 Line,
996}
997
998impl TextViewState {
999 fn reveal_frame(
1003 &mut self,
1004 now: Instant,
1005 window: &mut Window,
1006 ) -> (Option<RevealRequest>, Option<usize>) {
1007 if self.max_lines.is_some()
1008 || self
1009 .pending_reveal
1010 .as_ref()
1011 .is_some_and(|reveal| reveal.is_expired(now))
1012 {
1013 self.pending_reveal = None;
1014 }
1015 let Some(pending) = &self.pending_reveal else {
1016 return (None, None);
1017 };
1018
1019 let block_ix = pending.block_ix(&self.parsed_content.document);
1020 let block_off_screen = |ix: usize| {
1021 let viewport = self.list_state.viewport_bounds();
1022 self.list_state.bounds_for_item(ix).is_none_or(|item| {
1023 item.bottom() <= viewport.top() || item.top() >= viewport.bottom()
1024 })
1025 };
1026 let reveal_block = block_ix.filter(|ix| {
1027 self.scrollable
1028 && (pending.is_block() || !pending.was_laid_out())
1029 && block_off_screen(*ix)
1030 });
1031 let request = pending.request();
1032 if pending.is_block() {
1033 self.pending_reveal = None;
1035 } else {
1036 window.request_animation_frame();
1037 }
1038 (request, reveal_block)
1039 }
1040}
1041
1042impl Render for TextViewState {
1043 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1044 let typography = (window.text_style(), window.rem_size());
1045 let body_font_size = typography.0.font_size.to_pixels(typography.1);
1046 if self
1047 .layout_text_style
1048 .as_ref()
1049 .is_some_and(|previous| previous != &typography)
1050 {
1051 self.preserve_selection_for_reflow();
1055 }
1056 self.layout_text_style = Some(typography);
1057 let state = cx.entity();
1058 let stream_fade = self.stream_fade.frame(Instant::now(), cx.reduce_motion());
1059 if stream_fade.is_some() && self.fade_tick.is_none() {
1060 self.fade_tick = Some(cx.spawn(async move |this, cx| {
1061 cx.background_executor().timer(STREAM_FADE_TICK).await;
1062 _ = this.update(cx, |state, cx| {
1063 state.fade_tick = None;
1064 cx.notify();
1065 });
1066 }));
1067 }
1068 let (reveal, reveal_block) = self.reveal_frame(cx.background_executor().now(), window);
1069 let node_cx = NodeContext {
1071 offset: self.parsed_content.node_cx.offset,
1072 link_refs: self.parsed_content.node_cx.link_refs.clone(),
1073 style: self.text_view_style.clone(),
1074 code_block_actions: self.code_block_actions.clone(),
1075 code_block_highlighter: self.code_block_highlighter.clone(),
1076 table_actions: self.table_actions.clone(),
1077 link_click_handler: self.link_click_handler.clone(),
1078 image_source: self.image_source.clone(),
1079 markdown_extensions: self.markdown_extensions.clone(),
1080 stream_fade,
1081 range_highlights: self.range_highlights.clone(),
1082 reveal,
1083 body_font_size,
1084 };
1085
1086 let content = v_flex()
1087 .w_full()
1088 .when(self.max_lines.is_none(), |this| this.h_full())
1091 .map(|this| match &self.parsed_error {
1092 None => this.child(self.parsed_content.document.render_root(
1093 if self.scrollable {
1094 Some(self.list_state.clone())
1095 } else {
1096 None
1097 },
1098 &node_cx,
1099 window,
1100 cx,
1101 )),
1102 Some(err) => this.child(
1103 v_flex()
1104 .gap_1()
1105 .child("Failed to parse content")
1106 .child(err.to_string()),
1107 ),
1108 })
1109 .on_prepaint(move |bounds, window, cx| {
1110 let (
1111 size_changed,
1112 selection_involves_view,
1113 has_selection_snapshot,
1114 is_selecting,
1115 compatible_layout_update,
1116 preserve_width_selection,
1117 ) = {
1118 let state = state.read(cx);
1119 let has_selection_snapshot = state.selection_adapter.has_selection_snapshot(cx);
1120 let preserve_width_selection = !state.is_selecting
1121 && state.bounds().size.width != bounds.size.width
1122 && (has_selection_snapshot || state.has_view_selection());
1123 (
1124 state.bounds().size != bounds.size,
1125 state.selection_adapter.is_part_of_window_selection(cx),
1126 has_selection_snapshot,
1127 state.is_selecting,
1128 state.compatible_layout_update
1131 || preserve_width_selection
1132 || state.preserve_inline_selection,
1133 preserve_width_selection,
1134 )
1135 };
1136 let mut revision_changed = false;
1137 state.update(cx, |state, cx| {
1138 revision_changed = state
1139 .selection_adapter
1140 .update_layout_revision(state.selection_revision, state.is_selecting);
1141 if preserve_width_selection {
1142 state.preserve_inline_selection = true;
1145 }
1146 state.update_bounds(bounds, cx);
1147 state.compatible_layout_update = false;
1148 });
1149 if !is_selecting
1150 && ((size_changed && selection_involves_view && !compatible_layout_update)
1151 || (revision_changed && has_selection_snapshot))
1152 {
1153 TextSelection::clear(window, cx);
1154 }
1155 });
1156 if let Some(block_ix) = reveal_block {
1159 self.list_state.scroll_to_reveal_item(block_ix);
1160 }
1161 content
1162 }
1163}
1164
1165#[derive(Clone, PartialEq, Default)]
1166pub(crate) struct ParsedContent {
1167 pub(crate) document: ParsedDocument,
1168 pub(crate) node_cx: node::NodeContext,
1169}
1170
1171struct UpdateFuture {
1172 format: TextViewFormat,
1173 content: ParsedContent,
1174 rx: Pin<Box<Receiver<UpdateOptions>>>,
1175 tx_result: Sender<ParsedUpdate>,
1176}
1177
1178impl UpdateFuture {
1179 fn new(
1180 format: TextViewFormat,
1181 rx: Receiver<UpdateOptions>,
1182 tx_result: Sender<ParsedUpdate>,
1183 ) -> Self {
1184 Self {
1185 format,
1186 content: Default::default(),
1187 rx: Box::pin(rx),
1188 tx_result,
1189 }
1190 }
1191}
1192
1193impl Future for UpdateFuture {
1194 type Output = ();
1195
1196 fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
1197 loop {
1198 match self.rx.as_mut().poll_next(cx) {
1199 Poll::Ready(Some(mut options)) => {
1200 let hit_coalesce_budget =
1201 merge_pending_options(&mut options, self.rx.as_ref().get_ref());
1202
1203 let res = parse_content(self.format, self.content.clone(), &options);
1204 if let Ok(content) = &res {
1205 self.content = content.clone();
1206 }
1207 _ = self.tx_result.try_send(ParsedUpdate {
1208 revision: options.revision,
1209 full_parse: !options.append,
1210 selection_compatible: options.mode == ParseMode::Compatible,
1211 baseline_ack: options.mode == ParseMode::BaselineAck,
1212 result: res,
1213 });
1214 if hit_coalesce_budget {
1215 cx.waker().wake_by_ref();
1216 return Poll::Pending;
1217 }
1218 continue;
1219 }
1220 Poll::Ready(None) => return Poll::Ready(()),
1221 Poll::Pending => return Poll::Pending,
1222 }
1223 }
1224 }
1225}
1226
1227#[derive(Clone)]
1228struct UpdateOptions {
1229 revision: usize,
1230 pending_text: String,
1231 append: bool,
1232 mode: ParseMode,
1233 markdown_extensions: Arc<MarkdownExtensions>,
1234}
1235
1236impl UpdateOptions {
1237 fn merge(&mut self, next: UpdateOptions) {
1238 if next.append {
1239 self.pending_text.push_str(&next.pending_text);
1240 self.revision = next.revision;
1241 if self.mode != ParseMode::Replace {
1242 self.mode = ParseMode::Compatible;
1243 }
1244 } else {
1245 *self = next;
1246 }
1247 }
1248}
1249
1250struct ParsedUpdate {
1251 revision: usize,
1252 full_parse: bool,
1253 selection_compatible: bool,
1254 baseline_ack: bool,
1255 result: Result<ParsedContent, SharedString>,
1256}
1257
1258#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1259enum ParseMode {
1260 BaselineAck,
1261 Replace,
1262 Compatible,
1263}
1264
1265fn merge_pending_options(options: &mut UpdateOptions, rx: &Receiver<UpdateOptions>) -> bool {
1266 let mut update_count = 1;
1267
1268 while update_count < MAX_COALESCED_UPDATES_PER_PARSE {
1269 match rx.try_recv() {
1270 Ok(next_options) => {
1271 options.merge(next_options);
1272 update_count += 1;
1273 }
1274 Err(_) => return false,
1275 }
1276 }
1277
1278 true
1279}
1280
1281fn parse_content(
1282 format: TextViewFormat,
1283 mut content: ParsedContent,
1284 options: &UpdateOptions,
1285) -> Result<ParsedContent, SharedString> {
1286 let mut node_cx = NodeContext {
1287 markdown_extensions: options.markdown_extensions.clone(),
1288 ..NodeContext::default()
1289 };
1290
1291 let last_span = options
1297 .append
1298 .then(|| {
1299 content
1300 .document
1301 .blocks
1302 .last()
1303 .and_then(|block| block.span())
1304 })
1305 .flatten();
1306
1307 let mut source = String::new();
1308 if let Some(span) = last_span {
1309 Arc::make_mut(&mut content.document.blocks).pop();
1310 node_cx.offset = span.start;
1311 source.push_str(&content.document.source[span.start..]);
1312 source.push_str(&options.pending_text);
1313 } else {
1314 if options.append {
1315 node_cx.offset = content.document.source.len();
1316 }
1317 source.push_str(&options.pending_text);
1318 }
1319
1320 let new_document = match format {
1321 TextViewFormat::Markdown => format::markdown::parse(&source, &mut node_cx),
1322 TextViewFormat::Html => format::html::parse(&source, &mut node_cx),
1323 }?;
1324
1325 if options.append {
1326 content.document.source =
1327 format!("{}{}", content.document.source, options.pending_text).into();
1328 Arc::make_mut(&mut content.document.blocks)
1329 .extend(Arc::unwrap_or_clone(new_document.blocks));
1330 } else {
1331 content.document = new_document;
1332 }
1333
1334 Ok(content)
1335}
1336
1337#[cfg(test)]
1338mod tests {
1339 use super::*;
1340 use crate::text::{
1341 MarkdownNode,
1342 node::{BlockNode, Span},
1343 };
1344 use gpui::{Entity, TestAppContext};
1345
1346 #[cfg(feature = "gpui-fast")]
1349 mod parse_ack {
1350 use std::{cell::Cell, rc::Rc};
1351
1352 use gpui::{
1353 AppContext as _, Context, Entity, IntoElement, ParentElement as _, Render,
1354 TestAppContext, Window, div,
1355 };
1356
1357 use super::super::TextViewState;
1358
1359 struct Reader {
1360 state: Entity<TextViewState>,
1361 renders: Rc<Cell<usize>>,
1362 }
1363
1364 impl Render for Reader {
1365 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1366 self.renders.set(self.renders.get() + 1);
1367 div().child(self.state.read(cx).text.clone())
1368 }
1369 }
1370
1371 struct Holder {
1372 reader: Entity<Reader>,
1373 }
1374
1375 impl Render for Holder {
1376 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1377 div().child(self.reader.clone())
1378 }
1379 }
1380
1381 #[gpui::test]
1385 fn acknowledging_a_parse_leaves_its_readers_retained(cx: &mut TestAppContext) {
1386 cx.update(crate::init);
1387 let renders = Rc::new(Cell::new(0));
1388 let (holder, cx) = cx.add_window_view(|_, cx| Holder {
1389 reader: cx.new(|cx| Reader {
1390 state: cx.new(|cx| TextViewState::markdown("Some **text**.", cx)),
1391 renders: renders.clone(),
1392 }),
1393 });
1394 cx.update(|window, cx| window.draw(cx).clear(cx));
1395 let before = renders.get();
1396 cx.run_until_parked();
1397 holder.update(cx, |_, cx| cx.notify());
1398 cx.update(|window, cx| window.draw(cx).clear(cx));
1399 assert_eq!(renders.get(), before, "the reader rendered again");
1400 }
1401 }
1402
1403 mod stream_fade {
1404 use std::{cell::Cell, ops::Range, rc::Rc, time::Duration};
1405
1406 use gpui::{Entity, TestAppContext};
1407
1408 use super::super::*;
1409 use crate::{motion::Easing, text::stream_fade::TextLeafKey};
1410
1411 const FADE: Duration = Duration::from_secs(10);
1414
1415 fn fading_state(markdown: &str, cx: &mut TestAppContext) -> Entity<TextViewState> {
1416 cx.update(crate::init);
1417 let state = cx.update(|cx| {
1418 cx.new(|cx| {
1419 TextViewState::markdown(markdown, cx).motion(
1420 TextViewMotion::default()
1421 .with_stream_fade(FADE)
1422 .with_stream_fade_easing(Easing::Linear),
1423 )
1424 })
1425 });
1426 cx.run_until_parked();
1427 state
1428 }
1429
1430 fn fades(
1433 state: &Entity<TextViewState>,
1434 key: TextLeafKey,
1435 cx: &mut TestAppContext,
1436 ) -> Option<Vec<Range<usize>>> {
1437 state.update(cx, |state, _| {
1438 let frame = state.stream_fade.frame(Instant::now(), false)?;
1439 let fades = frame.fades(key)?;
1440 assert!(
1441 fades.iter().all(|(_, fade_out)| *fade_out > 0.9),
1442 "a fade sampled right after it starts is still transparent: {fades:?}"
1443 );
1444 Some(fades.iter().map(|(range, _)| range.clone()).collect())
1445 })
1446 }
1447
1448 #[gpui::test]
1449 fn push_str_fades_only_the_appended_text(cx: &mut TestAppContext) {
1450 let state = fading_state("hello", cx);
1451 state.update(cx, |state, cx| state.push_str(" world", cx));
1452 cx.run_until_parked();
1453
1454 assert_eq!(fades(&state, TextLeafKey::block(0), cx), Some(vec![5..11]));
1455
1456 state.update(cx, |state, _| {
1457 let later = Instant::now() + FADE + Duration::from_secs(1);
1458 assert!(state.stream_fade.frame(later, false).is_none());
1459 assert!(state.stream_fade.frame(Instant::now(), false).is_none());
1460 });
1461 }
1462
1463 #[gpui::test]
1464 fn overtaken_parse_preserves_the_remaining_chunks_fade(cx: &mut TestAppContext) {
1465 let state = fading_state("hello", cx);
1466 let parsed = super::push_and_parse(&state, " one", cx);
1467 state.update(cx, |state, cx| {
1468 state.push_str(" two", cx);
1469 state.commit_parsed_update(parsed, cx);
1470 });
1471 assert_eq!(fades(&state, TextLeafKey::block(0), cx), Some(vec![5..9]));
1472
1473 cx.run_until_parked();
1474 assert_eq!(
1475 fades(&state, TextLeafKey::block(0), cx),
1476 Some(vec![5..9, 9..13])
1477 );
1478 }
1479
1480 #[gpui::test]
1481 fn overtaken_parse_preserves_a_remaining_blocks_fade(cx: &mut TestAppContext) {
1482 let state = fading_state("hello", cx);
1483 let parsed = super::push_and_parse(&state, " one", cx);
1484 state.update(cx, |state, cx| {
1485 state.push_str("\n\nsecond", cx);
1486 state.commit_parsed_update(parsed, cx);
1487 });
1488 assert_eq!(fades(&state, TextLeafKey::block(0), cx), Some(vec![5..9]));
1489
1490 state.update(cx, |state, cx| state.push_str(" third", cx));
1492 cx.run_until_parked();
1493 let ranges = fades(&state, TextLeafKey::block(11), cx).expect("new paragraph fades");
1494 assert_eq!(
1495 ranges.into_iter().flatten().collect::<Vec<_>>(),
1496 (0..12).collect::<Vec<_>>()
1497 );
1498 }
1499
1500 #[gpui::test]
1501 fn set_text_extending_the_text_fades_like_push_str(cx: &mut TestAppContext) {
1502 let state = fading_state("hello", cx);
1503 state.update(cx, |state, cx| state.set_text("hello world", cx));
1504 cx.run_until_parked();
1505
1506 assert_eq!(fades(&state, TextLeafKey::block(0), cx), Some(vec![5..11]));
1507 }
1508
1509 #[gpui::test]
1510 fn set_text_replacing_the_text_shows_it_at_once(cx: &mut TestAppContext) {
1511 let state = fading_state("hello", cx);
1512 state.update(cx, |state, cx| state.push_str(" world", cx));
1513 cx.run_until_parked();
1514 assert!(fades(&state, TextLeafKey::block(0), cx).is_some());
1515
1516 state.update(cx, |state, cx| state.set_text("other", cx));
1517 cx.run_until_parked();
1518
1519 assert_eq!(fades(&state, TextLeafKey::block(0), cx), None);
1520 }
1521
1522 #[gpui::test]
1523 fn completed_markup_refades_from_the_divergence(cx: &mut TestAppContext) {
1524 let state = fading_state("text ", cx);
1527 state.update(cx, |state, cx| state.push_str("**bo", cx));
1528 cx.run_until_parked();
1529 assert_eq!(fades(&state, TextLeafKey::block(0), cx), Some(vec![4..9]));
1530
1531 state.update(cx, |state, cx| state.push_str("ld**", cx));
1534 cx.run_until_parked();
1535 assert_eq!(
1536 fades(&state, TextLeafKey::block(0), cx),
1537 Some(vec![4..5, 5..9])
1538 );
1539 }
1540
1541 #[gpui::test]
1542 fn without_stagger_an_update_fades_as_one_chunk(cx: &mut TestAppContext) {
1543 let state = fading_state("hello", cx);
1544 state.update(cx, |state, cx| state.push_str(" one two three", cx));
1545 cx.run_until_parked();
1546
1547 assert_eq!(fades(&state, TextLeafKey::block(0), cx), Some(vec![5..19]));
1548 }
1549
1550 #[gpui::test]
1551 fn words_of_one_update_start_one_after_another(cx: &mut TestAppContext) {
1552 let stagger = Duration::from_millis(100);
1553 let state = fading_state("hello", cx);
1554 state.update(cx, |state, _| {
1555 state.set_motion(
1556 TextViewMotion::default()
1557 .with_stream_fade(FADE)
1558 .with_stream_fade_stagger(stagger)
1559 .with_stream_fade_easing(Easing::Linear),
1560 )
1561 });
1562 state.update(cx, |state, cx| state.push_str(" one two three", cx));
1563 cx.run_until_parked();
1564
1565 state.update(cx, |state, _| {
1566 let frame = state
1567 .stream_fade
1568 .frame(Instant::now() + stagger * 3, false)
1569 .expect("words still fading");
1570 let fades = frame.fades(TextLeafKey::block(0)).expect("paragraph fades");
1571 let ranges: Vec<_> = fades.iter().map(|(range, _)| range.clone()).collect();
1572 assert_eq!(ranges, vec![5..10, 10..14, 14..19]);
1573 assert!(
1575 fades[0].1 < fades[1].1 && fades[1].1 < fades[2].1,
1576 "{fades:?}"
1577 );
1578 });
1579 }
1580
1581 #[gpui::test]
1582 fn a_new_paragraph_fades_as_a_whole(cx: &mut TestAppContext) {
1583 let state = fading_state("first", cx);
1584 state.update(cx, |state, cx| state.push_str("\n\nsecond", cx));
1585 cx.run_until_parked();
1586
1587 assert_eq!(fades(&state, TextLeafKey::block(0), cx), None);
1588 assert_eq!(fades(&state, TextLeafKey::block(7), cx), Some(vec![0..6]));
1589 }
1590
1591 #[gpui::test]
1592 fn code_block_text_fades_by_block(cx: &mut TestAppContext) {
1593 let state = fading_state("```rs\nlet", cx);
1594 state.update(cx, |state, cx| state.push_str(" x", cx));
1595 cx.run_until_parked();
1596
1597 assert_eq!(fades(&state, TextLeafKey::block(0), cx), Some(vec![3..5]));
1598 }
1599
1600 #[gpui::test]
1601 fn table_cells_fade_by_ordinal(cx: &mut TestAppContext) {
1602 let state = fading_state("| a | b |\n|---|---|\n| c | d", cx);
1603 state.update(cx, |state, cx| state.push_str("e |", cx));
1604 cx.run_until_parked();
1605
1606 assert_eq!(fades(&state, TextLeafKey::table_cell(0, 2), cx), None);
1607 assert_eq!(
1608 fades(&state, TextLeafKey::table_cell(0, 3), cx),
1609 Some(vec![1..2])
1610 );
1611 }
1612
1613 #[gpui::test]
1614 fn zero_duration_records_nothing(cx: &mut TestAppContext) {
1615 cx.update(crate::init);
1616 let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown("hello", cx)));
1617 cx.run_until_parked();
1618 state.update(cx, |state, cx| state.push_str(" world", cx));
1619 cx.run_until_parked();
1620
1621 state.update(cx, |state, _| {
1622 assert!(state.stream_fade.frame(Instant::now(), false).is_none());
1623 });
1624 }
1625
1626 #[gpui::test]
1627 fn reduced_motion_drops_the_fade(cx: &mut TestAppContext) {
1628 let state = fading_state("hello", cx);
1629 state.update(cx, |state, cx| state.push_str(" world", cx));
1630 cx.run_until_parked();
1631
1632 state.update(cx, |state, _| {
1633 assert!(state.stream_fade.frame(Instant::now(), true).is_none());
1634 assert!(state.stream_fade.frame(Instant::now(), false).is_none());
1635 });
1636 }
1637
1638 #[gpui::test]
1639 fn a_fade_repaints_on_a_timer_until_nothing_fades(cx: &mut TestAppContext) {
1640 struct FadeRoot(Entity<TextViewState>);
1641 impl Render for FadeRoot {
1642 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1643 crate::text::TextView::new(&self.0)
1644 }
1645 }
1646
1647 let state = fading_state("hello", cx);
1648 let (_, cx) = cx.add_window_view(|_, _| FadeRoot(state.clone()));
1649 let notifies = Rc::new(Cell::new(0));
1650 let _subscription = cx.update(|_, cx| {
1651 let notifies = notifies.clone();
1652 cx.observe(&state, move |_, _| notifies.set(notifies.get() + 1))
1653 });
1654
1655 state.update(cx, |state, cx| state.push_str(" world", cx));
1656 cx.run_until_parked();
1657 cx.update(|window, cx| window.draw(cx).clear(cx));
1658 let before = notifies.get();
1659
1660 cx.executor().advance_clock(STREAM_FADE_TICK / 2);
1661 cx.run_until_parked();
1662 assert_eq!(notifies.get(), before);
1663
1664 for tick in 1..=3 {
1666 cx.executor().advance_clock(STREAM_FADE_TICK);
1667 cx.run_until_parked();
1668 assert_eq!(notifies.get(), before + tick);
1669 }
1670
1671 state.update(cx, |state, cx| state.set_text("other", cx));
1673 cx.run_until_parked();
1674 cx.executor().advance_clock(STREAM_FADE_TICK);
1675 cx.run_until_parked();
1676 let after = notifies.get();
1677 cx.executor().advance_clock(STREAM_FADE_TICK * 10);
1678 cx.run_until_parked();
1679 assert_eq!(notifies.get(), after);
1680 assert!(state.read_with(cx, |state, _| state.fade_tick.is_none()));
1681 }
1682 }
1683
1684 #[gpui::test]
1685 fn small_full_replace_parses_before_background_executor_runs(cx: &mut TestAppContext) {
1686 cx.update(crate::init);
1687 let markdown = "# ready";
1688 let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown(markdown, cx)));
1689
1690 state.read_with(cx, |state, _| {
1691 assert_eq!(state.source().as_str(), markdown);
1692 assert_eq!(state.parsed_content.document.blocks.len(), 1);
1693 });
1694 }
1695
1696 #[gpui::test]
1697 fn large_markdown_and_html_full_replacements_wait_for_background_executor(
1698 cx: &mut TestAppContext,
1699 ) {
1700 cx.update(crate::init);
1701 let markdown = "# x\n\n".repeat(MAX_SYNC_FULL_REPLACE_BYTES / 5 + 1);
1702 let html = format!("<p>{}</p>", "x".repeat(MAX_SYNC_FULL_REPLACE_BYTES + 1));
1703 assert!(markdown.len() > MAX_SYNC_FULL_REPLACE_BYTES);
1704 assert!(html.len() > MAX_SYNC_FULL_REPLACE_BYTES);
1705
1706 let (markdown_state, html_state) = cx.update(|cx| {
1707 (
1708 cx.new(|cx| TextViewState::markdown(&markdown, cx)),
1709 cx.new(|cx| TextViewState::html(&html, cx)),
1710 )
1711 });
1712
1713 markdown_state.read_with(cx, |state, _| {
1714 assert_eq!(state.text.as_str(), markdown.as_str());
1715 assert!(state.source().as_str().is_empty());
1716 assert!(state.parsed_content.document.blocks.is_empty());
1717 });
1718 html_state.read_with(cx, |state, _| {
1719 assert_eq!(state.text.as_str(), html.as_str());
1720 assert!(state.source().as_str().is_empty());
1721 assert!(state.parsed_content.document.blocks.is_empty());
1722 });
1723
1724 cx.run_until_parked();
1725
1726 markdown_state.read_with(cx, |state, _| {
1727 assert_eq!(state.source().as_str(), markdown.as_str());
1728 assert!(!state.parsed_content.document.blocks.is_empty());
1729 });
1730 html_state.read_with(cx, |state, _| {
1731 assert_eq!(state.source().as_str(), html.as_str());
1732 assert!(!state.parsed_content.document.blocks.is_empty());
1733 });
1734 }
1735
1736 #[gpui::test]
1737 fn inline_source_ranges_follow_streamed_tail_reparsing(cx: &mut TestAppContext) {
1738 cx.update(crate::init);
1739 let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown("中文 $a$\n\n尾 $x", cx)));
1740 state.update(cx, |state, cx| {
1741 let extensions = MarkdownExtensions::default().plugin(
1742 crate::text::markdown_ext::TestInlinePlugin::new("test").parse_with(|node, _| {
1743 let markdown::mdast::Node::InlineMath(math) = node else {
1744 return None;
1745 };
1746 Some(super::super::MarkdownNode::new("formula", ()).text(math.value.clone()))
1747 }),
1748 );
1749 state.set_markdown_extensions(Arc::new(extensions), cx);
1750 });
1751 cx.run_until_parked();
1752 state.update(cx, |state, cx| state.push_str("^2$ 后", cx));
1753 cx.run_until_parked();
1754 state.read_with(cx, |state, _| {
1755 let source = "中文 $a$\n\n尾 $x^2$ 后";
1756 assert_eq!(state.source().as_str(), source);
1757 let objects: Vec<_> = state
1758 .parsed_content
1759 .document
1760 .blocks
1761 .iter()
1762 .flat_map(|block| {
1763 let super::super::node::BlockNode::Paragraph(paragraph) = block else {
1764 panic!()
1765 };
1766 paragraph
1767 .children
1768 .iter()
1769 .filter_map(|node| node.custom.as_ref())
1770 })
1771 .collect();
1772 assert_eq!(objects.len(), 2);
1773 for (object, markdown) in objects.iter().zip(["$a$", "$x^2$"]) {
1774 let start = source.find(markdown).unwrap();
1775 assert_eq!(object.source_range(), Some(start..start + markdown.len()));
1776 assert_eq!(object.as_markdown(), markdown);
1777 }
1778 });
1779 }
1780
1781 #[gpui::test]
1782 fn async_full_replace_then_push_str_preserves_complete_source(cx: &mut TestAppContext) {
1783 cx.update(crate::init);
1784 let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown("old", cx)));
1785 cx.run_until_parked();
1786
1787 let replacement = "x".repeat(MAX_SYNC_FULL_REPLACE_BYTES + 1);
1788 let expected = format!("{replacement} tail");
1789 state.update(cx, |state, cx| {
1790 state.set_text(&replacement, cx);
1791 state.push_str(" tail", cx);
1792 });
1793 cx.run_until_parked();
1794
1795 state.read_with(cx, |state, _| {
1796 assert_eq!(state.text.as_str(), expected.as_str());
1797 assert_eq!(state.source().as_str(), expected.as_str());
1798 });
1799 }
1800
1801 fn push_and_parse(
1804 state: &Entity<TextViewState>,
1805 chunk: &str,
1806 cx: &mut TestAppContext,
1807 ) -> ParsedUpdate {
1808 let (revision, baseline) = state.update(cx, |state, cx| {
1809 state.push_str(chunk, cx);
1810 (state.revision, state.parsed_content.clone())
1811 });
1812 let options = UpdateOptions {
1813 revision,
1814 pending_text: chunk.to_string(),
1815 append: true,
1816 mode: ParseMode::Compatible,
1817 markdown_extensions: Arc::default(),
1818 };
1819 ParsedUpdate {
1820 revision,
1821 full_parse: false,
1822 selection_compatible: true,
1823 baseline_ack: false,
1824 result: parse_content(TextViewFormat::Markdown, baseline, &options),
1825 }
1826 }
1827
1828 #[gpui::test]
1829 fn stream_commits_a_parse_that_a_newer_chunk_overtook(cx: &mut TestAppContext) {
1830 cx.update(crate::init);
1831 let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown("# Answer\n\n", cx)));
1832 cx.run_until_parked();
1833
1834 let parsed = push_and_parse(&state, "Streaming", cx);
1837 state.update(cx, |state, cx| {
1838 state.push_str(" tokens", cx);
1839 state.commit_parsed_update(parsed, cx);
1840 assert_eq!(state.source().as_str(), "# Answer\n\nStreaming");
1841 });
1842
1843 cx.run_until_parked();
1844 state.read_with(cx, |state, _| {
1845 assert_eq!(state.source().as_str(), "# Answer\n\nStreaming tokens");
1846 });
1847 }
1848
1849 #[gpui::test]
1850 fn a_parse_from_before_a_replacement_is_discarded(cx: &mut TestAppContext) {
1851 cx.update(crate::init);
1852 let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown("old", cx)));
1853 cx.run_until_parked();
1854
1855 let parsed = push_and_parse(&state, " text", cx);
1856 let replacement = "x".repeat(MAX_SYNC_FULL_REPLACE_BYTES + 1);
1859 state.update(cx, |state, cx| {
1860 state.set_text(&replacement, cx);
1861 state.commit_parsed_update(parsed, cx);
1862 assert_eq!(state.source().as_str(), "old");
1863 });
1864
1865 cx.run_until_parked();
1866 state.read_with(cx, |state, _| {
1867 assert_eq!(state.source().as_str(), replacement.as_str());
1868 });
1869 }
1870
1871 #[gpui::test]
1872 fn html_push_str_keeps_earlier_blocks(cx: &mut TestAppContext) {
1873 cx.update(crate::init);
1874 let state = cx.update(|cx| cx.new(|cx| TextViewState::html("<p>first</p>", cx)));
1875 cx.run_until_parked();
1876
1877 state.update(cx, |state, cx| {
1878 state.push_str("<p>second</p>", cx);
1879 });
1880 cx.run_until_parked();
1881
1882 state.read_with(cx, |state, _| {
1883 assert_eq!(state.source().as_str(), "<p>first</p><p>second</p>");
1884 let text = state
1885 .parsed_content
1886 .document
1887 .blocks
1888 .iter()
1889 .map(|block| block.text())
1890 .collect::<String>();
1891 assert!(text.contains("first"), "lost the first block: {text:?}");
1892 assert!(text.contains("second"), "lost the appended block: {text:?}");
1893 });
1894 }
1895
1896 #[gpui::test]
1897 fn element_text_of_the_same_string_is_not_compared_again(cx: &mut TestAppContext) {
1898 cx.update(crate::init);
1899 let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown("", cx)));
1900 let revision = |cx: &mut TestAppContext| state.read_with(cx, |state, _| state.revision);
1901 let text = SharedString::from("hello");
1902
1903 state.update(cx, |state, cx| state.set_element_text(&text, cx));
1904 let parsed = revision(cx);
1905 assert!(parsed > 0);
1906
1907 state.update(cx, |state, cx| state.set_element_text(&text, cx));
1910 assert_eq!(revision(cx), parsed);
1911 let equal = SharedString::from("hello".to_string());
1912 state.update(cx, |state, cx| state.set_element_text(&equal, cx));
1913 assert_eq!(revision(cx), parsed);
1914
1915 state.update(cx, |state, cx| {
1917 state.push_str(" world", cx);
1918 state.set_element_text(&equal, cx);
1919 });
1920 state.read_with(cx, |state, _| assert_eq!(state.text.as_str(), "hello"));
1921 }
1922
1923 #[gpui::test]
1924 fn set_text_then_push_str_appends_to_replaced_content(cx: &mut TestAppContext) {
1925 cx.update(crate::init);
1926 let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown("old", cx)));
1927 cx.run_until_parked();
1928
1929 state.update(cx, |state, cx| {
1930 state.set_text("", cx);
1931 state.push_str("new", cx);
1932 state.push_str(" text", cx);
1933 });
1934 cx.run_until_parked();
1935
1936 state.read_with(cx, |state, _| {
1937 assert_eq!(state.text.as_str(), "new text");
1938 assert_eq!(state.source().as_str(), "new text");
1939 });
1940
1941 state.update(cx, |state, cx| {
1942 state.set_text("", cx);
1943 });
1944 cx.run_until_parked();
1945
1946 state.read_with(cx, |state, _| {
1947 assert_eq!(state.text.as_str(), "");
1948 assert_eq!(state.source().as_str(), "");
1949 });
1950 }
1951
1952 #[gpui::test]
1953 fn full_parse_coalesced_with_append_preserves_new_select_all(cx: &mut TestAppContext) {
1954 cx.update(crate::init);
1955 let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown("old", cx)));
1956 cx.run_until_parked();
1957
1958 state.update(cx, |state, cx| {
1959 state.set_text("new", cx);
1960 state.push_str(" text", cx);
1961 state.select_all(cx);
1962 });
1963 cx.run_until_parked();
1964
1965 state.read_with(cx, |state, _| {
1966 assert!(state.select_all);
1967 assert_eq!(state.selected_text().trim(), "new text");
1968 });
1969 }
1970
1971 fn parsed_blocks(
1972 state: &Entity<TextViewState>,
1973 cx: &mut TestAppContext,
1974 ) -> Vec<(String, Option<Span>)> {
1975 state.read_with(cx, |state, _| {
1976 state
1977 .parsed_content
1978 .document
1979 .blocks
1980 .iter()
1981 .map(|block| (block.text(), block.span()))
1982 .collect()
1983 })
1984 }
1985
1986 #[gpui::test]
1987 fn set_text_extending_markdown_appends_and_keeps_selection(cx: &mut TestAppContext) {
1988 cx.update(crate::init);
1989 let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown("hello", cx)));
1990 cx.run_until_parked();
1991 let revisions =
1992 |state: &TextViewState| (state.selection_revision, state.full_update_revision);
1993 let initial = state.read_with(cx, |state, _| revisions(state));
1994
1995 state.update(cx, |state, cx| state.set_text("hello world", cx));
1996 cx.run_until_parked();
1997 state.read_with(cx, |state, _| {
1998 assert_eq!(state.source().as_str(), "hello world");
1999 assert_eq!(revisions(state), initial);
2000 });
2001
2002 state.update(cx, |state, cx| state.set_text("hello", cx));
2003 cx.run_until_parked();
2004 state.read_with(cx, |state, _| {
2005 assert_eq!(state.source().as_str(), "hello");
2006 assert_ne!(state.selection_revision, initial.0);
2007 assert_ne!(state.full_update_revision, initial.1);
2008 });
2009 }
2010
2011 #[gpui::test]
2012 fn set_text_streaming_markdown_matches_a_full_parse(cx: &mut TestAppContext) {
2013 cx.update(crate::init);
2014 let streamed = cx.update(|cx| cx.new(|cx| TextViewState::markdown("# Title", cx)));
2015 cx.run_until_parked();
2016
2017 let filler = "word ".repeat(MAX_SYNC_FULL_REPLACE_BYTES / 5 + 1);
2021 let chunks = [
2022 "\n\nfirst para",
2023 "graph with `co",
2024 "de`\n\n- one\n",
2025 "- two\n\n```rust\nfn main() {",
2026 "}\n```\n\n",
2027 filler.as_str(),
2028 "\n\nlast",
2029 ];
2030 let full_update_revision = streamed.read_with(cx, |state, _| state.full_update_revision);
2031 let mut text = "# Title".to_string();
2032 for chunk in chunks {
2033 text.push_str(chunk);
2034 streamed.update(cx, |state, cx| state.set_text(&text, cx));
2035 cx.run_until_parked();
2036 }
2037 assert_eq!(
2038 streamed.read_with(cx, |state, _| state.full_update_revision),
2039 full_update_revision
2040 );
2041
2042 let parsed = cx.update(|cx| cx.new(|cx| TextViewState::markdown(&text, cx)));
2043 cx.run_until_parked();
2044
2045 assert_eq!(
2046 streamed.read_with(cx, |state, _| state.source()),
2047 parsed.read_with(cx, |state, _| state.source())
2048 );
2049 assert_eq!(parsed_blocks(&streamed, cx), parsed_blocks(&parsed, cx));
2050 }
2051
2052 #[gpui::test]
2053 fn set_text_extending_html_parses_it_again(cx: &mut TestAppContext) {
2054 cx.update(crate::init);
2055 let streamed = cx.update(|cx| cx.new(|cx| TextViewState::html("<ul><li>a</li>", cx)));
2056 cx.run_until_parked();
2057
2058 let text = "<ul><li>a</li><li>b</li></ul>";
2059 streamed.update(cx, |state, cx| state.set_text(text, cx));
2060 cx.run_until_parked();
2061 assert_ne!(
2062 streamed.read_with(cx, |state, _| state.full_update_revision),
2063 0
2064 );
2065
2066 let parsed = cx.update(|cx| cx.new(|cx| TextViewState::html(text, cx)));
2067 cx.run_until_parked();
2068 assert_eq!(parsed_blocks(&streamed, cx), parsed_blocks(&parsed, cx));
2069 }
2070
2071 #[gpui::test]
2072 fn set_text_extending_after_a_parse_error_parses_it_again(cx: &mut TestAppContext) {
2073 cx.update(crate::init);
2074 let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown("hello", cx)));
2075 cx.run_until_parked();
2076
2077 let full_update_revision = state.read_with(cx, |state, _| state.full_update_revision);
2078
2079 state.update(cx, |state, cx| {
2080 state.parsed_error = Some("failed".into());
2081 state.set_text("hello world", cx);
2082 });
2083 cx.run_until_parked();
2084
2085 state.read_with(cx, |state, _| {
2086 assert!(state.parsed_error.is_none());
2087 assert_ne!(state.full_update_revision, full_update_revision);
2088 assert_eq!(state.source().as_str(), "hello world");
2089 });
2090 }
2091
2092 #[test]
2093 fn update_options_merge_keeps_latest_full_text() {
2094 let mut options = UpdateOptions {
2095 revision: 1,
2096 pending_text: "old".to_string(),
2097 append: true,
2098 mode: ParseMode::Compatible,
2099 markdown_extensions: Arc::default(),
2100 };
2101
2102 options.merge(UpdateOptions {
2103 revision: 2,
2104 pending_text: "new".to_string(),
2105 append: false,
2106 mode: ParseMode::BaselineAck,
2107 markdown_extensions: Arc::default(),
2108 });
2109 options.merge(UpdateOptions {
2110 revision: 3,
2111 pending_text: " text".to_string(),
2112 append: true,
2113 mode: ParseMode::Compatible,
2114 markdown_extensions: Arc::default(),
2115 });
2116
2117 assert_eq!(options.revision, 3);
2118 assert_eq!(options.pending_text, "new text");
2119 assert!(!options.append);
2120 }
2121
2122 #[test]
2123 fn append_merged_into_async_replace_remains_a_replacement() {
2124 let mut options = UpdateOptions {
2125 revision: 1,
2126 pending_text: "new".to_string(),
2127 append: false,
2128 mode: ParseMode::Replace,
2129 markdown_extensions: Arc::default(),
2130 };
2131
2132 options.merge(UpdateOptions {
2133 revision: 2,
2134 pending_text: " text".to_string(),
2135 append: true,
2136 mode: ParseMode::Compatible,
2137 markdown_extensions: Arc::default(),
2138 });
2139
2140 assert_eq!(options.revision, 2);
2141 assert_eq!(options.pending_text, "new text");
2142 assert!(!options.append);
2143 assert_eq!(options.mode, ParseMode::Replace);
2144 }
2145
2146 #[test]
2147 fn update_future_yields_before_coalescing_all_queued_updates() {
2148 let (tx, rx) = unbounded::<UpdateOptions>();
2149 let (tx_result, rx_result) = unbounded::<ParsedUpdate>();
2150 let total_updates = 128;
2151
2152 for revision in 1..=total_updates {
2153 tx.try_send(UpdateOptions {
2154 revision,
2155 pending_text: format!("{revision}\n"),
2156 append: revision != 1,
2157 mode: if revision == 1 {
2158 ParseMode::BaselineAck
2159 } else {
2160 ParseMode::Compatible
2161 },
2162 markdown_extensions: Arc::default(),
2163 })
2164 .unwrap();
2165 }
2166
2167 let mut future = Box::pin(UpdateFuture::new(TextViewFormat::Markdown, rx, tx_result));
2168 let waker = futures::task::noop_waker();
2169 let mut task_cx = std::task::Context::from_waker(&waker);
2170
2171 assert!(matches!(
2172 std::future::Future::poll(future.as_mut(), &mut task_cx),
2173 Poll::Pending
2174 ));
2175 let parsed_update = rx_result.try_recv().expect("parse result");
2176
2177 assert!(
2178 parsed_update.revision < total_updates,
2179 "single poll coalesced every queued update through revision {}",
2180 parsed_update.revision
2181 );
2182
2183 assert!(matches!(
2184 std::future::Future::poll(future.as_mut(), &mut task_cx),
2185 Poll::Pending
2186 ));
2187 let parsed_update = rx_result.try_recv().expect("next parse result");
2188 assert_eq!(parsed_update.revision, total_updates);
2189 }
2190
2191 #[gpui::test]
2192 fn select_all_returns_rendered_text(cx: &mut TestAppContext) {
2193 cx.update(crate::init);
2194 let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown("**quick** value", cx)));
2195 cx.run_until_parked();
2196
2197 state.update(cx, |state, cx| {
2198 state.select_all(cx);
2199 });
2200
2201 state.read_with(cx, |state, _| {
2202 assert!(state.has_view_selection());
2203 assert_eq!(state.selected_text().trim(), "quick value");
2204 });
2205
2206 state.update(cx, |state, cx| {
2207 state.clear_selection(cx);
2208 });
2209
2210 state.read_with(cx, |state, _| {
2211 assert!(!state.has_view_selection());
2212 assert_eq!(state.selected_text(), "");
2213 });
2214 }
2215
2216 #[gpui::test]
2217 fn select_all_in_source_format_returns_source(cx: &mut TestAppContext) {
2218 cx.update(crate::init);
2219 let markdown = "**quick** value";
2220 let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown(markdown, cx)));
2221 cx.run_until_parked();
2222
2223 state.update(cx, |state, cx| state.select_all(cx));
2224
2225 state.read_with(cx, |state, _| {
2227 assert_eq!(state.selected_text().trim(), "quick value");
2228 });
2229
2230 state.update(cx, |state, cx| {
2231 state.set_selection_format(SelectionFormat::Source, cx)
2232 });
2233
2234 state.read_with(cx, |state, _| {
2236 assert_eq!(state.selected_text().trim(), markdown);
2237 });
2238 }
2239
2240 #[gpui::test]
2241 fn selected_source_range_returns_full_markdown_source_for_select_all(cx: &mut TestAppContext) {
2242 cx.update(crate::init);
2243 let markdown = "**quick** value";
2244 let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown(markdown, cx)));
2245 cx.run_until_parked();
2246
2247 state.update(cx, |state, cx| state.select_all(cx));
2248
2249 state.read_with(cx, |state, _| {
2250 assert_eq!(state.selected_source_range(), Some(0..markdown.len()));
2251 });
2252 }
2253
2254 #[gpui::test]
2255 fn selected_source_range_returns_none_for_html(cx: &mut TestAppContext) {
2256 cx.update(crate::init);
2257 let state = cx.update(|cx| cx.new(|cx| TextViewState::html("<b>quick</b>", cx)));
2258 cx.run_until_parked();
2259
2260 state.update(cx, |state, cx| state.select_all(cx));
2261
2262 state.read_with(cx, |state, _| {
2263 assert_eq!(state.selected_source_range(), None);
2264 });
2265 }
2266
2267 #[test]
2268 fn selected_source_range_keeps_global_offsets_after_incremental_tail_parse() {
2269 let options = UpdateOptions {
2270 revision: 1,
2271 pending_text: "first\n\nsecond".to_string(),
2272 append: false,
2273 mode: ParseMode::Replace,
2274 markdown_extensions: Arc::default(),
2275 };
2276 let content = parse_content(TextViewFormat::Markdown, ParsedContent::default(), &options)
2277 .expect("initial parse");
2278 let content = parse_content(
2279 TextViewFormat::Markdown,
2280 content,
2281 &UpdateOptions {
2282 revision: 2,
2283 pending_text: "\n\n**écho**".to_string(),
2284 append: true,
2285 mode: ParseMode::Compatible,
2286 markdown_extensions: Arc::default(),
2287 },
2288 )
2289 .expect("incremental parse");
2290 let BlockNode::Paragraph(paragraph) = &content.document.blocks[2] else {
2291 panic!("expected appended paragraph");
2292 };
2293 let mut state = paragraph.state.lock().unwrap();
2294 state.set_text(paragraph.text().into());
2295 state.selection = Some((0.."écho".len()).into());
2296 drop(state);
2297
2298 assert_eq!(content.document.selected_source_range(), Some(17..22));
2299 }
2300
2301 #[test]
2302 fn streamed_ordered_list_continuation_preserves_start() {
2303 let initial = parse_content(
2304 TextViewFormat::Markdown,
2305 ParsedContent::default(),
2306 &UpdateOptions {
2307 revision: 1,
2308 pending_text: "3. three".to_string(),
2309 append: false,
2310 mode: ParseMode::Replace,
2311 markdown_extensions: Arc::default(),
2312 },
2313 )
2314 .expect("initial list parse");
2315 let continued = parse_content(
2316 TextViewFormat::Markdown,
2317 initial,
2318 &UpdateOptions {
2319 revision: 2,
2320 pending_text: "\n4. four".to_string(),
2321 append: true,
2322 mode: ParseMode::Compatible,
2323 markdown_extensions: Arc::default(),
2324 },
2325 )
2326 .expect("streamed list continuation parse");
2327
2328 let BlockNode::List {
2329 start, children, ..
2330 } = &continued.document.blocks[0]
2331 else {
2332 panic!("expected streamed ordered list");
2333 };
2334 assert_eq!(*start, Some(3));
2335 assert_eq!(children.len(), 2);
2336 }
2337
2338 #[gpui::test]
2339 fn parser_revision_reparses_same_name_inline_configuration(cx: &mut TestAppContext) {
2340 cx.update(crate::init);
2341 let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown("@member", cx)));
2342 for (revision, label) in [(1, "Alice"), (2, "Bob")] {
2343 let extensions = MarkdownExtensions::default()
2344 .parser_revision(revision)
2345 .plugin(
2346 crate::text::markdown_ext::TestInlinePlugin::new("test").parse_with(
2347 move |node, _| {
2348 matches!(node, markdown::mdast::Node::Text(_))
2349 .then(|| MarkdownNode::new("mention", label).text(label))
2350 },
2351 ),
2352 );
2353 state.update(cx, |state, cx| {
2354 state.set_markdown_extensions(Arc::new(extensions), cx)
2355 });
2356 cx.run_until_parked();
2357 state.read_with(cx, |state, _| {
2358 let node::BlockNode::Paragraph(paragraph) =
2359 &state.parsed_content.document.blocks[0]
2360 else {
2361 panic!()
2362 };
2363 assert_eq!(
2364 paragraph.children[0].custom.as_ref().unwrap().as_text(),
2365 label
2366 );
2367 });
2368 }
2369 }
2370
2371 #[gpui::test]
2372 fn set_markdown_extensions_reparses_existing_text(cx: &mut TestAppContext) {
2373 cx.update(crate::init);
2374 let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown("$TSLA.US", cx)));
2375 cx.run_until_parked();
2376
2377 let extensions = MarkdownExtensions::default().block_parser(|node, cx| {
2378 let markdown::mdast::Node::Paragraph(paragraph) = node else {
2379 return None;
2380 };
2381 let [markdown::mdast::Node::Text(text)] = paragraph.children.as_slice() else {
2382 return None;
2383 };
2384 let symbol = text.value.strip_prefix('$')?.to_string();
2385 let node_text = format!("${symbol}");
2386
2387 Some(
2388 MarkdownNode::new("ticker", symbol)
2389 .text(node_text)
2390 .markdown(cx.node_source(node).unwrap_or_default()),
2391 )
2392 });
2393
2394 state.update(cx, |state, cx| {
2395 state.set_markdown_extensions(Arc::new(extensions), cx);
2396 });
2397 cx.run_until_parked();
2398
2399 state.read_with(cx, |state, _| {
2400 let node::BlockNode::Custom { node, .. } = &state.parsed_content.document.blocks[0]
2401 else {
2402 panic!("expected custom markdown node");
2403 };
2404 assert_eq!(node.name(), "ticker");
2405 assert_eq!(node.data::<String>().map(String::as_str), Some("TSLA.US"));
2406 });
2407 }
2408
2409 mod range_highlights {
2410 use std::ops::Range;
2411
2412 use gpui::{Entity, TestAppContext};
2413
2414 use super::super::*;
2415 use crate::text::stream_fade::TextLeafKey;
2416
2417 fn state(markdown: &str, cx: &mut TestAppContext) -> Entity<TextViewState> {
2418 cx.update(crate::init);
2419 let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown(markdown, cx)));
2420 cx.run_until_parked();
2421 state
2422 }
2423
2424 fn highlight(range: Range<usize>) -> RangeHighlight {
2425 RangeHighlight::new(range, gpui::hsla(0.15, 1., 0.5, 0.4))
2426 }
2427
2428 fn set(
2430 state: &Entity<TextViewState>,
2431 ranges: impl IntoIterator<Item = Range<usize>>,
2432 cx: &mut TestAppContext,
2433 ) -> Result<(), RangeHighlightError> {
2434 state.update(cx, |state, cx| {
2435 state.set_range_highlights(ranges.into_iter().map(highlight), cx)
2436 })
2437 }
2438
2439 fn painted(
2441 state: &Entity<TextViewState>,
2442 key: TextLeafKey,
2443 cx: &mut TestAppContext,
2444 ) -> Vec<Range<usize>> {
2445 state.read_with(cx, |state, _| {
2446 state.range_highlights.as_ref().map_or(Vec::new(), |frame| {
2447 frame
2448 .backgrounds(key)
2449 .iter()
2450 .map(|(range, _)| range.clone())
2451 .collect()
2452 })
2453 })
2454 }
2455
2456 fn has_highlights(state: &Entity<TextViewState>, cx: &mut TestAppContext) -> bool {
2457 state.read_with(cx, |state, _| state.range_highlights.is_some())
2458 }
2459
2460 #[gpui::test]
2461 fn rendered_text_is_the_plain_copy_text(cx: &mut TestAppContext) {
2462 let state = state(
2463 "# Title\n\nhello **world** \\*\n\n- item\n\n| a | b |\n|---|---|\n| c | d |\n\n```\nlet x\n```",
2464 cx,
2465 );
2466 state.read_with(cx, |state, _| {
2467 assert_eq!(
2468 state.rendered_text().as_str(),
2469 "Title\nhello world *\nitem\na b\nc d\n\nlet x\n"
2470 );
2471 });
2472 }
2473
2474 #[gpui::test]
2475 fn a_match_across_marks_paints_in_its_paragraph(cx: &mut TestAppContext) {
2476 let state = state("hello **world**", cx);
2477 set(&state, [0.."hello world".len()], cx).unwrap();
2478 assert_eq!(painted(&state, TextLeafKey::block(0), cx), [0..11]);
2479 }
2480
2481 #[gpui::test]
2482 fn repeated_text_maps_to_the_occurrence_addressed(cx: &mut TestAppContext) {
2483 let state = state("foo\n\nfoo", cx);
2484 let second = state.read_with(cx, |state, _| {
2485 state.rendered_text().as_str().rfind("foo").unwrap()
2486 });
2487 set(&state, [second..second + 3], cx).unwrap();
2488 assert!(painted(&state, TextLeafKey::block(0), cx).is_empty());
2489 assert_eq!(painted(&state, TextLeafKey::block(5), cx), [0..3]);
2490 }
2491
2492 #[gpui::test]
2493 fn a_range_across_blocks_skips_the_separator(cx: &mut TestAppContext) {
2494 let state = state("ab\n\ncd", cx);
2495 set(&state, [1..4], cx).unwrap();
2497 assert_eq!(painted(&state, TextLeafKey::block(0), cx), [1..2]);
2498 assert_eq!(painted(&state, TextLeafKey::block(4), cx), [0..1]);
2499 }
2500
2501 #[gpui::test]
2502 fn table_cells_and_code_blocks_are_leaves(cx: &mut TestAppContext) {
2503 let state = state("| a | b |\n|---|---|\n| c | d |\n\n```\nlet x\n```", cx);
2504 set(&state, [0..3, 6..7, 9..12], cx).unwrap();
2506 assert_eq!(painted(&state, TextLeafKey::table_cell(0, 0), cx), [0..1]);
2507 assert_eq!(painted(&state, TextLeafKey::table_cell(0, 1), cx), [0..1]);
2508 assert_eq!(painted(&state, TextLeafKey::table_cell(0, 3), cx), [0..1]);
2509 let code_start = "| a | b |\n|---|---|\n| c | d |\n\n".len();
2510 assert_eq!(painted(&state, TextLeafKey::block(code_start), cx), [0..3]);
2511 }
2512
2513 #[gpui::test]
2514 fn invalid_ranges_reject_the_whole_set(cx: &mut TestAppContext) {
2515 let state = state("中文\n\nab", cx);
2516 set(&state, [0.."中".len()], cx).unwrap();
2518 let invalid = [Range { start: 6, end: 3 }, 0..1, 0..100];
2519 for range in invalid {
2520 assert_eq!(
2521 set(&state, [0..3, range.clone()], cx),
2522 Err(RangeHighlightError::InvalidRange(1)),
2523 "{range:?}"
2524 );
2525 }
2526 assert_eq!(painted(&state, TextLeafKey::block(0), cx), [0..3]);
2528 }
2529
2530 #[gpui::test]
2531 fn text_outside_every_block_is_left_unpainted(cx: &mut TestAppContext) {
2532 let source = "foo one\n\n<div>foo two</div>\n\n| foo | x |\n|---|---|\n\nfoo three";
2533 let state = state(source, cx);
2534 let text = state.read_with(cx, |state, _| state.rendered_text().as_str().to_string());
2535 let ranges = text
2539 .match_indices("foo")
2540 .chain(text.match_indices(' '))
2541 .map(|(start, found)| start..start + found.len())
2542 .chain([3..3, 7..8])
2543 .collect::<Vec<_>>();
2544 set(&state, ranges, cx).unwrap();
2545 assert_eq!(painted(&state, TextLeafKey::block(0), cx), [0..3, 3..4]);
2546 let table = source.find("| foo").unwrap();
2547 assert_eq!(
2548 painted(&state, TextLeafKey::table_cell(table, 0), cx),
2549 [0..3]
2550 );
2551 let last = source.find("foo three").unwrap();
2552 assert_eq!(painted(&state, TextLeafKey::block(last), cx), [0..3, 3..4]);
2553 }
2554
2555 #[gpui::test]
2556 fn a_later_highlight_paints_over_an_earlier_one(cx: &mut TestAppContext) {
2557 let state = state("abcdef", cx);
2558 set(&state, [0..6, 2..3], cx).unwrap();
2559 assert_eq!(painted(&state, TextLeafKey::block(0), cx), [0..6, 2..3]);
2560 }
2561
2562 #[gpui::test]
2563 fn html_text_is_unsupported(cx: &mut TestAppContext) {
2564 let html = cx.update(|cx| cx.new(|cx| TextViewState::html("<p>one</p>", cx)));
2565 cx.run_until_parked();
2566 html.update(cx, |html, cx| {
2567 assert_eq!(
2568 html.set_range_highlights([highlight(0..3)], cx),
2569 Err(RangeHighlightError::Unsupported)
2570 );
2571 });
2572 }
2573
2574 #[gpui::test]
2575 fn inline_objects_are_skipped(cx: &mut TestAppContext) {
2576 let state = state("x $a$ y", cx);
2577 state.update(cx, |state, cx| {
2578 let extensions = MarkdownExtensions::default().plugin(
2579 crate::text::markdown_ext::TestInlinePlugin::new("test").parse_with(
2580 |node, _| {
2581 let markdown::mdast::Node::InlineMath(math) = node else {
2582 return None;
2583 };
2584 Some(
2585 crate::text::MarkdownNode::new("formula", ())
2586 .text(math.value.clone()),
2587 )
2588 },
2589 ),
2590 );
2591 state.set_markdown_extensions(Arc::new(extensions), cx);
2592 });
2593 cx.run_until_parked();
2594 set(&state, [2..3], cx).unwrap();
2596 assert!(painted(&state, TextLeafKey::block(0), cx).is_empty());
2597 set(&state, [0..5], cx).unwrap();
2598 assert_eq!(painted(&state, TextLeafKey::block(0), cx), [0..2, 3..5]);
2599 }
2600
2601 #[gpui::test]
2602 fn push_str_keeps_earlier_blocks_and_clips_the_changed_tail(cx: &mut TestAppContext) {
2603 let state = state("first\n\na **b", cx);
2604 set(&state, [0..5, 6..11], cx).unwrap();
2606
2607 state.update(cx, |state, cx| state.push_str("c**", cx));
2609 cx.run_until_parked();
2610
2611 assert_eq!(painted(&state, TextLeafKey::block(0), cx), [0..5]);
2612 assert_eq!(painted(&state, TextLeafKey::block(7), cx), [0..2]);
2613 }
2614
2615 #[gpui::test]
2616 fn push_str_keeps_a_block_whose_text_is_unchanged(cx: &mut TestAppContext) {
2617 let state = state("first\n\nsecond", cx);
2618 set(&state, [6..12], cx).unwrap();
2619 state.update(cx, |state, cx| state.push_str("\n===", cx));
2622 cx.run_until_parked();
2623 assert_eq!(painted(&state, TextLeafKey::block(7), cx), [0..6]);
2624 }
2625
2626 #[gpui::test]
2627 fn push_str_drops_highlights_of_a_leaf_that_is_gone(cx: &mut TestAppContext) {
2628 let state = state("first\n\n| a |", cx);
2629 set(&state, [0..5, 6..11], cx).unwrap();
2630 state.update(cx, |state, cx| state.push_str("\n|---|", cx));
2633 cx.run_until_parked();
2634 assert_eq!(painted(&state, TextLeafKey::block(0), cx), [0..5]);
2635 assert!(painted(&state, TextLeafKey::block(7), cx).is_empty());
2636 assert!(painted(&state, TextLeafKey::table_cell(7, 0), cx).is_empty());
2637 }
2638
2639 #[gpui::test]
2640 fn replacing_text_drops_highlights_only_where_it_changed(cx: &mut TestAppContext) {
2641 let state = state("first\n\nsecond", cx);
2642 set(&state, [0..5, 6..12], cx).unwrap();
2643 state.update(cx, |state, cx| state.set_text("first\n\nchanged", cx));
2644 cx.run_until_parked();
2645 assert_eq!(painted(&state, TextLeafKey::block(0), cx), [0..5]);
2646 assert!(painted(&state, TextLeafKey::block(7), cx).is_empty());
2647
2648 let large = "x".repeat(MAX_SYNC_FULL_REPLACE_BYTES + 1);
2649 state.update(cx, |state, cx| state.set_text(&large, cx));
2650 cx.run_until_parked();
2651 assert!(!has_highlights(&state, cx));
2652 }
2653
2654 #[gpui::test]
2655 fn a_highlight_follows_its_block_past_an_earlier_edit(cx: &mut TestAppContext) {
2656 let state = state("foo\n\nbar\n\nbar", cx);
2657 set(&state, [4..7], cx).unwrap();
2659 state.update(cx, |state, cx| state.set_text("bar\n\nbar", cx));
2661 cx.run_until_parked();
2662 assert_eq!(painted(&state, TextLeafKey::block(0), cx), [0..3]);
2663 assert!(painted(&state, TextLeafKey::block(5), cx).is_empty());
2664
2665 let state = self::state("ERROR a\n\nERROR b\n\nERROR c", cx);
2666 set(&state, [8..15], cx).unwrap();
2667 state.update(cx, |state, cx| state.set_text("ERROR b\n\nERROR c", cx));
2668 cx.run_until_parked();
2669 assert_eq!(painted(&state, TextLeafKey::block(0), cx), [0..7]);
2670 assert!(painted(&state, TextLeafKey::block(9), cx).is_empty());
2671 }
2672
2673 #[gpui::test]
2674 fn appending_a_copy_of_the_last_block_keeps_the_highlight_on_it(cx: &mut TestAppContext) {
2675 let state = state("a\n\nfoo", cx);
2676 set(&state, [2..5], cx).unwrap();
2678 state.update(cx, |state, cx| state.set_text("a\n\nfoo\n\nfoo", cx));
2679 cx.run_until_parked();
2680 assert_eq!(painted(&state, TextLeafKey::block(3), cx), [0..3]);
2681 assert!(painted(&state, TextLeafKey::block(8), cx).is_empty());
2682 }
2683
2684 #[gpui::test]
2685 fn deleting_a_table_row_drops_highlights_in_the_rows_it_moves(cx: &mut TestAppContext) {
2686 let table = "| a | b |\n|---|---|\n| x | 1 |\n| x | 2 |";
2687 let without_middle_row = "| a | b |\n|---|---|\n| x | 2 |";
2688 for ranges in [vec![8..9, 10..11], vec![4..5]] {
2691 let state = state(table, cx);
2692 set(&state, [0..1].into_iter().chain(ranges), cx).unwrap();
2693 state.update(cx, |state, cx| state.set_text(without_middle_row, cx));
2694 cx.run_until_parked();
2695 assert_eq!(painted(&state, TextLeafKey::table_cell(0, 0), cx), [0..1]);
2696 for cell in 1..4 {
2697 assert!(
2698 painted(&state, TextLeafKey::table_cell(0, cell), cx).is_empty(),
2699 "cell {cell}"
2700 );
2701 }
2702 }
2703 }
2704
2705 #[gpui::test]
2706 fn streaming_table_rows_keeps_the_highlights_of_earlier_rows(cx: &mut TestAppContext) {
2707 let state = state("| a | b |\n|---|---|\n| x | 1 |", cx);
2708 set(&state, [0..1, 4..5], cx).unwrap();
2710 state.update(cx, |state, cx| {
2711 state.set_text("| a | b |\n|---|---|\n| x | 1 |\n| y | 2 |", cx)
2712 });
2713 cx.run_until_parked();
2714 assert_eq!(painted(&state, TextLeafKey::table_cell(0, 0), cx), [0..1]);
2715 assert_eq!(painted(&state, TextLeafKey::table_cell(0, 2), cx), [0..1]);
2716 }
2717
2718 #[gpui::test]
2719 fn an_edit_in_an_earlier_block_keeps_later_highlights(cx: &mut TestAppContext) {
2720 let state = state("one\n\ntwo", cx);
2721 set(&state, [0..3, 4..7], cx).unwrap();
2722 state.update(cx, |state, cx| state.set_text("one!\n\ntwo", cx));
2723 cx.run_until_parked();
2724 assert_eq!(painted(&state, TextLeafKey::block(0), cx), [0..3]);
2725 assert_eq!(painted(&state, TextLeafKey::block(6), cx), [0..3]);
2726 }
2727
2728 #[gpui::test]
2729 fn an_append_after_a_full_update_compares_every_block(cx: &mut TestAppContext) {
2730 let state = state("first\n\nsecond", cx);
2731 set(&state, [0..5], cx).unwrap();
2732 let options = UpdateOptions {
2735 revision: 0,
2736 pending_text: "FIRST\n\nsecond".to_string(),
2737 append: false,
2738 mode: ParseMode::Replace,
2739 markdown_extensions: Arc::default(),
2740 };
2741 let new = parse_content(TextViewFormat::Markdown, ParsedContent::default(), &options)
2742 .unwrap()
2743 .document;
2744 state.update(cx, |state, _| {
2745 state.full_update_revision = state.committed_revision + 1;
2746 let revision = state.committed_revision + 2;
2747 state.reconcile_range_highlights(&new, revision, true);
2748 });
2749 assert!(!has_highlights(&state, cx));
2750 }
2751
2752 #[gpui::test]
2753 fn streaming_through_set_text_keeps_highlights(cx: &mut TestAppContext) {
2754 let state = state("first\n\nsec", cx);
2755 set(&state, [0..5, 6..9], cx).unwrap();
2757 state.update(cx, |state, cx| state.set_text("first\n\nsecond", cx));
2758 cx.run_until_parked();
2759 assert_eq!(painted(&state, TextLeafKey::block(0), cx), [0..5]);
2760 assert_eq!(painted(&state, TextLeafKey::block(7), cx), [0..3]);
2761 }
2762
2763 #[gpui::test]
2764 fn a_full_parse_merged_with_an_append_compares_every_block(cx: &mut TestAppContext) {
2765 let state = state("", cx);
2766 state.update(cx, |state, cx| {
2770 state.set_text("[foo] and some text\n\nmore", cx);
2771 let text = state.rendered_text();
2772 let some = text.as_str().find("some").unwrap();
2773 state
2774 .set_range_highlights([highlight(some..some + 4)], cx)
2775 .unwrap();
2776 state.push_str("\n\n[foo]: https://example.com", cx);
2777 });
2778 cx.run_until_parked();
2779 state.read_with(cx, |state, _| {
2780 assert!(state.rendered_text().as_str().starts_with("foo and some"));
2781 });
2782 assert!(painted(&state, TextLeafKey::block(0), cx).is_empty());
2783 }
2784
2785 #[gpui::test]
2786 fn a_full_update_before_an_append_drops_replaced_highlights(cx: &mut TestAppContext) {
2787 let state = state("first", cx);
2788 set(&state, [0..5], cx).unwrap();
2789 let large = "x".repeat(MAX_SYNC_FULL_REPLACE_BYTES + 1);
2790 state.update(cx, |state, cx| {
2791 state.set_text(&large, cx);
2792 state.push_str(" tail", cx);
2793 });
2794 cx.run_until_parked();
2795 assert!(!has_highlights(&state, cx));
2796 }
2797
2798 #[gpui::test]
2799 fn reparsing_unchanged_text_keeps_highlights(cx: &mut TestAppContext) {
2800 let state = state("first", cx);
2801 set(&state, [0..5], cx).unwrap();
2802 state.update(cx, |state, cx| {
2803 state.set_markdown_extensions(
2804 Arc::new(MarkdownExtensions::default().parser_revision(1)),
2805 cx,
2806 )
2807 });
2808 cx.run_until_parked();
2809 assert_eq!(painted(&state, TextLeafKey::block(0), cx), [0..5]);
2810 }
2811
2812 pub(super) struct Root {
2813 pub(super) state: Entity<TextViewState>,
2814 }
2815
2816 impl Render for Root {
2817 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2818 gpui::div()
2819 .w(px(120.))
2820 .child(crate::text::TextView::new(&self.state).selectable(true))
2821 }
2822 }
2823
2824 #[gpui::test]
2825 fn highlights_paint_across_inline_code_tables_and_code_blocks(cx: &mut TestAppContext) {
2826 cx.update(crate::init);
2827 let markdown = "wrapping text with `inline code` and a [link](https://x.y) \
2828 that wraps\n\n| a | b |\n|---|---|\n| c | d |\n\n```\nlet x = 1;\n```";
2829 let (root, cx) = cx.add_window_view(|_, cx| Root {
2830 state: cx.new(|cx| TextViewState::markdown(markdown, cx)),
2831 });
2832 cx.run_until_parked();
2833 let state = root.read_with(cx, |root, _| root.state.clone());
2834 let text = state.read_with(cx, |state, _| state.rendered_text().as_str().to_string());
2835 let code = text.find("code").unwrap();
2836 let len = text.len();
2837 let ranges = [code..code + 2]
2839 .into_iter()
2840 .chain(text.char_indices().map(|(ix, c)| ix..ix + c.len_utf8()))
2841 .chain([0..len])
2842 .filter(|range| !text[range.clone()].trim().is_empty());
2843 state.update(cx, |state, cx| {
2844 state
2845 .set_range_highlights(ranges.map(highlight), cx)
2846 .unwrap();
2847 });
2848 cx.update(|window, cx| window.draw(cx).clear(cx));
2849 assert_eq!(
2850 painted(&state, TextLeafKey::block(0), cx)[0],
2851 code..code + 2
2852 );
2853 }
2854
2855 fn converted(
2857 state: &Entity<TextViewState>,
2858 sources: impl IntoIterator<Item = Range<usize>>,
2859 cx: &mut TestAppContext,
2860 ) -> Vec<Option<Range<usize>>> {
2861 state.read_with(cx, |state, _| {
2862 let text = state.rendered_text();
2863 sources
2864 .into_iter()
2865 .map(|source| text.range_for_source(source))
2866 .collect()
2867 })
2868 }
2869
2870 fn find(haystack: &str, needle: &str) -> Range<usize> {
2871 let start = haystack.find(needle).unwrap();
2872 start..start + needle.len()
2873 }
2874
2875 #[gpui::test]
2876 fn source_ranges_highlight_the_text_rendered_from_them(cx: &mut TestAppContext) {
2877 let markdown =
2878 "In Rust, we use `u128` to handle **larger** numbers\n\n| a | **b** |\n|---|---|";
2879 let state = state(markdown, cx);
2880 state.update(cx, |state, cx| {
2881 let text = state.rendered_text();
2882 assert_eq!(text.source(), markdown);
2883 let highlights = ["`u128` to handle **larger**", "**b**"]
2884 .into_iter()
2885 .map(|needle| text.range_for_source(find(markdown, needle)).unwrap())
2886 .map(highlight)
2887 .collect::<Vec<_>>();
2888 state.set_range_highlights(highlights, cx).unwrap();
2889 });
2890 let rendered = "In Rust, we use u128 to handle larger numbers";
2891 assert_eq!(
2892 painted(&state, TextLeafKey::block(0), cx),
2893 [find(rendered, "u128 to handle larger")]
2894 );
2895 let table = markdown.find("| a").unwrap();
2896 assert_eq!(
2897 painted(&state, TextLeafKey::table_cell(table, 1), cx),
2898 [0..1]
2899 );
2900 }
2901
2902 #[gpui::test]
2903 fn select_all_converts_to_every_rendered_character(cx: &mut TestAppContext) {
2904 let markdown = "# Title\n\nhello **world**\n\n- item";
2905 let state = state(markdown, cx);
2906 state.update(cx, |state, cx| state.select_all(cx));
2907 state.read_with(cx, |state, _| {
2908 let text = state.rendered_text();
2909 assert_eq!(text.as_str(), "Title\nhello world\nitem\n");
2910 let source = state.selected_source_range().unwrap();
2911 assert_eq!(text.range_for_source(source), Some(0..text.len() - 1));
2913 });
2914 }
2915
2916 #[gpui::test]
2917 fn a_snapshot_converts_against_its_own_source(cx: &mut TestAppContext) {
2918 let state = state("first", cx);
2919 let before = state.read_with(cx, |state, _| state.rendered_text());
2920 state.update(cx, |state, cx| state.push_str("\n\nsecond **part**", cx));
2921 let source = "first\n\nsecond **part**";
2922 let second = find(source, "second **part**");
2923
2924 state.read_with(cx, |state, _| {
2927 let text = state.rendered_text();
2928 assert_eq!(text, before);
2929 assert_eq!(text.source(), "first");
2930 assert_eq!(text.range_for_source(second.clone()), None);
2931 });
2932 cx.run_until_parked();
2933
2934 assert_eq!(
2935 converted(&state, [second.clone(), 0..5], cx),
2936 [Some(6..17), Some(0..5)]
2937 );
2938 state.read_with(cx, |state, _| {
2939 let text = state.rendered_text();
2940 assert_ne!(text, before);
2941 assert_eq!(text.source(), source);
2942 assert_eq!(&text.as_str()[6..17], "second part");
2943 });
2944 assert_eq!(before.source(), "first");
2946 assert_eq!(before.range_for_source(0..5), Some(0..5));
2947 assert_eq!(before.range_for_source(second), None);
2948 }
2949
2950 #[gpui::test]
2951 fn appended_blocks_convert_at_their_place_in_the_whole_source(cx: &mut TestAppContext) {
2952 let state = state("first\n\nsecond", cx);
2953 state.update(cx, |state, cx| state.push_str(" more\n\n**écho** end", cx));
2956 cx.run_until_parked();
2957 let source = "first\n\nsecond more\n\n**écho** end";
2958 let converted = converted(
2959 &state,
2960 [
2961 find(source, "second more"),
2962 find(source, "**écho**"),
2963 find(source, "end"),
2964 ],
2965 cx,
2966 );
2967 state.read_with(cx, |state, _| {
2968 let text = state.rendered_text();
2969 assert_eq!(text.source(), source);
2970 let texts = converted
2971 .into_iter()
2972 .map(|range| &text.as_str()[range.unwrap()])
2973 .collect::<Vec<_>>();
2974 assert_eq!(texts, ["second more", "écho", "end"]);
2975 });
2976 }
2977
2978 #[gpui::test]
2979 fn a_background_parse_converts_once_it_lands(cx: &mut TestAppContext) {
2980 cx.update(crate::init);
2981 let markdown = format!(
2982 "{}\n\n**tail**",
2983 "word ".repeat(MAX_SYNC_FULL_REPLACE_BYTES / 4)
2984 );
2985 let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown(&markdown, cx)));
2986 let tail = find(&markdown, "**tail**");
2987 state.read_with(cx, |state, _| {
2988 let text = state.rendered_text();
2989 assert!(text.source().is_empty());
2990 assert!(text.is_empty());
2991 assert_eq!(text.range_for_source(tail.clone()), None);
2992 });
2993 cx.run_until_parked();
2994 state.read_with(cx, |state, _| {
2995 let text = state.rendered_text();
2996 assert_eq!(text.source(), markdown);
2997 let range = text.range_for_source(tail).unwrap();
2998 assert_eq!(&text.as_str()[range], "tail");
2999 });
3000 }
3001
3002 #[gpui::test]
3003 fn inline_objects_convert_whole_and_stay_unpainted(cx: &mut TestAppContext) {
3004 let markdown = "x $ab$ y";
3005 let state = state(markdown, cx);
3006 state.update(cx, |state, cx| {
3007 let extensions = MarkdownExtensions::default().plugin(
3008 crate::text::markdown_ext::TestInlinePlugin::new("test").parse_with(
3009 |node, _| {
3010 let markdown::mdast::Node::InlineMath(math) = node else {
3011 return None;
3012 };
3013 Some(
3014 crate::text::MarkdownNode::new("formula", ())
3015 .text(math.value.clone()),
3016 )
3017 },
3018 ),
3019 );
3020 state.set_markdown_extensions(Arc::new(extensions), cx);
3021 });
3022 cx.run_until_parked();
3023 let formula = find(markdown, "$ab$");
3025 assert_eq!(
3026 converted(
3027 &state,
3028 [
3029 formula.clone(),
3030 formula.start + 1..formula.start + 2,
3031 0..markdown.len(),
3032 find(markdown, "ab$ y"),
3033 ],
3034 cx
3035 ),
3036 [Some(2..4), Some(2..4), Some(0..6), Some(2..6)]
3037 );
3038 let ranges = converted(&state, [formula, 0..markdown.len()], cx);
3039 set(&state, [ranges[0].clone().unwrap()], cx).unwrap();
3040 assert!(painted(&state, TextLeafKey::block(0), cx).is_empty());
3041 set(&state, [ranges[1].clone().unwrap()], cx).unwrap();
3042 assert_eq!(painted(&state, TextLeafKey::block(0), cx), [0..2, 4..6]);
3043 }
3044
3045 #[gpui::test]
3046 fn a_dragged_selection_highlights_through_its_source_range(cx: &mut TestAppContext) {
3047 struct SelectionRoot {
3048 state: Entity<TextViewState>,
3049 }
3050
3051 impl Render for SelectionRoot {
3052 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3053 gpui::div()
3054 .w(px(160.))
3055 .child(crate::TextSelectionLayer)
3056 .child(crate::text::TextView::new(&self.state))
3057 }
3058 }
3059
3060 cx.update(crate::init);
3061 let markdown = "hello **world** and `code` & more";
3062 let (root, cx) = cx.add_window_view(|_, cx| SelectionRoot {
3063 state: cx.new(|cx| TextViewState::markdown(markdown, cx)),
3064 });
3065 cx.run_until_parked();
3066 let state = root.read_with(cx, |root, _| root.state.clone());
3067 cx.update(|window, cx| window.draw(cx).clear(cx));
3068 let bounds = state.read_with(cx, |state, _| state.bounds());
3069 assert!(bounds.size.height > px(30.), "the text wraps, {bounds:?}");
3070 let start = bounds.origin + gpui::point(px(2.), px(4.));
3071 let end = bounds.bottom_right() - gpui::point(px(2.), px(4.));
3072 cx.simulate_mouse_down(start, gpui::MouseButton::Left, gpui::Modifiers::default());
3073 cx.update(|window, cx| window.draw(cx).clear(cx));
3074 cx.simulate_mouse_move(end, gpui::MouseButton::Left, gpui::Modifiers::default());
3075 cx.update(|window, cx| window.draw(cx).clear(cx));
3076 cx.simulate_mouse_up(end, gpui::MouseButton::Left, gpui::Modifiers::default());
3077 cx.update(|window, cx| window.draw(cx).clear(cx));
3078
3079 let range = state.update(cx, |state, cx| {
3080 let selected = state.selected_text();
3081 let source = state
3082 .selected_source_range()
3083 .expect("selection maps to source");
3084 let text = state.rendered_text();
3085 let range = text.range_for_source(source).expect("source renders text");
3086 assert!(selected.contains("code &"), "selected {selected:?}");
3087 assert_eq!(
3088 &text.as_str()[range.clone()],
3089 selected.trim_end_matches('\n')
3090 );
3091 state.clear_selection(cx);
3092 state
3093 .set_range_highlights([highlight(range.clone())], cx)
3094 .unwrap();
3095 range
3096 });
3097 cx.update(|window, cx| window.draw(cx).clear(cx));
3098 assert_eq!(painted(&state, TextLeafKey::block(0), cx), [range]);
3099 }
3100
3101 #[gpui::test]
3102 fn html_views_convert_no_source(cx: &mut TestAppContext) {
3103 let html = cx.update(|cx| cx.new(|cx| TextViewState::html("<p>one</p>", cx)));
3104 cx.run_until_parked();
3105 html.read_with(cx, |html, _| {
3106 let text = html.rendered_text();
3107 assert_eq!(text.source(), "<p>one</p>");
3108 assert_eq!(text.range_for_source(0..10), None);
3109 assert_eq!(text.range_for_source(3..6), None);
3110 });
3111 }
3112
3113 #[gpui::test]
3114 fn clear_range_highlights_removes_them(cx: &mut TestAppContext) {
3115 let state = state("first", cx);
3116 set(&state, [0..5], cx).unwrap();
3117 state.update(cx, |state, cx| state.clear_range_highlights(cx));
3118 assert!(!has_highlights(&state, cx));
3119 }
3120 }
3121
3122 mod reveal_range {
3123 use gpui::{
3124 Entity, InteractiveElement as _, ListAlignment, ListState, ScrollHandle,
3125 StatefulInteractiveElement as _, TestAppContext, VisualTestContext, div, list,
3126 };
3127
3128 use super::super::*;
3129 use crate::text::TextView;
3130
3131 #[derive(Clone)]
3133 enum Container {
3134 Scrollable,
3136 List(ListState),
3138 Div(ScrollHandle),
3141 Clamped(ListState),
3143 Fixed,
3145 }
3146
3147 struct Root {
3148 state: Entity<TextViewState>,
3149 container: Container,
3150 }
3151
3152 impl Render for Root {
3153 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3154 let state = self.state.clone();
3155 let frame = div().w(px(200.)).h(px(100.));
3156 let row = |list_state: &ListState, clamp: bool| {
3157 let state = state.clone();
3158 list(list_state.clone(), move |ix, _, _| match ix {
3159 0 => div().h(px(40.)).into_any_element(),
3160 _ => TextView::new(&state)
3161 .when(clamp, |view| view.max_lines(2))
3162 .into_any_element(),
3163 })
3164 .size_full()
3165 };
3166 match &self.container {
3167 Container::Scrollable => frame
3168 .child(TextView::new(&state).scrollable(true))
3169 .into_any_element(),
3170 Container::List(list_state) => {
3171 frame.child(row(list_state, false)).into_any_element()
3172 }
3173 Container::Clamped(list_state) => {
3174 frame.child(row(list_state, true)).into_any_element()
3175 }
3176 Container::Div(handle) => {
3177 let scroll = handle.clone();
3178 frame
3179 .child(
3180 div()
3181 .id("scroll")
3182 .size_full()
3183 .overflow_y_scroll()
3184 .track_scroll(handle)
3185 .child(TextView::new(&state).on_reveal(move |line, _, _| {
3186 let viewport = scroll.bounds();
3187 let mut offset = scroll.offset();
3188 if line.bottom() > viewport.bottom() {
3189 offset.y -= line.bottom() - viewport.bottom();
3190 } else if line.top() < viewport.top() {
3191 offset.y += viewport.top() - line.top();
3192 }
3193 scroll.set_offset(offset);
3194 })),
3195 )
3196 .into_any_element()
3197 }
3198 Container::Fixed => frame.child(TextView::new(&state)).into_any_element(),
3199 }
3200 }
3201 }
3202
3203 fn window<'a>(
3204 markdown: &str,
3205 container: Container,
3206 cx: &'a mut TestAppContext,
3207 ) -> (Entity<TextViewState>, &'a mut VisualTestContext) {
3208 cx.update(crate::init);
3209 let (root, cx) = cx.add_window_view(|_, cx| Root {
3210 state: cx.new(|cx| TextViewState::markdown(markdown, cx)),
3211 container,
3212 });
3213 cx.run_until_parked();
3214 draw(cx);
3215 let state = root.read_with(cx, |root, _| root.state.clone());
3216 (state, cx)
3217 }
3218
3219 fn draw(cx: &mut VisualTestContext) {
3220 cx.update(|window, cx| window.draw(cx).clear(cx));
3221 }
3222
3223 fn request(
3226 state: &Entity<TextViewState>,
3227 needle: &str,
3228 cx: &mut VisualTestContext,
3229 then: impl FnOnce(&mut TextViewState, &mut Context<TextViewState>),
3230 ) {
3231 state.update(cx, |state, cx| {
3232 let text = state.rendered_text();
3233 let start = text.as_str().find(needle).unwrap();
3234 state.reveal_range(start..start + needle.len(), cx).unwrap();
3235 then(state, cx);
3236 });
3237 }
3238
3239 fn reveal(state: &Entity<TextViewState>, needle: &str, cx: &mut VisualTestContext) {
3241 request(state, needle, cx, |_, _| {});
3242 for _ in 0..3 {
3243 draw(cx);
3244 }
3245 }
3246
3247 fn is_pending(state: &Entity<TextViewState>, cx: &mut VisualTestContext) -> bool {
3248 state.read_with(cx, |state, _| state.pending_reveal.is_some())
3249 }
3250
3251 fn scroll_top(
3252 state: &Entity<TextViewState>,
3253 cx: &mut VisualTestContext,
3254 ) -> gpui::ListOffset {
3255 state.read_with(cx, |state, _| state.list_state.logical_scroll_top())
3256 }
3257
3258 fn paragraphs(count: usize) -> String {
3259 (0..count)
3260 .map(|ix| format!("paragraph {ix}"))
3261 .collect::<Vec<_>>()
3262 .join("\n\n")
3263 }
3264
3265 fn words(count: usize) -> String {
3266 (0..count)
3267 .map(|ix| format!("w{ix}"))
3268 .collect::<Vec<_>>()
3269 .join(" ")
3270 }
3271
3272 #[gpui::test]
3273 fn a_scrollable_view_scrolls_to_an_offscreen_block(cx: &mut TestAppContext) {
3274 let (state, cx) = window(¶graphs(200), Container::Scrollable, cx);
3275 reveal(&state, "paragraph 150", cx);
3276 assert!(!is_pending(&state, cx));
3277 let top = scroll_top(&state, cx);
3278 assert!((140..=150).contains(&top.item_ix), "{top:?}");
3279
3280 reveal(&state, "paragraph 3", cx);
3281 assert!(!is_pending(&state, cx));
3282 assert!(scroll_top(&state, cx).item_ix <= 3);
3283 }
3284
3285 #[gpui::test]
3286 fn an_append_adding_blocks_keeps_the_scroll_position(cx: &mut TestAppContext) {
3287 let (state, cx) = window(¶graphs(200), Container::Scrollable, cx);
3288 reveal(&state, "paragraph 100", cx);
3289 let top = scroll_top(&state, cx);
3290 assert!(top.item_ix > 0, "{top:?}");
3291
3292 state.update(cx, |state, cx| state.push_str("\n\nparagraph 200", cx));
3293 cx.run_until_parked();
3294 draw(cx);
3295
3296 state.read_with(cx, |state, _| {
3297 assert_eq!(state.list_state.item_count(), 201);
3298 });
3299 assert_unmoved(&state, top, cx);
3300 }
3301
3302 #[gpui::test]
3303 fn a_scrollable_view_scrolls_to_a_line_inside_a_long_paragraph(cx: &mut TestAppContext) {
3304 let (state, cx) = window(&words(400), Container::Scrollable, cx);
3305 reveal(&state, "w390", cx);
3306 assert!(!is_pending(&state, cx));
3307 let top = scroll_top(&state, cx);
3308 assert_eq!(top.item_ix, 0);
3309 assert!(top.offset_in_item > px(100.), "{top:?}");
3310 }
3311
3312 #[gpui::test]
3313 fn revealing_a_visible_line_does_not_scroll(cx: &mut TestAppContext) {
3314 let (state, cx) = window(&words(400), Container::Scrollable, cx);
3315 reveal(&state, "w200", cx);
3316 let top = scroll_top(&state, cx);
3317 assert!(top.offset_in_item > px(0.), "{top:?}");
3318 for word in ["w199", "w198", "w197", "w196"] {
3319 reveal(&state, word, cx);
3320 assert!(!is_pending(&state, cx));
3321 let now = scroll_top(&state, cx);
3322 assert_eq!(
3323 (now.item_ix, now.offset_in_item),
3324 (top.item_ix, top.offset_in_item),
3325 "{word}"
3326 );
3327 }
3328 }
3329
3330 #[gpui::test]
3331 fn an_enclosing_list_scrolls_to_a_line_of_a_fit_content_view(cx: &mut TestAppContext) {
3332 let outer = ListState::new(2, ListAlignment::Top, px(1000.));
3333 let (state, cx) = window(&words(400), Container::List(outer.clone()), cx);
3334 reveal(&state, "w390", cx);
3335 assert!(!is_pending(&state, cx));
3336 let top = outer.logical_scroll_top();
3337 assert_eq!(top.item_ix, 1, "{top:?}");
3338 assert!(top.offset_in_item > px(100.), "{top:?}");
3339 }
3340
3341 #[gpui::test]
3342 fn on_reveal_scrolls_a_container_that_ignores_scroll_requests(cx: &mut TestAppContext) {
3343 let handle = ScrollHandle::new();
3344 let (state, cx) = window(&words(400), Container::Div(handle.clone()), cx);
3345 reveal(&state, "w390", cx);
3346 assert!(!is_pending(&state, cx));
3347 assert!(handle.offset().y < px(-100.), "{:?}", handle.offset());
3348 }
3349
3350 #[gpui::test]
3351 fn a_reveal_that_cannot_be_shown_gives_up(cx: &mut TestAppContext) {
3352 let (state, cx) = window(&words(4000), Container::Fixed, cx);
3353 reveal(&state, "w3990", cx);
3354 assert!(is_pending(&state, cx));
3355 for _ in 0..10 {
3356 draw(cx);
3357 }
3358 assert!(!is_pending(&state, cx));
3359 }
3360
3361 #[gpui::test]
3362 fn a_reveal_not_carried_out_in_time_is_dropped(cx: &mut TestAppContext) {
3363 let (state, cx) = window(¶graphs(200), Container::Scrollable, cx);
3364 request(&state, "paragraph 150", cx, |_, cx| {
3365 cx.background_executor()
3366 .advance_clock(std::time::Duration::from_secs(2))
3367 });
3368 draw(cx);
3369 assert!(!is_pending(&state, cx));
3370 assert_eq!(scroll_top(&state, cx).item_ix, 0);
3371 }
3372
3373 #[gpui::test]
3374 fn an_empty_range_reveals_its_line(cx: &mut TestAppContext) {
3375 let (state, cx) = window(&words(400), Container::Scrollable, cx);
3376 reveal(&state, "w390", cx);
3377 let top = scroll_top(&state, cx);
3378 state.update(cx, |state, cx| {
3380 let text = state.rendered_text();
3381 let start = text.as_str().find("w391").unwrap();
3382 state.reveal_range(start..start, cx).unwrap();
3383 });
3384 for _ in 0..3 {
3385 draw(cx);
3386 }
3387 assert!(!is_pending(&state, cx));
3388 let now = scroll_top(&state, cx);
3389 assert_eq!(
3390 (now.item_ix, now.offset_in_item),
3391 (top.item_ix, top.offset_in_item)
3392 );
3393 }
3394
3395 #[gpui::test]
3396 fn a_position_after_the_last_character_reveals_its_line(cx: &mut TestAppContext) {
3397 let (state, cx) = window(&words(400), Container::Scrollable, cx);
3398 reveal(&state, "w399", cx);
3399 let top = scroll_top(&state, cx);
3400 assert!(top.offset_in_item > px(1000.), "{top:?}");
3401 state.update(cx, |state, cx| {
3403 let text = state.rendered_text();
3404 let end = text.as_str().find("w399").unwrap() + "w399".len();
3405 state.reveal_range(end..end, cx).unwrap();
3406 });
3407 for _ in 0..3 {
3408 draw(cx);
3409 }
3410 assert!(!is_pending(&state, cx));
3411 let now = scroll_top(&state, cx);
3412 assert_eq!(
3413 (now.item_ix, now.offset_in_item),
3414 (top.item_ix, top.offset_in_item)
3415 );
3416 }
3417
3418 fn reveal_at(
3420 state: &Entity<TextViewState>,
3421 range: impl FnOnce(&str) -> std::ops::Range<usize>,
3422 cx: &mut VisualTestContext,
3423 ) {
3424 state.update(cx, |state, cx| {
3425 let text = state.rendered_text();
3426 let range = range(text.as_str());
3427 state.reveal_range(range, cx).unwrap();
3428 });
3429 for _ in 0..3 {
3430 draw(cx);
3431 }
3432 }
3433
3434 fn assert_unmoved(
3435 state: &Entity<TextViewState>,
3436 top: gpui::ListOffset,
3437 cx: &mut VisualTestContext,
3438 ) {
3439 assert!(!is_pending(state, cx));
3440 let now = scroll_top(state, cx);
3441 assert_eq!(
3442 (now.item_ix, now.offset_in_item),
3443 (top.item_ix, top.offset_in_item)
3444 );
3445 }
3446
3447 #[gpui::test]
3448 fn the_end_of_the_text_and_its_separators_reveal_the_last_line(cx: &mut TestAppContext) {
3449 let (state, cx) = window(&words(400), Container::Scrollable, cx);
3450 reveal(&state, "w399", cx);
3451 let top = scroll_top(&state, cx);
3452 assert!(top.offset_in_item > px(1000.), "{top:?}");
3453 reveal_at(&state, |text| text.len()..text.len(), cx);
3455 assert_unmoved(&state, top, cx);
3456 reveal_at(&state, |text| text.len() - 1..text.len(), cx);
3458 assert_unmoved(&state, top, cx);
3459
3460 let code = (0..200)
3461 .map(|ix| format!("line {ix}"))
3462 .collect::<Vec<_>>()
3463 .join("\n");
3464 state.update(cx, |state, cx| {
3465 state.set_text(&format!("```\n{code}\n```"), cx)
3466 });
3467 cx.run_until_parked();
3468 reveal(&state, "line 199", cx);
3469 let top = scroll_top(&state, cx);
3470 assert!(top.offset_in_item > px(1000.), "{top:?}");
3471 reveal_at(&state, |text| text.len()..text.len(), cx);
3472 assert_unmoved(&state, top, cx);
3473 }
3474
3475 #[gpui::test]
3476 fn the_end_of_a_block_above_reveals_its_last_line(cx: &mut TestAppContext) {
3477 let second = (0..400)
3478 .map(|ix| format!("v{ix}"))
3479 .collect::<Vec<_>>()
3480 .join(" ");
3481 let markdown = format!("{}\n\n{second}", words(400));
3482 let (state, cx) = window(&markdown, Container::Scrollable, cx);
3483 reveal(&state, "v399", cx);
3484 assert_eq!(scroll_top(&state, cx).item_ix, 1);
3485 reveal_at(
3487 &state,
3488 |text| {
3489 let end = text.find("w399").unwrap() + "w399".len();
3490 end..end
3491 },
3492 cx,
3493 );
3494 assert!(!is_pending(&state, cx));
3495 let top = scroll_top(&state, cx);
3496 assert_eq!(top.item_ix, 0, "{top:?}");
3497 assert!(top.offset_in_item > px(1000.), "{top:?}");
3498 }
3499
3500 #[gpui::test]
3501 fn an_empty_view_has_nothing_to_reveal(cx: &mut TestAppContext) {
3502 cx.update(crate::init);
3503 let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown("", cx)));
3504 cx.run_until_parked();
3505 state.update(cx, |state, cx| {
3506 assert_eq!(state.reveal_range(0..0, cx), Ok(()));
3507 assert!(state.pending_reveal.is_none());
3508 });
3509 }
3510
3511 #[gpui::test]
3512 fn revealing_a_visible_block_does_not_scroll(cx: &mut TestAppContext) {
3513 let markdown = format!("{}\n\n<div>html</div>\n\n{}", words(400), words(400));
3514 let (state, cx) = window(&markdown, Container::Scrollable, cx);
3515 reveal(&state, "html", cx);
3516 state.update(cx, |state, _| state.list_state.scroll_by(px(30.)));
3518 draw(cx);
3519 let top = scroll_top(&state, cx);
3520 reveal(&state, "html", cx);
3521 assert_unmoved(&state, top, cx);
3522 }
3523
3524 #[gpui::test]
3525 fn a_line_of_an_inline_flow_counts_as_shown_once_scrolled_to(cx: &mut TestAppContext) {
3526 let markdown = (0..400)
3529 .map(|ix| {
3530 if ix % 10 == 0 {
3531 format!("`c{ix}`")
3532 } else {
3533 format!("w{ix}")
3534 }
3535 })
3536 .collect::<Vec<_>>()
3537 .join(" ");
3538 let (state, cx) = window(&markdown, Container::Scrollable, cx);
3539 reveal(&state, "c390", cx);
3540 assert!(!is_pending(&state, cx));
3541 assert!(scroll_top(&state, cx).offset_in_item > px(1000.));
3542 }
3543
3544 #[gpui::test]
3545 fn a_range_starting_on_a_line_break_reveals_the_next_line(cx: &mut TestAppContext) {
3546 let code = (0..200)
3547 .map(|ix| format!("line {ix}"))
3548 .collect::<Vec<_>>()
3549 .join("\n");
3550 let (state, cx) = window(&format!("```\n{code}\n```"), Container::Scrollable, cx);
3551 reveal(&state, "\nline 190", cx);
3552 assert!(!is_pending(&state, cx));
3553 let top = scroll_top(&state, cx);
3554 assert!(top.offset_in_item > px(1000.), "{top:?}");
3555 }
3556
3557 #[gpui::test]
3558 fn a_range_starting_on_a_line_break_in_an_inline_flow_reveals_the_next_line(
3559 cx: &mut TestAppContext,
3560 ) {
3561 let lines = (0..200)
3563 .map(|ix| format!("line {ix} `code`"))
3564 .collect::<Vec<_>>()
3565 .join("\\\n");
3566 let (state, cx) = window(&lines, Container::Scrollable, cx);
3567 reveal(&state, "\nline 190", cx);
3568 assert!(!is_pending(&state, cx));
3569 let top = scroll_top(&state, cx);
3570 assert!(top.offset_in_item > px(1000.), "{top:?}");
3571 }
3572
3573 #[gpui::test]
3574 fn a_reveal_is_dropped_when_its_text_before_it_changes(cx: &mut TestAppContext) {
3575 let (state, cx) = window(¶graphs(200), Container::Scrollable, cx);
3576 let target = "first words then the target";
3577 let markdown = format!("{target}\n\n{}", paragraphs(200));
3578 state.update(cx, |state, cx| state.set_text(&markdown, cx));
3579 cx.run_until_parked();
3580
3581 request(&state, "target", cx, |state, cx| {
3583 state.set_text(&markdown.replacen("target", "target and more", 1), cx);
3584 assert!(state.pending_reveal.is_some());
3585 });
3586 request(&state, "target", cx, |state, cx| {
3588 state.set_text(&markdown.replacen("words", "WORDS", 1), cx);
3589 assert!(state.pending_reveal.is_none());
3590 });
3591 }
3592
3593 #[gpui::test]
3594 fn a_clamped_view_does_not_reveal(cx: &mut TestAppContext) {
3595 let outer = ListState::new(2, ListAlignment::Top, px(1000.));
3596 let (state, cx) = window(&words(400), Container::Clamped(outer.clone()), cx);
3597 reveal(&state, "w390", cx);
3598 assert!(!is_pending(&state, cx));
3599 assert_eq!(outer.logical_scroll_top().item_ix, 0);
3600 }
3601
3602 #[gpui::test]
3603 fn text_outside_every_block_reveals_its_block(cx: &mut TestAppContext) {
3604 let markdown = format!("{}\n\n<div>html text</div>", paragraphs(100));
3605 let (state, cx) = window(&markdown, Container::Scrollable, cx);
3606 reveal(&state, "html text", cx);
3607 assert!(!is_pending(&state, cx));
3608 assert!(
3609 scroll_top(&state, cx).item_ix >= 90,
3610 "{:?}",
3611 scroll_top(&state, cx)
3612 );
3613 }
3614
3615 #[gpui::test]
3616 fn a_reveal_follows_its_text_past_an_edit_before_it(cx: &mut TestAppContext) {
3617 let (state, cx) = window(¶graphs(200), Container::Scrollable, cx);
3618 request(&state, "paragraph 150", cx, |state, cx| {
3620 state.set_text(&format!("inserted\n\n{}", paragraphs(200)), cx);
3621 assert!(state.pending_reveal.is_some());
3622 });
3623 for _ in 0..3 {
3624 draw(cx);
3625 }
3626 assert!(!is_pending(&state, cx));
3627 let top = scroll_top(&state, cx);
3628 assert!((141..=151).contains(&top.item_ix), "{top:?}");
3629
3630 request(&state, "paragraph 150", cx, |state, cx| {
3632 state.set_text(¶graphs(200).replace("paragraph 150", "changed"), cx);
3633 assert!(state.pending_reveal.is_none());
3634 });
3635 }
3636
3637 #[gpui::test]
3638 fn malformed_ranges_and_html_views_are_rejected(cx: &mut TestAppContext) {
3639 cx.update(crate::init);
3640 let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown("first\n\nsecond", cx)));
3641 cx.run_until_parked();
3642 state.update(cx, |state, cx| {
3643 assert_eq!(
3644 state.reveal_range(std::ops::Range { start: 5, end: 3 }, cx),
3645 Err(RangeHighlightError::InvalidRange(0))
3646 );
3647 assert_eq!(
3648 state.reveal_range(0..100, cx),
3649 Err(RangeHighlightError::InvalidRange(0))
3650 );
3651 state.reveal_range(0..5, cx).unwrap();
3652 });
3653
3654 let html = cx.update(|cx| cx.new(|cx| TextViewState::html("<p>one</p>", cx)));
3655 cx.run_until_parked();
3656 html.update(cx, |html, cx| {
3657 assert_eq!(
3658 html.reveal_range(0..3, cx),
3659 Err(RangeHighlightError::Unsupported)
3660 );
3661 });
3662 }
3663 }
3664}