Merge branch 'main' into mic-denoising

David Kleingeld created

Change summary

Cargo.lock                                                    |  28 
Cargo.toml                                                    |   2 
crates/collab/src/tests/integration_tests.rs                  |  27 
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/src/edit_prediction_tools.rs     | 457 +++++
crates/fs/src/fake_git_repo.rs                                |  15 
crates/git/src/git.rs                                         |  13 
crates/git/src/repository.rs                                  |  21 
crates/git_ui/src/git_ui.rs                                   | 133 +
crates/language/src/language_registry.rs                      |  18 
crates/project/src/git_store.rs                               |  50 
crates/project/src/project_settings.rs                        |   1 
crates/proto/proto/git.proto                                  |   7 
crates/proto/proto/zed.proto                                  |   4 
crates/proto/src/proto.rs                                     |   3 
crates/rope/src/rope.rs                                       |   5 
crates/ui/src/components/badge.rs                             |   2 
crates/ui/src/components/scrollbar.rs                         |   2 
crates/ui_input/src/ui_input.rs                               |  12 
crates/zed/Cargo.toml                                         |   1 
crates/zed/src/main.rs                                        |   1 
27 files changed, 847 insertions(+), 37 deletions(-)

Detailed changes

Cargo.lock ๐Ÿ”—

@@ -5198,6 +5198,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/collab/src/tests/integration_tests.rs ๐Ÿ”—

