configure_context_server_modal.rs

  1use std::{
  2    path::PathBuf,
  3    sync::{Arc, Mutex},
  4};
  5
  6use anyhow::{Context as _, Result};
  7use context_server::{ContextServerCommand, ContextServerId};
  8use editor::{Editor, EditorElement, EditorStyle};
  9use gpui::{
 10    AsyncWindowContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Task,
 11    TextStyle, TextStyleRefinement, UnderlineStyle, WeakEntity, prelude::*,
 12};
 13use language::{Language, LanguageRegistry};
 14use markdown::{Markdown, MarkdownElement, MarkdownStyle};
 15use notifications::status_toast::{StatusToast, ToastIcon};
 16use project::{
 17    context_server_store::{
 18        ContextServerStatus, ContextServerStore, registry::ContextServerDescriptorRegistry,
 19    },
 20    project_settings::{ContextServerSettings, ProjectSettings},
 21    worktree_store::WorktreeStore,
 22};
 23use settings::{Settings as _, update_settings_file};
 24use theme::ThemeSettings;
 25use ui::{
 26    CommonAnimationExt, KeyBinding, Modal, ModalFooter, ModalHeader, Section, Tooltip, prelude::*,
 27};
 28use util::ResultExt as _;
 29use workspace::{ModalView, Workspace};
 30
 31use crate::AddContextServer;
 32
 33enum ConfigurationTarget {
 34    New,
 35    Existing {
 36        id: ContextServerId,
 37        command: ContextServerCommand,
 38    },
 39    Extension {
 40        id: ContextServerId,
 41        repository_url: Option<SharedString>,
 42        installation: Option<extension::ContextServerConfiguration>,
 43    },
 44}
 45
 46enum ConfigurationSource {
 47    New {
 48        editor: Entity<Editor>,
 49    },
 50    Existing {
 51        editor: Entity<Editor>,
 52    },
 53    Extension {
 54        id: ContextServerId,
 55        editor: Option<Entity<Editor>>,
 56        repository_url: Option<SharedString>,
 57        installation_instructions: Option<Entity<markdown::Markdown>>,
 58        settings_validator: Option<jsonschema::Validator>,
 59    },
 60}
 61
 62impl ConfigurationSource {
 63    fn has_configuration_options(&self) -> bool {
 64        !matches!(self, ConfigurationSource::Extension { editor: None, .. })
 65    }
 66
 67    fn is_new(&self) -> bool {
 68        matches!(self, ConfigurationSource::New { .. })
 69    }
 70
 71    fn from_target(
 72        target: ConfigurationTarget,
 73        language_registry: Arc<LanguageRegistry>,
 74        jsonc_language: Option<Arc<Language>>,
 75        window: &mut Window,
 76        cx: &mut App,
 77    ) -> Self {
 78        fn create_editor(
 79            json: String,
 80            jsonc_language: Option<Arc<Language>>,
 81            window: &mut Window,
 82            cx: &mut App,
 83        ) -> Entity<Editor> {
 84            cx.new(|cx| {
 85                let mut editor = Editor::auto_height(4, 16, window, cx);
 86                editor.set_text(json, window, cx);
 87                editor.set_show_gutter(false, cx);
 88                editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx);
 89                if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
 90                    buffer.update(cx, |buffer, cx| buffer.set_language(jsonc_language, cx))
 91                }
 92                editor
 93            })
 94        }
 95
 96        match target {
 97            ConfigurationTarget::New => ConfigurationSource::New {
 98                editor: create_editor(context_server_input(None), jsonc_language, window, cx),
 99            },
100            ConfigurationTarget::Existing { id, command } => ConfigurationSource::Existing {
101                editor: create_editor(
102                    context_server_input(Some((id, command))),
103                    jsonc_language,
104                    window,
105                    cx,
106                ),
107            },
108            ConfigurationTarget::Extension {
109                id,
110                repository_url,
111                installation,
112            } => {
113                let settings_validator = installation.as_ref().and_then(|installation| {
114                    jsonschema::validator_for(&installation.settings_schema)
115                        .context("Failed to load JSON schema for context server settings")
116                        .log_err()
117                });
118                let installation_instructions = installation.as_ref().map(|installation| {
119                    cx.new(|cx| {
120                        Markdown::new(
121                            installation.installation_instructions.clone().into(),
122                            Some(language_registry.clone()),
123                            None,
124                            cx,
125                        )
126                    })
127                });
128                ConfigurationSource::Extension {
129                    id,
130                    repository_url,
131                    installation_instructions,
132                    settings_validator,
133                    editor: installation.map(|installation| {
134                        create_editor(installation.default_settings, jsonc_language, window, cx)
135                    }),
136                }
137            }
138        }
139    }
140
141    fn output(&self, cx: &mut App) -> Result<(ContextServerId, ContextServerSettings)> {
142        match self {
143            ConfigurationSource::New { editor } | ConfigurationSource::Existing { editor } => {
144                parse_input(&editor.read(cx).text(cx)).map(|(id, command)| {
145                    (
146                        id,
147                        ContextServerSettings::Custom {
148                            enabled: true,
149                            command,
150                        },
151                    )
152                })
153            }
154            ConfigurationSource::Extension {
155                id,
156                editor,
157                settings_validator,
158                ..
159            } => {
160                let text = editor
161                    .as_ref()
162                    .context("No output available")?
163                    .read(cx)
164                    .text(cx);
165                let settings = serde_json_lenient::from_str::<serde_json::Value>(&text)?;
166                if let Some(settings_validator) = settings_validator
167                    && let Err(error) = settings_validator.validate(&settings)
168                {
169                    return Err(anyhow::anyhow!(error.to_string()));
170                }
171                Ok((
172                    id.clone(),
173                    ContextServerSettings::Extension {
174                        enabled: true,
175                        settings,
176                    },
177                ))
178            }
179        }
180    }
181}
182
183fn context_server_input(existing: Option<(ContextServerId, ContextServerCommand)>) -> String {
184    let (name, command, args, env) = match existing {
185        Some((id, cmd)) => {
186            let args = serde_json::to_string(&cmd.args).unwrap();
187            let env = serde_json::to_string(&cmd.env.unwrap_or_default()).unwrap();
188            (id.0.to_string(), cmd.path, args, env)
189        }
190        None => (
191            "some-mcp-server".to_string(),
192            PathBuf::new(),
193            "[]".to_string(),
194            "{}".to_string(),
195        ),
196    };
197
198    format!(
199        r#"{{
200  /// The name of your MCP server
201  "{name}": {{
202    /// The command which runs the MCP server
203    "command": "{}",
204    /// The arguments to pass to the MCP server
205    "args": {args},
206    /// The environment variables to set
207    "env": {env}
208  }}
209}}"#,
210        command.display()
211    )
212}
213
214fn resolve_context_server_extension(
215    id: ContextServerId,
216    worktree_store: Entity<WorktreeStore>,
217    cx: &mut App,
218) -> Task<Option<ConfigurationTarget>> {
219    let registry = ContextServerDescriptorRegistry::default_global(cx).read(cx);
220
221    let Some(descriptor) = registry.context_server_descriptor(&id.0) else {
222        return Task::ready(None);
223    };
224
225    let extension = crate::agent_configuration::resolve_extension_for_context_server(&id, cx);
226    cx.spawn(async move |cx| {
227        let installation = descriptor
228            .configuration(worktree_store, cx)
229            .await
230            .context("Failed to resolve context server configuration")
231            .log_err()
232            .flatten();
233
234        Some(ConfigurationTarget::Extension {
235            id,
236            repository_url: extension
237                .and_then(|(_, manifest)| manifest.repository.clone().map(SharedString::from)),
238            installation,
239        })
240    })
241}
242
243enum State {
244    Idle,
245    Waiting,
246    Error(SharedString),
247}
248
249pub struct ConfigureContextServerModal {
250    context_server_store: Entity<ContextServerStore>,
251    workspace: WeakEntity<Workspace>,
252    source: ConfigurationSource,
253    state: State,
254    original_server_id: Option<ContextServerId>,
255}
256
257impl ConfigureContextServerModal {
258    pub fn register(
259        workspace: &mut Workspace,
260        language_registry: Arc<LanguageRegistry>,
261        _window: Option<&mut Window>,
262        _cx: &mut Context<Workspace>,
263    ) {
264        workspace.register_action({
265            move |_workspace, _: &AddContextServer, window, cx| {
266                let workspace_handle = cx.weak_entity();
267                let language_registry = language_registry.clone();
268                window
269                    .spawn(cx, async move |cx| {
270                        Self::show_modal(
271                            ConfigurationTarget::New,
272                            language_registry,
273                            workspace_handle,
274                            cx,
275                        )
276                        .await
277                    })
278                    .detach_and_log_err(cx);
279            }
280        });
281    }
282
283    pub fn show_modal_for_existing_server(
284        server_id: ContextServerId,
285        language_registry: Arc<LanguageRegistry>,
286        workspace: WeakEntity<Workspace>,
287        window: &mut Window,
288        cx: &mut App,
289    ) -> Task<Result<()>> {
290        let Some(settings) = ProjectSettings::get_global(cx)
291            .context_servers
292            .get(&server_id.0)
293            .cloned()
294            .or_else(|| {
295                ContextServerDescriptorRegistry::default_global(cx)
296                    .read(cx)
297                    .context_server_descriptor(&server_id.0)
298                    .map(|_| ContextServerSettings::default_extension())
299            })
300        else {
301            return Task::ready(Err(anyhow::anyhow!("Context server not found")));
302        };
303
304        window.spawn(cx, async move |cx| {
305            let target = match settings {
306                ContextServerSettings::Custom {
307                    enabled: _,
308                    command,
309                } => Some(ConfigurationTarget::Existing {
310                    id: server_id,
311                    command,
312                }),
313                ContextServerSettings::Extension { .. } => {
314                    match workspace
315                        .update(cx, |workspace, cx| {
316                            resolve_context_server_extension(
317                                server_id,
318                                workspace.project().read(cx).worktree_store(),
319                                cx,
320                            )
321                        })
322                        .ok()
323                    {
324                        Some(task) => task.await,
325                        None => None,
326                    }
327                }
328            };
329
330            match target {
331                Some(target) => Self::show_modal(target, language_registry, workspace, cx).await,
332                None => Err(anyhow::anyhow!("Failed to resolve context server")),
333            }
334        })
335    }
336
337    fn show_modal(
338        target: ConfigurationTarget,
339        language_registry: Arc<LanguageRegistry>,
340        workspace: WeakEntity<Workspace>,
341        cx: &mut AsyncWindowContext,
342    ) -> Task<Result<()>> {
343        cx.spawn(async move |cx| {
344            let jsonc_language = language_registry.language_for_name("jsonc").await.ok();
345            workspace.update_in(cx, |workspace, window, cx| {
346                let workspace_handle = cx.weak_entity();
347                let context_server_store = workspace.project().read(cx).context_server_store();
348                workspace.toggle_modal(window, cx, |window, cx| Self {
349                    context_server_store,
350                    workspace: workspace_handle,
351                    state: State::Idle,
352                    original_server_id: match &target {
353                        ConfigurationTarget::Existing { id, .. } => Some(id.clone()),
354                        ConfigurationTarget::Extension { id, .. } => Some(id.clone()),
355                        ConfigurationTarget::New => None,
356                    },
357                    source: ConfigurationSource::from_target(
358                        target,
359                        language_registry,
360                        jsonc_language,
361                        window,
362                        cx,
363                    ),
364                })
365            })
366        })
367    }
368
369    fn set_error(&mut self, err: impl Into<SharedString>, cx: &mut Context<Self>) {
370        self.state = State::Error(err.into());
371        cx.notify();
372    }
373
374    fn confirm(&mut self, _: &menu::Confirm, cx: &mut Context<Self>) {
375        self.state = State::Idle;
376        let Some(workspace) = self.workspace.upgrade() else {
377            return;
378        };
379
380        let (id, settings) = match self.source.output(cx) {
381            Ok(val) => val,
382            Err(error) => {
383                self.set_error(error.to_string(), cx);
384                return;
385            }
386        };
387
388        self.state = State::Waiting;
389
390        let existing_server = self.context_server_store.read(cx).get_running_server(&id);
391        if existing_server.is_some() {
392            self.context_server_store.update(cx, |store, cx| {
393                store.stop_server(&id, cx).log_err();
394            });
395        }
396
397        let wait_for_context_server_task =
398            wait_for_context_server(&self.context_server_store, id.clone(), cx);
399        cx.spawn({
400            let id = id.clone();
401            async move |this, cx| {
402                let result = wait_for_context_server_task.await;
403                this.update(cx, |this, cx| match result {
404                    Ok(_) => {
405                        this.state = State::Idle;
406                        this.show_configured_context_server_toast(id, cx);
407                        cx.emit(DismissEvent);
408                    }
409                    Err(err) => {
410                        this.set_error(err, cx);
411                    }
412                })
413            }
414        })
415        .detach();
416
417        let settings_changed =
418            ProjectSettings::get_global(cx).context_servers.get(&id.0) != Some(&settings);
419
420        if settings_changed {
421            // When we write the settings to the file, the context server will be restarted.
422            workspace.update(cx, |workspace, cx| {
423                let fs = workspace.app_state().fs.clone();
424                let original_server_id = self.original_server_id.clone();
425                update_settings_file::<ProjectSettings>(
426                    fs.clone(),
427                    cx,
428                    move |project_settings, _| {
429                        if let Some(original_id) = original_server_id {
430                            if original_id != id {
431                                project_settings.context_servers.remove(&original_id.0);
432                            }
433                        }
434                        project_settings.context_servers.insert(id.0, settings);
435                    },
436                );
437            });
438        } else if let Some(existing_server) = existing_server {
439            self.context_server_store
440                .update(cx, |store, cx| store.start_server(existing_server, cx));
441        }
442    }
443
444    fn cancel(&mut self, _: &menu::Cancel, cx: &mut Context<Self>) {
445        cx.emit(DismissEvent);
446    }
447
448    fn show_configured_context_server_toast(&self, id: ContextServerId, cx: &mut App) {
449        self.workspace
450            .update(cx, {
451                |workspace, cx| {
452                    let status_toast = StatusToast::new(
453                        format!("{} configured successfully.", id.0),
454                        cx,
455                        |this, _cx| {
456                            this.icon(ToastIcon::new(IconName::ToolHammer).color(Color::Muted))
457                                .action("Dismiss", |_, _| {})
458                        },
459                    );
460
461                    workspace.toggle_status_toast(status_toast, cx);
462                }
463            })
464            .log_err();
465    }
466}
467
468fn parse_input(text: &str) -> Result<(ContextServerId, ContextServerCommand)> {
469    let value: serde_json::Value = serde_json_lenient::from_str(text)?;
470    let object = value.as_object().context("Expected object")?;
471    anyhow::ensure!(object.len() == 1, "Expected exactly one key-value pair");
472    let (context_server_name, value) = object.into_iter().next().unwrap();
473    let command: ContextServerCommand = serde_json::from_value(value.clone())?;
474    Ok((ContextServerId(context_server_name.clone().into()), command))
475}
476
477impl ModalView for ConfigureContextServerModal {}
478
479impl Focusable for ConfigureContextServerModal {
480    fn focus_handle(&self, cx: &App) -> FocusHandle {
481        match &self.source {
482            ConfigurationSource::New { editor } => editor.focus_handle(cx),
483            ConfigurationSource::Existing { editor, .. } => editor.focus_handle(cx),
484            ConfigurationSource::Extension { editor, .. } => editor
485                .as_ref()
486                .map(|editor| editor.focus_handle(cx))
487                .unwrap_or_else(|| cx.focus_handle()),
488        }
489    }
490}
491
492impl EventEmitter<DismissEvent> for ConfigureContextServerModal {}
493
494impl ConfigureContextServerModal {
495    fn render_modal_header(&self) -> ModalHeader {
496        let text: SharedString = match &self.source {
497            ConfigurationSource::New { .. } => "Add MCP Server".into(),
498            ConfigurationSource::Existing { .. } => "Configure MCP Server".into(),
499            ConfigurationSource::Extension { id, .. } => format!("Configure {}", id.0).into(),
500        };
501        ModalHeader::new().headline(text)
502    }
503
504    fn render_modal_description(&self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
505        const MODAL_DESCRIPTION: &str = "Visit the MCP server configuration docs to find all necessary arguments and environment variables.";
506
507        if let ConfigurationSource::Extension {
508            installation_instructions: Some(installation_instructions),
509            ..
510        } = &self.source
511        {
512            div()
513                .pb_2()
514                .text_sm()
515                .child(MarkdownElement::new(
516                    installation_instructions.clone(),
517                    default_markdown_style(window, cx),
518                ))
519                .into_any_element()
520        } else {
521            Label::new(MODAL_DESCRIPTION)
522                .color(Color::Muted)
523                .into_any_element()
524        }
525    }
526
527    fn render_modal_content(&self, cx: &App) -> AnyElement {
528        let editor = match &self.source {
529            ConfigurationSource::New { editor } => editor,
530            ConfigurationSource::Existing { editor } => editor,
531            ConfigurationSource::Extension { editor, .. } => {
532                let Some(editor) = editor else {
533                    return div().into_any_element();
534                };
535                editor
536            }
537        };
538
539        div()
540            .p_2()
541            .rounded_md()
542            .border_1()
543            .border_color(cx.theme().colors().border_variant)
544            .bg(cx.theme().colors().editor_background)
545            .child({
546                let settings = ThemeSettings::get_global(cx);
547                let text_style = TextStyle {
548                    color: cx.theme().colors().text,
549                    font_family: settings.buffer_font.family.clone(),
550                    font_fallbacks: settings.buffer_font.fallbacks.clone(),
551                    font_size: settings.buffer_font_size(cx).into(),
552                    font_weight: settings.buffer_font.weight,
553                    line_height: relative(settings.buffer_line_height.value()),
554                    ..Default::default()
555                };
556                EditorElement::new(
557                    editor,
558                    EditorStyle {
559                        background: cx.theme().colors().editor_background,
560                        local_player: cx.theme().players().local(),
561                        text: text_style,
562                        syntax: cx.theme().syntax().clone(),
563                        ..Default::default()
564                    },
565                )
566            })
567            .into_any_element()
568    }
569
570    fn render_modal_footer(&self, window: &mut Window, cx: &mut Context<Self>) -> ModalFooter {
571        let focus_handle = self.focus_handle(cx);
572        let is_connecting = matches!(self.state, State::Waiting);
573
574        ModalFooter::new()
575            .start_slot::<Button>(
576                if let ConfigurationSource::Extension {
577                    repository_url: Some(repository_url),
578                    ..
579                } = &self.source
580                {
581                    Some(
582                        Button::new("open-repository", "Open Repository")
583                            .icon(IconName::ArrowUpRight)
584                            .icon_color(Color::Muted)
585                            .icon_size(IconSize::Small)
586                            .tooltip({
587                                let repository_url = repository_url.clone();
588                                move |window, cx| {
589                                    Tooltip::with_meta(
590                                        "Open Repository",
591                                        None,
592                                        repository_url.clone(),
593                                        window,
594                                        cx,
595                                    )
596                                }
597                            })
598                            .on_click({
599                                let repository_url = repository_url.clone();
600                                move |_, _, cx| cx.open_url(&repository_url)
601                            }),
602                    )
603                } else {
604                    None
605                },
606            )
607            .end_slot(
608                h_flex()
609                    .gap_2()
610                    .child(
611                        Button::new(
612                            "cancel",
613                            if self.source.has_configuration_options() {
614                                "Cancel"
615                            } else {
616                                "Dismiss"
617                            },
618                        )
619                        .key_binding(
620                            KeyBinding::for_action_in(&menu::Cancel, &focus_handle, window, cx)
621                                .map(|kb| kb.size(rems_from_px(12.))),
622                        )
623                        .on_click(
624                            cx.listener(|this, _event, _window, cx| this.cancel(&menu::Cancel, cx)),
625                        ),
626                    )
627                    .children(self.source.has_configuration_options().then(|| {
628                        Button::new(
629                            "add-server",
630                            if self.source.is_new() {
631                                "Add Server"
632                            } else {
633                                "Configure Server"
634                            },
635                        )
636                        .disabled(is_connecting)
637                        .key_binding(
638                            KeyBinding::for_action_in(&menu::Confirm, &focus_handle, window, cx)
639                                .map(|kb| kb.size(rems_from_px(12.))),
640                        )
641                        .on_click(
642                            cx.listener(|this, _event, _window, cx| {
643                                this.confirm(&menu::Confirm, cx)
644                            }),
645                        )
646                    })),
647            )
648    }
649
650    fn render_waiting_for_context_server() -> Div {
651        h_flex()
652            .gap_2()
653            .child(
654                Icon::new(IconName::ArrowCircle)
655                    .size(IconSize::XSmall)
656                    .color(Color::Info)
657                    .with_rotate_animation(2)
658                    .into_any_element(),
659            )
660            .child(
661                Label::new("Waiting for Context Server")
662                    .size(LabelSize::Small)
663                    .color(Color::Muted),
664            )
665    }
666
667    fn render_modal_error(error: SharedString) -> Div {
668        h_flex()
669            .gap_2()
670            .child(
671                Icon::new(IconName::Warning)
672                    .size(IconSize::XSmall)
673                    .color(Color::Warning),
674            )
675            .child(
676                div()
677                    .w_full()
678                    .child(Label::new(error).size(LabelSize::Small).color(Color::Muted)),
679            )
680    }
681}
682
683impl Render for ConfigureContextServerModal {
684    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
685        div()
686            .elevation_3(cx)
687            .w(rems(34.))
688            .key_context("ConfigureContextServerModal")
689            .on_action(
690                cx.listener(|this, _: &menu::Cancel, _window, cx| this.cancel(&menu::Cancel, cx)),
691            )
692            .on_action(
693                cx.listener(|this, _: &menu::Confirm, _window, cx| {
694                    this.confirm(&menu::Confirm, cx)
695                }),
696            )
697            .capture_any_mouse_down(cx.listener(|this, _, window, cx| {
698                this.focus_handle(cx).focus(window);
699            }))
700            .child(
701                Modal::new("configure-context-server", None)
702                    .header(self.render_modal_header())
703                    .section(
704                        Section::new()
705                            .child(self.render_modal_description(window, cx))
706                            .child(self.render_modal_content(cx))
707                            .child(match &self.state {
708                                State::Idle => div(),
709                                State::Waiting => Self::render_waiting_for_context_server(),
710                                State::Error(error) => Self::render_modal_error(error.clone()),
711                            }),
712                    )
713                    .footer(self.render_modal_footer(window, cx)),
714            )
715    }
716}
717
718fn wait_for_context_server(
719    context_server_store: &Entity<ContextServerStore>,
720    context_server_id: ContextServerId,
721    cx: &mut App,
722) -> Task<Result<(), Arc<str>>> {
723    let (tx, rx) = futures::channel::oneshot::channel();
724    let tx = Arc::new(Mutex::new(Some(tx)));
725
726    let subscription = cx.subscribe(context_server_store, move |_, event, _cx| match event {
727        project::context_server_store::Event::ServerStatusChanged { server_id, status } => {
728            match status {
729                ContextServerStatus::Running => {
730                    if server_id == &context_server_id
731                        && let Some(tx) = tx.lock().unwrap().take()
732                    {
733                        let _ = tx.send(Ok(()));
734                    }
735                }
736                ContextServerStatus::Stopped => {
737                    if server_id == &context_server_id
738                        && let Some(tx) = tx.lock().unwrap().take()
739                    {
740                        let _ = tx.send(Err("Context server stopped running".into()));
741                    }
742                }
743                ContextServerStatus::Error(error) => {
744                    if server_id == &context_server_id
745                        && let Some(tx) = tx.lock().unwrap().take()
746                    {
747                        let _ = tx.send(Err(error.clone()));
748                    }
749                }
750                _ => {}
751            }
752        }
753    });
754
755    cx.spawn(async move |_cx| {
756        let result = rx
757            .await
758            .map_err(|_| Arc::from("Context server store was dropped"))?;
759        drop(subscription);
760        result
761    })
762}
763
764pub(crate) fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
765    let theme_settings = ThemeSettings::get_global(cx);
766    let colors = cx.theme().colors();
767    let mut text_style = window.text_style();
768    text_style.refine(&TextStyleRefinement {
769        font_family: Some(theme_settings.ui_font.family.clone()),
770        font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
771        font_features: Some(theme_settings.ui_font.features.clone()),
772        font_size: Some(TextSize::XSmall.rems(cx).into()),
773        color: Some(colors.text_muted),
774        ..Default::default()
775    });
776
777    MarkdownStyle {
778        base_text_style: text_style.clone(),
779        selection_background_color: colors.element_selection_background,
780        link: TextStyleRefinement {
781            background_color: Some(colors.editor_foreground.opacity(0.025)),
782            underline: Some(UnderlineStyle {
783                color: Some(colors.text_accent.opacity(0.5)),
784                thickness: px(1.),
785                ..Default::default()
786            }),
787            ..Default::default()
788        },
789        ..Default::default()
790    }
791}