From 8afa9699fcdced1bdab6c5ce037a108c41adc749 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Tue, 7 Apr 2026 04:22:58 -0400 Subject: [PATCH] Replace file history actions with git graph UI --- Cargo.lock | 2 + crates/git/src/repository.rs | 5 +- crates/git_graph/Cargo.toml | 2 + crates/git_graph/src/git_graph.rs | 477 +++++++++++++++++----- crates/git_ui/src/git_panel.rs | 44 +- crates/project_panel/src/project_panel.rs | 107 ++--- 6 files changed, 428 insertions(+), 209 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 97412711a55667a4976a35313eb6c0388acc74ef..04e12d32be6e6ba2b793c538d730c9daceada6d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7198,8 +7198,10 @@ dependencies = [ "git_ui", "gpui", "language", + "language_model", "menu", "project", + "project_panel", "rand 0.9.2", "remote_connection", "search", diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index 504480cb25b1f3d69c8e482b069568d710f57c29..1c116c9fb52023e7c4fd71a6603337f1c0e3582c 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -2732,7 +2732,6 @@ impl GitRepository for RealGitRepository { async move { let git = git_binary?; - // todo!: should we include no optional locks here? let mut git_log_command = vec![ "log", GRAPH_COMMIT_FORMAT, @@ -2826,6 +2825,10 @@ impl GitRepository for RealGitRepository { args.push("--grep"); args.push(search_args.query.as_str()); + if let LogSource::File(file_path) = &log_source { + args.extend(["--", file_path.as_unix_str()]); + } + let mut command = git.build_command(&args); command.stdout(Stdio::piped()); command.stderr(Stdio::null()); diff --git a/crates/git_graph/Cargo.toml b/crates/git_graph/Cargo.toml index e9e31a8361e367275c994e125ae6e04cbd652fc3..55c054b138b1322d17685689080dd2290d311250 100644 --- a/crates/git_graph/Cargo.toml +++ b/crates/git_graph/Cargo.toml @@ -30,6 +30,7 @@ gpui.workspace = true language.workspace = true menu.workspace = true project.workspace = true +project_panel.workspace = true search.workspace = true settings.workspace = true smallvec.workspace = true @@ -45,6 +46,7 @@ db = { workspace = true, features = ["test-support"] } fs = { workspace = true, features = ["test-support"] } git = { workspace = true, features = ["test-support"] } gpui = { workspace = true, features = ["test-support"] } +language_model.workspace = true project = { workspace = true, features = ["test-support"] } rand.workspace = true remote_connection = { workspace = true, features = ["test-support"] } diff --git a/crates/git_graph/src/git_graph.rs b/crates/git_graph/src/git_graph.rs index d2dfda592753e0259c150cab7cce142cdab0851c..37261f7722def5bf5594d291c3332f515112b026 100644 --- a/crates/git_graph/src/git_graph.rs +++ b/crates/git_graph/src/git_graph.rs @@ -26,6 +26,7 @@ use project::{ RepositoryEvent, RepositoryId, }, }; +use project_panel::ProjectPanel; use search::{ SearchOption, SearchOptions, SearchSource, SelectNextMatch, SelectPreviousMatch, ToggleCaseSensitive, buffer_search, @@ -734,15 +735,32 @@ pub fn init(cx: &mut App) { workspace::register_serializable_item::(cx); cx.observe_new(|workspace: &mut workspace::Workspace, _, _| { - workspace.register_action_renderer(|div, workspace, _, cx| { - let active_item_file = workspace - .active_item(cx) - .and_then(|item| item.downcast::()) - .and_then(|editor| editor.read(cx).buffer().read(cx).as_singleton()) - .and_then(|buffer| buffer.read(cx).file()) - .cloned(); - - div.when( + workspace.register_action_renderer(|div, workspace, window, cx| { + div.when_some( + resolve_file_history_target(workspace, window, cx), + |div, (repo_id, log_source)| { + let git_store = workspace.project().read(cx).git_store().clone(); + let workspace = workspace.weak_handle(); + + div.on_action(move |_: &git::FileHistory, window, cx| { + let git_store = git_store.clone(); + workspace + .update(cx, |workspace, cx| { + open_or_reuse_graph( + workspace, + repo_id, + git_store, + log_source.clone(), + None, + window, + cx, + ); + }) + .ok(); + }) + }, + ) + .when( workspace.project().read(cx).active_repository(cx).is_some(), |div| { let workspace = workspace.weak_handle(); @@ -759,31 +777,14 @@ pub fn init(cx: &mut App) { }; let selected_repo_id = repo.read(cx).id; - let existing = workspace - .items_of_type::(cx) - .find(|graph| graph.read(cx).repo_id == selected_repo_id); - if let Some(existing) = existing { - workspace.activate_item(&existing, true, true, window, cx); - return; - } - let git_store = workspace.project().read(cx).git_store().clone(); - let workspace_handle = workspace.weak_handle(); - let git_graph = cx.new(|cx| { - GitGraph::new( - selected_repo_id, - git_store, - workspace_handle, - None, - window, - cx, - ) - }); - workspace.add_item_to_active_pane( - Box::new(git_graph), + open_or_reuse_graph( + workspace, + selected_repo_id, + git_store, + LogSource::All, None, - true, window, cx, ); @@ -803,36 +804,14 @@ pub fn init(cx: &mut App) { }; let selected_repo_id = repo.read(cx).id; - let existing = workspace - .items_of_type::(cx) - .find(|graph| graph.read(cx).repo_id == selected_repo_id); - if let Some(existing) = existing { - existing.update(cx, |graph, cx| { - graph.select_commit_by_sha(sha.as_str(), cx); - }); - workspace.activate_item(&existing, true, true, window, cx); - return; - } - let git_store = workspace.project().read(cx).git_store().clone(); - let workspace_handle = workspace.weak_handle(); - let git_graph = cx.new(|cx| { - let mut graph = GitGraph::new( - selected_repo_id, - git_store, - workspace_handle, - None, - window, - cx, - ); - graph.select_commit_by_sha(sha.as_str(), cx); - graph - }); - workspace.add_item_to_active_pane( - Box::new(git_graph), - None, - true, + open_or_reuse_graph( + workspace, + selected_repo_id, + git_store, + LogSource::All, + Some(sha), window, cx, ); @@ -842,58 +821,91 @@ pub fn init(cx: &mut App) { ) }, ) - .when_some(active_item_file, move |this, active_file| { - this.on_action({ - let workspace = workspace.weak_handle(); + }); + }) + .detach(); +} - move |_: &git::FileHistory, window, cx| { - workspace - .update(cx, |workspace, cx| { - let git_store = workspace.project().read(cx).git_store().clone(); - let workspace_handle = workspace.weak_handle(); - let file_path = active_file.path(); - let file_worktree_id = active_file.worktree_id(cx); - - let project_path = ProjectPath { - worktree_id: file_worktree_id, - path: file_path.clone(), - }; +fn resolve_file_history_target( + workspace: &Workspace, + window: &Window, + cx: &App, +) -> Option<(RepositoryId, LogSource)> { + if let Some(panel) = workspace.panel::(cx) + && panel.read(cx).focus_handle(cx).contains_focused(window, cx) + && let Some(project_path) = panel.read(cx).selected_file_project_path(cx) + { + let git_store = workspace.project().read(cx).git_store(); + let (repo, repo_path) = git_store + .read(cx) + .repository_and_path_for_project_path(&project_path, cx)?; + return Some((repo.read(cx).id, LogSource::File(repo_path))); + } - let Some((repo, repo_path)) = git_store - .read(cx) - .repository_and_path_for_project_path(&project_path, cx) - else { - return; - }; + if let Some(panel) = workspace.panel::(cx) + && panel.read(cx).focus_handle(cx).contains_focused(window, cx) + && let Some((repository, repo_path)) = panel.read(cx).selected_file_history_target() + { + return Some((repository.read(cx).id, LogSource::File(repo_path))); + } - let repo_id = repo.read(cx).id; - let log_source = LogSource::File(repo_path); + let editor = workspace.active_item_as::(cx)?; - let git_graph = cx.new(|cx| { - GitGraph::new( - repo_id, - git_store, - workspace_handle, - Some(log_source), - window, - cx, - ) - }); - workspace.add_item_to_active_pane( - Box::new(git_graph), - None, - true, - window, - cx, - ); - }) - .ok(); - } - }) - }) - }); - }) - .detach(); + let file = editor + .read(cx) + .file_at(editor.read(cx).selections.newest_anchor().head(), cx)?; + let project_path = ProjectPath { + worktree_id: file.worktree_id(cx), + path: file.path().clone(), + }; + + let git_store = workspace.project().read(cx).git_store(); + let (repo, repo_path) = git_store + .read(cx) + .repository_and_path_for_project_path(&project_path, cx)?; + Some((repo.read(cx).id, LogSource::File(repo_path))) +} + +fn open_or_reuse_graph( + workspace: &mut Workspace, + repo_id: RepositoryId, + git_store: Entity, + log_source: LogSource, + sha: Option, + window: &mut Window, + cx: &mut Context, +) { + let existing = workspace.items_of_type::(cx).find(|graph| { + let graph = graph.read(cx); + graph.repo_id == repo_id && graph.log_source == log_source + }); + + if let Some(existing) = existing { + if let Some(sha) = sha { + existing.update(cx, |graph, cx| { + graph.select_commit_by_sha(sha.as_str(), cx); + }); + } + workspace.activate_item(&existing, true, true, window, cx); + return; + } + + let workspace_handle = workspace.weak_handle(); + let git_graph = cx.new(|cx| { + let mut graph = GitGraph::new( + repo_id, + git_store, + workspace_handle, + Some(log_source), + window, + cx, + ); + if let Some(sha) = sha { + graph.select_commit_by_sha(sha.as_str(), cx); + } + graph + }); + workspace.add_item_to_active_pane(Box::new(git_graph), None, true, window, cx); } fn lane_center_x(bounds: Bounds, lane: f32) -> Pixels { @@ -1628,9 +1640,11 @@ impl GitGraph { .and_then(|data| data.commit_oid_to_index.get(&oid)) .copied() else { + this.pending_select_sha = Some(oid); return; }; + this.pending_select_sha = None; this.select_entry(index, ScrollStrategy::Center, cx); } @@ -2910,11 +2924,22 @@ impl Item for GitGraph { .file_name() .map(|name| name.to_string_lossy().to_string()) }); + let file_history_path = match &self.log_source { + LogSource::File(path) => Some(path.as_unix_str().to_string()), + _ => None, + }; Some(TabTooltipContent::Custom(Box::new(Tooltip::element({ move |_, _| { v_flex() - .child(Label::new("Git Graph")) + .child(Label::new(if file_history_path.is_some() { + "File History" + } else { + "Git Graph" + })) + .when_some(file_history_path.clone(), |this, path| { + this.child(Label::new(path).color(Color::Muted).size(LabelSize::Small)) + }) .when_some(repo_name.clone(), |this, name| { this.child(Label::new(name).color(Color::Muted).size(LabelSize::Small)) }) @@ -2924,6 +2949,14 @@ impl Item for GitGraph { } fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString { + if let LogSource::File(path) = &self.log_source { + return path + .as_ref() + .file_name() + .map(|name| SharedString::from(name.to_string())) + .unwrap_or_else(|| SharedString::from(path.as_unix_str().to_string())); + } + self.get_repository(cx) .and_then(|repo| { repo.read(cx) @@ -3117,6 +3150,10 @@ mod tests { let settings_store = SettingsStore::test(cx); cx.set_global(settings_store); theme_settings::init(theme::LoadThemes::JustBase, cx); + language_model::init(cx); + git_ui::init(cx); + project_panel::init(cx); + init(cx); }); } @@ -3942,6 +3979,230 @@ mod tests { ); } + #[gpui::test] + async fn test_file_history_action_uses_focused_source_and_reuses_matching_graph( + cx: &mut TestAppContext, + ) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + Path::new("/project"), + json!({ + ".git": {}, + "tracked1.txt": "tracked 1", + "tracked2.txt": "tracked 2", + }), + ) + .await; + + let commits = vec![Arc::new(InitialGraphCommitData { + sha: Oid::from_bytes(&[1; 20]).unwrap(), + parents: smallvec![], + ref_names: vec!["HEAD".into(), "refs/heads/main".into()], + })]; + fs.set_graph_commits(Path::new("/project/.git"), commits); + + let project = Project::test(fs.clone(), [Path::new("/project")], cx).await; + cx.run_until_parked(); + + let repository = project.read_with(cx, |project, cx| { + project + .active_repository(cx) + .expect("should have active repository") + }); + let tracked1_repo_path = RepoPath::new(&"tracked1.txt").unwrap(); + let tracked2_repo_path = RepoPath::new(&"tracked2.txt").unwrap(); + let tracked1 = repository + .read_with(cx, |repository, cx| { + repository.repo_path_to_project_path(&tracked1_repo_path, cx) + }) + .expect("tracked1 should resolve to project path"); + let tracked2 = repository + .read_with(cx, |repository, cx| { + repository.repo_path_to_project_path(&tracked2_repo_path, cx) + }) + .expect("tracked2 should resolve to project path"); + + let workspace_window = cx.add_window(|window, cx| { + workspace::MultiWorkspace::test_new(project.clone(), window, cx) + }); + let workspace = workspace_window + .read_with(cx, |multi, _| multi.workspace().clone()) + .expect("workspace should exist"); + + let (weak_workspace, async_window_cx) = workspace_window + .update(cx, |multi, window, cx| { + (multi.workspace().downgrade(), window.to_async(cx)) + }) + .expect("window should be available"); + cx.background_executor.allow_parking(); + let project_panel = cx + .foreground_executor() + .clone() + .block_test(ProjectPanel::load( + weak_workspace.clone(), + async_window_cx.clone(), + )) + .expect("project panel should load"); + let git_panel = cx + .foreground_executor() + .clone() + .block_test(git_ui::git_panel::GitPanel::load( + weak_workspace, + async_window_cx, + )) + .expect("git panel should load"); + cx.background_executor.forbid_parking(); + + workspace_window + .update(cx, |multi, window, cx| { + let workspace = multi.workspace(); + workspace.update(cx, |workspace, cx| { + workspace.add_panel(project_panel.clone(), window, cx); + workspace.add_panel(git_panel.clone(), window, cx); + }); + }) + .expect("workspace window should be available"); + cx.run_until_parked(); + + workspace_window + .update(cx, |multi, window, cx| { + let workspace = multi.workspace(); + project_panel.update(cx, |panel, cx| { + panel.select_path_for_test(tracked1.clone(), cx) + }); + workspace.update(cx, |workspace, cx| { + workspace.focus_panel::(window, cx); + }); + }) + .expect("workspace window should be available"); + cx.run_until_parked(); + workspace_window + .update(cx, |_, window, cx| { + window.dispatch_action(Box::new(git::FileHistory), cx); + }) + .expect("workspace window should be available"); + cx.run_until_parked(); + + workspace.read_with(cx, |workspace, cx| { + let graphs = workspace.items_of_type::(cx).collect::>(); + assert_eq!(graphs.len(), 1); + assert_eq!( + graphs[0].read(cx).log_source, + LogSource::File(tracked1_repo_path.clone()) + ); + }); + + workspace_window + .update(cx, |multi, window, cx| { + let workspace = multi.workspace(); + git_panel.update(cx, |panel, cx| { + panel.select_entry_by_path(tracked1.clone(), window, cx); + }); + workspace.update(cx, |workspace, cx| { + workspace.focus_panel::(window, cx); + }); + }) + .expect("workspace window should be available"); + cx.run_until_parked(); + workspace_window + .update(cx, |_, window, cx| { + window.dispatch_action(Box::new(git::FileHistory), cx); + }) + .expect("workspace window should be available"); + cx.run_until_parked(); + + workspace.read_with(cx, |workspace, cx| { + let graphs = workspace.items_of_type::(cx).collect::>(); + assert_eq!(graphs.len(), 1); + assert_eq!( + graphs[0].read(cx).log_source, + LogSource::File(tracked1_repo_path.clone()) + ); + }); + + let tracked1_buffer = project + .update(cx, |project, cx| project.open_buffer(tracked1.clone(), cx)) + .await + .expect("tracked1 buffer should open"); + let tracked2_buffer = project + .update(cx, |project, cx| project.open_buffer(tracked2.clone(), cx)) + .await + .expect("tracked2 buffer should open"); + workspace_window + .update(cx, |multi, window, cx| { + let workspace = multi.workspace(); + let multibuffer = cx.new(|cx| { + let mut multibuffer = editor::MultiBuffer::new(language::Capability::ReadWrite); + multibuffer.set_excerpts_for_buffer( + tracked1_buffer.clone(), + [Default::default()..tracked1_buffer.read(cx).max_point()], + 0, + cx, + ); + multibuffer.set_excerpts_for_buffer( + tracked2_buffer.clone(), + [Default::default()..tracked2_buffer.read(cx).max_point()], + 0, + cx, + ); + multibuffer + }); + let editor = cx.new(|cx| { + Editor::for_multibuffer(multibuffer, Some(project.clone()), window, cx) + }); + workspace.update(cx, |workspace, cx| { + workspace.add_item_to_active_pane( + Box::new(editor.clone()), + None, + true, + window, + cx, + ); + }); + editor.update(cx, |editor, cx| { + let snapshot = editor.buffer().read(cx).snapshot(cx); + let second_excerpt_point = snapshot + .range_for_buffer(tracked2_buffer.read(cx).remote_id()) + .expect("tracked2 excerpt should exist") + .start; + let anchor = snapshot.anchor_before(second_excerpt_point); + editor.change_selections( + editor::SelectionEffects::no_scroll(), + window, + cx, + |selections| { + selections.select_anchor_ranges([anchor..anchor]); + }, + ); + window.focus(&editor.focus_handle(cx), cx); + }); + }) + .expect("workspace window should be available"); + cx.run_until_parked(); + + workspace_window + .update(cx, |_, window, cx| { + window.dispatch_action(Box::new(git::FileHistory), cx); + }) + .expect("workspace window should be available"); + cx.run_until_parked(); + + workspace.read_with(cx, |workspace, cx| { + let graphs = workspace.items_of_type::(cx).collect::>(); + assert_eq!(graphs.len(), 2); + let latest = graphs + .into_iter() + .max_by_key(|graph| graph.entity_id()) + .expect("expected a git graph"); + assert_eq!( + latest.read(cx).log_source, + LogSource::File(tracked2_repo_path) + ); + }); + } + #[gpui::test] async fn test_graph_data_reloaded_after_stash_change(cx: &mut TestAppContext) { init_test(cx); diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 1d907b969f9b6288a9b72b0cdc7b5445d63268fa..fc03dad59fb6c184beee21dfc85905e0d3133f18 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -7,8 +7,7 @@ use crate::project_diff::{self, BranchDiff, Diff, ProjectDiff}; use crate::remote_output::{self, RemoteAction, SuccessMessage}; use crate::{branch_picker, picker_prompt, render_remote_button}; use crate::{ - file_history_view::FileHistoryView, git_panel_settings::GitPanelSettings, git_status_icon, - repository_selector::RepositorySelector, + git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector, }; use agent_settings::AgentSettings; use anyhow::Context as _; @@ -1300,26 +1299,6 @@ impl GitPanel { }); } - fn file_history(&mut self, _: &git::FileHistory, window: &mut Window, cx: &mut Context) { - maybe!({ - let entry = self.entries.get(self.selected_entry?)?.status_entry()?; - let active_repo = self.active_repository.as_ref()?; - let repo_path = entry.repo_path.clone(); - let git_store = self.project.read(cx).git_store(); - - FileHistoryView::open( - repo_path, - git_store.downgrade(), - active_repo.downgrade(), - self.workspace.clone(), - window, - cx, - ); - - Some(()) - }); - } - fn open_file( &mut self, _: &menu::SecondaryConfirm, @@ -4983,8 +4962,11 @@ impl GitPanel { .separator() .action("Open Diff", menu::Confirm.boxed_clone()) .action("Open File", menu::SecondaryConfirm.boxed_clone()) - .separator() - .action_disabled_when(is_created, "View File History", Box::new(git::FileHistory)) + .when(!is_created, |context_menu| { + context_menu + .separator() + .action("View File History", Box::new(git::FileHistory)) + }) }); self.selected_entry = Some(ix); self.set_context_menu(context_menu, position, window, cx); @@ -5617,6 +5599,17 @@ impl GitPanel { } } +impl GitPanel { + pub fn selected_file_history_target(&self) -> Option<(Entity, RepoPath)> { + let entry = self.get_selected_entry()?.status_entry()?; + let repository = self.active_repository.clone()?; + if entry.status.is_created() { + return None; + } + Some((repository, entry.repo_path.clone())) + } +} + #[cfg(any(test, feature = "test-support"))] impl GitPanel { pub fn new_test( @@ -5685,9 +5678,6 @@ impl Render for GitPanel { .on_action(cx.listener(Self::close_panel)) .on_action(cx.listener(Self::open_diff)) .on_action(cx.listener(Self::open_file)) - // TODO!: We should remove this listener, so that git graph - // implementation of file history is used. - .on_action(cx.listener(Self::file_history)) .on_action(cx.listener(Self::focus_changes_list)) .on_action(cx.listener(Self::focus_editor)) .on_action(cx.listener(Self::expand_commit_editor)) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index d1a5b8a0ece5e3ddc6b1fe924154583b401a0fc9..1063316129cec2dfe2254f461a0c60be4a797250 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -523,74 +523,6 @@ pub fn init(cx: &mut App) { panel.update(cx, |panel, cx| panel.delete(action, window, cx)); } }); - - // TODO!: We should remove this `register_action` call, so that git - // graph implementation of file history is used. - workspace.register_action(|workspace, _: &git::FileHistory, window, cx| { - // First try to get from project panel if it's focused - if let Some(panel) = workspace.panel::(cx) { - let maybe_project_path = panel.read(cx).selection.and_then(|selection| { - let project = workspace.project().read(cx); - let worktree = project.worktree_for_id(selection.worktree_id, cx)?; - let entry = worktree.read(cx).entry_for_id(selection.entry_id)?; - if entry.is_file() { - Some(ProjectPath { - worktree_id: selection.worktree_id, - path: entry.path.clone(), - }) - } else { - None - } - }); - - if let Some(project_path) = maybe_project_path { - let project = workspace.project(); - let git_store = project.read(cx).git_store(); - if let Some((repo, repo_path)) = git_store - .read(cx) - .repository_and_path_for_project_path(&project_path, cx) - { - git_ui::file_history_view::FileHistoryView::open( - repo_path, - git_store.downgrade(), - repo.downgrade(), - workspace.weak_handle(), - window, - cx, - ); - return; - } - } - } - - // Fallback: try to get from active editor - if let Some(active_item) = workspace.active_item(cx) - && let Some(editor) = active_item.downcast::() - && let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton() - && let Some(file) = buffer.read(cx).file() - { - let worktree_id = file.worktree_id(cx); - let project_path = ProjectPath { - worktree_id, - path: file.path().clone(), - }; - let project = workspace.project(); - let git_store = project.read(cx).git_store(); - if let Some((repo, repo_path)) = git_store - .read(cx) - .repository_and_path_for_project_path(&project_path, cx) - { - git_ui::file_history_view::FileHistoryView::open( - repo_path, - git_store.downgrade(), - repo.downgrade(), - workspace.weak_handle(), - window, - cx, - ); - } - } - }); }) .detach(); } @@ -1115,16 +1047,18 @@ impl ProjectPanel { || (settings.hide_root && visible_worktrees_count == 1)); let should_show_compare = !is_dir && self.file_abs_paths_to_diff(cx).is_some(); - let has_git_repo = !is_dir && { + let has_file_history = !is_dir && { let project_path = project::ProjectPath { worktree_id, path: entry.path.clone(), }; - project - .git_store() - .read(cx) + let git_store = project.git_store().read(cx); + git_store .repository_and_path_for_project_path(&project_path, cx) .is_some() + && !git_store + .project_path_git_status(&project_path, cx) + .is_some_and(|status| status.is_created()) }; let has_pasteable_content = self.has_pasteable_content(cx); @@ -1192,7 +1126,7 @@ impl ProjectPanel { Box::new(git::RestoreFile { skip_prompt: false }), ) }) - .when(has_git_repo, |menu| { + .when(has_file_history, |menu| { menu.separator() .action("View File History", Box::new(git::FileHistory)) }) @@ -3777,6 +3711,14 @@ impl ProjectPanel { Some((worktree.read(cx), entry)) } + pub fn selected_file_project_path(&self, cx: &App) -> Option { + let (worktree, entry) = self.selected_sub_entry(cx)?; + Some(ProjectPath { + worktree_id: worktree.read(cx).id(), + path: entry.is_file().then(|| entry.path.clone())?, + }) + } + /// Compared to selected_entry, this function resolves to the currently /// selected subentry if dir auto-folding is enabled. fn selected_sub_entry<'a>( @@ -7249,6 +7191,25 @@ impl Panel for ProjectPanel { } } +impl ProjectPanel { + pub fn select_path_for_test(&mut self, project_path: ProjectPath, cx: &App) { + let Some(worktree) = self + .project + .read(cx) + .worktree_for_id(project_path.worktree_id, cx) + else { + return; + }; + let Some(entry) = worktree.read(cx).entry_for_path(project_path.path.as_ref()) else { + return; + }; + self.selection = Some(SelectedEntry { + worktree_id: project_path.worktree_id, + entry_id: entry.id, + }); + } +} + impl Focusable for ProjectPanel { fn focus_handle(&self, _cx: &App) -> FocusHandle { self.focus_handle.clone()