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