@@ -3505,10 +3505,14 @@ async fn test_local_settings(
         assert_eq!(
             store
                 .local_settings(worktree_b.read(cx).id())
+                .map(|(path, content)| (
+                    path,
+                    content.all_languages.defaults.tab_size.map(Into::into)
+                ))
                 .collect::<Vec<_>>(),
             &[
-                (Path::new("").into(), r#"{"tab_size":2}"#.to_string()),
-                (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
+                (Path::new("").into(), Some(2)),
+                (Path::new("a").into(), Some(8)),
             ]
         )
     });
@@ -3524,10 +3528,14 @@ async fn test_local_settings(
         assert_eq!(
             store
                 .local_settings(worktree_b.read(cx).id())
+                .map(|(path, content)| (
+                    path,
+                    content.all_languages.defaults.tab_size.map(Into::into)
+                ))
                 .collect::<Vec<_>>(),
             &[
-                (Path::new("").into(), r#"{}"#.to_string()),
-                (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
+                (Path::new("").into(), None),
+                (Path::new("a").into(), Some(8)),
             ]
         )
     });
@@ -3553,10 +3561,14 @@ async fn test_local_settings(
         assert_eq!(
             store
                 .local_settings(worktree_b.read(cx).id())
+                .map(|(path, content)| (
+                    path,
+                    content.all_languages.defaults.tab_size.map(Into::into)
+                ))
                 .collect::<Vec<_>>(),
             &[
-                (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
-                (Path::new("b").into(), r#"{"tab_size":4}"#.to_string()),
+                (Path::new("a").into(), Some(8)),
+                (Path::new("b").into(), Some(4)),
             ]
         )
     });
@@ -3585,8 +3597,9 @@ async fn test_local_settings(
         assert_eq!(
             store
                 .local_settings(worktree_b.read(cx).id())
+                .map(|(path, content)| (path, content.all_languages.defaults.hard_tabs))
                 .collect::<Vec<_>>(),
-            &[(Path::new("a").into(), r#"{"hard_tabs":true}"#.to_string()),]
+            &[(Path::new("a").into(), Some(true))],
         )
     });
 }

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/fs/src/fake_git_repo.rs ๐Ÿ”—

@@ -1,5 +1,5 @@
 use crate::{FakeFs, FakeFsEntry, Fs};
-use anyhow::{Context as _, Result};
+use anyhow::{Context as _, Result, bail};
 use collections::{HashMap, HashSet};
 use futures::future::{self, BoxFuture, join_all};
 use git::{
@@ -354,6 +354,19 @@ impl GitRepository for FakeGitRepository {
         })
     }
 
+    fn rename_branch(&self, branch: String, new_name: String) -> BoxFuture<'_, Result<()>> {
+        self.with_state_async(true, move |state| {
+            if !state.branches.remove(&branch) {
+                bail!("no such branch: {branch}");
+            }
+            state.branches.insert(new_name.clone());
+            if state.current_branch_name == Some(branch) {
+                state.current_branch_name = Some(new_name);
+            }
+            Ok(())
+        })
+    }
+
     fn blame(&self, path: RepoPath, _content: Rope) -> BoxFuture<'_, Result<git::blame::Blame>> {
         self.with_state_async(false, move |state| {
             state

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

@@ -11,6 +11,7 @@ pub use crate::remote::*;
 use anyhow::{Context as _, Result};
 pub use git2 as libgit;
 use gpui::{Action, actions};
+pub use repository::RemoteCommandOutput;
 pub use repository::WORK_DIRECTORY_REPO_PATH;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
@@ -101,6 +102,18 @@ actions!(
     ]
 );
 
+/// Renames a git branch.
+#[derive(Clone, Debug, Default, PartialEq, Deserialize, JsonSchema, Action)]
+#[action(namespace = git)]
+#[serde(deny_unknown_fields)]
+pub struct RenameBranch {
+    /// The branch to rename.
+    ///
+    /// Default: the current branch.
+    #[serde(default)]
+    pub branch: Option<String>,
+}
+
 /// Restores a file to its last committed state, discarding local changes.
 #[derive(Clone, Debug, Default, PartialEq, Deserialize, JsonSchema, Action)]
 #[action(namespace = git, deprecated_aliases = ["editor::RevertFile"])]

crates/git/src/repository.rs ๐Ÿ”—

@@ -346,6 +346,7 @@ pub trait GitRepository: Send + Sync {
 
     fn change_branch(&self, name: String) -> BoxFuture<'_, Result<()>>;
     fn create_branch(&self, name: String) -> BoxFuture<'_, Result<()>>;
+    fn rename_branch(&self, branch: String, new_name: String) -> BoxFuture<'_, Result<()>>;
 
     fn reset(
         &self,
@@ -1095,11 +1096,11 @@ impl GitRepository for RealGitRepository {
                 let (_, branch_name) = name.split_once("/").context("Unexpected branch format")?;
                 let revision = revision.get();
                 let branch_commit = revision.peel_to_commit()?;
-                let mut branch = repo.branch(branch_name, &branch_commit, false)?;
+                let mut branch = repo.branch(&branch_name, &branch_commit, false)?;
                 branch.set_upstream(Some(&name))?;
                 branch
             } else {
-                anyhow::bail!("Branch not found");
+                anyhow::bail!("Branch '{}' not found", name);
             };
 
             Ok(branch
@@ -1115,7 +1116,6 @@ impl GitRepository for RealGitRepository {
                 GitBinary::new(git_binary_path, working_directory?, executor)
                     .run(&["checkout", &branch])
                     .await?;
-
                 anyhow::Ok(())
             })
             .boxed()
@@ -1133,6 +1133,21 @@ impl GitRepository for RealGitRepository {
             .boxed()
     }
 
+    fn rename_branch(&self, branch: String, new_name: String) -> BoxFuture<'_, Result<()>> {
+        let git_binary_path = self.git_binary_path.clone();
+        let working_directory = self.working_directory();
+        let executor = self.executor.clone();
+
+        self.executor
+            .spawn(async move {
+                GitBinary::new(git_binary_path, working_directory?, executor)
+                    .run(&["branch", "-m", &branch, &new_name])
+                    .await?;
+                anyhow::Ok(())
+            })
+            .boxed()
+    }
+
     fn blame(&self, path: RepoPath, content: Rope) -> BoxFuture<'_, Result<crate::blame::Blame>> {
         let working_directory = self.working_directory();
         let git_binary_path = self.git_binary_path.clone();

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

@@ -4,20 +4,28 @@ use ::settings::Settings;
 use command_palette_hooks::CommandPaletteFilter;
 use commit_modal::CommitModal;
 use editor::{Editor, actions::DiffClipboardWithSelectionData};
+use ui::{
+    Headline, HeadlineSize, Icon, IconName, IconSize, IntoElement, ParentElement, Render, Styled,
+    StyledExt, div, h_flex, rems, v_flex,
+};
+
 mod blame_ui;
+
 use git::{
     repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus},
     status::{FileStatus, StatusCode, UnmergedStatus, UnmergedStatusCode},
 };
 use git_panel_settings::GitPanelSettings;
 use gpui::{
-    Action, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Window,
-    actions,
+    Action, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, SharedString,
+    Window, actions,
 };
+use menu::{Cancel, Confirm};
 use onboarding::GitOnboardingModal;
+use project::git_store::Repository;
 use project_diff::ProjectDiff;
 use ui::prelude::*;
-use workspace::{ModalView, Workspace};
+use workspace::{ModalView, Workspace, notifications::DetachAndPromptErr};
 use zed_actions;
 
 use crate::{git_panel::GitPanel, text_diff_view::TextDiffView};
@@ -202,6 +210,9 @@ pub fn init(cx: &mut App) {
         workspace.register_action(|workspace, _: &git::OpenModifiedFiles, window, cx| {
             open_modified_files(workspace, window, cx);
         });
+        workspace.register_action(|workspace, _: &git::RenameBranch, window, cx| {
+            rename_current_branch(workspace, window, cx);
+        });
         workspace.register_action(
             |workspace, action: &DiffClipboardWithSelectionData, window, cx| {
                 if let Some(task) = TextDiffView::open(action, workspace, window, cx) {
@@ -245,6 +256,122 @@ pub fn git_status_icon(status: FileStatus) -> impl IntoElement {
     GitStatusIcon::new(status)
 }
 
+struct RenameBranchModal {
+    current_branch: SharedString,
+    editor: Entity<Editor>,
+    repo: Entity<Repository>,
+}
+
+impl RenameBranchModal {
+    fn new(
+        current_branch: String,
+        repo: Entity<Repository>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Self {
+        let editor = cx.new(|cx| {
+            let mut editor = Editor::single_line(window, cx);
+            editor.set_text(current_branch.clone(), window, cx);
+            editor
+        });
+        Self {
+            current_branch: current_branch.into(),
+            editor,
+            repo,
+        }
+    }
+
+    fn cancel(&mut self, _: &Cancel, _window: &mut Window, cx: &mut Context<Self>) {
+        cx.emit(DismissEvent);
+    }
+
+    fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
+        let new_name = self.editor.read(cx).text(cx);
+        if new_name.is_empty() || new_name == self.current_branch.as_ref() {
+            cx.emit(DismissEvent);
+            return;
+        }
+
+        let repo = self.repo.clone();
+        let current_branch = self.current_branch.to_string();
+        cx.spawn(async move |_, cx| {
+            match repo
+                .update(cx, |repo, _| {
+                    repo.rename_branch(current_branch, new_name.clone())
+                })?
+                .await
+            {
+                Ok(Ok(_)) => Ok(()),
+                Ok(Err(error)) => Err(error),
+                Err(_) => Err(anyhow::anyhow!("Operation was canceled")),
+            }
+        })
+        .detach_and_prompt_err("Failed to rename branch", window, cx, |_, _, _| None);
+        cx.emit(DismissEvent);
+    }
+}
+
+impl EventEmitter<DismissEvent> for RenameBranchModal {}
+impl ModalView for RenameBranchModal {}
+impl Focusable for RenameBranchModal {
+    fn focus_handle(&self, cx: &App) -> FocusHandle {
+        self.editor.focus_handle(cx)
+    }
+}
+
+impl Render for RenameBranchModal {
+    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        v_flex()
+            .key_context("RenameBranchModal")
+            .on_action(cx.listener(Self::cancel))
+            .on_action(cx.listener(Self::confirm))
+            .elevation_2(cx)
+            .w(rems(34.))
+            .child(
+                h_flex()
+                    .px_3()
+                    .pt_2()
+                    .pb_1()
+                    .w_full()
+                    .gap_1p5()
+                    .child(Icon::new(IconName::GitBranch).size(IconSize::XSmall))
+                    .child(
+                        Headline::new(format!("Rename Branch ({})", self.current_branch))
+                            .size(HeadlineSize::XSmall),
+                    ),
+            )
+            .child(div().px_3().pb_3().w_full().child(self.editor.clone()))
+    }
+}
+
+fn rename_current_branch(
+    workspace: &mut Workspace,
+    window: &mut Window,
+    cx: &mut Context<Workspace>,
+) {
+    let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
+        return;
+    };
+    let current_branch: Option<String> = panel.update(cx, |panel, cx| {
+        let repo = panel.active_repository.as_ref()?;
+        let repo = repo.read(cx);
+        repo.branch.as_ref().map(|branch| branch.name().to_string())
+    });
+
+    let Some(current_branch_name) = current_branch else {
+        return;
+    };
+
+    let repo = panel.read(cx).active_repository.clone();
+    let Some(repo) = repo else {
+        return;
+    };
+
+    workspace.toggle_modal(window, cx, |window, cx| {
+        RenameBranchModal::new(current_branch_name, repo, window, cx)
+    });
+}
+
 fn render_remote_button(
     id: impl Into<SharedString>,
     branch: &Branch,

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/project/src/git_store.rs ๐Ÿ”—

@@ -398,6 +398,7 @@ impl GitStore {
         client.add_entity_request_handler(Self::handle_get_default_branch);
         client.add_entity_request_handler(Self::handle_change_branch);
         client.add_entity_request_handler(Self::handle_create_branch);
+        client.add_entity_request_handler(Self::handle_rename_branch);
         client.add_entity_request_handler(Self::handle_git_init);
         client.add_entity_request_handler(Self::handle_push);
         client.add_entity_request_handler(Self::handle_pull);
@@ -1944,6 +1945,25 @@ impl GitStore {
         Ok(proto::Ack {})
     }
 
+    async fn handle_rename_branch(
+        this: Entity<Self>,
+        envelope: TypedEnvelope<proto::GitRenameBranch>,
+        mut cx: AsyncApp,
+    ) -> Result<proto::Ack> {
+        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
+        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
+        let branch = envelope.payload.branch;
+        let new_name = envelope.payload.new_name;
+
+        repository_handle
+            .update(&mut cx, |repository_handle, _| {
+                repository_handle.rename_branch(branch, new_name)
+            })?
+            .await??;
+
+        Ok(proto::Ack {})
+    }
+
     async fn handle_show(
         this: Entity<Self>,
         envelope: TypedEnvelope<proto::GitShow>,
@@ -4331,6 +4351,36 @@ impl Repository {
         )
     }
 
+    pub fn rename_branch(
+        &mut self,
+        branch: String,
+        new_name: String,
+    ) -> oneshot::Receiver<Result<()>> {
+        let id = self.id;
+        self.send_job(
+            Some(format!("git branch -m {branch} {new_name}").into()),
+            move |repo, _cx| async move {
+                match repo {
+                    RepositoryState::Local { backend, .. } => {
+                        backend.rename_branch(branch, new_name).await
+                    }
+                    RepositoryState::Remote { project_id, client } => {
+                        client
+                            .request(proto::GitRenameBranch {
+                                project_id: project_id.0,
+                                repository_id: id.to_proto(),
+                                branch,
+                                new_name,
+                            })
+                            .await?;
+
+                        Ok(())
+                    }
+                }
+            },
+        )
+    }
+
     pub fn check_for_pushed_commits(&mut self) -> oneshot::Receiver<Result<Vec<SharedString>>> {
         let id = self.id;
         self.send_job(None, move |repo, _cx| async move {

crates/project/src/project_settings.rs ๐Ÿ”—

@@ -824,6 +824,7 @@ impl SettingsObserver {
         for worktree in self.worktree_store.read(cx).worktrees() {
             let worktree_id = worktree.read(cx).id().to_proto();
             for (path, content) in store.local_settings(worktree.read(cx).id()) {
+                let content = serde_json::to_string(&content).unwrap();
                 downstream_client
                     .send(proto::UpdateWorktreeSettings {
                         project_id,

crates/proto/proto/git.proto ๐Ÿ”—

@@ -183,6 +183,13 @@ message GitChangeBranch {
     string branch_name = 4;
 }
 
+message GitRenameBranch {
+    uint64 project_id = 1;
+    uint64 repository_id = 2;
+    string branch = 3;
+    string new_name = 4;
+}
+
 message GitDiff {
     uint64 project_id = 1;
     reserved 2;

crates/proto/proto/zed.proto ๐Ÿ”—

@@ -414,7 +414,9 @@ message Envelope {
         NewExternalAgentVersionAvailable new_external_agent_version_available = 377;
 
         StashDrop stash_drop = 378;
-        StashApply stash_apply = 379; // current max
+        StashApply stash_apply = 379;
+
+        GitRenameBranch git_rename_branch = 380; // current max
     }
 
     reserved 87 to 88;

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

@@ -300,6 +300,7 @@ messages!(
     (AskPassResponse, Background),
     (GitCreateBranch, Background),
     (GitChangeBranch, Background),
+    (GitRenameBranch, Background),
     (CheckForPushedCommits, Background),
     (CheckForPushedCommitsResponse, Background),
     (GitDiff, Background),
@@ -483,6 +484,7 @@ request_messages!(
     (AskPassRequest, AskPassResponse),
     (GitCreateBranch, Ack),
     (GitChangeBranch, Ack),
+    (GitRenameBranch, Ack),
     (CheckForPushedCommits, CheckForPushedCommitsResponse),
     (GitDiff, GitDiffResponse),
     (GitInit, Ack),
@@ -637,6 +639,7 @@ entity_messages!(
     Pull,
     AskPassRequest,
     GitChangeBranch,
+    GitRenameBranch,
     GitCreateBranch,
     CheckForPushedCommits,
     GitDiff,

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

@@ -784,7 +784,10 @@ impl<'a> Chunks<'a> {
             slice_start..slice_end
         };
 
-        let bitmask = (1u128 << slice_range.end as u128).saturating_sub(1);
+        // slice range has a bounds between 0 and 128 in non test builds
+        // We use a non wrapping sub because we want to overflow in the case where slice_range.end == 128
+        // because that represents a full chunk and the bitmask shouldn't remove anything
+        let bitmask = (1u128.unbounded_shl(slice_range.end as u32)).wrapping_sub(1);
 
         let chars = (chunk.chars() & bitmask) >> slice_range.start;
         let tabs = (chunk.tabs & bitmask) >> slice_range.start;

crates/ui/src/components/badge.rs ๐Ÿ”—

@@ -58,7 +58,7 @@ impl RenderOnce for Badge {
             .child(Divider::vertical().color(DividerColor::Border))
             .child(Label::new(self.label.clone()).size(LabelSize::Small).ml_1())
             .when_some(tooltip, |this, tooltip| {
-                this.tooltip(move |window, cx| tooltip(window, cx))
+                this.hoverable_tooltip(move |window, cx| tooltip(window, cx))
             })
     }
 }

crates/ui/src/components/scrollbar.rs ๐Ÿ”—

@@ -35,11 +35,11 @@ pub mod scrollbars {
     pub enum ShowScrollbar {
         /// Show the scrollbar if there's important information or
         /// follow the system's configured behavior.
+        #[default]
         Auto,
         /// Match the system's configured behavior.
         System,
         /// Always show the scrollbar.
-        #[default]
         Always,
         /// Never show the scrollbar.
         Never,

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);