edit prediction: Context debug view (#38435)

Agus Zubiaga and Bennet created

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>

Change summary

Cargo.lock                                                    |  28 
Cargo.toml                                                    |   2 
crates/edit_prediction_context/src/declaration.rs             |   6 
crates/edit_prediction_context/src/declaration_scoring.rs     |   6 
crates/edit_prediction_context/src/edit_prediction_context.rs |  20 
crates/edit_prediction_context/src/excerpt.rs                 |   2 
crates/edit_prediction_context/src/reference.rs               |   3 
crates/edit_prediction_context/src/syntax_index.rs            |   4 
crates/edit_prediction_tools/Cargo.toml                       |  41 
crates/edit_prediction_tools/LICENSE-GPL                      |   1 
crates/edit_prediction_tools/src/edit_prediction_tools.rs     | 457 +++++
crates/language/src/language_registry.rs                      |  18 
crates/ui_input/src/ui_input.rs                               |  12 
crates/zed/Cargo.toml                                         |   1 
crates/zed/src/main.rs                                        |   1 
15 files changed, 583 insertions(+), 19 deletions(-)

Detailed changes

Cargo.lock ๐Ÿ”—

@@ -5197,6 +5197,33 @@ dependencies = [
  "zlog",
 ]
 
+[[package]]
+name = "edit_prediction_tools"
+version = "0.1.0"
+dependencies = [
+ "clap",
+ "collections",
+ "edit_prediction_context",
+ "editor",
+ "futures 0.3.31",
+ "gpui",
+ "indoc",
+ "language",
+ "log",
+ "pretty_assertions",
+ "project",
+ "serde",
+ "serde_json",
+ "settings",
+ "text",
+ "ui",
+ "ui_input",
+ "util",
+ "workspace",
+ "workspace-hack",
+ "zlog",
+]
+
 [[package]]
 name = "editor"
 version = "0.1.0"
@@ -21217,6 +21244,7 @@ dependencies = [
  "debugger_ui",
  "diagnostics",
  "edit_prediction_button",
+ "edit_prediction_tools",
  "editor",
  "env_logger 0.11.8",
  "extension",

Cargo.toml ๐Ÿ”—

@@ -58,6 +58,7 @@ members = [
     "crates/edit_prediction",
     "crates/edit_prediction_button",
     "crates/edit_prediction_context",
+    "crates/edit_prediction_tools",
     "crates/editor",
     "crates/eval",
     "crates/explorer_command_injector",
@@ -314,6 +315,7 @@ image_viewer = { path = "crates/image_viewer" }
 edit_prediction = { path = "crates/edit_prediction" }
 edit_prediction_button = { path = "crates/edit_prediction_button" }
 edit_prediction_context = { path = "crates/edit_prediction_context" }
+edit_prediction_tools = { path = "crates/edit_prediction_tools" }
 inspector_ui = { path = "crates/inspector_ui" }
 install_cli = { path = "crates/install_cli" }
 jj = { path = "crates/jj" }

crates/edit_prediction_context/src/declaration.rs ๐Ÿ”—

@@ -41,14 +41,14 @@ impl Declaration {
         }
     }
 
-    pub fn project_entry_id(&self) -> Option<ProjectEntryId> {
+    pub fn project_entry_id(&self) -> ProjectEntryId {
         match self {
             Declaration::File {
                 project_entry_id, ..
-            } => Some(*project_entry_id),
+            } => *project_entry_id,
             Declaration::Buffer {
                 project_entry_id, ..
-            } => Some(*project_entry_id),
+            } => *project_entry_id,
         }
     }
 

crates/edit_prediction_context/src/declaration_scoring.rs ๐Ÿ”—

@@ -119,15 +119,13 @@ pub fn scored_snippets(
                                 )
                             })
                         } else {
-                            // TODO should we prefer the current file instead?
-                            Some((false, 0, declaration))
+                            Some((false, u32::MAX, declaration))
                         }
                     }
                     Declaration::File { .. } => {
-                        // TODO should we prefer the current file instead?
                         // We can assume that a file declaration is in a different file,
                         // because the current one must be open
-                        Some((false, 0, declaration))
+                        Some((false, u32::MAX, declaration))
                     }
                 })
                 .sorted_by_key(|&(_, distance, _)| distance)

crates/edit_prediction_context/src/edit_prediction_context.rs ๐Ÿ”—

