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}