zeta2_tools.rs

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