Adds a `dev: open edit prediction context` action that opens a new workspace pane that displays the excerpts and snippets that would be included in the edit prediction request. Release Notes: - N/A --------- Co-authored-by: Bennet <bennet@zed.dev>
458 lines
17 KiB
Rust
458 lines
17 KiB
Rust
use std::{
|
|
collections::hash_map::Entry,
|
|
ffi::OsStr,
|
|
path::{Path, PathBuf},
|
|
str::FromStr,
|
|
sync::Arc,
|
|
time::Duration,
|
|
};
|
|
|
|
use collections::HashMap;
|
|
use editor::{Editor, EditorEvent, EditorMode, ExcerptRange, MultiBuffer};
|
|
use gpui::{
|
|
Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity, actions,
|
|
prelude::*,
|
|
};
|
|
use language::{Buffer, DiskState};
|
|
use project::{Project, WorktreeId};
|
|
use text::ToPoint;
|
|
use ui::prelude::*;
|
|
use ui_input::SingleLineInput;
|
|
use workspace::{Item, SplitDirection, Workspace};
|
|
|
|
use edit_prediction_context::{
|
|
EditPredictionContext, EditPredictionExcerptOptions, SnippetStyle, SyntaxIndex,
|
|
};
|
|
|
|
actions!(
|
|
dev,
|
|
[
|
|
/// Opens the language server protocol logs viewer.
|
|
OpenEditPredictionContext
|
|
]
|
|
);
|
|
|
|
pub fn init(cx: &mut App) {
|
|
cx.observe_new(move |workspace: &mut Workspace, _, _cx| {
|
|
workspace.register_action(
|
|
move |workspace, _: &OpenEditPredictionContext, window, cx| {
|
|
let workspace_entity = cx.entity();
|
|
let project = workspace.project();
|
|
let active_editor = workspace.active_item_as::<Editor>(cx);
|
|
workspace.split_item(
|
|
SplitDirection::Right,
|
|
Box::new(cx.new(|cx| {
|
|
EditPredictionTools::new(
|
|
&workspace_entity,
|
|
&project,
|
|
active_editor,
|
|
window,
|
|
cx,
|
|
)
|
|
})),
|
|
window,
|
|
cx,
|
|
);
|
|
},
|
|
);
|
|
})
|
|
.detach();
|
|
}
|
|
|
|
pub struct EditPredictionTools {
|
|
focus_handle: FocusHandle,
|
|
project: Entity<Project>,
|
|
last_context: Option<ContextState>,
|
|
max_bytes_input: Entity<SingleLineInput>,
|
|
min_bytes_input: Entity<SingleLineInput>,
|
|
cursor_context_ratio_input: Entity<SingleLineInput>,
|
|
// TODO move to project or provider?
|
|
syntax_index: Entity<SyntaxIndex>,
|
|
last_editor: WeakEntity<Editor>,
|
|
_active_editor_subscription: Option<Subscription>,
|
|
_edit_prediction_context_task: Task<()>,
|
|
}
|
|
|
|
struct ContextState {
|
|
context_editor: Entity<Editor>,
|
|
retrieval_duration: Duration,
|
|
}
|
|
|
|
impl EditPredictionTools {
|
|
pub fn new(
|
|
workspace: &Entity<Workspace>,
|
|
project: &Entity<Project>,
|
|
active_editor: Option<Entity<Editor>>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> Self {
|
|
cx.subscribe_in(workspace, window, |this, workspace, event, window, cx| {
|
|
if let workspace::Event::ActiveItemChanged = event {
|
|
if let Some(editor) = workspace.read(cx).active_item_as::<Editor>(cx) {
|
|
this._active_editor_subscription = Some(cx.subscribe_in(
|
|
&editor,
|
|
window,
|
|
|this, editor, event, window, cx| {
|
|
if let EditorEvent::SelectionsChanged { .. } = event {
|
|
this.update_context(editor, window, cx);
|
|
}
|
|
},
|
|
));
|
|
this.update_context(&editor, window, cx);
|
|
} else {
|
|
this._active_editor_subscription = None;
|
|
}
|
|
}
|
|
})
|
|
.detach();
|
|
let syntax_index = cx.new(|cx| SyntaxIndex::new(project, cx));
|
|
|
|
let number_input = |label: &'static str,
|
|
value: &'static str,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>|
|
|
-> Entity<SingleLineInput> {
|
|
let input = cx.new(|cx| {
|
|
let input = SingleLineInput::new(window, cx, "")
|
|
.label(label)
|
|
.label_min_width(px(64.));
|
|
input.set_text(value, window, cx);
|
|
input
|
|
});
|
|
cx.subscribe_in(
|
|
&input.read(cx).editor().clone(),
|
|
window,
|
|
|this, _, event, window, cx| {
|
|
if let EditorEvent::BufferEdited = event
|
|
&& let Some(editor) = this.last_editor.upgrade()
|
|
{
|
|
this.update_context(&editor, window, cx);
|
|
}
|
|
},
|
|
)
|
|
.detach();
|
|
input
|
|
};
|
|
|
|
let mut this = Self {
|
|
focus_handle: cx.focus_handle(),
|
|
project: project.clone(),
|
|
last_context: None,
|
|
max_bytes_input: number_input("Max Bytes", "512", window, cx),
|
|
min_bytes_input: number_input("Min Bytes", "128", window, cx),
|
|
cursor_context_ratio_input: number_input("Cursor Context Ratio", "0.5", window, cx),
|
|
syntax_index,
|
|
last_editor: WeakEntity::new_invalid(),
|
|
_active_editor_subscription: None,
|
|
_edit_prediction_context_task: Task::ready(()),
|
|
};
|
|
|
|
if let Some(editor) = active_editor {
|
|
this.update_context(&editor, window, cx);
|
|
}
|
|
|
|
this
|
|
}
|
|
|
|
fn update_context(
|
|
&mut self,
|
|
editor: &Entity<Editor>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.last_editor = editor.downgrade();
|
|
|
|
let editor = editor.read(cx);
|
|
let buffer = editor.buffer().clone();
|
|
let cursor_position = editor.selections.newest_anchor().start;
|
|
|
|
let Some(buffer) = buffer.read(cx).buffer_for_anchor(cursor_position, cx) else {
|
|
self.last_context.take();
|
|
return;
|
|
};
|
|
let current_buffer_snapshot = buffer.read(cx).snapshot();
|
|
let cursor_position = cursor_position
|
|
.text_anchor
|
|
.to_point(¤t_buffer_snapshot);
|
|
|
|
let language = current_buffer_snapshot.language().cloned();
|
|
let Some(worktree_id) = self
|
|
.project
|
|
.read(cx)
|
|
.worktrees(cx)
|
|
.next()
|
|
.map(|worktree| worktree.read(cx).id())
|
|
else {
|
|
log::error!("Open a worktree to use edit prediction debug view");
|
|
self.last_context.take();
|
|
return;
|
|
};
|
|
|
|
self._edit_prediction_context_task = cx.spawn_in(window, {
|
|
let language_registry = self.project.read(cx).languages().clone();
|
|
async move |this, cx| {
|
|
cx.background_executor()
|
|
.timer(Duration::from_millis(50))
|
|
.await;
|
|
|
|
let Ok(task) = this.update(cx, |this, cx| {
|
|
fn number_input_value<T: FromStr + Default>(
|
|
input: &Entity<SingleLineInput>,
|
|
cx: &App,
|
|
) -> T {
|
|
input
|
|
.read(cx)
|
|
.editor()
|
|
.read(cx)
|
|
.text(cx)
|
|
.parse::<T>()
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
let options = EditPredictionExcerptOptions {
|
|
max_bytes: number_input_value(&this.max_bytes_input, cx),
|
|
min_bytes: number_input_value(&this.min_bytes_input, cx),
|
|
target_before_cursor_over_total_bytes: number_input_value(
|
|
&this.cursor_context_ratio_input,
|
|
cx,
|
|
),
|
|
// TODO Display and add to options
|
|
include_parent_signatures: false,
|
|
};
|
|
|
|
EditPredictionContext::gather(
|
|
cursor_position,
|
|
current_buffer_snapshot,
|
|
options,
|
|
this.syntax_index.clone(),
|
|
cx,
|
|
)
|
|
}) else {
|
|
this.update(cx, |this, _cx| {
|
|
this.last_context.take();
|
|
})
|
|
.ok();
|
|
return;
|
|
};
|
|
|
|
let Some(context) = task.await else {
|
|
// TODO: Display message
|
|
this.update(cx, |this, _cx| {
|
|
this.last_context.take();
|
|
})
|
|
.ok();
|
|
return;
|
|
};
|
|
|
|
let mut languages = HashMap::default();
|
|
for snippet in context.snippets.iter() {
|
|
let lang_id = snippet.declaration.identifier().language_id;
|
|
if let Entry::Vacant(entry) = languages.entry(lang_id) {
|
|
// Most snippets are gonna be the same language,
|
|
// so we think it's fine to do this sequentially for now
|
|
entry.insert(language_registry.language_for_id(lang_id).await.ok());
|
|
}
|
|
}
|
|
|
|
this.update_in(cx, |this, window, cx| {
|
|
let context_editor = cx.new(|cx| {
|
|
let multibuffer = cx.new(|cx| {
|
|
let mut multibuffer = MultiBuffer::new(language::Capability::ReadOnly);
|
|
let excerpt_file = Arc::new(ExcerptMetadataFile {
|
|
title: PathBuf::from("Cursor Excerpt").into(),
|
|
worktree_id,
|
|
});
|
|
|
|
let excerpt_buffer = cx.new(|cx| {
|
|
let mut buffer = Buffer::local(context.excerpt_text.body, cx);
|
|
buffer.set_language(language, cx);
|
|
buffer.file_updated(excerpt_file, cx);
|
|
buffer
|
|
});
|
|
|
|
multibuffer.push_excerpts(
|
|
excerpt_buffer,
|
|
[ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
|
|
cx,
|
|
);
|
|
|
|
for snippet in context.snippets {
|
|
let path = this
|
|
.project
|
|
.read(cx)
|
|
.path_for_entry(snippet.declaration.project_entry_id(), cx);
|
|
|
|
let snippet_file = Arc::new(ExcerptMetadataFile {
|
|
title: PathBuf::from(format!(
|
|
"{} (Score density: {})",
|
|
path.map(|p| p.path.to_string_lossy().to_string())
|
|
.unwrap_or_else(|| "".to_string()),
|
|
snippet.score_density(SnippetStyle::Declaration)
|
|
))
|
|
.into(),
|
|
worktree_id,
|
|
});
|
|
|
|
let excerpt_buffer = cx.new(|cx| {
|
|
let mut buffer =
|
|
Buffer::local(snippet.declaration.item_text().0, cx);
|
|
buffer.file_updated(snippet_file, cx);
|
|
if let Some(language) =
|
|
languages.get(&snippet.declaration.identifier().language_id)
|
|
{
|
|
buffer.set_language(language.clone(), cx);
|
|
}
|
|
buffer
|
|
});
|
|
|
|
multibuffer.push_excerpts(
|
|
excerpt_buffer,
|
|
[ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
|
|
cx,
|
|
);
|
|
}
|
|
|
|
multibuffer
|
|
});
|
|
|
|
Editor::new(EditorMode::full(), multibuffer, None, window, cx)
|
|
});
|
|
|
|
this.last_context = Some(ContextState {
|
|
context_editor,
|
|
retrieval_duration: context.retrieval_duration,
|
|
});
|
|
cx.notify();
|
|
})
|
|
.ok();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
impl Focusable for EditPredictionTools {
|
|
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
|
self.focus_handle.clone()
|
|
}
|
|
}
|
|
|
|
impl Item for EditPredictionTools {
|
|
type Event = ();
|
|
|
|
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
|
|
"Edit Prediction Context Debug View".into()
|
|
}
|
|
|
|
fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
|
|
Some(Icon::new(IconName::ZedPredict))
|
|
}
|
|
}
|
|
|
|
impl EventEmitter<()> for EditPredictionTools {}
|
|
|
|
impl Render for EditPredictionTools {
|
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
v_flex()
|
|
.size_full()
|
|
.bg(cx.theme().colors().editor_background)
|
|
.child(
|
|
h_flex()
|
|
.items_start()
|
|
.w_full()
|
|
.child(
|
|
v_flex()
|
|
.flex_1()
|
|
.p_4()
|
|
.gap_2()
|
|
.child(Headline::new("Excerpt Options").size(HeadlineSize::Small))
|
|
.child(
|
|
h_flex()
|
|
.gap_2()
|
|
.child(self.max_bytes_input.clone())
|
|
.child(self.min_bytes_input.clone())
|
|
.child(self.cursor_context_ratio_input.clone()),
|
|
),
|
|
)
|
|
.child(ui::Divider::vertical())
|
|
.when_some(self.last_context.as_ref(), |this, last_context| {
|
|
this.child(
|
|
v_flex()
|
|
.p_4()
|
|
.gap_2()
|
|
.min_w(px(160.))
|
|
.child(Headline::new("Stats").size(HeadlineSize::Small))
|
|
.child(
|
|
h_flex()
|
|
.gap_1()
|
|
.child(
|
|
Label::new("Time to retrieve")
|
|
.color(Color::Muted)
|
|
.size(LabelSize::Small),
|
|
)
|
|
.child(
|
|
Label::new(
|
|
if last_context.retrieval_duration.as_micros()
|
|
> 1000
|
|
{
|
|
format!(
|
|
"{} ms",
|
|
last_context.retrieval_duration.as_millis()
|
|
)
|
|
} else {
|
|
format!(
|
|
"{} µs",
|
|
last_context.retrieval_duration.as_micros()
|
|
)
|
|
},
|
|
)
|
|
.size(LabelSize::Small),
|
|
),
|
|
),
|
|
)
|
|
}),
|
|
)
|
|
.children(self.last_context.as_ref().map(|c| c.context_editor.clone()))
|
|
}
|
|
}
|
|
|
|
// Using same approach as commit view
|
|
|
|
struct ExcerptMetadataFile {
|
|
title: Arc<Path>,
|
|
worktree_id: WorktreeId,
|
|
}
|
|
|
|
impl language::File for ExcerptMetadataFile {
|
|
fn as_local(&self) -> Option<&dyn language::LocalFile> {
|
|
None
|
|
}
|
|
|
|
fn disk_state(&self) -> DiskState {
|
|
DiskState::New
|
|
}
|
|
|
|
fn path(&self) -> &Arc<Path> {
|
|
&self.title
|
|
}
|
|
|
|
fn full_path(&self, _: &App) -> PathBuf {
|
|
self.title.as_ref().into()
|
|
}
|
|
|
|
fn file_name<'a>(&'a self, _: &'a App) -> &'a OsStr {
|
|
self.title.file_name().unwrap()
|
|
}
|
|
|
|
fn worktree_id(&self, _: &App) -> WorktreeId {
|
|
self.worktree_id
|
|
}
|
|
|
|
fn to_proto(&self, _: &App) -> language::proto::File {
|
|
unimplemented!()
|
|
}
|
|
|
|
fn is_private(&self) -> bool {
|
|
false
|
|
}
|
|
}
|