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:
Kirill Bulatov authored and GitHub committed 2026-08-01 19:42:37 +00:00
1 parent 0500fd9ee9
commit 01961bef59
41 files changed
+734 -376

No files matched your search

+2
View File
@@ -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
View File
@@ -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",
+2
View File
@@ -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" }
+2 -2
View File
@@ -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 }
+9 -9
View File
@@ -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()
+6 -1
View File
@@ -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);
-1
View File
@@ -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
+10 -3
View File
@@ -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(
+70 -112
View File
@@ -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
+3 -6
View File
@@ -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"]
+2 -1
View File
@@ -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,
+30 -20
View File
@@ -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()
+4 -123
View File
@@ -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
View File
@@ -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 -1
View File
@@ -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,
+62
View File
@@ -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
+1
View File
@@ -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>,
@@ -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,
+116
View File
@@ -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);
}
+133
View File
@@ -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,
-1
View File
@@ -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),
+1 -2
View File
@@ -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 -36
View File
@@ -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,
);
}
}
+27 -7
View File
@@ -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
+3 -9
View File
@@ -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.
+1 -1
View File
@@ -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]
+2 -2
View File
@@ -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,
+1 -1
View File
@@ -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
+3 -5
View File
@@ -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 {
+1
View File
@@ -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"] }
+2 -1
View File
@@ -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;
+21 -1
View File
@@ -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()),
))
}
+92
View File
@@ -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
}
}