Move git status out of Entry (#22224)
- [x] Rewrite worktree git handling - [x] Fix tests - [x] Fix `test_propagate_statuses_for_repos_under_project` - [x] Replace `WorkDirectoryEntry` with `WorkDirectory` in `RepositoryEntry` - [x] Add a worktree event for capturing git status changes - [x] Confirm that the local repositories are correctly updating the new WorkDirectory field - [x] Implement the git statuses query as a join when pulling entries out of worktree - [x] Use this new join to implement the project panel and outline panel. - [x] Synchronize git statuses over the wire for collab and remote dev (use the existing `worktree_repository_statuses` table, adjust as needed) - [x] Only send changed statuses to collab Release Notes: - N/A --------- Co-authored-by: Cole Miller <cole@zed.dev> Co-authored-by: Mikayla <mikayla@zed.com> Co-authored-by: Nathan <nathan@zed.dev>
This commit is contained in:
parent
72057e5716
commit
9613084f59
57 changed files with 2824 additions and 1254 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
@ -2784,7 +2784,8 @@ dependencies = [
|
|||
name = "collections"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"rustc-hash 1.1.0",
|
||||
"indexmap",
|
||||
"rustc-hash 2.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -5193,6 +5194,7 @@ dependencies = [
|
|||
"util",
|
||||
"windows 0.58.0",
|
||||
"workspace",
|
||||
"worktree",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
@ -389,7 +389,7 @@ hyper = "0.14"
|
|||
http = "1.1"
|
||||
ignore = "0.4.22"
|
||||
image = "0.25.1"
|
||||
indexmap = { version = "2", features = ["serde"] }
|
||||
indexmap = { version = "2.7.0", features = ["serde"] }
|
||||
indoc = "2"
|
||||
itertools = "0.13.0"
|
||||
jsonwebtoken = "9.3"
|
||||
|
@ -440,9 +440,10 @@ runtimelib = { version = "0.24.0", default-features = false, features = [
|
|||
] }
|
||||
rustc-demangle = "0.1.23"
|
||||
rust-embed = { version = "8.4", features = ["include-exclude"] }
|
||||
rustc-hash = "2.1.0"
|
||||
rustls = "0.21.12"
|
||||
rustls-native-certs = "0.8.0"
|
||||
schemars = { version = "0.8", features = ["impl_json_schema"] }
|
||||
schemars = { version = "0.8", features = ["impl_json_schema", "indexmap2"] }
|
||||
semver = "1.0"
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
|
||||
|
|
|
@ -122,7 +122,7 @@ pub fn init(cx: &mut AppContext) {
|
|||
cx.observe_new_views(
|
||||
|terminal_panel: &mut TerminalPanel, cx: &mut ViewContext<TerminalPanel>| {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
terminal_panel.asssistant_enabled(settings.enabled, cx);
|
||||
terminal_panel.set_assistant_enabled(settings.enabled, cx);
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
|
|
|
@ -133,7 +133,7 @@ impl InlineAssistant {
|
|||
};
|
||||
let enabled = AssistantSettings::get_global(cx).enabled;
|
||||
terminal_panel.update(cx, |terminal_panel, cx| {
|
||||
terminal_panel.asssistant_enabled(enabled, cx)
|
||||
terminal_panel.set_assistant_enabled(enabled, cx)
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
|
|
|
@ -118,7 +118,7 @@ impl InlineAssistant {
|
|||
};
|
||||
let enabled = AssistantSettings::get_global(cx).enabled;
|
||||
terminal_panel.update(cx, |terminal_panel, cx| {
|
||||
terminal_panel.asssistant_enabled(enabled, cx)
|
||||
terminal_panel.set_assistant_enabled(enabled, cx)
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
|
|
|
@ -79,7 +79,7 @@ fn parse_path_with_position(argument_str: &str) -> anyhow::Result<String> {
|
|||
Ok(existing_path) => PathWithPosition::from_path(existing_path),
|
||||
Err(_) => {
|
||||
let path = PathWithPosition::parse_str(argument_str);
|
||||
let curdir = env::current_dir().context("reteiving current directory")?;
|
||||
let curdir = env::current_dir().context("retrieving current directory")?;
|
||||
path.map_path(|path| match fs::canonicalize(&path) {
|
||||
Ok(path) => Ok(path),
|
||||
Err(e) => {
|
||||
|
|
|
@ -106,6 +106,22 @@ CREATE TABLE "worktree_repositories" (
|
|||
CREATE INDEX "index_worktree_repositories_on_project_id" ON "worktree_repositories" ("project_id");
|
||||
CREATE INDEX "index_worktree_repositories_on_project_id_and_worktree_id" ON "worktree_repositories" ("project_id", "worktree_id");
|
||||
|
||||
CREATE TABLE "worktree_repository_statuses" (
|
||||
"project_id" INTEGER NOT NULL,
|
||||
"worktree_id" INT8 NOT NULL,
|
||||
"work_directory_id" INT8 NOT NULL,
|
||||
"repo_path" VARCHAR NOT NULL,
|
||||
"status" INT8 NOT NULL,
|
||||
"scan_id" INT8 NOT NULL,
|
||||
"is_deleted" BOOL NOT NULL,
|
||||
PRIMARY KEY(project_id, worktree_id, work_directory_id, repo_path),
|
||||
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(project_id, worktree_id, work_directory_id) REFERENCES worktree_entries (project_id, worktree_id, id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX "index_wt_repos_statuses_on_project_id" ON "worktree_repository_statuses" ("project_id");
|
||||
CREATE INDEX "index_wt_repos_statuses_on_project_id_and_wt_id" ON "worktree_repository_statuses" ("project_id", "worktree_id");
|
||||
CREATE INDEX "index_wt_repos_statuses_on_project_id_and_wt_id_and_wd_id" ON "worktree_repository_statuses" ("project_id", "worktree_id", "work_directory_id");
|
||||
|
||||
CREATE TABLE "worktree_settings_files" (
|
||||
"project_id" INTEGER NOT NULL,
|
||||
"worktree_id" INTEGER NOT NULL,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use anyhow::Context as _;
|
||||
|
||||
use util::ResultExt;
|
||||
|
||||
use super::*;
|
||||
|
@ -274,8 +275,8 @@ impl Database {
|
|||
mtime_nanos: ActiveValue::set(mtime.nanos as i32),
|
||||
canonical_path: ActiveValue::set(entry.canonical_path.clone()),
|
||||
is_ignored: ActiveValue::set(entry.is_ignored),
|
||||
git_status: ActiveValue::set(None),
|
||||
is_external: ActiveValue::set(entry.is_external),
|
||||
git_status: ActiveValue::set(entry.git_status.map(|status| status as i64)),
|
||||
is_deleted: ActiveValue::set(false),
|
||||
scan_id: ActiveValue::set(update.scan_id as i64),
|
||||
is_fifo: ActiveValue::set(entry.is_fifo),
|
||||
|
@ -295,7 +296,6 @@ impl Database {
|
|||
worktree_entry::Column::MtimeNanos,
|
||||
worktree_entry::Column::CanonicalPath,
|
||||
worktree_entry::Column::IsIgnored,
|
||||
worktree_entry::Column::GitStatus,
|
||||
worktree_entry::Column::ScanId,
|
||||
])
|
||||
.to_owned(),
|
||||
|
@ -349,6 +349,79 @@ impl Database {
|
|||
)
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
|
||||
let has_any_statuses = update
|
||||
.updated_repositories
|
||||
.iter()
|
||||
.any(|repository| !repository.updated_statuses.is_empty());
|
||||
|
||||
if has_any_statuses {
|
||||
worktree_repository_statuses::Entity::insert_many(
|
||||
update.updated_repositories.iter().flat_map(
|
||||
|repository: &proto::RepositoryEntry| {
|
||||
repository.updated_statuses.iter().map(|status_entry| {
|
||||
worktree_repository_statuses::ActiveModel {
|
||||
project_id: ActiveValue::set(project_id),
|
||||
worktree_id: ActiveValue::set(worktree_id),
|
||||
work_directory_id: ActiveValue::set(
|
||||
repository.work_directory_id as i64,
|
||||
),
|
||||
scan_id: ActiveValue::set(update.scan_id as i64),
|
||||
is_deleted: ActiveValue::set(false),
|
||||
repo_path: ActiveValue::set(status_entry.repo_path.clone()),
|
||||
status: ActiveValue::set(status_entry.status as i64),
|
||||
}
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
.on_conflict(
|
||||
OnConflict::columns([
|
||||
worktree_repository_statuses::Column::ProjectId,
|
||||
worktree_repository_statuses::Column::WorktreeId,
|
||||
worktree_repository_statuses::Column::WorkDirectoryId,
|
||||
worktree_repository_statuses::Column::RepoPath,
|
||||
])
|
||||
.update_columns([
|
||||
worktree_repository_statuses::Column::ScanId,
|
||||
worktree_repository_statuses::Column::Status,
|
||||
])
|
||||
.to_owned(),
|
||||
)
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let has_any_removed_statuses = update
|
||||
.updated_repositories
|
||||
.iter()
|
||||
.any(|repository| !repository.removed_statuses.is_empty());
|
||||
|
||||
if has_any_removed_statuses {
|
||||
worktree_repository_statuses::Entity::update_many()
|
||||
.filter(
|
||||
worktree_repository_statuses::Column::ProjectId
|
||||
.eq(project_id)
|
||||
.and(
|
||||
worktree_repository_statuses::Column::WorktreeId
|
||||
.eq(worktree_id),
|
||||
)
|
||||
.and(
|
||||
worktree_repository_statuses::Column::RepoPath.is_in(
|
||||
update.updated_repositories.iter().flat_map(|repository| {
|
||||
repository.removed_statuses.iter()
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
.set(worktree_repository_statuses::ActiveModel {
|
||||
is_deleted: ActiveValue::Set(true),
|
||||
scan_id: ActiveValue::Set(update.scan_id as i64),
|
||||
..Default::default()
|
||||
})
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
if !update.removed_repositories.is_empty() {
|
||||
|
@ -643,7 +716,6 @@ impl Database {
|
|||
canonical_path: db_entry.canonical_path,
|
||||
is_ignored: db_entry.is_ignored,
|
||||
is_external: db_entry.is_external,
|
||||
git_status: db_entry.git_status.map(|status| status as i32),
|
||||
// This is only used in the summarization backlog, so if it's None,
|
||||
// that just means we won't be able to detect when to resummarize
|
||||
// based on total number of backlogged bytes - instead, we'd go
|
||||
|
@ -657,23 +729,49 @@ impl Database {
|
|||
|
||||
// Populate repository entries.
|
||||
{
|
||||
let mut db_repository_entries = worktree_repository::Entity::find()
|
||||
let db_repository_entries = worktree_repository::Entity::find()
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(worktree_repository::Column::ProjectId.eq(project.id))
|
||||
.add(worktree_repository::Column::IsDeleted.eq(false)),
|
||||
)
|
||||
.stream(tx)
|
||||
.all(tx)
|
||||
.await?;
|
||||
while let Some(db_repository_entry) = db_repository_entries.next().await {
|
||||
let db_repository_entry = db_repository_entry?;
|
||||
for db_repository_entry in db_repository_entries {
|
||||
if let Some(worktree) = worktrees.get_mut(&(db_repository_entry.worktree_id as u64))
|
||||
{
|
||||
let mut repository_statuses = worktree_repository_statuses::Entity::find()
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(worktree_repository_statuses::Column::ProjectId.eq(project.id))
|
||||
.add(
|
||||
worktree_repository_statuses::Column::WorktreeId
|
||||
.eq(worktree.id),
|
||||
)
|
||||
.add(
|
||||
worktree_repository_statuses::Column::WorkDirectoryId
|
||||
.eq(db_repository_entry.work_directory_id),
|
||||
)
|
||||
.add(worktree_repository_statuses::Column::IsDeleted.eq(false)),
|
||||
)
|
||||
.stream(tx)
|
||||
.await?;
|
||||
let mut updated_statuses = Vec::new();
|
||||
while let Some(status_entry) = repository_statuses.next().await {
|
||||
let status_entry: worktree_repository_statuses::Model = status_entry?;
|
||||
updated_statuses.push(proto::StatusEntry {
|
||||
repo_path: status_entry.repo_path,
|
||||
status: status_entry.status as i32,
|
||||
});
|
||||
}
|
||||
|
||||
worktree.repository_entries.insert(
|
||||
db_repository_entry.work_directory_id as u64,
|
||||
proto::RepositoryEntry {
|
||||
work_directory_id: db_repository_entry.work_directory_id as u64,
|
||||
branch: db_repository_entry.branch,
|
||||
updated_statuses,
|
||||
removed_statuses: Vec::new(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
@ -662,7 +662,6 @@ impl Database {
|
|||
canonical_path: db_entry.canonical_path,
|
||||
is_ignored: db_entry.is_ignored,
|
||||
is_external: db_entry.is_external,
|
||||
git_status: db_entry.git_status.map(|status| status as i32),
|
||||
// This is only used in the summarization backlog, so if it's None,
|
||||
// that just means we won't be able to detect when to resummarize
|
||||
// based on total number of backlogged bytes - instead, we'd go
|
||||
|
@ -682,26 +681,69 @@ impl Database {
|
|||
worktree_repository::Column::IsDeleted.eq(false)
|
||||
};
|
||||
|
||||
let mut db_repositories = worktree_repository::Entity::find()
|
||||
let db_repositories = worktree_repository::Entity::find()
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(worktree_repository::Column::ProjectId.eq(project.id))
|
||||
.add(worktree_repository::Column::WorktreeId.eq(worktree.id))
|
||||
.add(repository_entry_filter),
|
||||
)
|
||||
.stream(tx)
|
||||
.all(tx)
|
||||
.await?;
|
||||
|
||||
while let Some(db_repository) = db_repositories.next().await {
|
||||
let db_repository = db_repository?;
|
||||
for db_repository in db_repositories.into_iter() {
|
||||
if db_repository.is_deleted {
|
||||
worktree
|
||||
.removed_repositories
|
||||
.push(db_repository.work_directory_id as u64);
|
||||
} else {
|
||||
let status_entry_filter = if let Some(rejoined_worktree) = rejoined_worktree
|
||||
{
|
||||
worktree_repository_statuses::Column::ScanId
|
||||
.gt(rejoined_worktree.scan_id)
|
||||
} else {
|
||||
worktree_repository_statuses::Column::IsDeleted.eq(false)
|
||||
};
|
||||
|
||||
let mut db_statuses = worktree_repository_statuses::Entity::find()
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(
|
||||
worktree_repository_statuses::Column::ProjectId
|
||||
.eq(project.id),
|
||||
)
|
||||
.add(
|
||||
worktree_repository_statuses::Column::WorktreeId
|
||||
.eq(worktree.id),
|
||||
)
|
||||
.add(
|
||||
worktree_repository_statuses::Column::WorkDirectoryId
|
||||
.eq(db_repository.work_directory_id),
|
||||
)
|
||||
.add(status_entry_filter),
|
||||
)
|
||||
.stream(tx)
|
||||
.await?;
|
||||
let mut removed_statuses = Vec::new();
|
||||
let mut updated_statuses = Vec::new();
|
||||
|
||||
while let Some(db_status) = db_statuses.next().await {
|
||||
let db_status: worktree_repository_statuses::Model = db_status?;
|
||||
if db_status.is_deleted {
|
||||
removed_statuses.push(db_status.repo_path);
|
||||
} else {
|
||||
updated_statuses.push(proto::StatusEntry {
|
||||
repo_path: db_status.repo_path,
|
||||
status: db_status.status as i32,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
worktree.updated_repositories.push(proto::RepositoryEntry {
|
||||
work_directory_id: db_repository.work_directory_id as u64,
|
||||
branch: db_repository.branch,
|
||||
updated_statuses,
|
||||
removed_statuses,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2925,8 +2925,6 @@ async fn test_git_status_sync(
|
|||
assert_eq!(snapshot.status_for_file(file), status);
|
||||
}
|
||||
|
||||
// Smoke test status reading
|
||||
|
||||
project_local.read_with(cx_a, |project, cx| {
|
||||
assert_status(&Path::new(A_TXT), Some(GitFileStatus::Added), project, cx);
|
||||
assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), project, cx);
|
||||
|
@ -6669,6 +6667,10 @@ async fn test_remote_git_branches(
|
|||
client_a
|
||||
.fs()
|
||||
.insert_branches(Path::new("/project/.git"), &branches);
|
||||
let branches_set = branches
|
||||
.into_iter()
|
||||
.map(ToString::to_string)
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
let (project_a, worktree_id) = client_a.build_local_project("/project", cx_a).await;
|
||||
let project_id = active_call_a
|
||||
|
@ -6690,10 +6692,10 @@ async fn test_remote_git_branches(
|
|||
|
||||
let branches_b = branches_b
|
||||
.into_iter()
|
||||
.map(|branch| branch.name)
|
||||
.collect::<Vec<_>>();
|
||||
.map(|branch| branch.name.to_string())
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
assert_eq!(&branches_b, &branches);
|
||||
assert_eq!(branches_b, branches_set);
|
||||
|
||||
cx_b.update(|cx| {
|
||||
project_b.update(cx, |project, cx| {
|
||||
|
|
|
@ -229,6 +229,10 @@ async fn test_ssh_collaboration_git_branches(
|
|||
.await;
|
||||
|
||||
let branches = ["main", "dev", "feature-1"];
|
||||
let branches_set = branches
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect::<HashSet<_>>();
|
||||
remote_fs.insert_branches(Path::new("/project/.git"), &branches);
|
||||
|
||||
// User A connects to the remote project via SSH.
|
||||
|
@ -281,10 +285,10 @@ async fn test_ssh_collaboration_git_branches(
|
|||
|
||||
let branches_b = branches_b
|
||||
.into_iter()
|
||||
.map(|branch| branch.name)
|
||||
.collect::<Vec<_>>();
|
||||
.map(|branch| branch.name.to_string())
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
assert_eq!(&branches_b, &branches);
|
||||
assert_eq!(&branches_b, &branches_set);
|
||||
|
||||
cx_b.update(|cx| {
|
||||
project_b.update(cx, |project, cx| {
|
||||
|
|
|
@ -16,4 +16,5 @@ doctest = false
|
|||
test-support = []
|
||||
|
||||
[dependencies]
|
||||
rustc-hash = "1.1"
|
||||
indexmap.workspace = true
|
||||
rustc-hash.workspace = true
|
||||
|
|
|
@ -4,12 +4,24 @@ pub type HashMap<K, V> = FxHashMap<K, V>;
|
|||
#[cfg(feature = "test-support")]
|
||||
pub type HashSet<T> = FxHashSet<T>;
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
pub type IndexMap<K, V> = indexmap::IndexMap<K, V, rustc_hash::FxBuildHasher>;
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
pub type IndexSet<T> = indexmap::IndexSet<T, rustc_hash::FxBuildHasher>;
|
||||
|
||||
#[cfg(not(feature = "test-support"))]
|
||||
pub type HashMap<K, V> = std::collections::HashMap<K, V>;
|
||||
|
||||
#[cfg(not(feature = "test-support"))]
|
||||
pub type HashSet<T> = std::collections::HashSet<T>;
|
||||
|
||||
#[cfg(not(feature = "test-support"))]
|
||||
pub type IndexMap<K, V> = indexmap::IndexMap<K, V>;
|
||||
|
||||
#[cfg(not(feature = "test-support"))]
|
||||
pub type IndexSet<T> = indexmap::IndexSet<T>;
|
||||
|
||||
pub use rustc_hash::FxHasher;
|
||||
pub use rustc_hash::{FxHashMap, FxHashSet};
|
||||
pub use std::collections::*;
|
||||
|
|
|
@ -2748,7 +2748,7 @@ mod tests {
|
|||
.iter()
|
||||
.filter(|(_, block)| matches!(block, Block::FoldedBuffer { .. }))
|
||||
.count(),
|
||||
"Should have one folded block, prodicing a header of the second buffer"
|
||||
"Should have one folded block, producing a header of the second buffer"
|
||||
);
|
||||
assert_eq!(
|
||||
blocks_snapshot.text(),
|
||||
|
@ -2994,7 +2994,7 @@ mod tests {
|
|||
}
|
||||
})
|
||||
.count(),
|
||||
"Should have one folded block, prodicing a header of the second buffer"
|
||||
"Should have one folded block, producing a header of the second buffer"
|
||||
);
|
||||
assert_eq!(blocks_snapshot.text(), "\n");
|
||||
assert_eq!(
|
||||
|
|
|
@ -11780,7 +11780,7 @@ impl Editor {
|
|||
}
|
||||
|
||||
/// Merges all anchor ranges for all context types ever set, picking the last highlight added in case of a row conflict.
|
||||
/// Rerturns a map of display rows that are highlighted and their corresponding highlight color.
|
||||
/// Returns a map of display rows that are highlighted and their corresponding highlight color.
|
||||
/// Allows to ignore certain kinds of highlights.
|
||||
pub fn highlighted_display_rows(
|
||||
&mut self,
|
||||
|
@ -12573,7 +12573,7 @@ impl Editor {
|
|||
.file()
|
||||
.is_none()
|
||||
.then(|| {
|
||||
// Handle file-less buffers separately: those are not really the project items, so won't have a paroject path or entity id,
|
||||
// Handle file-less buffers separately: those are not really the project items, so won't have a project path or entity id,
|
||||
// so `workspace.open_project_item` will never find them, always opening a new editor.
|
||||
// Instead, we try to activate the existing editor in the pane first.
|
||||
let (editor, pane_item_index) =
|
||||
|
|
|
@ -194,14 +194,24 @@ impl ProjectDiffEditor {
|
|||
let open_tasks = project
|
||||
.update(&mut cx, |project, cx| {
|
||||
let worktree = project.worktree_for_id(id, cx)?;
|
||||
let applicable_entries = worktree
|
||||
.read(cx)
|
||||
.entries(false, 0)
|
||||
.filter(|entry| !entry.is_external)
|
||||
.filter(|entry| entry.is_file())
|
||||
.filter_map(|entry| Some((entry.git_status?, entry)))
|
||||
.filter_map(|(git_status, entry)| {
|
||||
Some((git_status, entry.id, project.path_for_entry(entry.id, cx)?))
|
||||
let snapshot = worktree.read(cx).snapshot();
|
||||
let applicable_entries = snapshot
|
||||
.repositories()
|
||||
.flat_map(|entry| {
|
||||
entry.status().map(|git_entry| {
|
||||
(git_entry.status, entry.join(git_entry.repo_path))
|
||||
})
|
||||
})
|
||||
.filter_map(|(status, path)| {
|
||||
let id = snapshot.entry_for_path(&path)?.id;
|
||||
Some((
|
||||
status,
|
||||
id,
|
||||
ProjectPath {
|
||||
worktree_id: snapshot.id(),
|
||||
path: path.into(),
|
||||
},
|
||||
))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
Some(
|
||||
|
|
|
@ -615,9 +615,20 @@ impl Item for Editor {
|
|||
.read(cx)
|
||||
.as_singleton()
|
||||
.and_then(|buffer| buffer.read(cx).project_path(cx))
|
||||
.and_then(|path| self.project.as_ref()?.read(cx).entry_for_path(&path, cx))
|
||||
.map(|entry| {
|
||||
entry_git_aware_label_color(entry.git_status, entry.is_ignored, params.selected)
|
||||
.and_then(|path| {
|
||||
let project = self.project.as_ref()?.read(cx);
|
||||
let entry = project.entry_for_path(&path, cx)?;
|
||||
let git_status = project
|
||||
.worktree_for_id(path.worktree_id, cx)?
|
||||
.read(cx)
|
||||
.snapshot()
|
||||
.status_for_file(path.path);
|
||||
|
||||
Some(entry_git_aware_label_color(
|
||||
git_status,
|
||||
entry.is_ignored,
|
||||
params.selected,
|
||||
))
|
||||
})
|
||||
.unwrap_or_else(|| entry_label_color(params.selected))
|
||||
} else {
|
||||
|
@ -1559,10 +1570,10 @@ pub fn entry_git_aware_label_color(
|
|||
Color::Ignored
|
||||
} else {
|
||||
match git_status {
|
||||
Some(GitFileStatus::Added) => Color::Created,
|
||||
Some(GitFileStatus::Added) | Some(GitFileStatus::Untracked) => Color::Created,
|
||||
Some(GitFileStatus::Modified) => Color::Modified,
|
||||
Some(GitFileStatus::Conflict) => Color::Conflict,
|
||||
None => entry_label_color(selected),
|
||||
Some(GitFileStatus::Deleted) | None => entry_label_color(selected),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -257,7 +257,8 @@ impl EditorLspTestContext {
|
|||
Self::new(language, Default::default(), cx).await
|
||||
}
|
||||
|
||||
// Constructs lsp range using a marked string with '[', ']' range delimiters
|
||||
/// Constructs lsp range using a marked string with '[', ']' range delimiters
|
||||
#[track_caller]
|
||||
pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {
|
||||
let ranges = self.ranges(marked_text);
|
||||
self.to_lsp_range(ranges[0].clone())
|
||||
|
|
|
@ -230,6 +230,7 @@ impl EditorTestContext {
|
|||
self.cx.background_executor.run_until_parked();
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn ranges(&mut self, marked_text: &str) -> Vec<Range<usize>> {
|
||||
let (unmarked_text, ranges) = marked_text_ranges(marked_text, false);
|
||||
assert_eq!(self.buffer_text(), unmarked_text);
|
||||
|
|
|
@ -16,6 +16,7 @@ use std::sync::LazyLock;
|
|||
pub use crate::hosting_provider::*;
|
||||
pub use crate::remote::*;
|
||||
pub use git2 as libgit;
|
||||
pub use repository::WORK_DIRECTORY_REPO_PATH;
|
||||
|
||||
pub static DOT_GIT: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new(".git"));
|
||||
pub static COOKIES: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new("cookies"));
|
||||
|
|
|
@ -7,6 +7,8 @@ use gpui::SharedString;
|
|||
use parking_lot::Mutex;
|
||||
use rope::Rope;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::borrow::Borrow;
|
||||
use std::sync::LazyLock;
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
path::{Component, Path, PathBuf},
|
||||
|
@ -37,7 +39,8 @@ pub trait GitRepository: Send + Sync {
|
|||
/// Returns the SHA of the current HEAD.
|
||||
fn head_sha(&self) -> Option<String>;
|
||||
|
||||
fn status(&self, path_prefixes: &[PathBuf]) -> Result<GitStatus>;
|
||||
/// Returns the list of git statuses, sorted by path
|
||||
fn status(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus>;
|
||||
|
||||
fn branches(&self) -> Result<Vec<Branch>>;
|
||||
fn change_branch(&self, _: &str) -> Result<()>;
|
||||
|
@ -132,7 +135,7 @@ impl GitRepository for RealGitRepository {
|
|||
Some(self.repository.lock().head().ok()?.target()?.to_string())
|
||||
}
|
||||
|
||||
fn status(&self, path_prefixes: &[PathBuf]) -> Result<GitStatus> {
|
||||
fn status(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus> {
|
||||
let working_directory = self
|
||||
.repository
|
||||
.lock()
|
||||
|
@ -289,8 +292,9 @@ impl GitRepository for FakeGitRepository {
|
|||
state.dot_git_dir.clone()
|
||||
}
|
||||
|
||||
fn status(&self, path_prefixes: &[PathBuf]) -> Result<GitStatus> {
|
||||
fn status(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus> {
|
||||
let state = self.state.lock();
|
||||
|
||||
let mut entries = state
|
||||
.worktree_statuses
|
||||
.iter()
|
||||
|
@ -306,6 +310,7 @@ impl GitRepository for FakeGitRepository {
|
|||
})
|
||||
.collect::<Vec<_>>();
|
||||
entries.sort_unstable_by(|a, b| a.0.cmp(&b.0));
|
||||
|
||||
Ok(GitStatus {
|
||||
entries: entries.into(),
|
||||
})
|
||||
|
@ -394,6 +399,8 @@ pub enum GitFileStatus {
|
|||
Added,
|
||||
Modified,
|
||||
Conflict,
|
||||
Deleted,
|
||||
Untracked,
|
||||
}
|
||||
|
||||
impl GitFileStatus {
|
||||
|
@ -421,20 +428,34 @@ impl GitFileStatus {
|
|||
}
|
||||
}
|
||||
|
||||
pub static WORK_DIRECTORY_REPO_PATH: LazyLock<RepoPath> =
|
||||
LazyLock::new(|| RepoPath(Path::new("").into()));
|
||||
|
||||
#[derive(Clone, Debug, Ord, Hash, PartialOrd, Eq, PartialEq)]
|
||||
pub struct RepoPath(pub PathBuf);
|
||||
pub struct RepoPath(pub Arc<Path>);
|
||||
|
||||
impl RepoPath {
|
||||
pub fn new(path: PathBuf) -> Self {
|
||||
debug_assert!(path.is_relative(), "Repo paths must be relative");
|
||||
|
||||
RepoPath(path)
|
||||
RepoPath(path.into())
|
||||
}
|
||||
|
||||
pub fn from_str(path: &str) -> Self {
|
||||
let path = Path::new(path);
|
||||
debug_assert!(path.is_relative(), "Repo paths must be relative");
|
||||
|
||||
RepoPath(path.into())
|
||||
}
|
||||
|
||||
pub fn to_proto(&self) -> String {
|
||||
self.0.to_string_lossy().to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Path> for RepoPath {
|
||||
fn from(value: &Path) -> Self {
|
||||
RepoPath::new(value.to_path_buf())
|
||||
RepoPath::new(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -444,9 +465,15 @@ impl From<PathBuf> for RepoPath {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<&str> for RepoPath {
|
||||
fn from(value: &str) -> Self {
|
||||
Self::from_str(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RepoPath {
|
||||
fn default() -> Self {
|
||||
RepoPath(PathBuf::new())
|
||||
RepoPath(Path::new("").into())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -457,13 +484,19 @@ impl AsRef<Path> for RepoPath {
|
|||
}
|
||||
|
||||
impl std::ops::Deref for RepoPath {
|
||||
type Target = PathBuf;
|
||||
type Target = Path;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Borrow<Path> for RepoPath {
|
||||
fn borrow(&self) -> &Path {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RepoPathDescendants<'a>(pub &'a Path);
|
||||
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
use crate::repository::{GitFileStatus, RepoPath};
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
process::Stdio,
|
||||
sync::Arc,
|
||||
};
|
||||
use std::{path::Path, process::Stdio, sync::Arc};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct GitStatus {
|
||||
|
@ -15,7 +11,7 @@ impl GitStatus {
|
|||
pub(crate) fn new(
|
||||
git_binary: &Path,
|
||||
working_directory: &Path,
|
||||
path_prefixes: &[PathBuf],
|
||||
path_prefixes: &[RepoPath],
|
||||
) -> Result<Self> {
|
||||
let child = util::command::new_std_command(git_binary)
|
||||
.current_dir(working_directory)
|
||||
|
@ -27,7 +23,7 @@ impl GitStatus {
|
|||
"-z",
|
||||
])
|
||||
.args(path_prefixes.iter().map(|path_prefix| {
|
||||
if *path_prefix == Path::new("") {
|
||||
if path_prefix.0.as_ref() == Path::new("") {
|
||||
Path::new(".")
|
||||
} else {
|
||||
path_prefix
|
||||
|
@ -55,10 +51,12 @@ impl GitStatus {
|
|||
let (status, path) = entry.split_at(3);
|
||||
let status = status.trim();
|
||||
Some((
|
||||
RepoPath(PathBuf::from(path)),
|
||||
RepoPath(Path::new(path).into()),
|
||||
match status {
|
||||
"A" | "??" => GitFileStatus::Added,
|
||||
"A" => GitFileStatus::Added,
|
||||
"M" => GitFileStatus::Modified,
|
||||
"D" => GitFileStatus::Deleted,
|
||||
"??" => GitFileStatus::Untracked,
|
||||
_ => return None,
|
||||
},
|
||||
))
|
||||
|
@ -75,7 +73,7 @@ impl GitStatus {
|
|||
|
||||
pub fn get(&self, path: &Path) -> Option<GitFileStatus> {
|
||||
self.entries
|
||||
.binary_search_by(|(repo_path, _)| repo_path.0.as_path().cmp(path))
|
||||
.binary_search_by(|(repo_path, _)| repo_path.0.as_ref().cmp(path))
|
||||
.ok()
|
||||
.map(|index| self.entries[index].1)
|
||||
}
|
||||
|
|
|
@ -14,9 +14,11 @@ path = "src/git_ui.rs"
|
|||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
collections.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
futures.workspace = true
|
||||
git.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
menu.workspace = true
|
||||
|
@ -29,8 +31,7 @@ settings.workspace = true
|
|||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
git.workspace = true
|
||||
collections.workspace = true
|
||||
worktree.workspace = true
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows.workspace = true
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
use crate::{git_status_icon, settings::GitPanelSettings};
|
||||
use crate::{CommitAllChanges, CommitStagedChanges, DiscardAll, StageAll, UnstageAll};
|
||||
use anyhow::{Context as _, Result};
|
||||
use collections::HashMap;
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use editor::{
|
||||
scroll::{Autoscroll, AutoscrollStrategy},
|
||||
Editor, MultiBuffer, DEFAULT_MULTIBUFFER_CONTEXT,
|
||||
};
|
||||
use git::{diff::DiffHunk, repository::GitFileStatus};
|
||||
use git::{
|
||||
diff::DiffHunk,
|
||||
repository::{GitFileStatus, RepoPath},
|
||||
};
|
||||
use gpui::*;
|
||||
use gpui::{
|
||||
actions, prelude::*, uniform_list, Action, AppContext, AsyncWindowContext, ClickEvent,
|
||||
CursorStyle, EventEmitter, FocusHandle, FocusableView, KeyContext,
|
||||
|
@ -14,7 +19,7 @@ use gpui::{
|
|||
};
|
||||
use language::{Buffer, BufferRow, OffsetRangeExt};
|
||||
use menu::{SelectNext, SelectPrev};
|
||||
use project::{Entry, EntryKind, Fs, Project, ProjectEntryId, WorktreeId};
|
||||
use project::{EntryKind, Fs, Project, ProjectEntryId, ProjectPath, WorktreeId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings as _;
|
||||
use std::{
|
||||
|
@ -22,7 +27,7 @@ use std::{
|
|||
collections::HashSet,
|
||||
ffi::OsStr,
|
||||
ops::{Deref, Range},
|
||||
path::{Path, PathBuf},
|
||||
path::PathBuf,
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
|
@ -37,9 +42,7 @@ use workspace::{
|
|||
dock::{DockPosition, Panel, PanelEvent},
|
||||
ItemHandle, Workspace,
|
||||
};
|
||||
|
||||
use crate::{git_status_icon, settings::GitPanelSettings};
|
||||
use crate::{CommitAllChanges, CommitStagedChanges, DiscardAll, StageAll, UnstageAll};
|
||||
use worktree::StatusEntry;
|
||||
|
||||
actions!(git_panel, [ToggleFocus]);
|
||||
|
||||
|
@ -69,7 +72,7 @@ pub struct GitStatusEntry {}
|
|||
struct EntryDetails {
|
||||
filename: String,
|
||||
display_name: String,
|
||||
path: Arc<Path>,
|
||||
path: RepoPath,
|
||||
kind: EntryKind,
|
||||
depth: usize,
|
||||
is_expanded: bool,
|
||||
|
@ -101,7 +104,8 @@ pub struct GitPanel {
|
|||
scrollbar_state: ScrollbarState,
|
||||
selected_item: Option<usize>,
|
||||
show_scrollbar: bool,
|
||||
expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>,
|
||||
// TODO Reintroduce expanded directories, once we're deriving directories from paths
|
||||
// expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>,
|
||||
|
||||
// The entries that are currently shown in the panel, aka
|
||||
// not hidden by folding or such
|
||||
|
@ -115,18 +119,20 @@ pub struct GitPanel {
|
|||
#[derive(Debug, Clone)]
|
||||
struct WorktreeEntries {
|
||||
worktree_id: WorktreeId,
|
||||
// TODO support multiple repositories per worktree
|
||||
work_directory: worktree::WorkDirectory,
|
||||
visible_entries: Vec<GitPanelEntry>,
|
||||
paths: Rc<OnceCell<HashSet<Arc<Path>>>>,
|
||||
paths: Rc<OnceCell<HashSet<RepoPath>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct GitPanelEntry {
|
||||
entry: Entry,
|
||||
entry: worktree::StatusEntry,
|
||||
hunks: Rc<OnceCell<Vec<DiffHunk>>>,
|
||||
}
|
||||
|
||||
impl Deref for GitPanelEntry {
|
||||
type Target = Entry;
|
||||
type Target = worktree::StatusEntry;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.entry
|
||||
|
@ -134,11 +140,11 @@ impl Deref for GitPanelEntry {
|
|||
}
|
||||
|
||||
impl WorktreeEntries {
|
||||
fn paths(&self) -> &HashSet<Arc<Path>> {
|
||||
fn paths(&self) -> &HashSet<RepoPath> {
|
||||
self.paths.get_or_init(|| {
|
||||
self.visible_entries
|
||||
.iter()
|
||||
.map(|e| (e.entry.path.clone()))
|
||||
.map(|e| (e.entry.repo_path.clone()))
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
@ -165,8 +171,11 @@ impl GitPanel {
|
|||
})
|
||||
.detach();
|
||||
cx.subscribe(&project, |this, _, event, cx| match event {
|
||||
project::Event::WorktreeRemoved(id) => {
|
||||
this.expanded_dir_ids.remove(id);
|
||||
project::Event::GitRepositoryUpdated => {
|
||||
this.update_visible_entries(None, None, cx);
|
||||
}
|
||||
project::Event::WorktreeRemoved(_id) => {
|
||||
// this.expanded_dir_ids.remove(id);
|
||||
this.update_visible_entries(None, None, cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
@ -183,7 +192,7 @@ impl GitPanel {
|
|||
project::Event::Closed => {
|
||||
this.git_diff_editor_updates = Task::ready(());
|
||||
this.reveal_in_editor = Task::ready(());
|
||||
this.expanded_dir_ids.clear();
|
||||
// this.expanded_dir_ids.clear();
|
||||
this.visible_entries.clear();
|
||||
this.git_diff_editor = None;
|
||||
}
|
||||
|
@ -200,8 +209,7 @@ impl GitPanel {
|
|||
pending_serialization: Task::ready(None),
|
||||
visible_entries: Vec::new(),
|
||||
current_modifiers: cx.modifiers(),
|
||||
expanded_dir_ids: Default::default(),
|
||||
|
||||
// expanded_dir_ids: Default::default(),
|
||||
width: Some(px(360.)),
|
||||
scrollbar_state: ScrollbarState::new(scroll_handle.clone()).parent_view(cx.view()),
|
||||
scroll_handle,
|
||||
|
@ -288,16 +296,16 @@ impl GitPanel {
|
|||
}
|
||||
|
||||
fn calculate_depth_and_difference(
|
||||
entry: &Entry,
|
||||
visible_worktree_entries: &HashSet<Arc<Path>>,
|
||||
entry: &StatusEntry,
|
||||
visible_worktree_entries: &HashSet<RepoPath>,
|
||||
) -> (usize, usize) {
|
||||
let (depth, difference) = entry
|
||||
.path
|
||||
.repo_path
|
||||
.ancestors()
|
||||
.skip(1) // Skip the entry itself
|
||||
.find_map(|ancestor| {
|
||||
if let Some(parent_entry) = visible_worktree_entries.get(ancestor) {
|
||||
let entry_path_components_count = entry.path.components().count();
|
||||
let entry_path_components_count = entry.repo_path.components().count();
|
||||
let parent_path_components_count = parent_entry.components().count();
|
||||
let difference = entry_path_components_count - parent_path_components_count;
|
||||
let depth = parent_entry
|
||||
|
@ -432,13 +440,7 @@ impl GitPanel {
|
|||
fn entry_count(&self) -> usize {
|
||||
self.visible_entries
|
||||
.iter()
|
||||
.map(|worktree_entries| {
|
||||
worktree_entries
|
||||
.visible_entries
|
||||
.iter()
|
||||
.filter(|entry| entry.git_status.is_some())
|
||||
.count()
|
||||
})
|
||||
.map(|worktree_entries| worktree_entries.visible_entries.len())
|
||||
.sum()
|
||||
}
|
||||
|
||||
|
@ -446,7 +448,7 @@ impl GitPanel {
|
|||
&self,
|
||||
range: Range<usize>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut ViewContext<Self>),
|
||||
mut callback: impl FnMut(usize, EntryDetails, &mut ViewContext<Self>),
|
||||
) {
|
||||
let mut ix = 0;
|
||||
for worktree_entries in &self.visible_entries {
|
||||
|
@ -468,11 +470,11 @@ impl GitPanel {
|
|||
{
|
||||
let snapshot = worktree.read(cx).snapshot();
|
||||
let root_name = OsStr::new(snapshot.root_name());
|
||||
let expanded_entry_ids = self
|
||||
.expanded_dir_ids
|
||||
.get(&snapshot.id())
|
||||
.map(Vec::as_slice)
|
||||
.unwrap_or(&[]);
|
||||
// let expanded_entry_ids = self
|
||||
// .expanded_dir_ids
|
||||
// .get(&snapshot.id())
|
||||
// .map(Vec::as_slice)
|
||||
// .unwrap_or(&[]);
|
||||
|
||||
let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
|
||||
let entries = worktree_entries.paths();
|
||||
|
@ -483,22 +485,22 @@ impl GitPanel {
|
|||
.enumerate()
|
||||
{
|
||||
let index = index_start + i;
|
||||
let status = entry.git_status;
|
||||
let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok();
|
||||
let status = entry.status;
|
||||
let is_expanded = true; //expanded_entry_ids.binary_search(&entry.id).is_ok();
|
||||
|
||||
let (depth, difference) = Self::calculate_depth_and_difference(entry, entries);
|
||||
|
||||
let filename = match difference {
|
||||
diff if diff > 1 => entry
|
||||
.path
|
||||
.repo_path
|
||||
.iter()
|
||||
.skip(entry.path.components().count() - diff)
|
||||
.skip(entry.repo_path.components().count() - diff)
|
||||
.collect::<PathBuf>()
|
||||
.to_str()
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
_ => entry
|
||||
.path
|
||||
.repo_path
|
||||
.file_name()
|
||||
.map(|name| name.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| root_name.to_string_lossy().to_string()),
|
||||
|
@ -506,16 +508,17 @@ impl GitPanel {
|
|||
|
||||
let details = EntryDetails {
|
||||
filename,
|
||||
display_name: entry.path.to_string_lossy().into_owned(),
|
||||
kind: entry.kind,
|
||||
display_name: entry.repo_path.to_string_lossy().into_owned(),
|
||||
// TODO get it from StatusEntry?
|
||||
kind: EntryKind::File,
|
||||
is_expanded,
|
||||
path: entry.path.clone(),
|
||||
status,
|
||||
path: entry.repo_path.clone(),
|
||||
status: Some(status),
|
||||
hunks: entry.hunks.clone(),
|
||||
depth,
|
||||
index,
|
||||
};
|
||||
callback(entry.id, details, cx);
|
||||
callback(ix, details, cx);
|
||||
}
|
||||
}
|
||||
ix = end_ix;
|
||||
|
@ -527,7 +530,7 @@ impl GitPanel {
|
|||
fn update_visible_entries(
|
||||
&mut self,
|
||||
for_worktree: Option<WorktreeId>,
|
||||
new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
|
||||
_new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let project = self.project.read(cx);
|
||||
|
@ -549,24 +552,36 @@ impl GitPanel {
|
|||
None => false,
|
||||
});
|
||||
for worktree in project.visible_worktrees(cx) {
|
||||
let worktree_id = worktree.read(cx).id();
|
||||
let snapshot = worktree.read(cx).snapshot();
|
||||
let worktree_id = snapshot.id();
|
||||
|
||||
if for_worktree.is_some() && for_worktree != Some(worktree_id) {
|
||||
continue;
|
||||
}
|
||||
let snapshot = worktree.read(cx).snapshot();
|
||||
|
||||
let mut visible_worktree_entries = snapshot
|
||||
.entries(false, 0)
|
||||
.filter(|entry| !entry.is_external)
|
||||
.filter(|entry| entry.git_status.is_some())
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
snapshot.propagate_git_statuses(&mut visible_worktree_entries);
|
||||
project::sort_worktree_entries(&mut visible_worktree_entries);
|
||||
let mut visible_worktree_entries = Vec::new();
|
||||
// Only use the first repository for now
|
||||
let repositories = snapshot.repositories().take(1);
|
||||
let mut work_directory = None;
|
||||
for repository in repositories {
|
||||
visible_worktree_entries.extend(repository.status());
|
||||
work_directory = Some(worktree::WorkDirectory::clone(repository));
|
||||
}
|
||||
|
||||
// TODO use the GitTraversal
|
||||
// let mut visible_worktree_entries = snapshot
|
||||
// .entries(false, 0)
|
||||
// .filter(|entry| !entry.is_external)
|
||||
// .filter(|entry| entry.git_status.is_some())
|
||||
// .cloned()
|
||||
// .collect::<Vec<_>>();
|
||||
// snapshot.propagate_git_statuses(&mut visible_worktree_entries);
|
||||
// project::sort_worktree_entries(&mut visible_worktree_entries);
|
||||
|
||||
if !visible_worktree_entries.is_empty() {
|
||||
self.visible_entries.push(WorktreeEntries {
|
||||
worktree_id,
|
||||
work_directory: work_directory.unwrap(),
|
||||
visible_entries: visible_worktree_entries
|
||||
.into_iter()
|
||||
.map(|entry| GitPanelEntry {
|
||||
|
@ -580,24 +595,25 @@ impl GitPanel {
|
|||
}
|
||||
self.visible_entries.extend(after_update);
|
||||
|
||||
if let Some((worktree_id, entry_id)) = new_selected_entry {
|
||||
self.selected_item = self.visible_entries.iter().enumerate().find_map(
|
||||
|(worktree_index, worktree_entries)| {
|
||||
if worktree_entries.worktree_id == worktree_id {
|
||||
worktree_entries
|
||||
.visible_entries
|
||||
.iter()
|
||||
.position(|entry| entry.id == entry_id)
|
||||
.map(|entry_index| {
|
||||
worktree_index * worktree_entries.visible_entries.len()
|
||||
+ entry_index
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
// TODO re-implement this
|
||||
// if let Some((worktree_id, entry_id)) = new_selected_entry {
|
||||
// self.selected_item = self.visible_entries.iter().enumerate().find_map(
|
||||
// |(worktree_index, worktree_entries)| {
|
||||
// if worktree_entries.worktree_id == worktree_id {
|
||||
// worktree_entries
|
||||
// .visible_entries
|
||||
// .iter()
|
||||
// .position(|entry| entry.id == entry_id)
|
||||
// .map(|entry_index| {
|
||||
// worktree_index * worktree_entries.visible_entries.len()
|
||||
// + entry_index
|
||||
// })
|
||||
// } else {
|
||||
// None
|
||||
// }
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
|
||||
let project = self.project.downgrade();
|
||||
self.git_diff_editor_updates = cx.spawn(|git_panel, mut cx| async move {
|
||||
|
@ -612,12 +628,14 @@ impl GitPanel {
|
|||
.visible_entries
|
||||
.iter()
|
||||
.filter_map(|entry| {
|
||||
let git_status = entry.git_status()?;
|
||||
let git_status = entry.status;
|
||||
let entry_hunks = entry.hunks.clone();
|
||||
let (entry_path, unstaged_changes_task) =
|
||||
project.update(cx, |project, cx| {
|
||||
let entry_path =
|
||||
project.path_for_entry(entry.id, cx)?;
|
||||
let entry_path = ProjectPath {
|
||||
worktree_id: worktree_entries.worktree_id,
|
||||
path: worktree_entries.work_directory.unrelativize(&entry.repo_path)?,
|
||||
};
|
||||
let open_task =
|
||||
project.open_path(entry_path.clone(), cx);
|
||||
let unstaged_changes_task =
|
||||
|
@ -682,8 +700,8 @@ impl GitPanel {
|
|||
)
|
||||
.collect()
|
||||
}
|
||||
// TODO support conflicts display
|
||||
GitFileStatus::Conflict => Vec::new(),
|
||||
// TODO support these
|
||||
GitFileStatus::Conflict | GitFileStatus::Deleted | GitFileStatus::Untracked => Vec::new(),
|
||||
}
|
||||
}).clone()
|
||||
})?;
|
||||
|
@ -992,18 +1010,17 @@ impl GitPanel {
|
|||
|
||||
fn render_entry(
|
||||
&self,
|
||||
id: ProjectEntryId,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
details: EntryDetails,
|
||||
cx: &ViewContext<Self>,
|
||||
) -> impl IntoElement {
|
||||
let id = id.to_proto() as usize;
|
||||
let checkbox_id = ElementId::Name(format!("checkbox_{}", id).into());
|
||||
let checkbox_id = ElementId::Name(format!("checkbox_{}", ix).into());
|
||||
let is_staged = ToggleState::Selected;
|
||||
let handle = cx.view().downgrade();
|
||||
|
||||
h_flex()
|
||||
.id(id)
|
||||
.id(("git-panel-entry", ix))
|
||||
.h(px(28.))
|
||||
.w_full()
|
||||
.pl(px(12. + 12. * details.depth as f32))
|
||||
|
@ -1019,7 +1036,7 @@ impl GitPanel {
|
|||
this.child(git_status_icon(status))
|
||||
})
|
||||
.child(
|
||||
ListItem::new(("label", id))
|
||||
ListItem::new(details.path.0.clone())
|
||||
.toggle_state(selected)
|
||||
.child(h_flex().gap_1p5().child(details.display_name.clone()))
|
||||
.on_click(move |e, cx| {
|
||||
|
|
|
@ -44,10 +44,13 @@ const REMOVED_COLOR: Hsla = Hsla {
|
|||
// TODO: Add updated status colors to theme
|
||||
pub fn git_status_icon(status: GitFileStatus) -> impl IntoElement {
|
||||
match status {
|
||||
GitFileStatus::Added => Icon::new(IconName::SquarePlus).color(Color::Custom(ADDED_COLOR)),
|
||||
GitFileStatus::Added | GitFileStatus::Untracked => {
|
||||
Icon::new(IconName::SquarePlus).color(Color::Custom(ADDED_COLOR))
|
||||
}
|
||||
GitFileStatus::Modified => {
|
||||
Icon::new(IconName::SquareDot).color(Color::Custom(MODIFIED_COLOR))
|
||||
}
|
||||
GitFileStatus::Conflict => Icon::new(IconName::Warning).color(Color::Custom(REMOVED_COLOR)),
|
||||
GitFileStatus::Deleted => Icon::new(IconName::Warning).color(Color::Custom(REMOVED_COLOR)),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1580,7 +1580,7 @@ impl LinuxClient for X11Client {
|
|||
}
|
||||
}
|
||||
|
||||
// Adatpted from:
|
||||
// Adapted from:
|
||||
// https://docs.rs/winit/0.29.11/src/winit/platform_impl/linux/x11/monitor.rs.html#103-111
|
||||
pub fn mode_refresh_rate(mode: &randr::ModeInfo) -> Duration {
|
||||
if mode.dot_clock == 0 || mode.htotal == 0 || mode.vtotal == 0 {
|
||||
|
|
|
@ -322,7 +322,7 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
|
|||
let mut chars_with_shift = chars_for_modified_key(native_event.keyCode(), SHIFT_MOD);
|
||||
let always_use_cmd_layout = always_use_command_layout();
|
||||
|
||||
// Handle Dvorak+QWERTY / Russian / Armeniam
|
||||
// Handle Dvorak+QWERTY / Russian / Armenian
|
||||
if command || always_use_cmd_layout {
|
||||
let chars_with_cmd = chars_for_modified_key(native_event.keyCode(), CMD_MOD);
|
||||
let chars_with_both =
|
||||
|
|
|
@ -4880,6 +4880,8 @@ pub enum ElementId {
|
|||
FocusHandle(FocusId),
|
||||
/// A combination of a name and an integer.
|
||||
NamedInteger(SharedString, usize),
|
||||
/// A path
|
||||
Path(Arc<std::path::Path>),
|
||||
}
|
||||
|
||||
impl Display for ElementId {
|
||||
|
@ -4891,6 +4893,7 @@ impl Display for ElementId {
|
|||
ElementId::FocusHandle(_) => write!(f, "FocusHandle")?,
|
||||
ElementId::NamedInteger(s, i) => write!(f, "{}-{}", s, i)?,
|
||||
ElementId::Uuid(uuid) => write!(f, "{}", uuid)?,
|
||||
ElementId::Path(path) => write!(f, "{}", path.display())?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -4927,6 +4930,12 @@ impl From<SharedString> for ElementId {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<Arc<std::path::Path>> for ElementId {
|
||||
fn from(path: Arc<std::path::Path>) -> Self {
|
||||
ElementId::Path(path)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for ElementId {
|
||||
fn from(name: &'static str) -> Self {
|
||||
ElementId::Name(name.into())
|
||||
|
|
|
@ -96,12 +96,18 @@ impl Item for ImageView {
|
|||
|
||||
fn tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement {
|
||||
let project_path = self.image_item.read(cx).project_path(cx);
|
||||
|
||||
let label_color = if ItemSettings::get_global(cx).git_status {
|
||||
let git_status = self
|
||||
.project
|
||||
.read(cx)
|
||||
.project_path_git_status(&project_path, cx);
|
||||
|
||||
self.project
|
||||
.read(cx)
|
||||
.entry_for_path(&project_path, cx)
|
||||
.map(|entry| {
|
||||
entry_git_aware_label_color(entry.git_status, entry.is_ignored, params.selected)
|
||||
entry_git_aware_label_color(git_status, entry.is_ignored, params.selected)
|
||||
})
|
||||
.unwrap_or_else(|| params.text_color())
|
||||
} else {
|
||||
|
|
|
@ -19,8 +19,8 @@ db.workspace = true
|
|||
editor.workspace = true
|
||||
file_icons.workspace = true
|
||||
fuzzy.workspace = true
|
||||
itertools.workspace = true
|
||||
gpui.workspace = true
|
||||
itertools.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
menu.workspace = true
|
||||
|
@ -36,8 +36,8 @@ smol.workspace = true
|
|||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
worktree.workspace = true
|
||||
workspace.workspace = true
|
||||
worktree.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
search = { workspace = true, features = ["test-support"] }
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -569,9 +569,9 @@ impl LocalBufferStore {
|
|||
buffer_change_sets
|
||||
.into_iter()
|
||||
.filter_map(|(change_set, buffer_snapshot, path)| {
|
||||
let (repo_entry, local_repo_entry) = snapshot.repo_for_path(&path)?;
|
||||
let relative_path = repo_entry.relativize(&snapshot, &path).ok()?;
|
||||
let base_text = local_repo_entry.repo().load_index_text(&relative_path);
|
||||
let local_repo = snapshot.local_repo_for_path(&path)?;
|
||||
let relative_path = local_repo.relativize(&path).ok()?;
|
||||
let base_text = local_repo.repo().load_index_text(&relative_path);
|
||||
Some((change_set, buffer_snapshot, base_text))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
|
@ -1161,16 +1161,16 @@ impl BufferStore {
|
|||
Worktree::Local(worktree) => {
|
||||
let worktree = worktree.snapshot();
|
||||
let blame_params = maybe!({
|
||||
let (repo_entry, local_repo_entry) = match worktree.repo_for_path(&file.path) {
|
||||
let local_repo = match worktree.local_repo_for_path(&file.path) {
|
||||
Some(repo_for_path) => repo_for_path,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let relative_path = repo_entry
|
||||
.relativize(&worktree, &file.path)
|
||||
let relative_path = local_repo
|
||||
.relativize(&file.path)
|
||||
.context("failed to relativize buffer path")?;
|
||||
|
||||
let repo = local_repo_entry.repo().clone();
|
||||
let repo = local_repo.repo().clone();
|
||||
|
||||
let content = match version {
|
||||
Some(version) => buffer.rope_for_version(&version).clone(),
|
||||
|
@ -1247,7 +1247,7 @@ impl BufferStore {
|
|||
});
|
||||
};
|
||||
|
||||
let path = match repo_entry.relativize(worktree, file.path()) {
|
||||
let path = match repo_entry.relativize(file.path()) {
|
||||
Ok(RepoPath(path)) => path,
|
||||
Err(e) => return Task::ready(Err(e)),
|
||||
};
|
||||
|
|
|
@ -87,9 +87,8 @@ pub use language::Location;
|
|||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub use prettier::FORMAT_SUFFIX as TEST_PRETTIER_FORMAT_SUFFIX;
|
||||
pub use worktree::{
|
||||
Entry, EntryKind, File, LocalWorktree, PathChange, ProjectEntryId, RepositoryEntry,
|
||||
UpdatedEntriesSet, UpdatedGitRepositoriesSet, Worktree, WorktreeId, WorktreeSettings,
|
||||
FS_WATCH_LATENCY,
|
||||
Entry, EntryKind, File, LocalWorktree, PathChange, ProjectEntryId, UpdatedEntriesSet,
|
||||
UpdatedGitRepositoriesSet, Worktree, WorktreeId, WorktreeSettings, FS_WATCH_LATENCY,
|
||||
};
|
||||
|
||||
const SERVER_LAUNCHING_BEFORE_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
@ -3109,6 +3108,7 @@ impl LspStore {
|
|||
WorktreeStoreEvent::WorktreeUpdateSent(worktree) => {
|
||||
worktree.update(cx, |worktree, _cx| self.send_diagnostic_summaries(worktree));
|
||||
}
|
||||
WorktreeStoreEvent::GitRepositoryUpdated => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -39,7 +39,10 @@ use futures::{
|
|||
pub use image_store::{ImageItem, ImageStore};
|
||||
use image_store::{ImageItemEvent, ImageStoreEvent};
|
||||
|
||||
use git::{blame::Blame, repository::GitRepository};
|
||||
use git::{
|
||||
blame::Blame,
|
||||
repository::{GitFileStatus, GitRepository},
|
||||
};
|
||||
use gpui::{
|
||||
AnyModel, AppContext, AsyncAppContext, BorrowAppContext, Context as _, EventEmitter, Hsla,
|
||||
Model, ModelContext, SharedString, Task, WeakModel, WindowContext,
|
||||
|
@ -95,9 +98,8 @@ pub use task_inventory::{
|
|||
BasicContextProvider, ContextProviderWithTasks, Inventory, TaskSourceKind,
|
||||
};
|
||||
pub use worktree::{
|
||||
Entry, EntryKind, File, LocalWorktree, PathChange, ProjectEntryId, RepositoryEntry,
|
||||
UpdatedEntriesSet, UpdatedGitRepositoriesSet, Worktree, WorktreeId, WorktreeSettings,
|
||||
FS_WATCH_LATENCY,
|
||||
Entry, EntryKind, File, LocalWorktree, PathChange, ProjectEntryId, UpdatedEntriesSet,
|
||||
UpdatedGitRepositoriesSet, Worktree, WorktreeId, WorktreeSettings, FS_WATCH_LATENCY,
|
||||
};
|
||||
|
||||
pub use buffer_store::ProjectTransaction;
|
||||
|
@ -242,6 +244,7 @@ pub enum Event {
|
|||
ActivateProjectPanel,
|
||||
WorktreeAdded(WorktreeId),
|
||||
WorktreeOrderChanged,
|
||||
GitRepositoryUpdated,
|
||||
WorktreeRemoved(WorktreeId),
|
||||
WorktreeUpdatedEntries(WorktreeId, UpdatedEntriesSet),
|
||||
WorktreeUpdatedGitRepositories(WorktreeId),
|
||||
|
@ -1433,6 +1436,15 @@ impl Project {
|
|||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn project_path_git_status(
|
||||
&self,
|
||||
project_path: &ProjectPath,
|
||||
cx: &AppContext,
|
||||
) -> Option<GitFileStatus> {
|
||||
self.worktree_for_id(project_path.worktree_id, cx)
|
||||
.and_then(|worktree| worktree.read(cx).status_for_file(&project_path.path))
|
||||
}
|
||||
|
||||
pub fn visibility_for_paths(&self, paths: &[PathBuf], cx: &AppContext) -> Option<bool> {
|
||||
paths
|
||||
.iter()
|
||||
|
@ -2295,6 +2307,7 @@ impl Project {
|
|||
}
|
||||
WorktreeStoreEvent::WorktreeOrderChanged => cx.emit(Event::WorktreeOrderChanged),
|
||||
WorktreeStoreEvent::WorktreeUpdateSent(_) => {}
|
||||
WorktreeStoreEvent::GitRepositoryUpdated => cx.emit(Event::GitRepositoryUpdated),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3516,17 +3529,6 @@ impl Project {
|
|||
)
|
||||
}
|
||||
|
||||
pub fn get_repo(
|
||||
&self,
|
||||
project_path: &ProjectPath,
|
||||
cx: &AppContext,
|
||||
) -> Option<Arc<dyn GitRepository>> {
|
||||
self.worktree_for_id(project_path.worktree_id, cx)?
|
||||
.read(cx)
|
||||
.as_local()?
|
||||
.local_git_repo(&project_path.path)
|
||||
}
|
||||
|
||||
pub fn get_first_worktree_root_repo(&self, cx: &AppContext) -> Option<Arc<dyn GitRepository>> {
|
||||
let worktree = self.visible_worktrees(cx).next()?.read(cx).as_local()?;
|
||||
let root_entry = worktree.root_git_entry()?;
|
||||
|
@ -4426,8 +4428,10 @@ impl Completion {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn sort_worktree_entries(entries: &mut [Entry]) {
|
||||
pub fn sort_worktree_entries(entries: &mut [impl AsRef<Entry>]) {
|
||||
entries.sort_by(|entry_a, entry_b| {
|
||||
let entry_a = entry_a.as_ref();
|
||||
let entry_b = entry_b.as_ref();
|
||||
compare_paths(
|
||||
(&entry_a.path, entry_a.is_file()),
|
||||
(&entry_b.path, entry_b.is_file()),
|
||||
|
|
|
@ -109,7 +109,7 @@ impl Inventory {
|
|||
/// Pulls its task sources relevant to the worktree and the language given and resolves them with the [`TaskContext`] given.
|
||||
/// Joins the new resolutions with the resolved tasks that were used (spawned) before,
|
||||
/// orders them so that the most recently used come first, all equally used ones are ordered so that the most specific tasks come first.
|
||||
/// Deduplicates the tasks by their labels and contenxt and splits the ordered list into two: used tasks and the rest, newly resolved tasks.
|
||||
/// Deduplicates the tasks by their labels and context and splits the ordered list into two: used tasks and the rest, newly resolved tasks.
|
||||
pub fn used_and_current_resolved_tasks(
|
||||
&self,
|
||||
worktree: Option<WorktreeId>,
|
||||
|
|
|
@ -62,6 +62,7 @@ pub enum WorktreeStoreEvent {
|
|||
WorktreeReleased(EntityId, WorktreeId),
|
||||
WorktreeOrderChanged,
|
||||
WorktreeUpdateSent(Model<Worktree>),
|
||||
GitRepositoryUpdated,
|
||||
}
|
||||
|
||||
impl EventEmitter<WorktreeStoreEvent> for WorktreeStore {}
|
||||
|
@ -322,6 +323,7 @@ impl WorktreeStore {
|
|||
let worktree = Worktree::local(path.clone(), visible, fs, next_entry_id, &mut cx).await;
|
||||
|
||||
let worktree = worktree?;
|
||||
|
||||
this.update(&mut cx, |this, cx| this.add(&worktree, cx))?;
|
||||
|
||||
if visible {
|
||||
|
@ -374,6 +376,17 @@ impl WorktreeStore {
|
|||
this.send_project_updates(cx);
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.subscribe(
|
||||
worktree,
|
||||
|_this, _, event: &worktree::Event, cx| match event {
|
||||
worktree::Event::UpdatedGitRepositories(_) => {
|
||||
cx.emit(WorktreeStoreEvent::GitRepositoryUpdated);
|
||||
}
|
||||
worktree::Event::DeletedEntry(_) | worktree::Event::UpdatedEntries(_) => {}
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn remove_worktree(&mut self, id_to_remove: WorktreeId, cx: &mut ModelContext<Self>) {
|
||||
|
@ -583,11 +596,11 @@ impl WorktreeStore {
|
|||
pub fn shared(
|
||||
&mut self,
|
||||
remote_id: u64,
|
||||
downsteam_client: AnyProtoClient,
|
||||
downstream_client: AnyProtoClient,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.retain_worktrees = true;
|
||||
self.downstream_client = Some((downsteam_client, remote_id));
|
||||
self.downstream_client = Some((downstream_client, remote_id));
|
||||
|
||||
// When shared, retain all worktrees
|
||||
for worktree_handle in self.worktrees.iter_mut() {
|
||||
|
|
|
@ -63,7 +63,7 @@ use workspace::{
|
|||
notifications::{DetachAndPromptErr, NotifyTaskExt},
|
||||
DraggedSelection, OpenInTerminal, PreviewTabsSettings, SelectedEntry, Workspace,
|
||||
};
|
||||
use worktree::CreatedEntry;
|
||||
use worktree::{CreatedEntry, GitEntry, GitEntryRef};
|
||||
|
||||
const PROJECT_PANEL_KEY: &str = "ProjectPanel";
|
||||
const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX;
|
||||
|
@ -76,7 +76,7 @@ pub struct ProjectPanel {
|
|||
// An update loop that keeps incrementing/decrementing scroll offset while there is a dragged entry that's
|
||||
// hovered over the start/end of a list.
|
||||
hover_scroll_task: Option<Task<()>>,
|
||||
visible_entries: Vec<(WorktreeId, Vec<Entry>, OnceCell<HashSet<Arc<Path>>>)>,
|
||||
visible_entries: Vec<(WorktreeId, Vec<GitEntry>, OnceCell<HashSet<Arc<Path>>>)>,
|
||||
/// Maps from leaf project entry ID to the currently selected ancestor.
|
||||
/// Relevant only for auto-fold dirs, where a single project panel entry may actually consist of several
|
||||
/// project entries (and all non-leaf nodes are guaranteed to be directories).
|
||||
|
@ -311,7 +311,8 @@ impl ProjectPanel {
|
|||
this.update_visible_entries(None, cx);
|
||||
cx.notify();
|
||||
}
|
||||
project::Event::WorktreeUpdatedEntries(_, _)
|
||||
project::Event::GitRepositoryUpdated
|
||||
| project::Event::WorktreeUpdatedEntries(_, _)
|
||||
| project::Event::WorktreeAdded(_)
|
||||
| project::Event::WorktreeOrderChanged => {
|
||||
this.update_visible_entries(None, cx);
|
||||
|
@ -1366,9 +1367,10 @@ impl ProjectPanel {
|
|||
let parent_entry = worktree.entry_for_path(parent_path)?;
|
||||
|
||||
// Remove all siblings that are being deleted except the last marked entry
|
||||
let mut siblings: Vec<Entry> = worktree
|
||||
let mut siblings: Vec<_> = worktree
|
||||
.snapshot()
|
||||
.child_entries(parent_path)
|
||||
.with_git_statuses()
|
||||
.filter(|sibling| {
|
||||
sibling.id == latest_entry.id
|
||||
|| !marked_entries_in_worktree.contains(&&SelectedEntry {
|
||||
|
@ -1376,7 +1378,7 @@ impl ProjectPanel {
|
|||
entry_id: sibling.id,
|
||||
})
|
||||
})
|
||||
.cloned()
|
||||
.map(|entry| entry.to_owned())
|
||||
.collect();
|
||||
|
||||
project::sort_worktree_entries(&mut siblings);
|
||||
|
@ -2334,7 +2336,7 @@ impl ProjectPanel {
|
|||
}
|
||||
|
||||
let mut visible_worktree_entries = Vec::new();
|
||||
let mut entry_iter = snapshot.entries(true, 0);
|
||||
let mut entry_iter = snapshot.entries(true, 0).with_git_statuses();
|
||||
let mut auto_folded_ancestors = vec![];
|
||||
while let Some(entry) = entry_iter.entry() {
|
||||
if auto_collapse_dirs && entry.kind.is_dir() {
|
||||
|
@ -2376,7 +2378,7 @@ impl ProjectPanel {
|
|||
}
|
||||
}
|
||||
auto_folded_ancestors.clear();
|
||||
visible_worktree_entries.push(entry.clone());
|
||||
visible_worktree_entries.push(entry.to_owned());
|
||||
let precedes_new_entry = if let Some(new_entry_id) = new_entry_parent_id {
|
||||
entry.id == new_entry_id || {
|
||||
self.ancestors.get(&entry.id).map_or(false, |entries| {
|
||||
|
@ -2390,25 +2392,27 @@ impl ProjectPanel {
|
|||
false
|
||||
};
|
||||
if precedes_new_entry {
|
||||
visible_worktree_entries.push(Entry {
|
||||
id: NEW_ENTRY_ID,
|
||||
kind: new_entry_kind,
|
||||
path: entry.path.join("\0").into(),
|
||||
inode: 0,
|
||||
mtime: entry.mtime,
|
||||
size: entry.size,
|
||||
is_ignored: entry.is_ignored,
|
||||
is_external: false,
|
||||
is_private: false,
|
||||
is_always_included: entry.is_always_included,
|
||||
visible_worktree_entries.push(GitEntry {
|
||||
entry: Entry {
|
||||
id: NEW_ENTRY_ID,
|
||||
kind: new_entry_kind,
|
||||
path: entry.path.join("\0").into(),
|
||||
inode: 0,
|
||||
mtime: entry.mtime,
|
||||
size: entry.size,
|
||||
is_ignored: entry.is_ignored,
|
||||
is_external: false,
|
||||
is_private: false,
|
||||
is_always_included: entry.is_always_included,
|
||||
canonical_path: entry.canonical_path.clone(),
|
||||
char_bag: entry.char_bag,
|
||||
is_fifo: entry.is_fifo,
|
||||
},
|
||||
git_status: entry.git_status,
|
||||
canonical_path: entry.canonical_path.clone(),
|
||||
char_bag: entry.char_bag,
|
||||
is_fifo: entry.is_fifo,
|
||||
});
|
||||
}
|
||||
let worktree_abs_path = worktree.read(cx).abs_path();
|
||||
let (depth, path) = if Some(entry) == worktree.read(cx).root_entry() {
|
||||
let (depth, path) = if Some(entry.entry) == worktree.read(cx).root_entry() {
|
||||
let Some(path_name) = worktree_abs_path
|
||||
.file_name()
|
||||
.with_context(|| {
|
||||
|
@ -2485,8 +2489,8 @@ impl ProjectPanel {
|
|||
entry_iter.advance();
|
||||
}
|
||||
|
||||
snapshot.propagate_git_statuses(&mut visible_worktree_entries);
|
||||
project::sort_worktree_entries(&mut visible_worktree_entries);
|
||||
|
||||
self.visible_entries
|
||||
.push((worktree_id, visible_worktree_entries, OnceCell::new()));
|
||||
}
|
||||
|
@ -2714,13 +2718,13 @@ impl ProjectPanel {
|
|||
None
|
||||
}
|
||||
|
||||
fn entry_at_index(&self, index: usize) -> Option<(WorktreeId, &Entry)> {
|
||||
fn entry_at_index(&self, index: usize) -> Option<(WorktreeId, GitEntryRef)> {
|
||||
let mut offset = 0;
|
||||
for (worktree_id, visible_worktree_entries, _) in &self.visible_entries {
|
||||
if visible_worktree_entries.len() > offset + index {
|
||||
return visible_worktree_entries
|
||||
.get(index)
|
||||
.map(|entry| (*worktree_id, entry));
|
||||
.map(|entry| (*worktree_id, entry.to_ref()));
|
||||
}
|
||||
offset += visible_worktree_entries.len();
|
||||
}
|
||||
|
@ -2753,7 +2757,7 @@ impl ProjectPanel {
|
|||
.collect()
|
||||
});
|
||||
for entry in visible_worktree_entries[entry_range].iter() {
|
||||
callback(entry, entries, cx);
|
||||
callback(&entry, entries, cx);
|
||||
}
|
||||
ix = end_ix;
|
||||
}
|
||||
|
@ -2822,7 +2826,7 @@ impl ProjectPanel {
|
|||
};
|
||||
|
||||
let (depth, difference) =
|
||||
ProjectPanel::calculate_depth_and_difference(entry, entries);
|
||||
ProjectPanel::calculate_depth_and_difference(&entry, entries);
|
||||
|
||||
let filename = match difference {
|
||||
diff if diff > 1 => entry
|
||||
|
@ -2951,9 +2955,9 @@ impl ProjectPanel {
|
|||
worktree_id: WorktreeId,
|
||||
reverse_search: bool,
|
||||
only_visible_entries: bool,
|
||||
predicate: impl Fn(&Entry, WorktreeId) -> bool,
|
||||
predicate: impl Fn(GitEntryRef, WorktreeId) -> bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<Entry> {
|
||||
) -> Option<GitEntry> {
|
||||
if only_visible_entries {
|
||||
let entries = self
|
||||
.visible_entries
|
||||
|
@ -2968,15 +2972,18 @@ impl ProjectPanel {
|
|||
.clone();
|
||||
|
||||
return utils::ReversibleIterable::new(entries.iter(), reverse_search)
|
||||
.find(|ele| predicate(ele, worktree_id))
|
||||
.find(|ele| predicate(ele.to_ref(), worktree_id))
|
||||
.cloned();
|
||||
}
|
||||
|
||||
let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
|
||||
worktree.update(cx, |tree, _| {
|
||||
utils::ReversibleIterable::new(tree.entries(true, 0usize), reverse_search)
|
||||
.find_single_ended(|ele| predicate(ele, worktree_id))
|
||||
.cloned()
|
||||
utils::ReversibleIterable::new(
|
||||
tree.entries(true, 0usize).with_git_statuses(),
|
||||
reverse_search,
|
||||
)
|
||||
.find_single_ended(|ele| predicate(*ele, worktree_id))
|
||||
.map(|ele| ele.to_owned())
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -2984,7 +2991,7 @@ impl ProjectPanel {
|
|||
&self,
|
||||
start: Option<&SelectedEntry>,
|
||||
reverse_search: bool,
|
||||
predicate: impl Fn(&Entry, WorktreeId) -> bool,
|
||||
predicate: impl Fn(GitEntryRef, WorktreeId) -> bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<SelectedEntry> {
|
||||
let mut worktree_ids: Vec<_> = self
|
||||
|
@ -3006,7 +3013,9 @@ impl ProjectPanel {
|
|||
let root_entry = tree.root_entry()?;
|
||||
let tree_id = tree.id();
|
||||
|
||||
let mut first_iter = tree.traverse_from_path(true, true, true, entry.path.as_ref());
|
||||
let mut first_iter = tree
|
||||
.traverse_from_path(true, true, true, entry.path.as_ref())
|
||||
.with_git_statuses();
|
||||
|
||||
if reverse_search {
|
||||
first_iter.next();
|
||||
|
@ -3014,25 +3023,25 @@ impl ProjectPanel {
|
|||
|
||||
let first = first_iter
|
||||
.enumerate()
|
||||
.take_until(|(count, ele)| *ele == root_entry && *count != 0usize)
|
||||
.map(|(_, ele)| ele)
|
||||
.find(|ele| predicate(ele, tree_id))
|
||||
.cloned();
|
||||
.take_until(|(count, entry)| entry.entry == root_entry && *count != 0usize)
|
||||
.map(|(_, entry)| entry)
|
||||
.find(|ele| predicate(*ele, tree_id))
|
||||
.map(|ele| ele.to_owned());
|
||||
|
||||
let second_iter = tree.entries(true, 0usize);
|
||||
let second_iter = tree.entries(true, 0usize).with_git_statuses();
|
||||
|
||||
let second = if reverse_search {
|
||||
second_iter
|
||||
.take_until(|ele| ele.id == start.entry_id)
|
||||
.filter(|ele| predicate(ele, tree_id))
|
||||
.filter(|ele| predicate(*ele, tree_id))
|
||||
.last()
|
||||
.cloned()
|
||||
.map(|ele| ele.to_owned())
|
||||
} else {
|
||||
second_iter
|
||||
.take_while(|ele| ele.id != start.entry_id)
|
||||
.filter(|ele| predicate(ele, tree_id))
|
||||
.filter(|ele| predicate(*ele, tree_id))
|
||||
.last()
|
||||
.cloned()
|
||||
.map(|ele| ele.to_owned())
|
||||
};
|
||||
|
||||
if reverse_search {
|
||||
|
@ -3089,7 +3098,7 @@ impl ProjectPanel {
|
|||
&self,
|
||||
start: Option<&SelectedEntry>,
|
||||
reverse_search: bool,
|
||||
predicate: impl Fn(&Entry, WorktreeId) -> bool,
|
||||
predicate: impl Fn(GitEntryRef, WorktreeId) -> bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<SelectedEntry> {
|
||||
let mut worktree_ids: Vec<_> = self
|
||||
|
@ -3131,8 +3140,8 @@ impl ProjectPanel {
|
|||
)
|
||||
};
|
||||
|
||||
let first_search = first_iter.find(|ele| predicate(ele, start.worktree_id));
|
||||
let second_search = second_iter.find(|ele| predicate(ele, start.worktree_id));
|
||||
let first_search = first_iter.find(|ele| predicate(ele.to_ref(), start.worktree_id));
|
||||
let second_search = second_iter.find(|ele| predicate(ele.to_ref(), start.worktree_id));
|
||||
|
||||
if first_search.is_some() {
|
||||
return first_search.map(|entry| SelectedEntry {
|
||||
|
|
|
@ -1768,7 +1768,7 @@ message Entry {
|
|||
bool is_ignored = 7;
|
||||
bool is_external = 8;
|
||||
reserved 6;
|
||||
optional GitStatus git_status = 9;
|
||||
reserved 9;
|
||||
bool is_fifo = 10;
|
||||
optional uint64 size = 11;
|
||||
optional string canonical_path = 12;
|
||||
|
@ -1777,6 +1777,8 @@ message Entry {
|
|||
message RepositoryEntry {
|
||||
uint64 work_directory_id = 1;
|
||||
optional string branch = 2;
|
||||
repeated StatusEntry updated_statuses = 3;
|
||||
repeated string removed_statuses = 4;
|
||||
}
|
||||
|
||||
message StatusEntry {
|
||||
|
@ -1788,6 +1790,7 @@ enum GitStatus {
|
|||
Added = 0;
|
||||
Modified = 1;
|
||||
Conflict = 2;
|
||||
Deleted = 3;
|
||||
}
|
||||
|
||||
message BufferState {
|
||||
|
|
|
@ -20,6 +20,7 @@ use serde_json::json;
|
|||
use settings::{initial_server_settings_content, Settings, SettingsLocation, SettingsStore};
|
||||
use smol::stream::StreamExt;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
@ -1150,6 +1151,10 @@ async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestA
|
|||
|
||||
let (project, headless_project) = init_test(&fs, cx, server_cx).await;
|
||||
let branches = ["main", "dev", "feature-1"];
|
||||
let branches_set = branches
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect::<HashSet<_>>();
|
||||
fs.insert_branches(Path::new("/code/project1/.git"), &branches);
|
||||
|
||||
let (worktree, _) = project
|
||||
|
@ -1173,10 +1178,10 @@ async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestA
|
|||
|
||||
let remote_branches = remote_branches
|
||||
.into_iter()
|
||||
.map(|branch| branch.name)
|
||||
.collect::<Vec<_>>();
|
||||
.map(|branch| branch.name.to_string())
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
assert_eq!(&remote_branches, &branches);
|
||||
assert_eq!(&remote_branches, &branches_set);
|
||||
|
||||
cx.update(|cx| {
|
||||
project.update(cx, |project, cx| {
|
||||
|
|
|
@ -39,7 +39,7 @@ pub async fn launch_remote_kernel(
|
|||
let kernel_launch_request = KernelLaunchRequest {
|
||||
name: kernel_name.to_string(),
|
||||
// Note: since the path we have locally may not be the same as the one on the remote server,
|
||||
// we don't send it. We'll have to evaluate this decisiion along the way.
|
||||
// we don't send it. We'll have to evaluate this decision along the way.
|
||||
path: None,
|
||||
};
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ use collections::HashMap;
|
|||
// for those users.
|
||||
//
|
||||
// The way macOS solves this problem is to move shortcuts around so that they are all reachable,
|
||||
// even if the mnemoic changes. https://developer.apple.com/documentation/swiftui/keyboardshortcut/localization-swift.struct
|
||||
// even if the mnemonic changes. https://developer.apple.com/documentation/swiftui/keyboardshortcut/localization-swift.struct
|
||||
//
|
||||
// For example, cmd-> is the "switch window" shortcut because the > key is right above tab.
|
||||
// To ensure this doesn't cause problems for shortcuts defined for a QWERTY layout, apple moves
|
||||
|
|
|
@ -9,6 +9,15 @@ struct StackEntry<'a, T: Item, D> {
|
|||
position: D,
|
||||
}
|
||||
|
||||
impl<'a, T: Item + fmt::Debug, D: fmt::Debug> fmt::Debug for StackEntry<'a, T, D> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("StackEntry")
|
||||
.field("index", &self.index)
|
||||
.field("position", &self.position)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Cursor<'a, T: Item, D> {
|
||||
tree: &'a SumTree<T>,
|
||||
|
@ -18,6 +27,21 @@ pub struct Cursor<'a, T: Item, D> {
|
|||
at_end: bool,
|
||||
}
|
||||
|
||||
impl<'a, T: Item + fmt::Debug, D: fmt::Debug> fmt::Debug for Cursor<'a, T, D>
|
||||
where
|
||||
T::Summary: fmt::Debug,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Cursor")
|
||||
.field("tree", &self.tree)
|
||||
.field("stack", &self.stack)
|
||||
.field("position", &self.position)
|
||||
.field("did_seek", &self.did_seek)
|
||||
.field("at_end", &self.at_end)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Iter<'a, T: Item> {
|
||||
tree: &'a SumTree<T>,
|
||||
stack: ArrayVec<StackEntry<'a, T, ()>, 16>,
|
||||
|
@ -60,6 +84,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
/// Item is None, when the list is empty, or this cursor is at the end of the list.
|
||||
#[track_caller]
|
||||
pub fn item(&self) -> Option<&'a T> {
|
||||
self.assert_did_seek();
|
||||
|
|
|
@ -42,6 +42,21 @@ pub trait Summary: Clone {
|
|||
fn add_summary(&mut self, summary: &Self, cx: &Self::Context);
|
||||
}
|
||||
|
||||
/// This type exists because we can't implement Summary for () without causing
|
||||
/// type resolution errors
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
||||
pub struct Unit;
|
||||
|
||||
impl Summary for Unit {
|
||||
type Context = ();
|
||||
|
||||
fn zero(_: &()) -> Self {
|
||||
Unit
|
||||
}
|
||||
|
||||
fn add_summary(&mut self, _: &Self, _: &()) {}
|
||||
}
|
||||
|
||||
/// Each [`Summary`] type can have more than one [`Dimension`] type that it measures.
|
||||
///
|
||||
/// You can use dimensions to seek to a specific location in the [`SumTree`]
|
||||
|
@ -761,6 +776,55 @@ impl<T: KeyedItem> SumTree<T> {
|
|||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn contains(&self, key: &T::Key, cx: &<T::Summary as Summary>::Context) -> bool {
|
||||
self.get(key, cx).is_some()
|
||||
}
|
||||
|
||||
pub fn update<F, R>(
|
||||
&mut self,
|
||||
key: &T::Key,
|
||||
cx: &<T::Summary as Summary>::Context,
|
||||
f: F,
|
||||
) -> Option<R>
|
||||
where
|
||||
F: FnOnce(&mut T) -> R,
|
||||
{
|
||||
let mut cursor = self.cursor::<T::Key>(cx);
|
||||
let mut new_tree = cursor.slice(key, Bias::Left, cx);
|
||||
let mut result = None;
|
||||
if Ord::cmp(key, &cursor.end(cx)) == Ordering::Equal {
|
||||
let mut updated = cursor.item().unwrap().clone();
|
||||
result = Some(f(&mut updated));
|
||||
new_tree.push(updated, cx);
|
||||
cursor.next(cx);
|
||||
}
|
||||
new_tree.append(cursor.suffix(cx), cx);
|
||||
drop(cursor);
|
||||
*self = new_tree;
|
||||
result
|
||||
}
|
||||
|
||||
pub fn retain<F: FnMut(&T) -> bool>(
|
||||
&mut self,
|
||||
cx: &<T::Summary as Summary>::Context,
|
||||
mut predicate: F,
|
||||
) {
|
||||
let mut new_map = SumTree::new(cx);
|
||||
|
||||
let mut cursor = self.cursor::<T::Key>(cx);
|
||||
cursor.next(cx);
|
||||
while let Some(item) = cursor.item() {
|
||||
if predicate(&item) {
|
||||
new_map.push(item.clone(), cx);
|
||||
}
|
||||
cursor.next(cx);
|
||||
}
|
||||
drop(cursor);
|
||||
|
||||
*self = new_map;
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> Default for SumTree<T>
|
||||
|
|
|
@ -358,13 +358,14 @@ impl PickerDelegate for TabSwitcherDelegate {
|
|||
.item
|
||||
.project_path(cx)
|
||||
.as_ref()
|
||||
.and_then(|path| self.project.read(cx).entry_for_path(path, cx))
|
||||
.map(|entry| {
|
||||
entry_git_aware_label_color(
|
||||
entry.git_status,
|
||||
entry.is_ignored,
|
||||
selected,
|
||||
)
|
||||
.and_then(|path| {
|
||||
let project = self.project.read(cx);
|
||||
let entry = project.entry_for_path(path, cx)?;
|
||||
let git_status = project.project_path_git_status(path, cx);
|
||||
Some((entry, git_status))
|
||||
})
|
||||
.map(|(entry, git_status)| {
|
||||
entry_git_aware_label_color(git_status, entry.is_ignored, selected)
|
||||
})
|
||||
})
|
||||
.flatten();
|
||||
|
|
|
@ -60,7 +60,7 @@ pub struct SpawnInTerminal {
|
|||
pub show_command: bool,
|
||||
}
|
||||
|
||||
/// A final form of the [`TaskTemplate`], that got resolved with a particualar [`TaskContext`] and now is ready to spawn the actual task.
|
||||
/// A final form of the [`TaskTemplate`], that got resolved with a particular [`TaskContext`] and now is ready to spawn the actual task.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ResolvedTask {
|
||||
/// A way to distinguish tasks produced by the same template, but different contexts.
|
||||
|
|
|
@ -143,13 +143,13 @@ impl TaskTemplate {
|
|||
let truncated_variables = truncate_variables(&task_variables);
|
||||
let cwd = match self.cwd.as_deref() {
|
||||
Some(cwd) => {
|
||||
let substitured_cwd = substitute_all_template_variables_in_str(
|
||||
let substituted_cwd = substitute_all_template_variables_in_str(
|
||||
cwd,
|
||||
&task_variables,
|
||||
&variable_names,
|
||||
&mut substituted_variables,
|
||||
)?;
|
||||
Some(PathBuf::from(substitured_cwd))
|
||||
Some(PathBuf::from(substituted_cwd))
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
|
|
|
@ -100,7 +100,7 @@ impl TerminalPanel {
|
|||
terminal_panel
|
||||
}
|
||||
|
||||
pub fn asssistant_enabled(&mut self, enabled: bool, cx: &mut ViewContext<Self>) {
|
||||
pub fn set_assistant_enabled(&mut self, enabled: bool, cx: &mut ViewContext<Self>) {
|
||||
self.assistant_enabled = enabled;
|
||||
if enabled {
|
||||
let focus_handle = self
|
||||
|
|
|
@ -270,7 +270,7 @@ pub struct ThemeColorsContent {
|
|||
|
||||
/// Fill Color. Used for the muted or deemphasized fill color of an icon.
|
||||
///
|
||||
/// This might be used to show an icon in an inactive pane, or to demphasize a series of icons to give them less visual weight.
|
||||
/// This might be used to show an icon in an inactive pane, or to deemphasize a series of icons to give them less visual weight.
|
||||
#[serde(rename = "icon.muted")]
|
||||
pub icon_muted: Option<String>,
|
||||
|
||||
|
|
|
@ -97,7 +97,7 @@ pub struct ThemeColors {
|
|||
pub icon: Hsla,
|
||||
/// Fill Color. Used for the muted or deemphasized fill color of an icon.
|
||||
///
|
||||
/// This might be used to show an icon in an inactive pane, or to demphasize a series of icons to give them less visual weight.
|
||||
/// This might be used to show an icon in an inactive pane, or to deemphasize a series of icons to give them less visual weight.
|
||||
pub icon_muted: Hsla,
|
||||
/// Fill Color. Used for the disabled fill color of an icon.
|
||||
///
|
||||
|
|
|
@ -71,7 +71,7 @@ impl From<WindowAppearance> for Appearance {
|
|||
}
|
||||
}
|
||||
|
||||
/// Which themes should be loaded. This is used primarlily for testing.
|
||||
/// Which themes should be loaded. This is used primarily for testing.
|
||||
pub enum LoadThemes {
|
||||
/// Only load the base theme.
|
||||
///
|
||||
|
|
|
@ -21,7 +21,7 @@ use gpui::{
|
|||
Interactivity, IntoElement, Model, MouseButton, ParentElement, Render, Stateful,
|
||||
StatefulInteractiveElement, Styled, Subscription, View, ViewContext, VisualContext, WeakView,
|
||||
};
|
||||
use project::{Project, RepositoryEntry};
|
||||
use project::Project;
|
||||
use rpc::proto;
|
||||
use settings::Settings as _;
|
||||
use smallvec::SmallVec;
|
||||
|
@ -487,7 +487,7 @@ impl TitleBar {
|
|||
let workspace = self.workspace.upgrade()?;
|
||||
let branch_name = entry
|
||||
.as_ref()
|
||||
.and_then(RepositoryEntry::branch)
|
||||
.and_then(|entry| entry.branch())
|
||||
.map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH))?;
|
||||
Some(
|
||||
Button::new("project_branch_trigger", branch_name)
|
||||
|
|
|
@ -179,7 +179,7 @@ define_connection! {
|
|||
// group_id: usize, // Primary key for pane_groups
|
||||
// workspace_id: usize, // References workspaces table
|
||||
// parent_group_id: Option<usize>, // None indicates that this is the root node
|
||||
// position: Optiopn<usize>, // None indicates that this is the root node
|
||||
// position: Option<usize>, // None indicates that this is the root node
|
||||
// axis: Option<Axis>, // 'Vertical', 'Horizontal'
|
||||
// flexes: Option<Vec<f32>>, // A JSON array of floats
|
||||
// )
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1497,7 +1497,8 @@ async fn test_bump_mtime_of_git_repo_workdir(cx: &mut TestAppContext) {
|
|||
cx.executor().run_until_parked();
|
||||
|
||||
let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
|
||||
check_propagated_statuses(
|
||||
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&[
|
||||
(Path::new(""), Some(GitFileStatus::Modified)),
|
||||
|
@ -2178,15 +2179,15 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) {
|
|||
|
||||
cx.read(|cx| {
|
||||
let tree = tree.read(cx);
|
||||
let (work_dir, _) = tree.repositories().next().unwrap();
|
||||
assert_eq!(work_dir.as_ref(), Path::new("projects/project1"));
|
||||
let repo = tree.repositories().next().unwrap();
|
||||
assert_eq!(repo.path.as_ref(), Path::new("projects/project1"));
|
||||
assert_eq!(
|
||||
tree.status_for_file(Path::new("projects/project1/a")),
|
||||
Some(GitFileStatus::Modified)
|
||||
);
|
||||
assert_eq!(
|
||||
tree.status_for_file(Path::new("projects/project1/b")),
|
||||
Some(GitFileStatus::Added)
|
||||
Some(GitFileStatus::Untracked)
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -2199,15 +2200,15 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) {
|
|||
|
||||
cx.read(|cx| {
|
||||
let tree = tree.read(cx);
|
||||
let (work_dir, _) = tree.repositories().next().unwrap();
|
||||
assert_eq!(work_dir.as_ref(), Path::new("projects/project2"));
|
||||
let repo = tree.repositories().next().unwrap();
|
||||
assert_eq!(repo.path.as_ref(), Path::new("projects/project2"));
|
||||
assert_eq!(
|
||||
tree.status_for_file(Path::new("projects/project2/a")),
|
||||
Some(GitFileStatus::Modified)
|
||||
);
|
||||
assert_eq!(
|
||||
tree.status_for_file(Path::new("projects/project2/b")),
|
||||
Some(GitFileStatus::Added)
|
||||
Some(GitFileStatus::Untracked)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -2253,23 +2254,13 @@ async fn test_git_repository_for_path(cx: &mut TestAppContext) {
|
|||
|
||||
assert!(tree.repository_for_path("c.txt".as_ref()).is_none());
|
||||
|
||||
let entry = tree.repository_for_path("dir1/src/b.txt".as_ref()).unwrap();
|
||||
assert_eq!(
|
||||
entry
|
||||
.work_directory(tree)
|
||||
.map(|directory| directory.as_ref().to_owned()),
|
||||
Some(Path::new("dir1").to_owned())
|
||||
);
|
||||
let repo = tree.repository_for_path("dir1/src/b.txt".as_ref()).unwrap();
|
||||
assert_eq!(repo.path.as_ref(), Path::new("dir1"));
|
||||
|
||||
let entry = tree
|
||||
let repo = tree
|
||||
.repository_for_path("dir1/deps/dep1/src/a.txt".as_ref())
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
entry
|
||||
.work_directory(tree)
|
||||
.map(|directory| directory.as_ref().to_owned()),
|
||||
Some(Path::new("dir1/deps/dep1").to_owned())
|
||||
);
|
||||
assert_eq!(repo.path.as_ref(), Path::new("dir1/deps/dep1"));
|
||||
|
||||
let entries = tree.files(false, 0);
|
||||
|
||||
|
@ -2278,10 +2269,7 @@ async fn test_git_repository_for_path(cx: &mut TestAppContext) {
|
|||
.map(|(entry, repo)| {
|
||||
(
|
||||
entry.path.as_ref(),
|
||||
repo.and_then(|repo| {
|
||||
repo.work_directory(tree)
|
||||
.map(|work_directory| work_directory.0.to_path_buf())
|
||||
}),
|
||||
repo.map(|repo| repo.path.to_path_buf()),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
@ -2334,7 +2322,7 @@ async fn test_git_repository_for_path(cx: &mut TestAppContext) {
|
|||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_git_status(cx: &mut TestAppContext) {
|
||||
async fn test_file_status(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.executor().allow_parking();
|
||||
const IGNORE_RULE: &str = "**/target";
|
||||
|
@ -2393,17 +2381,17 @@ async fn test_git_status(cx: &mut TestAppContext) {
|
|||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
assert_eq!(snapshot.repositories().count(), 1);
|
||||
let (dir, repo_entry) = snapshot.repositories().next().unwrap();
|
||||
assert_eq!(dir.as_ref(), Path::new("project"));
|
||||
let repo_entry = snapshot.repositories().next().unwrap();
|
||||
assert_eq!(repo_entry.path.as_ref(), Path::new("project"));
|
||||
assert!(repo_entry.location_in_repo.is_none());
|
||||
|
||||
assert_eq!(
|
||||
snapshot.status_for_file(project_path.join(B_TXT)),
|
||||
Some(GitFileStatus::Added)
|
||||
Some(GitFileStatus::Untracked)
|
||||
);
|
||||
assert_eq!(
|
||||
snapshot.status_for_file(project_path.join(F_TXT)),
|
||||
Some(GitFileStatus::Added)
|
||||
Some(GitFileStatus::Untracked)
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -2433,7 +2421,7 @@ async fn test_git_status(cx: &mut TestAppContext) {
|
|||
let snapshot = tree.snapshot();
|
||||
assert_eq!(
|
||||
snapshot.status_for_file(project_path.join(F_TXT)),
|
||||
Some(GitFileStatus::Added)
|
||||
Some(GitFileStatus::Untracked)
|
||||
);
|
||||
assert_eq!(snapshot.status_for_file(project_path.join(B_TXT)), None);
|
||||
assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None);
|
||||
|
@ -2455,7 +2443,7 @@ async fn test_git_status(cx: &mut TestAppContext) {
|
|||
assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None);
|
||||
assert_eq!(
|
||||
snapshot.status_for_file(project_path.join(B_TXT)),
|
||||
Some(GitFileStatus::Added)
|
||||
Some(GitFileStatus::Untracked)
|
||||
);
|
||||
assert_eq!(
|
||||
snapshot.status_for_file(project_path.join(E_TXT)),
|
||||
|
@ -2494,7 +2482,7 @@ async fn test_git_status(cx: &mut TestAppContext) {
|
|||
let snapshot = tree.snapshot();
|
||||
assert_eq!(
|
||||
snapshot.status_for_file(project_path.join(renamed_dir_name).join(RENAMED_FILE)),
|
||||
Some(GitFileStatus::Added)
|
||||
Some(GitFileStatus::Untracked)
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -2518,11 +2506,125 @@ async fn test_git_status(cx: &mut TestAppContext) {
|
|||
.join(Path::new(renamed_dir_name))
|
||||
.join(RENAMED_FILE)
|
||||
),
|
||||
Some(GitFileStatus::Added)
|
||||
Some(GitFileStatus::Untracked)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_git_repository_status(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let root = temp_tree(json!({
|
||||
"project": {
|
||||
"a.txt": "a", // Modified
|
||||
"b.txt": "bb", // Added
|
||||
"c.txt": "ccc", // Unchanged
|
||||
"d.txt": "dddd", // Deleted
|
||||
},
|
||||
|
||||
}));
|
||||
|
||||
// Set up git repository before creating the worktree.
|
||||
let work_dir = root.path().join("project");
|
||||
let repo = git_init(work_dir.as_path());
|
||||
git_add("a.txt", &repo);
|
||||
git_add("c.txt", &repo);
|
||||
git_add("d.txt", &repo);
|
||||
git_commit("Initial commit", &repo);
|
||||
std::fs::remove_file(work_dir.join("d.txt")).unwrap();
|
||||
std::fs::write(work_dir.join("a.txt"), "aa").unwrap();
|
||||
|
||||
let tree = Worktree::local(
|
||||
root.path(),
|
||||
true,
|
||||
Arc::new(RealFs::default()),
|
||||
Default::default(),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
tree.flush_fs_events(cx).await;
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
// Check that the right git state is observed on startup
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
let repo = snapshot.repositories().next().unwrap();
|
||||
let entries = repo.status().collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(entries.len(), 3);
|
||||
assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
|
||||
assert_eq!(entries[0].status, GitFileStatus::Modified);
|
||||
assert_eq!(entries[1].repo_path.as_ref(), Path::new("b.txt"));
|
||||
assert_eq!(entries[1].status, GitFileStatus::Untracked);
|
||||
assert_eq!(entries[2].repo_path.as_ref(), Path::new("d.txt"));
|
||||
assert_eq!(entries[2].status, GitFileStatus::Deleted);
|
||||
});
|
||||
|
||||
std::fs::write(work_dir.join("c.txt"), "some changes").unwrap();
|
||||
eprintln!("File c.txt has been modified");
|
||||
|
||||
tree.flush_fs_events(cx).await;
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
let repository = snapshot.repositories().next().unwrap();
|
||||
let entries = repository.status().collect::<Vec<_>>();
|
||||
|
||||
std::assert_eq!(entries.len(), 4, "entries: {entries:?}");
|
||||
assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
|
||||
assert_eq!(entries[0].status, GitFileStatus::Modified);
|
||||
assert_eq!(entries[1].repo_path.as_ref(), Path::new("b.txt"));
|
||||
assert_eq!(entries[1].status, GitFileStatus::Untracked);
|
||||
// Status updated
|
||||
assert_eq!(entries[2].repo_path.as_ref(), Path::new("c.txt"));
|
||||
assert_eq!(entries[2].status, GitFileStatus::Modified);
|
||||
assert_eq!(entries[3].repo_path.as_ref(), Path::new("d.txt"));
|
||||
assert_eq!(entries[3].status, GitFileStatus::Deleted);
|
||||
});
|
||||
|
||||
git_add("a.txt", &repo);
|
||||
git_add("c.txt", &repo);
|
||||
git_remove_index(Path::new("d.txt"), &repo);
|
||||
git_commit("Another commit", &repo);
|
||||
tree.flush_fs_events(cx).await;
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
std::fs::remove_file(work_dir.join("a.txt")).unwrap();
|
||||
std::fs::remove_file(work_dir.join("b.txt")).unwrap();
|
||||
tree.flush_fs_events(cx).await;
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
let repo = snapshot.repositories().next().unwrap();
|
||||
let entries = repo.status().collect::<Vec<_>>();
|
||||
|
||||
// Deleting an untracked entry, b.txt, should leave no status
|
||||
// a.txt was tracked, and so should have a status
|
||||
assert_eq!(
|
||||
entries.len(),
|
||||
1,
|
||||
"Entries length was incorrect\n{:#?}",
|
||||
&entries
|
||||
);
|
||||
assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
|
||||
assert_eq!(entries[0].status, GitFileStatus::Deleted);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_repository_subfolder_git_status(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
@ -2575,22 +2677,22 @@ async fn test_repository_subfolder_git_status(cx: &mut TestAppContext) {
|
|||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
assert_eq!(snapshot.repositories().count(), 1);
|
||||
let (dir, repo_entry) = snapshot.repositories().next().unwrap();
|
||||
let repo = snapshot.repositories().next().unwrap();
|
||||
// Path is blank because the working directory of
|
||||
// the git repository is located at the root of the project
|
||||
assert_eq!(dir.as_ref(), Path::new(""));
|
||||
assert_eq!(repo.path.as_ref(), Path::new(""));
|
||||
|
||||
// This is the missing path between the root of the project (sub-folder-2) and its
|
||||
// location relative to the root of the repository.
|
||||
assert_eq!(
|
||||
repo_entry.location_in_repo,
|
||||
repo.location_in_repo,
|
||||
Some(Arc::from(Path::new("sub-folder-1/sub-folder-2")))
|
||||
);
|
||||
|
||||
assert_eq!(snapshot.status_for_file("c.txt"), None);
|
||||
assert_eq!(
|
||||
snapshot.status_for_file("d/e.txt"),
|
||||
Some(GitFileStatus::Added)
|
||||
Some(GitFileStatus::Untracked)
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -2612,6 +2714,93 @@ async fn test_repository_subfolder_git_status(cx: &mut TestAppContext) {
|
|||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_traverse_with_git_status(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"x": {
|
||||
".git": {},
|
||||
"x1.txt": "foo",
|
||||
"x2.txt": "bar",
|
||||
"y": {
|
||||
".git": {},
|
||||
"y1.txt": "baz",
|
||||
"y2.txt": "qux"
|
||||
},
|
||||
"z.txt": "sneaky..."
|
||||
},
|
||||
"z": {
|
||||
".git": {},
|
||||
"z1.txt": "quux",
|
||||
"z2.txt": "quuux"
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
fs.set_status_for_repo_via_git_operation(
|
||||
Path::new("/root/x/.git"),
|
||||
&[
|
||||
(Path::new("x2.txt"), GitFileStatus::Modified),
|
||||
(Path::new("z.txt"), GitFileStatus::Added),
|
||||
],
|
||||
);
|
||||
fs.set_status_for_repo_via_git_operation(
|
||||
Path::new("/root/x/y/.git"),
|
||||
&[(Path::new("y1.txt"), GitFileStatus::Conflict)],
|
||||
);
|
||||
fs.set_status_for_repo_via_git_operation(
|
||||
Path::new("/root/z/.git"),
|
||||
&[(Path::new("z2.txt"), GitFileStatus::Added)],
|
||||
);
|
||||
|
||||
let tree = Worktree::local(
|
||||
Path::new("/root"),
|
||||
true,
|
||||
fs.clone(),
|
||||
Default::default(),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
tree.flush_fs_events(cx).await;
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
|
||||
|
||||
let mut traversal = snapshot
|
||||
.traverse_from_path(true, false, true, Path::new("x"))
|
||||
.with_git_statuses();
|
||||
|
||||
let entry = traversal.next().unwrap();
|
||||
assert_eq!(entry.path.as_ref(), Path::new("x/x1.txt"));
|
||||
assert_eq!(entry.git_status, None);
|
||||
let entry = traversal.next().unwrap();
|
||||
assert_eq!(entry.path.as_ref(), Path::new("x/x2.txt"));
|
||||
assert_eq!(entry.git_status, Some(GitFileStatus::Modified));
|
||||
let entry = traversal.next().unwrap();
|
||||
assert_eq!(entry.path.as_ref(), Path::new("x/y/y1.txt"));
|
||||
assert_eq!(entry.git_status, Some(GitFileStatus::Conflict));
|
||||
let entry = traversal.next().unwrap();
|
||||
assert_eq!(entry.path.as_ref(), Path::new("x/y/y2.txt"));
|
||||
assert_eq!(entry.git_status, None);
|
||||
let entry = traversal.next().unwrap();
|
||||
assert_eq!(entry.path.as_ref(), Path::new("x/z.txt"));
|
||||
assert_eq!(entry.git_status, Some(GitFileStatus::Added));
|
||||
let entry = traversal.next().unwrap();
|
||||
assert_eq!(entry.path.as_ref(), Path::new("z/z1.txt"));
|
||||
assert_eq!(entry.git_status, None);
|
||||
let entry = traversal.next().unwrap();
|
||||
assert_eq!(entry.path.as_ref(), Path::new("z/z2.txt"));
|
||||
assert_eq!(entry.git_status, Some(GitFileStatus::Added));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
@ -2638,7 +2827,6 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
|
|||
"h1.txt": "",
|
||||
"h2.txt": ""
|
||||
},
|
||||
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
@ -2668,7 +2856,16 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
|
|||
cx.executor().run_until_parked();
|
||||
let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
|
||||
|
||||
check_propagated_statuses(
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&[
|
||||
(Path::new(""), Some(GitFileStatus::Conflict)),
|
||||
(Path::new("g"), Some(GitFileStatus::Conflict)),
|
||||
(Path::new("g/h2.txt"), Some(GitFileStatus::Conflict)),
|
||||
],
|
||||
);
|
||||
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&[
|
||||
(Path::new(""), Some(GitFileStatus::Conflict)),
|
||||
|
@ -2685,7 +2882,7 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
|
|||
],
|
||||
);
|
||||
|
||||
check_propagated_statuses(
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&[
|
||||
(Path::new("a/b"), Some(GitFileStatus::Added)),
|
||||
|
@ -2700,7 +2897,7 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
|
|||
],
|
||||
);
|
||||
|
||||
check_propagated_statuses(
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&[
|
||||
(Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
|
||||
|
@ -2712,6 +2909,246 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
|
|||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_propagate_statuses_for_repos_under_project(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"x": {
|
||||
".git": {},
|
||||
"x1.txt": "foo",
|
||||
"x2.txt": "bar"
|
||||
},
|
||||
"y": {
|
||||
".git": {},
|
||||
"y1.txt": "baz",
|
||||
"y2.txt": "qux"
|
||||
},
|
||||
"z": {
|
||||
".git": {},
|
||||
"z1.txt": "quux",
|
||||
"z2.txt": "quuux"
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
fs.set_status_for_repo_via_git_operation(
|
||||
Path::new("/root/x/.git"),
|
||||
&[(Path::new("x1.txt"), GitFileStatus::Added)],
|
||||
);
|
||||
fs.set_status_for_repo_via_git_operation(
|
||||
Path::new("/root/y/.git"),
|
||||
&[
|
||||
(Path::new("y1.txt"), GitFileStatus::Conflict),
|
||||
(Path::new("y2.txt"), GitFileStatus::Modified),
|
||||
],
|
||||
);
|
||||
fs.set_status_for_repo_via_git_operation(
|
||||
Path::new("/root/z/.git"),
|
||||
&[(Path::new("z2.txt"), GitFileStatus::Modified)],
|
||||
);
|
||||
|
||||
let tree = Worktree::local(
|
||||
Path::new("/root"),
|
||||
true,
|
||||
fs.clone(),
|
||||
Default::default(),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
tree.flush_fs_events(cx).await;
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
|
||||
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&[
|
||||
(Path::new("x"), Some(GitFileStatus::Added)),
|
||||
(Path::new("x/x1.txt"), Some(GitFileStatus::Added)),
|
||||
],
|
||||
);
|
||||
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&[
|
||||
(Path::new("y"), Some(GitFileStatus::Conflict)),
|
||||
(Path::new("y/y1.txt"), Some(GitFileStatus::Conflict)),
|
||||
(Path::new("y/y2.txt"), Some(GitFileStatus::Modified)),
|
||||
],
|
||||
);
|
||||
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&[
|
||||
(Path::new("z"), Some(GitFileStatus::Modified)),
|
||||
(Path::new("z/z2.txt"), Some(GitFileStatus::Modified)),
|
||||
],
|
||||
);
|
||||
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&[
|
||||
(Path::new("x"), Some(GitFileStatus::Added)),
|
||||
(Path::new("x/x1.txt"), Some(GitFileStatus::Added)),
|
||||
],
|
||||
);
|
||||
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&[
|
||||
(Path::new("x"), Some(GitFileStatus::Added)),
|
||||
(Path::new("x/x1.txt"), Some(GitFileStatus::Added)),
|
||||
(Path::new("x/x2.txt"), None),
|
||||
(Path::new("y"), Some(GitFileStatus::Conflict)),
|
||||
(Path::new("y/y1.txt"), Some(GitFileStatus::Conflict)),
|
||||
(Path::new("y/y2.txt"), Some(GitFileStatus::Modified)),
|
||||
(Path::new("z"), Some(GitFileStatus::Modified)),
|
||||
(Path::new("z/z1.txt"), None),
|
||||
(Path::new("z/z2.txt"), Some(GitFileStatus::Modified)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_propagate_statuses_for_nested_repos(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"x": {
|
||||
".git": {},
|
||||
"x1.txt": "foo",
|
||||
"x2.txt": "bar",
|
||||
"y": {
|
||||
".git": {},
|
||||
"y1.txt": "baz",
|
||||
"y2.txt": "qux"
|
||||
},
|
||||
"z.txt": "sneaky..."
|
||||
},
|
||||
"z": {
|
||||
".git": {},
|
||||
"z1.txt": "quux",
|
||||
"z2.txt": "quuux"
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
fs.set_status_for_repo_via_git_operation(
|
||||
Path::new("/root/x/.git"),
|
||||
&[
|
||||
(Path::new("x2.txt"), GitFileStatus::Modified),
|
||||
(Path::new("z.txt"), GitFileStatus::Added),
|
||||
],
|
||||
);
|
||||
fs.set_status_for_repo_via_git_operation(
|
||||
Path::new("/root/x/y/.git"),
|
||||
&[(Path::new("y1.txt"), GitFileStatus::Conflict)],
|
||||
);
|
||||
|
||||
fs.set_status_for_repo_via_git_operation(
|
||||
Path::new("/root/z/.git"),
|
||||
&[(Path::new("z2.txt"), GitFileStatus::Added)],
|
||||
);
|
||||
|
||||
let tree = Worktree::local(
|
||||
Path::new("/root"),
|
||||
true,
|
||||
fs.clone(),
|
||||
Default::default(),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
tree.flush_fs_events(cx).await;
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
|
||||
|
||||
// Sanity check the propagation for x/y and z
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&[
|
||||
(Path::new("x/y"), Some(GitFileStatus::Conflict)), // the y git repository has conflict file in it, and so should have a conflict status
|
||||
(Path::new("x/y/y1.txt"), Some(GitFileStatus::Conflict)),
|
||||
(Path::new("x/y/y2.txt"), None),
|
||||
],
|
||||
);
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&[
|
||||
(Path::new("z"), Some(GitFileStatus::Added)),
|
||||
(Path::new("z/z1.txt"), None),
|
||||
(Path::new("z/z2.txt"), Some(GitFileStatus::Added)),
|
||||
],
|
||||
);
|
||||
|
||||
// Test one of the fundamental cases of propagation blocking, the transition from one git repository to another
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&[
|
||||
(Path::new("x"), Some(GitFileStatus::Modified)),
|
||||
(Path::new("x/y"), Some(GitFileStatus::Conflict)),
|
||||
(Path::new("x/y/y1.txt"), Some(GitFileStatus::Conflict)),
|
||||
],
|
||||
);
|
||||
|
||||
// Sanity check everything around it
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&[
|
||||
(Path::new("x"), Some(GitFileStatus::Modified)),
|
||||
(Path::new("x/x1.txt"), None),
|
||||
(Path::new("x/x2.txt"), Some(GitFileStatus::Modified)),
|
||||
(Path::new("x/y"), Some(GitFileStatus::Conflict)),
|
||||
(Path::new("x/y/y1.txt"), Some(GitFileStatus::Conflict)),
|
||||
(Path::new("x/y/y2.txt"), None),
|
||||
(Path::new("x/z.txt"), Some(GitFileStatus::Added)),
|
||||
],
|
||||
);
|
||||
|
||||
// Test the other fundamental case, transitioning from git repository to non-git repository
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&[
|
||||
(Path::new(""), None),
|
||||
(Path::new("x"), Some(GitFileStatus::Modified)),
|
||||
(Path::new("x/x1.txt"), None),
|
||||
],
|
||||
);
|
||||
|
||||
// And all together now
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&[
|
||||
(Path::new(""), None),
|
||||
(Path::new("x"), Some(GitFileStatus::Modified)),
|
||||
(Path::new("x/x1.txt"), None),
|
||||
(Path::new("x/x2.txt"), Some(GitFileStatus::Modified)),
|
||||
(Path::new("x/y"), Some(GitFileStatus::Conflict)),
|
||||
(Path::new("x/y/y1.txt"), Some(GitFileStatus::Conflict)),
|
||||
(Path::new("x/y/y2.txt"), None),
|
||||
(Path::new("x/z.txt"), Some(GitFileStatus::Added)),
|
||||
(Path::new("z"), Some(GitFileStatus::Added)),
|
||||
(Path::new("z/z1.txt"), None),
|
||||
(Path::new("z/z2.txt"), Some(GitFileStatus::Added)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_private_single_file_worktree(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
@ -2736,22 +3173,20 @@ async fn test_private_single_file_worktree(cx: &mut TestAppContext) {
|
|||
}
|
||||
|
||||
#[track_caller]
|
||||
fn check_propagated_statuses(
|
||||
snapshot: &Snapshot,
|
||||
expected_statuses: &[(&Path, Option<GitFileStatus>)],
|
||||
) {
|
||||
let mut entries = expected_statuses
|
||||
fn check_git_statuses(snapshot: &Snapshot, expected_statuses: &[(&Path, Option<GitFileStatus>)]) {
|
||||
let mut traversal = snapshot
|
||||
.traverse_from_path(true, true, false, "".as_ref())
|
||||
.with_git_statuses();
|
||||
let found_statuses = expected_statuses
|
||||
.iter()
|
||||
.map(|(path, _)| snapshot.entry_for_path(path).unwrap().clone())
|
||||
.map(|&(path, _)| {
|
||||
let git_entry = traversal
|
||||
.find(|git_entry| &*git_entry.path == path)
|
||||
.expect("Traversal has no entry for {path:?}");
|
||||
(path, git_entry.git_status)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
snapshot.propagate_git_statuses(&mut entries);
|
||||
assert_eq!(
|
||||
entries
|
||||
.iter()
|
||||
.map(|e| (e.path.as_ref(), e.git_status))
|
||||
.collect::<Vec<_>>(),
|
||||
expected_statuses
|
||||
);
|
||||
assert_eq!(found_statuses, expected_statuses);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
|
@ -2763,14 +3198,14 @@ fn git_init(path: &Path) -> git2::Repository {
|
|||
fn git_add<P: AsRef<Path>>(path: P, repo: &git2::Repository) {
|
||||
let path = path.as_ref();
|
||||
let mut index = repo.index().expect("Failed to get index");
|
||||
index.add_path(path).expect("Failed to add a.txt");
|
||||
index.add_path(path).expect("Failed to add file");
|
||||
index.write().expect("Failed to write index");
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn git_remove_index(path: &Path, repo: &git2::Repository) {
|
||||
let mut index = repo.index().expect("Failed to get index");
|
||||
index.remove_path(path).expect("Failed to add a.txt");
|
||||
index.remove_path(path).expect("Failed to add file");
|
||||
index.write().expect("Failed to write index");
|
||||
}
|
||||
|
||||
|
@ -2900,7 +3335,8 @@ fn assert_entry_git_state(
|
|||
) {
|
||||
let entry = tree.entry_for_path(path).expect("entry {path} not found");
|
||||
assert_eq!(
|
||||
entry.git_status, git_status,
|
||||
tree.status_for_file(Path::new(path)),
|
||||
git_status,
|
||||
"expected {path} to have git status: {git_status:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
|
|
|
@ -70,7 +70,7 @@ use util::load_shell_from_passwd;
|
|||
#[global_allocator]
|
||||
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
||||
|
||||
fn files_not_createad_on_launch(errors: HashMap<io::ErrorKind, Vec<&Path>>) {
|
||||
fn files_not_created_on_launch(errors: HashMap<io::ErrorKind, Vec<&Path>>) {
|
||||
let message = "Zed failed to launch";
|
||||
let error_details = errors
|
||||
.into_iter()
|
||||
|
@ -179,7 +179,7 @@ fn main() {
|
|||
|
||||
let file_errors = init_paths();
|
||||
if !file_errors.is_empty() {
|
||||
files_not_createad_on_launch(file_errors);
|
||||
files_not_created_on_launch(file_errors);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -38,6 +38,6 @@ Open the AI Assistant panel (`cmd-r` or `ctrl-r`) and enter:
|
|||
|
||||
This extension requires a Perplexity API key to be available via the `PERPLEXITY_API_KEY` environment variable.
|
||||
|
||||
To onbtain a Perplexity.ai API token, login to your Perplexity.ai account and go [Settings->API](https://www.perplexity.ai/settings/api) and under "API Keys" click "Generate". This will require you to have [Perplexity Pro](https://www.perplexity.ai/pro) or to buy API credits. By default the extension uses `llama-3.1-sonar-small-128k-online`, currently cheapest model available which is roughly half a penny per request + a penny per 50,000 tokens. So most requests will cost less than $0.01 USD.
|
||||
To obtain a Perplexity.ai API token, login to your Perplexity.ai account and go [Settings->API](https://www.perplexity.ai/settings/api) and under "API Keys" click "Generate". This will require you to have [Perplexity Pro](https://www.perplexity.ai/pro) or to buy API credits. By default the extension uses `llama-3.1-sonar-small-128k-online`, currently cheapest model available which is roughly half a penny per request + a penny per 50,000 tokens. So most requests will cost less than $0.01 USD.
|
||||
|
||||
Take your API key and add it to your environment by adding `export PERPLEXITY_API_KEY="pplx-0123456789abcdef..."` to your `~/.zshrc` or `~/.bashrc`. Reload close and reopen your terminal session. Check with `env |grep PERPLEXITY_API_KEY`.
|
||||
|
|
|
@ -14,7 +14,7 @@ can_code_sign=false
|
|||
|
||||
# This must match the team in the provisioning profile.
|
||||
IDENTITY="Zed Industries, Inc."
|
||||
APPLE_NOTORIZATION_TEAM="MQ55VZLNZQ"
|
||||
APPLE_NOTARIZATION_TEAM="MQ55VZLNZQ"
|
||||
|
||||
# Function for displaying help info
|
||||
help_info() {
|
||||
|
@ -317,7 +317,7 @@ function sign_app_binaries() {
|
|||
/usr/bin/codesign --deep --force --timestamp --options runtime --sign "$IDENTITY" "$(pwd)/${dmg_file_path}" -v
|
||||
|
||||
echo "Notarizing DMG with Apple"
|
||||
"${xcode_bin_dir_path}/notarytool" submit --wait --apple-id "$APPLE_NOTARIZATION_USERNAME" --password "$APPLE_NOTARIZATION_PASSWORD" --team-id "$APPLE_NOTORIZATION_TEAM" "${dmg_file_path}"
|
||||
"${xcode_bin_dir_path}/notarytool" submit --wait --apple-id "$APPLE_NOTARIZATION_USERNAME" --password "$APPLE_NOTARIZATION_PASSWORD" --team-id "$APPLE_NOTARIZATION_TEAM" "${dmg_file_path}"
|
||||
|
||||
echo "Removing temporary DMG (used only for notarization)"
|
||||
rm "${dmg_file_path}"
|
||||
|
@ -344,7 +344,7 @@ function sign_app_binaries() {
|
|||
if [[ $can_code_sign = true ]]; then
|
||||
echo "Notarizing DMG with Apple"
|
||||
/usr/bin/codesign --deep --force --timestamp --options runtime --sign "$IDENTITY" "$(pwd)/${dmg_file_path}" -v
|
||||
"${xcode_bin_dir_path}/notarytool" submit --wait --apple-id "$APPLE_NOTARIZATION_USERNAME" --password "$APPLE_NOTARIZATION_PASSWORD" --team-id "$APPLE_NOTORIZATION_TEAM" "${dmg_file_path}"
|
||||
"${xcode_bin_dir_path}/notarytool" submit --wait --apple-id "$APPLE_NOTARIZATION_USERNAME" --password "$APPLE_NOTARIZATION_PASSWORD" --team-id "$APPLE_NOTARIZATION_TEAM" "${dmg_file_path}"
|
||||
"${xcode_bin_dir_path}/stapler" staple "${dmg_file_path}"
|
||||
fi
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue