edit_prediction_tools.rs

  1use std::{
  2    collections::hash_map::Entry,
  3    ffi::OsStr,
  4    path::{Path, PathBuf},
  5    str::FromStr,
  6    sync::Arc,
  7    time::Duration,
  8};
  9
 10use collections::HashMap;
 11use editor::{Editor, EditorEvent, EditorMode, ExcerptRange, MultiBuffer};
 12use gpui::{
 13    Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity, actions,
 14    prelude::*,
 15};
 16use language::{Buffer, DiskState};
 17use project::{Project, WorktreeId};
 18use text::ToPoint;
 19use ui::prelude::*;
 20use ui_input::SingleLineInput;
 21use workspace::{Item, SplitDirection, Workspace};
 22
 23use edit_prediction_context::{
 24    EditPredictionContext, EditPredictionExcerptOptions, SnippetStyle, SyntaxIndex,
 25};
 26
 27actions!(
 28    dev,
 29    [
 30        /// Opens the language server protocol logs viewer.
 31        OpenEditPredictionContext
 32    ]
 33);
 34
 35pub fn init(cx: &mut App) {
 36    cx.observe_new(move |workspace: &mut Workspace, _, _cx| {
 37        workspace.register_action(
 38            move |workspace, _: &OpenEditPredictionContext, window, cx| {
 39                let workspace_entity = cx.entity();
 40                let project = workspace.project();
 41                let active_editor = workspace.active_item_as::<Editor>(cx);
 42                workspace.split_item(
 43                    SplitDirection::Right,
 44                    Box::new(cx.new(|cx| {
 45                        EditPredictionTools::new(
 46                            &workspace_entity,
 47                            &project,
 48                            active_editor,
 49                            window,
 50                            cx,
 51                        )
 52                    })),
 53                    window,
 54                    cx,
 55                );
 56            },
 57        );
 58    })
 59    .detach();
 60}
 61
 62pub struct EditPredictionTools {
 63    focus_handle: FocusHandle,
 64    project: Entity<Project>,
 65    last_context: Option<ContextState>,
 66    max_bytes_input: Entity<SingleLineInput>,
 67    min_bytes_input: Entity<SingleLineInput>,
 68    cursor_context_ratio_input: Entity<SingleLineInput>,
 69    // TODO move to project or provider?
 70    syntax_index: Entity<SyntaxIndex>,
 71    last_editor: WeakEntity<Editor>,
 72    _active_editor_subscription: Option<Subscription>,
 73    _edit_prediction_context_task: Task<()>,
 74}
 75
 76struct ContextState {
 77    context_editor: Entity<Editor>,
 78    retrieval_duration: Duration,
 79}
 80
 81impl EditPredictionTools {
 82    pub fn new(
 83        workspace: &Entity<Workspace>,
 84        project: &Entity<Project>,
 85        active_editor: Option<Entity<Editor>>,
 86        window: &mut Window,
 87        cx: &mut Context<Self>,
 88    ) -> Self {
 89        cx.subscribe_in(workspace, window, |this, workspace, event, window, cx| {
 90            if let workspace::Event::ActiveItemChanged = event {
 91                if let Some(editor) = workspace.read(cx).active_item_as::<Editor>(cx) {
 92                    this._active_editor_subscription = Some(cx.subscribe_in(
 93                        &editor,
 94                        window,
 95                        |this, editor, event, window, cx| {
 96                            if let EditorEvent::SelectionsChanged { .. } = event {
 97                                this.update_context(editor, window, cx);
 98                            }
 99                        },
100                    ));
101                    this.update_context(&editor, window, cx);
102                } else {
103                    this._active_editor_subscription = None;
104                }
105            }
106        })
107        .detach();
108        let syntax_index = cx.new(|cx| SyntaxIndex::new(project, cx));
109
110        let number_input = |label: &'static str,
111                            value: &'static str,
112                            window: &mut Window,
113                            cx: &mut Context<Self>|
114         -> Entity<SingleLineInput> {
115            let input = cx.new(|cx| {
116                let input = SingleLineInput::new(window, cx, "")
117                    .label(label)
118                    .label_min_width(px(64.));
119                input.set_text(value, window, cx);
120                input
121            });
122            cx.subscribe_in(
123                &input.read(cx).editor().clone(),
124                window,
125                |this, _, event, window, cx| {
126                    if let EditorEvent::BufferEdited = event
127                        && let Some(editor) = this.last_editor.upgrade()
128                    {
129                        this.update_context(&editor, window, cx);
130                    }
131                },
132            )
133            .detach();
134            input
135        };
136
137        let mut this = Self {
138            focus_handle: cx.focus_handle(),
139            project: project.clone(),
140            last_context: None,
141            max_bytes_input: number_input("Max Bytes", "512", window, cx),
142            min_bytes_input: number_input("Min Bytes", "128", window, cx),
143            cursor_context_ratio_input: number_input("Cursor Context Ratio", "0.5", window, cx),
144            syntax_index,
145            last_editor: WeakEntity::new_invalid(),
146            _active_editor_subscription: None,
147            _edit_prediction_context_task: Task::ready(()),
148        };
149
150        if let Some(editor) = active_editor {
151            this.update_context(&editor, window, cx);
152        }
153
154        this
155    }
156
157    fn update_context(
158        &mut self,
159        editor: &Entity<Editor>,
160        window: &mut Window,
161        cx: &mut Context<Self>,
162    ) {
163        self.last_editor = editor.downgrade();
164
165        let editor = editor.read(cx);
166        let buffer = editor.buffer().clone();
167        let cursor_position = editor.selections.newest_anchor().start;
168
169        let Some(buffer) = buffer.read(cx).buffer_for_anchor(cursor_position, cx) else {
170            self.last_context.take();
171            return;
172        };
173        let current_buffer_snapshot = buffer.read(cx).snapshot();
174        let cursor_position = cursor_position
175            .text_anchor
176            .to_point(&current_buffer_snapshot);
177
178        let language = current_buffer_snapshot.language().cloned();
179        let Some(worktree_id) = self
180            .project
181            .read(cx)
182            .worktrees(cx)
183            .next()
184            .map(|worktree| worktree.read(cx).id())
185        else {
186            log::error!("Open a worktree to use edit prediction debug view");
187            self.last_context.take();
188            return;
189        };
190
191        self._edit_prediction_context_task = cx.spawn_in(window, {
192            let language_registry = self.project.read(cx).languages().clone();
193            async move |this, cx| {
194                cx.background_executor()
195                    .timer(Duration::from_millis(50))
196                    .await;
197
198                let Ok(task) = this.update(cx, |this, cx| {
199                    fn number_input_value<T: FromStr + Default>(
200                        input: &Entity<SingleLineInput>,
201                        cx: &App,
202                    ) -> T {
203                        input
204                            .read(cx)
205                            .editor()
206                            .read(cx)
207                            .text(cx)
208                            .parse::<T>()
209                            .unwrap_or_default()
210                    }
211
212                    let options = EditPredictionExcerptOptions {
213                        max_bytes: number_input_value(&this.max_bytes_input, cx),
214                        min_bytes: number_input_value(&this.min_bytes_input, cx),
215                        target_before_cursor_over_total_bytes: number_input_value(
216                            &this.cursor_context_ratio_input,
217                            cx,
218                        ),
219                        // TODO Display and add to options
220                        include_parent_signatures: false,
221                    };
222
223                    EditPredictionContext::gather(
224                        cursor_position,
225                        current_buffer_snapshot,
226                        options,
227                        this.syntax_index.clone(),
228                        cx,
229                    )
230                }) else {
231                    this.update(cx, |this, _cx| {
232                        this.last_context.take();
233                    })
234                    .ok();
235                    return;
236                };
237
238                let Some(context) = task.await else {
239                    // TODO: Display message
240                    this.update(cx, |this, _cx| {
241                        this.last_context.take();
242                    })
243                    .ok();
244                    return;
245                };
246
247                let mut languages = HashMap::default();
248                for snippet in context.snippets.iter() {
249                    let lang_id = snippet.declaration.identifier().language_id;
250                    if let Entry::Vacant(entry) = languages.entry(lang_id) {
251                        // Most snippets are gonna be the same language,
252                        // so we think it's fine to do this sequentially for now
253                        entry.insert(language_registry.language_for_id(lang_id).await.ok());
254                    }
255                }
256
257                this.update_in(cx, |this, window, cx| {
258                    let context_editor = cx.new(|cx| {
259                        let multibuffer = cx.new(|cx| {
260                            let mut multibuffer = MultiBuffer::new(language::Capability::ReadOnly);
261                            let excerpt_file = Arc::new(ExcerptMetadataFile {
262                                title: PathBuf::from("Cursor Excerpt").into(),
263                                worktree_id,
264                            });
265
266                            let excerpt_buffer = cx.new(|cx| {
267                                let mut buffer = Buffer::local(context.excerpt_text.body, cx);
268                                buffer.set_language(language, cx);
269                                buffer.file_updated(excerpt_file, cx);
270                                buffer
271                            });
272
273                            multibuffer.push_excerpts(
274                                excerpt_buffer,
275                                [ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
276                                cx,
277                            );
278
279                            for snippet in context.snippets {
280                                let path = this
281                                    .project
282                                    .read(cx)
283                                    .path_for_entry(snippet.declaration.project_entry_id(), cx);
284
285                                let snippet_file = Arc::new(ExcerptMetadataFile {
286                                    title: PathBuf::from(format!(
287                                        "{} (Score density: {})",
288                                        path.map(|p| p.path.to_string_lossy().to_string())
289                                            .unwrap_or_else(|| "".to_string()),
290                                        snippet.score_density(SnippetStyle::Declaration)
291                                    ))
292                                    .into(),
293                                    worktree_id,
294                                });
295
296                                let excerpt_buffer = cx.new(|cx| {
297                                    let mut buffer =
298                                        Buffer::local(snippet.declaration.item_text().0, cx);
299                                    buffer.file_updated(snippet_file, cx);
300                                    if let Some(language) =
301                                        languages.get(&snippet.declaration.identifier().language_id)
302                                    {
303                                        buffer.set_language(language.clone(), cx);
304                                    }
305                                    buffer
306                                });
307
308                                multibuffer.push_excerpts(
309                                    excerpt_buffer,
310                                    [ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
311                                    cx,
312                                );
313                            }
314
315                            multibuffer
316                        });
317
318                        Editor::new(EditorMode::full(), multibuffer, None, window, cx)
319                    });
320
321                    this.last_context = Some(ContextState {
322                        context_editor,
323                        retrieval_duration: context.retrieval_duration,
324                    });
325                    cx.notify();
326                })
327                .ok();
328            }
329        });
330    }
331}
332
333impl Focusable for EditPredictionTools {
334    fn focus_handle(&self, _cx: &App) -> FocusHandle {
335        self.focus_handle.clone()
336    }
337}
338
339impl Item for EditPredictionTools {
340    type Event = ();
341
342    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
343        "Edit Prediction Context Debug View".into()
344    }
345
346    fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
347        Some(Icon::new(IconName::ZedPredict))
348    }
349}
350
351impl EventEmitter<()> for EditPredictionTools {}
352
353impl Render for EditPredictionTools {
354    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
355        v_flex()
356            .size_full()
357            .bg(cx.theme().colors().editor_background)
358            .child(
359                h_flex()
360                    .items_start()
361                    .w_full()
362                    .child(
363                        v_flex()
364                            .flex_1()
365                            .p_4()
366                            .gap_2()
367                            .child(Headline::new("Excerpt Options").size(HeadlineSize::Small))
368                            .child(
369                                h_flex()
370                                    .gap_2()
371                                    .child(self.max_bytes_input.clone())
372                                    .child(self.min_bytes_input.clone())
373                                    .child(self.cursor_context_ratio_input.clone()),
374                            ),
375                    )
376                    .child(ui::Divider::vertical())
377                    .when_some(self.last_context.as_ref(), |this, last_context| {
378                        this.child(
379                            v_flex()
380                                .p_4()
381                                .gap_2()
382                                .min_w(px(160.))
383                                .child(Headline::new("Stats").size(HeadlineSize::Small))
384                                .child(
385                                    h_flex()
386                                        .gap_1()
387                                        .child(
388                                            Label::new("Time to retrieve")
389                                                .color(Color::Muted)
390                                                .size(LabelSize::Small),
391                                        )
392                                        .child(
393                                            Label::new(
394                                                if last_context.retrieval_duration.as_micros()
395                                                    > 1000
396                                                {
397                                                    format!(
398                                                        "{} ms",
399                                                        last_context.retrieval_duration.as_millis()
400                                                    )
401                                                } else {
402                                                    format!(
403                                                        "{} ยตs",
404                                                        last_context.retrieval_duration.as_micros()
405                                                    )
406                                                },
407                                            )
408                                            .size(LabelSize::Small),
409                                        ),
410                                ),
411                        )
412                    }),
413            )
414            .children(self.last_context.as_ref().map(|c| c.context_editor.clone()))
415    }
416}
417
418// Using same approach as commit view
419
420struct ExcerptMetadataFile {
421    title: Arc<Path>,
422    worktree_id: WorktreeId,
423}
424
425impl language::File for ExcerptMetadataFile {
426    fn as_local(&self) -> Option<&dyn language::LocalFile> {
427        None
428    }
429
430    fn disk_state(&self) -> DiskState {
431        DiskState::New
432    }
433
434    fn path(&self) -> &Arc<Path> {
435        &self.title
436    }
437
438    fn full_path(&self, _: &App) -> PathBuf {
439        self.title.as_ref().into()
440    }
441
442    fn file_name<'a>(&'a self, _: &'a App) -> &'a OsStr {
443        self.title.file_name().unwrap()
444    }
445
446    fn worktree_id(&self, _: &App) -> WorktreeId {
447        self.worktree_id
448    }
449
450    fn to_proto(&self, _: &App) -> language::proto::File {
451        unimplemented!()
452    }
453
454    fn is_private(&self) -> bool {
455        false
456    }
457}