1use futures::Stream as _;
2use std::{pin::Pin, task::Poll};
3
4use rgpui::{
5 App, AppContext as _, Bounds, Context, FocusHandle, IntoElement, KeyBinding, ListState,
6 ParentElement as _, Pixels, Point, Render, SharedString, Styled as _, Task, Window,
7 prelude::FluentBuilder as _, px,
8};
9
10use crate::{
11 ActiveTheme, ElementExt, HighlightTheme,
12 async_util::{Receiver, Sender, unbounded},
13 input::{self, SelectAll},
14 scroll::AutoScroll,
15 text::{
16 CodeBlockActionsFn, TextViewStyle,
17 document::ParsedDocument,
18 format,
19 node::{self, NodeContext},
20 },
21 v_flex,
22};
23
24const CONTEXT: &'static str = "TextView";
25pub(crate) fn init(cx: &mut App) {
26 cx.bind_keys(vec![
27 #[cfg(target_os = "macos")]
28 KeyBinding::new("cmd-c", input::Copy, Some(CONTEXT)),
29 #[cfg(not(target_os = "macos"))]
30 KeyBinding::new("ctrl-c", input::Copy, Some(CONTEXT)),
31 #[cfg(target_os = "macos")]
32 KeyBinding::new("cmd-a", input::SelectAll, Some(CONTEXT)),
33 #[cfg(not(target_os = "macos"))]
34 KeyBinding::new("ctrl-a", input::SelectAll, Some(CONTEXT)),
35 ]);
36}
37
38#[derive(Clone, Copy, PartialEq, Eq)]
40pub(super) enum TextViewFormat {
41 Markdown,
43 Html,
45 Plain,
47}
48
49pub struct TextViewState {
51 pub(super) focus_handle: FocusHandle,
52 pub(super) entity_id: rgpui::EntityId,
53 pub(super) list_state: ListState,
54
55 bounds: Bounds<Pixels>,
57
58 pub(super) selectable: bool,
59 pub(super) scrollable: bool,
60 pub(super) text_view_style: TextViewStyle,
61 pub(super) code_block_actions: Option<std::sync::Arc<CodeBlockActionsFn>>,
62
63 pub(super) is_selecting: bool,
64 multi_click_selection: Option<TextViewMultiClickSelection>,
65 selected_text_override: Option<String>,
66 select_all: bool,
67 pub(super) auto_scroll: AutoScroll,
68
69 pub(super) parsed_content: ParsedContent,
70 text: String,
71 parsed_error: Option<SharedString>,
72 tx: Sender<UpdateOptions>,
73 _parse_task: Task<()>,
74 _receive_task: Task<()>,
75}
76
77impl TextViewState {
78 pub fn markdown(text: &str, cx: &mut Context<Self>) -> Self {
80 Self::new(TextViewFormat::Markdown, text, cx)
81 }
82
83 pub fn html(text: &str, cx: &mut Context<Self>) -> Self {
85 Self::new(TextViewFormat::Html, text, cx)
86 }
87
88 pub fn plain(text: &str, cx: &mut Context<Self>) -> Self {
90 Self::new(TextViewFormat::Plain, text, cx)
91 }
92
93 fn new(format: TextViewFormat, text: &str, cx: &mut Context<Self>) -> Self {
95 let focus_handle = cx.focus_handle();
96 let entity_id = cx.entity_id();
97
98 let (tx, rx) = unbounded::<UpdateOptions>();
99 let (tx_result, rx_result) = unbounded::<Result<ParsedContent, SharedString>>();
100 let _receive_task = cx.spawn({
101 async move |weak_self, cx| {
102 while let Ok(parsed_result) = rx_result.recv().await {
103 _ = weak_self.update(cx, |state, cx| {
104 match parsed_result {
105 Ok(content) => {
106 state.parsed_content = content;
107 state.parsed_error = None;
108 }
109 Err(err) => {
110 state.parsed_error = Some(err);
111 }
112 }
113 if !state.is_selecting {
117 state.reset_selection();
118 }
119 cx.notify();
120 });
121 }
122 }
123 });
124
125 let _parse_task = cx.background_spawn(UpdateFuture::new(format, rx, tx_result, cx));
126
127 let mut this = Self {
128 focus_handle,
129 entity_id,
130 bounds: Bounds::default(),
131 multi_click_selection: None,
132 selected_text_override: None,
133 select_all: false,
134 selectable: false,
135 scrollable: false,
136 list_state: ListState::new(0, rgpui::ListAlignment::Top, px(1000.)),
137 text_view_style: TextViewStyle::default(),
138 code_block_actions: None,
139 is_selecting: false,
140 auto_scroll: AutoScroll::default(),
141 parsed_content: Default::default(),
142 parsed_error: None,
143 text: text.to_string(),
144 tx,
145 _parse_task,
146 _receive_task,
147 };
148 this.increment_update(&text, false, cx);
149 this
150 }
151
152 pub(crate) fn source(&self) -> SharedString {
154 self.parsed_content.document.source.clone()
155 }
156
157 pub fn selectable(mut self, selectable: bool) -> Self {
159 self.selectable = selectable;
160 self
161 }
162
163 pub fn set_selectable(&mut self, selectable: bool, cx: &mut Context<Self>) {
165 self.selectable = selectable;
166 cx.notify();
167 }
168
169 pub fn scrollable(mut self, scrollable: bool) -> Self {
171 self.scrollable = scrollable;
172 self
173 }
174
175 pub fn set_scrollable(&mut self, scrollable: bool, cx: &mut Context<Self>) {
177 if !scrollable {
178 self.reset_selection();
179 }
180 self.scrollable = scrollable;
181 cx.notify();
182 }
183
184 pub fn set_text(&mut self, text: &str, cx: &mut Context<Self>) {
186 if self.text.as_str() == text {
187 return;
188 }
189
190 self.text.clear();
191 self.text.push_str(text);
192 self.parsed_error = None;
193 self.increment_update(text, false, cx);
194 }
195
196 pub fn push_str(&mut self, new_text: &str, cx: &mut Context<Self>) {
198 if new_text.is_empty() {
199 return;
200 }
201 self.text.push_str(new_text);
202 self.increment_update(new_text, true, cx);
203 }
204
205 pub fn selected_text(&self) -> String {
207 if self.select_all {
208 return self.parsed_content.document.text();
209 }
210
211 if let Some(text) = &self.selected_text_override {
212 return text.clone();
213 }
214
215 self.parsed_content.document.selected_text()
216 }
217
218 fn increment_update(&mut self, text: &str, append: bool, cx: &mut Context<Self>) {
219 let update_options = UpdateOptions {
220 append,
221 pending_text: text.to_string(),
222 highlight_theme: cx.theme().highlight_theme.clone(),
223 };
224
225 _ = self.tx.try_send(update_options);
226 }
227
228 pub(super) fn update_bounds(&mut self, bounds: Bounds<Pixels>) {
230 if self.bounds.size != bounds.size {
231 self.reset_selection();
232 }
233 self.bounds = bounds;
234 }
235
236 pub(super) fn bounds(&self) -> Bounds<Pixels> {
237 self.bounds
238 }
239
240 pub(super) fn has_view_selection(&self) -> bool {
243 self.select_all
244 || self.multi_click_selection.is_some()
245 || self.selected_text_override.is_some()
246 }
247
248 pub(super) fn stop_auto_scroll(&mut self) {
249 self.auto_scroll.stop();
250 }
251
252 fn reset_selection(&mut self) {
253 self.multi_click_selection = None;
254 self.selected_text_override = None;
255 self.select_all = false;
256 self.is_selecting = false;
257 self.auto_scroll.stop();
258 self.parsed_content.document.clear_selection();
262 }
263
264 pub fn clear_selection(&mut self, cx: &mut Context<Self>) {
266 self.reset_selection();
267 cx.notify();
268 }
269
270 pub(super) fn scroll_offset(&self) -> Point<Pixels> {
271 if self.scrollable {
272 self.list_state.scroll_px_offset_for_scrollbar()
273 } else {
274 Point::default()
275 }
276 }
277
278 pub fn select_all(&mut self, cx: &mut Context<Self>) {
280 self.multi_click_selection = None;
281 self.selected_text_override = None;
282 self.select_all = true;
283 self.is_selecting = false;
284 self.auto_scroll.stop();
285 cx.notify();
286 }
287
288 pub(crate) fn set_multi_click_selection(
289 &mut self,
290 pos: Point<Pixels>,
291 kind: TextViewMultiClickKind,
292 selected_text: String,
293 ) {
294 let scroll_offset = self.scroll_offset();
295 let pos = pos - self.bounds.origin - scroll_offset;
296 self.multi_click_selection = Some(TextViewMultiClickSelection { pos, kind });
297 self.selected_text_override = Some(selected_text);
298 self.select_all = false;
299 self.is_selecting = false;
300 self.auto_scroll.stop();
301 }
302
303 pub(super) fn set_auto_scroll(&mut self, delta: Option<Pixels>, cx: &mut Context<Self>) {
304 self.auto_scroll.set(delta, cx, |delta, state, cx| {
305 state.list_state.scroll_by(delta);
306 cx.notify();
307 });
308 }
309
310 pub(crate) fn selection_points(
317 &self,
318 window: &Window,
319 cx: &App,
320 ) -> Option<(Point<Pixels>, Point<Pixels>)> {
321 if !self.selectable {
322 return None;
323 }
324 let root = window.root::<crate::Root>().flatten()?;
325 let selection = &root.read(cx).text_selection;
326 if let Some(view_id) = selection.single_view() {
327 if view_id != self.entity_id {
328 return None;
329 }
330 }
331 selection.resolved_points(cx)
332 }
333
334 pub(crate) fn has_selection(&self, window: &Window, cx: &App) -> bool {
335 self.has_view_selection() || self.selection_points(window, cx).is_some()
336 }
337
338 pub(super) fn on_action_select_all(
339 &mut self,
340 _: &SelectAll,
341 _: &mut Window,
342 cx: &mut Context<Self>,
343 ) {
344 if !self.selectable {
345 cx.propagate();
346 return;
347 }
348
349 self.select_all(cx);
350 }
351
352 pub(crate) fn is_selectable(&self) -> bool {
353 self.selectable
354 }
355
356 pub(crate) fn is_all_selected(&self) -> bool {
357 self.select_all
358 }
359
360 pub(crate) fn multi_click_selection(&self) -> Option<TextViewMultiClickSelection> {
361 let scroll_offset = self.scroll_offset();
362 self.multi_click_selection.map(|selection| {
363 let pos = selection.pos + scroll_offset + self.bounds.origin;
364 TextViewMultiClickSelection { pos, ..selection }
365 })
366 }
367}
368
369#[derive(Clone, Copy, Debug, PartialEq)]
370pub(crate) struct TextViewMultiClickSelection {
371 pub(crate) pos: Point<Pixels>,
372 pub(crate) kind: TextViewMultiClickKind,
373}
374
375#[derive(Clone, Copy, Debug, PartialEq, Eq)]
376pub(crate) enum TextViewMultiClickKind {
377 Word,
378 Paragraph,
379}
380
381impl Render for TextViewState {
382 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
383 let state = cx.entity();
384 let document = self.parsed_content.document.clone();
385 let mut node_cx = self.parsed_content.node_cx.clone();
386
387 node_cx.code_block_actions = self.code_block_actions.clone();
388 node_cx.style = self.text_view_style.clone();
389
390 v_flex()
391 .size_full()
392 .map(|this| match &mut self.parsed_error {
393 None => this.child(document.render_root(
394 if self.scrollable {
395 Some(self.list_state.clone())
396 } else {
397 None
398 },
399 &node_cx,
400 window,
401 cx,
402 )),
403 Some(err) => this.child(
404 v_flex()
405 .gap_1()
406 .child("Failed to parse content")
407 .child(err.to_string()),
408 ),
409 })
410 .on_prepaint(move |bounds, window, cx| {
411 let size_changed = state.read(cx).bounds().size != bounds.size;
412 let id = state.entity_id();
413 state.update(cx, |state, _| {
414 state.update_bounds(bounds);
415 });
416 if size_changed {
417 if let Some(root) = window.root::<crate::Root>().flatten() {
418 root.update(cx, |root, cx| {
419 root.clear_text_selection_for_resized_view(id, cx);
420 });
421 }
422 }
423 })
424 }
425}
426
427#[derive(Clone, PartialEq, Default)]
428pub(crate) struct ParsedContent {
429 pub(crate) document: ParsedDocument,
430 pub(crate) node_cx: node::NodeContext,
431}
432
433struct UpdateFuture {
434 format: TextViewFormat,
435 content: ParsedContent,
436 options: UpdateOptions,
437 pending_text: String,
438 rx: Pin<Box<Receiver<UpdateOptions>>>,
439 tx_result: Sender<Result<ParsedContent, SharedString>>,
440}
441
442impl UpdateFuture {
443 fn new(
444 format: TextViewFormat,
445 rx: Receiver<UpdateOptions>,
446 tx_result: Sender<Result<ParsedContent, SharedString>>,
447 cx: &App,
448 ) -> Self {
449 Self {
450 format,
451 content: Default::default(),
452 pending_text: String::new(),
453 options: UpdateOptions {
454 append: false,
455 pending_text: String::new(),
456 highlight_theme: cx.theme().highlight_theme.clone(),
457 },
458 rx: Box::pin(rx),
459 tx_result,
460 }
461 }
462}
463
464impl Future for UpdateFuture {
465 type Output = ();
466
467 fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
468 loop {
469 match self.rx.as_mut().poll_next(cx) {
470 Poll::Ready(Some(options)) => {
471 if options.append {
472 self.pending_text.push_str(options.pending_text.as_str());
473 } else {
474 self.pending_text = options.pending_text.clone();
475 }
476 self.options = options;
477
478 let pending_text = std::mem::take(&mut self.pending_text);
480 let options = UpdateOptions {
481 pending_text,
482 ..self.options.clone()
483 };
484 let res = parse_content(self.format, self.content.clone(), &options);
485 if let Ok(content) = &res {
486 self.content = content.clone();
487 }
488 _ = self.tx_result.try_send(res);
489 continue;
490 }
491 Poll::Ready(None) => return Poll::Ready(()),
492 Poll::Pending => return Poll::Pending,
493 }
494 }
495 }
496}
497
498#[derive(Clone)]
499struct UpdateOptions {
500 pending_text: String,
501 append: bool,
502 highlight_theme: std::sync::Arc<HighlightTheme>,
503}
504
505fn parse_content(
506 format: TextViewFormat,
507 mut content: ParsedContent,
508 options: &UpdateOptions,
509) -> Result<ParsedContent, SharedString> {
510 let mut node_cx = NodeContext {
511 ..NodeContext::default()
512 };
513
514 let mut source = String::new();
515 if options.append
516 && let Some(last_block) = content.document.blocks.pop()
517 && let Some(span) = last_block.span()
518 {
519 node_cx.offset = span.start;
520 let last_source = &content.document.source[span.start..];
521 source.push_str(last_source);
522 source.push_str(&options.pending_text);
523 } else {
524 source = options.pending_text.to_string();
525 }
526
527 let new_document = match format {
528 TextViewFormat::Markdown => {
529 format::markdown::parse(&source, &mut node_cx, &options.highlight_theme)
530 }
531 TextViewFormat::Html => format::html::parse(&source, &mut node_cx),
532 TextViewFormat::Plain => format::plain::parse(&source, &mut node_cx),
533 }?;
534
535 if options.append {
536 content.document.source =
537 format!("{}{}", content.document.source, options.pending_text).into();
538 content.document.blocks.extend(new_document.blocks);
539 } else {
540 content.document = new_document;
541 }
542
543 Ok(content)
544}
545
546#[cfg(test)]
547mod tests {
548 use super::*;
549 use rgpui::TestAppContext;
550
551 #[rgpui::test]
552 fn set_text_then_push_str_appends_to_replaced_content(cx: &mut TestAppContext) {
553 cx.update(crate::init);
554 let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown("old", cx)));
555 cx.run_until_parked();
556
557 state.update(cx, |state, cx| {
558 state.set_text("", cx);
559 state.push_str("new", cx);
560 state.push_str(" text", cx);
561 });
562 cx.run_until_parked();
563
564 state.read_with(cx, |state, _| {
565 assert_eq!(state.text.as_str(), "new text");
566 assert_eq!(state.source().as_str(), "new text");
567 });
568
569 state.update(cx, |state, cx| {
570 state.set_text("", cx);
571 });
572 cx.run_until_parked();
573
574 state.read_with(cx, |state, _| {
575 assert_eq!(state.text.as_str(), "");
576 assert_eq!(state.source().as_str(), "");
577 });
578 }
579
580 #[rgpui::test]
581 fn select_all_returns_rendered_text(cx: &mut TestAppContext) {
582 cx.update(crate::init);
583 let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown("**quick** value", cx)));
584 cx.run_until_parked();
585
586 state.update(cx, |state, cx| {
587 state.select_all(cx);
588 });
589
590 state.read_with(cx, |state, _| {
591 assert!(state.has_view_selection());
592 assert_eq!(state.selected_text().trim(), "quick value");
593 });
594
595 state.update(cx, |state, cx| {
596 state.clear_selection(cx);
597 });
598
599 state.read_with(cx, |state, _| {
600 assert!(!state.has_view_selection());
601 assert_eq!(state.selected_text(), "");
602 });
603 }
604}