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