@@ -26,8 +26,8 @@ use gpui::{
FocusableView, ListAlignment, ListState, Model, Render, Task, View, WeakView,
};
use language::{language_settings::SoftWrap, LanguageRegistry};
+use markdown::{Markdown, MarkdownStyle};
use open_ai::{FunctionContent, ToolCall, ToolCallContent};
-use rich_text::RichText;
use saved_conversation::{SavedAssistantMessagePart, SavedChatMessage, SavedConversation};
use saved_conversations::SavedConversations;
use semantic_index::{CloudEmbeddingProvider, ProjectIndex, ProjectIndexDebugView, SemanticIndex};
@@ -261,11 +261,11 @@ pub struct AssistantChat {
tool_registry: Arc<ToolRegistry>,
attachment_registry: Arc<AttachmentRegistry>,
project_index: Model<ProjectIndex>,
+ markdown_style: MarkdownStyle,
}
struct EditingMessage {
id: MessageId,
- old_body: Arc<str>,
body: View<Editor>,
}
@@ -348,21 +348,49 @@ impl AssistantChat {
pending_completion: None,
attachment_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> {
- self.editing_message.as_ref().map(|message| message.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,
+ fn message_for_id(&self, id: MessageId) -> Option<&ChatMessage> {
+ self.messages.iter().find(|message| match message {
+ ChatMessage::User(message) => message.id == id,
+ ChatMessage::Assistant(message) => message.id == id,
})
}
@@ -372,10 +400,8 @@ impl AssistantChat {
fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
// If we're currently editing a message, cancel the edit.
- if let Some(editing_message) = self.editing_message.take() {
- editing_message
- .body
- .update(cx, |body, cx| body.set_text(editing_message.old_body, cx));
+ if self.editing_message.take().is_some() {
+ cx.notify();
return;
}
@@ -392,14 +418,7 @@ impl AssistantChat {
}
fn submit(&mut self, Submit(mode): &Submit, cx: &mut ViewContext<Self>) {
- if let Some(focused_message_id) = self.focused_message_id(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) {
+ if self.composer_editor.focus_handle(cx).is_focused(cx) {
// Don't allow multiple concurrent completions.
if self.pending_completion.is_some() {
cx.propagate();
@@ -410,10 +429,12 @@ impl AssistantChat {
let text = composer_editor.text(cx);
let id = self.next_message_id.post_inc();
let body = cx.new_view(|cx| {
- let mut editor = Editor::auto_height(80, cx);
- editor.set_text(text, cx);
- editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
- editor
+ Markdown::new(
+ text,
+ self.markdown_style.clone(),
+ self.language_registry.clone(),
+ cx,
+ )
});
composer_editor.clear(cx);
@@ -424,6 +445,26 @@ impl AssistantChat {
})
});
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 {
log::error!("unexpected state: no user message editor is focused.");
return;
@@ -512,7 +553,6 @@ impl AssistantChat {
});
let mut stream = completion?.await?;
- let mut body = String::new();
while let Some(delta) = stream.next().await {
let delta = delta?;
this.update(cx, |this, cx| {
@@ -521,7 +561,14 @@ impl AssistantChat {
{
if messages.is_empty() {
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(),
})
}
@@ -529,7 +576,9 @@ impl AssistantChat {
let message = messages.last_mut().unwrap();
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 {
@@ -558,8 +607,6 @@ impl AssistantChat {
}
}
- message.body =
- RichText::new(body.clone(), &[], &this.language_registry);
cx.notify();
} else {
unreachable!()
@@ -608,7 +655,14 @@ impl AssistantChat {
self.messages.last_mut()
{
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(),
});
return;
@@ -617,7 +671,14 @@ impl AssistantChat {
let message = ChatMessage::Assistant(AssistantMessage {
id: self.next_message_id.post_inc(),
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(),
}],
error: None,
@@ -760,66 +821,69 @@ impl AssistantChat {
.id(SharedString::from(format!("message-{}-container", id.0)))
.when(is_first, |this| this.pt(padding))
.map(|element| {
- if self.editing_message_id() == Some(*id) {
- element.child(Composer::new(
- body.clone(),
- self.project_index_button.clone(),
- self.active_file_button.clone(),
- crate::ui::ModelSelector::new(
- 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);
- }
+ if let Some(editing_message) = self.editing_message.as_ref() {
+ if editing_message.id == *id {
+ return element.child(Composer::new(
+ editing_message.body.clone(),
+ self.project_index_button.clone(),
+ self.active_file_button.clone(),
+ crate::ui::ModelSelector::new(
+ cx.view().downgrade(),
+ self.model.clone(),
+ )
+ .into_any_element(),
+ ));
+ }
+ }
+
+ 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![
- RichText::new(
- body.read(cx).text(cx),
- &[],
- &self.language_registry,
+ }
+ }))
+ .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()),
)
- .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.
- .selected(is_last),
+ .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(),
ChatMessage::Assistant(AssistantMessage {
@@ -831,13 +895,8 @@ impl AssistantChat {
let mut message_elements = Vec::new();
for message in messages {
- if !message.body.text.is_empty() {
- message_elements.push(
- 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(),
- )
+ if !message.body.read(cx).source().is_empty() {
+ message_elements.push(div().child(message.body.clone()).into_any())
}
let tools = message
@@ -847,7 +906,7 @@ impl AssistantChat {
.collect::<Vec<AnyElement>>();
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
completion_messages.push(CompletionMessage::User {
- content: body.read(cx).text(cx),
+ content: body.read(cx).source().to_string(),
});
}
ChatMessage::Assistant(AssistantMessage { messages, .. }) => {
for message in messages {
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;
}
@@ -926,7 +985,7 @@ impl AssistantChat {
.collect();
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,
});
@@ -964,7 +1023,7 @@ impl AssistantChat {
match message {
ChatMessage::User(message) => SavedChatMessage::User {
id: message.id,
- body: message.body.read(cx).text(cx),
+ body: message.body.read(cx).source().into(),
attachments: message
.attachments
.iter()
@@ -981,7 +1040,7 @@ impl AssistantChat {
.messages
.iter()
.map(|message| SavedAssistantMessagePart {
- body: message.body.text.clone(),
+ body: message.body.read(cx).source().to_string().into(),
tool_calls: message
.tool_calls
.iter()
@@ -1093,7 +1152,7 @@ enum ChatMessage {
impl ChatMessage {
fn focus_handle(&self, cx: &AppContext) -> Option<FocusHandle> {
match self {
- ChatMessage::User(UserMessage { body, .. }) => Some(body.focus_handle(cx)),
+ ChatMessage::User(message) => Some(message.body.focus_handle(cx)),
ChatMessage::Assistant(_) => None,
}
}
@@ -1101,12 +1160,12 @@ impl ChatMessage {
struct UserMessage {
pub id: MessageId,
- pub body: View<Editor>,
+ pub body: View<Markdown>,
pub attachments: Vec<UserAttachment>,
}
struct AssistantMessagePart {
- pub body: RichText,
+ pub body: View<Markdown>,
pub tool_calls: Vec<ToolFunctionCall>,
}
@@ -3,10 +3,10 @@ mod parser;
use crate::parser::CodeBlockKind;
use futures::FutureExt;
use gpui::{
- point, quad, AnyElement, Bounds, CursorStyle, DispatchPhase, Edges, FontStyle, FontWeight,
- GlobalElementId, Hitbox, Hsla, MouseDownEvent, MouseEvent, MouseMoveEvent, MouseUpEvent, Point,
- Render, StrikethroughStyle, Style, StyledText, Task, TextLayout, TextRun, TextStyle,
- TextStyleRefinement, View,
+ point, quad, AnyElement, AppContext, Bounds, CursorStyle, DispatchPhase, Edges, FocusHandle,
+ FocusableView, FontStyle, FontWeight, GlobalElementId, Hitbox, Hsla, KeyContext,
+ MouseDownEvent, MouseEvent, MouseMoveEvent, MouseUpEvent, Point, Render, StrikethroughStyle,
+ Style, StyledText, Task, TextLayout, TextRun, TextStyle, TextStyleRefinement, View,
};
use language::{Language, LanguageRegistry, Rope};
use parser::{parse_markdown, MarkdownEvent, MarkdownTag, MarkdownTagEnd};
@@ -36,6 +36,7 @@ pub struct Markdown {
parsed_markdown: ParsedMarkdown,
should_reparse: bool,
pending_parse: Option<Task<Option<()>>>,
+ focus_handle: FocusHandle,
language_registry: Arc<LanguageRegistry>,
}
@@ -46,6 +47,7 @@ impl Markdown {
language_registry: Arc<LanguageRegistry>,
cx: &mut ViewContext<Self>,
) -> Self {
+ let focus_handle = cx.focus_handle();
let mut this = Self {
source,
selection: Selection::default(),
@@ -55,6 +57,7 @@ impl Markdown {
should_reparse: false,
parsed_markdown: ParsedMarkdown::default(),
pending_parse: None,
+ focus_handle,
language_registry,
};
this.parse(cx);
@@ -66,6 +69,16 @@ impl Markdown {
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 {
&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)]
struct Selection {
start: usize,
@@ -309,6 +328,7 @@ impl MarkdownElement {
reversed: false,
pending: true,
};
+ cx.focus(&markdown.focus_handle);
}
cx.notify();
@@ -593,6 +613,13 @@ impl Element for MarkdownElement {
hitbox: &mut Self::PrepaintState,
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);
rendered_markdown.element.paint(cx);
self.paint_selection(bounds, &rendered_markdown.text, cx);