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}