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