Replace rich_text with markdown in assistant2 (#11650)

We don't implement copy yet but it should be pretty straightforward to
add.


https://github.com/zed-industries/zed/assets/482957/6b4d7c34-de6b-4b07-aed9-608c771bbbdb

/cc: @rgbkrk @maxbrunsfeld @maxdeviant 

Release Notes:

- N/A
This commit is contained in:
Antonio Scandurra 2024-05-10 10:22:14 +02:00 committed by GitHub
parent 0d760d8d19
commit 358bc2d225
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 205 additions and 118 deletions

2
Cargo.lock generated
View file

@ -390,6 +390,7 @@ dependencies = [
"language", "language",
"languages", "languages",
"log", "log",
"markdown",
"node_runtime", "node_runtime",
"open_ai", "open_ai",
"picker", "picker",
@ -397,7 +398,6 @@ dependencies = [
"rand 0.8.5", "rand 0.8.5",
"regex", "regex",
"release_channel", "release_channel",
"rich_text",
"schemars", "schemars",
"semantic_index", "semantic_index",
"serde", "serde",

View file

@ -29,11 +29,11 @@ fuzzy.workspace = true
gpui.workspace = true gpui.workspace = true
language.workspace = true language.workspace = true
log.workspace = true log.workspace = true
markdown.workspace = true
open_ai.workspace = true open_ai.workspace = true
picker.workspace = true picker.workspace = true
project.workspace = true project.workspace = true
regex.workspace = true regex.workspace = true
rich_text.workspace = true
schemars.workspace = true schemars.workspace = true
semantic_index.workspace = true semantic_index.workspace = true
serde.workspace = true serde.workspace = true
@ -52,6 +52,7 @@ env_logger.workspace = true
gpui = { workspace = true, features = ["test-support"] } gpui = { workspace = true, features = ["test-support"] }
language = { workspace = true, features = ["test-support"] } language = { workspace = true, features = ["test-support"] }
languages.workspace = true languages.workspace = true
markdown = { workspace = true, features = ["test-support"] }
node_runtime.workspace = true node_runtime.workspace = true
project = { workspace = true, features = ["test-support"] } project = { workspace = true, features = ["test-support"] }
rand.workspace = true rand.workspace = true

View file

@ -26,8 +26,8 @@ use gpui::{
FocusableView, ListAlignment, ListState, Model, Render, Task, View, WeakView, FocusableView, ListAlignment, ListState, Model, Render, Task, View, WeakView,
}; };
use language::{language_settings::SoftWrap, LanguageRegistry}; use language::{language_settings::SoftWrap, LanguageRegistry};
use markdown::{Markdown, MarkdownStyle};
use open_ai::{FunctionContent, ToolCall, ToolCallContent}; use open_ai::{FunctionContent, ToolCall, ToolCallContent};
use rich_text::RichText;
use saved_conversation::{SavedAssistantMessagePart, SavedChatMessage, SavedConversation}; use saved_conversation::{SavedAssistantMessagePart, SavedChatMessage, SavedConversation};
use saved_conversations::SavedConversations; use saved_conversations::SavedConversations;
use semantic_index::{CloudEmbeddingProvider, ProjectIndex, ProjectIndexDebugView, SemanticIndex}; use semantic_index::{CloudEmbeddingProvider, ProjectIndex, ProjectIndexDebugView, SemanticIndex};
@ -261,11 +261,11 @@ pub struct AssistantChat {
tool_registry: Arc<ToolRegistry>, tool_registry: Arc<ToolRegistry>,
attachment_registry: Arc<AttachmentRegistry>, attachment_registry: Arc<AttachmentRegistry>,
project_index: Model<ProjectIndex>, project_index: Model<ProjectIndex>,
markdown_style: MarkdownStyle,
} }
struct EditingMessage { struct EditingMessage {
id: MessageId, id: MessageId,
old_body: Arc<str>,
body: View<Editor>, body: View<Editor>,
} }
@ -348,21 +348,49 @@ impl AssistantChat {
pending_completion: None, pending_completion: None,
attachment_registry, attachment_registry,
tool_registry, tool_registry,
markdown_style: MarkdownStyle {
code_block: gpui::TextStyleRefinement {
font_family: Some("Zed Mono".into()),
color: Some(cx.theme().colors().editor_foreground),
background_color: Some(cx.theme().colors().editor_background),
..Default::default()
},
inline_code: gpui::TextStyleRefinement {
font_family: Some("Zed Mono".into()),
// @nate: Could we add inline-code specific styles to the theme?
color: Some(cx.theme().colors().editor_foreground),
background_color: Some(cx.theme().colors().editor_background),
..Default::default()
},
rule_color: Color::Muted.color(cx),
block_quote_border_color: Color::Muted.color(cx),
block_quote: gpui::TextStyleRefinement {
color: Some(Color::Muted.color(cx)),
..Default::default()
},
link: gpui::TextStyleRefinement {
color: Some(Color::Accent.color(cx)),
underline: Some(gpui::UnderlineStyle {
thickness: px(1.),
color: Some(Color::Accent.color(cx)),
wavy: false,
}),
..Default::default()
},
syntax: cx.theme().syntax().clone(),
selection_background_color: {
let mut selection = cx.theme().players().local().selection;
selection.fade_out(0.7);
selection
},
},
} }
} }
fn editing_message_id(&self) -> Option<MessageId> { fn message_for_id(&self, id: MessageId) -> Option<&ChatMessage> {
self.editing_message.as_ref().map(|message| message.id) self.messages.iter().find(|message| match message {
} ChatMessage::User(message) => message.id == id,
ChatMessage::Assistant(message) => message.id == id,
fn focused_message_id(&self, cx: &WindowContext) -> Option<MessageId> {
self.messages.iter().find_map(|message| match message {
ChatMessage::User(message) => message
.body
.focus_handle(cx)
.contains_focused(cx)
.then_some(message.id),
ChatMessage::Assistant(_) => None,
}) })
} }
@ -372,10 +400,8 @@ impl AssistantChat {
fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) { fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
// If we're currently editing a message, cancel the edit. // If we're currently editing a message, cancel the edit.
if let Some(editing_message) = self.editing_message.take() { if self.editing_message.take().is_some() {
editing_message cx.notify();
.body
.update(cx, |body, cx| body.set_text(editing_message.old_body, cx));
return; return;
} }
@ -392,14 +418,7 @@ impl AssistantChat {
} }
fn submit(&mut self, Submit(mode): &Submit, cx: &mut ViewContext<Self>) { fn submit(&mut self, Submit(mode): &Submit, cx: &mut ViewContext<Self>) {
if let Some(focused_message_id) = self.focused_message_id(cx) { if self.composer_editor.focus_handle(cx).is_focused(cx) {
self.truncate_messages(focused_message_id, cx);
self.pending_completion.take();
self.composer_editor.focus_handle(cx).focus(cx);
if self.editing_message_id() == Some(focused_message_id) {
self.editing_message.take();
}
} else if self.composer_editor.focus_handle(cx).is_focused(cx) {
// Don't allow multiple concurrent completions. // Don't allow multiple concurrent completions.
if self.pending_completion.is_some() { if self.pending_completion.is_some() {
cx.propagate(); cx.propagate();
@ -410,10 +429,12 @@ impl AssistantChat {
let text = composer_editor.text(cx); let text = composer_editor.text(cx);
let id = self.next_message_id.post_inc(); let id = self.next_message_id.post_inc();
let body = cx.new_view(|cx| { let body = cx.new_view(|cx| {
let mut editor = Editor::auto_height(80, cx); Markdown::new(
editor.set_text(text, cx); text,
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx); self.markdown_style.clone(),
editor self.language_registry.clone(),
cx,
)
}); });
composer_editor.clear(cx); composer_editor.clear(cx);
@ -424,6 +445,26 @@ impl AssistantChat {
}) })
}); });
self.push_message(message, cx); self.push_message(message, cx);
} else if let Some(editing_message) = self.editing_message.as_ref() {
let focus_handle = editing_message.body.focus_handle(cx);
if focus_handle.contains_focused(cx) {
if let Some(ChatMessage::User(user_message)) =
self.message_for_id(editing_message.id)
{
user_message.body.update(cx, |body, cx| {
body.reset(editing_message.body.read(cx).text(cx), cx);
});
}
self.truncate_messages(editing_message.id, cx);
self.pending_completion.take();
self.composer_editor.focus_handle(cx).focus(cx);
self.editing_message.take();
} else {
log::error!("unexpected state: no user message editor is focused.");
return;
}
} else { } else {
log::error!("unexpected state: no user message editor is focused."); log::error!("unexpected state: no user message editor is focused.");
return; return;
@ -512,7 +553,6 @@ impl AssistantChat {
}); });
let mut stream = completion?.await?; let mut stream = completion?.await?;
let mut body = String::new();
while let Some(delta) = stream.next().await { while let Some(delta) = stream.next().await {
let delta = delta?; let delta = delta?;
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
@ -521,7 +561,14 @@ impl AssistantChat {
{ {
if messages.is_empty() { if messages.is_empty() {
messages.push(AssistantMessagePart { messages.push(AssistantMessagePart {
body: RichText::default(), body: cx.new_view(|cx| {
Markdown::new(
"".into(),
this.markdown_style.clone(),
this.language_registry.clone(),
cx,
)
}),
tool_calls: Vec::new(), tool_calls: Vec::new(),
}) })
} }
@ -529,7 +576,9 @@ impl AssistantChat {
let message = messages.last_mut().unwrap(); let message = messages.last_mut().unwrap();
if let Some(content) = &delta.content { if let Some(content) = &delta.content {
body.push_str(content); message
.body
.update(cx, |message, cx| message.append(&content, cx));
} }
for tool_call_delta in delta.tool_calls { for tool_call_delta in delta.tool_calls {
@ -558,8 +607,6 @@ impl AssistantChat {
} }
} }
message.body =
RichText::new(body.clone(), &[], &this.language_registry);
cx.notify(); cx.notify();
} else { } else {
unreachable!() unreachable!()
@ -608,7 +655,14 @@ impl AssistantChat {
self.messages.last_mut() self.messages.last_mut()
{ {
messages.push(AssistantMessagePart { messages.push(AssistantMessagePart {
body: RichText::default(), body: cx.new_view(|cx| {
Markdown::new(
"".into(),
self.markdown_style.clone(),
self.language_registry.clone(),
cx,
)
}),
tool_calls: Vec::new(), tool_calls: Vec::new(),
}); });
return; return;
@ -617,7 +671,14 @@ impl AssistantChat {
let message = ChatMessage::Assistant(AssistantMessage { let message = ChatMessage::Assistant(AssistantMessage {
id: self.next_message_id.post_inc(), id: self.next_message_id.post_inc(),
messages: vec![AssistantMessagePart { messages: vec![AssistantMessagePart {
body: RichText::default(), body: cx.new_view(|cx| {
Markdown::new(
"".into(),
self.markdown_style.clone(),
self.language_registry.clone(),
cx,
)
}),
tool_calls: Vec::new(), tool_calls: Vec::new(),
}], }],
error: None, error: None,
@ -760,66 +821,69 @@ impl AssistantChat {
.id(SharedString::from(format!("message-{}-container", id.0))) .id(SharedString::from(format!("message-{}-container", id.0)))
.when(is_first, |this| this.pt(padding)) .when(is_first, |this| this.pt(padding))
.map(|element| { .map(|element| {
if self.editing_message_id() == Some(*id) { if let Some(editing_message) = self.editing_message.as_ref() {
element.child(Composer::new( if editing_message.id == *id {
body.clone(), return element.child(Composer::new(
self.project_index_button.clone(), editing_message.body.clone(),
self.active_file_button.clone(), self.project_index_button.clone(),
crate::ui::ModelSelector::new( self.active_file_button.clone(),
cx.view().downgrade(), crate::ui::ModelSelector::new(
self.model.clone(), cx.view().downgrade(),
) self.model.clone(),
.into_any_element(),
))
} else {
element
.on_click(cx.listener({
let id = *id;
let body = body.clone();
move |assistant_chat, event: &ClickEvent, cx| {
if event.up.click_count == 2 {
assistant_chat.editing_message = Some(EditingMessage {
id,
body: body.clone(),
old_body: body.read(cx).text(cx).into(),
});
body.focus_handle(cx).focus(cx);
}
}
}))
.child(
crate::ui::ChatMessage::new(
*id,
UserOrAssistant::User(self.user_store.read(cx).current_user()),
// todo!(): clean up the vec usage
vec![
RichText::new(
body.read(cx).text(cx),
&[],
&self.language_registry,
)
.element(ElementId::from(id.0), cx),
h_flex()
.gap_2()
.children(
attachments
.iter()
.map(|attachment| attachment.view.clone()),
)
.into_any_element(),
],
self.is_message_collapsed(id),
Box::new(cx.listener({
let id = *id;
move |assistant_chat, _event, _cx| {
assistant_chat.toggle_message_collapsed(id)
}
})),
) )
// TODO: Wire up selections. .into_any_element(),
.selected(is_last), ));
) }
} }
element
.on_click(cx.listener({
let id = *id;
let body = body.clone();
move |assistant_chat, event: &ClickEvent, cx| {
if event.up.click_count == 2 {
let body = cx.new_view(|cx| {
let mut editor = Editor::auto_height(80, cx);
let source = Arc::from(body.read(cx).source());
editor.set_text(source, cx);
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
editor
});
assistant_chat.editing_message = Some(EditingMessage {
id,
body: body.clone(),
});
body.focus_handle(cx).focus(cx);
}
}
}))
.child(
crate::ui::ChatMessage::new(
*id,
UserOrAssistant::User(self.user_store.read(cx).current_user()),
// todo!(): clean up the vec usage
vec![
body.clone().into_any_element(),
h_flex()
.gap_2()
.children(
attachments
.iter()
.map(|attachment| attachment.view.clone()),
)
.into_any_element(),
],
self.is_message_collapsed(id),
Box::new(cx.listener({
let id = *id;
move |assistant_chat, _event, _cx| {
assistant_chat.toggle_message_collapsed(id)
}
})),
)
// TODO: Wire up selections.
.selected(is_last),
)
}) })
.into_any(), .into_any(),
ChatMessage::Assistant(AssistantMessage { ChatMessage::Assistant(AssistantMessage {
@ -831,13 +895,8 @@ impl AssistantChat {
let mut message_elements = Vec::new(); let mut message_elements = Vec::new();
for message in messages { for message in messages {
if !message.body.text.is_empty() { if !message.body.read(cx).source().is_empty() {
message_elements.push( message_elements.push(div().child(message.body.clone()).into_any())
div()
// todo!(): The element Id will need to be a combo of the base ID and the index within the grouping
.child(message.body.element(ElementId::from(id.0), cx))
.into_any_element(),
)
} }
let tools = message let tools = message
@ -847,7 +906,7 @@ impl AssistantChat {
.collect::<Vec<AnyElement>>(); .collect::<Vec<AnyElement>>();
if !tools.is_empty() { if !tools.is_empty() {
message_elements.push(div().children(tools).into_any_element()) message_elements.push(div().children(tools).into_any())
} }
} }
@ -900,14 +959,14 @@ impl AssistantChat {
// Show user's message last so that the assistant is grounded in the user's request // Show user's message last so that the assistant is grounded in the user's request
completion_messages.push(CompletionMessage::User { completion_messages.push(CompletionMessage::User {
content: body.read(cx).text(cx), content: body.read(cx).source().to_string(),
}); });
} }
ChatMessage::Assistant(AssistantMessage { messages, .. }) => { ChatMessage::Assistant(AssistantMessage { messages, .. }) => {
for message in messages { for message in messages {
let body = message.body.clone(); let body = message.body.clone();
if body.text.is_empty() && message.tool_calls.is_empty() { if body.read(cx).source().is_empty() && message.tool_calls.is_empty() {
continue; continue;
} }
@ -926,7 +985,7 @@ impl AssistantChat {
.collect(); .collect();
completion_messages.push(CompletionMessage::Assistant { completion_messages.push(CompletionMessage::Assistant {
content: Some(body.text.to_string()), content: Some(body.read(cx).source().to_string()),
tool_calls: tool_calls_from_assistant, tool_calls: tool_calls_from_assistant,
}); });
@ -964,7 +1023,7 @@ impl AssistantChat {
match message { match message {
ChatMessage::User(message) => SavedChatMessage::User { ChatMessage::User(message) => SavedChatMessage::User {
id: message.id, id: message.id,
body: message.body.read(cx).text(cx), body: message.body.read(cx).source().into(),
attachments: message attachments: message
.attachments .attachments
.iter() .iter()
@ -981,7 +1040,7 @@ impl AssistantChat {
.messages .messages
.iter() .iter()
.map(|message| SavedAssistantMessagePart { .map(|message| SavedAssistantMessagePart {
body: message.body.text.clone(), body: message.body.read(cx).source().to_string().into(),
tool_calls: message tool_calls: message
.tool_calls .tool_calls
.iter() .iter()
@ -1093,7 +1152,7 @@ enum ChatMessage {
impl ChatMessage { impl ChatMessage {
fn focus_handle(&self, cx: &AppContext) -> Option<FocusHandle> { fn focus_handle(&self, cx: &AppContext) -> Option<FocusHandle> {
match self { match self {
ChatMessage::User(UserMessage { body, .. }) => Some(body.focus_handle(cx)), ChatMessage::User(message) => Some(message.body.focus_handle(cx)),
ChatMessage::Assistant(_) => None, ChatMessage::Assistant(_) => None,
} }
} }
@ -1101,12 +1160,12 @@ impl ChatMessage {
struct UserMessage { struct UserMessage {
pub id: MessageId, pub id: MessageId,
pub body: View<Editor>, pub body: View<Markdown>,
pub attachments: Vec<UserAttachment>, pub attachments: Vec<UserAttachment>,
} }
struct AssistantMessagePart { struct AssistantMessagePart {
pub body: RichText, pub body: View<Markdown>,
pub tool_calls: Vec<ToolFunctionCall>, pub tool_calls: Vec<ToolFunctionCall>,
} }

View file

@ -119,7 +119,7 @@ impl RenderOnce for ChatMessage {
) )
.when(self.messages.len() > 0, |el| { .when(self.messages.len() > 0, |el| {
el.child( el.child(
h_flex().child( h_flex().w_full().child(
v_flex() v_flex()
.relative() .relative()
.overflow_hidden() .overflow_hidden()

View file

@ -3,10 +3,10 @@ mod parser;
use crate::parser::CodeBlockKind; use crate::parser::CodeBlockKind;
use futures::FutureExt; use futures::FutureExt;
use gpui::{ use gpui::{
point, quad, AnyElement, Bounds, CursorStyle, DispatchPhase, Edges, FontStyle, FontWeight, point, quad, AnyElement, AppContext, Bounds, CursorStyle, DispatchPhase, Edges, FocusHandle,
GlobalElementId, Hitbox, Hsla, MouseDownEvent, MouseEvent, MouseMoveEvent, MouseUpEvent, Point, FocusableView, FontStyle, FontWeight, GlobalElementId, Hitbox, Hsla, KeyContext,
Render, StrikethroughStyle, Style, StyledText, Task, TextLayout, TextRun, TextStyle, MouseDownEvent, MouseEvent, MouseMoveEvent, MouseUpEvent, Point, Render, StrikethroughStyle,
TextStyleRefinement, View, Style, StyledText, Task, TextLayout, TextRun, TextStyle, TextStyleRefinement, View,
}; };
use language::{Language, LanguageRegistry, Rope}; use language::{Language, LanguageRegistry, Rope};
use parser::{parse_markdown, MarkdownEvent, MarkdownTag, MarkdownTagEnd}; use parser::{parse_markdown, MarkdownEvent, MarkdownTag, MarkdownTagEnd};
@ -36,6 +36,7 @@ pub struct Markdown {
parsed_markdown: ParsedMarkdown, parsed_markdown: ParsedMarkdown,
should_reparse: bool, should_reparse: bool,
pending_parse: Option<Task<Option<()>>>, pending_parse: Option<Task<Option<()>>>,
focus_handle: FocusHandle,
language_registry: Arc<LanguageRegistry>, language_registry: Arc<LanguageRegistry>,
} }
@ -46,6 +47,7 @@ impl Markdown {
language_registry: Arc<LanguageRegistry>, language_registry: Arc<LanguageRegistry>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Self { ) -> Self {
let focus_handle = cx.focus_handle();
let mut this = Self { let mut this = Self {
source, source,
selection: Selection::default(), selection: Selection::default(),
@ -55,6 +57,7 @@ impl Markdown {
should_reparse: false, should_reparse: false,
parsed_markdown: ParsedMarkdown::default(), parsed_markdown: ParsedMarkdown::default(),
pending_parse: None, pending_parse: None,
focus_handle,
language_registry, language_registry,
}; };
this.parse(cx); this.parse(cx);
@ -66,6 +69,16 @@ impl Markdown {
self.parse(cx); self.parse(cx);
} }
pub fn reset(&mut self, source: String, cx: &mut ViewContext<Self>) {
self.source = source;
self.selection = Selection::default();
self.autoscroll_request = None;
self.pending_parse = None;
self.should_reparse = false;
self.parsed_markdown = ParsedMarkdown::default();
self.parse(cx);
}
pub fn source(&self) -> &str { pub fn source(&self) -> &str {
&self.source &self.source
} }
@ -120,6 +133,12 @@ impl Render for Markdown {
} }
} }
impl FocusableView for Markdown {
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
self.focus_handle.clone()
}
}
#[derive(Copy, Clone, Default, Debug)] #[derive(Copy, Clone, Default, Debug)]
struct Selection { struct Selection {
start: usize, start: usize,
@ -309,6 +328,7 @@ impl MarkdownElement {
reversed: false, reversed: false,
pending: true, pending: true,
}; };
cx.focus(&markdown.focus_handle);
} }
cx.notify(); cx.notify();
@ -593,6 +613,13 @@ impl Element for MarkdownElement {
hitbox: &mut Self::PrepaintState, hitbox: &mut Self::PrepaintState,
cx: &mut WindowContext, cx: &mut WindowContext,
) { ) {
let focus_handle = self.markdown.read(cx).focus_handle.clone();
cx.set_focus_handle(&focus_handle);
let mut context = KeyContext::default();
context.add("Markdown");
cx.set_key_context(context);
self.paint_mouse_listeners(hitbox, &rendered_markdown.text, cx); self.paint_mouse_listeners(hitbox, &rendered_markdown.text, cx);
rendered_markdown.element.paint(cx); rendered_markdown.element.paint(cx);
self.paint_selection(bounds, &rendered_markdown.text, cx); self.paint_selection(bounds, &rendered_markdown.text, cx);