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