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