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}