@@ -6,8 +6,12 @@ mod reference;
 mod syntax_index;
 mod text_similarity;
 
+use std::time::Instant;
+
 pub use declaration::{BufferDeclaration, Declaration, FileDeclaration, Identifier};
+pub use declaration_scoring::SnippetStyle;
 pub use excerpt::{EditPredictionExcerpt, EditPredictionExcerptOptions, EditPredictionExcerptText};
+
 use gpui::{App, AppContext as _, Entity, Task};
 use language::BufferSnapshot;
 pub use reference::references_in_excerpt;
@@ -16,10 +20,12 @@ use text::{Point, ToOffset as _};
 
 use crate::declaration_scoring::{ScoredSnippet, scored_snippets};
 
+#[derive(Debug)]
 pub struct EditPredictionContext {
     pub excerpt: EditPredictionExcerpt,
     pub excerpt_text: EditPredictionExcerptText,
     pub snippets: Vec<ScoredSnippet>,
+    pub retrieval_duration: std::time::Duration,
 }
 
 impl EditPredictionContext {
@@ -29,14 +35,14 @@ impl EditPredictionContext {
         excerpt_options: EditPredictionExcerptOptions,
         syntax_index: Entity<SyntaxIndex>,
         cx: &mut App,
-    ) -> Task<Self> {
+    ) -> Task<Option<Self>> {
+        let start = Instant::now();
         let index_state = syntax_index.read_with(cx, |index, _cx| index.state().clone());
         cx.background_spawn(async move {
             let index_state = index_state.lock().await;
 
             let excerpt =
-                EditPredictionExcerpt::select_from_buffer(cursor_point, &buffer, &excerpt_options)
-                    .unwrap();
+                EditPredictionExcerpt::select_from_buffer(cursor_point, &buffer, &excerpt_options)?;
             let excerpt_text = excerpt.text(&buffer);
             let references = references_in_excerpt(&excerpt, &excerpt_text, &buffer);
             let cursor_offset = cursor_point.to_offset(&buffer);
@@ -50,11 +56,12 @@ impl EditPredictionContext {
                 &buffer,
             );
 
-            Self {
+            Some(Self {
                 excerpt,
                 excerpt_text,
                 snippets,
-            }
+                retrieval_duration: start.elapsed(),
+            })
         })
     }
 }
@@ -107,7 +114,8 @@ mod tests {
                     cx,
                 )
             })
-            .await;
+            .await
+            .unwrap();
 
         assert_eq!(context.snippets.len(), 1);
         assert_eq!(context.snippets[0].identifier.name.as_ref(), "process_data");

crates/edit_prediction_context/src/reference.rs ๐Ÿ”—

@@ -89,7 +89,8 @@ pub fn identifiers_in_range(
                     }
 
                     let identifier_text =
