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