Cargo.lock 🔗
@@ -5466,6 +5466,7 @@ dependencies = [
"panel",
"picker",
"postage",
+ "pretty_assertions",
"project",
"schemars",
"serde",
Cole Miller created
Modified version of #25950. We still use worktree paths, but repo paths
with a status that lie outside the worktree are not excluded; instead,
we relativize them by adding `..`. This makes the list in the git panel
match what you'd get from running `git status` (with the repo's worktree
root as the working directory).
- [x] Implement + test new unrelativization logic
- [x] ~~When collecting repositories, dedup by .git abs path, so
worktrees can share a repo at the project level~~ dedup repos at the
repository selector layer, with repos coming from larger worktrees being
preferred
- [x] Open single-file worktree with diff when activating a path not in
the worktree
Release Notes:
- N/A
Cargo.lock | 1
crates/editor/src/clangd_ext.rs | 3
crates/editor/src/hover_popover.rs | 13
crates/editor/src/items.rs | 16
crates/file_finder/src/file_finder.rs | 9
crates/file_finder/src/file_finder_tests.rs | 7
crates/git_ui/Cargo.toml | 1
crates/git_ui/src/git_panel.rs | 442 +++++++++++++----
crates/git_ui/src/repository_selector.rs | 46 +
crates/journal/src/journal.rs | 22
crates/markdown_preview/src/markdown_renderer.rs | 22
crates/outline_panel/src/outline_panel.rs | 17
crates/project/src/git.rs | 12
crates/project_panel/src/project_panel.rs | 5
crates/snippets_ui/src/snippets_ui.rs | 7
crates/tasks_ui/src/modal.rs | 62 ++
crates/terminal_view/src/terminal_view.rs | 9
crates/util/src/util.rs | 4
crates/workspace/src/pane.rs | 9
crates/workspace/src/workspace.rs | 68 +
crates/worktree/src/worktree.rs | 52 +
crates/worktree/src/worktree_tests.rs | 37 +
crates/zed/src/zed.rs | 36 +
23 files changed, 720 insertions(+), 180 deletions(-)
@@ -5466,6 +5466,7 @@ dependencies = [
"panel",
"picker",
"postage",
+ "pretty_assertions",
"project",
"schemars",
"serde",
@@ -2,6 +2,7 @@ use anyhow::Context as _;
use gpui::{App, Context, Entity, Window};
use language::Language;
use url::Url;
+use workspace::{OpenOptions, OpenVisible};
use crate::lsp_ext::find_specific_language_server_in_selection;
@@ -72,7 +73,7 @@ pub fn switch_source_header(
workspace
.update_in(&mut cx, |workspace, window, cx| {
- workspace.open_abs_path(path, false, window, cx)
+ workspace.open_abs_path(path, OpenOptions { visible: Some(OpenVisible::None), ..Default::default() }, window, cx)
})
.with_context(|| {
format!(
@@ -25,7 +25,7 @@ use theme::ThemeSettings;
use ui::{prelude::*, theme_is_transparent, Scrollbar, ScrollbarState};
use url::Url;
use util::TryFutureExt;
-use workspace::Workspace;
+use workspace::{OpenOptions, OpenVisible, Workspace};
pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200;
pub const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.;
@@ -632,8 +632,15 @@ pub fn open_markdown_url(link: SharedString, window: &mut Window, cx: &mut App)
if uri.scheme() == "file" {
if let Some(workspace) = window.root::<Workspace>().flatten() {
workspace.update(cx, |workspace, cx| {
- let task =
- workspace.open_abs_path(PathBuf::from(uri.path()), false, window, cx);
+ let task = workspace.open_abs_path(
+ PathBuf::from(uri.path()),
+ OpenOptions {
+ visible: Some(OpenVisible::None),
+ ..Default::default()
+ },
+ window,
+ cx,
+ );
cx.spawn_in(window, |_, mut cx| async move {
let item = task.await?;
@@ -38,10 +38,14 @@ use text::{BufferId, Selection};
use theme::{Theme, ThemeSettings};
use ui::{prelude::*, IconDecorationKind};
use util::{paths::PathExt, ResultExt, TryFutureExt};
-use workspace::item::{Dedup, ItemSettings, SerializableItem, TabContentParams};
use workspace::{
item::{BreadcrumbText, FollowEvent},
searchable::SearchOptions,
+ OpenVisible,
+};
+use workspace::{
+ item::{Dedup, ItemSettings, SerializableItem, TabContentParams},
+ OpenOptions,
};
use workspace::{
item::{FollowableItem, Item, ItemEvent, ProjectItem},
@@ -1157,7 +1161,15 @@ impl SerializableItem for Editor {
}
None => {
let open_by_abs_path = workspace.update(cx, |workspace, cx| {
- workspace.open_abs_path(abs_path.clone(), false, window, cx)
+ workspace.open_abs_path(
+ abs_path.clone(),
+ OpenOptions {
+ visible: Some(OpenVisible::None),
+ ..Default::default()
+ },
+ window,
+ cx,
+ )
});
window.spawn(cx, |mut cx| async move {
let editor = open_by_abs_path?.await?.downcast::<Editor>().with_context(|| format!("Failed to downcast to Editor after opening abs path {abs_path:?}"))?;
@@ -42,8 +42,8 @@ use ui::{
};
use util::{maybe, paths::PathWithPosition, post_inc, ResultExt};
use workspace::{
- item::PreviewTabsSettings, notifications::NotifyResultExt, pane, ModalView, SplitDirection,
- Workspace,
+ item::PreviewTabsSettings, notifications::NotifyResultExt, pane, ModalView, OpenOptions,
+ OpenVisible, SplitDirection, Workspace,
};
actions!(file_finder, [SelectPrevious, ToggleMenu]);
@@ -1239,7 +1239,10 @@ impl PickerDelegate for FileFinderDelegate {
} else {
workspace.open_abs_path(
abs_path.to_path_buf(),
- false,
+ OpenOptions {
+ visible: Some(OpenVisible::None),
+ ..Default::default()
+ },
window,
cx,
)
@@ -7,7 +7,7 @@ use menu::{Confirm, SelectNext, SelectPrevious};
use project::{RemoveOptions, FS_WATCH_LATENCY};
use serde_json::json;
use util::path;
-use workspace::{AppState, ToggleFileFinder, Workspace};
+use workspace::{AppState, OpenOptions, ToggleFileFinder, Workspace};
#[ctor::ctor]
fn init_logger() {
@@ -951,7 +951,10 @@ async fn test_external_files_history(cx: &mut gpui::TestAppContext) {
.update_in(cx, |workspace, window, cx| {
workspace.open_abs_path(
PathBuf::from(path!("/external-src/test/third.rs")),
- false,
+ OpenOptions {
+ visible: Some(OpenVisible::None),
+ ..Default::default()
+ },
window,
cx,
)
@@ -64,6 +64,7 @@ ctor.workspace = true
env_logger.workspace = true
editor = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
+pretty_assertions.workspace = true
project = { workspace = true, features = ["test-support"] }
settings = { workspace = true, features = ["test-support"] }
unindent.workspace = true
@@ -3,6 +3,7 @@ use crate::branch_picker;
use crate::commit_modal::CommitModal;
use crate::git_panel_settings::StatusStyle;
use crate::remote_output_toast::{RemoteAction, RemoteOutputToast};
+use crate::repository_selector::filtered_repository_entries;
use crate::{
git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector,
};
@@ -23,7 +24,14 @@ use git::repository::{
};
use git::{repository::RepoPath, status::FileStatus, Commit, ToggleStaged};
use git::{RestoreTrackedFiles, StageAll, TrashUntrackedFiles, UnstageAll};
-use gpui::*;
+use gpui::{
+ actions, anchored, deferred, hsla, percentage, point, uniform_list, Action, Animation,
+ AnimationExt as _, AnyView, BoxShadow, ClickEvent, Corner, DismissEvent, Entity, EventEmitter,
+ FocusHandle, Focusable, KeyContext, ListHorizontalSizingBehavior, ListSizingBehavior,
+ Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, Point, PromptLevel,
+ ScrollStrategy, Stateful, Subscription, Task, Transformation, UniformListScrollHandle,
+ WeakEntity,
+};
use itertools::Itertools;
use language::{Buffer, File};
use language_model::{
@@ -43,6 +51,7 @@ use settings::Settings as _;
use smallvec::smallvec;
use std::cell::RefCell;
use std::future::Future;
+use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::{collections::HashSet, sync::Arc, time::Duration, usize};
use strum::{IntoEnumIterator, VariantNames};
@@ -52,6 +61,7 @@ use ui::{
ScrollbarState, Tooltip,
};
use util::{maybe, post_inc, ResultExt, TryFutureExt};
+use workspace::{AppState, OpenOptions, OpenVisible};
use workspace::{
dock::{DockPosition, Panel, PanelEvent},
@@ -71,7 +81,12 @@ actions!(
]
);
-fn prompt<T>(msg: &str, detail: Option<&str>, window: &mut Window, cx: &mut App) -> Task<Result<T>>
+fn prompt<T>(
+ msg: &str,
+ detail: Option<&str>,
+ window: &mut Window,
+ cx: &mut App,
+) -> Task<anyhow::Result<T>>
where
T: IntoEnumIterator + VariantNames + 'static,
{
@@ -173,6 +188,8 @@ impl GitListEntry {
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct GitStatusEntry {
pub(crate) repo_path: RepoPath,
+ pub(crate) worktree_path: Arc<Path>,
+ pub(crate) abs_path: PathBuf,
pub(crate) status: FileStatus,
pub(crate) is_staged: Option<bool>,
}
@@ -269,99 +286,98 @@ pub(crate) fn commit_message_editor(
impl GitPanel {
pub fn new(
- workspace: &mut Workspace,
+ workspace: Entity<Workspace>,
+ project: Entity<Project>,
+ app_state: Arc<AppState>,
window: &mut Window,
- cx: &mut Context<Workspace>,
- ) -> Entity<Self> {
- let fs = workspace.app_state().fs.clone();
- let project = workspace.project().clone();
+ cx: &mut Context<Self>,
+ ) -> Self {
+ let fs = app_state.fs.clone();
let git_store = project.read(cx).git_store().clone();
let active_repository = project.read(cx).active_repository(cx);
- let workspace = cx.entity().downgrade();
+ let workspace = workspace.downgrade();
- cx.new(|cx| {
- let focus_handle = cx.focus_handle();
- cx.on_focus(&focus_handle, window, Self::focus_in).detach();
- cx.on_focus_out(&focus_handle, window, |this, _, window, cx| {
- this.hide_scrollbar(window, cx);
- })
- .detach();
+ let focus_handle = cx.focus_handle();
+ cx.on_focus(&focus_handle, window, Self::focus_in).detach();
+ cx.on_focus_out(&focus_handle, window, |this, _, window, cx| {
+ this.hide_scrollbar(window, cx);
+ })
+ .detach();
- // just to let us render a placeholder editor.
- // Once the active git repo is set, this buffer will be replaced.
- let temporary_buffer = cx.new(|cx| Buffer::local("", cx));
- let commit_editor = cx.new(|cx| {
- commit_message_editor(temporary_buffer, None, project.clone(), true, window, cx)
- });
+ // just to let us render a placeholder editor.
+ // Once the active git repo is set, this buffer will be replaced.
+ let temporary_buffer = cx.new(|cx| Buffer::local("", cx));
+ let commit_editor = cx.new(|cx| {
+ commit_message_editor(temporary_buffer, None, project.clone(), true, window, cx)
+ });
- commit_editor.update(cx, |editor, cx| {
- editor.clear(window, cx);
- });
+ commit_editor.update(cx, |editor, cx| {
+ editor.clear(window, cx);
+ });
- let scroll_handle = UniformListScrollHandle::new();
+ let scroll_handle = UniformListScrollHandle::new();
- cx.subscribe_in(
- &git_store,
- window,
- move |this, git_store, event, window, cx| match event {
- GitEvent::FileSystemUpdated => {
- this.schedule_update(false, window, cx);
- }
- GitEvent::ActiveRepositoryChanged | GitEvent::GitStateUpdated => {
- this.active_repository = git_store.read(cx).active_repository();
- this.schedule_update(true, window, cx);
- }
- GitEvent::IndexWriteError(error) => {
- this.workspace
- .update(cx, |workspace, cx| {
- workspace.show_error(error, cx);
- })
- .ok();
- }
- },
- )
- .detach();
+ cx.subscribe_in(
+ &git_store,
+ window,
+ move |this, git_store, event, window, cx| match event {
+ GitEvent::FileSystemUpdated => {
+ this.schedule_update(false, window, cx);
+ }
+ GitEvent::ActiveRepositoryChanged | GitEvent::GitStateUpdated => {
+ this.active_repository = git_store.read(cx).active_repository();
+ this.schedule_update(true, window, cx);
+ }
+ GitEvent::IndexWriteError(error) => {
+ this.workspace
+ .update(cx, |workspace, cx| {
+ workspace.show_error(error, cx);
+ })
+ .ok();
+ }
+ },
+ )
+ .detach();
- let scrollbar_state =
- ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity());
-
- let mut git_panel = Self {
- pending_remote_operations: Default::default(),
- remote_operation_id: 0,
- active_repository,
- commit_editor,
- conflicted_count: 0,
- conflicted_staged_count: 0,
- current_modifiers: window.modifiers(),
- add_coauthors: true,
- generate_commit_message_task: None,
- entries: Vec::new(),
- focus_handle: cx.focus_handle(),
- fs,
- hide_scrollbar_task: None,
- new_count: 0,
- new_staged_count: 0,
- pending: Vec::new(),
- pending_commit: None,
- pending_serialization: Task::ready(None),
- project,
- scroll_handle,
- scrollbar_state,
- selected_entry: None,
- marked_entries: Vec::new(),
- show_scrollbar: false,
- tracked_count: 0,
- tracked_staged_count: 0,
- update_visible_entries_task: Task::ready(()),
- width: Some(px(360.)),
- context_menu: None,
- workspace,
- modal_open: false,
- };
- git_panel.schedule_update(false, window, cx);
- git_panel.show_scrollbar = git_panel.should_show_scrollbar(cx);
- git_panel
- })
+ let scrollbar_state =
+ ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity());
+
+ let mut git_panel = Self {
+ pending_remote_operations: Default::default(),
+ remote_operation_id: 0,
+ active_repository,
+ commit_editor,
+ conflicted_count: 0,
+ conflicted_staged_count: 0,
+ current_modifiers: window.modifiers(),
+ add_coauthors: true,
+ generate_commit_message_task: None,
+ entries: Vec::new(),
+ focus_handle: cx.focus_handle(),
+ fs,
+ hide_scrollbar_task: None,
+ new_count: 0,
+ new_staged_count: 0,
+ pending: Vec::new(),
+ pending_commit: None,
+ pending_serialization: Task::ready(None),
+ project,
+ scroll_handle,
+ scrollbar_state,
+ selected_entry: None,
+ marked_entries: Vec::new(),
+ show_scrollbar: false,
+ tracked_count: 0,
+ tracked_staged_count: 0,
+ update_visible_entries_task: Task::ready(()),
+ width: Some(px(360.)),
+ context_menu: None,
+ workspace,
+ modal_open: false,
+ };
+ git_panel.schedule_update(false, window, cx);
+ git_panel.show_scrollbar = git_panel.should_show_scrollbar(cx);
+ git_panel
}
pub fn entry_by_path(&self, path: &RepoPath) -> Option<usize> {
@@ -723,12 +739,31 @@ impl GitPanel {
}
};
- self.workspace
- .update(cx, |workspace, cx| {
- ProjectDiff::deploy_at(workspace, Some(entry.clone()), window, cx);
- })
- .ok();
- self.focus_handle.focus(window);
+ if entry.worktree_path.starts_with("..") {
+ self.workspace
+ .update(cx, |workspace, cx| {
+ workspace
+ .open_abs_path(
+ entry.abs_path.clone(),
+ OpenOptions {
+ visible: Some(OpenVisible::All),
+ focus: Some(false),
+ ..Default::default()
+ },
+ window,
+ cx,
+ )
+ .detach_and_log_err(cx);
+ })
+ .ok();
+ } else {
+ self.workspace
+ .update(cx, |workspace, cx| {
+ ProjectDiff::deploy_at(workspace, Some(entry.clone()), window, cx);
+ })
+ .ok();
+ self.focus_handle.focus(window);
+ }
Some(())
});
@@ -1683,7 +1718,7 @@ impl GitPanel {
&mut self,
window: &mut Window,
cx: &mut Context<Self>,
- ) -> impl Future<Output = Result<Option<Remote>>> {
+ ) -> impl Future<Output = anyhow::Result<Option<Remote>>> {
let repo = self.active_repository.clone();
let workspace = self.workspace.clone();
let mut cx = window.to_async(cx);
@@ -1920,10 +1955,8 @@ impl GitPanel {
return;
};
- // First pass - collect all paths
let repo = repo.read(cx);
- // Second pass - create entries with proper depth calculation
for entry in repo.status() {
let is_conflict = repo.has_conflict(&entry.repo_path);
let is_new = entry.status.is_created();
@@ -1937,8 +1970,17 @@ impl GitPanel {
continue;
}
+ // dot_git_abs path always has at least one component, namely .git.
+ let abs_path = repo
+ .dot_git_abs_path
+ .parent()
+ .unwrap()
+ .join(&entry.repo_path);
+ let worktree_path = repo.repository_entry.unrelativize(&entry.repo_path);
let entry = GitStatusEntry {
repo_path: entry.repo_path.clone(),
+ worktree_path,
+ abs_path,
status: entry.status,
is_staged,
};
@@ -2636,7 +2678,7 @@ impl GitPanel {
&self,
sha: &str,
cx: &mut Context<Self>,
- ) -> Task<Result<CommitDetails>> {
+ ) -> Task<anyhow::Result<CommitDetails>> {
let Some(repo) = self.active_repository.clone() else {
return Task::ready(Err(anyhow::anyhow!("no active repo")));
};
@@ -2721,12 +2763,12 @@ impl GitPanel {
cx: &Context<Self>,
) -> AnyElement {
let display_name = entry
- .repo_path
+ .worktree_path
.file_name()
.map(|name| name.to_string_lossy().into_owned())
- .unwrap_or_else(|| entry.repo_path.to_string_lossy().into_owned());
+ .unwrap_or_else(|| entry.worktree_path.to_string_lossy().into_owned());
- let repo_path = entry.repo_path.clone();
+ let worktree_path = entry.worktree_path.clone();
let selected = self.selected_entry == Some(ix);
let marked = self.marked_entries.contains(&ix);
let status_style = GitPanelSettings::get_global(cx).status_style;
@@ -2897,7 +2939,7 @@ impl GitPanel {
h_flex()
.items_center()
.overflow_hidden()
- .when_some(repo_path.parent(), |this, parent| {
+ .when_some(worktree_path.parent(), |this, parent| {
let parent_str = parent.to_string_lossy();
if !parent_str.is_empty() {
this.child(
@@ -3570,7 +3612,9 @@ impl RenderOnce for PanelRepoFooter {
let single_repo = project
.as_ref()
- .map(|project| project.read(cx).all_repositories(cx).len() == 1)
+ .map(|project| {
+ filtered_repository_entries(project.read(cx).git_store().read(cx), cx).len() == 1
+ })
.unwrap_or(true);
let repo_selector = PopoverMenu::new("repository-switcher")
@@ -3936,3 +3980,199 @@ impl ComponentPreview for PanelRepoFooter {
.into_any_element()
}
}
+
+#[cfg(test)]
+mod tests {
+ use git::status::StatusCode;
+ use gpui::TestAppContext;
+ use project::{FakeFs, WorktreeSettings};
+ use serde_json::json;
+ use settings::SettingsStore;
+ use theme::LoadThemes;
+ use util::path;
+
+ use super::*;
+
+ fn init_test(cx: &mut gpui::TestAppContext) {
+ if std::env::var("RUST_LOG").is_ok() {
+ env_logger::try_init().ok();
+ }
+
+ cx.update(|cx| {
+ let settings_store = SettingsStore::test(cx);
+ cx.set_global(settings_store);
+ WorktreeSettings::register(cx);
+ workspace::init_settings(cx);
+ theme::init(LoadThemes::JustBase, cx);
+ language::init(cx);
+ editor::init(cx);
+ Project::init_settings(cx);
+ crate::init(cx);
+ });
+ }
+
+ #[gpui::test]
+ async fn test_entry_worktree_paths(cx: &mut TestAppContext) {
+ init_test(cx);
+ let fs = FakeFs::new(cx.background_executor.clone());
+ fs.insert_tree(
+ "/root",
+ json!({
+ "zed": {
+ ".git": {},
+ "crates": {
+ "gpui": {
+ "gpui.rs": "fn main() {}"
+ },
+ "util": {
+ "util.rs": "fn do_it() {}"
+ }
+ }
+ },
+ }),
+ )
+ .await;
+
+ fs.set_status_for_repo_via_git_operation(
+ Path::new("/root/zed/.git"),
+ &[
+ (
+ Path::new("crates/gpui/gpui.rs"),
+ StatusCode::Modified.worktree(),
+ ),
+ (
+ Path::new("crates/util/util.rs"),
+ StatusCode::Modified.worktree(),
+ ),
+ ],
+ );
+
+ let project =
+ Project::test(fs.clone(), [path!("/root/zed/crates/gpui").as_ref()], cx).await;
+ let (workspace, cx) =
+ cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
+
+ cx.read(|cx| {
+ project
+ .read(cx)
+ .worktrees(cx)
+ .nth(0)
+ .unwrap()
+ .read(cx)
+ .as_local()
+ .unwrap()
+ .scan_complete()
+ })
+ .await;
+
+ cx.executor().run_until_parked();
+
+ let app_state = workspace.update(cx, |workspace, _| workspace.app_state().clone());
+ let panel = cx.new_window_entity(|window, cx| {
+ GitPanel::new(workspace.clone(), project.clone(), app_state, window, cx)
+ });
+
+ let handle = cx.update_window_entity(&panel, |panel, _, _| {
+ std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(()))
+ });
+ cx.executor().advance_clock(2 * UPDATE_DEBOUNCE);
+ handle.await;
+
+ let entries = panel.update(cx, |panel, _| panel.entries.clone());
+ pretty_assertions::assert_eq!(
+ entries,
+ [
+ GitListEntry::Header(GitHeaderEntry {
+ header: Section::Tracked
+ }),
+ GitListEntry::GitStatusEntry(GitStatusEntry {
+ abs_path: "/root/zed/crates/gpui/gpui.rs".into(),
+ repo_path: "crates/gpui/gpui.rs".into(),
+ worktree_path: Path::new("gpui.rs").into(),
+ status: StatusCode::Modified.worktree(),
+ is_staged: Some(false),
+ }),
+ GitListEntry::GitStatusEntry(GitStatusEntry {
+ abs_path: "/root/zed/crates/util/util.rs".into(),
+ repo_path: "crates/util/util.rs".into(),
+ worktree_path: Path::new("../util/util.rs").into(),
+ status: StatusCode::Modified.worktree(),
+ is_staged: Some(false),
+ },),
+ ],
+ );
+
+ cx.update_window_entity(&panel, |panel, window, cx| {
+ panel.select_last(&Default::default(), window, cx);
+ assert_eq!(panel.selected_entry, Some(2));
+ panel.open_diff(&Default::default(), window, cx);
+ });
+ cx.run_until_parked();
+
+ let worktree_roots = workspace.update(cx, |workspace, cx| {
+ workspace
+ .worktrees(cx)
+ .map(|worktree| worktree.read(cx).abs_path())
+ .collect::<Vec<_>>()
+ });
+ pretty_assertions::assert_eq!(
+ worktree_roots,
+ vec![
+ Path::new("/root/zed/crates/gpui").into(),
+ Path::new("/root/zed/crates/util/util.rs").into(),
+ ]
+ );
+
+ let repo_from_single_file_worktree = project.update(cx, |project, cx| {
+ let git_store = project.git_store().read(cx);
+ // The repo that comes from the single-file worktree can't be selected through the UI.
+ let filtered_entries = filtered_repository_entries(git_store, cx)
+ .iter()
+ .map(|repo| repo.read(cx).worktree_abs_path.clone())
+ .collect::<Vec<_>>();
+ assert_eq!(
+ filtered_entries,
+ [Path::new("/root/zed/crates/gpui").into()]
+ );
+ // But we can select it artificially here.
+ git_store
+ .all_repositories()
+ .into_iter()
+ .find(|repo| {
+ &*repo.read(cx).worktree_abs_path == Path::new("/root/zed/crates/util/util.rs")
+ })
+ .unwrap()
+ });
+
+ // Paths still make sense when we somehow activate a repo that comes from a single-file worktree.
+ repo_from_single_file_worktree.update(cx, |repo, cx| repo.activate(cx));
+ let handle = cx.update_window_entity(&panel, |panel, _, _| {
+ std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(()))
+ });
+ cx.executor().advance_clock(2 * UPDATE_DEBOUNCE);
+ handle.await;
+ let entries = panel.update(cx, |panel, _| panel.entries.clone());
+ pretty_assertions::assert_eq!(
+ entries,
+ [
+ GitListEntry::Header(GitHeaderEntry {
+ header: Section::Tracked
+ }),
+ GitListEntry::GitStatusEntry(GitStatusEntry {
+ abs_path: "/root/zed/crates/gpui/gpui.rs".into(),
+ repo_path: "crates/gpui/gpui.rs".into(),
+ worktree_path: Path::new("../../gpui/gpui.rs").into(),
+ status: StatusCode::Modified.worktree(),
+ is_staged: Some(false),
+ }),
+ GitListEntry::GitStatusEntry(GitStatusEntry {
+ abs_path: "/root/zed/crates/util/util.rs".into(),
+ repo_path: "crates/util/util.rs".into(),
+ worktree_path: Path::new("util.rs").into(),
+ status: StatusCode::Modified.worktree(),
+ is_staged: Some(false),
+ },),
+ ],
+ );
+ }
+}
@@ -3,7 +3,10 @@ use gpui::{
};
use itertools::Itertools;
use picker::{Picker, PickerDelegate};
-use project::{git::Repository, Project};
+use project::{
+ git::{GitStore, Repository},
+ Project,
+};
use std::sync::Arc;
use ui::{prelude::*, ListItem, ListItemSpacing};
@@ -17,12 +20,14 @@ impl RepositorySelector {
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
+ let git_store = project_handle.read(cx).git_store().clone();
+ let repository_entries = git_store.update(cx, |git_store, cx| {
+ filtered_repository_entries(git_store, cx)
+ });
let project = project_handle.read(cx);
- let git_store = project.git_store().clone();
- let all_repositories = git_store.read(cx).all_repositories();
- let filtered_repositories = all_repositories.clone();
+ let filtered_repositories = repository_entries.clone();
- let widest_item_ix = all_repositories.iter().position_max_by(|a, b| {
+ let widest_item_ix = repository_entries.iter().position_max_by(|a, b| {
a.read(cx)
.display_name(project, cx)
.len()
@@ -32,7 +37,7 @@ impl RepositorySelector {
let delegate = RepositorySelectorDelegate {
project: project_handle.downgrade(),
repository_selector: cx.entity().downgrade(),
- repository_entries: all_repositories.clone(),
+ repository_entries,
filtered_repositories,
selected_index: 0,
};
@@ -47,6 +52,35 @@ impl RepositorySelector {
}
}
+pub(crate) fn filtered_repository_entries(
+ git_store: &GitStore,
+ cx: &App,
+) -> Vec<Entity<Repository>> {
+ let mut repository_entries = git_store.all_repositories();
+ repository_entries.sort_by_key(|repo| {
+ let repo = repo.read(cx);
+ (
+ repo.dot_git_abs_path.clone(),
+ repo.worktree_abs_path.clone(),
+ )
+ });
+ // Remove any entry that comes from a single file worktree and represents a repository that is also represented by a non-single-file worktree.
+ repository_entries
+ .chunk_by(|a, b| a.read(cx).dot_git_abs_path == b.read(cx).dot_git_abs_path)
+ .flat_map(|chunk| {
+ let has_non_single_file_worktree = chunk
+ .iter()
+ .any(|repo| !repo.read(cx).is_from_single_file_worktree);
+ chunk
+ .iter()
+ .filter(move |repo| {
+ !repo.read(cx).is_from_single_file_worktree || !has_non_single_file_worktree
+ })
+ .cloned()
+ })
+ .collect()
+}
+
impl EventEmitter<DismissEvent> for RepositorySelector {}
impl Focusable for RepositorySelector {
@@ -133,13 +133,31 @@ pub fn new_journal_entry(workspace: &Workspace, window: &mut Window, cx: &mut Ap
.await?;
new_workspace
.update(&mut cx, |workspace, window, cx| {
- workspace.open_paths(vec![entry_path], OpenVisible::All, None, window, cx)
+ workspace.open_paths(
+ vec![entry_path],
+ workspace::OpenOptions {
+ visible: Some(OpenVisible::All),
+ ..Default::default()
+ },
+ None,
+ window,
+ cx,
+ )
})?
.await
} else {
view_snapshot
.update_in(&mut cx, |workspace, window, cx| {
- workspace.open_paths(vec![entry_path], OpenVisible::All, None, window, cx)
+ workspace.open_paths(
+ vec![entry_path],
+ workspace::OpenOptions {
+ visible: Some(OpenVisible::All),
+ ..Default::default()
+ },
+ None,
+ window,
+ cx,
+ )
})?
.await
};
@@ -23,7 +23,7 @@ use ui::{
LabelSize, LinkPreview, StatefulInteractiveElement, StyledExt, StyledImage, ToggleState,
Tooltip, VisibleOnHover,
};
-use workspace::Workspace;
+use workspace::{OpenOptions, OpenVisible, Workspace};
type CheckboxClickedCallback = Arc<Box<dyn Fn(bool, Range<usize>, &mut Window, &mut App)>>;
@@ -490,7 +490,15 @@ fn render_markdown_text(parsed_new: &MarkdownParagraph, cx: &mut RenderContext)
if let Some(workspace) = &workspace {
_ = workspace.update(cx, |workspace, cx| {
workspace
- .open_abs_path(path.clone(), false, window, cx)
+ .open_abs_path(
+ path.clone(),
+ OpenOptions {
+ visible: Some(OpenVisible::None),
+ ..Default::default()
+ },
+ window,
+ cx,
+ )
.detach();
});
}
@@ -545,7 +553,15 @@ fn render_markdown_text(parsed_new: &MarkdownParagraph, cx: &mut RenderContext)
if let Some(workspace) = &workspace {
_ = workspace.update(cx, |workspace, cx| {
workspace
- .open_abs_path(path.clone(), false, window, cx)
+ .open_abs_path(
+ path.clone(),
+ OpenOptions {
+ visible: Some(OpenVisible::None),
+ ..Default::default()
+ },
+ window,
+ cx,
+ )
.detach();
});
}
@@ -5169,7 +5169,7 @@ mod tests {
use search::project_search::{self, perform_project_search};
use serde_json::json;
use util::path;
- use workspace::OpenVisible;
+ use workspace::{OpenOptions, OpenVisible};
use super::*;
@@ -5780,7 +5780,10 @@ mod tests {
.update(cx, |workspace, window, cx| {
workspace.open_paths(
vec![PathBuf::from("/root/two")],
- OpenVisible::OnlyDirectories,
+ OpenOptions {
+ visible: Some(OpenVisible::OnlyDirectories),
+ ..Default::default()
+ },
None,
window,
cx,
@@ -5971,7 +5974,15 @@ struct OutlineEntryExcerpt {
let _editor = workspace
.update(cx, |workspace, window, cx| {
- workspace.open_abs_path(PathBuf::from(path!("/root/src/lib.rs")), true, window, cx)
+ workspace.open_abs_path(
+ PathBuf::from(path!("/root/src/lib.rs")),
+ OpenOptions {
+ visible: Some(OpenVisible::All),
+ ..Default::default()
+ },
+ window,
+ cx,
+ )
})
.unwrap()
.await
@@ -56,6 +56,9 @@ pub struct Repository {
git_store: WeakEntity<GitStore>,
pub worktree_id: WorktreeId,
pub repository_entry: RepositoryEntry,
+ pub dot_git_abs_path: PathBuf,
+ pub worktree_abs_path: Arc<Path>,
+ pub is_from_single_file_worktree: bool,
pub git_repo: GitRepo,
pub merge_message: Option<String>,
job_sender: mpsc::UnboundedSender<GitJob>,
@@ -227,6 +230,9 @@ impl GitStore {
askpass_delegates: Default::default(),
latest_askpass_id: 0,
repository_entry: repo.clone(),
+ dot_git_abs_path: worktree.dot_git_abs_path(&repo.work_directory),
+ worktree_abs_path: worktree.abs_path(),
+ is_from_single_file_worktree: worktree.is_single_file(),
git_repo,
job_sender: self.update_sender.clone(),
merge_message,
@@ -979,7 +985,7 @@ impl Repository {
}
pub fn repo_path_to_project_path(&self, path: &RepoPath) -> Option<ProjectPath> {
- let path = self.repository_entry.unrelativize(path)?;
+ let path = self.repository_entry.try_unrelativize(path)?;
Some((self.worktree_id, path).into())
}
@@ -1218,7 +1224,7 @@ impl Repository {
if let Some(buffer_store) = self.buffer_store(cx) {
buffer_store.update(cx, |buffer_store, cx| {
for path in &entries {
- let Some(path) = self.repository_entry.unrelativize(path) else {
+ let Some(path) = self.repository_entry.try_unrelativize(path) else {
continue;
};
let project_path = (self.worktree_id, path).into();
@@ -1287,7 +1293,7 @@ impl Repository {
if let Some(buffer_store) = self.buffer_store(cx) {
buffer_store.update(cx, |buffer_store, cx| {
for path in &entries {
- let Some(path) = self.repository_entry.unrelativize(path) else {
+ let Some(path) = self.repository_entry.try_unrelativize(path) else {
continue;
};
let project_path = (self.worktree_id, path).into();
@@ -59,7 +59,8 @@ use util::{maybe, paths::compare_paths, ResultExt, TakeUntilExt, TryFutureExt};
use workspace::{
dock::{DockPosition, Panel, PanelEvent},
notifications::{DetachAndPromptErr, NotifyTaskExt},
- DraggedSelection, OpenInTerminal, PreviewTabsSettings, SelectedEntry, Workspace,
+ DraggedSelection, OpenInTerminal, OpenOptions, OpenVisible, PreviewTabsSettings, SelectedEntry,
+ Workspace,
};
use worktree::{CreatedEntry, GitEntry, GitEntryRef};
@@ -1211,7 +1212,7 @@ impl ProjectPanel {
project_panel
.workspace
.update(cx, |workspace, cx| {
- workspace.open_abs_path(abs_path, true, window, cx)
+ workspace.open_abs_path(abs_path, OpenOptions { visible: Some(OpenVisible::All), ..Default::default() }, window, cx)
})
.ok()
}
@@ -9,7 +9,7 @@ use picker::{Picker, PickerDelegate};
use std::{borrow::Borrow, fs, sync::Arc};
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
use util::ResultExt;
-use workspace::{notifications::NotifyResultExt, ModalView, Workspace};
+use workspace::{notifications::NotifyResultExt, ModalView, OpenOptions, OpenVisible, Workspace};
actions!(snippets, [ConfigureSnippets, OpenFolder]);
@@ -144,7 +144,10 @@ impl PickerDelegate for ScopeSelectorDelegate {
workspace
.open_abs_path(
config_dir().join("snippets").join(scope + ".json"),
- false,
+ OpenOptions {
+ visible: Some(OpenVisible::None),
+ ..Default::default()
+ },
window,
cx,
)
@@ -602,7 +602,7 @@ mod tests {
use serde_json::json;
use task::TaskTemplates;
use util::path;
- use workspace::CloseInactiveTabsAndPanes;
+ use workspace::{CloseInactiveTabsAndPanes, OpenOptions, OpenVisible};
use crate::{modal::Spawn, tests::init_test};
@@ -653,7 +653,15 @@ mod tests {
let _ = workspace
.update_in(cx, |workspace, window, cx| {
- workspace.open_abs_path(PathBuf::from(path!("/dir/a.ts")), true, window, cx)
+ workspace.open_abs_path(
+ PathBuf::from(path!("/dir/a.ts")),
+ OpenOptions {
+ visible: Some(OpenVisible::All),
+ ..Default::default()
+ },
+ window,
+ cx,
+ )
})
.await
.unwrap();
@@ -819,7 +827,10 @@ mod tests {
.update_in(cx, |workspace, window, cx| {
workspace.open_abs_path(
PathBuf::from(path!("/dir/file_with.odd_extension")),
- true,
+ OpenOptions {
+ visible: Some(OpenVisible::All),
+ ..Default::default()
+ },
window,
cx,
)
@@ -846,7 +857,10 @@ mod tests {
.update_in(cx, |workspace, window, cx| {
workspace.open_abs_path(
PathBuf::from(path!("/dir/file_without_extension")),
- true,
+ OpenOptions {
+ visible: Some(OpenVisible::All),
+ ..Default::default()
+ },
window,
cx,
)
@@ -954,7 +968,15 @@ mod tests {
let _ts_file_1 = workspace
.update_in(cx, |workspace, window, cx| {
- workspace.open_abs_path(PathBuf::from(path!("/dir/a1.ts")), true, window, cx)
+ workspace.open_abs_path(
+ PathBuf::from(path!("/dir/a1.ts")),
+ OpenOptions {
+ visible: Some(OpenVisible::All),
+ ..Default::default()
+ },
+ window,
+ cx,
+ )
})
.await
.unwrap();
@@ -995,7 +1017,15 @@ mod tests {
let _ts_file_2 = workspace
.update_in(cx, |workspace, window, cx| {
- workspace.open_abs_path(PathBuf::from(path!("/dir/a2.ts")), true, window, cx)
+ workspace.open_abs_path(
+ PathBuf::from(path!("/dir/a2.ts")),
+ OpenOptions {
+ visible: Some(OpenVisible::All),
+ ..Default::default()
+ },
+ window,
+ cx,
+ )
})
.await
.unwrap();
@@ -1018,7 +1048,15 @@ mod tests {
let _rs_file = workspace
.update_in(cx, |workspace, window, cx| {
- workspace.open_abs_path(PathBuf::from(path!("/dir/b.rs")), true, window, cx)
+ workspace.open_abs_path(
+ PathBuf::from(path!("/dir/b.rs")),
+ OpenOptions {
+ visible: Some(OpenVisible::All),
+ ..Default::default()
+ },
+ window,
+ cx,
+ )
})
.await
.unwrap();
@@ -1033,7 +1071,15 @@ mod tests {
emulate_task_schedule(tasks_picker, &project, "Rust task", cx);
let _ts_file_2 = workspace
.update_in(cx, |workspace, window, cx| {
- workspace.open_abs_path(PathBuf::from(path!("/dir/a2.ts")), true, window, cx)
+ workspace.open_abs_path(
+ PathBuf::from(path!("/dir/a2.ts")),
+ OpenOptions {
+ visible: Some(OpenVisible::All),
+ ..Default::default()
+ },
+ window,
+ cx,
+ )
})
.await
.unwrap();
@@ -38,8 +38,8 @@ use workspace::{
},
register_serializable_item,
searchable::{Direction, SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle},
- CloseActiveItem, NewCenterTerminal, NewTerminal, OpenVisible, ToolbarItemLocation, Workspace,
- WorkspaceId,
+ CloseActiveItem, NewCenterTerminal, NewTerminal, OpenOptions, OpenVisible, ToolbarItemLocation,
+ Workspace, WorkspaceId,
};
use anyhow::Context as _;
@@ -910,7 +910,10 @@ fn subscribe_for_terminal_events(
.update_in(&mut cx, |workspace, window, cx| {
workspace.open_paths(
vec![path_to_open.path.clone()],
- OpenVisible::OnlyDirectories,
+ OpenOptions {
+ visible: Some(OpenVisible::OnlyDirectories),
+ ..Default::default()
+ },
None,
window,
cx,
@@ -828,6 +828,10 @@ pub fn word_consists_of_emojis(s: &str) -> bool {
prev_end == s.len()
}
+pub fn default<D: Default>() -> D {
+ Default::default()
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -7,8 +7,8 @@ use crate::{
notifications::NotifyResultExt,
toolbar::Toolbar,
workspace_settings::{AutosaveSetting, TabBarSettings, WorkspaceSettings},
- CloseWindow, NewFile, NewTerminal, OpenInTerminal, OpenTerminal, OpenVisible, SplitDirection,
- ToggleFileFinder, ToggleProjectSymbols, ToggleZoom, Workspace,
+ CloseWindow, NewFile, NewTerminal, OpenInTerminal, OpenOptions, OpenTerminal, OpenVisible,
+ SplitDirection, ToggleFileFinder, ToggleProjectSymbols, ToggleZoom, Workspace,
};
use anyhow::Result;
use collections::{BTreeSet, HashMap, HashSet, VecDeque};
@@ -3086,7 +3086,10 @@ impl Pane {
}
workspace.open_paths(
paths,
- OpenVisible::OnlyDirectories,
+ OpenOptions {
+ visible: Some(OpenVisible::OnlyDirectories),
+ ..Default::default()
+ },
Some(to_pane.downgrade()),
window,
cx,
@@ -1534,7 +1534,7 @@ impl Workspace {
pane.active_item().map(|p| p.item_id())
})?;
let open_by_abs_path = workspace.update_in(&mut cx, |workspace, window, cx| {
- workspace.open_abs_path(abs_path.clone(), false, window, cx)
+ workspace.open_abs_path(abs_path.clone(), OpenOptions { visible: Some(OpenVisible::None), ..Default::default() }, window, cx)
})?;
match open_by_abs_path
.await
@@ -2112,7 +2112,7 @@ impl Workspace {
pub fn open_paths(
&mut self,
mut abs_paths: Vec<PathBuf>,
- visible: OpenVisible,
+ options: OpenOptions,
pane: Option<WeakEntity<Pane>>,
window: &mut Window,
cx: &mut Context<Self>,
@@ -2127,7 +2127,7 @@ impl Workspace {
let mut tasks = Vec::with_capacity(abs_paths.len());
for abs_path in &abs_paths {
- let visible = match visible {
+ let visible = match options.visible.as_ref().unwrap_or(&OpenVisible::None) {
OpenVisible::All => Some(true),
OpenVisible::None => Some(false),
OpenVisible::OnlyFiles => match fs.metadata(abs_path).await.log_err() {
@@ -2191,7 +2191,13 @@ impl Workspace {
} else {
Some(
this.update_in(&mut cx, |this, window, cx| {
- this.open_path(project_path, pane, true, window, cx)
+ this.open_path(
+ project_path,
+ pane,
+ options.focus.unwrap_or(true),
+ window,
+ cx,
+ )
})
.log_err()?
.await,
@@ -2215,7 +2221,15 @@ impl Workspace {
ResolvedPath::ProjectPath { project_path, .. } => {
self.open_path(project_path, None, true, window, cx)
}
- ResolvedPath::AbsPath { path, .. } => self.open_abs_path(path, false, window, cx),
+ ResolvedPath::AbsPath { path, .. } => self.open_abs_path(
+ path,
+ OpenOptions {
+ visible: Some(OpenVisible::None),
+ ..Default::default()
+ },
+ window,
+ cx,
+ ),
}
}
@@ -2259,7 +2273,16 @@ impl Workspace {
if let Some(paths) = paths.await.log_err().flatten() {
let results = this
.update_in(&mut cx, |this, window, cx| {
- this.open_paths(paths, OpenVisible::All, None, window, cx)
+ this.open_paths(
+ paths,
+ OpenOptions {
+ visible: Some(OpenVisible::All),
+ ..Default::default()
+ },
+ None,
+ window,
+ cx,
+ )
})?
.await;
for result in results.into_iter().flatten() {
@@ -2752,24 +2775,14 @@ impl Workspace {
pub fn open_abs_path(
&mut self,
abs_path: PathBuf,
- visible: bool,
+ options: OpenOptions,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
cx.spawn_in(window, |workspace, mut cx| async move {
let open_paths_task_result = workspace
.update_in(&mut cx, |workspace, window, cx| {
- workspace.open_paths(
- vec![abs_path.clone()],
- if visible {
- OpenVisible::All
- } else {
- OpenVisible::None
- },
- None,
- window,
- cx,
- )
+ workspace.open_paths(vec![abs_path.clone()], options, None, window, cx)
})
.with_context(|| format!("open abs path {abs_path:?} task spawn"))?
.await;
@@ -6002,10 +6015,13 @@ pub fn local_workspace_windows(cx: &App) -> Vec<WindowHandle<Workspace>> {
#[derive(Default)]
pub struct OpenOptions {
+ pub visible: Option<OpenVisible>,
+ pub focus: Option<bool>,
pub open_new_workspace: Option<bool>,
pub replace_window: Option<WindowHandle<Workspace>>,
pub env: Option<HashMap<String, String>>,
}
+
#[allow(clippy::type_complexity)]
pub fn open_paths(
abs_paths: &[PathBuf],
@@ -6089,7 +6105,16 @@ pub fn open_paths(
let open_task = existing
.update(&mut cx, |workspace, window, cx| {
window.activate_window();
- workspace.open_paths(abs_paths, open_visible, None, window, cx)
+ workspace.open_paths(
+ abs_paths,
+ OpenOptions {
+ visible: Some(open_visible),
+ ..Default::default()
+ },
+ None,
+ window,
+ cx,
+ )
})?
.await;
@@ -6154,7 +6179,10 @@ pub fn create_and_open_local_file(
workspace.with_local_workspace(window, cx, |workspace, window, cx| {
workspace.open_paths(
vec![path.to_path_buf()],
- OpenVisible::None,
+ OpenOptions {
+ visible: Some(OpenVisible::None),
+ ..Default::default()
+ },
None,
window,
cx,
@@ -58,7 +58,7 @@ use std::{
future::Future,
mem::{self},
ops::{Deref, DerefMut},
- path::{Path, PathBuf},
+ path::{Component, Path, PathBuf},
pin::Pin,
sync::{
atomic::{self, AtomicU32, AtomicUsize, Ordering::SeqCst},
@@ -212,7 +212,11 @@ impl RepositoryEntry {
self.work_directory.relativize(path)
}
- pub fn unrelativize(&self, path: &RepoPath) -> Option<Arc<Path>> {
+ pub fn try_unrelativize(&self, path: &RepoPath) -> Option<Arc<Path>> {
+ self.work_directory.try_unrelativize(path)
+ }
+
+ pub fn unrelativize(&self, path: &RepoPath) -> Arc<Path> {
self.work_directory.unrelativize(path)
}
@@ -491,7 +495,7 @@ impl WorkDirectory {
}
/// This is the opposite operation to `relativize` above
- pub fn unrelativize(&self, path: &RepoPath) -> Option<Arc<Path>> {
+ pub fn try_unrelativize(&self, path: &RepoPath) -> Option<Arc<Path>> {
match self {
WorkDirectory::InProject { relative_path } => Some(relative_path.join(path).into()),
WorkDirectory::AboveProject {
@@ -504,6 +508,33 @@ impl WorkDirectory {
}
}
+ pub fn unrelativize(&self, path: &RepoPath) -> Arc<Path> {
+ match self {
+ WorkDirectory::InProject { relative_path } => relative_path.join(path).into(),
+ WorkDirectory::AboveProject {
+ location_in_repo, ..
+ } => {
+ if &path.0 == location_in_repo {
+ // Single-file worktree
+ return location_in_repo
+ .file_name()
+ .map(Path::new)
+ .unwrap_or(Path::new(""))
+ .into();
+ }
+ let mut location_in_repo = &**location_in_repo;
+ let mut parents = PathBuf::new();
+ loop {
+ if let Ok(segment) = path.strip_prefix(location_in_repo) {
+ return parents.join(segment).into();
+ }
+ location_in_repo = location_in_repo.parent().unwrap_or(Path::new(""));
+ parents.push(Component::ParentDir);
+ }
+ }
+ }
+ }
+
pub fn display_name(&self) -> String {
match self {
WorkDirectory::InProject { relative_path } => relative_path.display().to_string(),
@@ -1422,6 +1453,19 @@ impl Worktree {
worktree_scan_id: scan_id as u64,
})
}
+
+ pub fn dot_git_abs_path(&self, work_directory: &WorkDirectory) -> PathBuf {
+ let mut path = match work_directory {
+ WorkDirectory::InProject { relative_path } => self.abs_path().join(relative_path),
+ WorkDirectory::AboveProject { absolute_path, .. } => absolute_path.as_ref().to_owned(),
+ };
+ path.push(".git");
+ path
+ }
+
+ pub fn is_single_file(&self) -> bool {
+ self.root_dir().is_none()
+ }
}
impl LocalWorktree {
@@ -5509,7 +5553,7 @@ impl BackgroundScanner {
let mut new_entries_by_path = SumTree::new(&());
for (repo_path, status) in statuses.entries.iter() {
- let project_path = repository.work_directory.unrelativize(repo_path);
+ let project_path = repository.work_directory.try_unrelativize(repo_path);
new_entries_by_path.insert_or_replace(
StatusEntry {
@@ -3412,6 +3412,43 @@ async fn test_private_single_file_worktree(cx: &mut TestAppContext) {
});
}
+#[gpui::test]
+fn test_unrelativize() {
+ let work_directory = WorkDirectory::in_project("");
+ pretty_assertions::assert_eq!(
+ work_directory.try_unrelativize(&"crates/gpui/gpui.rs".into()),
+ Some(Path::new("crates/gpui/gpui.rs").into())
+ );
+
+ let work_directory = WorkDirectory::in_project("vendor/some-submodule");
+ pretty_assertions::assert_eq!(
+ work_directory.try_unrelativize(&"src/thing.c".into()),
+ Some(Path::new("vendor/some-submodule/src/thing.c").into())
+ );
+
+ let work_directory = WorkDirectory::AboveProject {
+ absolute_path: Path::new("/projects/zed").into(),
+ location_in_repo: Path::new("crates/gpui").into(),
+ };
+
+ pretty_assertions::assert_eq!(
+ work_directory.try_unrelativize(&"crates/util/util.rs".into()),
+ None,
+ );
+
+ pretty_assertions::assert_eq!(
+ work_directory.unrelativize(&"crates/util/util.rs".into()),
+ Path::new("../util/util.rs").into()
+ );
+
+ pretty_assertions::assert_eq!(work_directory.try_unrelativize(&"README.md".into()), None,);
+
+ pretty_assertions::assert_eq!(
+ work_directory.unrelativize(&"README.md".into()),
+ Path::new("../../README.md").into()
+ );
+}
+
#[track_caller]
fn check_git_statuses(snapshot: &Snapshot, expected_statuses: &[(&Path, GitSummary)]) {
let mut traversal = snapshot
@@ -22,6 +22,7 @@ use editor::ProposedChangesEditorToolbar;
use editor::{scroll::Autoscroll, Editor, MultiBuffer};
use feature_flags::{FeatureFlagAppExt, FeatureFlagViewExt, GitUiFeatureFlag};
use futures::{channel::mpsc, select_biased, StreamExt};
+use git_ui::git_panel::GitPanel;
use git_ui::project_diff::ProjectDiffToolbar;
use gpui::{
actions, point, px, Action, App, AppContext as _, AsyncApp, Context, DismissEvent, Element,
@@ -429,7 +430,10 @@ fn initialize_panels(
workspace.add_panel(chat_panel, window, cx);
workspace.add_panel(notification_panel, window, cx);
cx.when_flag_enabled::<GitUiFeatureFlag>(window, |workspace, window, cx| {
- let git_panel = git_ui::git_panel::GitPanel::new(workspace, window, cx);
+ let entity = cx.entity();
+ let project = workspace.project().clone();
+ let app_state = workspace.app_state().clone();
+ let git_panel = cx.new(|cx| GitPanel::new(entity, project, app_state, window, cx));
workspace.add_panel(git_panel, window, cx);
});
})?;
@@ -1479,8 +1483,7 @@ pub fn open_new_ssh_project_from_project(
app_state,
workspace::OpenOptions {
open_new_workspace: Some(true),
- replace_window: None,
- env: None,
+ ..Default::default()
},
&mut cx,
)
@@ -1749,7 +1752,7 @@ mod tests {
use util::{path, separator};
use workspace::{
item::{Item, ItemHandle},
- open_new, open_paths, pane, NewFile, OpenVisible, SaveIntent, SplitDirection,
+ open_new, open_paths, pane, NewFile, OpenOptions, OpenVisible, SaveIntent, SplitDirection,
WorkspaceHandle, SERIALIZATION_THROTTLE_TIME,
};
@@ -2552,7 +2555,10 @@ mod tests {
.update(cx, |workspace, window, cx| {
workspace.open_paths(
vec![path!("/dir1/a.txt").into()],
- OpenVisible::All,
+ OpenOptions {
+ visible: Some(OpenVisible::All),
+ ..Default::default()
+ },
None,
window,
cx,
@@ -2587,7 +2593,10 @@ mod tests {
.update(cx, |workspace, window, cx| {
workspace.open_paths(
vec![path!("/dir2/b.txt").into()],
- OpenVisible::All,
+ OpenOptions {
+ visible: Some(OpenVisible::All),
+ ..Default::default()
+ },
None,
window,
cx,
@@ -2633,7 +2642,10 @@ mod tests {
.update(cx, |workspace, window, cx| {
workspace.open_paths(
vec![path!("/dir3").into(), path!("/dir3/c.txt").into()],
- OpenVisible::All,
+ OpenOptions {
+ visible: Some(OpenVisible::All),
+ ..Default::default()
+ },
None,
window,
cx,
@@ -2679,7 +2691,10 @@ mod tests {
.update(cx, |workspace, window, cx| {
workspace.open_paths(
vec![path!("/d.txt").into()],
- OpenVisible::None,
+ OpenOptions {
+ visible: Some(OpenVisible::None),
+ ..Default::default()
+ },
None,
window,
cx,
@@ -2889,7 +2904,10 @@ mod tests {
.update(cx, |workspace, window, cx| {
workspace.open_paths(
vec![PathBuf::from(path!("/root/a.txt"))],
- OpenVisible::All,
+ OpenOptions {
+ visible: Some(OpenVisible::All),
+ ..Default::default()
+ },
None,
window,
cx,