zeta2_tools.rs

  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}