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