Detailed changes
@@ -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",
@@ -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" }
@@ -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))],
)
});
}
@@ -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,
}
}
@@ -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)
@@ -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");
@@ -38,7 +38,7 @@ pub struct EditPredictionExcerpt {
pub size: usize,
}
-#[derive(Clone)]
+#[derive(Debug, Clone)]
pub struct EditPredictionExcerptText {
pub body: String,
pub parent_signatures: Vec<String>,
@@ -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(),
@@ -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);
@@ -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
@@ -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(¤t_buffer_snapshot);
+
+ let language = current_buffer_snapshot.language().cloned();
+ let Some(worktree_id) = self
+ .project
+ .read(cx)
+ .worktrees(cx)
+ .next()
+ .map(|worktree| worktree.read(cx).id())
+ else {
+ log::error!("Open a worktree to use edit prediction debug view");
+ self.last_context.take();
+ return;
+ };
+
+ self._edit_prediction_context_task = cx.spawn_in(window, {
+ let language_registry = self.project.read(cx).languages().clone();
+ async move |this, cx| {
+ cx.background_executor()
+ .timer(Duration::from_millis(50))
+ .await;
+
+ let Ok(task) = this.update(cx, |this, cx| {
+ fn number_input_value<T: FromStr + Default>(
+ input: &Entity<SingleLineInput>,
+ cx: &App,
+ ) -> T {
+ input
+ .read(cx)
+ .editor()
+ .read(cx)
+ .text(cx)
+ .parse::<T>()
+ .unwrap_or_default()
+ }
+
+ let options = EditPredictionExcerptOptions {
+ max_bytes: number_input_value(&this.max_bytes_input, cx),
+ min_bytes: number_input_value(&this.min_bytes_input, cx),
+ target_before_cursor_over_total_bytes: number_input_value(
+ &this.cursor_context_ratio_input,
+ cx,
+ ),
+ // TODO Display and add to options
+ include_parent_signatures: false,
+ };
+
+ EditPredictionContext::gather(
+ cursor_position,
+ current_buffer_snapshot,
+ options,
+ this.syntax_index.clone(),
+ cx,
+ )
+ }) else {
+ this.update(cx, |this, _cx| {
+ this.last_context.take();
+ })
+ .ok();
+ return;
+ };
+
+ let Some(context) = task.await else {
+ // TODO: Display message
+ this.update(cx, |this, _cx| {
+ this.last_context.take();
+ })
+ .ok();
+ return;
+ };
+
+ let mut languages = HashMap::default();
+ for snippet in context.snippets.iter() {
+ let lang_id = snippet.declaration.identifier().language_id;
+ if let Entry::Vacant(entry) = languages.entry(lang_id) {
+ // Most snippets are gonna be the same language,
+ // so we think it's fine to do this sequentially for now
+ entry.insert(language_registry.language_for_id(lang_id).await.ok());
+ }
+ }
+
+ this.update_in(cx, |this, window, cx| {
+ let context_editor = cx.new(|cx| {
+ let multibuffer = cx.new(|cx| {
+ let mut multibuffer = MultiBuffer::new(language::Capability::ReadOnly);
+ let excerpt_file = Arc::new(ExcerptMetadataFile {
+ title: PathBuf::from("Cursor Excerpt").into(),
+ worktree_id,
+ });
+
+ let excerpt_buffer = cx.new(|cx| {
+ let mut buffer = Buffer::local(context.excerpt_text.body, cx);
+ buffer.set_language(language, cx);
+ buffer.file_updated(excerpt_file, cx);
+ buffer
+ });
+
+ multibuffer.push_excerpts(
+ excerpt_buffer,
+ [ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
+ cx,
+ );
+
+ for snippet in context.snippets {
+ let path = this
+ .project
+ .read(cx)
+ .path_for_entry(snippet.declaration.project_entry_id(), cx);
+
+ let snippet_file = Arc::new(ExcerptMetadataFile {
+ title: PathBuf::from(format!(
+ "{} (Score density: {})",
+ path.map(|p| p.path.to_string_lossy().to_string())
+ .unwrap_or_else(|| "".to_string()),
+ snippet.score_density(SnippetStyle::Declaration)
+ ))
+ .into(),
+ worktree_id,
+ });
+
+ let excerpt_buffer = cx.new(|cx| {
+ let mut buffer =
+ Buffer::local(snippet.declaration.item_text().0, cx);
+ buffer.file_updated(snippet_file, cx);
+ if let Some(language) =
+ languages.get(&snippet.declaration.identifier().language_id)
+ {
+ buffer.set_language(language.clone(), cx);
+ }
+ buffer
+ });
+
+ multibuffer.push_excerpts(
+ excerpt_buffer,
+ [ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
+ cx,
+ );
+ }
+
+ multibuffer
+ });
+
+ Editor::new(EditorMode::full(), multibuffer, None, window, cx)
+ });
+
+ this.last_context = Some(ContextState {
+ context_editor,
+ retrieval_duration: context.retrieval_duration,
+ });
+ cx.notify();
+ })
+ .ok();
+ }
+ });
+ }
+}
+
+impl Focusable for EditPredictionTools {
+ fn focus_handle(&self, _cx: &App) -> FocusHandle {
+ self.focus_handle.clone()
+ }
+}
+
+impl Item for EditPredictionTools {
+ type Event = ();
+
+ fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
+ "Edit Prediction Context Debug View".into()
+ }
+
+ fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
+ Some(Icon::new(IconName::ZedPredict))
+ }
+}
+
+impl EventEmitter<()> for EditPredictionTools {}
+
+impl Render for EditPredictionTools {
+ fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ v_flex()
+ .size_full()
+ .bg(cx.theme().colors().editor_background)
+ .child(
+ h_flex()
+ .items_start()
+ .w_full()
+ .child(
+ v_flex()
+ .flex_1()
+ .p_4()
+ .gap_2()
+ .child(Headline::new("Excerpt Options").size(HeadlineSize::Small))
+ .child(
+ h_flex()
+ .gap_2()
+ .child(self.max_bytes_input.clone())
+ .child(self.min_bytes_input.clone())
+ .child(self.cursor_context_ratio_input.clone()),
+ ),
+ )
+ .child(ui::Divider::vertical())
+ .when_some(self.last_context.as_ref(), |this, last_context| {
+ this.child(
+ v_flex()
+ .p_4()
+ .gap_2()
+ .min_w(px(160.))
+ .child(Headline::new("Stats").size(HeadlineSize::Small))
+ .child(
+ h_flex()
+ .gap_1()
+ .child(
+ Label::new("Time to retrieve")
+ .color(Color::Muted)
+ .size(LabelSize::Small),
+ )
+ .child(
+ Label::new(
+ if last_context.retrieval_duration.as_micros()
+ > 1000
+ {
+ format!(
+ "{} ms",
+ last_context.retrieval_duration.as_millis()
+ )
+ } else {
+ format!(
+ "{} ยตs",
+ last_context.retrieval_duration.as_micros()
+ )
+ },
+ )
+ .size(LabelSize::Small),
+ ),
+ ),
+ )
+ }),
+ )
+ .children(self.last_context.as_ref().map(|c| c.context_editor.clone()))
+ }
+}
+
+// Using same approach as commit view
+
+struct ExcerptMetadataFile {
+ title: Arc<Path>,
+ worktree_id: WorktreeId,
+}
+
+impl language::File for ExcerptMetadataFile {
+ fn as_local(&self) -> Option<&dyn language::LocalFile> {
+ None
+ }
+
+ fn disk_state(&self) -> DiskState {
+ DiskState::New
+ }
+
+ fn path(&self) -> &Arc<Path> {
+ &self.title
+ }
+
+ fn full_path(&self, _: &App) -> PathBuf {
+ self.title.as_ref().into()
+ }
+
+ fn file_name<'a>(&'a self, _: &'a App) -> &'a OsStr {
+ self.title.file_name().unwrap()
+ }
+
+ fn worktree_id(&self, _: &App) -> WorktreeId {
+ self.worktree_id
+ }
+
+ fn to_proto(&self, _: &App) -> language::proto::File {
+ unimplemented!()
+ }
+
+ fn is_private(&self) -> bool {
+ false
+ }
+}
@@ -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
@@ -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"])]
@@ -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();
@@ -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,
@@ -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
@@ -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 {
@@ -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,
@@ -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;
@@ -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;
@@ -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,
@@ -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;
@@ -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))
})
}
}
@@ -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,
@@ -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()
@@ -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
@@ -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);