From 01961bef5966c2f4806789eaec517c4263d8b9ea Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 1 Aug 2026 22:42:37 +0300 Subject: [PATCH] Speed up `cargo build -p zed` compilation (#62059) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Splits the crate graph. Before, the compilation graph: `editor → picker_preview → search → project_panel → open_path_prompt → recent_projects → title_bar → collab_ui → zed` After, the compilation graph: `editor → picker_preview → search → agent_ui → sidebar → zed` With sccache disabled and project fully built, `touch crates/editor/src/editor.rs` and `cargo build -p zed` took time Before (0500fd9ee9): 13.19s After: 11.85s (-10.2% speed up) Each commit contains a separate compilation instructions, and a test in the final commit. The main approach is to split coupled crates and replace them with `zed_actions` and move some shared functionality into `git_ui_core` new, shared module. I've also tried to add a test to prevent common pitfalls, but not sure I'm happy with the end result — can remove it if it looks too synthetic. Release Notes: - N/A --- .github/workflows/run_tests.yml | 2 + Cargo.lock | 56 +++++- Cargo.toml | 2 + crates/agent_ui/Cargo.toml | 4 +- crates/agent_ui/src/agent_panel.rs | 18 +- crates/agent_ui/src/test_support.rs | 7 +- .../agent_ui/src/thread_worktree_archive.rs | 13 +- crates/file_finder/Cargo.toml | 1 - crates/file_finder/src/file_finder.rs | 13 +- crates/file_finder/src/file_finder_tests.rs | 182 +++++++----------- crates/git_ui/Cargo.toml | 9 +- crates/git_ui/src/branch_picker.rs | 3 +- crates/git_ui/src/git_graph.rs | 50 +++-- crates/git_ui/src/git_panel.rs | 127 +----------- crates/git_ui/src/git_ui.rs | 52 ++++- crates/git_ui/src/multi_diff_view.rs | 2 +- crates/git_ui_core/Cargo.toml | 62 ++++++ crates/git_ui_core/LICENSE-GPL | 1 + .../src/askpass_modal.rs | 2 +- .../src/created_worktrees.rs | 0 .../src/file_diff_view.rs | 2 +- crates/git_ui_core/src/git_ui_core.rs | 116 +++++++++++ crates/git_ui_core/src/notifications.rs | 133 +++++++++++++ .../src/worktree_names.rs | 0 .../src/worktree_picker.rs | 2 +- .../src/worktree_service.rs | 2 +- crates/open_path_prompt/Cargo.toml | 1 - .../open_path_prompt/src/open_path_prompt.rs | 7 +- crates/project_panel/Cargo.toml | 3 +- crates/project_panel/src/project_panel.rs | 53 ++--- crates/search/src/project_search.rs | 34 +++- crates/search/src/search.rs | 12 +- crates/sidebar/Cargo.toml | 2 +- crates/sidebar/src/sidebar.rs | 4 +- crates/title_bar/Cargo.toml | 2 +- crates/title_bar/src/title_bar.rs | 8 +- crates/zed/Cargo.toml | 1 + crates/zed/src/zed/open_listener.rs | 3 +- crates/zed_actions/src/lib.rs | 22 ++- .../xtask/src/tasks/workflows/run_tests.rs | 5 + tooling/xtask/src/workspace.rs | 92 +++++++++ 41 files changed, 734 insertions(+), 376 deletions(-) create mode 100644 crates/git_ui_core/Cargo.toml create mode 120000 crates/git_ui_core/LICENSE-GPL rename crates/{git_ui => git_ui_core}/src/askpass_modal.rs (99%) rename crates/{git_ui => git_ui_core}/src/created_worktrees.rs (100%) rename crates/{git_ui => git_ui_core}/src/file_diff_view.rs (99%) create mode 100644 crates/git_ui_core/src/git_ui_core.rs create mode 100644 crates/git_ui_core/src/notifications.rs rename crates/{git_ui => git_ui_core}/src/worktree_names.rs (100%) rename crates/{git_ui => git_ui_core}/src/worktree_picker.rs (99%) rename crates/{git_ui => git_ui_core}/src/worktree_service.rs (99%) diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index ebe8b57714..aa4ca894f0 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -708,6 +708,8 @@ jobs: run: cargo machete - name: run_tests::check_dependencies::check_cargo_lock run: cargo update --locked --workspace + - name: run_tests::check_dependencies::check_crate_graph + run: 'cargo test --package xtask -- workspace::' - name: run_tests::check_dependencies::check_vulnerable_dependencies if: github.event_name == 'pull_request' uses: actions/dependency-review-action@67d4f4bd7a9b17a0db54d2a7519187c65e339de8 diff --git a/Cargo.lock b/Cargo.lock index 96d2031998..7d575147f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -491,7 +491,7 @@ dependencies = [ "futures 0.3.32", "fuzzy", "git", - "git_ui", + "git_ui_core", "gpui", "gpui_tokio", "heapless", @@ -6628,7 +6628,6 @@ dependencies = [ "picker_preview", "pretty_assertions", "project", - "project_panel", "remote_connection", "serde", "serde_json", @@ -7422,6 +7421,7 @@ dependencies = [ "fuzzy", "fuzzy_nucleo", "git", + "git_ui_core", "gpui", "indoc", "itertools 0.14.0", @@ -7437,10 +7437,8 @@ dependencies = [ "pretty_assertions", "project", "prompt_store", - "proto", "rand 0.9.4", "release_channel", - "remote", "remote_connection", "schemars 1.0.4", "search", @@ -7466,6 +7464,48 @@ dependencies = [ "windows 0.61.3", "workspace", "zed_actions", + "zlog", + "ztracing", +] + +[[package]] +name = "git_ui_core" +version = "0.1.0" +dependencies = [ + "anyhow", + "askpass", + "buffer_diff", + "collections", + "db", + "editor", + "fs", + "futures 0.3.32", + "fuzzy", + "git", + "gpui", + "language", + "log", + "menu", + "notifications", + "picker", + "project", + "proto", + "rand 0.9.4", + "remote", + "remote_connection", + "serde", + "serde_json", + "settings", + "task", + "terminal", + "theme", + "theme_settings", + "ui", + "unindent", + "util", + "watch", + "workspace", + "zed_actions", "zeroize", "zlog", "ztracing", @@ -12437,7 +12477,6 @@ dependencies = [ "gpui", "picker", "project", - "project_panel", "schemars 1.0.4", "serde", "serde_json", @@ -14248,6 +14287,7 @@ dependencies = [ "futures 0.3.32", "git", "git_ui", + "git_ui_core", "gpui", "itertools 0.14.0", "language", @@ -14261,7 +14301,6 @@ dependencies = [ "rayon", "remote_connection", "schemars 1.0.4", - "search", "serde", "serde_json", "settings", @@ -17065,7 +17104,7 @@ dependencies = [ "feature_flags", "fs", "git", - "git_ui", + "git_ui_core", "gpui", "http_client", "itertools 0.14.0", @@ -18969,7 +19008,7 @@ dependencies = [ "command_palette_hooks", "db", "fs", - "git_ui", + "git_ui_core", "gpui", "icons", "livekit_client", @@ -23275,6 +23314,7 @@ dependencies = [ "git", "git_hosting_providers", "git_ui", + "git_ui_core", "go_to_line", "gpui", "gpui_platform", diff --git a/Cargo.toml b/Cargo.toml index a1a2e71087..4649b38844 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -86,6 +86,7 @@ members = [ "crates/git", "crates/git_hosting_providers", "crates/git_ui", + "crates/git_ui_core", "crates/go_to_line", "crates/google_ai", "crates/grammars", @@ -347,6 +348,7 @@ fuzzy_nucleo = { path = "crates/fuzzy_nucleo" } git = { path = "crates/git" } git_hosting_providers = { path = "crates/git_hosting_providers" } git_ui = { path = "crates/git_ui" } +git_ui_core = { path = "crates/git_ui_core" } go_to_line = { path = "crates/go_to_line" } google_ai = { path = "crates/google_ai" } grammars = { path = "crates/grammars" } diff --git a/crates/agent_ui/Cargo.toml b/crates/agent_ui/Cargo.toml index bfabeac626..97ffc8edcb 100644 --- a/crates/agent_ui/Cargo.toml +++ b/crates/agent_ui/Cargo.toml @@ -56,9 +56,8 @@ feature_flags.workspace = true file_icons.workspace = true fs.workspace = true futures.workspace = true -git.workspace = true -git_ui.workspace = true fuzzy.workspace = true +git.workspace = true gpui.workspace = true gpui_tokio.workspace = true html_to_markdown.workspace = true @@ -115,6 +114,7 @@ util.workspace = true uuid.workspace = true watch.workspace = true workspace.workspace = true +git_ui_core.workspace = true zed_actions.workspace = true image.workspace = true reqwest_client = { workspace = true, optional = true } diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 0a147aa1a4..a45538cfa0 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -4792,7 +4792,7 @@ impl agent::SiblingThreadHost for AgentPanelSiblingHost { }; let creation = window.update(cx, |_root, window, cx| { workspace.update(cx, |workspace, cx| { - git_ui::worktree_service::create_worktree_workspace( + git_ui_core::worktree_service::create_worktree_workspace( workspace, &action, window, None, cx, ) }) @@ -11154,19 +11154,19 @@ mod tests { #[gpui::test] fn test_resolve_worktree_branch_target() { - let resolved = git_ui::worktree_service::resolve_worktree_branch_target( + let resolved = git_ui_core::worktree_service::resolve_worktree_branch_target( &NewWorktreeBranchTarget::ExistingBranch { name: "feature".to_string(), }, ); assert_eq!(resolved, Some("feature".to_string())); - let resolved = git_ui::worktree_service::resolve_worktree_branch_target( + let resolved = git_ui_core::worktree_service::resolve_worktree_branch_target( &NewWorktreeBranchTarget::CurrentBranch, ); assert_eq!(resolved, None); - let resolved = git_ui::worktree_service::resolve_worktree_branch_target( + let resolved = git_ui_core::worktree_service::resolve_worktree_branch_target( &NewWorktreeBranchTarget::RemoteBranch { remote_name: "origin".to_string(), branch_name: "main".to_string(), @@ -12172,7 +12172,7 @@ mod tests { let result = multi_workspace .update(cx, |_, window, cx| { window.spawn(cx, async move |cx| { - git_ui::worktree_service::await_and_rollback_on_failure( + git_ui_core::worktree_service::await_and_rollback_on_failure( creation_infos, fs_clone, cx, @@ -12260,7 +12260,7 @@ mod tests { let result = multi_workspace .update(cx, |_, window, cx| { window.spawn(cx, async move |cx| { - git_ui::worktree_service::await_and_rollback_on_failure( + git_ui_core::worktree_service::await_and_rollback_on_failure( creation_infos, fs_clone, cx, @@ -12331,7 +12331,7 @@ mod tests { let result = multi_workspace .update(cx, |_, window, cx| { window.spawn(cx, async move |cx| { - git_ui::worktree_service::await_and_rollback_on_failure( + git_ui_core::worktree_service::await_and_rollback_on_failure( creation_infos, fs_clone, cx, @@ -12406,7 +12406,7 @@ mod tests { let result = multi_workspace .update(cx, |_, window, cx| { window.spawn(cx, async move |cx| { - git_ui::worktree_service::await_and_rollback_on_failure( + git_ui_core::worktree_service::await_and_rollback_on_failure( creation_infos, fs_clone, cx, @@ -12558,7 +12558,7 @@ mod tests { panel.read_with(cx, |panel, cx| { let (git_repos, non_git_paths) = - git_ui::worktree_service::classify_worktrees(panel.project.read(cx), cx); + git_ui_core::worktree_service::classify_worktrees(panel.project.read(cx), cx); let git_work_dirs: Vec = git_repos .iter() diff --git a/crates/agent_ui/src/test_support.rs b/crates/agent_ui/src/test_support.rs index 914662bdd2..bdce2da8da 100644 --- a/crates/agent_ui/src/test_support.rs +++ b/crates/agent_ui/src/test_support.rs @@ -146,7 +146,12 @@ pub async fn record_zed_created_worktree( ) { let created_at = fake_worktree_created_at(fs, worktree_path).await; cx.update(|cx| { - git_ui::created_worktrees::record_created_worktree(worktree_path, remote, created_at, cx) + git_ui_core::created_worktrees::record_created_worktree( + worktree_path, + remote, + created_at, + cx, + ) }) .await .unwrap(); diff --git a/crates/agent_ui/src/thread_worktree_archive.rs b/crates/agent_ui/src/thread_worktree_archive.rs index 1d392a8635..a01895ae9b 100644 --- a/crates/agent_ui/src/thread_worktree_archive.rs +++ b/crates/agent_ui/src/thread_worktree_archive.rs @@ -187,7 +187,7 @@ pub fn build_root_plan( // is re-verified against the filesystem in [`remove_root`] before // anything is deleted. let recorded_created_at = - git_ui::created_worktrees::recorded_created_at(&path, remote_connection, cx)?; + git_ui_core::created_worktrees::recorded_created_at(&path, remote_connection, cx)?; let branch_name = linked_snapshot .branch @@ -240,7 +240,7 @@ pub async fn remove_root(root: RootPlan, cx: &mut AsyncApp) -> Result<()> { // leftover record would only be saved by the creation time check, so // remove it eagerly. cx.update(|cx| { - git_ui::created_worktrees::forget_created_worktree( + git_ui_core::created_worktrees::forget_created_worktree( &root.root_path, root.remote_connection.as_ref(), cx, @@ -285,7 +285,7 @@ async fn verify_created_by_zed(root: &RootPlan, cx: &mut AsyncApp) -> Result<()> Some(created_at) if created_at == root.recorded_created_at => Ok(()), Some(_) => { cx.update(|cx| { - git_ui::created_worktrees::forget_created_worktree( + git_ui_core::created_worktrees::forget_created_worktree( &root.root_path, root.remote_connection.as_ref(), cx, @@ -792,7 +792,7 @@ pub async fn restore_worktree_via_git( if created_new_worktree { // Re-register the restored worktree as Zed-created so it can be // archived again later. - git_ui::created_worktrees::record_created_worktree_for_repo( + git_ui_core::created_worktrees::record_created_worktree_for_repo( &wt_repo, worktree_path, remote_connection, @@ -1573,7 +1573,7 @@ mod tests { let worktree_path = Path::new("/worktrees/project/feature/project"); let actual_created_at = fake_worktree_created_at(&fs, worktree_path).await; cx.update(|cx| { - git_ui::created_worktrees::record_created_worktree( + git_ui_core::created_worktrees::record_created_worktree( worktree_path, None, actual_created_at + Duration::from_secs(1), @@ -1637,7 +1637,8 @@ mod tests { // attempts skip the worktree entirely. workspace.read_with(cx, |_workspace, cx| { assert!( - git_ui::created_worktrees::recorded_created_at(worktree_path, None, cx).is_none(), + git_ui_core::created_worktrees::recorded_created_at(worktree_path, None, cx) + .is_none(), "stale created-worktree record should be removed" ); let plan = build_root_plan(worktree_path, None, std::slice::from_ref(&workspace), cx); diff --git a/crates/file_finder/Cargo.toml b/crates/file_finder/Cargo.toml index 7890bd3a87..284bd61a8d 100644 --- a/crates/file_finder/Cargo.toml +++ b/crates/file_finder/Cargo.toml @@ -36,7 +36,6 @@ ui.workspace = true util.workspace = true workspace.workspace = true zed_actions.workspace = true -project_panel.workspace = true [dev-dependencies] ctor.workspace = true diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 18859e74a9..4f8b5be5e4 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -27,8 +27,8 @@ use picker::{Picker, PickerDelegate}; use project::{ PathMatchCandidateSet, Project, ProjectPath, WorktreeId, worktree_store::WorktreeStore, }; -use project_panel::project_panel_settings::ProjectPanelSettings; -use settings::Settings; + +use settings::{Settings, SettingsStore}; use std::{ borrow::Cow, cmp, mem, @@ -789,7 +789,14 @@ fn should_hide_root_in_entry_path(worktree_store: &Entity, cx: &A .filter(|worktree| !worktree.read(cx).is_single_file()) .nth(1) .is_some(); - ProjectPanelSettings::get_global(cx).hide_root && !multiple_worktrees + let hide_root = cx + .global::() + .merged_settings() + .project_panel + .as_ref() + .and_then(|project_panel| project_panel.hide_root) + .unwrap_or(false); + hide_root && !multiple_worktrees } fn worktree_names_for_history_matching( diff --git a/crates/file_finder/src/file_finder_tests.rs b/crates/file_finder/src/file_finder_tests.rs index 22c901c0ed..7a31da1374 100644 --- a/crates/file_finder/src/file_finder_tests.rs +++ b/crates/file_finder/src/file_finder_tests.rs @@ -403,14 +403,11 @@ async fn test_complex_path(cx: &mut TestAppContext) { let app_state = init_test(cx); cx.update(|cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - hide_root: true, - ..settings - }, - cx, - ); + cx.update_global::(|store, cx| { + store.update_user_settings(cx, |settings| { + settings.project_panel.get_or_insert_default().hide_root = Some(true); + }); + }); }); app_state @@ -1796,14 +1793,11 @@ async fn test_path_distance_ordering(cx: &mut TestAppContext) { let app_state = init_test(cx); cx.update(|cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - hide_root: true, - ..settings - }, - cx, - ); + cx.update_global::(|store, cx| { + store.update_user_settings(cx, |settings| { + settings.project_panel.get_or_insert_default().hide_root = Some(true); + }); + }); }); app_state @@ -2042,14 +2036,11 @@ async fn test_history_match_positions(cx: &mut gpui::TestAppContext) { let app_state = init_test(cx); cx.update(|cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - hide_root: true, - ..settings - }, - cx, - ); + cx.update_global::(|store, cx| { + store.update_user_settings(cx, |settings| { + settings.project_panel.get_or_insert_default().hide_root = Some(true); + }); + }); }); app_state @@ -2111,14 +2102,11 @@ async fn test_history_labels_do_not_include_worktree_root_name(cx: &mut gpui::Te let app_state = init_test(cx); cx.update(|cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - hide_root: true, - ..settings - }, - cx, - ); + cx.update_global::(|store, cx| { + store.update_user_settings(cx, |settings| { + settings.project_panel.get_or_insert_default().hide_root = Some(true); + }); + }); }); app_state @@ -2211,14 +2199,11 @@ async fn test_history_labels_include_worktree_root_name_when_hide_root_false( let app_state = init_test(cx); cx.update(|cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - hide_root: false, - ..settings - }, - cx, - ); + cx.update_global::(|store, cx| { + store.update_user_settings(cx, |settings| { + settings.project_panel.get_or_insert_default().hide_root = Some(false); + }); + }); }); app_state @@ -2264,14 +2249,11 @@ async fn test_history_labels_include_worktree_root_name_when_hide_root_true_and_ let app_state = init_test(cx); cx.update(|cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - hide_root: true, - ..settings - }, - cx, - ); + cx.update_global::(|store, cx| { + store.update_user_settings(cx, |settings| { + settings.project_panel.get_or_insert_default().hide_root = Some(true); + }); + }); }); app_state @@ -2594,14 +2576,11 @@ async fn test_non_project_file_matches_history_with_hidden_root(cx: &mut gpui::T let app_state = init_test(cx); cx.update(|cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - hide_root: true, - ..settings - }, - cx, - ); + cx.update_global::(|store, cx| { + store.update_user_settings(cx, |settings| { + settings.project_panel.get_or_insert_default().hide_root = Some(true); + }); + }); }); app_state @@ -2799,14 +2778,11 @@ async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) { let app_state = init_test(cx); cx.update(|cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - hide_root: true, - ..settings - }, - cx, - ); + cx.update_global::(|store, cx| { + store.update_user_settings(cx, |settings| { + settings.project_panel.get_or_insert_default().hide_root = Some(true); + }); + }); }); app_state @@ -2915,14 +2891,11 @@ async fn test_search_sorts_history_items(cx: &mut gpui::TestAppContext) { let app_state = init_test(cx); cx.update(|cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - hide_root: true, - ..settings - }, - cx, - ); + cx.update_global::(|store, cx| { + store.update_user_settings(cx, |settings| { + settings.project_panel.get_or_insert_default().hide_root = Some(true); + }); + }); }); app_state @@ -3409,14 +3382,11 @@ async fn test_history_items_vs_very_good_external_match(cx: &mut gpui::TestAppCo let app_state = init_test(cx); cx.update(|cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - hide_root: true, - ..settings - }, - cx, - ); + cx.update_global::(|store, cx| { + store.update_user_settings(cx, |settings| { + settings.project_panel.get_or_insert_default().hide_root = Some(true); + }); + }); }); app_state @@ -3468,14 +3438,11 @@ async fn test_nonexistent_history_items_not_shown(cx: &mut gpui::TestAppContext) let app_state = init_test(cx); cx.update(|cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - hide_root: true, - ..settings - }, - cx, - ); + cx.update_global::(|store, cx| { + store.update_user_settings(cx, |settings| { + settings.project_panel.get_or_insert_default().hide_root = Some(true); + }); + }); }); app_state @@ -3953,14 +3920,11 @@ async fn test_selected_match_stays_selected_after_matches_refreshed(cx: &mut gpu let app_state = init_test(cx); cx.update(|cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - hide_root: true, - ..settings - }, - cx, - ); + cx.update_global::(|store, cx| { + store.update_user_settings(cx, |settings| { + settings.project_panel.get_or_insert_default().hide_root = Some(true); + }); + }); }); app_state.fs.as_fake().insert_tree("/src", json!({})).await; @@ -4792,14 +4756,11 @@ async fn test_filename_precedence(cx: &mut TestAppContext) { let app_state = init_test(cx); cx.update(|cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - hide_root: true, - ..settings - }, - cx, - ); + cx.update_global::(|store, cx| { + store.update_user_settings(cx, |settings| { + settings.project_panel.get_or_insert_default().hide_root = Some(true); + }); + }); }); app_state @@ -4848,14 +4809,11 @@ async fn test_paths_with_starting_slash(cx: &mut TestAppContext) { let app_state = init_test(cx); cx.update(|cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - hide_root: true, - ..settings - }, - cx, - ); + cx.update_global::(|store, cx| { + store.update_user_settings(cx, |settings| { + settings.project_panel.get_or_insert_default().hide_root = Some(true); + }); + }); }); app_state diff --git a/crates/git_ui/Cargo.toml b/crates/git_ui/Cargo.toml index f46146ef8c..528c609b59 100644 --- a/crates/git_ui/Cargo.toml +++ b/crates/git_ui/Cargo.toml @@ -29,7 +29,6 @@ component.workspace = true db.workspace = true editor.workspace = true file_icons.workspace = true -fs.workspace = true futures-lite.workspace = true futures.workspace = true fuzzy.workspace = true @@ -48,13 +47,10 @@ panel.workspace = true picker.workspace = true project.workspace = true prompt_store.workspace = true -proto.workspace = true rand.workspace = true release_channel.workspace = true -remote.workspace = true remote_connection.workspace = true schemars.workspace = true -search.workspace = true serde.workspace = true serde_json.workspace = true settings.workspace = true @@ -74,8 +70,8 @@ ui_input.workspace = true util.workspace = true watch.workspace = true workspace.workspace = true +git_ui_core.workspace = true zed_actions.workspace = true -zeroize.workspace = true ztracing.workspace = true [target.'cfg(windows)'.dependencies] @@ -93,6 +89,7 @@ pretty_assertions.workspace = true project = { workspace = true, features = ["test-support"] } rand.workspace = true remote_connection = { workspace = true, features = ["test-support"] } +search.workspace = true settings = { workspace = true, features = ["test-support"] } task.workspace = true unindent.workspace = true @@ -100,4 +97,4 @@ workspace = { workspace = true, features = ["test-support"] } zlog.workspace = true [package.metadata.cargo-machete] -ignored = ["tracing"] +ignored = ["remote_connection", "tracing"] diff --git a/crates/git_ui/src/branch_picker.rs b/crates/git_ui/src/branch_picker.rs index 3732355871..942cf0f660 100644 --- a/crates/git_ui/src/branch_picker.rs +++ b/crates/git_ui/src/branch_picker.rs @@ -27,7 +27,8 @@ use util::ResultExt; use workspace::notifications::DetachAndPromptErr; use workspace::{ModalView, Workspace}; -use crate::{branch_picker, git_panel::show_error_toast}; +use crate::branch_picker; +use git_ui_core::notifications::show_error_toast; actions!( branch_picker, diff --git a/crates/git_ui/src/git_graph.rs b/crates/git_ui/src/git_graph.rs index d9fc6bf52c..ffc9439190 100644 --- a/crates/git_ui/src/git_graph.rs +++ b/crates/git_ui/src/git_graph.rs @@ -18,9 +18,9 @@ use git::{ status::{FileStatus, StatusCode, TrackedStatus}, }; use gpui::{ - Anchor, AnyElement, App, Bounds, ClickEvent, ClipboardItem, DefiniteLength, DismissEvent, - DragMoveEvent, ElementId, Empty, Entity, EventEmitter, FocusHandle, Focusable, Hsla, - MouseButton, MouseDownEvent, PathBuilder, Pixels, Point, ScrollHandle, ScrollStrategy, + Action, Anchor, AnyElement, App, Bounds, ClickEvent, ClipboardItem, DefiniteLength, + DismissEvent, DragMoveEvent, ElementId, Empty, Entity, EventEmitter, FocusHandle, Focusable, + Hsla, MouseButton, MouseDownEvent, PathBuilder, Pixels, Point, ScrollHandle, ScrollStrategy, ScrollWheelEvent, SharedString, Subscription, Task, TextStyleRefinement, UniformListScrollHandle, WeakEntity, Window, actions, anchored, deferred, point, prelude::*, px, uniform_list, @@ -36,10 +36,6 @@ use project::{ RepositoryEvent, RepositoryId, }, }; -use search::{ - SearchOption, SearchOptions, SearchSource, SelectNextMatch, SelectPreviousMatch, - ToggleCaseSensitive, buffer_search, -}; use smallvec::{SmallVec, smallvec}; use std::{ cell::Cell, @@ -48,6 +44,10 @@ use std::{ sync::{Arc, OnceLock}, time::{Duration, Instant}, }; +use zed_actions::{ + buffer_search, + search::{SelectNextMatch, SelectPreviousMatch, ToggleCaseSensitive}, +}; use theme::AccentColors; use time::{OffsetDateTime, UtcOffset, format_description::BorrowedFormatItem}; @@ -2542,14 +2542,6 @@ impl GitGraph { .focus_handle(cx) .tab_index(1) .tab_stop(true); - let search_options = { - let mut options = SearchOptions::NONE; - options.set( - SearchOptions::CASE_SENSITIVE, - self.search_state.case_sensitive, - ); - options - }; h_flex() .key_context("GitGraphSearchBar") @@ -2575,11 +2567,29 @@ impl GitGraph { .bg(color.toolbar_background) .on_action(cx.listener(Self::confirm_search)) .child(self.search_state.editor.clone()) - .child(SearchOption::CaseSensitive.as_button( - search_options, - SearchSource::Buffer, - query_focus_handle, - )), + .child({ + let focus_handle = query_focus_handle.clone(); + IconButton::new("git-graph-search-case-sensitive", IconName::CaseSensitive) + .shape(ui::IconButtonShape::Square) + .toggle_state(self.search_state.case_sensitive) + .on_click({ + let focus_handle = query_focus_handle.clone(); + move |_, window, cx| { + if !focus_handle.is_focused(window) { + window.focus(&focus_handle, cx); + } + window.dispatch_action(ToggleCaseSensitive.boxed_clone(), cx); + } + }) + .tooltip(move |_window, cx| { + Tooltip::for_action_in( + "Match Case Sensitivity", + &ToggleCaseSensitive, + &focus_handle, + cx, + ) + }) + }), ) .child( h_flex() diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 7951fac48c..b4f36c7b0b 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -1,4 +1,6 @@ -use crate::askpass_modal::AskPassModal; +use git_ui_core::askpass_modal::AskPassModal; +pub(crate) use git_ui_core::notifications::{open_output, show_error_toast}; + use crate::commit_context_menu::{ CommitContextMenuData, CommitContextMenuSource, commit_context_menu, }; @@ -69,7 +71,7 @@ use project::{ project_settings::{GitPathStyle, ProjectSettings}, }; use prompt_store::RULES_FILE_NAMES; -use proto::RpcError; + use serde::{Deserialize, Serialize}; use settings::{ GitPanelClickBehavior, GitPanelGroupBy, GitPanelSortBy, Settings, SettingsStore, StatusStyle, @@ -8785,86 +8787,6 @@ impl Component for PanelRepoFooter { } } -pub(crate) fn open_output( - operation: impl Into, - workspace: &mut Workspace, - output: &str, - window: &mut Window, - cx: &mut Context, -) { - let operation = operation.into(); - - let plain_text = terminal::strip_ansi_text(output.as_bytes()); - - let buffer = cx.new(|cx| Buffer::local(plain_text.as_str(), cx)); - buffer.update(cx, |buffer, cx| { - buffer.set_capability(language::Capability::ReadOnly, cx); - }); - let editor = cx.new(|cx| { - let mut editor = Editor::for_buffer(buffer, None, window, cx); - editor.buffer().update(cx, |buffer, cx| { - buffer.set_title(format!("Output from git {operation}"), cx); - }); - editor.set_read_only(true); - editor - }); - - workspace.add_item_to_center(Box::new(editor), window, cx); -} - -pub(crate) fn show_error_toast( - workspace: Entity, - action: impl Into, - e: anyhow::Error, - cx: &mut App, -) { - let action = action.into(); - let message = format_git_error_toast_message(&e); - if message - .matches(git::repository::REMOTE_CANCELLED_BY_USER) - .next() - .is_some() - { // Hide the cancelled by user message - } else { - cx.defer(move |cx| { - workspace.update(cx, |workspace, cx| { - let workspace_weak = cx.weak_entity(); - let toast = StatusToast::new(format!("git {} failed", action), cx, |this, _cx| { - this.icon( - Icon::new(IconName::XCircle) - .size(IconSize::Small) - .color(Color::Error), - ) - .action("View Log", move |window, cx| { - let message = message.clone(); - let action = action.clone(); - workspace_weak - .update(cx, move |workspace, cx| { - open_output(action, workspace, &message, window, cx) - }) - .ok(); - }) - }); - workspace.toggle_status_toast(toast, cx) - }); - }); - } -} - -fn rpc_error_raw_message_from_chain(error: &anyhow::Error) -> Option<&str> { - error - .chain() - .find_map(|cause| cause.downcast_ref::().map(RpcError::raw_message)) -} - -fn format_git_error_toast_message(error: &anyhow::Error) -> String { - if let Some(message) = rpc_error_raw_message_from_chain(error) { - message.trim().to_string() - } else { - error.to_string().trim().to_string() - } -} - pub(crate) fn commit_title_exceeds_limit(title: &str, max_length: usize) -> bool { max_length > 0 && title.chars().count() > max_length } @@ -9379,47 +9301,6 @@ mod tests { .await; } - #[test] - fn test_format_git_error_toast_message_prefers_raw_rpc_message() { - let rpc_error = RpcError::from_proto( - &proto::Error { - message: - "Your local changes to the following files would be overwritten by merge\n" - .to_string(), - code: proto::ErrorCode::Internal as i32, - tags: Default::default(), - }, - "Pull", - ); - - let message = format_git_error_toast_message(&rpc_error); - assert_eq!( - message, - "Your local changes to the following files would be overwritten by merge" - ); - } - - #[test] - fn test_format_git_error_toast_message_prefers_raw_rpc_message_when_wrapped() { - let rpc_error = RpcError::from_proto( - &proto::Error { - message: - "Your local changes to the following files would be overwritten by merge\n" - .to_string(), - code: proto::ErrorCode::Internal as i32, - tags: Default::default(), - }, - "Pull", - ); - let wrapped = rpc_error.context("sending pull request"); - - let message = format_git_error_toast_message(&wrapped); - assert_eq!( - message, - "Your local changes to the following files would be overwritten by merge" - ); - } - #[gpui::test] async fn test_history_tab_stops_loading_for_unborn_branch(cx: &mut TestAppContext) { init_test(cx); diff --git a/crates/git_ui/src/git_ui.rs b/crates/git_ui/src/git_ui.rs index 39e9299bc4..d9d0d3370b 100644 --- a/crates/git_ui/src/git_ui.rs +++ b/crates/git_ui/src/git_ui.rs @@ -33,7 +33,6 @@ use crate::{ text_diff_view::TextDiffView, }; -mod askpass_modal; pub mod branch_diff; pub mod branch_picker; mod commit_context_menu; @@ -41,9 +40,7 @@ mod commit_modal; pub mod commit_tooltip; pub mod commit_view; mod conflict_view; -pub mod created_worktrees; mod diff_multibuffer; -pub mod file_diff_view; pub mod git_graph; pub mod git_panel; mod git_panel_settings; @@ -59,9 +56,6 @@ pub mod staged_diff; pub mod stash_picker; pub mod text_diff_view; pub mod unstaged_diff; -pub mod worktree_names; -pub mod worktree_picker; -pub mod worktree_service; pub use blame_ui::GitBlameStatus; pub use conflict_view::MergeConflictIndicator; @@ -85,6 +79,40 @@ pub fn init(cx: &mut App) { commit_view::init(cx); git_graph::init(cx); + git_ui_core::set_branch_picker_builder( + |workspace, repository, window, cx| { + let picker = git_picker::popover( + workspace, + repository, + git_picker::GitPickerTab::Branches, + gpui::rems(34.), + window, + cx, + ); + cx.new(|cx| git_ui_core::GitPickerPopover::new(picker, cx)) + }, + cx, + ); + + git_ui_core::set_file_history_opener( + |workspace, project_path, window, cx| { + let Some((repo_id, log_source)) = + git_graph::resolve_file_history_target_from_project_path( + workspace, + project_path, + cx, + ) + else { + return; + }; + let git_store = workspace.project().read(cx).git_store().clone(); + git_graph::open_or_reuse_graph( + workspace, repo_id, git_store, log_source, None, window, cx, + ); + }, + cx, + ); + cx.observe_new(|editor: &mut Editor, _, cx| { conflict_view::register_editor(editor, editor.buffer().clone(), cx); }) @@ -102,12 +130,16 @@ pub fn init(cx: &mut App) { workspace.register_action( |workspace, action: &zed_actions::CreateWorktree, window, cx| { - worktree_service::handle_create_worktree(workspace, action, window, None, cx); + git_ui_core::worktree_service::handle_create_worktree( + workspace, action, window, None, cx, + ); }, ); workspace.register_action( |workspace, action: &zed_actions::SwitchWorktree, window, cx| { - worktree_service::handle_switch_worktree(workspace, action, window, None, cx); + git_ui_core::worktree_service::handle_switch_worktree( + workspace, action, window, None, cx, + ); }, ); @@ -116,7 +148,7 @@ pub fn init(cx: &mut App) { let project = workspace.project().clone(); let workspace_handle = workspace.weak_handle(); workspace.toggle_modal(window, cx, |window, cx| { - worktree_picker::WorktreePicker::new_modal( + git_ui_core::worktree_picker::WorktreePicker::new_modal( project, workspace_handle, focused_dock, @@ -138,7 +170,7 @@ pub fn init(cx: &mut App) { let workspace_handle = workspace.weak_handle(); cx.spawn_in(window, async move |_, cx| { if let Some(connection_options) = connection_options { - crate::worktree_picker::open_remote_worktree( + git_ui_core::worktree_picker::open_remote_worktree( connection_options, vec![path], app_state, diff --git a/crates/git_ui/src/multi_diff_view.rs b/crates/git_ui/src/multi_diff_view.rs index 7834a7319f..46e4b2b637 100644 --- a/crates/git_ui/src/multi_diff_view.rs +++ b/crates/git_ui/src/multi_diff_view.rs @@ -1,10 +1,10 @@ -use crate::file_diff_view::build_buffer_diff; use anyhow::Result; use buffer_diff::BufferDiff; use editor::{ Editor, EditorEvent, MultiBuffer, RestoreOnlyUnstagedDiffHunkDelegate, multibuffer_context_lines, }; +use git_ui_core::file_diff_view::build_buffer_diff; use gpui::{ AnyElement, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, FocusHandle, Focusable, Font, IntoElement, Render, SharedString, Task, Window, diff --git a/crates/git_ui_core/Cargo.toml b/crates/git_ui_core/Cargo.toml new file mode 100644 index 0000000000..9538fec421 --- /dev/null +++ b/crates/git_ui_core/Cargo.toml @@ -0,0 +1,62 @@ +[package] +name = "git_ui_core" +version = "0.1.0" +edition.workspace = true +publish.workspace = true +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +name = "git_ui_core" +path = "src/git_ui_core.rs" + +[dependencies] +anyhow.workspace = true +askpass.workspace = true +buffer_diff.workspace = true +collections.workspace = true +db.workspace = true +editor.workspace = true +fs.workspace = true +futures.workspace = true +fuzzy.workspace = true +git.workspace = true +gpui.workspace = true +language.workspace = true +log.workspace = true +menu.workspace = true +notifications.workspace = true +picker.workspace = true +project.workspace = true +proto.workspace = true +rand.workspace = true +remote.workspace = true +remote_connection.workspace = true +serde.workspace = true +serde_json.workspace = true +settings.workspace = true +terminal.workspace = true +ui.workspace = true +util.workspace = true +watch.workspace = true +workspace.workspace = true +zed_actions.workspace = true +zeroize.workspace = true +ztracing.workspace = true + +[dev-dependencies] +editor = { workspace = true, features = ["test-support"] } +fs = { workspace = true, features = ["test-support"] } +git = { workspace = true, features = ["test-support"] } +gpui = { workspace = true, features = ["test-support"] } +project = { workspace = true, features = ["test-support"] } +remote_connection = { workspace = true, features = ["test-support"] } +settings = { workspace = true, features = ["test-support"] } +task.workspace = true +theme.workspace = true +theme_settings.workspace = true +unindent.workspace = true +workspace = { workspace = true, features = ["test-support"] } +zlog.workspace = true diff --git a/crates/git_ui_core/LICENSE-GPL b/crates/git_ui_core/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/git_ui_core/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/git_ui/src/askpass_modal.rs b/crates/git_ui_core/src/askpass_modal.rs similarity index 99% rename from crates/git_ui/src/askpass_modal.rs rename to crates/git_ui_core/src/askpass_modal.rs index bbd507cfc4..1e63a0d03e 100644 --- a/crates/git_ui/src/askpass_modal.rs +++ b/crates/git_ui_core/src/askpass_modal.rs @@ -12,7 +12,7 @@ use util::maybe; use workspace::ModalView; use zeroize::Zeroize; -pub(crate) struct AskPassModal { +pub struct AskPassModal { operation: SharedString, prompt: SharedString, editor: Entity, diff --git a/crates/git_ui/src/created_worktrees.rs b/crates/git_ui_core/src/created_worktrees.rs similarity index 100% rename from crates/git_ui/src/created_worktrees.rs rename to crates/git_ui_core/src/created_worktrees.rs diff --git a/crates/git_ui/src/file_diff_view.rs b/crates/git_ui_core/src/file_diff_view.rs similarity index 99% rename from crates/git_ui/src/file_diff_view.rs rename to crates/git_ui_core/src/file_diff_view.rs index 2b71d6988b..1d61075bcc 100644 --- a/crates/git_ui/src/file_diff_view.rs +++ b/crates/git_ui_core/src/file_diff_view.rs @@ -204,7 +204,7 @@ impl FileDiffView { } #[ztracing::instrument(skip_all)] -pub(crate) async fn build_buffer_diff( +pub async fn build_buffer_diff( old_buffer: &Entity, new_buffer: &Entity, cx: &mut AsyncApp, diff --git a/crates/git_ui_core/src/git_ui_core.rs b/crates/git_ui_core/src/git_ui_core.rs new file mode 100644 index 0000000000..4c89fcd352 --- /dev/null +++ b/crates/git_ui_core/src/git_ui_core.rs @@ -0,0 +1,116 @@ +use std::rc::Rc; + +use gpui::{ + AnyView, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Global, + IntoElement, ManagedView, Render, Subscription, WeakEntity, Window, +}; +use project::{ProjectPath, git_store::Repository}; +use workspace::Workspace; + +pub mod askpass_modal; +pub mod created_worktrees; +pub mod file_diff_view; +pub mod notifications; +pub mod worktree_names; +pub mod worktree_picker; +pub mod worktree_service; + +/// A type-erased picker view, e.g. for the title bar's branch popover. +/// +/// The concrete picker lives in a higher-level crate, which installs a +/// builder via [`set_branch_picker_builder`]; keeping only this wrapper here +/// lets consumers avoid depending on that crate. +pub struct GitPickerPopover { + view: AnyView, + focus_handle: FocusHandle, + _dismiss_subscription: Subscription, +} + +impl GitPickerPopover { + pub fn new(view: Entity, cx: &mut Context) -> Self { + Self { + focus_handle: view.focus_handle(cx), + _dismiss_subscription: cx + .subscribe(&view, |_, _, _: &DismissEvent, cx| cx.emit(DismissEvent)), + view: view.into(), + } + } +} + +impl EventEmitter for GitPickerPopover {} + +impl Focusable for GitPickerPopover { + fn focus_handle(&self, _cx: &App) -> FocusHandle { + self.focus_handle.clone() + } +} + +impl Render for GitPickerPopover { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + self.view.clone() + } +} + +type BranchPickerBuilder = dyn Fn( + WeakEntity, + Option>, + &mut Window, + &mut App, +) -> Entity; + +struct BranchPickerBuilderGlobal(Rc); + +impl Global for BranchPickerBuilderGlobal {} + +pub fn set_branch_picker_builder( + builder: impl Fn( + WeakEntity, + Option>, + &mut Window, + &mut App, + ) -> Entity + + 'static, + cx: &mut App, +) { + cx.set_global(BranchPickerBuilderGlobal(Rc::new(builder))); +} + +pub fn build_branch_picker( + workspace: WeakEntity, + repository: Option>, + window: &mut Window, + cx: &mut App, +) -> Option> { + let builder = cx.try_global::()?.0.clone(); + Some(builder(workspace, repository, window, cx)) +} + +type FileHistoryOpener = dyn Fn(&mut Workspace, &ProjectPath, &mut Window, &mut Context); + +struct FileHistoryOpenerGlobal(Rc); + +impl Global for FileHistoryOpenerGlobal {} + +pub fn set_file_history_opener( + opener: impl Fn(&mut Workspace, &ProjectPath, &mut Window, &mut Context) + 'static, + cx: &mut App, +) { + cx.set_global(FileHistoryOpenerGlobal(Rc::new(opener))); +} + +/// Opens the file history view for `path`, if an opener was installed via +/// [`set_file_history_opener`]. +pub fn open_file_history( + workspace: &mut Workspace, + path: &ProjectPath, + window: &mut Window, + cx: &mut Context, +) { + let Some(opener) = cx + .try_global::() + .map(|opener| opener.0.clone()) + else { + return; + }; + opener(workspace, path, window, cx); +} diff --git a/crates/git_ui_core/src/notifications.rs b/crates/git_ui_core/src/notifications.rs new file mode 100644 index 0000000000..5fc2932607 --- /dev/null +++ b/crates/git_ui_core/src/notifications.rs @@ -0,0 +1,133 @@ +use editor::Editor; +use gpui::{App, Context, Entity, SharedString}; +use language::Buffer; +use notifications::status_toast::StatusToast; +use proto::RpcError; +use ui::prelude::*; +use workspace::Workspace; + +pub fn open_output( + operation: impl Into, + workspace: &mut Workspace, + output: &str, + window: &mut Window, + cx: &mut Context, +) { + let operation = operation.into(); + + let plain_text = terminal::strip_ansi_text(output.as_bytes()); + + let buffer = cx.new(|cx| Buffer::local(plain_text.as_str(), cx)); + buffer.update(cx, |buffer, cx| { + buffer.set_capability(language::Capability::ReadOnly, cx); + }); + let editor = cx.new(|cx| { + let mut editor = Editor::for_buffer(buffer, None, window, cx); + editor.buffer().update(cx, |buffer, cx| { + buffer.set_title(format!("Output from git {operation}"), cx); + }); + editor.set_read_only(true); + editor + }); + + workspace.add_item_to_center(Box::new(editor), window, cx); +} + +pub fn show_error_toast( + workspace: Entity, + action: impl Into, + e: anyhow::Error, + cx: &mut App, +) { + let action = action.into(); + let message = format_git_error_toast_message(&e); + if message + .matches(git::repository::REMOTE_CANCELLED_BY_USER) + .next() + .is_some() + { // Hide the cancelled by user message + } else { + cx.defer(move |cx| { + workspace.update(cx, |workspace, cx| { + let workspace_weak = cx.weak_entity(); + let toast = StatusToast::new(format!("git {} failed", action), cx, |this, _cx| { + this.icon( + Icon::new(IconName::XCircle) + .size(IconSize::Small) + .color(Color::Error), + ) + .action("View Log", move |window, cx| { + let message = message.clone(); + let action = action.clone(); + workspace_weak + .update(cx, move |workspace, cx| { + open_output(action, workspace, &message, window, cx) + }) + .ok(); + }) + }); + workspace.toggle_status_toast(toast, cx) + }); + }); + } +} + +fn rpc_error_raw_message_from_chain(error: &anyhow::Error) -> Option<&str> { + error + .chain() + .find_map(|cause| cause.downcast_ref::().map(RpcError::raw_message)) +} + +fn format_git_error_toast_message(error: &anyhow::Error) -> String { + if let Some(message) = rpc_error_raw_message_from_chain(error) { + message.trim().to_string() + } else { + error.to_string().trim().to_string() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_format_git_error_toast_message_prefers_raw_rpc_message() { + let rpc_error = RpcError::from_proto( + &proto::Error { + message: + "Your local changes to the following files would be overwritten by merge\n" + .to_string(), + code: proto::ErrorCode::Internal as i32, + tags: Default::default(), + }, + "Pull", + ); + + let message = format_git_error_toast_message(&rpc_error); + assert_eq!( + message, + "Your local changes to the following files would be overwritten by merge" + ); + } + + #[test] + fn test_format_git_error_toast_message_prefers_raw_rpc_message_when_wrapped() { + let rpc_error = RpcError::from_proto( + &proto::Error { + message: + "Your local changes to the following files would be overwritten by merge\n" + .to_string(), + code: proto::ErrorCode::Internal as i32, + tags: Default::default(), + }, + "Pull", + ); + let wrapped = rpc_error.context("sending pull request"); + + let message = format_git_error_toast_message(&wrapped); + assert_eq!( + message, + "Your local changes to the following files would be overwritten by merge" + ); + } +} diff --git a/crates/git_ui/src/worktree_names.rs b/crates/git_ui_core/src/worktree_names.rs similarity index 100% rename from crates/git_ui/src/worktree_names.rs rename to crates/git_ui_core/src/worktree_names.rs diff --git a/crates/git_ui/src/worktree_picker.rs b/crates/git_ui_core/src/worktree_picker.rs similarity index 99% rename from crates/git_ui/src/worktree_picker.rs rename to crates/git_ui_core/src/worktree_picker.rs index 9d23880fbc..67ddbe8c23 100644 --- a/crates/git_ui/src/worktree_picker.rs +++ b/crates/git_ui_core/src/worktree_picker.rs @@ -23,7 +23,7 @@ use workspace::{ ModalView, MultiWorkspace, Workspace, dock::DockPosition, notifications::DetachAndPromptErr, }; -use crate::git_panel::show_error_toast; +use crate::notifications::show_error_toast; use crate::worktree_service::{RemoteBranchName, WorktreeCreateTarget, worktree_create_targets}; use zed_actions::{ CreateWorktree, NewWorktreeBranchTarget, OpenWorktreeInNewWindow, OpenWorktreeSetupTasks, diff --git a/crates/git_ui/src/worktree_service.rs b/crates/git_ui_core/src/worktree_service.rs similarity index 99% rename from crates/git_ui/src/worktree_service.rs rename to crates/git_ui_core/src/worktree_service.rs index 2425cbdf38..fece13da04 100644 --- a/crates/git_ui/src/worktree_service.rs +++ b/crates/git_ui_core/src/worktree_service.rs @@ -28,7 +28,7 @@ use git::repository::{FetchOptions, Remote}; use util::ResultExt as _; use crate::askpass_modal::AskPassModal; -use crate::git_panel::{open_output, show_error_toast}; +use crate::notifications::{open_output, show_error_toast}; use crate::worktree_names; /// A remote-tracking branch reference parsed into its remote and branch parts, diff --git a/crates/open_path_prompt/Cargo.toml b/crates/open_path_prompt/Cargo.toml index 05d6a200f8..e635797cfb 100644 --- a/crates/open_path_prompt/Cargo.toml +++ b/crates/open_path_prompt/Cargo.toml @@ -12,7 +12,6 @@ fuzzy.workspace = true gpui.workspace = true picker.workspace = true project.workspace = true -project_panel.workspace = true schemars.workspace = true serde.workspace = true settings.workspace = true diff --git a/crates/open_path_prompt/src/open_path_prompt.rs b/crates/open_path_prompt/src/open_path_prompt.rs index cbbebeb6a7..239edb676a 100644 --- a/crates/open_path_prompt/src/open_path_prompt.rs +++ b/crates/open_path_prompt/src/open_path_prompt.rs @@ -10,7 +10,7 @@ use fuzzy::{CharBag, StringMatch, StringMatchCandidate}; use gpui::{HighlightStyle, StyledText, Task}; use picker::{Picker, PickerDelegate}; use project::{DirectoryItem, DirectoryLister}; -use project_panel::project_panel_settings::ProjectPanelSettings; +use settings::SettingsStore; use settings::{ProjectPanelSortMode, Settings}; use std::{ path::{self, Path, PathBuf}, @@ -54,8 +54,9 @@ impl OpenPathDelegate { cx: &App, ) -> Self { let path_style = lister.path_style(cx); - let sort_mode = ProjectPanelSettings::try_get(cx) - .map(|s| s.sort_mode) + let sort_mode = cx + .try_global::() + .and_then(|store| store.merged_settings().project_panel.as_ref()?.sort_mode) .unwrap_or_default(); Self { tx: Some(tx), diff --git a/crates/project_panel/Cargo.toml b/crates/project_panel/Cargo.toml index 6f9d412579..1601ea78ba 100644 --- a/crates/project_panel/Cargo.toml +++ b/crates/project_panel/Cargo.toml @@ -22,7 +22,7 @@ collections.workspace = true editor.workspace = true file_icons.workspace = true futures.workspace = true -git_ui.workspace = true +git_ui_core.workspace = true git.workspace = true gpui.workspace = true itertools.workspace = true @@ -30,7 +30,6 @@ menu.workspace = true pretty_assertions.workspace = true project.workspace = true schemars.workspace = true -search.workspace = true serde.workspace = true serde_json.workspace = true settings.workspace = true diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 067adcd530..ea394947c8 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -17,8 +17,7 @@ use file_icons::FileIcons; use fs::TrashId; use git; use git::status::GitSummary; -use git_ui; -use git_ui::file_diff_view::FileDiffView; +use git_ui_core::file_diff_view::FileDiffView; use gpui::{ Action, AnyElement, App, AsyncWindowContext, Bounds, ClipboardEntry as GpuiClipboardEntry, ClipboardItem, Context, CursorStyle, DismissEvent, Div, DragMoveEvent, Entity, EventEmitter, @@ -544,10 +543,10 @@ pub fn init(cx: &mut App) { } }); - // Forwards `git::FileHistory` to `git_ui::git_graph` when the project - // panel is the focused source of selection. Lives here (and not in - // `git_ui`) so that `git_ui` does not need to depend on - // `project_panel`, which would create a dependency cycle. + // Forwards `git::FileHistory` to the file history opener installed by + // `git_ui` when the project panel is the focused source of selection. + // Lives here (and not in `git_ui`) so that `git_ui` does not need to + // depend on `project_panel`, which would create a dependency cycle. workspace.register_action_renderer(|div, workspace, window, cx| { let Some(panel) = workspace.panel::(cx) else { return div; @@ -569,19 +568,7 @@ pub fn init(cx: &mut App) { else { return; }; - let Some((repo_id, log_source)) = - git_ui::git_graph::resolve_file_history_target_from_project_path( - workspace, - &project_path, - cx, - ) - else { - return; - }; - let git_store = workspace.project().read(cx).git_store().clone(); - git_ui::git_graph::open_or_reuse_graph( - workspace, repo_id, git_store, log_source, None, window, cx, - ); + git_ui_core::open_file_history(workspace, &project_path, window, cx); }) .log_err(); cx.stop_propagation(); @@ -3881,16 +3868,10 @@ impl ProjectPanel { Some(parent) => Arc::from(parent), None => { // File at root, open search with empty filter - self.workspace - .update(cx, |workspace, cx| { - search::ProjectSearchView::new_search_in_directory( - workspace, - RelPath::empty(), - window, - cx, - ); - }) - .ok(); + window.dispatch_action( + Box::new(zed_actions::search::NewSearchInDirectory::default()), + cx, + ); return; } } @@ -3903,13 +3884,13 @@ impl ProjectPanel { dir_path.to_rel_path_buf() }; - self.workspace - .update(cx, |workspace, cx| { - search::ProjectSearchView::new_search_in_directory( - workspace, &dir_path, window, cx, - ); - }) - .ok(); + let directory = dir_path + .display(self.project.read(cx).path_style(cx)) + .into_owned(); + window.dispatch_action( + Box::new(zed_actions::search::NewSearchInDirectory { directory }), + cx, + ); } } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 22a56c7e38..719d44b57d 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -51,7 +51,7 @@ use ui::{ CommonAnimationExt, IconButtonShape, KeyBinding, Toggleable, Tooltip, prelude::*, utils::SearchInputWidth, }; -use util::{ResultExt as _, paths::PathMatcher, rel_path::RelPath}; +use util::{ResultExt as _, paths::PathMatcher}; use workspace::{ DeploySearch, ItemNavHistory, NewSearch, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId, @@ -227,6 +227,17 @@ pub fn init(cx: &mut App) { ProjectSearchView::new_search(workspace, action, window, cx); cx.notify(); }); + workspace.register_action( + move |workspace, action: &zed_actions::search::NewSearchInDirectory, window, cx| { + ProjectSearchView::new_search_with_filter( + workspace, + action.directory.clone(), + window, + cx, + ); + cx.notify(); + }, + ); }) .detach(); } @@ -1193,14 +1204,12 @@ impl ProjectSearchView { this } - pub fn new_search_in_directory( + pub fn new_search_with_filter( workspace: &mut Workspace, - dir_path: &RelPath, + filter_str: String, window: &mut Window, cx: &mut Context, ) { - let filter_str = dir_path.display(workspace.path_style(cx)); - let weak_workspace = cx.entity().downgrade(); let entity = cx.new(|cx| ProjectSearch::new(workspace.project().clone(), cx)); @@ -4143,9 +4152,20 @@ pub mod tests { .clone() }); assert!(a_dir_entry.is_dir()); - workspace.update_in(cx, |workspace, window, cx| { - ProjectSearchView::new_search_in_directory(workspace, &a_dir_entry.path, window, cx) + let directory = cx.update(|_, cx| { + a_dir_entry + .path + .display(workspace.read(cx).path_style(cx)) + .into_owned() }); + window + .update(cx, |_, window, cx| { + window.dispatch_action( + Box::new(zed_actions::search::NewSearchInDirectory { directory }), + cx, + ); + }) + .unwrap(); let Some(search_view) = cx.read(|cx| { workspace diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index cef989dbdf..63702a2257 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -9,7 +9,9 @@ use ui::{IconButtonShape, Tooltip, prelude::*}; use util::paths::PathMatcher; use workspace::notifications::NotificationId; use workspace::{Toast, Workspace}; -pub use zed_actions::search::ToggleIncludeIgnored; +pub use zed_actions::search::{ + FocusSearch, SelectNextMatch, SelectPreviousMatch, ToggleCaseSensitive, ToggleIncludeIgnored, +}; pub use search_status_button::SEARCH_ICON; @@ -31,22 +33,14 @@ pub fn init(cx: &mut App) { actions!( search, [ - /// Focuses on the search input field. - FocusSearch, /// Toggles whole word matching. ToggleWholeWord, - /// Toggles case-sensitive search. - ToggleCaseSensitive, /// Toggles regular expression mode. ToggleRegex, /// Toggles the replace interface. ToggleReplace, /// Toggles searching within selection only. ToggleSelection, - /// Selects the next search match. - SelectNextMatch, - /// Selects the previous search match. - SelectPreviousMatch, /// Selects all search matches. SelectAllMatches, /// Cycles through search modes. diff --git a/crates/sidebar/Cargo.toml b/crates/sidebar/Cargo.toml index ef3f255ee7..e15b7b339e 100644 --- a/crates/sidebar/Cargo.toml +++ b/crates/sidebar/Cargo.toml @@ -28,7 +28,6 @@ editor.workspace = true feature_flags.workspace = true fs.workspace = true git.workspace = true -git_ui.workspace = true gpui.workspace = true itertools.workspace = true language_model.workspace = true @@ -50,6 +49,7 @@ ui.workspace = true unicode-segmentation.workspace = true util.workspace = true workspace.workspace = true +git_ui_core.workspace = true zed_actions.workspace = true [dev-dependencies] diff --git a/crates/sidebar/src/sidebar.rs b/crates/sidebar/src/sidebar.rs index f55618fc2b..70baec374f 100644 --- a/crates/sidebar/src/sidebar.rs +++ b/crates/sidebar/src/sidebar.rs @@ -69,7 +69,7 @@ use workspace::{ notifications::NotificationId, sidebar_side_context_menu, }; -use git_ui::worktree_service::{RemoteBranchName, worktree_create_targets}; +use git_ui_core::worktree_service::{RemoteBranchName, worktree_create_targets}; use zed_actions::editor::{MoveDown, MoveUp}; use zed_actions::{CreateWorktree, NewWorktreeBranchTarget, OpenRecent}; @@ -748,7 +748,7 @@ fn create_worktree_in_workspace( ) { workspace.update(cx, |workspace, cx| { let focused_dock = workspace.focused_dock_position(window, cx); - git_ui::worktree_service::handle_create_worktree( + git_ui_core::worktree_service::handle_create_worktree( workspace, &CreateWorktree { worktree_name: None, diff --git a/crates/title_bar/Cargo.toml b/crates/title_bar/Cargo.toml index 90f5dc8c40..a6ee0a1f0a 100644 --- a/crates/title_bar/Cargo.toml +++ b/crates/title_bar/Cargo.toml @@ -42,7 +42,7 @@ cloud_api_types.workspace = true command_palette_hooks.workspace = true db.workspace = true -git_ui.workspace = true +git_ui_core.workspace = true gpui = { workspace = true, features = ["screen-capture"] } icons.workspace = true livekit_client.workspace = true diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index 0d0ea93fdf..f50a762649 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -9,7 +9,7 @@ use crate::application_menu::{ApplicationMenu, show_menus}; use crate::plan_chip::PlanChip; use agent_settings::{AgentSettings, WindowLayout}; use arrayvec::ArrayVec; -use git_ui::worktree_picker::WorktreePicker; +use git_ui_core::worktree_picker::WorktreePicker; pub use platform_title_bar::{ self, DraggedWindowTab, MergeAllWindows, MoveTabToNewWindow, PlatformTitleBar, ShowNextWindowTab, ShowPreviousWindowTab, @@ -1049,14 +1049,12 @@ impl TitleBar { PopoverMenu::new("branch-menu") .menu(move |window, cx| { - Some(git_ui::git_picker::popover( + git_ui_core::build_branch_picker( workspace.downgrade(), effective_repository.clone(), - git_ui::git_picker::GitPickerTab::Branches, - gpui::rems(34.), window, cx, - )) + ) }) .trigger_with_tooltip(trigger, move |_window, cx| { let meta = if is_detached_head { diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 9f3d078481..13822a42fb 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -115,6 +115,7 @@ futures.workspace = true git.workspace = true git_hosting_providers.workspace = true git_ui = { workspace = true, features = ["call"] } +git_ui_core.workspace = true go_to_line.workspace = true system_specs.workspace = true gpui = { workspace = true, features = ["input-latency-histogram", "profiler"] } diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs index b5bc3652f6..754aa9eac3 100644 --- a/crates/zed/src/zed/open_listener.rs +++ b/crates/zed/src/zed/open_listener.rs @@ -13,7 +13,8 @@ use futures::channel::{mpsc, oneshot}; use futures::future; use futures::{FutureExt, StreamExt}; -use git_ui::{file_diff_view::FileDiffView, multi_diff_view::MultiDiffView}; +use git_ui::multi_diff_view::MultiDiffView; +use git_ui_core::file_diff_view::FileDiffView; use gpui::{App, AsyncApp, Global, TaskExt, WindowHandle}; use onboarding::FIRST_OPEN; use onboarding::show_onboarding_view; diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index f7a05fc957..b082a687a2 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -474,10 +474,30 @@ pub mod icon_theme_selector { } pub mod search { - use gpui::actions; + use gpui::{Action, actions}; + + /// Opens a new project search filtered down to the given directory. + /// + /// An internal forwarding action: the user-facing, keybindable entry + /// point is `project_panel::NewSearchInDirectory`, which resolves the + /// selected directory and dispatches this action with it. + #[derive(Clone, Debug, Default, PartialEq, Action)] + #[action(namespace = search, no_json, no_register)] + pub struct NewSearchInDirectory { + pub directory: String, + } + actions!( search, [ + /// Focuses on the search input field. + FocusSearch, + /// Selects the next search match. + SelectNextMatch, + /// Selects the previous search match. + SelectPreviousMatch, + /// Toggles case-sensitive search. + ToggleCaseSensitive, /// Toggles searching in ignored files. ToggleIncludeIgnored ] diff --git a/tooling/xtask/src/tasks/workflows/run_tests.rs b/tooling/xtask/src/tasks/workflows/run_tests.rs index d7fdf84fe5..cb424ffa23 100644 --- a/tooling/xtask/src/tasks/workflows/run_tests.rs +++ b/tooling/xtask/src/tasks/workflows/run_tests.rs @@ -466,6 +466,10 @@ fn check_dependencies() -> NamedJob { named::bash("cargo update --locked --workspace") } + fn check_crate_graph() -> Step { + named::bash("cargo test --package xtask -- workspace::") + } + fn check_vulnerable_dependencies() -> Step { named::uses( "actions", @@ -485,6 +489,7 @@ fn check_dependencies() -> NamedJob { .add_step(install_cargo_machete()) .add_step(run_cargo_machete()) .add_step(check_cargo_lock()) + .add_step(check_crate_graph()) .add_step(check_vulnerable_dependencies()), )) } diff --git a/tooling/xtask/src/workspace.rs b/tooling/xtask/src/workspace.rs index fd71aa6bbd..74d1fed2f4 100644 --- a/tooling/xtask/src/workspace.rs +++ b/tooling/xtask/src/workspace.rs @@ -7,3 +7,95 @@ pub fn load_workspace() -> Result { .exec() .context("failed to load cargo metadata") } + +#[cfg(test)] +mod tests { + use std::collections::{BTreeMap, BTreeSet, VecDeque}; + + use cargo_metadata::DependencyKind; + + use super::load_workspace; + + /// Crates that must not depend on each other, directly or transitively: + /// such edges chain large UI crates one after another and serialize the + /// build, badly hurting incremental compile times. Dev-dependencies are + /// exempt since they don't affect `cargo build`. + const FORBIDDEN_DEPENDENCIES: &[(&str, &str)] = &[ + ("agent_ui", "git_ui"), + ("file_finder", "project_panel"), + ("git_ui", "agent_ui"), + ("git_ui", "search"), + ("open_path_prompt", "project_panel"), + ("picker", "editor"), + ("project_panel", "git_ui"), + ("project_panel", "search"), + ("search", "git_ui"), + ("search", "project_panel"), + ("sidebar", "git_ui"), + ("title_bar", "git_ui"), + ]; + + #[test] + fn no_forbidden_dependencies_between_feature_crates() { + let workspace = load_workspace().expect("failed to load cargo metadata"); + let packages = workspace.workspace_packages(); + let member_names = packages + .iter() + .map(|package| package.name.as_str()) + .collect::>(); + + let mut graph = BTreeMap::new(); + for package in &packages { + let dependencies = package + .dependencies + .iter() + .filter(|dependency| dependency.kind != DependencyKind::Development) + .map(|dependency| dependency.name.as_str()) + .filter(|name| member_names.contains(name)) + .collect::>(); + graph.insert(package.name.as_str(), dependencies); + } + + let mut violations = Vec::new(); + for &(from, to) in FORBIDDEN_DEPENDENCIES { + if let Some(path) = dependency_path(&graph, from, to) { + violations.push(path.join(" -> ")); + } + } + assert_eq!( + violations, + Vec::::new(), + "forbidden dependency paths between sibling feature crates; \ + break the dependency (e.g. by extracting shared code into a lower-level crate) \ + instead of joining these crates into one serial build chain", + ); + } + + fn dependency_path<'a>( + graph: &BTreeMap<&'a str, BTreeSet<&'a str>>, + from: &'a str, + to: &str, + ) -> Option> { + let mut parents = BTreeMap::new(); + let mut queue = VecDeque::from([from]); + while let Some(current) = queue.pop_front() { + if current == to { + let mut path = vec![current]; + let mut node = current; + while let Some(&parent) = parents.get(node) { + path.push(parent); + node = parent; + } + path.reverse(); + return Some(path); + } + for &dependency in graph.get(current).into_iter().flatten() { + if dependency != from && !parents.contains_key(dependency) { + parents.insert(dependency, current); + queue.push_back(dependency); + } + } + } + None + } +}