thread_view.rs

  1use std::path::Path;
  2use std::rc::Rc;
  3use std::time::Duration;
  4
  5use agentic_coding_protocol::{self as acp, ToolCallConfirmation};
  6use anyhow::Result;
  7use editor::{Editor, MultiBuffer};
  8use gpui::{
  9    Animation, AnimationExt, App, EdgesRefinement, Empty, Entity, Focusable, ListState,
 10    SharedString, StyleRefinement, Subscription, TextStyleRefinement, Transformation,
 11    UnderlineStyle, Window, div, list, percentage, prelude::*,
 12};
 13use gpui::{FocusHandle, Task};
 14use language::Buffer;
 15use markdown::{HeadingLevelStyles, MarkdownElement, MarkdownStyle};
 16use project::Project;
 17use settings::Settings as _;
 18use theme::ThemeSettings;
 19use ui::prelude::*;
 20use ui::{Button, Tooltip};
 21use util::ResultExt;
 22use zed_actions::agent::Chat;
 23
 24use crate::{
 25    AcpServer, AcpThread, AcpThreadEvent, AgentThreadEntryContent, MessageChunk, Role, ThreadEntry,
 26    ToolCall, ToolCallId, ToolCallStatus,
 27};
 28
 29pub struct AcpThreadView {
 30    thread_state: ThreadState,
 31    // todo! use full message editor from agent2
 32    message_editor: Entity<Editor>,
 33    list_state: ListState,
 34    send_task: Option<Task<Result<()>>>,
 35}
 36
 37enum ThreadState {
 38    Loading {
 39        _task: Task<()>,
 40    },
 41    Ready {
 42        thread: Entity<AcpThread>,
 43        _subscription: Subscription,
 44    },
 45    LoadError(SharedString),
 46}
 47
 48impl AcpThreadView {
 49    pub fn new(project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>) -> Self {
 50        let Some(root_dir) = project
 51            .read(cx)
 52            .visible_worktrees(cx)
 53            .next()
 54            .map(|worktree| worktree.read(cx).abs_path())
 55        else {
 56            todo!();
 57        };
 58
 59        let cli_path =
 60            Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../gemini-cli/packages/cli");
 61
 62        let child = util::command::new_smol_command("node")
 63            .arg(cli_path)
 64            .arg("--acp")
 65            .current_dir(root_dir)
 66            .stdin(std::process::Stdio::piped())
 67            .stdout(std::process::Stdio::piped())
 68            .stderr(std::process::Stdio::inherit())
 69            .kill_on_drop(true)
 70            .spawn()
 71            .unwrap();
 72
 73        let message_editor = cx.new(|cx| {
 74            let buffer = cx.new(|cx| Buffer::local("", cx));
 75            let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
 76
 77            let mut editor = Editor::new(
 78                editor::EditorMode::AutoHeight {
 79                    min_lines: 4,
 80                    max_lines: None,
 81                },
 82                buffer,
 83                None,
 84                window,
 85                cx,
 86            );
 87            editor.set_placeholder_text("Send a message", cx);
 88            editor.set_soft_wrap();
 89            editor
 90        });
 91
 92        let project = project.clone();
 93        let load_task = cx.spawn_in(window, async move |this, cx| {
 94            let agent = AcpServer::stdio(child, project, cx);
 95            let result = agent.create_thread(cx).await;
 96
 97            this.update(cx, |this, cx| {
 98                match result {
 99                    Ok(thread) => {
100                        let subscription = cx.subscribe(&thread, |this, _, event, cx| {
101                            let count = this.list_state.item_count();
102                            match event {
103                                AcpThreadEvent::NewEntry => {
104                                    this.list_state.splice(count..count, 1);
105                                }
106                                AcpThreadEvent::EntryUpdated(index) => {
107                                    this.list_state.splice(*index..*index + 1, 1);
108                                }
109                            }
110                            cx.notify();
111                        });
112                        this.list_state
113                            .splice(0..0, thread.read(cx).entries().len());
114
115                        this.thread_state = ThreadState::Ready {
116                            thread,
117                            _subscription: subscription,
118                        };
119                    }
120                    Err(e) => this.thread_state = ThreadState::LoadError(e.to_string().into()),
121                };
122                cx.notify();
123            })
124            .log_err();
125        });
126
127        let list_state = ListState::new(
128            0,
129            gpui::ListAlignment::Bottom,
130            px(2048.0),
131            cx.processor({
132                move |this: &mut Self, item: usize, window, cx| {
133                    let Some(entry) = this
134                        .thread()
135                        .and_then(|thread| thread.read(cx).entries.get(item))
136                    else {
137                        return Empty.into_any();
138                    };
139                    this.render_entry(entry, window, cx)
140                }
141            }),
142        );
143
144        Self {
145            thread_state: ThreadState::Loading { _task: load_task },
146            message_editor,
147            send_task: None,
148            list_state: list_state,
149        }
150    }
151
152    fn thread(&self) -> Option<&Entity<AcpThread>> {
153        match &self.thread_state {
154            ThreadState::Ready { thread, .. } => Some(thread),
155            ThreadState::Loading { .. } | ThreadState::LoadError(..) => None,
156        }
157    }
158
159    pub fn title(&self, cx: &App) -> SharedString {
160        match &self.thread_state {
161            ThreadState::Ready { thread, .. } => thread.read(cx).title(),
162            ThreadState::Loading { .. } => "Loading...".into(),
163            ThreadState::LoadError(_) => "Failed to load".into(),
164        }
165    }
166
167    pub fn cancel(&mut self) {
168        self.send_task.take();
169    }
170
171    fn chat(&mut self, _: &Chat, window: &mut Window, cx: &mut Context<Self>) {
172        let text = self.message_editor.read(cx).text(cx);
173        if text.is_empty() {
174            return;
175        }
176        let Some(thread) = self.thread() else { return };
177
178        let task = thread.update(cx, |thread, cx| thread.send(&text, cx));
179
180        self.send_task = Some(cx.spawn(async move |this, cx| {
181            task.await?;
182
183            this.update(cx, |this, _cx| {
184                this.send_task.take();
185            })
186        }));
187
188        self.message_editor.update(cx, |editor, cx| {
189            editor.clear(window, cx);
190        });
191    }
192
193    fn authorize_tool_call(
194        &mut self,
195        id: ToolCallId,
196        outcome: acp::ToolCallConfirmationOutcome,
197        cx: &mut Context<Self>,
198    ) {
199        let Some(thread) = self.thread() else {
200            return;
201        };
202        thread.update(cx, |thread, cx| {
203            thread.authorize_tool_call(id, outcome, cx);
204        });
205        cx.notify();
206    }
207
208    fn render_entry(
209        &self,
210        entry: &ThreadEntry,
211        window: &mut Window,
212        cx: &Context<Self>,
213    ) -> AnyElement {
214        match &entry.content {
215            AgentThreadEntryContent::Message(message) => {
216                let style = if message.role == Role::User {
217                    user_message_markdown_style(window, cx)
218                } else {
219                    default_markdown_style(window, cx)
220                };
221                let message_body = div()
222                    .children(message.chunks.iter().map(|chunk| match chunk {
223                        MessageChunk::Text { chunk } => {
224                            // todo!() open link
225                            MarkdownElement::new(chunk.clone(), style.clone())
226                        }
227                        _ => todo!(),
228                    }))
229                    .into_any();
230
231                match message.role {
232                    Role::User => div()
233                        .p_2()
234                        .pt_5()
235                        .child(
236                            div()
237                                .text_xs()
238                                .p_3()
239                                .bg(cx.theme().colors().editor_background)
240                                .rounded_lg()
241                                .shadow_md()
242                                .border_1()
243                                .border_color(cx.theme().colors().border)
244                                .child(message_body),
245                        )
246                        .into_any(),
247                    Role::Assistant => div()
248                        .text_ui(cx)
249                        .p_5()
250                        .pt_2()
251                        .child(message_body)
252                        .into_any(),
253                }
254            }
255            AgentThreadEntryContent::ToolCall(tool_call) => div()
256                .px_2()
257                .py_4()
258                .child(self.render_tool_call(tool_call, window, cx))
259                .into_any(),
260        }
261    }
262
263    fn render_tool_call(&self, tool_call: &ToolCall, window: &Window, cx: &Context<Self>) -> Div {
264        let status_icon = match &tool_call.status {
265            ToolCallStatus::WaitingForConfirmation { .. } => Empty.into_element().into_any(),
266            ToolCallStatus::Allowed {
267                status: acp::ToolCallStatus::Running,
268                ..
269            } => Icon::new(IconName::ArrowCircle)
270                .color(Color::Success)
271                .size(IconSize::Small)
272                .with_animation(
273                    "running",
274                    Animation::new(Duration::from_secs(2)).repeat(),
275                    |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
276                )
277                .into_any_element(),
278            ToolCallStatus::Allowed {
279                status: acp::ToolCallStatus::Finished,
280                ..
281            } => Icon::new(IconName::Check)
282                .color(Color::Success)
283                .size(IconSize::Small)
284                .into_any_element(),
285            ToolCallStatus::Rejected
286            | ToolCallStatus::Allowed {
287                status: acp::ToolCallStatus::Error,
288                ..
289            } => Icon::new(IconName::X)
290                .color(Color::Error)
291                .size(IconSize::Small)
292                .into_any_element(),
293        };
294
295        let content = match &tool_call.status {
296            ToolCallStatus::WaitingForConfirmation { confirmation, .. } => {
297                Some(self.render_tool_call_confirmation(tool_call.id, confirmation, cx))
298            }
299            ToolCallStatus::Allowed { content, .. } => content.clone().map(|content| {
300                div()
301                    .border_color(cx.theme().colors().border)
302                    .border_t_1()
303                    .px_2()
304                    .py_1p5()
305                    .child(MarkdownElement::new(
306                        content,
307                        default_markdown_style(window, cx),
308                    ))
309                    .into_any_element()
310            }),
311            ToolCallStatus::Rejected => None,
312        };
313
314        v_flex()
315            .text_xs()
316            .rounded_md()
317            .border_1()
318            .border_color(cx.theme().colors().border)
319            .bg(cx.theme().colors().editor_background)
320            .child(
321                h_flex()
322                    .px_2()
323                    .py_1p5()
324                    .w_full()
325                    .gap_1p5()
326                    .child(
327                        Icon::new(IconName::Cog)
328                            .size(IconSize::Small)
329                            .color(Color::Muted),
330                    )
331                    .child(MarkdownElement::new(
332                        tool_call.display_name.clone(),
333                        default_markdown_style(window, cx),
334                    ))
335                    .child(div().w_full())
336                    .child(status_icon),
337            )
338            .children(content)
339    }
340
341    fn render_tool_call_confirmation(
342        &self,
343        tool_call_id: ToolCallId,
344        confirmation: &ToolCallConfirmation,
345        cx: &Context<Self>,
346    ) -> AnyElement {
347        match confirmation {
348            ToolCallConfirmation::Edit {
349                file_name,
350                file_diff,
351            } => v_flex()
352                .border_color(cx.theme().colors().border)
353                .border_t_1()
354                .px_2()
355                .py_1p5()
356                // todo! nicer rendering
357                .child(file_name.clone())
358                .child(file_diff.clone())
359                .child(
360                    h_flex()
361                        .justify_end()
362                        .gap_1()
363                        .child(
364                            Button::new(
365                                ("always_allow", tool_call_id.as_u64()),
366                                "Always Allow Edits",
367                            )
368                            .icon(IconName::CheckDouble)
369                            .icon_position(IconPosition::Start)
370                            .icon_size(IconSize::Small)
371                            .icon_color(Color::Success)
372                            .on_click(cx.listener({
373                                let id = tool_call_id;
374                                move |this, _, _, cx| {
375                                    this.authorize_tool_call(
376                                        id,
377                                        acp::ToolCallConfirmationOutcome::AlwaysAllow,
378                                        cx,
379                                    );
380                                }
381                            })),
382                        )
383                        .child(
384                            Button::new(("allow", tool_call_id.as_u64()), "Allow")
385                                .icon(IconName::Check)
386                                .icon_position(IconPosition::Start)
387                                .icon_size(IconSize::Small)
388                                .icon_color(Color::Success)
389                                .on_click(cx.listener({
390                                    let id = tool_call_id;
391                                    move |this, _, _, cx| {
392                                        this.authorize_tool_call(
393                                            id,
394                                            acp::ToolCallConfirmationOutcome::Allow,
395                                            cx,
396                                        );
397                                    }
398                                })),
399                        )
400                        .child(
401                            Button::new(("reject", tool_call_id.as_u64()), "Reject")
402                                .icon(IconName::X)
403                                .icon_position(IconPosition::Start)
404                                .icon_size(IconSize::Small)
405                                .icon_color(Color::Error)
406                                .on_click(cx.listener({
407                                    let id = tool_call_id;
408                                    move |this, _, _, cx| {
409                                        this.authorize_tool_call(
410                                            id,
411                                            acp::ToolCallConfirmationOutcome::Reject,
412                                            cx,
413                                        );
414                                    }
415                                })),
416                        ),
417                )
418                .into_any(),
419            ToolCallConfirmation::Execute {
420                command,
421                root_command,
422            } => v_flex()
423                .border_color(cx.theme().colors().border)
424                .border_t_1()
425                .px_2()
426                .py_1p5()
427                // todo! nicer rendering
428                .child(command.clone())
429                .child(
430                    h_flex()
431                        .justify_end()
432                        .gap_1()
433                        .child(
434                            Button::new(
435                                ("always_allow", tool_call_id.as_u64()),
436                                format!("Always Allow {root_command}"),
437                            )
438                            .icon(IconName::CheckDouble)
439                            .icon_position(IconPosition::Start)
440                            .icon_size(IconSize::Small)
441                            .icon_color(Color::Success)
442                            .on_click(cx.listener({
443                                let id = tool_call_id;
444                                move |this, _, _, cx| {
445                                    this.authorize_tool_call(
446                                        id,
447                                        acp::ToolCallConfirmationOutcome::AlwaysAllow,
448                                        cx,
449                                    );
450                                }
451                            })),
452                        )
453                        .child(
454                            Button::new(("allow", tool_call_id.as_u64()), "Allow")
455                                .icon(IconName::Check)
456                                .icon_position(IconPosition::Start)
457                                .icon_size(IconSize::Small)
458                                .icon_color(Color::Success)
459                                .on_click(cx.listener({
460                                    let id = tool_call_id;
461                                    move |this, _, _, cx| {
462                                        this.authorize_tool_call(
463                                            id,
464                                            acp::ToolCallConfirmationOutcome::Allow,
465                                            cx,
466                                        );
467                                    }
468                                })),
469                        )
470                        .child(
471                            Button::new(("reject", tool_call_id.as_u64()), "Reject")
472                                .icon(IconName::X)
473                                .icon_position(IconPosition::Start)
474                                .icon_size(IconSize::Small)
475                                .icon_color(Color::Error)
476                                .on_click(cx.listener({
477                                    let id = tool_call_id;
478                                    move |this, _, _, cx| {
479                                        this.authorize_tool_call(
480                                            id,
481                                            acp::ToolCallConfirmationOutcome::Reject,
482                                            cx,
483                                        );
484                                    }
485                                })),
486                        ),
487                )
488                .into_any(),
489            ToolCallConfirmation::Mcp {
490                server_name,
491                tool_name: _,
492                tool_display_name,
493            } => v_flex()
494                .border_color(cx.theme().colors().border)
495                .border_t_1()
496                .px_2()
497                .py_1p5()
498                // todo! nicer rendering
499                .child(format!("{server_name} - {tool_display_name}"))
500                .child(
501                    h_flex()
502                        .justify_end()
503                        .gap_1()
504                        .child(
505                            Button::new(
506                                ("always_allow_server", tool_call_id.as_u64()),
507                                format!("Always Allow {server_name}"),
508                            )
509                            .icon(IconName::CheckDouble)
510                            .icon_position(IconPosition::Start)
511                            .icon_size(IconSize::Small)
512                            .icon_color(Color::Success)
513                            .on_click(cx.listener({
514                                let id = tool_call_id;
515                                move |this, _, _, cx| {
516                                    this.authorize_tool_call(
517                                        id,
518                                        acp::ToolCallConfirmationOutcome::AlwaysAllowMcpServer,
519                                        cx,
520                                    );
521                                }
522                            })),
523                        )
524                        .child(
525                            Button::new(
526                                ("always_allow_tool", tool_call_id.as_u64()),
527                                format!("Always Allow {tool_display_name}"),
528                            )
529                            .icon(IconName::CheckDouble)
530                            .icon_position(IconPosition::Start)
531                            .icon_size(IconSize::Small)
532                            .icon_color(Color::Success)
533                            .on_click(cx.listener({
534                                let id = tool_call_id;
535                                move |this, _, _, cx| {
536                                    this.authorize_tool_call(
537                                        id,
538                                        acp::ToolCallConfirmationOutcome::AlwaysAllowTool,
539                                        cx,
540                                    );
541                                }
542                            })),
543                        )
544                        .child(
545                            Button::new(("allow", tool_call_id.as_u64()), "Allow")
546                                .icon(IconName::Check)
547                                .icon_position(IconPosition::Start)
548                                .icon_size(IconSize::Small)
549                                .icon_color(Color::Success)
550                                .on_click(cx.listener({
551                                    let id = tool_call_id;
552                                    move |this, _, _, cx| {
553                                        this.authorize_tool_call(
554                                            id,
555                                            acp::ToolCallConfirmationOutcome::Allow,
556                                            cx,
557                                        );
558                                    }
559                                })),
560                        )
561                        .child(
562                            Button::new(("reject", tool_call_id.as_u64()), "Reject")
563                                .icon(IconName::X)
564                                .icon_position(IconPosition::Start)
565                                .icon_size(IconSize::Small)
566                                .icon_color(Color::Error)
567                                .on_click(cx.listener({
568                                    let id = tool_call_id;
569                                    move |this, _, _, cx| {
570                                        this.authorize_tool_call(
571                                            id,
572                                            acp::ToolCallConfirmationOutcome::Reject,
573                                            cx,
574                                        );
575                                    }
576                                })),
577                        ),
578                )
579                .into_any(),
580            ToolCallConfirmation::Info { prompt, urls: _ } => v_flex()
581                .border_color(cx.theme().colors().border)
582                .border_t_1()
583                .px_2()
584                .py_1p5()
585                // todo! nicer rendering
586                .child(prompt.clone())
587                .child(
588                    h_flex()
589                        .justify_end()
590                        .gap_1()
591                        .child(
592                            Button::new(("always_allow", tool_call_id.as_u64()), "Always Allow")
593                                .icon(IconName::CheckDouble)
594                                .icon_position(IconPosition::Start)
595                                .icon_size(IconSize::Small)
596                                .icon_color(Color::Success)
597                                .on_click(cx.listener({
598                                    let id = tool_call_id;
599                                    move |this, _, _, cx| {
600                                        this.authorize_tool_call(
601                                            id,
602                                            acp::ToolCallConfirmationOutcome::AlwaysAllow,
603                                            cx,
604                                        );
605                                    }
606                                })),
607                        )
608                        .child(
609                            Button::new(("allow", tool_call_id.as_u64()), "Allow")
610                                .icon(IconName::Check)
611                                .icon_position(IconPosition::Start)
612                                .icon_size(IconSize::Small)
613                                .icon_color(Color::Success)
614                                .on_click(cx.listener({
615                                    let id = tool_call_id;
616                                    move |this, _, _, cx| {
617                                        this.authorize_tool_call(
618                                            id,
619                                            acp::ToolCallConfirmationOutcome::Allow,
620                                            cx,
621                                        );
622                                    }
623                                })),
624                        )
625                        .child(
626                            Button::new(("reject", tool_call_id.as_u64()), "Reject")
627                                .icon(IconName::X)
628                                .icon_position(IconPosition::Start)
629                                .icon_size(IconSize::Small)
630                                .icon_color(Color::Error)
631                                .on_click(cx.listener({
632                                    let id = tool_call_id;
633                                    move |this, _, _, cx| {
634                                        this.authorize_tool_call(
635                                            id,
636                                            acp::ToolCallConfirmationOutcome::Reject,
637                                            cx,
638                                        );
639                                    }
640                                })),
641                        ),
642                )
643                .into_any(),
644        }
645    }
646}
647
648impl Focusable for AcpThreadView {
649    fn focus_handle(&self, cx: &App) -> FocusHandle {
650        self.message_editor.focus_handle(cx)
651    }
652}
653
654impl Render for AcpThreadView {
655    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
656        let text = self.message_editor.read(cx).text(cx);
657        let is_editor_empty = text.is_empty();
658        let focus_handle = self.message_editor.focus_handle(cx);
659
660        v_flex()
661            .key_context("MessageEditor")
662            .on_action(cx.listener(Self::chat))
663            .h_full()
664            .child(match &self.thread_state {
665                ThreadState::Loading { .. } => v_flex()
666                    .p_2()
667                    .flex_1()
668                    .justify_end()
669                    .child(Label::new("Connecting to Gemini...")),
670                ThreadState::LoadError(e) => div()
671                    .p_2()
672                    .flex_1()
673                    .justify_end()
674                    .child(Label::new(format!("Failed to load {e}")).into_any_element()),
675                ThreadState::Ready { thread, .. } => v_flex()
676                    .flex_1()
677                    .gap_2()
678                    .pb_2()
679                    .child(
680                        list(self.list_state.clone())
681                            .with_sizing_behavior(gpui::ListSizingBehavior::Auto)
682                            .flex_grow(),
683                    )
684                    .child(div().px_3().children(if self.send_task.is_none() {
685                        None
686                    } else {
687                        Label::new(if thread.read(cx).waiting_for_tool_confirmation() {
688                            "Waiting for tool confirmation"
689                        } else {
690                            "Generating..."
691                        })
692                        .color(Color::Muted)
693                        .size(LabelSize::Small)
694                        .into()
695                    })),
696            })
697            .child(
698                v_flex()
699                    .bg(cx.theme().colors().editor_background)
700                    .border_t_1()
701                    .border_color(cx.theme().colors().border)
702                    .p_2()
703                    .gap_2()
704                    .child(self.message_editor.clone())
705                    .child(h_flex().justify_end().child(if self.send_task.is_some() {
706                        IconButton::new("stop-generation", IconName::StopFilled)
707                            .icon_color(Color::Error)
708                            .style(ButtonStyle::Tinted(ui::TintColor::Error))
709                            .tooltip(move |window, cx| {
710                                Tooltip::for_action(
711                                    "Stop Generation",
712                                    &editor::actions::Cancel,
713                                    window,
714                                    cx,
715                                )
716                            })
717                            .disabled(is_editor_empty)
718                            .on_click(cx.listener(|this, _event, _, _| this.cancel()))
719                    } else {
720                        IconButton::new("send-message", IconName::Send)
721                            .icon_color(Color::Accent)
722                            .style(ButtonStyle::Filled)
723                            .disabled(is_editor_empty)
724                            .on_click({
725                                let focus_handle = focus_handle.clone();
726                                move |_event, window, cx| {
727                                    focus_handle.dispatch_action(&Chat, window, cx);
728                                }
729                            })
730                            .when(!is_editor_empty, |button| {
731                                button.tooltip(move |window, cx| {
732                                    Tooltip::for_action("Send", &Chat, window, cx)
733                                })
734                            })
735                            .when(is_editor_empty, |button| {
736                                button.tooltip(Tooltip::text("Type a message to submit"))
737                            })
738                    })),
739            )
740    }
741}
742
743fn user_message_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
744    let mut style = default_markdown_style(window, cx);
745    let mut text_style = window.text_style();
746    let theme_settings = ThemeSettings::get_global(cx);
747
748    let buffer_font = theme_settings.buffer_font.family.clone();
749    let buffer_font_size = TextSize::Small.rems(cx);
750
751    text_style.refine(&TextStyleRefinement {
752        font_family: Some(buffer_font),
753        font_size: Some(buffer_font_size.into()),
754        ..Default::default()
755    });
756
757    style.base_text_style = text_style;
758    style
759}
760
761fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
762    let theme_settings = ThemeSettings::get_global(cx);
763    let colors = cx.theme().colors();
764    let ui_font_size = TextSize::Default.rems(cx);
765    let buffer_font_size = TextSize::Small.rems(cx);
766    let mut text_style = window.text_style();
767    let line_height = buffer_font_size * 1.75;
768
769    text_style.refine(&TextStyleRefinement {
770        font_family: Some(theme_settings.ui_font.family.clone()),
771        font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
772        font_features: Some(theme_settings.ui_font.features.clone()),
773        font_size: Some(ui_font_size.into()),
774        line_height: Some(line_height.into()),
775        color: Some(cx.theme().colors().text),
776        ..Default::default()
777    });
778
779    MarkdownStyle {
780        base_text_style: text_style.clone(),
781        syntax: cx.theme().syntax().clone(),
782        selection_background_color: cx.theme().colors().element_selection_background,
783        code_block_overflow_x_scroll: true,
784        table_overflow_x_scroll: true,
785        heading_level_styles: Some(HeadingLevelStyles {
786            h1: Some(TextStyleRefinement {
787                font_size: Some(rems(1.15).into()),
788                ..Default::default()
789            }),
790            h2: Some(TextStyleRefinement {
791                font_size: Some(rems(1.1).into()),
792                ..Default::default()
793            }),
794            h3: Some(TextStyleRefinement {
795                font_size: Some(rems(1.05).into()),
796                ..Default::default()
797            }),
798            h4: Some(TextStyleRefinement {
799                font_size: Some(rems(1.).into()),
800                ..Default::default()
801            }),
802            h5: Some(TextStyleRefinement {
803                font_size: Some(rems(0.95).into()),
804                ..Default::default()
805            }),
806            h6: Some(TextStyleRefinement {
807                font_size: Some(rems(0.875).into()),
808                ..Default::default()
809            }),
810        }),
811        code_block: StyleRefinement {
812            padding: EdgesRefinement {
813                top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
814                left: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
815                right: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
816                bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
817            },
818            background: Some(colors.editor_background.into()),
819            text: Some(TextStyleRefinement {
820                font_family: Some(theme_settings.buffer_font.family.clone()),
821                font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
822                font_features: Some(theme_settings.buffer_font.features.clone()),
823                font_size: Some(buffer_font_size.into()),
824                ..Default::default()
825            }),
826            ..Default::default()
827        },
828        inline_code: TextStyleRefinement {
829            font_family: Some(theme_settings.buffer_font.family.clone()),
830            font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
831            font_features: Some(theme_settings.buffer_font.features.clone()),
832            font_size: Some(buffer_font_size.into()),
833            background_color: Some(colors.editor_foreground.opacity(0.08)),
834            ..Default::default()
835        },
836        link: TextStyleRefinement {
837            background_color: Some(colors.editor_foreground.opacity(0.025)),
838            underline: Some(UnderlineStyle {
839                color: Some(colors.text_accent.opacity(0.5)),
840                thickness: px(1.),
841                ..Default::default()
842            }),
843            ..Default::default()
844        },
845        link_callback: Some(Rc::new(move |_url, _cx| {
846            // todo!()
847            // if MentionLink::is_valid(url) {
848            //     let colors = cx.theme().colors();
849            //     Some(TextStyleRefinement {
850            //         background_color: Some(colors.element_background),
851            //         ..Default::default()
852            //     })
853            // } else {
854            None
855            // }
856        })),
857        ..Default::default()
858    }
859}