-                        &range_text[node_range.start - range.start..node_range.end - range.start];
+                        // TODO we changed this to saturating_sub for now, but we should fix the actually issue
+                        &range_text[node_range.start.saturating_sub(range.start)..node_range.end.saturating_sub(range.start)];
                     references.push(Reference {
                         identifier: Identifier {
                             name: identifier_text.into(),

crates/edit_prediction_context/src/syntax_index.rs ๐Ÿ”—

@@ -9,7 +9,7 @@ use project::worktree_store::{WorktreeStore, WorktreeStoreEvent};
 use project::{PathChange, Project, ProjectEntryId, ProjectPath};
 use slotmap::SlotMap;
 use text::BufferId;
-use util::{ResultExt as _, debug_panic, some_or_debug_panic};
+use util::{debug_panic, some_or_debug_panic};
 
 use crate::declaration::{
     BufferDeclaration, Declaration, DeclarationId, FileDeclaration, Identifier,
@@ -332,7 +332,7 @@ impl SyntaxIndex {
                 let language = language_registry
                     .language_for_file_path(&project_path.path)
                     .await
-                    .log_err();
+                    .ok();
 
                 let buffer = cx.new(|cx| {
                     let mut buffer = Buffer::local(loaded_file.text, cx);

crates/edit_prediction_tools/Cargo.toml ๐Ÿ”—

@@ -0,0 +1,41 @@
+[package]
+name = "edit_prediction_tools"
+version = "0.1.0"
+edition.workspace = true
+publish.workspace = true
+license = "GPL-3.0-or-later"
+
+[lints]
+workspace = true
+
+[lib]
+path = "src/edit_prediction_tools.rs"
+
+[dependencies]
+edit_prediction_context.workspace = true
+collections.workspace = true
+editor.workspace = true
+gpui.workspace = true
+language.workspace = true
+log.workspace = true
+project.workspace = true
+serde.workspace = true
+text.workspace = true
+ui.workspace = true
+ui_input.workspace = true
+workspace-hack.workspace = true
+workspace.workspace = true
+
+[dev-dependencies]
+clap.workspace = true
+futures.workspace = true
+gpui = { workspace = true, features = ["test-support"] }
+indoc.workspace = true
+language = { workspace = true, features = ["test-support"] }
+pretty_assertions.workspace = true
+project = {workspace= true, features = ["test-support"]}
+serde_json.workspace = true
+settings = {workspace= true, features = ["test-support"]}
+text = { workspace = true, features = ["test-support"] }
+util = { workspace = true, features = ["test-support"] }
+zlog.workspace = true

crates/edit_prediction_tools/src/edit_prediction_tools.rs ๐Ÿ”—

@@ -0,0 +1,457 @@
+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(&current_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
+    }
+}

crates/language/src/language_registry.rs ๐Ÿ”—

@@ -646,6 +646,24 @@ impl LanguageRegistry {
         async move { rx.await? }
     }
 
+    pub async fn language_for_id(self: &Arc<Self>, id: LanguageId) -> Result<Arc<Language>> {
+        let available_language = {
+            let state = self.state.read();
+
+            let Some(available_language) = state
+                .available_languages
+                .iter()
+                .find(|lang| lang.id == id)
+                .cloned()
+            else {
+                anyhow::bail!(LanguageNotFound);
+            };
+            available_language
+        };
+
+        self.load_language(&available_language).await?
+    }
+
     pub fn language_name_for_extension(self: &Arc<Self>, extension: &str) -> Option<LanguageName> {
         self.state.try_read().and_then(|state| {
             state

crates/ui_input/src/ui_input.rs ๐Ÿ”—

@@ -7,7 +7,7 @@
 
 use component::{example_group, single_example};
 use editor::{Editor, EditorElement, EditorStyle};
-use gpui::{App, Entity, FocusHandle, Focusable, FontStyle, Hsla, TextStyle};
+use gpui::{App, Entity, FocusHandle, Focusable, FontStyle, Hsla, Length, TextStyle};
 use settings::Settings;
 use std::sync::Arc;
 use theme::ThemeSettings;
@@ -42,6 +42,8 @@ pub struct SingleLineInput {
     start_icon: Option<IconName>,
     /// Whether the text field is disabled.
     disabled: bool,
+    /// The minimum width of for the input
+    min_width: Length,
 }
 
 impl Focusable for SingleLineInput {
@@ -67,6 +69,7 @@ impl SingleLineInput {
             editor,
             start_icon: None,
             disabled: false,
+            min_width: px(192.).into(),
         }
     }
 
@@ -85,6 +88,11 @@ impl SingleLineInput {
         self
     }
 
+    pub fn label_min_width(mut self, width: impl Into<Length>) -> Self {
+        self.min_width = width.into();
+        self
+    }
+
     pub fn set_disabled(&mut self, disabled: bool, cx: &mut Context<Self>) {
         self.disabled = disabled;
         self.editor
@@ -167,7 +175,7 @@ impl Render for SingleLineInput {
             })
             .child(
                 h_flex()
-                    .min_w_48()
+                    .min_w(self.min_width)
                     .min_h_8()
                     .w_full()
                     .px_2()

crates/zed/Cargo.toml ๐Ÿ”—

@@ -52,6 +52,7 @@ debugger_tools.workspace = true
 debugger_ui.workspace = true
 diagnostics.workspace = true
 editor.workspace = true
+edit_prediction_tools.workspace = true
 env_logger.workspace = true
 extension.workspace = true
 extension_host.workspace = true

crates/zed/src/main.rs ๐Ÿ”—

@@ -549,6 +549,7 @@ pub fn main() {
         language_models::init(app_state.user_store.clone(), app_state.client.clone(), cx);
         agent_settings::init(cx);
         acp_tools::init(cx);
+        edit_prediction_tools::init(cx);
         web_search::init(cx);
         web_search_providers::init(app_state.client.clone(), cx);
         snippet_provider::init(cx);