diff --git a/Cargo.lock b/Cargo.lock index 7da4fe7cc2..86347b7b20 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5466,6 +5466,7 @@ dependencies = [ "panel", "picker", "postage", + "pretty_assertions", "project", "schemars", "serde", diff --git a/crates/editor/src/clangd_ext.rs b/crates/editor/src/clangd_ext.rs index 4a145f0d33..4d4ef2a5ed 100644 --- a/crates/editor/src/clangd_ext.rs +++ b/crates/editor/src/clangd_ext.rs @@ -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!( diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index d9b57b030e..465cd307c7 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -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::().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?; diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index aa27905f1e..51653503fb 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -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::().with_context(|| format!("Failed to downcast to Editor after opening abs path {abs_path:?}"))?; diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index ecabc209bd..0df1c4d0c7 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -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, ) diff --git a/crates/file_finder/src/file_finder_tests.rs b/crates/file_finder/src/file_finder_tests.rs index 13c226ded2..a42270202b 100644 --- a/crates/file_finder/src/file_finder_tests.rs +++ b/crates/file_finder/src/file_finder_tests.rs @@ -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, ) diff --git a/crates/git_ui/Cargo.toml b/crates/git_ui/Cargo.toml index 56c3bdc9e4..43e2f030ec 100644 --- a/crates/git_ui/Cargo.toml +++ b/crates/git_ui/Cargo.toml @@ -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 diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 2e3f73949e..9cae18167d 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -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(msg: &str, detail: Option<&str>, window: &mut Window, cx: &mut App) -> Task> +fn prompt( + msg: &str, + detail: Option<&str>, + window: &mut Window, + cx: &mut App, +) -> Task> 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, + pub(crate) abs_path: PathBuf, pub(crate) status: FileStatus, pub(crate) is_staged: Option, } @@ -269,99 +286,98 @@ pub(crate) fn commit_message_editor( impl GitPanel { pub fn new( - workspace: &mut Workspace, + workspace: Entity, + project: Entity, + app_state: Arc, window: &mut Window, - cx: &mut Context, - ) -> Entity { - let fs = workspace.app_state().fs.clone(); - let project = workspace.project().clone(); + cx: &mut Context, + ) -> 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(); - - // 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); - }); - - 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(); - - 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 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) + }); + + commit_editor.update(cx, |editor, cx| { + editor.clear(window, cx); + }); + + 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(); + + 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 { @@ -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, - ) -> impl Future>> { + ) -> impl Future>> { 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, - ) -> Task> { + ) -> Task> { 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, ) -> 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::>() + }); + 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::>(); + 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), + },), + ], + ); + } +} diff --git a/crates/git_ui/src/repository_selector.rs b/crates/git_ui/src/repository_selector.rs index f0755f968e..3e8947b17b 100644 --- a/crates/git_ui/src/repository_selector.rs +++ b/crates/git_ui/src/repository_selector.rs @@ -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 { + 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> { + 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 for RepositorySelector {} impl Focusable for RepositorySelector { diff --git a/crates/journal/src/journal.rs b/crates/journal/src/journal.rs index 6afcced5f6..af46609ef4 100644 --- a/crates/journal/src/journal.rs +++ b/crates/journal/src/journal.rs @@ -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 }; diff --git a/crates/markdown_preview/src/markdown_renderer.rs b/crates/markdown_preview/src/markdown_renderer.rs index 00dbf033d1..d235b14129 100644 --- a/crates/markdown_preview/src/markdown_renderer.rs +++ b/crates/markdown_preview/src/markdown_renderer.rs @@ -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, &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(); }); } diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 58cf5db5ed..3e1d39564a 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -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 diff --git a/crates/project/src/git.rs b/crates/project/src/git.rs index a67ad39047..bc5b3a6774 100644 --- a/crates/project/src/git.rs +++ b/crates/project/src/git.rs @@ -56,6 +56,9 @@ pub struct Repository { git_store: WeakEntity, pub worktree_id: WorktreeId, pub repository_entry: RepositoryEntry, + pub dot_git_abs_path: PathBuf, + pub worktree_abs_path: Arc, + pub is_from_single_file_worktree: bool, pub git_repo: GitRepo, pub merge_message: Option, job_sender: mpsc::UnboundedSender, @@ -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 { - 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(); diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index bdcf4281f1..29f8f6459e 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -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() } diff --git a/crates/snippets_ui/src/snippets_ui.rs b/crates/snippets_ui/src/snippets_ui.rs index 4d86e615ce..fbab1dd4b2 100644 --- a/crates/snippets_ui/src/snippets_ui.rs +++ b/crates/snippets_ui/src/snippets_ui.rs @@ -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, ) diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index 237bddb448..c0dc456db1 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -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(); diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 027ceb4307..7db75af327 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -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, diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index bda78e9ddc..ece84bb46b 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -828,6 +828,10 @@ pub fn word_consists_of_emojis(s: &str) -> bool { prev_end == s.len() } +pub fn default() -> D { + Default::default() +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index d579624378..c865840634 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -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, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index d867b1f215..779c730d9b 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -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, - visible: OpenVisible, + options: OpenOptions, pane: Option>, window: &mut Window, cx: &mut Context, @@ -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, ) -> Task>> { 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> { #[derive(Default)] pub struct OpenOptions { + pub visible: Option, + pub focus: Option, pub open_new_workspace: Option, pub replace_window: Option>, pub env: Option>, } + #[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, diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index e3430d2183..c2ac090a1d 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -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> { + pub fn try_unrelativize(&self, path: &RepoPath) -> Option> { + self.work_directory.try_unrelativize(path) + } + + pub fn unrelativize(&self, path: &RepoPath) -> Arc { 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> { + pub fn try_unrelativize(&self, path: &RepoPath) -> Option> { 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 { + 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 { diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index 53a74db398..15131b0eac 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -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 diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 6aa23ad53e..e3f7bccc01 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -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::(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,