1use std::{
2 any::TypeId,
3 collections::VecDeque,
4 ops::Add,
5 sync::Arc,
6 time::{Duration, Instant},
7};
8
9use anyhow::Result;
10use client::{Client, UserStore};
11use editor::{Editor, PathKey};
12use futures::StreamExt as _;
13use gpui::{
14 Animation, AnimationExt, App, AppContext as _, Context, Entity, EventEmitter, FocusHandle,
15 Focusable, ParentElement as _, SharedString, Styled as _, Task, TextAlign, Window, actions,
16 pulsating_between,
17};
18use multi_buffer::MultiBuffer;
19use project::Project;
20use text::OffsetRangeExt;
21use ui::{
22 ButtonCommon, Clickable, Color, Disableable, FluentBuilder as _, Icon, IconButton, IconName,
23 IconSize, InteractiveElement, IntoElement, ListHeader, ListItem, StyledTypography, div, h_flex,
24 v_flex,
25};
26use workspace::{Item, ItemHandle as _};
27use zeta2::{Zeta, ZetaContextRetrievalDebugInfo, ZetaDebugInfo, ZetaSearchQueryDebugInfo};
28
29pub struct Zeta2ContextView {
30 empty_focus_handle: FocusHandle,
31 project: Entity<Project>,
32 zeta: Entity<Zeta>,
33 runs: VecDeque<RetrievalRun>,
34 current_ix: usize,
35 _update_task: Task<Result<()>>,
36}
37
38#[derive(Debug)]
39struct RetrievalRun {
40 editor: Entity<Editor>,
41 search_queries: Vec<GlobQueries>,
42 started_at: Instant,
43 search_results_generated_at: Option<Instant>,
44 search_results_executed_at: Option<Instant>,
45 search_results_filtered_at: Option<Instant>,
46 finished_at: Option<Instant>,
47}
48
49#[derive(Debug)]
50struct GlobQueries {
51 glob: String,
52 alternations: Vec<String>,
53}
54
55actions!(
56 dev,
57 [
58 /// Go to the previous context retrieval run
59 Zeta2ContextGoBack,
60 /// Go to the next context retrieval run
61 Zeta2ContextGoForward
62 ]
63);
64
65impl Zeta2ContextView {
66 pub fn new(
67 project: Entity<Project>,
68 client: &Arc<Client>,
69 user_store: &Entity<UserStore>,
70 window: &mut gpui::Window,
71 cx: &mut Context<Self>,
72 ) -> Self {
73 let zeta = Zeta::global(client, user_store, cx);
74
75 let mut debug_rx = zeta.update(cx, |zeta, _| zeta.debug_info());
76 let _update_task = cx.spawn_in(window, async move |this, cx| {
77 while let Some(event) = debug_rx.next().await {
78 this.update_in(cx, |this, window, cx| {
79 this.handle_zeta_event(event, window, cx)
80 })?;
81 }
82 Ok(())
83 });
84
85 Self {
86 empty_focus_handle: cx.focus_handle(),
87 project,
88 runs: VecDeque::new(),
89 current_ix: 0,
90 zeta,
91 _update_task,
92 }
93 }
94
95 fn handle_zeta_event(
96 &mut self,
97 event: ZetaDebugInfo,
98 window: &mut gpui::Window,
99 cx: &mut Context<Self>,
100 ) {
101 match event {
102 ZetaDebugInfo::ContextRetrievalStarted(info) => {
103 if info.project == self.project {
104 self.handle_context_retrieval_started(info, window, cx);
105 }
106 }
107 ZetaDebugInfo::SearchQueriesGenerated(info) => {
108 if info.project == self.project {
109 self.handle_search_queries_generated(info, window, cx);
110 }
111 }
112 ZetaDebugInfo::SearchQueriesExecuted(info) => {
113 if info.project == self.project {
114 self.handle_search_queries_executed(info, window, cx);
115 }
116 }
117 ZetaDebugInfo::SearchResultsFiltered(info) => {
118 if info.project == self.project {
119 self.handle_search_results_filtered(info, window, cx);
120 }
121 }
122 ZetaDebugInfo::ContextRetrievalFinished(info) => {
123 if info.project == self.project {
124 self.handle_context_retrieval_finished(info, window, cx);
125 }
126 }
127 ZetaDebugInfo::EditPredicted(_) => {}
128 }
129 }
130
131 fn handle_context_retrieval_started(
132 &mut self,
133 info: ZetaContextRetrievalDebugInfo,
134 window: &mut Window,
135 cx: &mut Context<Self>,
136 ) {
137 if self
138 .runs
139 .back()
140 .is_some_and(|run| run.search_results_executed_at.is_none())
141 {
142 self.runs.pop_back();
143 }
144
145 let multibuffer = cx.new(|_| MultiBuffer::new(language::Capability::ReadOnly));
146 let editor = cx
147 .new(|cx| Editor::for_multibuffer(multibuffer, Some(self.project.clone()), window, cx));
148
149 if self.runs.len() == 32 {
150 self.runs.pop_front();
151 }
152
153 self.runs.push_back(RetrievalRun {
154 editor,
155 search_queries: Vec::new(),
156 started_at: info.timestamp,
157 search_results_generated_at: None,
158 search_results_executed_at: None,
159 search_results_filtered_at: None,
160 finished_at: None,
161 });
162
163 cx.notify();
164 }
165
166 fn handle_context_retrieval_finished(
167 &mut self,
168 info: ZetaContextRetrievalDebugInfo,
169 window: &mut Window,
170 cx: &mut Context<Self>,
171 ) {
172 let Some(run) = self.runs.back_mut() else {
173 return;
174 };
175
176 run.finished_at = Some(info.timestamp);
177
178 let multibuffer = run.editor.read(cx).buffer().clone();
179 multibuffer.update(cx, |multibuffer, cx| {
180 multibuffer.clear(cx);
181
182 let context = self.zeta.read(cx).context_for_project(&self.project);
183 let mut paths = Vec::new();
184 for (buffer, ranges) in context {
185 let path = PathKey::for_buffer(&buffer, cx);
186 let snapshot = buffer.read(cx).snapshot();
187 let ranges = ranges
188 .iter()
189 .map(|range| range.to_point(&snapshot))
190 .collect::<Vec<_>>();
191 paths.push((path, buffer, ranges));
192 }
193
194 for (path, buffer, ranges) in paths {
195 multibuffer.set_excerpts_for_path(path, buffer, ranges, 0, cx);
196 }
197 });
198
199 run.editor.update(cx, |editor, cx| {
200 editor.move_to_beginning(&Default::default(), window, cx);
201 });
202
203 cx.notify();
204 }
205
206 fn handle_search_queries_generated(
207 &mut self,
208 info: ZetaSearchQueryDebugInfo,
209 _window: &mut Window,
210 cx: &mut Context<Self>,
211 ) {
212 let Some(run) = self.runs.back_mut() else {
213 return;
214 };
215
216 run.search_results_generated_at = Some(info.timestamp);
217 run.search_queries = info
218 .queries
219 .into_iter()
220 .map(|query| {
221 let mut regex_parser = regex_syntax::ast::parse::Parser::new();
222
223 GlobQueries {
224 glob: query.glob,
225 alternations: match regex_parser.parse(&query.regex) {
226 Ok(regex_syntax::ast::Ast::Alternation(ref alt)) => {
227 alt.asts.iter().map(|ast| ast.to_string()).collect()
228 }
229 _ => vec![query.regex],
230 },
231 }
232 })
233 .collect();
234 cx.notify();
235 }
236
237 fn handle_search_queries_executed(
238 &mut self,
239 info: ZetaContextRetrievalDebugInfo,
240 _window: &mut Window,
241 cx: &mut Context<Self>,
242 ) {
243 if self.current_ix + 2 == self.runs.len() {
244 // Switch to latest when the queries are executed
245 self.current_ix += 1;
246 }
247
248 let Some(run) = self.runs.back_mut() else {
249 return;
250 };
251
252 run.search_results_executed_at = Some(info.timestamp);
253 cx.notify();
254 }
255
256 fn handle_search_results_filtered(
257 &mut self,
258 info: ZetaContextRetrievalDebugInfo,
259 _window: &mut Window,
260 cx: &mut Context<Self>,
261 ) {
262 let Some(run) = self.runs.back_mut() else {
263 return;
264 };
265
266 run.search_results_filtered_at = Some(info.timestamp);
267 cx.notify();
268 }
269
270 fn handle_go_back(
271 &mut self,
272 _: &Zeta2ContextGoBack,
273 window: &mut Window,
274 cx: &mut Context<Self>,
275 ) {
276 self.current_ix = self.current_ix.saturating_sub(1);
277 cx.focus_self(window);
278 cx.notify();
279 }
280
281 fn handle_go_forward(
282 &mut self,
283 _: &Zeta2ContextGoForward,
284 window: &mut Window,
285 cx: &mut Context<Self>,
286 ) {
287 self.current_ix = self
288 .current_ix
289 .add(1)
290 .min(self.runs.len().saturating_sub(1));
291 cx.focus_self(window);
292 cx.notify();
293 }
294
295 fn render_informational_footer(&self, cx: &mut Context<'_, Zeta2ContextView>) -> ui::Div {
296 let is_latest = self.runs.len() == self.current_ix + 1;
297 let run = &self.runs[self.current_ix];
298
299 h_flex()
300 .p_2()
301 .w_full()
302 .font_buffer(cx)
303 .text_xs()
304 .border_t_1()
305 .gap_2()
306 .child(
307 v_flex().h_full().flex_1().children(
308 run.search_queries
309 .iter()
310 .enumerate()
311 .flat_map(|(ix, query)| {
312 std::iter::once(ListHeader::new(query.glob.clone()).into_any_element())
313 .chain(query.alternations.iter().enumerate().map(
314 move |(alt_ix, alt)| {
315 ListItem::new(ix * 100 + alt_ix)
316 .start_slot(
317 Icon::new(IconName::MagnifyingGlass)
318 .color(Color::Muted)
319 .size(IconSize::Small),
320 )
321 .child(alt.clone())
322 .into_any_element()
323 },
324 ))
325 }),
326 ),
327 )
328 .child(
329 v_flex()
330 .h_full()
331 .text_align(TextAlign::Right)
332 .child(
333 h_flex()
334 .justify_end()
335 .child(
336 IconButton::new("go-back", IconName::ChevronLeft)
337 .disabled(self.current_ix == 0 || self.runs.len() < 2)
338 .tooltip(ui::Tooltip::for_action_title(
339 "Go to previous run",
340 &Zeta2ContextGoBack,
341 ))
342 .on_click(cx.listener(|this, _, window, cx| {
343 this.handle_go_back(&Zeta2ContextGoBack, window, cx);
344 })),
345 )
346 .child(
347 div()
348 .child(format!("{}/{}", self.current_ix + 1, self.runs.len()))
349 .map(|this| {
350 if self.runs.back().is_some_and(|back| {
351 back.search_results_executed_at.is_none()
352 }) {
353 this.with_animation(
354 "pulsating-count",
355 Animation::new(Duration::from_secs(2))
356 .repeat()
357 .with_easing(pulsating_between(0.4, 0.8)),
358 |label, delta| label.opacity(delta),
359 )
360 .into_any_element()
361 } else {
362 this.into_any_element()
363 }
364 }),
365 )
366 .child(
367 IconButton::new("go-forward", IconName::ChevronRight)
368 .disabled(self.current_ix + 1 == self.runs.len())
369 .tooltip(ui::Tooltip::for_action_title(
370 "Go to next run",
371 &Zeta2ContextGoBack,
372 ))
373 .on_click(cx.listener(|this, _, window, cx| {
374 this.handle_go_forward(&Zeta2ContextGoForward, window, cx);
375 })),
376 ),
377 )
378 .map(|mut div| {
379 let pending_message = |div: ui::Div, msg: &'static str| {
380 if is_latest {
381 return div.child(msg);
382 } else {
383 return div.child("Canceled");
384 }
385 };
386
387 let t0 = run.started_at;
388 let Some(t1) = run.search_results_generated_at else {
389 return pending_message(div, "Planning search...");
390 };
391 div = div.child(format!("Planned search: {:>5} ms", (t1 - t0).as_millis()));
392
393 let Some(t2) = run.search_results_executed_at else {
394 return pending_message(div, "Running search...");
395 };
396 div = div.child(format!("Ran search: {:>5} ms", (t2 - t1).as_millis()));
397
398 let Some(t3) = run.search_results_filtered_at else {
399 return pending_message(div, "Filtering results...");
400 };
401 div =
402 div.child(format!("Filtered results: {:>5} ms", (t3 - t2).as_millis()));
403
404 let Some(t4) = run.finished_at else {
405 return pending_message(div, "Building excerpts");
406 };
407 div = div
408 .child(format!("Build excerpts: {:>5} ยตs", (t4 - t3).as_micros()))
409 .child(format!("Total: {:>5} ms", (t4 - t0).as_millis()));
410 div
411 }),
412 )
413 }
414}
415
416impl Focusable for Zeta2ContextView {
417 fn focus_handle(&self, cx: &App) -> FocusHandle {
418 self.runs
419 .get(self.current_ix)
420 .map(|run| run.editor.read(cx).focus_handle(cx))
421 .unwrap_or_else(|| self.empty_focus_handle.clone())
422 }
423}
424
425impl EventEmitter<()> for Zeta2ContextView {}
426
427impl Item for Zeta2ContextView {
428 type Event = ();
429
430 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
431 "Edit Prediction Context".into()
432 }
433
434 fn buffer_kind(&self, _cx: &App) -> workspace::item::ItemBufferKind {
435 workspace::item::ItemBufferKind::Multibuffer
436 }
437
438 fn act_as_type<'a>(
439 &'a self,
440 type_id: TypeId,
441 self_handle: &'a Entity<Self>,
442 _: &'a App,
443 ) -> Option<gpui::AnyView> {
444 if type_id == TypeId::of::<Self>() {
445 Some(self_handle.to_any())
446 } else if type_id == TypeId::of::<Editor>() {
447 Some(self.runs.get(self.current_ix)?.editor.to_any())
448 } else {
449 None
450 }
451 }
452}
453
454impl gpui::Render for Zeta2ContextView {
455 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
456 v_flex()
457 .key_context("Zeta2Context")
458 .on_action(cx.listener(Self::handle_go_back))
459 .on_action(cx.listener(Self::handle_go_forward))
460 .size_full()
461 .map(|this| {
462 if self.runs.is_empty() {
463 this.child(
464 v_flex()
465 .size_full()
466 .justify_center()
467 .items_center()
468 .child("No retrieval runs yet"),
469 )
470 } else {
471 this.child(self.runs[self.current_ix].editor.clone())
472 .child(self.render_informational_footer(cx))
473 }
474 })
475 }
476}