1use std::{
2 cmp::Reverse, collections::hash_map::Entry, path::PathBuf, str::FromStr, sync::Arc,
3 time::Duration,
4};
5
6use chrono::TimeDelta;
7use client::{Client, UserStore};
8use cloud_llm_client::predict_edits_v3::{DeclarationScoreComponents, PromptFormat};
9use collections::HashMap;
10use editor::{Editor, EditorEvent, EditorMode, ExcerptRange, MultiBuffer};
11use feature_flags::FeatureFlagAppExt as _;
12use futures::{StreamExt as _, channel::oneshot};
13use gpui::{
14 CursorStyle, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity,
15 actions, prelude::*,
16};
17use language::{Buffer, DiskState};
18use ordered_float::OrderedFloat;
19use project::{Project, WorktreeId};
20use ui::{ContextMenu, ContextMenuEntry, DropdownMenu, prelude::*};
21use ui_input::SingleLineInput;
22use util::{ResultExt, paths::PathStyle, rel_path::RelPath};
23use workspace::{Item, SplitDirection, Workspace};
24use zeta2::{PredictionDebugInfo, Zeta, Zeta2FeatureFlag, ZetaOptions};
25
26use edit_prediction_context::{
27 DeclarationStyle, EditPredictionContextOptions, EditPredictionExcerptOptions,
28};
29
30actions!(
31 dev,
32 [
33 /// Opens the language server protocol logs viewer.
34 OpenZeta2Inspector
35 ]
36);
37
38pub fn init(cx: &mut App) {
39 cx.observe_new(move |workspace: &mut Workspace, _, _cx| {
40 workspace.register_action(move |workspace, _: &OpenZeta2Inspector, window, cx| {
41 let project = workspace.project();
42 workspace.split_item(
43 SplitDirection::Right,
44 Box::new(cx.new(|cx| {
45 Zeta2Inspector::new(
46 &project,
47 workspace.client(),
48 workspace.user_store(),
49 window,
50 cx,
51 )
52 })),
53 window,
54 cx,
55 );
56 });
57 })
58 .detach();
59}
60
61// TODO show included diagnostics, and events
62
63pub struct Zeta2Inspector {
64 focus_handle: FocusHandle,
65 project: Entity<Project>,
66 last_prediction: Option<LastPrediction>,
67 max_excerpt_bytes_input: Entity<SingleLineInput>,
68 min_excerpt_bytes_input: Entity<SingleLineInput>,
69 cursor_context_ratio_input: Entity<SingleLineInput>,
70 max_prompt_bytes_input: Entity<SingleLineInput>,
71 max_retrieved_declarations: Entity<SingleLineInput>,
72 active_view: ActiveView,
73 zeta: Entity<Zeta>,
74 _active_editor_subscription: Option<Subscription>,
75 _update_state_task: Task<()>,
76 _receive_task: Task<()>,
77}
78
79#[derive(PartialEq)]
80enum ActiveView {
81 Context,
82 Inference,
83}
84
85struct LastPrediction {
86 context_editor: Entity<Editor>,
87 prompt_editor: Entity<Editor>,
88 retrieval_time: TimeDelta,
89 buffer: WeakEntity<Buffer>,
90 position: language::Anchor,
91 state: LastPredictionState,
92 _task: Option<Task<()>>,
93}
94
95enum LastPredictionState {
96 Requested,
97 Success {
98 inference_time: TimeDelta,
99 parsing_time: TimeDelta,
100 prompt_planning_time: TimeDelta,
101 model_response_editor: Entity<Editor>,
102 },
103 Failed {
104 message: String,
105 },
106}
107
108impl Zeta2Inspector {
109 pub fn new(
110 project: &Entity<Project>,
111 client: &Arc<Client>,
112 user_store: &Entity<UserStore>,
113 window: &mut Window,
114 cx: &mut Context<Self>,
115 ) -> Self {
116 let zeta = Zeta::global(client, user_store, cx);
117 let mut request_rx = zeta.update(cx, |zeta, _cx| zeta.debug_info());
118
119 let receive_task = cx.spawn_in(window, async move |this, cx| {
120 while let Some(prediction) = request_rx.next().await {
121 this.update_in(cx, |this, window, cx| {
122 this.update_last_prediction(prediction, window, cx)
123 })
124 .ok();
125 }
126 });
127
128 let mut this = Self {
129 focus_handle: cx.focus_handle(),
130 project: project.clone(),
131 last_prediction: None,
132 active_view: ActiveView::Context,
133 max_excerpt_bytes_input: Self::number_input("Max Excerpt Bytes", window, cx),
134 min_excerpt_bytes_input: Self::number_input("Min Excerpt Bytes", window, cx),
135 cursor_context_ratio_input: Self::number_input("Cursor Context Ratio", window, cx),
136 max_prompt_bytes_input: Self::number_input("Max Prompt Bytes", window, cx),
137 max_retrieved_declarations: Self::number_input("Max Retrieved Definitions", window, cx),
138 zeta: zeta.clone(),
139 _active_editor_subscription: None,
140 _update_state_task: Task::ready(()),
141 _receive_task: receive_task,
142 };
143 this.set_input_options(&zeta.read(cx).options().clone(), window, cx);
144 this
145 }
146
147 fn set_input_options(
148 &mut self,
149 options: &ZetaOptions,
150 window: &mut Window,
151 cx: &mut Context<Self>,
152 ) {
153 self.max_excerpt_bytes_input.update(cx, |input, cx| {
154 input.set_text(options.context.excerpt.max_bytes.to_string(), window, cx);
155 });
156 self.min_excerpt_bytes_input.update(cx, |input, cx| {
157 input.set_text(options.context.excerpt.min_bytes.to_string(), window, cx);
158 });
159 self.cursor_context_ratio_input.update(cx, |input, cx| {
160 input.set_text(
161 format!(
162 "{:.2}",
163 options
164 .context
165 .excerpt
166 .target_before_cursor_over_total_bytes
167 ),
168 window,
169 cx,
170 );
171 });
172 self.max_prompt_bytes_input.update(cx, |input, cx| {
173 input.set_text(options.max_prompt_bytes.to_string(), window, cx);
174 });
175 self.max_retrieved_declarations.update(cx, |input, cx| {
176 input.set_text(
177 options.context.max_retrieved_declarations.to_string(),
178 window,
179 cx,
180 );
181 });
182 cx.notify();
183 }
184
185 fn set_options(&mut self, options: ZetaOptions, cx: &mut Context<Self>) {
186 self.zeta.update(cx, |this, _cx| this.set_options(options));
187
188 const THROTTLE_TIME: Duration = Duration::from_millis(100);
189
190 if let Some(prediction) = self.last_prediction.as_mut() {
191 if let Some(buffer) = prediction.buffer.upgrade() {
192 let position = prediction.position;
193 let zeta = self.zeta.clone();
194 let project = self.project.clone();
195 prediction._task = Some(cx.spawn(async move |_this, cx| {
196 cx.background_executor().timer(THROTTLE_TIME).await;
197 if let Some(task) = zeta
198 .update(cx, |zeta, cx| {
199 zeta.refresh_prediction(&project, &buffer, position, cx)
200 })
201 .ok()
202 {
203 task.await.log_err();
204 }
205 }));
206 prediction.state = LastPredictionState::Requested;
207 } else {
208 self.last_prediction.take();
209 }
210 }
211
212 cx.notify();
213 }
214
215 fn number_input(
216 label: &'static str,
217 window: &mut Window,
218 cx: &mut Context<Self>,
219 ) -> Entity<SingleLineInput> {
220 let input = cx.new(|cx| {
221 SingleLineInput::new(window, cx, "")
222 .label(label)
223 .label_min_width(px(64.))
224 });
225
226 cx.subscribe_in(
227 &input.read(cx).editor().clone(),
228 window,
229 |this, _, event, _window, cx| {
230 let EditorEvent::BufferEdited = event else {
231 return;
232 };
233
234 fn number_input_value<T: FromStr + Default>(
235 input: &Entity<SingleLineInput>,
236 cx: &App,
237 ) -> T {
238 input
239 .read(cx)
240 .editor()
241 .read(cx)
242 .text(cx)
243 .parse::<T>()
244 .unwrap_or_default()
245 }
246
247 let zeta_options = this.zeta.read(cx).options().clone();
248
249 let context_options = EditPredictionContextOptions {
250 excerpt: EditPredictionExcerptOptions {
251 max_bytes: number_input_value(&this.max_excerpt_bytes_input, cx),
252 min_bytes: number_input_value(&this.min_excerpt_bytes_input, cx),
253 target_before_cursor_over_total_bytes: number_input_value(
254 &this.cursor_context_ratio_input,
255 cx,
256 ),
257 },
258 max_retrieved_declarations: number_input_value(
259 &this.max_retrieved_declarations,
260 cx,
261 ),
262 ..zeta_options.context
263 };
264
265 this.set_options(
266 ZetaOptions {
267 context: context_options,
268 max_prompt_bytes: number_input_value(&this.max_prompt_bytes_input, cx),
269 max_diagnostic_bytes: zeta_options.max_diagnostic_bytes,
270 prompt_format: zeta_options.prompt_format,
271 file_indexing_parallelism: zeta_options.file_indexing_parallelism,
272 },
273 cx,
274 );
275 },
276 )
277 .detach();
278 input
279 }
280
281 fn update_last_prediction(
282 &mut self,
283 prediction: zeta2::PredictionDebugInfo,
284 window: &mut Window,
285 cx: &mut Context<Self>,
286 ) {
287 let project = self.project.read(cx);
288 let path_style = project.path_style(cx);
289 let Some(worktree_id) = project
290 .worktrees(cx)
291 .next()
292 .map(|worktree| worktree.read(cx).id())
293 else {
294 log::error!("Open a worktree to use edit prediction debug view");
295 self.last_prediction.take();
296 return;
297 };
298
299 self._update_state_task = cx.spawn_in(window, {
300 let language_registry = self.project.read(cx).languages().clone();
301 async move |this, cx| {
302 let mut languages = HashMap::default();
303 for lang_id in prediction
304 .context
305 .declarations
306 .iter()
307 .map(|snippet| snippet.declaration.identifier().language_id)
308 .chain(prediction.context.excerpt_text.language_id)
309 {
310 if let Entry::Vacant(entry) = languages.entry(lang_id) {
311 // Most snippets are gonna be the same language,
312 // so we think it's fine to do this sequentially for now
313 entry.insert(language_registry.language_for_id(lang_id).await.ok());
314 }
315 }
316
317 let markdown_language = language_registry
318 .language_for_name("Markdown")
319 .await
320 .log_err();
321
322 this.update_in(cx, |this, window, cx| {
323 let context_editor = cx.new(|cx| {
324 let mut excerpt_score_components = HashMap::default();
325
326 let multibuffer = cx.new(|cx| {
327 let mut multibuffer = MultiBuffer::new(language::Capability::ReadOnly);
328 let excerpt_file = Arc::new(ExcerptMetadataFile {
329 title: RelPath::unix("Cursor Excerpt").unwrap().into(),
330 path_style,
331 worktree_id,
332 });
333
334 let excerpt_buffer = cx.new(|cx| {
335 let mut buffer =
336 Buffer::local(prediction.context.excerpt_text.body, cx);
337 if let Some(language) = prediction
338 .context
339 .excerpt_text
340 .language_id
341 .as_ref()
342 .and_then(|id| languages.get(id))
343 {
344 buffer.set_language(language.clone(), cx);
345 }
346 buffer.file_updated(excerpt_file, cx);
347 buffer
348 });
349
350 multibuffer.push_excerpts(
351 excerpt_buffer,
352 [ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
353 cx,
354 );
355
356 let mut declarations = prediction.context.declarations.clone();
357 declarations.sort_unstable_by_key(|declaration| {
358 Reverse(OrderedFloat(
359 declaration.score(DeclarationStyle::Declaration),
360 ))
361 });
362
363 for snippet in &declarations {
364 let path = this
365 .project
366 .read(cx)
367 .path_for_entry(snippet.declaration.project_entry_id(), cx);
368
369 let snippet_file = Arc::new(ExcerptMetadataFile {
370 title: RelPath::unix(&format!(
371 "{} (Score: {})",
372 path.map(|p| p.path.display(path_style).to_string())
373 .unwrap_or_else(|| "".to_string()),
374 snippet.score(DeclarationStyle::Declaration)
375 ))
376 .unwrap()
377 .into(),
378 path_style,
379 worktree_id,
380 });
381
382 let excerpt_buffer = cx.new(|cx| {
383 let mut buffer =
384 Buffer::local(snippet.declaration.item_text().0, cx);
385 buffer.file_updated(snippet_file, cx);
386 if let Some(language) =
387 languages.get(&snippet.declaration.identifier().language_id)
388 {
389 buffer.set_language(language.clone(), cx);
390 }
391 buffer
392 });
393
394 let excerpt_ids = multibuffer.push_excerpts(
395 excerpt_buffer,
396 [ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
397 cx,
398 );
399 let excerpt_id = excerpt_ids.first().unwrap();
400
401 excerpt_score_components
402 .insert(*excerpt_id, snippet.components.clone());
403 }
404
405 multibuffer
406 });
407
408 let mut editor =
409 Editor::new(EditorMode::full(), multibuffer, None, window, cx);
410 editor.register_addon(ZetaContextAddon {
411 excerpt_score_components,
412 });
413 editor
414 });
415
416 let PredictionDebugInfo {
417 response_rx,
418 position,
419 buffer,
420 retrieval_time,
421 local_prompt,
422 ..
423 } = prediction;
424
425 let task = cx.spawn_in(window, {
426 let markdown_language = markdown_language.clone();
427 async move |this, cx| {
428 let response = response_rx.await;
429
430 this.update_in(cx, |this, window, cx| {
431 if let Some(prediction) = this.last_prediction.as_mut() {
432 prediction.state = match response {
433 Ok(Ok(response)) => {
434 prediction.prompt_editor.update(
435 cx,
436 |prompt_editor, cx| {
437 prompt_editor.set_text(
438 response.prompt,
439 window,
440 cx,
441 );
442 },
443 );
444
445 LastPredictionState::Success {
446 prompt_planning_time: response.prompt_planning_time,
447 inference_time: response.inference_time,
448 parsing_time: response.parsing_time,
449 model_response_editor: cx.new(|cx| {
450 let buffer = cx.new(|cx| {
451 let mut buffer = Buffer::local(
452 response.model_response,
453 cx,
454 );
455 buffer.set_language(markdown_language, cx);
456 buffer
457 });
458 let buffer = cx.new(|cx| {
459 MultiBuffer::singleton(buffer, cx)
460 });
461 let mut editor = Editor::new(
462 EditorMode::full(),
463 buffer,
464 None,
465 window,
466 cx,
467 );
468 editor.set_read_only(true);
469 editor.set_show_line_numbers(false, cx);
470 editor.set_show_gutter(false, cx);
471 editor.set_show_scrollbars(false, cx);
472 editor
473 }),
474 }
475 }
476 Ok(Err(err)) => {
477 LastPredictionState::Failed { message: err }
478 }
479 Err(oneshot::Canceled) => LastPredictionState::Failed {
480 message: "Canceled".to_string(),
481 },
482 };
483 }
484 })
485 .ok();
486 }
487 });
488
489 this.last_prediction = Some(LastPrediction {
490 context_editor,
491 prompt_editor: cx.new(|cx| {
492 let buffer = cx.new(|cx| {
493 let mut buffer =
494 Buffer::local(local_prompt.unwrap_or_else(|err| err), cx);
495 buffer.set_language(markdown_language.clone(), cx);
496 buffer
497 });
498 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
499 let mut editor =
500 Editor::new(EditorMode::full(), buffer, None, window, cx);
501 editor.set_read_only(true);
502 editor.set_show_line_numbers(false, cx);
503 editor.set_show_gutter(false, cx);
504 editor.set_show_scrollbars(false, cx);
505 editor
506 }),
507 retrieval_time,
508 buffer,
509 position,
510 state: LastPredictionState::Requested,
511 _task: Some(task),
512 });
513 cx.notify();
514 })
515 .ok();
516 }
517 });
518 }
519
520 fn render_options(&self, window: &mut Window, cx: &mut Context<Self>) -> Div {
521 v_flex()
522 .gap_2()
523 .child(
524 h_flex()
525 .child(Headline::new("Options").size(HeadlineSize::Small))
526 .justify_between()
527 .child(
528 ui::Button::new("reset-options", "Reset")
529 .disabled(self.zeta.read(cx).options() == &zeta2::DEFAULT_OPTIONS)
530 .style(ButtonStyle::Outlined)
531 .size(ButtonSize::Large)
532 .on_click(cx.listener(|this, _, window, cx| {
533 this.set_input_options(&zeta2::DEFAULT_OPTIONS, window, cx);
534 })),
535 ),
536 )
537 .child(
538 v_flex()
539 .gap_2()
540 .child(
541 h_flex()
542 .gap_2()
543 .items_end()
544 .child(self.max_excerpt_bytes_input.clone())
545 .child(self.min_excerpt_bytes_input.clone())
546 .child(self.cursor_context_ratio_input.clone()),
547 )
548 .child(
549 h_flex()
550 .gap_2()
551 .items_end()
552 .child(self.max_retrieved_declarations.clone())
553 .child(self.max_prompt_bytes_input.clone())
554 .child(self.render_prompt_format_dropdown(window, cx)),
555 ),
556 )
557 }
558
559 fn render_prompt_format_dropdown(&self, window: &mut Window, cx: &mut Context<Self>) -> Div {
560 let active_format = self.zeta.read(cx).options().prompt_format;
561 let this = cx.weak_entity();
562
563 v_flex()
564 .gap_1p5()
565 .child(
566 Label::new("Prompt Format")
567 .size(LabelSize::Small)
568 .color(Color::Muted),
569 )
570 .child(
571 DropdownMenu::new(
572 "ep-prompt-format",
573 active_format.to_string(),
574 ContextMenu::build(window, cx, move |mut menu, _window, _cx| {
575 for prompt_format in PromptFormat::iter() {
576 menu = menu.item(
577 ContextMenuEntry::new(prompt_format.to_string())
578 .toggleable(IconPosition::End, active_format == prompt_format)
579 .handler({
580 let this = this.clone();
581 move |_window, cx| {
582 this.update(cx, |this, cx| {
583 let current_options =
584 this.zeta.read(cx).options().clone();
585 let options = ZetaOptions {
586 prompt_format,
587 ..current_options
588 };
589 this.set_options(options, cx);
590 })
591 .ok();
592 }
593 }),
594 )
595 }
596 menu
597 }),
598 )
599 .style(ui::DropdownStyle::Outlined),
600 )
601 }
602
603 fn render_tabs(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
604 if self.last_prediction.is_none() {
605 return None;
606 };
607
608 Some(
609 ui::ToggleButtonGroup::single_row(
610 "prediction",
611 [
612 ui::ToggleButtonSimple::new(
613 "Context",
614 cx.listener(|this, _, _, cx| {
615 this.active_view = ActiveView::Context;
616 cx.notify();
617 }),
618 ),
619 ui::ToggleButtonSimple::new(
620 "Inference",
621 cx.listener(|this, _, _, cx| {
622 this.active_view = ActiveView::Inference;
623 cx.notify();
624 }),
625 ),
626 ],
627 )
628 .style(ui::ToggleButtonGroupStyle::Outlined)
629 .selected_index(if self.active_view == ActiveView::Context {
630 0
631 } else {
632 1
633 })
634 .into_any_element(),
635 )
636 }
637
638 fn render_stats(&self) -> Option<Div> {
639 let Some(prediction) = self.last_prediction.as_ref() else {
640 return None;
641 };
642
643 let (prompt_planning_time, inference_time, parsing_time) = match &prediction.state {
644 LastPredictionState::Success {
645 inference_time,
646 parsing_time,
647 prompt_planning_time,
648 ..
649 } => (
650 Some(*prompt_planning_time),
651 Some(*inference_time),
652 Some(*parsing_time),
653 ),
654 LastPredictionState::Requested | LastPredictionState::Failed { .. } => {
655 (None, None, None)
656 }
657 };
658
659 Some(
660 v_flex()
661 .p_4()
662 .gap_2()
663 .min_w(px(160.))
664 .child(Headline::new("Stats").size(HeadlineSize::Small))
665 .child(Self::render_duration(
666 "Context retrieval",
667 Some(prediction.retrieval_time),
668 ))
669 .child(Self::render_duration(
670 "Prompt planning",
671 prompt_planning_time,
672 ))
673 .child(Self::render_duration("Inference", inference_time))
674 .child(Self::render_duration("Parsing", parsing_time)),
675 )
676 }
677
678 fn render_duration(name: &'static str, time: Option<chrono::TimeDelta>) -> Div {
679 h_flex()
680 .gap_1()
681 .child(Label::new(name).color(Color::Muted).size(LabelSize::Small))
682 .child(match time {
683 Some(time) => Label::new(if time.num_microseconds().unwrap_or(0) >= 1000 {
684 format!("{} ms", time.num_milliseconds())
685 } else {
686 format!("{} µs", time.num_microseconds().unwrap_or(0))
687 })
688 .size(LabelSize::Small),
689 None => Label::new("...").size(LabelSize::Small),
690 })
691 }
692
693 fn render_content(&self, cx: &mut Context<Self>) -> AnyElement {
694 if !cx.has_flag::<Zeta2FeatureFlag>() {
695 return Self::render_message("`zeta2` feature flag is not enabled");
696 }
697
698 match self.last_prediction.as_ref() {
699 None => Self::render_message("No prediction"),
700 Some(prediction) => self.render_last_prediction(prediction, cx).into_any(),
701 }
702 }
703
704 fn render_message(message: impl Into<SharedString>) -> AnyElement {
705 v_flex()
706 .size_full()
707 .justify_center()
708 .items_center()
709 .child(Label::new(message).size(LabelSize::Large))
710 .into_any()
711 }
712
713 fn render_last_prediction(&self, prediction: &LastPrediction, cx: &mut Context<Self>) -> Div {
714 match &self.active_view {
715 ActiveView::Context => div().size_full().child(prediction.context_editor.clone()),
716 ActiveView::Inference => h_flex()
717 .items_start()
718 .w_full()
719 .flex_1()
720 .border_t_1()
721 .border_color(cx.theme().colors().border)
722 .bg(cx.theme().colors().editor_background)
723 .child(
724 v_flex()
725 .flex_1()
726 .gap_2()
727 .p_4()
728 .h_full()
729 .child(
730 h_flex()
731 .justify_between()
732 .child(ui::Headline::new("Prompt").size(ui::HeadlineSize::XSmall))
733 .child(match prediction.state {
734 LastPredictionState::Requested
735 | LastPredictionState::Failed { .. } => ui::Chip::new("Local")
736 .bg_color(cx.theme().status().warning_background)
737 .label_color(Color::Success),
738 LastPredictionState::Success { .. } => ui::Chip::new("Cloud")
739 .bg_color(cx.theme().status().success_background)
740 .label_color(Color::Success),
741 }),
742 )
743 .child(prediction.prompt_editor.clone()),
744 )
745 .child(ui::vertical_divider())
746 .child(
747 v_flex()
748 .flex_1()
749 .gap_2()
750 .h_full()
751 .p_4()
752 .child(ui::Headline::new("Model Response").size(ui::HeadlineSize::XSmall))
753 .child(match &prediction.state {
754 LastPredictionState::Success {
755 model_response_editor,
756 ..
757 } => model_response_editor.clone().into_any_element(),
758 LastPredictionState::Requested => v_flex()
759 .p_4()
760 .gap_2()
761 .child(Label::new("Loading...").buffer_font(cx))
762 .into_any(),
763 LastPredictionState::Failed { message } => v_flex()
764 .p_4()
765 .gap_2()
766 .child(Label::new(message.clone()).buffer_font(cx))
767 .into_any(),
768 }),
769 ),
770 }
771 }
772}
773
774impl Focusable for Zeta2Inspector {
775 fn focus_handle(&self, _cx: &App) -> FocusHandle {
776 self.focus_handle.clone()
777 }
778}
779
780impl Item for Zeta2Inspector {
781 type Event = ();
782
783 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
784 "Zeta2 Inspector".into()
785 }
786}
787
788impl EventEmitter<()> for Zeta2Inspector {}
789
790impl Render for Zeta2Inspector {
791 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
792 v_flex()
793 .size_full()
794 .bg(cx.theme().colors().editor_background)
795 .child(
796 h_flex()
797 .w_full()
798 .child(
799 v_flex()
800 .flex_1()
801 .p_4()
802 .h_full()
803 .justify_between()
804 .child(self.render_options(window, cx))
805 .gap_4()
806 .children(self.render_tabs(cx)),
807 )
808 .child(ui::vertical_divider())
809 .children(self.render_stats()),
810 )
811 .child(self.render_content(cx))
812 }
813}
814
815// Using same approach as commit view
816
817struct ExcerptMetadataFile {
818 title: Arc<RelPath>,
819 worktree_id: WorktreeId,
820 path_style: PathStyle,
821}
822
823impl language::File for ExcerptMetadataFile {
824 fn as_local(&self) -> Option<&dyn language::LocalFile> {
825 None
826 }
827
828 fn disk_state(&self) -> DiskState {
829 DiskState::New
830 }
831
832 fn path(&self) -> &Arc<RelPath> {
833 &self.title
834 }
835
836 fn full_path(&self, _: &App) -> PathBuf {
837 self.title.as_std_path().to_path_buf()
838 }
839
840 fn file_name<'a>(&'a self, _: &'a App) -> &'a str {
841 self.title.file_name().unwrap()
842 }
843
844 fn path_style(&self, _: &App) -> PathStyle {
845 self.path_style
846 }
847
848 fn worktree_id(&self, _: &App) -> WorktreeId {
849 self.worktree_id
850 }
851
852 fn to_proto(&self, _: &App) -> language::proto::File {
853 unimplemented!()
854 }
855
856 fn is_private(&self) -> bool {
857 false
858 }
859}
860
861struct ZetaContextAddon {
862 excerpt_score_components: HashMap<editor::ExcerptId, DeclarationScoreComponents>,
863}
864
865impl editor::Addon for ZetaContextAddon {
866 fn to_any(&self) -> &dyn std::any::Any {
867 self
868 }
869
870 fn render_buffer_header_controls(
871 &self,
872 excerpt_info: &multi_buffer::ExcerptInfo,
873 _window: &Window,
874 _cx: &App,
875 ) -> Option<AnyElement> {
876 let score_components = self.excerpt_score_components.get(&excerpt_info.id)?.clone();
877
878 Some(
879 div()
880 .id(excerpt_info.id.to_proto() as usize)
881 .child(ui::Icon::new(IconName::Info))
882 .cursor(CursorStyle::PointingHand)
883 .tooltip(move |_, cx| {
884 cx.new(|_| ScoreComponentsTooltip::new(&score_components))
885 .into()
886 })
887 .into_any(),
888 )
889 }
890}
891
892struct ScoreComponentsTooltip {
893 text: SharedString,
894}
895
896impl ScoreComponentsTooltip {
897 fn new(components: &DeclarationScoreComponents) -> Self {
898 Self {
899 text: format!("{:#?}", components).into(),
900 }
901 }
902}
903
904impl Render for ScoreComponentsTooltip {
905 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
906 div().pl_2().pt_2p5().child(
907 div()
908 .elevation_2(cx)
909 .py_1()
910 .px_2()
911 .child(ui::Label::new(self.text.clone()).buffer_font(cx)),
912 )
913 }
914}