Speed up cargo build -p zed compilation (#62059)
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
This commit is contained in:
1 parent
0500fd9ee9
commit
01961bef59
41 files changed
+734
-376
No files matched your search
@@ -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
|
||||
|
||||
Generated
+48
-8
@@ -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",
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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<PathBuf> = git_repos
|
||||
.iter()
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<WorktreeStore>, 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::<SettingsStore>()
|
||||
.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(
|
||||
|
||||
@@ -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::<SettingsStore, _>(|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::<SettingsStore, _>(|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::<SettingsStore, _>(|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::<SettingsStore, _>(|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::<SettingsStore, _>(|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::<SettingsStore, _>(|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::<SettingsStore, _>(|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::<SettingsStore, _>(|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::<SettingsStore, _>(|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::<SettingsStore, _>(|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::<SettingsStore, _>(|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::<SettingsStore, _>(|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::<SettingsStore, _>(|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::<SettingsStore, _>(|store, cx| {
|
||||
store.update_user_settings(cx, |settings| {
|
||||
settings.project_panel.get_or_insert_default().hide_root = Some(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
app_state
|
||||
|
||||
@@ -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"]
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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<SharedString>,
|
||||
workspace: &mut Workspace,
|
||||
output: &str,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
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<Workspace>,
|
||||
action: impl Into<SharedString>,
|
||||
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::<RpcError>().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);
|
||||
|
||||
+42
-10
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
@@ -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<Editor>,
|
||||
File renamed without changes.
@@ -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<Buffer>,
|
||||
new_buffer: &Entity<Buffer>,
|
||||
cx: &mut AsyncApp,
|
||||
@@ -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<V: ManagedView>(view: Entity<V>, cx: &mut Context<Self>) -> Self {
|
||||
Self {
|
||||
focus_handle: view.focus_handle(cx),
|
||||
_dismiss_subscription: cx
|
||||
.subscribe(&view, |_, _, _: &DismissEvent, cx| cx.emit(DismissEvent)),
|
||||
view: view.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> 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<Self>) -> impl IntoElement {
|
||||
self.view.clone()
|
||||
}
|
||||
}
|
||||
|
||||
type BranchPickerBuilder = dyn Fn(
|
||||
WeakEntity<Workspace>,
|
||||
Option<Entity<Repository>>,
|
||||
&mut Window,
|
||||
&mut App,
|
||||
) -> Entity<GitPickerPopover>;
|
||||
|
||||
struct BranchPickerBuilderGlobal(Rc<BranchPickerBuilder>);
|
||||
|
||||
impl Global for BranchPickerBuilderGlobal {}
|
||||
|
||||
pub fn set_branch_picker_builder(
|
||||
builder: impl Fn(
|
||||
WeakEntity<Workspace>,
|
||||
Option<Entity<Repository>>,
|
||||
&mut Window,
|
||||
&mut App,
|
||||
) -> Entity<GitPickerPopover>
|
||||
+ 'static,
|
||||
cx: &mut App,
|
||||
) {
|
||||
cx.set_global(BranchPickerBuilderGlobal(Rc::new(builder)));
|
||||
}
|
||||
|
||||
pub fn build_branch_picker(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
repository: Option<Entity<Repository>>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<Entity<GitPickerPopover>> {
|
||||
let builder = cx.try_global::<BranchPickerBuilderGlobal>()?.0.clone();
|
||||
Some(builder(workspace, repository, window, cx))
|
||||
}
|
||||
|
||||
type FileHistoryOpener = dyn Fn(&mut Workspace, &ProjectPath, &mut Window, &mut Context<Workspace>);
|
||||
|
||||
struct FileHistoryOpenerGlobal(Rc<FileHistoryOpener>);
|
||||
|
||||
impl Global for FileHistoryOpenerGlobal {}
|
||||
|
||||
pub fn set_file_history_opener(
|
||||
opener: impl Fn(&mut Workspace, &ProjectPath, &mut Window, &mut Context<Workspace>) + '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<Workspace>,
|
||||
) {
|
||||
let Some(opener) = cx
|
||||
.try_global::<FileHistoryOpenerGlobal>()
|
||||
.map(|opener| opener.0.clone())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
opener(workspace, path, window, cx);
|
||||
}
|
||||
@@ -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<SharedString>,
|
||||
workspace: &mut Workspace,
|
||||
output: &str,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
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<Workspace>,
|
||||
action: impl Into<SharedString>,
|
||||
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::<RpcError>().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"
|
||||
);
|
||||
}
|
||||
}
|
||||
File renamed without changes.
@@ -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,
|
||||
@@ -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,
|
||||
@@ -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
|
||||
|
||||
@@ -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::<SettingsStore>()
|
||||
.and_then(|store| store.merged_settings().project_panel.as_ref()?.sort_mode)
|
||||
.unwrap_or_default();
|
||||
Self {
|
||||
tx: Some(tx),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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::<ProjectPanel>(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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Workspace>,
|
||||
) {
|
||||
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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
]
|
||||
|
||||
@@ -466,6 +466,10 @@ fn check_dependencies() -> NamedJob {
|
||||
named::bash("cargo update --locked --workspace")
|
||||
}
|
||||
|
||||
fn check_crate_graph() -> Step<Run> {
|
||||
named::bash("cargo test --package xtask -- workspace::")
|
||||
}
|
||||
|
||||
fn check_vulnerable_dependencies() -> Step<Use> {
|
||||
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()),
|
||||
))
|
||||
}
|
||||
|
||||
@@ -7,3 +7,95 @@ pub fn load_workspace() -> Result<Metadata> {
|
||||
.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::<BTreeSet<_>>();
|
||||
|
||||
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::<BTreeSet<_>>();
|
||||
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::<String>::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<Vec<&'a str>> {
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user