From d03e29d55d91377be4b68f3d56e58775729a9969 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 8 Dec 2023 14:31:01 -0800 Subject: [PATCH] Start work on rendering formatted chat messages --- crates/collab_ui2/src/chat_panel.rs | 28 +-- crates/rich_text2/src/rich_text.rs | 287 +++++++++++++--------------- 2 files changed, 146 insertions(+), 169 deletions(-) diff --git a/crates/collab_ui2/src/chat_panel.rs b/crates/collab_ui2/src/chat_panel.rs index eb99c3e6a2..b463e14deb 100644 --- a/crates/collab_ui2/src/chat_panel.rs +++ b/crates/collab_ui2/src/chat_panel.rs @@ -8,8 +8,8 @@ use db::kvp::KEY_VALUE_STORE; use editor::Editor; use gpui::{ actions, div, list, prelude::*, px, serde_json, AnyElement, AppContext, AsyncWindowContext, - ClickEvent, Div, EventEmitter, FocusableView, ListOffset, ListScrollEvent, ListState, Model, - Render, SharedString, Subscription, Task, View, ViewContext, VisualContext, WeakView, + ClickEvent, Div, ElementId, EventEmitter, FocusableView, ListOffset, ListScrollEvent, + ListState, Model, Render, Subscription, Task, View, ViewContext, VisualContext, WeakView, }; use language::LanguageRegistry; use menu::Confirm; @@ -342,10 +342,15 @@ impl ChatPanel { None }; + let element_id: ElementId = match message.id { + ChannelMessageId::Saved(id) => ("saved-message", id).into(), + ChannelMessageId::Pending(id) => ("pending-message", id).into(), + }; + // todo!("render the text with markdown formatting") if is_continuation { h_stack() - .child(SharedString::from(text.text.clone())) + .child(text.element(element_id, cx)) .child(render_remove(message_id_to_remove, cx)) .mb_1() .into_any() @@ -370,7 +375,7 @@ impl ChatPanel { ) .child( h_stack() - .child(SharedString::from(text.text.clone())) + .child(text.element(element_id, cx)) .child(render_remove(None, cx)), ) .mb_1() @@ -629,7 +634,7 @@ mod tests { use super::*; use gpui::HighlightStyle; use pretty_assertions::assert_eq; - use rich_text::{BackgroundKind, Highlight, RenderedRegion}; + use rich_text::Highlight; use util::test::marked_text_ranges; #[gpui::test] @@ -677,18 +682,5 @@ mod tests { (ranges[3].clone(), Highlight::SelfMention) ] ); - assert_eq!( - message.regions, - vec![ - RenderedRegion { - background_kind: Some(BackgroundKind::Mention), - link_url: None - }, - RenderedRegion { - background_kind: Some(BackgroundKind::SelfMention), - link_url: None - }, - ] - ); } } diff --git a/crates/rich_text2/src/rich_text.rs b/crates/rich_text2/src/rich_text.rs index 263a7f311b..b4a87b1e5d 100644 --- a/crates/rich_text2/src/rich_text.rs +++ b/crates/rich_text2/src/rich_text.rs @@ -1,13 +1,16 @@ -use std::{ops::Range, sync::Arc}; - -use anyhow::bail; use futures::FutureExt; -use gpui::{AnyElement, FontStyle, FontWeight, HighlightStyle, UnderlineStyle, WindowContext}; +use gpui::{ + AnyElement, ElementId, FontStyle, FontWeight, HighlightStyle, InteractiveText, IntoElement, + SharedString, StyledText, UnderlineStyle, WindowContext, +}; use language::{HighlightId, Language, LanguageRegistry}; +use std::{ops::Range, sync::Arc}; +use theme::ActiveTheme; use util::RangeExt; #[derive(Debug, Clone, PartialEq, Eq)] pub enum Highlight { + Code, Id(HighlightId), Highlight(HighlightStyle), Mention, @@ -28,24 +31,10 @@ impl From for Highlight { #[derive(Debug, Clone)] pub struct RichText { - pub text: String, + pub text: SharedString, pub highlights: Vec<(Range, Highlight)>, - pub region_ranges: Vec>, - pub regions: Vec, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum BackgroundKind { - Code, - /// A mention background for non-self user. - Mention, - SelfMention, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct RenderedRegion { - pub background_kind: Option, - pub link_url: Option, + pub link_ranges: Vec>, + pub link_urls: Arc<[String]>, } /// Allows one to specify extra links to the rendered markdown, which can be used @@ -56,89 +45,71 @@ pub struct Mention { } impl RichText { - pub fn element(&self, _cx: &mut WindowContext) -> AnyElement { - todo!(); + pub fn element(&self, id: ElementId, cx: &mut WindowContext) -> AnyElement { + let theme = cx.theme(); + let code_background = theme.colors().surface_background; - // let mut region_id = 0; - // let view_id = cx.view_id(); - - // let regions = self.regions.clone(); - - // enum Markdown {} - // Text::new(self.text.clone(), style.text.clone()) - // .with_highlights( - // self.highlights - // .iter() - // .filter_map(|(range, highlight)| { - // let style = match highlight { - // Highlight::Id(id) => id.style(&syntax)?, - // Highlight::Highlight(style) => style.clone(), - // Highlight::Mention => style.mention_highlight, - // Highlight::SelfMention => style.self_mention_highlight, - // }; - // Some((range.clone(), style)) - // }) - // .collect::>(), - // ) - // .with_custom_runs(self.region_ranges.clone(), move |ix, bounds, cx| { - // region_id += 1; - // let region = regions[ix].clone(); - // if let Some(url) = region.link_url { - // cx.scene().push_cursor_region(CursorRegion { - // bounds, - // style: CursorStyle::PointingHand, - // }); - // cx.scene().push_mouse_region( - // MouseRegion::new::(view_id, region_id, bounds) - // .on_click::(MouseButton::Left, move |_, _, cx| { - // cx.platform().open_url(&url) - // }), - // ); - // } - // if let Some(region_kind) = ®ion.background_kind { - // let background = match region_kind { - // BackgroundKind::Code => style.code_background, - // BackgroundKind::Mention => style.mention_background, - // BackgroundKind::SelfMention => style.self_mention_background, - // }; - // if background.is_some() { - // cx.scene().push_quad(gpui::Quad { - // bounds, - // background, - // border: Default::default(), - // corner_radii: (2.0).into(), - // }); - // } - // } - // }) - // .with_soft_wrap(true) - // .into_any() + InteractiveText::new( + id, + StyledText::new(self.text.clone()).with_highlights( + &cx.text_style(), + self.highlights.iter().map(|(range, highlight)| { + ( + range.clone(), + match highlight { + Highlight::Code => HighlightStyle { + background_color: Some(code_background), + ..Default::default() + }, + Highlight::Id(id) => HighlightStyle { + background_color: Some(code_background), + ..id.style(&theme.syntax()).unwrap_or_default() + }, + Highlight::Highlight(highlight) => *highlight, + Highlight::Mention => HighlightStyle { + font_weight: Some(FontWeight::BOLD), + ..Default::default() + }, + Highlight::SelfMention => HighlightStyle { + font_weight: Some(FontWeight::BOLD), + ..Default::default() + }, + }, + ) + }), + ), + ) + .on_click(self.link_ranges.clone(), { + let link_urls = self.link_urls.clone(); + move |ix, cx| cx.open_url(&link_urls[ix]) + }) + .into_any_element() } - pub fn add_mention( - &mut self, - range: Range, - is_current_user: bool, - mention_style: HighlightStyle, - ) -> anyhow::Result<()> { - if range.end > self.text.len() { - bail!( - "Mention in range {range:?} is outside of bounds for a message of length {}", - self.text.len() - ); - } + // pub fn add_mention( + // &mut self, + // range: Range, + // is_current_user: bool, + // mention_style: HighlightStyle, + // ) -> anyhow::Result<()> { + // if range.end > self.text.len() { + // bail!( + // "Mention in range {range:?} is outside of bounds for a message of length {}", + // self.text.len() + // ); + // } - if is_current_user { - self.region_ranges.push(range.clone()); - self.regions.push(RenderedRegion { - background_kind: Some(BackgroundKind::Mention), - link_url: None, - }); - } - self.highlights - .push((range, Highlight::Highlight(mention_style))); - Ok(()) - } + // if is_current_user { + // self.region_ranges.push(range.clone()); + // self.regions.push(RenderedRegion { + // background_kind: Some(BackgroundKind::Mention), + // link_url: None, + // }); + // } + // self.highlights + // .push((range, Highlight::Highlight(mention_style))); + // Ok(()) + // } } pub fn render_markdown_mut( @@ -146,7 +117,10 @@ pub fn render_markdown_mut( mut mentions: &[Mention], language_registry: &Arc, language: Option<&Arc>, - data: &mut RichText, + text: &mut String, + highlights: &mut Vec<(Range, Highlight)>, + link_ranges: &mut Vec>, + link_urls: &mut Vec, ) { use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag}; @@ -158,18 +132,18 @@ pub fn render_markdown_mut( let options = Options::all(); for (event, source_range) in Parser::new_ext(&block, options).into_offset_iter() { - let prev_len = data.text.len(); + let prev_len = text.len(); match event { Event::Text(t) => { if let Some(language) = ¤t_language { - render_code(&mut data.text, &mut data.highlights, t.as_ref(), language); + render_code(text, highlights, t.as_ref(), language); } else { if let Some(mention) = mentions.first() { if source_range.contains_inclusive(&mention.range) { mentions = &mentions[1..]; let range = (prev_len + mention.range.start - source_range.start) ..(prev_len + mention.range.end - source_range.start); - data.highlights.push(( + highlights.push(( range.clone(), if mention.is_self_mention { Highlight::SelfMention @@ -177,19 +151,10 @@ pub fn render_markdown_mut( Highlight::Mention }, )); - data.region_ranges.push(range); - data.regions.push(RenderedRegion { - background_kind: Some(if mention.is_self_mention { - BackgroundKind::SelfMention - } else { - BackgroundKind::Mention - }), - link_url: None, - }); } } - data.text.push_str(t.as_ref()); + text.push_str(t.as_ref()); let mut style = HighlightStyle::default(); if bold_depth > 0 { style.font_weight = Some(FontWeight::BOLD); @@ -198,11 +163,8 @@ pub fn render_markdown_mut( style.font_style = Some(FontStyle::Italic); } if let Some(link_url) = link_url.clone() { - data.region_ranges.push(prev_len..data.text.len()); - data.regions.push(RenderedRegion { - link_url: Some(link_url), - background_kind: None, - }); + link_ranges.push(prev_len..text.len()); + link_urls.push(link_url); style.underline = Some(UnderlineStyle { thickness: 1.0.into(), ..Default::default() @@ -211,27 +173,25 @@ pub fn render_markdown_mut( if style != HighlightStyle::default() { let mut new_highlight = true; - if let Some((last_range, last_style)) = data.highlights.last_mut() { + if let Some((last_range, last_style)) = highlights.last_mut() { if last_range.end == prev_len && last_style == &Highlight::Highlight(style) { - last_range.end = data.text.len(); + last_range.end = text.len(); new_highlight = false; } } if new_highlight { - data.highlights - .push((prev_len..data.text.len(), Highlight::Highlight(style))); + highlights.push((prev_len..text.len(), Highlight::Highlight(style))); } } } } Event::Code(t) => { - data.text.push_str(t.as_ref()); - data.region_ranges.push(prev_len..data.text.len()); + text.push_str(t.as_ref()); if link_url.is_some() { - data.highlights.push(( - prev_len..data.text.len(), + highlights.push(( + prev_len..text.len(), Highlight::Highlight(HighlightStyle { underline: Some(UnderlineStyle { thickness: 1.0.into(), @@ -241,19 +201,19 @@ pub fn render_markdown_mut( }), )); } - data.regions.push(RenderedRegion { - background_kind: Some(BackgroundKind::Code), - link_url: link_url.clone(), - }); + if let Some(link_url) = link_url.clone() { + link_ranges.push(prev_len..text.len()); + link_urls.push(link_url); + } } Event::Start(tag) => match tag { - Tag::Paragraph => new_paragraph(&mut data.text, &mut list_stack), + Tag::Paragraph => new_paragraph(text, &mut list_stack), Tag::Heading(_, _, _) => { - new_paragraph(&mut data.text, &mut list_stack); + new_paragraph(text, &mut list_stack); bold_depth += 1; } Tag::CodeBlock(kind) => { - new_paragraph(&mut data.text, &mut list_stack); + new_paragraph(text, &mut list_stack); current_language = if let CodeBlockKind::Fenced(language) = kind { language_registry .language_for_name(language.as_ref()) @@ -273,18 +233,18 @@ pub fn render_markdown_mut( let len = list_stack.len(); if let Some((list_number, has_content)) = list_stack.last_mut() { *has_content = false; - if !data.text.is_empty() && !data.text.ends_with('\n') { - data.text.push('\n'); + if !text.is_empty() && !text.ends_with('\n') { + text.push('\n'); } for _ in 0..len - 1 { - data.text.push_str(" "); + text.push_str(" "); } if let Some(number) = list_number { - data.text.push_str(&format!("{}. ", number)); + text.push_str(&format!("{}. ", number)); *number += 1; *has_content = false; } else { - data.text.push_str("- "); + text.push_str("- "); } } } @@ -299,8 +259,8 @@ pub fn render_markdown_mut( Tag::List(_) => drop(list_stack.pop()), _ => {} }, - Event::HardBreak => data.text.push('\n'), - Event::SoftBreak => data.text.push(' '), + Event::HardBreak => text.push('\n'), + Event::SoftBreak => text.push(' '), _ => {} } } @@ -312,18 +272,35 @@ pub fn render_markdown( language_registry: &Arc, language: Option<&Arc>, ) -> RichText { - let mut data = RichText { - text: Default::default(), - highlights: Default::default(), - region_ranges: Default::default(), - regions: Default::default(), - }; + // let mut data = RichText { + // text: Default::default(), + // highlights: Default::default(), + // region_ranges: Default::default(), + // regions: Default::default(), + // }; - render_markdown_mut(&block, mentions, language_registry, language, &mut data); + let mut text = String::new(); + let mut highlights = Vec::new(); + let mut link_ranges = Vec::new(); + let mut link_urls = Vec::new(); + render_markdown_mut( + &block, + mentions, + language_registry, + language, + &mut text, + &mut highlights, + &mut link_ranges, + &mut link_urls, + ); + text.truncate(text.trim_end().len()); - data.text = data.text.trim().to_string(); - - data + RichText { + text: SharedString::from(text), + link_urls: link_urls.into(), + link_ranges, + highlights, + } } pub fn render_code( @@ -334,11 +311,19 @@ pub fn render_code( ) { let prev_len = text.len(); text.push_str(content); + let mut offset = 0; for (range, highlight_id) in language.highlight_text(&content.into(), 0..content.len()) { + if range.start > offset { + highlights.push((prev_len + offset..prev_len + range.start, Highlight::Code)); + } highlights.push(( prev_len + range.start..prev_len + range.end, Highlight::Id(highlight_id), )); + offset = range.end; + } + if offset < content.len() { + highlights.push((prev_len + offset..prev_len + content.len(), Highlight::Code)); } }