1use std::future::Future;
2use std::pin::Pin;
3use std::sync::Arc;
4use std::task::Poll;
5use std::time::Duration;
6
7use gpui::prelude::FluentBuilder;
8use gpui::{
9 AnyElement, App, AppContext, Bounds, ClipboardItem, Context, Element, ElementId, Entity,
10 EntityId, FocusHandle, GlobalElementId, InspectorElementId, InteractiveElement, IntoElement,
11 KeyBinding, LayoutId, ListState, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement,
12 Pixels, Point, RenderOnce, SharedString, Size, StyleRefinement, Styled, Timer, Window, div, px,
13};
14use smol::stream::StreamExt;
15
16use crate::highlighter::HighlightTheme;
17use crate::scroll::ScrollableElement;
18use crate::text::node::CodeBlock;
19use crate::{ActiveTheme, StyledExt, v_flex};
20use crate::{
21 global_state::GlobalState,
22 input::{self},
23 text::{
24 TextViewStyle,
25 node::{self, NodeContext},
26 },
27};
28
29const CONTEXT: &'static str = "TextView";
30
31pub(crate) fn init(cx: &mut App) {
32 cx.bind_keys(vec![
33 #[cfg(target_os = "macos")]
34 KeyBinding::new("cmd-c", input::Copy, Some(CONTEXT)),
35 #[cfg(not(target_os = "macos"))]
36 KeyBinding::new("ctrl-c", input::Copy, Some(CONTEXT)),
37 ]);
38}
39
40#[derive(IntoElement, Clone)]
41struct TextViewElement {
42 list_state: Option<ListState>,
43 state: Entity<TextViewState>,
44}
45
46impl RenderOnce for TextViewElement {
47 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
48 self.state.update(cx, |state, cx| {
49 v_flex()
50 .size_full()
51 .map(|this| match &mut state.parsed_result {
52 Some(Ok(content)) => this.child(content.root_node.render_root(
53 self.list_state.clone(),
54 &content.node_cx,
55 window,
56 cx,
57 )),
58 Some(Err(err)) => this.child(
59 v_flex()
60 .gap_1()
61 .child("Failed to parse content")
62 .child(err.to_string()),
63 ),
64 None => this,
65 })
66 })
67 }
68}
69
70pub(crate) type CodeBlockActionsFn =
72 dyn Fn(&CodeBlock, &mut Window, &mut App) -> AnyElement + Send + Sync;
73
74#[derive(Clone)]
91pub struct TextView {
92 id: ElementId,
93 init_state: Option<InitState>,
94 raw: SharedString,
95 state: Entity<TextViewState>,
96 style: StyleRefinement,
97 selectable: bool,
98 scrollable: bool,
99 code_block_actions: Option<Arc<CodeBlockActionsFn>>,
100}
101
102#[derive(PartialEq)]
103pub(crate) struct ParsedContent {
104 pub(crate) root_node: node::Node,
105 pub(crate) node_cx: node::NodeContext,
106}
107
108#[derive(Clone, Copy, PartialEq, Eq)]
110enum TextViewType {
111 Markdown,
113 Html,
115}
116
117enum Update {
118 Text(SharedString),
119 Style(Box<TextViewStyle>),
120}
121
122struct UpdateFuture {
123 type_: TextViewType,
124 highlight_theme: Arc<HighlightTheme>,
125 current_style: TextViewStyle,
126 current_text: SharedString,
127 timer: Timer,
128 rx: Pin<Box<smol::channel::Receiver<Update>>>,
129 tx_result: smol::channel::Sender<Result<ParsedContent, SharedString>>,
130 delay: Duration,
131 code_block_actions: Option<Arc<CodeBlockActionsFn>>,
132}
133
134impl UpdateFuture {
135 #[allow(clippy::too_many_arguments)]
136 fn new(
137 type_: TextViewType,
138 style: TextViewStyle,
139 text: SharedString,
140 highlight_theme: Arc<HighlightTheme>,
141 rx: smol::channel::Receiver<Update>,
142 tx_result: smol::channel::Sender<Result<ParsedContent, SharedString>>,
143 delay: Duration,
144 code_block_actions: Option<Arc<CodeBlockActionsFn>>,
145 ) -> Self {
146 Self {
147 type_,
148 highlight_theme,
149 current_style: style,
150 current_text: text,
151 timer: Timer::never(),
152 rx: Box::pin(rx),
153 tx_result,
154 delay,
155 code_block_actions,
156 }
157 }
158}
159
160impl Future for UpdateFuture {
161 type Output = ();
162
163 fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
164 loop {
165 match self.rx.poll_next(cx) {
166 Poll::Ready(Some(update)) => {
167 let changed = match update {
168 Update::Text(text) if self.current_text != text => {
169 self.current_text = text;
170 true
171 }
172 Update::Style(style) if self.current_style != *style => {
173 self.current_style = *style;
174 true
175 }
176 _ => false,
177 };
178 if changed {
179 let delay = self.delay;
180 self.timer.set_after(delay);
181 }
182 continue;
183 }
184 Poll::Ready(None) => return Poll::Ready(()),
185 Poll::Pending => {}
186 }
187
188 match self.timer.poll_next(cx) {
189 Poll::Ready(Some(_)) => {
190 let res = parse_content(
191 self.type_,
192 &self.current_text,
193 self.current_style.clone(),
194 &self.highlight_theme,
195 &self.code_block_actions.clone(),
196 );
197 _ = self.tx_result.try_send(res);
198 continue;
199 }
200 Poll::Ready(None) | Poll::Pending => return Poll::Pending,
201 }
202 }
203 }
204}
205
206#[derive(Clone)]
207enum InitState {
208 Initializing {
209 type_: TextViewType,
210 text: SharedString,
211 style: Box<TextViewStyle>,
212 highlight_theme: Arc<HighlightTheme>,
213 },
214 Initialized {
215 tx: smol::channel::Sender<Update>,
216 },
217}
218
219pub(crate) struct TextViewState {
220 parent_entity: Option<EntityId>,
221 tx: Option<smol::channel::Sender<Update>>,
222 parsed_result: Option<Result<ParsedContent, SharedString>>,
223 focus_handle: Option<FocusHandle>,
224 bounds: Bounds<Pixels>,
226 selection_positions: (Option<Point<Pixels>>, Option<Point<Pixels>>),
228 is_selecting: bool,
230 is_selectable: bool,
231 list_state: ListState,
232}
233
234impl TextViewState {
235 fn new(cx: &mut Context<TextViewState>) -> Self {
236 let focus_handle = cx.focus_handle();
237 Self {
238 parent_entity: None,
239 tx: None,
240 parsed_result: None,
241 focus_handle: Some(focus_handle),
242 bounds: Bounds::default(),
243 selection_positions: (None, None),
244 is_selecting: false,
245 is_selectable: false,
246 list_state: ListState::new(0, gpui::ListAlignment::Top, px(1000.)),
247 }
248 }
249}
250
251impl TextViewState {
252 fn update_bounds(&mut self, bounds: Bounds<Pixels>) {
254 if self.bounds.size != bounds.size {
255 self.clear_selection();
256 }
257 self.bounds = bounds;
258 }
259
260 fn clear_selection(&mut self) {
261 self.selection_positions = (None, None);
262 self.is_selecting = false;
263 }
264
265 fn start_selection(&mut self, pos: Point<Pixels>) {
266 let pos = pos - self.bounds.origin;
267 self.selection_positions = (Some(pos), Some(pos));
268 self.is_selecting = true;
269 }
270
271 fn update_selection(&mut self, pos: Point<Pixels>) {
272 let pos = pos - self.bounds.origin;
273 if let (Some(start), Some(_)) = self.selection_positions {
274 self.selection_positions = (Some(start), Some(pos))
275 }
276 }
277
278 fn end_selection(&mut self) {
279 self.is_selecting = false;
280 }
281
282 pub(crate) fn has_selection(&self) -> bool {
283 if let (Some(start), Some(end)) = self.selection_positions {
284 start != end
285 } else {
286 false
287 }
288 }
289
290 pub(crate) fn is_selectable(&self) -> bool {
291 self.is_selectable
292 }
293
294 pub(crate) fn selection_bounds(&self) -> Bounds<Pixels> {
296 selection_bounds(
297 self.selection_positions.0,
298 self.selection_positions.1,
299 self.bounds,
300 )
301 }
302
303 fn selection_text(&self) -> Option<String> {
304 Some(
305 self.parsed_result
306 .as_ref()?
307 .as_ref()
308 .ok()?
309 .root_node
310 .selected_text(),
311 )
312 }
313}
314
315#[derive(IntoElement, Clone)]
316pub enum Text {
317 String(SharedString),
318 TextView(Box<TextView>),
319}
320
321impl From<SharedString> for Text {
322 fn from(s: SharedString) -> Self {
323 Self::String(s)
324 }
325}
326
327impl From<&str> for Text {
328 fn from(s: &str) -> Self {
329 Self::String(SharedString::from(s.to_string()))
330 }
331}
332
333impl From<String> for Text {
334 fn from(s: String) -> Self {
335 Self::String(s.into())
336 }
337}
338
339impl From<TextView> for Text {
340 fn from(e: TextView) -> Self {
341 Self::TextView(Box::new(e))
342 }
343}
344
345impl Text {
346 pub fn style(self, style: TextViewStyle) -> Self {
350 match self {
351 Self::String(s) => Self::String(s),
352 Self::TextView(e) => Self::TextView(Box::new(e.style(style))),
353 }
354 }
355
356 pub fn as_str(&self) -> &str {
358 match self {
359 Self::String(s) => s.as_str(),
360 Self::TextView(view) => view.raw.as_str(),
361 }
362 }
363}
364
365impl RenderOnce for Text {
366 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
367 match self {
368 Self::String(s) => s.into_any_element(),
369 Self::TextView(e) => e.into_any_element(),
370 }
371 }
372}
373
374impl Styled for TextView {
375 fn style(&mut self) -> &mut StyleRefinement {
376 &mut self.style
377 }
378}
379
380impl TextView {
381 fn create_init_state(
382 type_: TextViewType,
383 text: &SharedString,
384 highlight_theme: &Arc<HighlightTheme>,
385 state: &Entity<TextViewState>,
386 cx: &mut App,
387 ) -> InitState {
388 let state = state.read(cx);
389 if let Some(tx) = &state.tx {
390 InitState::Initialized { tx: tx.clone() }
391 } else {
392 InitState::Initializing {
393 type_,
394 text: text.clone(),
395 style: Default::default(),
396 highlight_theme: highlight_theme.clone(),
397 }
398 }
399 }
400
401 pub fn markdown(
403 id: impl Into<ElementId>,
404 markdown: impl Into<SharedString>,
405 window: &mut Window,
406 cx: &mut App,
407 ) -> Self {
408 let id: ElementId = id.into();
409 let markdown = markdown.into();
410 let highlight_theme = cx.theme().highlight_theme.clone();
411 let state =
412 window.use_keyed_state(SharedString::from(format!("{}/state", id)), cx, |_, cx| {
413 TextViewState::new(cx)
414 });
415 let init_state = Self::create_init_state(
416 TextViewType::Markdown,
417 &markdown,
418 &highlight_theme,
419 &state,
420 cx,
421 );
422 if let Some(tx) = &state.read(cx).tx {
423 let _ = tx.try_send(Update::Text(markdown.clone()));
424 }
425 Self {
426 id,
427 init_state: Some(init_state),
428 raw: markdown.clone(),
429 style: StyleRefinement::default(),
430 state,
431 selectable: false,
432 scrollable: false,
433 code_block_actions: None,
434 }
435 }
436
437 pub fn html(
439 id: impl Into<ElementId>,
440 html: impl Into<SharedString>,
441 window: &mut Window,
442 cx: &mut App,
443 ) -> Self {
444 let id: ElementId = id.into();
445 let html = html.into();
446 let highlight_theme = cx.theme().highlight_theme.clone();
447 let state =
448 window.use_keyed_state(SharedString::from(format!("{}/state", id)), cx, |_, cx| {
449 TextViewState::new(cx)
450 });
451 let init_state =
452 Self::create_init_state(TextViewType::Html, &html, &highlight_theme, &state, cx);
453 if let Some(tx) = &state.read(cx).tx {
454 let _ = tx.try_send(Update::Text(html.clone()));
455 }
456 Self {
457 id,
458 init_state: Some(init_state),
459 style: StyleRefinement::default(),
460 state,
461 raw: html,
462 selectable: false,
463 scrollable: false,
464 code_block_actions: None,
465 }
466 }
467
468 pub fn text(mut self, raw: impl Into<SharedString>) -> Self {
470 let raw: SharedString = raw.into();
471 if let Some(init_state) = &mut self.init_state {
472 match init_state {
473 InitState::Initializing { text, .. } => *text = raw.clone(),
474 InitState::Initialized { tx } => {
475 let _ = tx.try_send(Update::Text(raw.clone()));
476 }
477 }
478 }
479 self.raw = raw;
480 self
481 }
482
483 pub fn style(mut self, style: TextViewStyle) -> Self {
485 if let Some(init_state) = &mut self.init_state {
486 match init_state {
487 InitState::Initializing { style: s, .. } => *s = Box::new(style),
488 InitState::Initialized { tx } => {
489 let _ = tx.try_send(Update::Style(Box::new(style)));
490 }
491 }
492 }
493 self
494 }
495
496 pub fn selectable(mut self, selectable: bool) -> Self {
498 self.selectable = selectable;
499 self
500 }
501
502 pub fn scrollable(mut self, scrollable: bool) -> Self {
515 self.scrollable = scrollable;
516 self
517 }
518
519 fn on_action_copy(state: &Entity<TextViewState>, cx: &mut App) {
520 let Some(selected_text) = state.read(cx).selection_text() else {
521 return;
522 };
523
524 cx.write_to_clipboard(ClipboardItem::new_string(selected_text.trim().to_string()));
525 }
526
527 pub fn code_block_actions<F, E>(mut self, f: F) -> Self
532 where
533 F: Fn(&CodeBlock, &mut Window, &mut App) -> E + Send + Sync + 'static,
534 E: IntoElement,
535 {
536 self.code_block_actions = Some(Arc::new(move |code_block, window, cx| {
537 f(&code_block, window, cx).into_any_element()
538 }));
539 self
540 }
541}
542
543impl IntoElement for TextView {
544 type Element = Self;
545
546 fn into_element(self) -> Self::Element {
547 self
548 }
549}
550
551impl Element for TextView {
552 type RequestLayoutState = AnyElement;
553 type PrepaintState = ();
554
555 fn id(&self) -> Option<ElementId> {
556 Some(self.id.clone())
557 }
558
559 fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
560 None
561 }
562
563 fn request_layout(
564 &mut self,
565 _: Option<&GlobalElementId>,
566 _: Option<&InspectorElementId>,
567 window: &mut Window,
568 cx: &mut App,
569 ) -> (LayoutId, Self::RequestLayoutState) {
570 if let Some(InitState::Initializing {
571 type_,
572 text,
573 style,
574 highlight_theme,
575 }) = self.init_state.take()
576 {
577 let style = *style;
578 let highlight_theme = highlight_theme.clone();
579 let code_block_actions = self.code_block_actions.clone();
580 let (tx, rx) = smol::channel::unbounded::<Update>();
581 let (tx_result, rx_result) =
582 smol::channel::unbounded::<Result<ParsedContent, SharedString>>();
583 let parsed_result = parse_content(
584 type_,
585 &text,
586 style.clone(),
587 &highlight_theme,
588 &code_block_actions,
589 );
590
591 self.state.update(cx, {
592 let tx = tx.clone();
593 |state, _| {
594 state.parsed_result = Some(parsed_result);
595 state.tx = Some(tx);
596 }
597 });
598
599 cx.spawn({
600 let state = self.state.downgrade();
601 async move |cx| {
602 while let Ok(parsed_result) = rx_result.recv().await {
603 if let Some(state) = state.upgrade() {
604 _ = state.update(cx, |state, cx| {
605 state.parsed_result = Some(parsed_result);
606 if let Some(parent_entity) = state.parent_entity {
607 let app = &mut **cx;
608 app.notify(parent_entity);
609 }
610 state.clear_selection();
611 });
612 } else {
613 break;
615 }
616 }
617 }
618 })
619 .detach();
620
621 cx.background_spawn(UpdateFuture::new(
622 type_,
623 style,
624 text,
625 highlight_theme,
626 rx,
627 tx_result,
628 Duration::from_millis(200),
629 code_block_actions,
630 ))
631 .detach();
632
633 self.init_state = Some(InitState::Initialized { tx });
634 }
635
636 let list_state = &self.state.read(cx).list_state;
637
638 let focus_handle = self
639 .state
640 .read(cx)
641 .focus_handle
642 .as_ref()
643 .expect("focus_handle should init by TextViewState::new");
644
645 let mut el = div()
646 .key_context(CONTEXT)
647 .track_focus(focus_handle)
648 .size_full()
649 .relative()
650 .on_action({
651 let state = self.state.clone();
652 move |_: &input::Copy, _, cx| {
653 Self::on_action_copy(&state, cx);
654 }
655 })
656 .child(TextViewElement {
657 list_state: if self.scrollable {
658 Some(list_state.clone())
659 } else {
660 None
661 },
662 state: self.state.clone(),
663 })
664 .refine_style(&self.style)
665 .vertical_scrollbar(list_state)
666 .into_any_element();
667 let layout_id = el.request_layout(window, cx);
668 (layout_id, el)
669 }
670
671 fn prepaint(
672 &mut self,
673 _: Option<&GlobalElementId>,
674 _: Option<&InspectorElementId>,
675 _: Bounds<Pixels>,
676 request_layout: &mut Self::RequestLayoutState,
677 window: &mut Window,
678 cx: &mut App,
679 ) -> Self::PrepaintState {
680 request_layout.prepaint(window, cx);
681 }
682
683 fn paint(
684 &mut self,
685 _: Option<&GlobalElementId>,
686 _: Option<&InspectorElementId>,
687 bounds: Bounds<Pixels>,
688 request_layout: &mut Self::RequestLayoutState,
689 _: &mut Self::PrepaintState,
690 window: &mut Window,
691 cx: &mut App,
692 ) {
693 let entity_id = window.current_view();
694 let is_selectable = self.selectable;
695
696 self.state.update(cx, |state, _| {
697 state.parent_entity = Some(entity_id);
698 state.update_bounds(bounds);
699 state.is_selectable = is_selectable;
700 });
701
702 GlobalState::global_mut(cx)
703 .text_view_state_stack
704 .push(self.state.clone());
705 request_layout.paint(window, cx);
706 GlobalState::global_mut(cx).text_view_state_stack.pop();
707
708 if self.selectable {
709 let is_selecting = self.state.read(cx).is_selecting;
710 let has_selection = self.state.read(cx).has_selection();
711
712 window.on_mouse_event({
713 let state = self.state.clone();
714 move |event: &MouseDownEvent, phase, _, cx| {
715 if !bounds.contains(&event.position) || !phase.bubble() {
716 return;
717 }
718
719 state.update(cx, |state, _| {
720 state.start_selection(event.position);
721 });
722 cx.notify(entity_id);
723 }
724 });
725
726 if is_selecting {
727 window.on_mouse_event({
729 let state = self.state.clone();
730 move |event: &MouseMoveEvent, phase, _, cx| {
731 if !phase.bubble() {
732 return;
733 }
734
735 state.update(cx, |state, _| {
736 state.update_selection(event.position);
737 });
738 cx.notify(entity_id);
739 }
740 });
741
742 window.on_mouse_event({
744 let state = self.state.clone();
745 move |_: &MouseUpEvent, phase, _, cx| {
746 if !phase.bubble() {
747 return;
748 }
749
750 state.update(cx, |state, _| {
751 state.end_selection();
752 });
753 cx.notify(entity_id);
754 }
755 });
756 }
757
758 if has_selection {
759 window.on_mouse_event({
761 let state = self.state.clone();
762 move |event: &MouseDownEvent, _, _, cx| {
763 if bounds.contains(&event.position) {
764 return;
765 }
766
767 state.update(cx, |state, _| {
768 state.clear_selection();
769 });
770 cx.notify(entity_id);
771 }
772 });
773 }
774 }
775 }
776}
777
778fn parse_content(
779 type_: TextViewType,
780 text: &str,
781 style: TextViewStyle,
782 highlight_theme: &HighlightTheme,
783 code_block_actions: &Option<Arc<CodeBlockActionsFn>>,
784) -> Result<ParsedContent, SharedString> {
785 let mut node_cx = NodeContext {
786 style: style.clone(),
787 code_block_actions: code_block_actions.clone(),
788 ..NodeContext::default()
789 };
790
791 let res = match type_ {
792 TextViewType::Markdown => {
793 super::format::markdown::parse(text, &style, &mut node_cx, highlight_theme)
794 }
795 TextViewType::Html => super::format::html::parse(text, &mut node_cx),
796 };
797 res.map(move |root_node| ParsedContent { root_node, node_cx })
798}
799
800fn selection_bounds(
801 start: Option<Point<Pixels>>,
802 end: Option<Point<Pixels>>,
803 bounds: Bounds<Pixels>,
804) -> Bounds<Pixels> {
805 if let (Some(start), Some(end)) = (start, end) {
806 let start = start + bounds.origin;
807 let end = end + bounds.origin;
808
809 let origin = Point {
810 x: start.x.min(end.x),
811 y: start.y.min(end.y),
812 };
813 let size = Size {
814 width: (start.x - end.x).abs(),
815 height: (start.y - end.y).abs(),
816 };
817
818 return Bounds { origin, size };
819 }
820
821 Bounds::default()
822}
823
824#[cfg(test)]
825mod tests {
826 use super::*;
827 use gpui::{Bounds, point, px, size};
828
829 #[test]
830 fn test_text_view_state_selection_bounds() {
831 assert_eq!(
832 selection_bounds(None, None, Default::default()),
833 Bounds::default()
834 );
835 assert_eq!(
836 selection_bounds(None, Some(point(px(10.), px(20.))), Default::default()),
837 Bounds::default()
838 );
839 assert_eq!(
840 selection_bounds(Some(point(px(10.), px(20.))), None, Default::default()),
841 Bounds::default()
842 );
843
844 assert_eq!(
850 selection_bounds(
851 Some(point(px(10.), px(10.))),
852 Some(point(px(50.), px(50.))),
853 Default::default()
854 ),
855 Bounds {
856 origin: point(px(10.), px(10.)),
857 size: size(px(40.), px(40.))
858 }
859 );
860 assert_eq!(
866 selection_bounds(
867 Some(point(px(50.), px(50.))),
868 Some(point(px(10.), px(10.))),
869 Default::default()
870 ),
871 Bounds {
872 origin: point(px(10.), px(10.)),
873 size: size(px(40.), px(40.))
874 }
875 );
876 assert_eq!(
882 selection_bounds(
883 Some(point(px(50.), px(10.))),
884 Some(point(px(10.), px(50.))),
885 Default::default()
886 ),
887 Bounds {
888 origin: point(px(10.), px(10.)),
889 size: size(px(40.), px(40.))
890 }
891 );
892 assert_eq!(
898 selection_bounds(
899 Some(point(px(10.), px(50.))),
900 Some(point(px(50.), px(10.))),
901 Default::default()
902 ),
903 Bounds {
904 origin: point(px(10.), px(10.)),
905 size: size(px(40.), px(40.))
906 }
907 );
908 }
909}