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