diff --git a/Cargo.lock b/Cargo.lock index fbd19d934ca5c4..a3b967c76d2571 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5330,6 +5330,7 @@ dependencies = [ "editor", "feature_flags", "futures 0.3.31", + "fuzzy", "git", "gpui", "language", @@ -5349,6 +5350,7 @@ dependencies = [ "util", "windows 0.58.0", "workspace", + "zed_actions", ] [[package]] @@ -14616,22 +14618,6 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" -[[package]] -name = "vcs_menu" -version = "0.1.0" -dependencies = [ - "anyhow", - "fuzzy", - "git", - "gpui", - "picker", - "project", - "ui", - "util", - "workspace", - "zed_actions", -] - [[package]] name = "version-compare" version = "0.2.0" @@ -16657,7 +16643,6 @@ dependencies = [ "urlencoding", "util", "uuid", - "vcs_menu", "vim", "vim_mode_setting", "welcome", diff --git a/Cargo.toml b/Cargo.toml index 576f4bb797a0c8..ee6a66f909888b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -147,7 +147,6 @@ members = [ "crates/ui_macros", "crates/util", "crates/util_macros", - "crates/vcs_menu", "crates/vim", "crates/vim_mode_setting", "crates/welcome", @@ -346,7 +345,6 @@ ui_input = { path = "crates/ui_input" } ui_macros = { path = "crates/ui_macros" } util = { path = "crates/util" } util_macros = { path = "crates/util_macros" } -vcs_menu = { path = "crates/vcs_menu" } vim = { path = "crates/vim" } vim_mode_setting = { path = "crates/vim_mode_setting" } welcome = { path = "crates/welcome" } @@ -676,7 +674,6 @@ telemetry_events = { codegen-units = 1 } theme_selector = { codegen-units = 1 } time_format = { codegen-units = 1 } ui_input = { codegen-units = 1 } -vcs_menu = { codegen-units = 1 } zed_actions = { codegen-units = 1 } [profile.release] diff --git a/crates/git_ui/Cargo.toml b/crates/git_ui/Cargo.toml index ad4dbdf9905e40..a30792fe1051b2 100644 --- a/crates/git_ui/Cargo.toml +++ b/crates/git_ui/Cargo.toml @@ -20,6 +20,7 @@ diff.workspace = true editor.workspace = true feature_flags.workspace = true futures.workspace = true +fuzzy.workspace = true git.workspace = true gpui.workspace = true language.workspace = true @@ -38,6 +39,7 @@ theme.workspace = true ui.workspace = true util.workspace = true workspace.workspace = true +zed_actions.workspace = true [target.'cfg(windows)'.dependencies] windows.workspace = true diff --git a/crates/vcs_menu/src/lib.rs b/crates/git_ui/src/branch_picker.rs similarity index 77% rename from crates/vcs_menu/src/lib.rs rename to crates/git_ui/src/branch_picker.rs index e4a63d9f0f8ef2..bff1c8bf52431c 100644 --- a/crates/vcs_menu/src/lib.rs +++ b/crates/git_ui/src/branch_picker.rs @@ -1,27 +1,49 @@ use anyhow::{anyhow, Context as _, Result}; use fuzzy::{StringMatch, StringMatchCandidate}; + use git::repository::Branch; use gpui::{ - rems, AnyElement, App, AsyncApp, Context, DismissEvent, Entity, EventEmitter, FocusHandle, - Focusable, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, - Subscription, Task, WeakEntity, Window, + rems, App, AsyncApp, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, + InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription, + Task, WeakEntity, Window, }; use picker::{Picker, PickerDelegate}; use project::ProjectPath; -use std::{ops::Not, sync::Arc}; +use std::sync::Arc; use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing}; use util::ResultExt; use workspace::notifications::DetachAndPromptErr; use workspace::{ModalView, Workspace}; -use zed_actions::branches::OpenRecent; pub fn init(cx: &mut App) { cx.observe_new(|workspace: &mut Workspace, _, _| { - workspace.register_action(BranchList::open); + workspace.register_action(open); }) .detach(); } +pub fn open( + _: &mut Workspace, + _: &zed_actions::git::Branch, + window: &mut Window, + cx: &mut Context, +) { + let this = cx.entity().clone(); + cx.spawn_in(window, |_, mut cx| async move { + // Modal branch picker has a longer trailoff than a popover one. + let delegate = BranchListDelegate::new(this.clone(), 70, &cx).await?; + + this.update_in(&mut cx, |workspace, window, cx| { + workspace.toggle_modal(window, cx, |window, cx| { + BranchList::new(delegate, 34., window, cx) + }) + })?; + + Ok(()) + }) + .detach_and_prompt_err("Failed to read branches", window, cx, |_, _, _| None) +} + pub struct BranchList { pub picker: Entity>, rem_width: f32, @@ -29,29 +51,7 @@ pub struct BranchList { } impl BranchList { - pub fn open( - _: &mut Workspace, - _: &OpenRecent, - window: &mut Window, - cx: &mut Context, - ) { - let this = cx.entity().clone(); - cx.spawn_in(window, |_, mut cx| async move { - // Modal branch picker has a longer trailoff than a popover one. - let delegate = BranchListDelegate::new(this.clone(), 70, &cx).await?; - - this.update_in(&mut cx, |workspace, window, cx| { - workspace.toggle_modal(window, cx, |window, cx| { - BranchList::new(delegate, 34., window, cx) - }) - })?; - - Ok(()) - }) - .detach_and_prompt_err("Failed to read branches", window, cx, |_, _, _| None) - } - - fn new( + pub fn new( delegate: BranchListDelegate, rem_width: f32, window: &mut Window, @@ -91,6 +91,7 @@ impl Render for BranchList { #[derive(Debug, Clone)] enum BranchEntry { Branch(StringMatch), + History(String), NewBranch { name: String }, } @@ -98,6 +99,7 @@ impl BranchEntry { fn name(&self) -> &str { match self { Self::Branch(branch) => &branch.string, + Self::History(branch) => &branch, Self::NewBranch { name } => &name, } } @@ -114,7 +116,7 @@ pub struct BranchListDelegate { } impl BranchListDelegate { - async fn new( + pub async fn new( workspace: Entity, branch_name_trailoff_after: usize, cx: &AsyncApp, @@ -141,7 +143,7 @@ impl BranchListDelegate { }) } - fn branch_count(&self) -> usize { + pub fn branch_count(&self) -> usize { self.matches .iter() .filter(|item| matches!(item, BranchEntry::Branch(_))) @@ -207,16 +209,10 @@ impl PickerDelegate for BranchListDelegate { let Some(candidates) = candidates.log_err() else { return; }; - let matches = if query.is_empty() { + let matches: Vec = if query.is_empty() { candidates .into_iter() - .enumerate() - .map(|(index, candidate)| StringMatch { - candidate_id: index, - string: candidate.string, - positions: Vec::new(), - score: 0.0, - }) + .map(|candidate| BranchEntry::History(candidate.string)) .collect() } else { fuzzy::match_strings( @@ -228,11 +224,15 @@ impl PickerDelegate for BranchListDelegate { cx.background_executor().clone(), ) .await + .iter() + .cloned() + .map(BranchEntry::Branch) + .collect() }; picker .update(&mut cx, |picker, _| { let delegate = &mut picker.delegate; - delegate.matches = matches.into_iter().map(BranchEntry::Branch).collect(); + delegate.matches = matches; if delegate.matches.is_empty() { if !query.is_empty() { delegate.matches.push(BranchEntry::NewBranch { @@ -268,6 +268,7 @@ impl PickerDelegate for BranchListDelegate { let project = workspace.read(cx).project().read(cx); let branch_to_checkout = match branch { BranchEntry::Branch(branch) => branch.string, + BranchEntry::History(string) => string, BranchEntry::NewBranch { name: branch_name } => branch_name, }; let worktree = project @@ -311,7 +312,14 @@ impl PickerDelegate for BranchListDelegate { .inset(true) .spacing(ListItemSpacing::Sparse) .toggle_state(selected) - .map(|parent| match hit { + .when(matches!(hit, BranchEntry::History(_)), |el| { + el.end_slot( + Icon::new(IconName::HistoryRerun) + .color(Color::Muted) + .size(IconSize::Small), + ) + }) + .map(|el| match hit { BranchEntry::Branch(branch) => { let highlights: Vec<_> = branch .positions @@ -320,40 +328,13 @@ impl PickerDelegate for BranchListDelegate { .copied() .collect(); - parent.child(HighlightedLabel::new(shortened_branch_name, highlights)) + el.child(HighlightedLabel::new(shortened_branch_name, highlights)) } + BranchEntry::History(_) => el.child(Label::new(shortened_branch_name)), BranchEntry::NewBranch { name } => { - parent.child(Label::new(format!("Create branch '{name}'"))) + el.child(Label::new(format!("Create branch '{name}'"))) } }), ) } - - fn render_header( - &self, - _window: &mut Window, - _: &mut Context>, - ) -> Option { - let label = if self.last_query.is_empty() { - Label::new("Recent Branches") - .size(LabelSize::Small) - .mt_1() - .ml_3() - .into_any_element() - } else { - let match_label = self.matches.is_empty().not().then(|| { - let suffix = if self.branch_count() == 1 { "" } else { "es" }; - Label::new(format!("{} match{}", self.branch_count(), suffix)) - .color(Color::Muted) - .size(LabelSize::Small) - }); - h_flex() - .px_3() - .justify_between() - .child(Label::new("Branches").size(LabelSize::Small)) - .children(match_label) - .into_any_element() - }; - Some(v_flex().mt_1().child(label).into_any_element()) - } } diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 50ed70b3b40fa1..d7aa40f8d9d6eb 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -1110,33 +1110,43 @@ impl GitPanel { .git_state() .read(cx) .all_repositories(); - let entry_count = self + + let branch = self .active_repository .as_ref() - .map_or(0, |repo| repo.read(cx).entry_count()); + .and_then(|repository| repository.read(cx).branch()) + .unwrap_or_else(|| "(no current branch)".into()); + + let has_repo_above = all_repositories.iter().any(|repo| { + repo.read(cx) + .repository_entry + .work_directory + .is_above_project() + }); - let changes_string = match entry_count { - 0 => "No changes".to_string(), - 1 => "1 change".to_string(), - n => format!("{} changes", n), - }; + let icon_button = Button::new("branch-selector", branch) + .color(Color::Muted) + .style(ButtonStyle::Subtle) + .icon(IconName::GitBranch) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .size(ButtonSize::Compact) + .icon_position(IconPosition::Start) + .tooltip(Tooltip::for_action_title( + "Switch Branch", + &zed_actions::git::Branch, + )) + .on_click(cx.listener(|_, _, window, cx| { + window.dispatch_action(zed_actions::git::Branch.boxed_clone(), cx); + })) + .style(ButtonStyle::Transparent); self.panel_header_container(window, cx) - .child(h_flex().gap_2().child(if all_repositories.len() <= 1 { - div() - .id("changes-label") - .text_buffer(cx) - .text_ui_sm(cx) - .child( - Label::new(changes_string) - .single_line() - .size(LabelSize::Small), - ) - .into_any_element() - } else { - self.render_repository_selector(cx).into_any_element() - })) + .child(h_flex().pl_1().child(icon_button)) .child(div().flex_grow()) + .when(all_repositories.len() > 1 || has_repo_above, |el| { + el.child(self.render_repository_selector(cx)) + }) } pub fn render_repository_selector(&self, cx: &mut Context) -> impl IntoElement { @@ -1146,35 +1156,11 @@ impl GitPanel { .map(|repo| repo.read(cx).display_name(self.project.read(cx), cx)) .unwrap_or_default(); - let entry_count = self.entries.len(); - RepositorySelectorPopoverMenu::new( self.repository_selector.clone(), ButtonLike::new("active-repository") .style(ButtonStyle::Subtle) - .child( - h_flex().w_full().gap_0p5().child( - div() - .overflow_x_hidden() - .flex_grow() - .whitespace_nowrap() - .child( - h_flex() - .gap_1() - .child( - Label::new(repository_display_name).size(LabelSize::Small), - ) - .when(entry_count > 0, |flex| { - flex.child( - Label::new(format!("({})", entry_count)) - .size(LabelSize::Small) - .color(Color::Muted), - ) - }) - .into_any_element(), - ), - ), - ), + .child(Label::new(repository_display_name).size(LabelSize::Small)), ) } diff --git a/crates/git_ui/src/git_ui.rs b/crates/git_ui/src/git_ui.rs index 3757daaf7e64ae..a8313aa9d5b3cf 100644 --- a/crates/git_ui/src/git_ui.rs +++ b/crates/git_ui/src/git_ui.rs @@ -5,6 +5,7 @@ use gpui::App; use project_diff::ProjectDiff; use ui::{ActiveTheme, Color, Icon, IconName, IntoElement}; +pub mod branch_picker; pub mod git_panel; mod git_panel_settings; pub mod project_diff; @@ -12,6 +13,7 @@ pub mod repository_selector; pub fn init(cx: &mut App) { GitPanelSettings::register(cx); + branch_picker::init(cx); cx.observe_new(ProjectDiff::register).detach(); } diff --git a/crates/git_ui/src/repository_selector.rs b/crates/git_ui/src/repository_selector.rs index 81d5f06635d6a7..ff8cfa406eb943 100644 --- a/crates/git_ui/src/repository_selector.rs +++ b/crates/git_ui/src/repository_selector.rs @@ -34,6 +34,7 @@ impl RepositorySelector { let picker = cx.new(|cx| { Picker::nonsearchable_uniform_list(delegate, window, cx) .max_height(Some(rems(20.).into())) + .width(rems(15.)) }); let _subscriptions = diff --git a/crates/project/src/git.rs b/crates/project/src/git.rs index 2c24a63079d9e2..debc89b3210e73 100644 --- a/crates/project/src/git.rs +++ b/crates/project/src/git.rs @@ -15,7 +15,7 @@ use gpui::{ use language::{Buffer, LanguageRegistry}; use rpc::{proto, AnyProtoClient}; use settings::WorktreeId; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::sync::Arc; use text::BufferId; use util::{maybe, ResultExt}; @@ -299,19 +299,25 @@ impl Repository { (self.worktree_id, self.repository_entry.work_directory_id()) } + pub fn branch(&self) -> Option> { + self.repository_entry.branch() + } + pub fn display_name(&self, project: &Project, cx: &App) -> SharedString { maybe!({ - let path = self.repo_path_to_project_path(&"".into())?; - Some( - project - .absolute_path(&path, cx)? - .file_name()? - .to_string_lossy() - .to_string() - .into(), - ) + let project_path = self.repo_path_to_project_path(&"".into())?; + let worktree_name = project + .worktree_for_id(project_path.worktree_id, cx)? + .read(cx) + .root_name(); + + let mut path = PathBuf::new(); + path = path.join(worktree_name); + path = path.join(project_path.path); + Some(path.to_string_lossy().to_string()) }) - .unwrap_or("".into()) + .unwrap_or_else(|| self.repository_entry.work_directory.display_name()) + .into() } pub fn activate(&self, cx: &mut Context) { diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index cd28890b2e69d7..b396e770141f06 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -530,7 +530,7 @@ impl TitleBar { .tooltip(move |window, cx| { Tooltip::with_meta( "Recent Branches", - Some(&zed_actions::branches::OpenRecent), + Some(&zed_actions::git::Branch), "Local branches only", window, cx, @@ -538,7 +538,7 @@ impl TitleBar { }) .on_click(move |_, window, cx| { let _ = workspace.update(cx, |_this, cx| { - window.dispatch_action(zed_actions::branches::OpenRecent.boxed_clone(), cx); + window.dispatch_action(zed_actions::git::Branch.boxed_clone(), cx); }); }), ) diff --git a/crates/ui/src/components/tooltip.rs b/crates/ui/src/components/tooltip.rs index 753937810f2e60..640bb8dc903894 100644 --- a/crates/ui/src/components/tooltip.rs +++ b/crates/ui/src/components/tooltip.rs @@ -35,6 +35,22 @@ impl Tooltip { } } + pub fn for_action_title( + title: impl Into, + action: &dyn Action, + ) -> impl Fn(&mut Window, &mut App) -> AnyView { + let title = title.into(); + let action = action.boxed_clone(); + move |window, cx| { + cx.new(|_| Self { + title: title.clone(), + meta: None, + key_binding: KeyBinding::for_action(action.as_ref(), window), + }) + .into() + } + } + pub fn for_action( title: impl Into, action: &dyn Action, diff --git a/crates/vcs_menu/Cargo.toml b/crates/vcs_menu/Cargo.toml deleted file mode 100644 index 1e9826d53d3e98..00000000000000 --- a/crates/vcs_menu/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "vcs_menu" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[dependencies] -anyhow.workspace = true -fuzzy.workspace = true -git.workspace = true -gpui.workspace = true -picker.workspace = true -project.workspace = true -ui.workspace = true -util.workspace = true -workspace.workspace = true -zed_actions.workspace = true diff --git a/crates/vcs_menu/LICENSE-GPL b/crates/vcs_menu/LICENSE-GPL deleted file mode 120000 index 89e542f750cd38..00000000000000 --- a/crates/vcs_menu/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/worktree/Cargo.toml b/crates/worktree/Cargo.toml index 0f76e6e4bbbfd5..8630f2019449fa 100644 --- a/crates/worktree/Cargo.toml +++ b/crates/worktree/Cargo.toml @@ -14,11 +14,12 @@ workspace = true [features] test-support = [ + "gpui/test-support", + "http_client/test-support", "language/test-support", "settings/test-support", "text/test-support", - "gpui/test-support", - "http_client/test-support", + "util/test-support", ] [dependencies] @@ -59,3 +60,4 @@ pretty_assertions.workspace = true rand.workspace = true rpc = { workspace = true, features = ["test-support"] } settings = { workspace = true, features = ["test-support"] } +util = { workspace = true, features = ["test-support"] } diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index ed559eea177d85..eee87f3cc51d19 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -213,12 +213,6 @@ impl Deref for RepositoryEntry { } } -impl AsRef for RepositoryEntry { - fn as_ref(&self) -> &Path { - &self.path - } -} - impl RepositoryEntry { pub fn branch(&self) -> Option> { self.branch.clone() @@ -326,33 +320,53 @@ impl RepositoryEntry { /// But if a sub-folder of a git repository is opened, this corresponds to the /// project root and the .git folder is located in a parent directory. #[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)] -pub struct WorkDirectory { - path: Arc, - - /// If location_in_repo is set, it means the .git folder is external - /// and in a parent folder of the project root. - /// In that case, the work_directory field will point to the - /// project-root and location_in_repo contains the location of the - /// project-root in the repository. - /// - /// Example: - /// - /// my_root_folder/ <-- repository root - /// .git - /// my_sub_folder_1/ - /// project_root/ <-- Project root, Zed opened here - /// ... - /// - /// For this setup, the attributes will have the following values: - /// - /// work_directory: pointing to "" entry - /// location_in_repo: Some("my_sub_folder_1/project_root") - pub(crate) location_in_repo: Option>, +pub enum WorkDirectory { + InProject { + relative_path: Arc, + }, + AboveProject { + absolute_path: Arc, + location_in_repo: Arc, + }, } impl WorkDirectory { - pub fn path_key(&self) -> PathKey { - PathKey(self.path.clone()) + #[cfg(test)] + fn in_project(path: &str) -> Self { + let path = Path::new(path); + Self::InProject { + relative_path: path.into(), + } + } + + #[cfg(test)] + fn canonicalize(&self) -> Self { + match self { + WorkDirectory::InProject { relative_path } => WorkDirectory::InProject { + relative_path: relative_path.clone(), + }, + WorkDirectory::AboveProject { + absolute_path, + location_in_repo, + } => WorkDirectory::AboveProject { + absolute_path: absolute_path.canonicalize().unwrap().into(), + location_in_repo: location_in_repo.clone(), + }, + } + } + + pub fn is_above_project(&self) -> bool { + match self { + WorkDirectory::InProject { .. } => false, + WorkDirectory::AboveProject { .. } => true, + } + } + + fn path_key(&self) -> PathKey { + match self { + WorkDirectory::InProject { relative_path } => PathKey(relative_path.clone()), + WorkDirectory::AboveProject { .. } => PathKey(Path::new("").into()), + } } /// Returns true if the given path is a child of the work directory. @@ -360,9 +374,14 @@ impl WorkDirectory { /// Note that the path may not be a member of this repository, if there /// is a repository in a directory between these two paths /// external .git folder in a parent folder of the project root. + #[track_caller] pub fn directory_contains(&self, path: impl AsRef) -> bool { let path = path.as_ref(); - path.starts_with(&self.path) + debug_assert!(path.is_relative()); + match self { + WorkDirectory::InProject { relative_path } => path.starts_with(relative_path), + WorkDirectory::AboveProject { .. } => true, + } } /// relativize returns the given project path relative to the root folder of the @@ -371,53 +390,71 @@ impl WorkDirectory { /// of the project root folder, then the returned RepoPath is relative to the root /// of the repository and not a valid path inside the project. pub fn relativize(&self, path: &Path) -> Result { - let repo_path = if let Some(location_in_repo) = &self.location_in_repo { - // Avoid joining a `/` to location_in_repo in the case of a single-file worktree. - if path == Path::new("") { - RepoPath(location_in_repo.clone()) - } else { - location_in_repo.join(path).into() + // path is assumed to be relative to worktree root. + debug_assert!(path.is_relative()); + match self { + WorkDirectory::InProject { relative_path } => Ok(path + .strip_prefix(relative_path) + .map_err(|_| { + anyhow!( + "could not relativize {:?} against {:?}", + path, + relative_path + ) + })? + .into()), + WorkDirectory::AboveProject { + location_in_repo, .. + } => { + // Avoid joining a `/` to location_in_repo in the case of a single-file worktree. + if path == Path::new("") { + Ok(RepoPath(location_in_repo.clone())) + } else { + Ok(location_in_repo.join(path).into()) + } } - } else { - path.strip_prefix(&self.path) - .map_err(|_| anyhow!("could not relativize {:?} against {:?}", path, self.path))? - .into() - }; - Ok(repo_path) + } } /// This is the opposite operation to `relativize` above pub fn unrelativize(&self, path: &RepoPath) -> Option> { - if let Some(location) = &self.location_in_repo { - // If we fail to strip the prefix, that means this status entry is - // external to this worktree, and we definitely won't have an entry_id - path.strip_prefix(location).ok().map(Into::into) - } else { - Some(self.path.join(path).into()) + match self { + WorkDirectory::InProject { relative_path } => Some(relative_path.join(path).into()), + WorkDirectory::AboveProject { + location_in_repo, .. + } => { + // If we fail to strip the prefix, that means this status entry is + // external to this worktree, and we definitely won't have an entry_id + path.strip_prefix(location_in_repo).ok().map(Into::into) + } } } -} -impl Default for WorkDirectory { - fn default() -> Self { - Self { - path: Arc::from(Path::new("")), - location_in_repo: None, + pub fn display_name(&self) -> String { + match self { + WorkDirectory::InProject { relative_path } => relative_path.display().to_string(), + WorkDirectory::AboveProject { + absolute_path, + location_in_repo, + } => { + let num_of_dots = location_in_repo.components().count(); + + "../".repeat(num_of_dots) + + &absolute_path + .file_name() + .map(|s| s.to_string_lossy()) + .unwrap_or_default() + + "/" + } } } } -impl Deref for WorkDirectory { - type Target = Path; - - fn deref(&self) -> &Self::Target { - self.as_ref() - } -} - -impl AsRef for WorkDirectory { - fn as_ref(&self) -> &Path { - self.path.as_ref() +impl Default for WorkDirectory { + fn default() -> Self { + Self::InProject { + relative_path: Arc::from(Path::new("")), + } } } @@ -487,7 +524,7 @@ impl sum_tree::Item for LocalRepositoryEntry { fn summary(&self, _: &::Context) -> Self::Summary { PathSummary { - max_path: self.work_directory.path.clone(), + max_path: self.work_directory.path_key().0, item_summary: Unit, } } @@ -497,7 +534,7 @@ impl KeyedItem for LocalRepositoryEntry { type Key = PathKey; fn key(&self) -> Self::Key { - PathKey(self.work_directory.path.clone()) + self.work_directory.path_key() } } @@ -2574,12 +2611,11 @@ impl Snapshot { self.repositories.insert_or_replace( RepositoryEntry { work_directory_id, - work_directory: WorkDirectory { - path: work_dir_entry.path.clone(), - // When syncing repository entries from a peer, we don't need - // the location_in_repo field, since git operations don't happen locally - // anyway. - location_in_repo: None, + // When syncing repository entries from a peer, we don't need + // the location_in_repo field, since git operations don't happen locally + // anyway. + work_directory: WorkDirectory::InProject { + relative_path: work_dir_entry.path.clone(), }, branch: repository.branch.map(Into::into), statuses_by_path: statuses, @@ -2690,23 +2726,13 @@ impl Snapshot { &self.repositories } - pub fn repositories_with_abs_paths( - &self, - ) -> impl '_ + Iterator { - let base = self.abs_path(); - self.repositories.iter().map(|repo| { - let path = repo.work_directory.location_in_repo.as_deref(); - let path = path.unwrap_or(repo.work_directory.as_ref()); - (repo, base.join(path)) - }) - } - /// Get the repository whose work directory corresponds to the given path. pub(crate) fn repository(&self, work_directory: PathKey) -> Option { self.repositories.get(&work_directory, &()).cloned() } /// Get the repository whose work directory contains the given path. + #[track_caller] pub fn repository_for_path(&self, path: &Path) -> Option<&RepositoryEntry> { self.repositories .iter() @@ -2716,6 +2742,7 @@ impl Snapshot { /// Given an ordered iterator of entries, returns an iterator of those entries, /// along with their containing git repository. + #[track_caller] pub fn entries_with_repositories<'a>( &'a self, entries: impl 'a + Iterator, @@ -3081,7 +3108,7 @@ impl LocalSnapshot { let work_dir_paths = self .repositories .iter() - .map(|repo| repo.work_directory.path.clone()) + .map(|repo| repo.work_directory.path_key()) .collect::>(); assert_eq!(dotgit_paths.len(), work_dir_paths.len()); assert_eq!(self.repositories.iter().count(), work_dir_paths.len()); @@ -3289,7 +3316,7 @@ impl BackgroundScannerState { .git_repositories .retain(|id, _| removed_ids.binary_search(id).is_err()); self.snapshot.repositories.retain(&(), |repository| { - !repository.work_directory.starts_with(path) + !repository.work_directory.path_key().0.starts_with(path) }); #[cfg(test)] @@ -3327,20 +3354,26 @@ impl BackgroundScannerState { } }; - self.insert_git_repository_for_path(work_dir_path, dot_git_path, None, fs, watcher) + self.insert_git_repository_for_path( + WorkDirectory::InProject { + relative_path: work_dir_path, + }, + dot_git_path, + fs, + watcher, + ) } fn insert_git_repository_for_path( &mut self, - work_dir_path: Arc, + work_directory: WorkDirectory, dot_git_path: Arc, - location_in_repo: Option>, fs: &dyn Fs, watcher: &dyn Watcher, ) -> Option { let work_dir_id = self .snapshot - .entry_for_path(work_dir_path.clone()) + .entry_for_path(work_directory.path_key().0) .map(|entry| entry.id)?; if self.snapshot.git_repositories.get(&work_dir_id).is_some() { @@ -3374,10 +3407,6 @@ impl BackgroundScannerState { }; log::trace!("constructed libgit2 repo in {:?}", t0.elapsed()); - let work_directory = WorkDirectory { - path: work_dir_path.clone(), - location_in_repo, - }; if let Some(git_hosting_provider_registry) = self.git_hosting_provider_registry.clone() { git_hosting_providers::register_additional_providers( @@ -3840,7 +3869,7 @@ impl sum_tree::Item for RepositoryEntry { fn summary(&self, _: &::Context) -> Self::Summary { PathSummary { - max_path: self.work_directory.path.clone(), + max_path: self.work_directory.path_key().0, item_summary: Unit, } } @@ -3850,7 +3879,7 @@ impl sum_tree::KeyedItem for RepositoryEntry { type Key = PathKey; fn key(&self) -> Self::Key { - PathKey(self.work_directory.path.clone()) + self.work_directory.path_key() } } @@ -4089,7 +4118,7 @@ impl<'a> sum_tree::Dimension<'a, PathEntrySummary> for ProjectEntryId { } } -#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] +#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct PathKey(Arc); impl Default for PathKey { @@ -4168,15 +4197,15 @@ impl BackgroundScanner { // We associate the external git repo with our root folder and // also mark where in the git repo the root folder is located. self.state.lock().insert_git_repository_for_path( - Path::new("").into(), - ancestor_dot_git.into(), - Some( - root_abs_path + WorkDirectory::AboveProject { + absolute_path: ancestor.into(), + location_in_repo: root_abs_path .as_path() .strip_prefix(ancestor) .unwrap() .into(), - ), + }, + ancestor_dot_git.into(), self.fs.as_ref(), self.watcher.as_ref(), ); @@ -4385,13 +4414,6 @@ impl BackgroundScanner { dot_git_abs_paths.push(dot_git_abs_path); } } - if abs_path.0.file_name() == Some(*GITIGNORE) { - for (_, repo) in snapshot.git_repositories.iter().filter(|(_, repo)| repo.directory_contains(&abs_path.0)) { - if !dot_git_abs_paths.iter().any(|dot_git_abs_path| dot_git_abs_path == repo.dot_git_dir_abs_path.as_ref()) { - dot_git_abs_paths.push(repo.dot_git_dir_abs_path.to_path_buf()); - } - } - } let relative_path: Arc = if let Ok(path) = abs_path.strip_prefix(&root_canonical_path) { @@ -4409,6 +4431,14 @@ impl BackgroundScanner { return false; }; + if abs_path.0.file_name() == Some(*GITIGNORE) { + for (_, repo) in snapshot.git_repositories.iter().filter(|(_, repo)| repo.directory_contains(&relative_path)) { + if !dot_git_abs_paths.iter().any(|dot_git_abs_path| dot_git_abs_path == repo.dot_git_dir_abs_path.as_ref()) { + dot_git_abs_paths.push(repo.dot_git_dir_abs_path.to_path_buf()); + } + } + } + let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| { snapshot .entry_for_path(parent) @@ -4992,7 +5022,7 @@ impl BackgroundScanner { snapshot .snapshot .repositories - .remove(&PathKey(repository.work_directory.path.clone()), &()); + .remove(&repository.work_directory.path_key(), &()); return Some(()); } } @@ -5286,7 +5316,7 @@ impl BackgroundScanner { fn update_git_statuses(&self, job: UpdateGitStatusesJob) { log::trace!( "updating git statuses for repo {:?}", - job.local_repository.work_directory.path + job.local_repository.work_directory.display_name() ); let t0 = Instant::now(); @@ -5300,7 +5330,7 @@ impl BackgroundScanner { }; log::trace!( "computed git statuses for repo {:?} in {:?}", - job.local_repository.work_directory.path, + job.local_repository.work_directory.display_name(), t0.elapsed() ); @@ -5364,7 +5394,7 @@ impl BackgroundScanner { log::trace!( "applied git status updates for repo {:?} in {:?}", - job.local_repository.work_directory.path, + job.local_repository.work_directory.display_name(), t0.elapsed(), ); } diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index 2cee728aec89e4..f4e6da23455c5b 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -1,6 +1,6 @@ use crate::{ - worktree_settings::WorktreeSettings, Entry, EntryKind, Event, PathChange, Snapshot, Worktree, - WorktreeModelHandle, + worktree_settings::WorktreeSettings, Entry, EntryKind, Event, PathChange, Snapshot, + WorkDirectory, Worktree, WorktreeModelHandle, }; use anyhow::Result; use fs::{FakeFs, Fs, RealFs, RemoveOptions}; @@ -2200,7 +2200,10 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) { cx.read(|cx| { let tree = tree.read(cx); let repo = tree.repositories().iter().next().unwrap(); - assert_eq!(repo.path.as_ref(), Path::new("projects/project1")); + assert_eq!( + repo.work_directory, + WorkDirectory::in_project("projects/project1") + ); assert_eq!( tree.status_for_file(Path::new("projects/project1/a")), Some(StatusCode::Modified.worktree()), @@ -2221,7 +2224,10 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) { cx.read(|cx| { let tree = tree.read(cx); let repo = tree.repositories().iter().next().unwrap(); - assert_eq!(repo.path.as_ref(), Path::new("projects/project2")); + assert_eq!( + repo.work_directory, + WorkDirectory::in_project("projects/project2") + ); assert_eq!( tree.status_for_file(Path::new("projects/project2/a")), Some(StatusCode::Modified.worktree()), @@ -2275,12 +2281,15 @@ async fn test_git_repository_for_path(cx: &mut TestAppContext) { assert!(tree.repository_for_path("c.txt".as_ref()).is_none()); let repo = tree.repository_for_path("dir1/src/b.txt".as_ref()).unwrap(); - assert_eq!(repo.path.as_ref(), Path::new("dir1")); + assert_eq!(repo.work_directory, WorkDirectory::in_project("dir1")); let repo = tree .repository_for_path("dir1/deps/dep1/src/a.txt".as_ref()) .unwrap(); - assert_eq!(repo.path.as_ref(), Path::new("dir1/deps/dep1")); + assert_eq!( + repo.work_directory, + WorkDirectory::in_project("dir1/deps/dep1") + ); let entries = tree.files(false, 0); @@ -2289,7 +2298,7 @@ async fn test_git_repository_for_path(cx: &mut TestAppContext) { .map(|(entry, repo)| { ( entry.path.as_ref(), - repo.map(|repo| repo.path.to_path_buf()), + repo.map(|repo| repo.work_directory.clone()), ) }) .collect::>(); @@ -2300,9 +2309,12 @@ async fn test_git_repository_for_path(cx: &mut TestAppContext) { (Path::new("c.txt"), None), ( Path::new("dir1/deps/dep1/src/a.txt"), - Some(Path::new("dir1/deps/dep1").into()) + Some(WorkDirectory::in_project("dir1/deps/dep1")) + ), + ( + Path::new("dir1/src/b.txt"), + Some(WorkDirectory::in_project("dir1")) ), - (Path::new("dir1/src/b.txt"), Some(Path::new("dir1").into())), ] ); }); @@ -2408,8 +2420,10 @@ async fn test_file_status(cx: &mut TestAppContext) { let snapshot = tree.snapshot(); assert_eq!(snapshot.repositories().iter().count(), 1); let repo_entry = snapshot.repositories().iter().next().unwrap(); - assert_eq!(repo_entry.path.as_ref(), Path::new("project")); - assert!(repo_entry.location_in_repo.is_none()); + assert_eq!( + repo_entry.work_directory, + WorkDirectory::in_project("project") + ); assert_eq!( snapshot.status_for_file(project_path.join(B_TXT)), @@ -2760,15 +2774,14 @@ async fn test_repository_subfolder_git_status(cx: &mut TestAppContext) { let snapshot = tree.snapshot(); assert_eq!(snapshot.repositories().iter().count(), 1); let repo = snapshot.repositories().iter().next().unwrap(); - // Path is blank because the working directory of - // the git repository is located at the root of the project - assert_eq!(repo.path.as_ref(), Path::new("")); - - // This is the missing path between the root of the project (sub-folder-2) and its - // location relative to the root of the repository. assert_eq!( - repo.location_in_repo, - Some(Arc::from(Path::new("sub-folder-1/sub-folder-2"))) + repo.work_directory.canonicalize(), + WorkDirectory::AboveProject { + absolute_path: Arc::from(root.path().join("my-repo").canonicalize().unwrap()), + location_in_repo: Arc::from(Path::new(util::separator!( + "sub-folder-1/sub-folder-2" + ))) + } ); assert_eq!(snapshot.status_for_file("c.txt"), None); diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 5677203d1d6c48..6106a382e17cc9 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -126,7 +126,6 @@ url.workspace = true urlencoding = "2.1.2" util.workspace = true uuid.workspace = true -vcs_menu.workspace = true vim.workspace = true vim_mode_setting.workspace = true welcome.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 019af54c541a0d..78cd4d19cdd831 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -505,7 +505,6 @@ fn main() { notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx); collab_ui::init(&app_state, cx); git_ui::init(cx); - vcs_menu::init(cx); feedback::init(cx); markdown_preview::init(cx); welcome::init(cx); diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index 2299bf58bc2c7b..08ec86afa09fe4 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -47,10 +47,10 @@ actions!( ] ); -pub mod branches { - use gpui::actions; +pub mod git { + use gpui::action_with_deprecated_aliases; - actions!(branches, [OpenRecent]); + action_with_deprecated_aliases!(git, Branch, ["branches::OpenRecent"]); } pub mod command_palette {