Allow inspection of zeta2's LLM-based context retrieval (#41340)

Release Notes:

- N/A

---------

Co-authored-by: Agus Zubiaga <agus@zed.dev>
This commit is contained in:
Max Brunsfeld
2025-10-28 22:26:48 +00:00
committed by GitHub
co-authored by Agus Zubiaga
parent d31194dcf8
commit 60bd417d8b
9 changed files with 591 additions and 33 deletions
Generated
+1
View File
@@ -21670,6 +21670,7 @@ dependencies = [
name = "zeta2_tools"
version = "0.1.0"
dependencies = [
"anyhow",
"chrono",
"clap",
"client",
+7
View File
@@ -1298,5 +1298,12 @@
"ctrl-enter up": "dev::Zeta2RatePredictionPositive",
"ctrl-enter down": "dev::Zeta2RatePredictionNegative"
}
},
{
"context": "Zeta2Context > Editor",
"bindings": {
"alt-left": "dev::Zeta2ContextGoBack",
"alt-right": "dev::Zeta2ContextGoForward"
}
}
]
+7
View File
@@ -1404,5 +1404,12 @@
"cmd-enter up": "dev::Zeta2RatePredictionPositive",
"cmd-enter down": "dev::Zeta2RatePredictionNegative"
}
},
{
"context": "Zeta2Context > Editor",
"bindings": {
"alt-left": "dev::Zeta2ContextGoBack",
"alt-right": "dev::Zeta2ContextGoForward"
}
}
]
+7
View File
@@ -1327,5 +1327,12 @@
"ctrl-enter up": "dev::Zeta2RatePredictionPositive",
"ctrl-enter down": "dev::Zeta2RatePredictionNegative"
}
},
{
"context": "Zeta2Context > Editor",
"bindings": {
"alt-left": "dev::Zeta2ContextGoBack",
"alt-right": "dev::Zeta2ContextGoForward"
}
}
]
+51 -14
View File
@@ -1,10 +1,13 @@
use std::{cmp::Reverse, fmt::Write, ops::Range, path::PathBuf, sync::Arc};
use std::{cmp::Reverse, fmt::Write, ops::Range, path::PathBuf, sync::Arc, time::Instant};
use crate::merge_excerpts::write_merged_excerpts;
use crate::{
ZetaContextRetrievalDebugInfo, ZetaDebugInfo, ZetaSearchQueryDebugInfo,
merge_excerpts::write_merged_excerpts,
};
use anyhow::{Result, anyhow};
use collections::HashMap;
use edit_prediction_context::{EditPredictionExcerpt, EditPredictionExcerptOptions, Line};
use futures::{StreamExt, stream::BoxStream};
use futures::{StreamExt, channel::mpsc, stream::BoxStream};
use gpui::{App, AsyncApp, Entity, Task};
use indoc::indoc;
use language::{Anchor, Bias, Buffer, OffsetRangeExt, Point, TextBufferSnapshot, ToPoint as _};
@@ -61,22 +64,22 @@ const SEARCH_TOOL_NAME: &str = "search";
/// Search for relevant code
///
/// For the best results, run multiple queries at once with a single invocation of this tool.
#[derive(Deserialize, JsonSchema)]
struct SearchToolInput {
#[derive(Clone, Deserialize, JsonSchema)]
pub struct SearchToolInput {
/// An array of queries to run for gathering context relevant to the next prediction
#[schemars(length(max = 5))]
queries: Box<[SearchToolQuery]>,
pub queries: Box<[SearchToolQuery]>,
}
#[derive(Deserialize, JsonSchema)]
struct SearchToolQuery {
#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct SearchToolQuery {
/// A glob pattern to match file paths in the codebase
glob: String,
pub glob: String,
/// A regular expression to match content within the files matched by the glob pattern
regex: String,
pub regex: String,
/// Whether the regex is case-sensitive. Defaults to false (case-insensitive).
#[serde(default)]
case_sensitive: bool,
pub case_sensitive: bool,
}
const RESULTS_MESSAGE: &str = indoc! {"
@@ -124,6 +127,7 @@ pub fn find_related_excerpts<'a>(
project: &Entity<Project>,
events: impl Iterator<Item = &'a crate::Event>,
options: &LlmContextOptions,
debug_tx: Option<mpsc::UnboundedSender<ZetaDebugInfo>>,
cx: &App,
) -> Task<Result<HashMap<Entity<Buffer>, Vec<Range<Anchor>>>>> {
let language_model_registry = LanguageModelRegistry::global(cx);
@@ -304,11 +308,33 @@ pub fn find_related_excerpts<'a>(
snapshot: TextBufferSnapshot,
}
let search_queries = search_calls
.iter()
.map(|(_, tool_use)| {
Ok(serde_json::from_value::<SearchToolInput>(
tool_use.input.clone(),
)?)
})
.collect::<Result<Vec<_>>>()?;
if let Some(debug_tx) = &debug_tx {
debug_tx
.unbounded_send(ZetaDebugInfo::SearchQueriesGenerated(
ZetaSearchQueryDebugInfo {
project: project.clone(),
timestamp: Instant::now(),
queries: search_queries
.iter()
.flat_map(|call| call.queries.iter().cloned())
.collect(),
},
))
.ok();
}
let mut result_buffers_by_path = HashMap::default();
for (index, tool_use) in search_calls.into_iter().rev() {
let call = serde_json::from_value::<SearchToolInput>(tool_use.input.clone())?;
for ((index, tool_use), call) in search_calls.into_iter().zip(search_queries).rev() {
let mut excerpts_by_buffer = HashMap::default();
for query in call.queries {
@@ -392,6 +418,17 @@ pub fn find_related_excerpts<'a>(
},
],
);
if let Some(debug_tx) = &debug_tx {
debug_tx
.unbounded_send(ZetaDebugInfo::SearchQueriesExecuted(
ZetaContextRetrievalDebugInfo {
project: project.clone(),
timestamp: Instant::now(),
},
))
.ok();
}
}
if result_buffers_by_path.is_empty() {
+71 -14
View File
@@ -45,8 +45,8 @@ mod related_excerpts;
use crate::merge_excerpts::merge_excerpts;
use crate::prediction::EditPrediction;
pub use crate::related_excerpts::LlmContextOptions;
use crate::related_excerpts::find_related_excerpts;
pub use crate::related_excerpts::{LlmContextOptions, SearchToolQuery};
pub use provider::ZetaEditPredictionProvider;
const BUFFER_CHANGE_GROUPING_INTERVAL: Duration = Duration::from_secs(1);
@@ -107,7 +107,7 @@ pub struct Zeta {
projects: HashMap<EntityId, ZetaProject>,
options: ZetaOptions,
update_required: bool,
debug_tx: Option<mpsc::UnboundedSender<PredictionDebugInfo>>,
debug_tx: Option<mpsc::UnboundedSender<ZetaDebugInfo>>,
}
#[derive(Debug, Clone, PartialEq)]
@@ -134,7 +134,20 @@ impl ContextMode {
}
}
pub struct PredictionDebugInfo {
pub enum ZetaDebugInfo {
ContextRetrievalStarted(ZetaContextRetrievalDebugInfo),
SearchQueriesGenerated(ZetaSearchQueryDebugInfo),
SearchQueriesExecuted(ZetaContextRetrievalDebugInfo),
ContextRetrievalFinished(ZetaContextRetrievalDebugInfo),
EditPredicted(ZetaEditPredictionDebugInfo),
}
pub struct ZetaContextRetrievalDebugInfo {
pub project: Entity<Project>,
pub timestamp: Instant,
}
pub struct ZetaEditPredictionDebugInfo {
pub request: predict_edits_v3::PredictEditsRequest,
pub retrieval_time: TimeDelta,
pub buffer: WeakEntity<Buffer>,
@@ -143,6 +156,12 @@ pub struct PredictionDebugInfo {
pub response_rx: oneshot::Receiver<Result<predict_edits_v3::PredictEditsResponse, String>>,
}
pub struct ZetaSearchQueryDebugInfo {
pub project: Entity<Project>,
pub timestamp: Instant,
pub queries: Vec<SearchToolQuery>,
}
pub type RequestDebugInfo = predict_edits_v3::DebugInfo;
struct ZetaProject {
@@ -303,7 +322,7 @@ impl Zeta {
}
}
pub fn debug_info(&mut self) -> mpsc::UnboundedReceiver<PredictionDebugInfo> {
pub fn debug_info(&mut self) -> mpsc::UnboundedReceiver<ZetaDebugInfo> {
let (debug_watch_tx, debug_watch_rx) = mpsc::unbounded();
self.debug_tx = Some(debug_watch_tx);
debug_watch_rx
@@ -324,11 +343,30 @@ impl Zeta {
}
pub fn history_for_project(&self, project: &Entity<Project>) -> impl Iterator<Item = &Event> {
static EMPTY_EVENTS: VecDeque<Event> = VecDeque::new();
self.projects
.get(&project.entity_id())
.map_or(&EMPTY_EVENTS, |project| &project.events)
.iter()
.map(|project| project.events.iter())
.into_iter()
.flatten()
}
pub fn context_for_project(
&self,
project: &Entity<Project>,
) -> impl Iterator<Item = (Entity<Buffer>, &[Range<Anchor>])> {
self.projects
.get(&project.entity_id())
.and_then(|project| {
Some(
project
.context
.as_ref()?
.iter()
.map(|(buffer, ranges)| (buffer.clone(), ranges.as_slice())),
)
})
.into_iter()
.flatten()
}
pub fn usage(&self, cx: &App) -> Option<EditPredictionUsage> {
@@ -781,24 +819,19 @@ impl Zeta {
let debug_response_tx = if let Some(debug_tx) = &debug_tx {
let (response_tx, response_rx) = oneshot::channel();
if !request.referenced_declarations.is_empty() || !request.signatures.is_empty()
{
} else {
};
let local_prompt = build_prompt(&request)
.map(|(prompt, _)| prompt)
.map_err(|err| err.to_string());
debug_tx
.unbounded_send(PredictionDebugInfo {
.unbounded_send(ZetaDebugInfo::EditPredicted(ZetaEditPredictionDebugInfo {
request: request.clone(),
retrieval_time,
buffer: buffer.downgrade(),
local_prompt,
position,
response_rx,
})
}))
.ok();
Some(response_tx)
} else {
@@ -1047,9 +1080,22 @@ impl Zeta {
return;
};
let debug_tx = self.debug_tx.clone();
zeta_project
.refresh_context_task
.get_or_insert(cx.spawn(async move |this, cx| {
if let Some(debug_tx) = &debug_tx {
debug_tx
.unbounded_send(ZetaDebugInfo::ContextRetrievalStarted(
ZetaContextRetrievalDebugInfo {
project: project.clone(),
timestamp: Instant::now(),
},
))
.ok();
}
let related_excerpts = this
.update(cx, |this, cx| {
let Some(zeta_project) = this.projects.get(&project.entity_id()) else {
@@ -1066,6 +1112,7 @@ impl Zeta {
&project,
zeta_project.events.iter(),
options,
debug_tx,
cx,
)
})
@@ -1079,6 +1126,16 @@ impl Zeta {
};
zeta_project.context = Some(related_excerpts);
zeta_project.refresh_context_task.take();
if let Some(debug_tx) = &this.debug_tx {
debug_tx
.unbounded_send(ZetaDebugInfo::ContextRetrievalFinished(
ZetaContextRetrievalDebugInfo {
project,
timestamp: Instant::now(),
},
))
.ok();
}
})
.ok()
}));
+1
View File
@@ -12,6 +12,7 @@ workspace = true
path = "src/zeta2_tools.rs"
[dependencies]
anyhow.workspace = true
chrono.workspace = true
client.workspace = true
cloud_llm_client.workspace = true
@@ -0,0 +1,412 @@
use std::{
any::TypeId,
collections::VecDeque,
ops::Add,
sync::Arc,
time::{Duration, Instant},
};
use anyhow::Result;
use client::{Client, UserStore};
use editor::{Editor, PathKey};
use futures::StreamExt as _;
use gpui::{
Animation, AnimationExt, App, AppContext as _, Context, Entity, EventEmitter, FocusHandle,
Focusable, ParentElement as _, SharedString, Styled as _, Task, TextAlign, Window, actions,
pulsating_between,
};
use multi_buffer::MultiBuffer;
use project::Project;
use text::OffsetRangeExt;
use ui::{
ButtonCommon, Clickable, Color, Disableable, FluentBuilder as _, Icon, IconButton, IconName,
IconSize, InteractiveElement, IntoElement, ListItem, StyledTypography, div, h_flex, v_flex,
};
use workspace::{Item, ItemHandle as _};
use zeta2::{
SearchToolQuery, Zeta, ZetaContextRetrievalDebugInfo, ZetaDebugInfo, ZetaSearchQueryDebugInfo,
};
pub struct Zeta2ContextView {
empty_focus_handle: FocusHandle,
project: Entity<Project>,
zeta: Entity<Zeta>,
runs: VecDeque<RetrievalRun>,
current_ix: usize,
_update_task: Task<Result<()>>,
}
#[derive(Debug)]
pub struct RetrievalRun {
editor: Entity<Editor>,
search_queries: Vec<SearchToolQuery>,
started_at: Instant,
search_results_generated_at: Option<Instant>,
search_results_executed_at: Option<Instant>,
finished_at: Option<Instant>,
}
actions!(
dev,
[
/// Go to the previous context retrieval run
Zeta2ContextGoBack,
/// Go to the next context retrieval run
Zeta2ContextGoForward
]
);
impl Zeta2ContextView {
pub fn new(
project: Entity<Project>,
client: &Arc<Client>,
user_store: &Entity<UserStore>,
window: &mut gpui::Window,
cx: &mut Context<Self>,
) -> Self {
let zeta = Zeta::global(client, user_store, cx);
let mut debug_rx = zeta.update(cx, |zeta, _| zeta.debug_info());
let _update_task = cx.spawn_in(window, async move |this, cx| {
while let Some(event) = debug_rx.next().await {
this.update_in(cx, |this, window, cx| {
this.handle_zeta_event(event, window, cx)
})?;
}
Ok(())
});
Self {
empty_focus_handle: cx.focus_handle(),
project,
runs: VecDeque::new(),
current_ix: 0,
zeta,
_update_task,
}
}
fn handle_zeta_event(
&mut self,
event: ZetaDebugInfo,
window: &mut gpui::Window,
cx: &mut Context<Self>,
) {
match event {
ZetaDebugInfo::ContextRetrievalStarted(info) => {
if info.project == self.project {
self.handle_context_retrieval_started(info, window, cx);
}
}
ZetaDebugInfo::SearchQueriesGenerated(info) => {
if info.project == self.project {
self.handle_search_queries_generated(info, window, cx);
}
}
ZetaDebugInfo::SearchQueriesExecuted(info) => {
if info.project == self.project {
self.handle_search_queries_executed(info, window, cx);
}
}
ZetaDebugInfo::ContextRetrievalFinished(info) => {
if info.project == self.project {
self.handle_context_retrieval_finished(info, window, cx);
}
}
ZetaDebugInfo::EditPredicted(_) => {}
}
}
fn handle_context_retrieval_started(
&mut self,
info: ZetaContextRetrievalDebugInfo,
window: &mut Window,
cx: &mut Context<Self>,
) {
if self
.runs
.back()
.is_some_and(|run| run.search_results_executed_at.is_none())
{
self.runs.pop_back();
}
let multibuffer = cx.new(|_| MultiBuffer::new(language::Capability::ReadOnly));
let editor = cx
.new(|cx| Editor::for_multibuffer(multibuffer, Some(self.project.clone()), window, cx));
if self.runs.len() == 32 {
self.runs.pop_front();
}
self.runs.push_back(RetrievalRun {
editor,
search_queries: Vec::new(),
started_at: info.timestamp,
search_results_generated_at: None,
search_results_executed_at: None,
finished_at: None,
});
cx.notify();
}
fn handle_context_retrieval_finished(
&mut self,
info: ZetaContextRetrievalDebugInfo,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(run) = self.runs.back_mut() else {
return;
};
run.finished_at = Some(info.timestamp);
let multibuffer = run.editor.read(cx).buffer().clone();
multibuffer.update(cx, |multibuffer, cx| {
multibuffer.clear(cx);
let context = self.zeta.read(cx).context_for_project(&self.project);
let mut paths = Vec::new();
for (buffer, ranges) in context {
let path = PathKey::for_buffer(&buffer, cx);
let snapshot = buffer.read(cx).snapshot();
let ranges = ranges
.iter()
.map(|range| range.to_point(&snapshot))
.collect::<Vec<_>>();
paths.push((path, buffer, ranges));
}
for (path, buffer, ranges) in paths {
multibuffer.set_excerpts_for_path(path, buffer, ranges, 0, cx);
}
});
run.editor.update(cx, |editor, cx| {
editor.move_to_beginning(&Default::default(), window, cx);
});
cx.notify();
}
fn handle_search_queries_generated(
&mut self,
info: ZetaSearchQueryDebugInfo,
_window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(run) = self.runs.back_mut() else {
return;
};
run.search_results_generated_at = Some(info.timestamp);
run.search_queries = info.queries;
cx.notify();
}
fn handle_search_queries_executed(
&mut self,
info: ZetaContextRetrievalDebugInfo,
_window: &mut Window,
cx: &mut Context<Self>,
) {
if self.current_ix + 2 == self.runs.len() {
// Switch to latest when the queries are executed
self.current_ix += 1;
}
let Some(run) = self.runs.back_mut() else {
return;
};
run.search_results_executed_at = Some(info.timestamp);
cx.notify();
}
fn handle_go_back(
&mut self,
_: &Zeta2ContextGoBack,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.current_ix = self.current_ix.saturating_sub(1);
cx.focus_self(window);
cx.notify();
}
fn handle_go_forward(
&mut self,
_: &Zeta2ContextGoForward,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.current_ix = self
.current_ix
.add(1)
.min(self.runs.len().saturating_sub(1));
cx.focus_self(window);
cx.notify();
}
fn render_informational_footer(&self, cx: &mut Context<'_, Zeta2ContextView>) -> ui::Div {
let is_latest = self.runs.len() == self.current_ix + 1;
let run = &self.runs[self.current_ix];
h_flex()
.w_full()
.font_buffer(cx)
.text_xs()
.border_t_1()
.child(
v_flex()
.h_full()
.flex_1()
.children(run.search_queries.iter().enumerate().map(|(ix, query)| {
ListItem::new(ix)
.start_slot(
Icon::new(IconName::MagnifyingGlass)
.color(Color::Muted)
.size(IconSize::Small),
)
.child(query.regex.clone())
})),
)
.child(
v_flex()
.h_full()
.pr_2()
.text_align(TextAlign::Right)
.child(
h_flex()
.justify_end()
.child(
IconButton::new("go-back", IconName::ChevronLeft)
.disabled(self.current_ix == 0 || self.runs.len() < 2)
.tooltip(ui::Tooltip::for_action_title(
"Go to previous run",
&Zeta2ContextGoBack,
))
.on_click(cx.listener(|this, _, window, cx| {
this.handle_go_back(&Zeta2ContextGoBack, window, cx);
})),
)
.child(
div()
.child(format!("{}/{}", self.current_ix + 1, self.runs.len()))
.map(|this| {
if self.runs.back().is_some_and(|back| {
back.search_results_executed_at.is_none()
}) {
this.with_animation(
"pulsating-count",
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.4, 0.8)),
|label, delta| label.opacity(delta),
)
.into_any_element()
} else {
this.into_any_element()
}
}),
)
.child(
IconButton::new("go-forward", IconName::ChevronRight)
.disabled(self.current_ix + 1 == self.runs.len())
.tooltip(ui::Tooltip::for_action_title(
"Go to next run",
&Zeta2ContextGoBack,
))
.on_click(cx.listener(|this, _, window, cx| {
this.handle_go_forward(&Zeta2ContextGoForward, window, cx);
})),
),
)
.map(|mut div| {
let t0 = run.started_at;
let Some(t1) = run.search_results_generated_at else {
return div.child("Planning search...");
};
div = div.child(format!("Planned search: {:>5} ms", (t1 - t0).as_millis()));
let Some(t2) = run.search_results_executed_at else {
return div.child("Running search...");
};
div = div.child(format!("Ran search: {:>5} ms", (t2 - t1).as_millis()));
let Some(t3) = run.finished_at else {
if is_latest {
return div.child("Filtering results...");
} else {
return div.child("Canceled");
}
};
div.child(format!("Filtered results: {:>5} ms", (t3 - t2).as_millis()))
}),
)
}
}
impl Focusable for Zeta2ContextView {
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.runs
.get(self.current_ix)
.map(|run| run.editor.read(cx).focus_handle(cx))
.unwrap_or_else(|| self.empty_focus_handle.clone())
}
}
impl EventEmitter<()> for Zeta2ContextView {}
impl Item for Zeta2ContextView {
type Event = ();
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
"Edit Prediction Context".into()
}
fn buffer_kind(&self, _cx: &App) -> workspace::item::ItemBufferKind {
workspace::item::ItemBufferKind::Multibuffer
}
fn act_as_type<'a>(
&'a self,
type_id: TypeId,
self_handle: &'a Entity<Self>,
_: &'a App,
) -> Option<gpui::AnyView> {
if type_id == TypeId::of::<Self>() {
Some(self_handle.to_any())
} else if type_id == TypeId::of::<Editor>() {
Some(self.runs.get(self.current_ix)?.editor.to_any())
} else {
None
}
}
}
impl gpui::Render for Zeta2ContextView {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
v_flex()
.key_context("Zeta2Context")
.on_action(cx.listener(Self::handle_go_back))
.on_action(cx.listener(Self::handle_go_forward))
.size_full()
.map(|this| {
if self.runs.is_empty() {
this.child(
v_flex()
.size_full()
.justify_center()
.items_center()
.child("No retrieval runs yet"),
)
} else {
this.child(self.runs[self.current_ix].editor.clone())
.child(self.render_informational_footer(cx))
}
})
}
}
+34 -5
View File
@@ -1,3 +1,5 @@
mod zeta2_context_view;
use std::{cmp::Reverse, path::PathBuf, str::FromStr, sync::Arc, time::Duration};
use chrono::TimeDelta;
@@ -21,16 +23,19 @@ use ui_input::InputField;
use util::{ResultExt, paths::PathStyle, rel_path::RelPath};
use workspace::{Item, SplitDirection, Workspace};
use zeta2::{
ContextMode, DEFAULT_SYNTAX_CONTEXT_OPTIONS, LlmContextOptions, PredictionDebugInfo, Zeta,
Zeta2FeatureFlag, ZetaOptions,
ContextMode, DEFAULT_SYNTAX_CONTEXT_OPTIONS, LlmContextOptions, Zeta, Zeta2FeatureFlag,
ZetaDebugInfo, ZetaEditPredictionDebugInfo, ZetaOptions,
};
use edit_prediction_context::{EditPredictionContextOptions, EditPredictionExcerptOptions};
use zeta2_context_view::Zeta2ContextView;
actions!(
dev,
[
/// Opens the language server protocol logs viewer.
/// Opens the edit prediction context view.
OpenZeta2ContextView,
/// Opens the edit prediction inspector.
OpenZeta2Inspector,
/// Rate prediction as positive.
Zeta2RatePredictionPositive,
@@ -60,6 +65,27 @@ pub fn init(cx: &mut App) {
});
})
.detach();
cx.observe_new(move |workspace: &mut Workspace, _, _cx| {
workspace.register_action(move |workspace, _: &OpenZeta2ContextView, window, cx| {
let project = workspace.project();
workspace.split_item(
SplitDirection::Right,
Box::new(cx.new(|cx| {
Zeta2ContextView::new(
project.clone(),
workspace.client(),
workspace.user_store(),
window,
cx,
)
})),
window,
cx,
);
});
})
.detach();
}
// TODO show included diagnostics, and events
@@ -320,7 +346,7 @@ impl Zeta2Inspector {
fn update_last_prediction(
&mut self,
prediction: zeta2::PredictionDebugInfo,
prediction: zeta2::ZetaDebugInfo,
window: &mut Window,
cx: &mut Context<Self>,
) {
@@ -340,6 +366,9 @@ impl Zeta2Inspector {
let language_registry = self.project.read(cx).languages().clone();
async move |this, cx| {
let mut languages = HashMap::default();
let ZetaDebugInfo::EditPredicted(prediction) = prediction else {
return;
};
for ext in prediction
.request
.referenced_declarations
@@ -450,7 +479,7 @@ impl Zeta2Inspector {
editor
});
let PredictionDebugInfo {
let ZetaEditPredictionDebugInfo {
response_rx,
position,
buffer,