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