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"
|
name = "collections"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustc-hash 1.1.0",
|
"indexmap",
|
||||||
|
"rustc-hash 2.1.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -5193,6 +5194,7 @@ dependencies = [
|
||||||
"util",
|
"util",
|
||||||
"windows 0.58.0",
|
"windows 0.58.0",
|
||||||
"workspace",
|
"workspace",
|
||||||
|
"worktree",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
@ -389,7 +389,7 @@ hyper = "0.14"
|
||||||
http = "1.1"
|
http = "1.1"
|
||||||
ignore = "0.4.22"
|
ignore = "0.4.22"
|
||||||
image = "0.25.1"
|
image = "0.25.1"
|
||||||
indexmap = { version = "2", features = ["serde"] }
|
indexmap = { version = "2.7.0", features = ["serde"] }
|
||||||
indoc = "2"
|
indoc = "2"
|
||||||
itertools = "0.13.0"
|
itertools = "0.13.0"
|
||||||
jsonwebtoken = "9.3"
|
jsonwebtoken = "9.3"
|
||||||
|
@ -440,9 +440,10 @@ runtimelib = { version = "0.24.0", default-features = false, features = [
|
||||||
] }
|
] }
|
||||||
rustc-demangle = "0.1.23"
|
rustc-demangle = "0.1.23"
|
||||||
rust-embed = { version = "8.4", features = ["include-exclude"] }
|
rust-embed = { version = "8.4", features = ["include-exclude"] }
|
||||||
|
rustc-hash = "2.1.0"
|
||||||
rustls = "0.21.12"
|
rustls = "0.21.12"
|
||||||
rustls-native-certs = "0.8.0"
|
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"
|
semver = "1.0"
|
||||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||||
serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
|
serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
|
||||||
|
|
|
@ -122,7 +122,7 @@ pub fn init(cx: &mut AppContext) {
|
||||||
cx.observe_new_views(
|
cx.observe_new_views(
|
||||||
|terminal_panel: &mut TerminalPanel, cx: &mut ViewContext<TerminalPanel>| {
|
|terminal_panel: &mut TerminalPanel, cx: &mut ViewContext<TerminalPanel>| {
|
||||||
let settings = AssistantSettings::get_global(cx);
|
let settings = AssistantSettings::get_global(cx);
|
||||||
terminal_panel.asssistant_enabled(settings.enabled, cx);
|
terminal_panel.set_assistant_enabled(settings.enabled, cx);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.detach();
|
.detach();
|
||||||
|
|
|
@ -133,7 +133,7 @@ impl InlineAssistant {
|
||||||
};
|
};
|
||||||
let enabled = AssistantSettings::get_global(cx).enabled;
|
let enabled = AssistantSettings::get_global(cx).enabled;
|
||||||
terminal_panel.update(cx, |terminal_panel, cx| {
|
terminal_panel.update(cx, |terminal_panel, cx| {
|
||||||
terminal_panel.asssistant_enabled(enabled, cx)
|
terminal_panel.set_assistant_enabled(enabled, cx)
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
|
@ -118,7 +118,7 @@ impl InlineAssistant {
|
||||||
};
|
};
|
||||||
let enabled = AssistantSettings::get_global(cx).enabled;
|
let enabled = AssistantSettings::get_global(cx).enabled;
|
||||||
terminal_panel.update(cx, |terminal_panel, cx| {
|
terminal_panel.update(cx, |terminal_panel, cx| {
|
||||||
terminal_panel.asssistant_enabled(enabled, cx)
|
terminal_panel.set_assistant_enabled(enabled, cx)
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
|
@ -79,7 +79,7 @@ fn parse_path_with_position(argument_str: &str) -> anyhow::Result<String> {
|
||||||
Ok(existing_path) => PathWithPosition::from_path(existing_path),
|
Ok(existing_path) => PathWithPosition::from_path(existing_path),
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
let path = PathWithPosition::parse_str(argument_str);
|
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) {
|
path.map_path(|path| match fs::canonicalize(&path) {
|
||||||
Ok(path) => Ok(path),
|
Ok(path) => Ok(path),
|
||||||
Err(e) => {
|
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" ON "worktree_repositories" ("project_id");
|
||||||
CREATE INDEX "index_worktree_repositories_on_project_id_and_worktree_id" ON "worktree_repositories" ("project_id", "worktree_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" (
|
CREATE TABLE "worktree_settings_files" (
|
||||||
"project_id" INTEGER NOT NULL,
|
"project_id" INTEGER NOT NULL,
|
||||||
"worktree_id" INTEGER NOT NULL,
|
"worktree_id" INTEGER NOT NULL,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use anyhow::Context as _;
|
use anyhow::Context as _;
|
||||||
|
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -274,8 +275,8 @@ impl Database {
|
||||||
mtime_nanos: ActiveValue::set(mtime.nanos as i32),
|
mtime_nanos: ActiveValue::set(mtime.nanos as i32),
|
||||||
canonical_path: ActiveValue::set(entry.canonical_path.clone()),
|
canonical_path: ActiveValue::set(entry.canonical_path.clone()),
|
||||||
is_ignored: ActiveValue::set(entry.is_ignored),
|
is_ignored: ActiveValue::set(entry.is_ignored),
|
||||||
|
git_status: ActiveValue::set(None),
|
||||||
is_external: ActiveValue::set(entry.is_external),
|
is_external: ActiveValue::set(entry.is_external),
|
||||||
git_status: ActiveValue::set(entry.git_status.map(|status| status as i64)),
|
|
||||||
is_deleted: ActiveValue::set(false),
|
is_deleted: ActiveValue::set(false),
|
||||||
scan_id: ActiveValue::set(update.scan_id as i64),
|
scan_id: ActiveValue::set(update.scan_id as i64),
|
||||||
is_fifo: ActiveValue::set(entry.is_fifo),
|
is_fifo: ActiveValue::set(entry.is_fifo),
|
||||||
|
@ -295,7 +296,6 @@ impl Database {
|
||||||
worktree_entry::Column::MtimeNanos,
|
worktree_entry::Column::MtimeNanos,
|
||||||
worktree_entry::Column::CanonicalPath,
|
worktree_entry::Column::CanonicalPath,
|
||||||
worktree_entry::Column::IsIgnored,
|
worktree_entry::Column::IsIgnored,
|
||||||
worktree_entry::Column::GitStatus,
|
|
||||||
worktree_entry::Column::ScanId,
|
worktree_entry::Column::ScanId,
|
||||||
])
|
])
|
||||||
.to_owned(),
|
.to_owned(),
|
||||||
|
@ -349,6 +349,79 @@ impl Database {
|
||||||
)
|
)
|
||||||
.exec(&*tx)
|
.exec(&*tx)
|
||||||
.await?;
|
.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() {
|
if !update.removed_repositories.is_empty() {
|
||||||
|
@ -643,7 +716,6 @@ impl Database {
|
||||||
canonical_path: db_entry.canonical_path,
|
canonical_path: db_entry.canonical_path,
|
||||||
is_ignored: db_entry.is_ignored,
|
is_ignored: db_entry.is_ignored,
|
||||||
is_external: db_entry.is_external,
|
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,
|
// 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
|
// that just means we won't be able to detect when to resummarize
|
||||||
// based on total number of backlogged bytes - instead, we'd go
|
// based on total number of backlogged bytes - instead, we'd go
|
||||||
|
@ -657,23 +729,49 @@ impl Database {
|
||||||
|
|
||||||
// Populate repository entries.
|
// Populate repository entries.
|
||||||
{
|
{
|
||||||
let mut db_repository_entries = worktree_repository::Entity::find()
|
let db_repository_entries = worktree_repository::Entity::find()
|
||||||
.filter(
|
.filter(
|
||||||
Condition::all()
|
Condition::all()
|
||||||
.add(worktree_repository::Column::ProjectId.eq(project.id))
|
.add(worktree_repository::Column::ProjectId.eq(project.id))
|
||||||
.add(worktree_repository::Column::IsDeleted.eq(false)),
|
.add(worktree_repository::Column::IsDeleted.eq(false)),
|
||||||
)
|
)
|
||||||
.stream(tx)
|
.all(tx)
|
||||||
.await?;
|
.await?;
|
||||||
while let Some(db_repository_entry) = db_repository_entries.next().await {
|
for db_repository_entry in db_repository_entries {
|
||||||
let db_repository_entry = db_repository_entry?;
|
|
||||||
if let Some(worktree) = worktrees.get_mut(&(db_repository_entry.worktree_id as u64))
|
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(
|
worktree.repository_entries.insert(
|
||||||
db_repository_entry.work_directory_id as u64,
|
db_repository_entry.work_directory_id as u64,
|
||||||
proto::RepositoryEntry {
|
proto::RepositoryEntry {
|
||||||
work_directory_id: db_repository_entry.work_directory_id as u64,
|
work_directory_id: db_repository_entry.work_directory_id as u64,
|
||||||
branch: db_repository_entry.branch,
|
branch: db_repository_entry.branch,
|
||||||
|
updated_statuses,
|
||||||
|
removed_statuses: Vec::new(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -662,7 +662,6 @@ impl Database {
|
||||||
canonical_path: db_entry.canonical_path,
|
canonical_path: db_entry.canonical_path,
|
||||||
is_ignored: db_entry.is_ignored,
|
is_ignored: db_entry.is_ignored,
|
||||||
is_external: db_entry.is_external,
|
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,
|
// 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
|
// that just means we won't be able to detect when to resummarize
|
||||||
// based on total number of backlogged bytes - instead, we'd go
|
// based on total number of backlogged bytes - instead, we'd go
|
||||||
|
@ -682,26 +681,69 @@ impl Database {
|
||||||
worktree_repository::Column::IsDeleted.eq(false)
|
worktree_repository::Column::IsDeleted.eq(false)
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut db_repositories = worktree_repository::Entity::find()
|
let db_repositories = worktree_repository::Entity::find()
|
||||||
.filter(
|
.filter(
|
||||||
Condition::all()
|
Condition::all()
|
||||||
.add(worktree_repository::Column::ProjectId.eq(project.id))
|
.add(worktree_repository::Column::ProjectId.eq(project.id))
|
||||||
.add(worktree_repository::Column::WorktreeId.eq(worktree.id))
|
.add(worktree_repository::Column::WorktreeId.eq(worktree.id))
|
||||||
.add(repository_entry_filter),
|
.add(repository_entry_filter),
|
||||||
)
|
)
|
||||||
.stream(tx)
|
.all(tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
while let Some(db_repository) = db_repositories.next().await {
|
for db_repository in db_repositories.into_iter() {
|
||||||
let db_repository = db_repository?;
|
|
||||||
if db_repository.is_deleted {
|
if db_repository.is_deleted {
|
||||||
worktree
|
worktree
|
||||||
.removed_repositories
|
.removed_repositories
|
||||||
.push(db_repository.work_directory_id as u64);
|
.push(db_repository.work_directory_id as u64);
|
||||||
} else {
|
} 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 {
|
worktree.updated_repositories.push(proto::RepositoryEntry {
|
||||||
work_directory_id: db_repository.work_directory_id as u64,
|
work_directory_id: db_repository.work_directory_id as u64,
|
||||||
branch: db_repository.branch,
|
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);
|
assert_eq!(snapshot.status_for_file(file), status);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Smoke test status reading
|
|
||||||
|
|
||||||
project_local.read_with(cx_a, |project, cx| {
|
project_local.read_with(cx_a, |project, cx| {
|
||||||
assert_status(&Path::new(A_TXT), Some(GitFileStatus::Added), project, cx);
|
assert_status(&Path::new(A_TXT), Some(GitFileStatus::Added), project, cx);
|
||||||
assert_status(&Path::new(B_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
|
client_a
|
||||||
.fs()
|
.fs()
|
||||||
.insert_branches(Path::new("/project/.git"), &branches);
|
.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_a, worktree_id) = client_a.build_local_project("/project", cx_a).await;
|
||||||
let project_id = active_call_a
|
let project_id = active_call_a
|
||||||
|
@ -6690,10 +6692,10 @@ async fn test_remote_git_branches(
|
||||||
|
|
||||||
let branches_b = branches_b
|
let branches_b = branches_b
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|branch| branch.name)
|
.map(|branch| branch.name.to_string())
|
||||||
.collect::<Vec<_>>();
|
.collect::<HashSet<_>>();
|
||||||
|
|
||||||
assert_eq!(&branches_b, &branches);
|
assert_eq!(branches_b, branches_set);
|
||||||
|
|
||||||
cx_b.update(|cx| {
|
cx_b.update(|cx| {
|
||||||
project_b.update(cx, |project, cx| {
|
project_b.update(cx, |project, cx| {
|
||||||
|
|
|
@ -229,6 +229,10 @@ async fn test_ssh_collaboration_git_branches(
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let branches = ["main", "dev", "feature-1"];
|
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);
|
remote_fs.insert_branches(Path::new("/project/.git"), &branches);
|
||||||
|
|
||||||
// User A connects to the remote project via SSH.
|
// 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
|
let branches_b = branches_b
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|branch| branch.name)
|
.map(|branch| branch.name.to_string())
|
||||||
.collect::<Vec<_>>();
|
.collect::<HashSet<_>>();
|
||||||
|
|
||||||
assert_eq!(&branches_b, &branches);
|
assert_eq!(&branches_b, &branches_set);
|
||||||
|
|
||||||
cx_b.update(|cx| {
|
cx_b.update(|cx| {
|
||||||
project_b.update(cx, |project, cx| {
|
project_b.update(cx, |project, cx| {
|
||||||
|
|
|
@ -16,4 +16,5 @@ doctest = false
|
||||||
test-support = []
|
test-support = []
|
||||||
|
|
||||||
[dependencies]
|
[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")]
|
#[cfg(feature = "test-support")]
|
||||||
pub type HashSet<T> = FxHashSet<T>;
|
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"))]
|
#[cfg(not(feature = "test-support"))]
|
||||||
pub type HashMap<K, V> = std::collections::HashMap<K, V>;
|
pub type HashMap<K, V> = std::collections::HashMap<K, V>;
|
||||||
|
|
||||||
#[cfg(not(feature = "test-support"))]
|
#[cfg(not(feature = "test-support"))]
|
||||||
pub type HashSet<T> = std::collections::HashSet<T>;
|
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::FxHasher;
|
||||||
pub use rustc_hash::{FxHashMap, FxHashSet};
|
pub use rustc_hash::{FxHashMap, FxHashSet};
|
||||||
pub use std::collections::*;
|
pub use std::collections::*;
|
||||||
|
|
|
@ -2748,7 +2748,7 @@ mod tests {
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(_, block)| matches!(block, Block::FoldedBuffer { .. }))
|
.filter(|(_, block)| matches!(block, Block::FoldedBuffer { .. }))
|
||||||
.count(),
|
.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!(
|
assert_eq!(
|
||||||
blocks_snapshot.text(),
|
blocks_snapshot.text(),
|
||||||
|
@ -2994,7 +2994,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.count(),
|
.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!(blocks_snapshot.text(), "\n");
|
||||||
assert_eq!(
|
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.
|
/// 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.
|
/// Allows to ignore certain kinds of highlights.
|
||||||
pub fn highlighted_display_rows(
|
pub fn highlighted_display_rows(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
@ -12573,7 +12573,7 @@ impl Editor {
|
||||||
.file()
|
.file()
|
||||||
.is_none()
|
.is_none()
|
||||||
.then(|| {
|
.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.
|
// 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.
|
// Instead, we try to activate the existing editor in the pane first.
|
||||||
let (editor, pane_item_index) =
|
let (editor, pane_item_index) =
|
||||||
|
|
|
@ -194,14 +194,24 @@ impl ProjectDiffEditor {
|
||||||
let open_tasks = project
|
let open_tasks = project
|
||||||
.update(&mut cx, |project, cx| {
|
.update(&mut cx, |project, cx| {
|
||||||
let worktree = project.worktree_for_id(id, cx)?;
|
let worktree = project.worktree_for_id(id, cx)?;
|
||||||
let applicable_entries = worktree
|
let snapshot = worktree.read(cx).snapshot();
|
||||||
.read(cx)
|
let applicable_entries = snapshot
|
||||||
.entries(false, 0)
|
.repositories()
|
||||||
.filter(|entry| !entry.is_external)
|
.flat_map(|entry| {
|
||||||
.filter(|entry| entry.is_file())
|
entry.status().map(|git_entry| {
|
||||||
.filter_map(|entry| Some((entry.git_status?, entry)))
|
(git_entry.status, entry.join(git_entry.repo_path))
|
||||||
.filter_map(|(git_status, entry)| {
|
})
|
||||||
Some((git_status, entry.id, project.path_for_entry(entry.id, cx)?))
|
})
|
||||||
|
.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<_>>();
|
.collect::<Vec<_>>();
|
||||||
Some(
|
Some(
|
||||||
|
|
|
@ -615,9 +615,20 @@ impl Item for Editor {
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.as_singleton()
|
.as_singleton()
|
||||||
.and_then(|buffer| buffer.read(cx).project_path(cx))
|
.and_then(|buffer| buffer.read(cx).project_path(cx))
|
||||||
.and_then(|path| self.project.as_ref()?.read(cx).entry_for_path(&path, cx))
|
.and_then(|path| {
|
||||||
.map(|entry| {
|
let project = self.project.as_ref()?.read(cx);
|
||||||
entry_git_aware_label_color(entry.git_status, entry.is_ignored, params.selected)
|
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))
|
.unwrap_or_else(|| entry_label_color(params.selected))
|
||||||
} else {
|
} else {
|
||||||
|
@ -1559,10 +1570,10 @@ pub fn entry_git_aware_label_color(
|
||||||
Color::Ignored
|
Color::Ignored
|
||||||
} else {
|
} else {
|
||||||
match git_status {
|
match git_status {
|
||||||
Some(GitFileStatus::Added) => Color::Created,
|
Some(GitFileStatus::Added) | Some(GitFileStatus::Untracked) => Color::Created,
|
||||||
Some(GitFileStatus::Modified) => Color::Modified,
|
Some(GitFileStatus::Modified) => Color::Modified,
|
||||||
Some(GitFileStatus::Conflict) => Color::Conflict,
|
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
|
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 {
|
pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {
|
||||||
let ranges = self.ranges(marked_text);
|
let ranges = self.ranges(marked_text);
|
||||||
self.to_lsp_range(ranges[0].clone())
|
self.to_lsp_range(ranges[0].clone())
|
||||||
|
|
|
@ -230,6 +230,7 @@ impl EditorTestContext {
|
||||||
self.cx.background_executor.run_until_parked();
|
self.cx.background_executor.run_until_parked();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
pub fn ranges(&mut self, marked_text: &str) -> Vec<Range<usize>> {
|
pub fn ranges(&mut self, marked_text: &str) -> Vec<Range<usize>> {
|
||||||
let (unmarked_text, ranges) = marked_text_ranges(marked_text, false);
|
let (unmarked_text, ranges) = marked_text_ranges(marked_text, false);
|
||||||
assert_eq!(self.buffer_text(), unmarked_text);
|
assert_eq!(self.buffer_text(), unmarked_text);
|
||||||
|
|
|
@ -16,6 +16,7 @@ use std::sync::LazyLock;
|
||||||
pub use crate::hosting_provider::*;
|
pub use crate::hosting_provider::*;
|
||||||
pub use crate::remote::*;
|
pub use crate::remote::*;
|
||||||
pub use git2 as libgit;
|
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 DOT_GIT: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new(".git"));
|
||||||
pub static COOKIES: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new("cookies"));
|
pub static COOKIES: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new("cookies"));
|
||||||
|
|
|
@ -7,6 +7,8 @@ use gpui::SharedString;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use rope::Rope;
|
use rope::Rope;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::borrow::Borrow;
|
||||||
|
use std::sync::LazyLock;
|
||||||
use std::{
|
use std::{
|
||||||
cmp::Ordering,
|
cmp::Ordering,
|
||||||
path::{Component, Path, PathBuf},
|
path::{Component, Path, PathBuf},
|
||||||
|
@ -37,7 +39,8 @@ pub trait GitRepository: Send + Sync {
|
||||||
/// Returns the SHA of the current HEAD.
|
/// Returns the SHA of the current HEAD.
|
||||||
fn head_sha(&self) -> Option<String>;
|
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 branches(&self) -> Result<Vec<Branch>>;
|
||||||
fn change_branch(&self, _: &str) -> Result<()>;
|
fn change_branch(&self, _: &str) -> Result<()>;
|
||||||
|
@ -132,7 +135,7 @@ impl GitRepository for RealGitRepository {
|
||||||
Some(self.repository.lock().head().ok()?.target()?.to_string())
|
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
|
let working_directory = self
|
||||||
.repository
|
.repository
|
||||||
.lock()
|
.lock()
|
||||||
|
@ -289,8 +292,9 @@ impl GitRepository for FakeGitRepository {
|
||||||
state.dot_git_dir.clone()
|
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 state = self.state.lock();
|
||||||
|
|
||||||
let mut entries = state
|
let mut entries = state
|
||||||
.worktree_statuses
|
.worktree_statuses
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -306,6 +310,7 @@ impl GitRepository for FakeGitRepository {
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
entries.sort_unstable_by(|a, b| a.0.cmp(&b.0));
|
entries.sort_unstable_by(|a, b| a.0.cmp(&b.0));
|
||||||
|
|
||||||
Ok(GitStatus {
|
Ok(GitStatus {
|
||||||
entries: entries.into(),
|
entries: entries.into(),
|
||||||
})
|
})
|
||||||
|
@ -394,6 +399,8 @@ pub enum GitFileStatus {
|
||||||
Added,
|
Added,
|
||||||
Modified,
|
Modified,
|
||||||
Conflict,
|
Conflict,
|
||||||
|
Deleted,
|
||||||
|
Untracked,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GitFileStatus {
|
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)]
|
#[derive(Clone, Debug, Ord, Hash, PartialOrd, Eq, PartialEq)]
|
||||||
pub struct RepoPath(pub PathBuf);
|
pub struct RepoPath(pub Arc<Path>);
|
||||||
|
|
||||||
impl RepoPath {
|
impl RepoPath {
|
||||||
pub fn new(path: PathBuf) -> Self {
|
pub fn new(path: PathBuf) -> Self {
|
||||||
debug_assert!(path.is_relative(), "Repo paths must be relative");
|
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 {
|
impl From<&Path> for RepoPath {
|
||||||
fn from(value: &Path) -> Self {
|
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 {
|
impl Default for RepoPath {
|
||||||
fn default() -> Self {
|
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 {
|
impl std::ops::Deref for RepoPath {
|
||||||
type Target = PathBuf;
|
type Target = Path;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
&self.0
|
&self.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Borrow<Path> for RepoPath {
|
||||||
|
fn borrow(&self) -> &Path {
|
||||||
|
self.0.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct RepoPathDescendants<'a>(pub &'a Path);
|
pub struct RepoPathDescendants<'a>(pub &'a Path);
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
use crate::repository::{GitFileStatus, RepoPath};
|
use crate::repository::{GitFileStatus, RepoPath};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use std::{
|
use std::{path::Path, process::Stdio, sync::Arc};
|
||||||
path::{Path, PathBuf},
|
|
||||||
process::Stdio,
|
|
||||||
sync::Arc,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct GitStatus {
|
pub struct GitStatus {
|
||||||
|
@ -15,7 +11,7 @@ impl GitStatus {
|
||||||
pub(crate) fn new(
|
pub(crate) fn new(
|
||||||
git_binary: &Path,
|
git_binary: &Path,
|
||||||
working_directory: &Path,
|
working_directory: &Path,
|
||||||
path_prefixes: &[PathBuf],
|
path_prefixes: &[RepoPath],
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
let child = util::command::new_std_command(git_binary)
|
let child = util::command::new_std_command(git_binary)
|
||||||
.current_dir(working_directory)
|
.current_dir(working_directory)
|
||||||
|
@ -27,7 +23,7 @@ impl GitStatus {
|
||||||
"-z",
|
"-z",
|
||||||
])
|
])
|
||||||
.args(path_prefixes.iter().map(|path_prefix| {
|
.args(path_prefixes.iter().map(|path_prefix| {
|
||||||
if *path_prefix == Path::new("") {
|
if path_prefix.0.as_ref() == Path::new("") {
|
||||||
Path::new(".")
|
Path::new(".")
|
||||||
} else {
|
} else {
|
||||||
path_prefix
|
path_prefix
|
||||||
|
@ -55,10 +51,12 @@ impl GitStatus {
|
||||||
let (status, path) = entry.split_at(3);
|
let (status, path) = entry.split_at(3);
|
||||||
let status = status.trim();
|
let status = status.trim();
|
||||||
Some((
|
Some((
|
||||||
RepoPath(PathBuf::from(path)),
|
RepoPath(Path::new(path).into()),
|
||||||
match status {
|
match status {
|
||||||
"A" | "??" => GitFileStatus::Added,
|
"A" => GitFileStatus::Added,
|
||||||
"M" => GitFileStatus::Modified,
|
"M" => GitFileStatus::Modified,
|
||||||
|
"D" => GitFileStatus::Deleted,
|
||||||
|
"??" => GitFileStatus::Untracked,
|
||||||
_ => return None,
|
_ => return None,
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
|
@ -75,7 +73,7 @@ impl GitStatus {
|
||||||
|
|
||||||
pub fn get(&self, path: &Path) -> Option<GitFileStatus> {
|
pub fn get(&self, path: &Path) -> Option<GitFileStatus> {
|
||||||
self.entries
|
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()
|
.ok()
|
||||||
.map(|index| self.entries[index].1)
|
.map(|index| self.entries[index].1)
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,9 +14,11 @@ path = "src/git_ui.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
|
collections.workspace = true
|
||||||
db.workspace = true
|
db.workspace = true
|
||||||
editor.workspace = true
|
editor.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
|
git.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
language.workspace = true
|
language.workspace = true
|
||||||
menu.workspace = true
|
menu.workspace = true
|
||||||
|
@ -29,8 +31,7 @@ settings.workspace = true
|
||||||
ui.workspace = true
|
ui.workspace = true
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
workspace.workspace = true
|
workspace.workspace = true
|
||||||
git.workspace = true
|
worktree.workspace = true
|
||||||
collections.workspace = true
|
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
windows.workspace = true
|
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 anyhow::{Context as _, Result};
|
||||||
use collections::HashMap;
|
|
||||||
use db::kvp::KEY_VALUE_STORE;
|
use db::kvp::KEY_VALUE_STORE;
|
||||||
use editor::{
|
use editor::{
|
||||||
scroll::{Autoscroll, AutoscrollStrategy},
|
scroll::{Autoscroll, AutoscrollStrategy},
|
||||||
Editor, MultiBuffer, DEFAULT_MULTIBUFFER_CONTEXT,
|
Editor, MultiBuffer, DEFAULT_MULTIBUFFER_CONTEXT,
|
||||||
};
|
};
|
||||||
use git::{diff::DiffHunk, repository::GitFileStatus};
|
use git::{
|
||||||
|
diff::DiffHunk,
|
||||||
|
repository::{GitFileStatus, RepoPath},
|
||||||
|
};
|
||||||
|
use gpui::*;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, prelude::*, uniform_list, Action, AppContext, AsyncWindowContext, ClickEvent,
|
actions, prelude::*, uniform_list, Action, AppContext, AsyncWindowContext, ClickEvent,
|
||||||
CursorStyle, EventEmitter, FocusHandle, FocusableView, KeyContext,
|
CursorStyle, EventEmitter, FocusHandle, FocusableView, KeyContext,
|
||||||
|
@ -14,7 +19,7 @@ use gpui::{
|
||||||
};
|
};
|
||||||
use language::{Buffer, BufferRow, OffsetRangeExt};
|
use language::{Buffer, BufferRow, OffsetRangeExt};
|
||||||
use menu::{SelectNext, SelectPrev};
|
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 serde::{Deserialize, Serialize};
|
||||||
use settings::Settings as _;
|
use settings::Settings as _;
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -22,7 +27,7 @@ use std::{
|
||||||
collections::HashSet,
|
collections::HashSet,
|
||||||
ffi::OsStr,
|
ffi::OsStr,
|
||||||
ops::{Deref, Range},
|
ops::{Deref, Range},
|
||||||
path::{Path, PathBuf},
|
path::PathBuf,
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
time::Duration,
|
time::Duration,
|
||||||
|
@ -37,9 +42,7 @@ use workspace::{
|
||||||
dock::{DockPosition, Panel, PanelEvent},
|
dock::{DockPosition, Panel, PanelEvent},
|
||||||
ItemHandle, Workspace,
|
ItemHandle, Workspace,
|
||||||
};
|
};
|
||||||
|
use worktree::StatusEntry;
|
||||||
use crate::{git_status_icon, settings::GitPanelSettings};
|
|
||||||
use crate::{CommitAllChanges, CommitStagedChanges, DiscardAll, StageAll, UnstageAll};
|
|
||||||
|
|
||||||
actions!(git_panel, [ToggleFocus]);
|
actions!(git_panel, [ToggleFocus]);
|
||||||
|
|
||||||
|
@ -69,7 +72,7 @@ pub struct GitStatusEntry {}
|
||||||
struct EntryDetails {
|
struct EntryDetails {
|
||||||
filename: String,
|
filename: String,
|
||||||
display_name: String,
|
display_name: String,
|
||||||
path: Arc<Path>,
|
path: RepoPath,
|
||||||
kind: EntryKind,
|
kind: EntryKind,
|
||||||
depth: usize,
|
depth: usize,
|
||||||
is_expanded: bool,
|
is_expanded: bool,
|
||||||
|
@ -101,7 +104,8 @@ pub struct GitPanel {
|
||||||
scrollbar_state: ScrollbarState,
|
scrollbar_state: ScrollbarState,
|
||||||
selected_item: Option<usize>,
|
selected_item: Option<usize>,
|
||||||
show_scrollbar: bool,
|
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
|
// The entries that are currently shown in the panel, aka
|
||||||
// not hidden by folding or such
|
// not hidden by folding or such
|
||||||
|
@ -115,18 +119,20 @@ pub struct GitPanel {
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct WorktreeEntries {
|
struct WorktreeEntries {
|
||||||
worktree_id: WorktreeId,
|
worktree_id: WorktreeId,
|
||||||
|
// TODO support multiple repositories per worktree
|
||||||
|
work_directory: worktree::WorkDirectory,
|
||||||
visible_entries: Vec<GitPanelEntry>,
|
visible_entries: Vec<GitPanelEntry>,
|
||||||
paths: Rc<OnceCell<HashSet<Arc<Path>>>>,
|
paths: Rc<OnceCell<HashSet<RepoPath>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct GitPanelEntry {
|
struct GitPanelEntry {
|
||||||
entry: Entry,
|
entry: worktree::StatusEntry,
|
||||||
hunks: Rc<OnceCell<Vec<DiffHunk>>>,
|
hunks: Rc<OnceCell<Vec<DiffHunk>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Deref for GitPanelEntry {
|
impl Deref for GitPanelEntry {
|
||||||
type Target = Entry;
|
type Target = worktree::StatusEntry;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
&self.entry
|
&self.entry
|
||||||
|
@ -134,11 +140,11 @@ impl Deref for GitPanelEntry {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WorktreeEntries {
|
impl WorktreeEntries {
|
||||||
fn paths(&self) -> &HashSet<Arc<Path>> {
|
fn paths(&self) -> &HashSet<RepoPath> {
|
||||||
self.paths.get_or_init(|| {
|
self.paths.get_or_init(|| {
|
||||||
self.visible_entries
|
self.visible_entries
|
||||||
.iter()
|
.iter()
|
||||||
.map(|e| (e.entry.path.clone()))
|
.map(|e| (e.entry.repo_path.clone()))
|
||||||
.collect()
|
.collect()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -165,8 +171,11 @@ impl GitPanel {
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
cx.subscribe(&project, |this, _, event, cx| match event {
|
cx.subscribe(&project, |this, _, event, cx| match event {
|
||||||
project::Event::WorktreeRemoved(id) => {
|
project::Event::GitRepositoryUpdated => {
|
||||||
this.expanded_dir_ids.remove(id);
|
this.update_visible_entries(None, None, cx);
|
||||||
|
}
|
||||||
|
project::Event::WorktreeRemoved(_id) => {
|
||||||
|
// this.expanded_dir_ids.remove(id);
|
||||||
this.update_visible_entries(None, None, cx);
|
this.update_visible_entries(None, None, cx);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
@ -183,7 +192,7 @@ impl GitPanel {
|
||||||
project::Event::Closed => {
|
project::Event::Closed => {
|
||||||
this.git_diff_editor_updates = Task::ready(());
|
this.git_diff_editor_updates = Task::ready(());
|
||||||
this.reveal_in_editor = Task::ready(());
|
this.reveal_in_editor = Task::ready(());
|
||||||
this.expanded_dir_ids.clear();
|
// this.expanded_dir_ids.clear();
|
||||||
this.visible_entries.clear();
|
this.visible_entries.clear();
|
||||||
this.git_diff_editor = None;
|
this.git_diff_editor = None;
|
||||||
}
|
}
|
||||||
|
@ -200,8 +209,7 @@ impl GitPanel {
|
||||||
pending_serialization: Task::ready(None),
|
pending_serialization: Task::ready(None),
|
||||||
visible_entries: Vec::new(),
|
visible_entries: Vec::new(),
|
||||||
current_modifiers: cx.modifiers(),
|
current_modifiers: cx.modifiers(),
|
||||||
expanded_dir_ids: Default::default(),
|
// expanded_dir_ids: Default::default(),
|
||||||
|
|
||||||
width: Some(px(360.)),
|
width: Some(px(360.)),
|
||||||
scrollbar_state: ScrollbarState::new(scroll_handle.clone()).parent_view(cx.view()),
|
scrollbar_state: ScrollbarState::new(scroll_handle.clone()).parent_view(cx.view()),
|
||||||
scroll_handle,
|
scroll_handle,
|
||||||
|
@ -288,16 +296,16 @@ impl GitPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn calculate_depth_and_difference(
|
fn calculate_depth_and_difference(
|
||||||
entry: &Entry,
|
entry: &StatusEntry,
|
||||||
visible_worktree_entries: &HashSet<Arc<Path>>,
|
visible_worktree_entries: &HashSet<RepoPath>,
|
||||||
) -> (usize, usize) {
|
) -> (usize, usize) {
|
||||||
let (depth, difference) = entry
|
let (depth, difference) = entry
|
||||||
.path
|
.repo_path
|
||||||
.ancestors()
|
.ancestors()
|
||||||
.skip(1) // Skip the entry itself
|
.skip(1) // Skip the entry itself
|
||||||
.find_map(|ancestor| {
|
.find_map(|ancestor| {
|
||||||
if let Some(parent_entry) = visible_worktree_entries.get(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 parent_path_components_count = parent_entry.components().count();
|
||||||
let difference = entry_path_components_count - parent_path_components_count;
|
let difference = entry_path_components_count - parent_path_components_count;
|
||||||
let depth = parent_entry
|
let depth = parent_entry
|
||||||
|
@ -432,13 +440,7 @@ impl GitPanel {
|
||||||
fn entry_count(&self) -> usize {
|
fn entry_count(&self) -> usize {
|
||||||
self.visible_entries
|
self.visible_entries
|
||||||
.iter()
|
.iter()
|
||||||
.map(|worktree_entries| {
|
.map(|worktree_entries| worktree_entries.visible_entries.len())
|
||||||
worktree_entries
|
|
||||||
.visible_entries
|
|
||||||
.iter()
|
|
||||||
.filter(|entry| entry.git_status.is_some())
|
|
||||||
.count()
|
|
||||||
})
|
|
||||||
.sum()
|
.sum()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -446,7 +448,7 @@ impl GitPanel {
|
||||||
&self,
|
&self,
|
||||||
range: Range<usize>,
|
range: Range<usize>,
|
||||||
cx: &mut ViewContext<Self>,
|
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;
|
let mut ix = 0;
|
||||||
for worktree_entries in &self.visible_entries {
|
for worktree_entries in &self.visible_entries {
|
||||||
|
@ -468,11 +470,11 @@ impl GitPanel {
|
||||||
{
|
{
|
||||||
let snapshot = worktree.read(cx).snapshot();
|
let snapshot = worktree.read(cx).snapshot();
|
||||||
let root_name = OsStr::new(snapshot.root_name());
|
let root_name = OsStr::new(snapshot.root_name());
|
||||||
let expanded_entry_ids = self
|
// let expanded_entry_ids = self
|
||||||
.expanded_dir_ids
|
// .expanded_dir_ids
|
||||||
.get(&snapshot.id())
|
// .get(&snapshot.id())
|
||||||
.map(Vec::as_slice)
|
// .map(Vec::as_slice)
|
||||||
.unwrap_or(&[]);
|
// .unwrap_or(&[]);
|
||||||
|
|
||||||
let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
|
let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
|
||||||
let entries = worktree_entries.paths();
|
let entries = worktree_entries.paths();
|
||||||
|
@ -483,22 +485,22 @@ impl GitPanel {
|
||||||
.enumerate()
|
.enumerate()
|
||||||
{
|
{
|
||||||
let index = index_start + i;
|
let index = index_start + i;
|
||||||
let status = entry.git_status;
|
let status = entry.status;
|
||||||
let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok();
|
let is_expanded = true; //expanded_entry_ids.binary_search(&entry.id).is_ok();
|
||||||
|
|
||||||
let (depth, difference) = Self::calculate_depth_and_difference(entry, entries);
|
let (depth, difference) = Self::calculate_depth_and_difference(entry, entries);
|
||||||
|
|
||||||
let filename = match difference {
|
let filename = match difference {
|
||||||
diff if diff > 1 => entry
|
diff if diff > 1 => entry
|
||||||
.path
|
.repo_path
|
||||||
.iter()
|
.iter()
|
||||||
.skip(entry.path.components().count() - diff)
|
.skip(entry.repo_path.components().count() - diff)
|
||||||
.collect::<PathBuf>()
|
.collect::<PathBuf>()
|
||||||
.to_str()
|
.to_str()
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.to_string(),
|
.to_string(),
|
||||||
_ => entry
|
_ => entry
|
||||||
.path
|
.repo_path
|
||||||
.file_name()
|
.file_name()
|
||||||
.map(|name| name.to_string_lossy().into_owned())
|
.map(|name| name.to_string_lossy().into_owned())
|
||||||
.unwrap_or_else(|| root_name.to_string_lossy().to_string()),
|
.unwrap_or_else(|| root_name.to_string_lossy().to_string()),
|
||||||
|
@ -506,16 +508,17 @@ impl GitPanel {
|
||||||
|
|
||||||
let details = EntryDetails {
|
let details = EntryDetails {
|
||||||
filename,
|
filename,
|
||||||
display_name: entry.path.to_string_lossy().into_owned(),
|
display_name: entry.repo_path.to_string_lossy().into_owned(),
|
||||||
kind: entry.kind,
|
// TODO get it from StatusEntry?
|
||||||
|
kind: EntryKind::File,
|
||||||
is_expanded,
|
is_expanded,
|
||||||
path: entry.path.clone(),
|
path: entry.repo_path.clone(),
|
||||||
status,
|
status: Some(status),
|
||||||
hunks: entry.hunks.clone(),
|
hunks: entry.hunks.clone(),
|
||||||
depth,
|
depth,
|
||||||
index,
|
index,
|
||||||
};
|
};
|
||||||
callback(entry.id, details, cx);
|
callback(ix, details, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ix = end_ix;
|
ix = end_ix;
|
||||||
|
@ -527,7 +530,7 @@ impl GitPanel {
|
||||||
fn update_visible_entries(
|
fn update_visible_entries(
|
||||||
&mut self,
|
&mut self,
|
||||||
for_worktree: Option<WorktreeId>,
|
for_worktree: Option<WorktreeId>,
|
||||||
new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
|
_new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
let project = self.project.read(cx);
|
let project = self.project.read(cx);
|
||||||
|
@ -549,24 +552,36 @@ impl GitPanel {
|
||||||
None => false,
|
None => false,
|
||||||
});
|
});
|
||||||
for worktree in project.visible_worktrees(cx) {
|
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) {
|
if for_worktree.is_some() && for_worktree != Some(worktree_id) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let snapshot = worktree.read(cx).snapshot();
|
|
||||||
|
|
||||||
let mut visible_worktree_entries = snapshot
|
let mut visible_worktree_entries = Vec::new();
|
||||||
.entries(false, 0)
|
// Only use the first repository for now
|
||||||
.filter(|entry| !entry.is_external)
|
let repositories = snapshot.repositories().take(1);
|
||||||
.filter(|entry| entry.git_status.is_some())
|
let mut work_directory = None;
|
||||||
.cloned()
|
for repository in repositories {
|
||||||
.collect::<Vec<_>>();
|
visible_worktree_entries.extend(repository.status());
|
||||||
snapshot.propagate_git_statuses(&mut visible_worktree_entries);
|
work_directory = Some(worktree::WorkDirectory::clone(repository));
|
||||||
project::sort_worktree_entries(&mut visible_worktree_entries);
|
}
|
||||||
|
|
||||||
|
// 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() {
|
if !visible_worktree_entries.is_empty() {
|
||||||
self.visible_entries.push(WorktreeEntries {
|
self.visible_entries.push(WorktreeEntries {
|
||||||
worktree_id,
|
worktree_id,
|
||||||
|
work_directory: work_directory.unwrap(),
|
||||||
visible_entries: visible_worktree_entries
|
visible_entries: visible_worktree_entries
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|entry| GitPanelEntry {
|
.map(|entry| GitPanelEntry {
|
||||||
|
@ -580,24 +595,25 @@ impl GitPanel {
|
||||||
}
|
}
|
||||||
self.visible_entries.extend(after_update);
|
self.visible_entries.extend(after_update);
|
||||||
|
|
||||||
if let Some((worktree_id, entry_id)) = new_selected_entry {
|
// TODO re-implement this
|
||||||
self.selected_item = self.visible_entries.iter().enumerate().find_map(
|
// if let Some((worktree_id, entry_id)) = new_selected_entry {
|
||||||
|(worktree_index, worktree_entries)| {
|
// self.selected_item = self.visible_entries.iter().enumerate().find_map(
|
||||||
if worktree_entries.worktree_id == worktree_id {
|
// |(worktree_index, worktree_entries)| {
|
||||||
worktree_entries
|
// if worktree_entries.worktree_id == worktree_id {
|
||||||
.visible_entries
|
// worktree_entries
|
||||||
.iter()
|
// .visible_entries
|
||||||
.position(|entry| entry.id == entry_id)
|
// .iter()
|
||||||
.map(|entry_index| {
|
// .position(|entry| entry.id == entry_id)
|
||||||
worktree_index * worktree_entries.visible_entries.len()
|
// .map(|entry_index| {
|
||||||
+ entry_index
|
// worktree_index * worktree_entries.visible_entries.len()
|
||||||
})
|
// + entry_index
|
||||||
} else {
|
// })
|
||||||
None
|
// } else {
|
||||||
}
|
// None
|
||||||
},
|
// }
|
||||||
);
|
// },
|
||||||
}
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
let project = self.project.downgrade();
|
let project = self.project.downgrade();
|
||||||
self.git_diff_editor_updates = cx.spawn(|git_panel, mut cx| async move {
|
self.git_diff_editor_updates = cx.spawn(|git_panel, mut cx| async move {
|
||||||
|
@ -612,12 +628,14 @@ impl GitPanel {
|
||||||
.visible_entries
|
.visible_entries
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|entry| {
|
.filter_map(|entry| {
|
||||||
let git_status = entry.git_status()?;
|
let git_status = entry.status;
|
||||||
let entry_hunks = entry.hunks.clone();
|
let entry_hunks = entry.hunks.clone();
|
||||||
let (entry_path, unstaged_changes_task) =
|
let (entry_path, unstaged_changes_task) =
|
||||||
project.update(cx, |project, cx| {
|
project.update(cx, |project, cx| {
|
||||||
let entry_path =
|
let entry_path = ProjectPath {
|
||||||
project.path_for_entry(entry.id, cx)?;
|
worktree_id: worktree_entries.worktree_id,
|
||||||
|
path: worktree_entries.work_directory.unrelativize(&entry.repo_path)?,
|
||||||
|
};
|
||||||
let open_task =
|
let open_task =
|
||||||
project.open_path(entry_path.clone(), cx);
|
project.open_path(entry_path.clone(), cx);
|
||||||
let unstaged_changes_task =
|
let unstaged_changes_task =
|
||||||
|
@ -682,8 +700,8 @@ impl GitPanel {
|
||||||
)
|
)
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
// TODO support conflicts display
|
// TODO support these
|
||||||
GitFileStatus::Conflict => Vec::new(),
|
GitFileStatus::Conflict | GitFileStatus::Deleted | GitFileStatus::Untracked => Vec::new(),
|
||||||
}
|
}
|
||||||
}).clone()
|
}).clone()
|
||||||
})?;
|
})?;
|
||||||
|
@ -992,18 +1010,17 @@ impl GitPanel {
|
||||||
|
|
||||||
fn render_entry(
|
fn render_entry(
|
||||||
&self,
|
&self,
|
||||||
id: ProjectEntryId,
|
ix: usize,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
details: EntryDetails,
|
details: EntryDetails,
|
||||||
cx: &ViewContext<Self>,
|
cx: &ViewContext<Self>,
|
||||||
) -> impl IntoElement {
|
) -> impl IntoElement {
|
||||||
let id = id.to_proto() as usize;
|
let checkbox_id = ElementId::Name(format!("checkbox_{}", ix).into());
|
||||||
let checkbox_id = ElementId::Name(format!("checkbox_{}", id).into());
|
|
||||||
let is_staged = ToggleState::Selected;
|
let is_staged = ToggleState::Selected;
|
||||||
let handle = cx.view().downgrade();
|
let handle = cx.view().downgrade();
|
||||||
|
|
||||||
h_flex()
|
h_flex()
|
||||||
.id(id)
|
.id(("git-panel-entry", ix))
|
||||||
.h(px(28.))
|
.h(px(28.))
|
||||||
.w_full()
|
.w_full()
|
||||||
.pl(px(12. + 12. * details.depth as f32))
|
.pl(px(12. + 12. * details.depth as f32))
|
||||||
|
@ -1019,7 +1036,7 @@ impl GitPanel {
|
||||||
this.child(git_status_icon(status))
|
this.child(git_status_icon(status))
|
||||||
})
|
})
|
||||||
.child(
|
.child(
|
||||||
ListItem::new(("label", id))
|
ListItem::new(details.path.0.clone())
|
||||||
.toggle_state(selected)
|
.toggle_state(selected)
|
||||||
.child(h_flex().gap_1p5().child(details.display_name.clone()))
|
.child(h_flex().gap_1p5().child(details.display_name.clone()))
|
||||||
.on_click(move |e, cx| {
|
.on_click(move |e, cx| {
|
||||||
|
|
|
@ -44,10 +44,13 @@ const REMOVED_COLOR: Hsla = Hsla {
|
||||||
// TODO: Add updated status colors to theme
|
// TODO: Add updated status colors to theme
|
||||||
pub fn git_status_icon(status: GitFileStatus) -> impl IntoElement {
|
pub fn git_status_icon(status: GitFileStatus) -> impl IntoElement {
|
||||||
match status {
|
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 => {
|
GitFileStatus::Modified => {
|
||||||
Icon::new(IconName::SquareDot).color(Color::Custom(MODIFIED_COLOR))
|
Icon::new(IconName::SquareDot).color(Color::Custom(MODIFIED_COLOR))
|
||||||
}
|
}
|
||||||
GitFileStatus::Conflict => Icon::new(IconName::Warning).color(Color::Custom(REMOVED_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
|
// 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 {
|
pub fn mode_refresh_rate(mode: &randr::ModeInfo) -> Duration {
|
||||||
if mode.dot_clock == 0 || mode.htotal == 0 || mode.vtotal == 0 {
|
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 mut chars_with_shift = chars_for_modified_key(native_event.keyCode(), SHIFT_MOD);
|
||||||
let always_use_cmd_layout = always_use_command_layout();
|
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 {
|
if command || always_use_cmd_layout {
|
||||||
let chars_with_cmd = chars_for_modified_key(native_event.keyCode(), CMD_MOD);
|
let chars_with_cmd = chars_for_modified_key(native_event.keyCode(), CMD_MOD);
|
||||||
let chars_with_both =
|
let chars_with_both =
|
||||||
|
|
|
@ -4880,6 +4880,8 @@ pub enum ElementId {
|
||||||
FocusHandle(FocusId),
|
FocusHandle(FocusId),
|
||||||
/// A combination of a name and an integer.
|
/// A combination of a name and an integer.
|
||||||
NamedInteger(SharedString, usize),
|
NamedInteger(SharedString, usize),
|
||||||
|
/// A path
|
||||||
|
Path(Arc<std::path::Path>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for ElementId {
|
impl Display for ElementId {
|
||||||
|
@ -4891,6 +4893,7 @@ impl Display for ElementId {
|
||||||
ElementId::FocusHandle(_) => write!(f, "FocusHandle")?,
|
ElementId::FocusHandle(_) => write!(f, "FocusHandle")?,
|
||||||
ElementId::NamedInteger(s, i) => write!(f, "{}-{}", s, i)?,
|
ElementId::NamedInteger(s, i) => write!(f, "{}-{}", s, i)?,
|
||||||
ElementId::Uuid(uuid) => write!(f, "{}", uuid)?,
|
ElementId::Uuid(uuid) => write!(f, "{}", uuid)?,
|
||||||
|
ElementId::Path(path) => write!(f, "{}", path.display())?,
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
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 {
|
impl From<&'static str> for ElementId {
|
||||||
fn from(name: &'static str) -> Self {
|
fn from(name: &'static str) -> Self {
|
||||||
ElementId::Name(name.into())
|
ElementId::Name(name.into())
|
||||||
|
|
|
@ -96,12 +96,18 @@ impl Item for ImageView {
|
||||||
|
|
||||||
fn tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement {
|
fn tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement {
|
||||||
let project_path = self.image_item.read(cx).project_path(cx);
|
let project_path = self.image_item.read(cx).project_path(cx);
|
||||||
|
|
||||||
let label_color = if ItemSettings::get_global(cx).git_status {
|
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
|
self.project
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.entry_for_path(&project_path, cx)
|
.entry_for_path(&project_path, cx)
|
||||||
.map(|entry| {
|
.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())
|
.unwrap_or_else(|| params.text_color())
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -19,8 +19,8 @@ db.workspace = true
|
||||||
editor.workspace = true
|
editor.workspace = true
|
||||||
file_icons.workspace = true
|
file_icons.workspace = true
|
||||||
fuzzy.workspace = true
|
fuzzy.workspace = true
|
||||||
itertools.workspace = true
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
|
itertools.workspace = true
|
||||||
language.workspace = true
|
language.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
menu.workspace = true
|
menu.workspace = true
|
||||||
|
@ -36,8 +36,8 @@ smol.workspace = true
|
||||||
theme.workspace = true
|
theme.workspace = true
|
||||||
ui.workspace = true
|
ui.workspace = true
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
worktree.workspace = true
|
|
||||||
workspace.workspace = true
|
workspace.workspace = true
|
||||||
|
worktree.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
search = { workspace = true, features = ["test-support"] }
|
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
|
buffer_change_sets
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|(change_set, buffer_snapshot, path)| {
|
.filter_map(|(change_set, buffer_snapshot, path)| {
|
||||||
let (repo_entry, local_repo_entry) = snapshot.repo_for_path(&path)?;
|
let local_repo = snapshot.local_repo_for_path(&path)?;
|
||||||
let relative_path = repo_entry.relativize(&snapshot, &path).ok()?;
|
let relative_path = local_repo.relativize(&path).ok()?;
|
||||||
let base_text = local_repo_entry.repo().load_index_text(&relative_path);
|
let base_text = local_repo.repo().load_index_text(&relative_path);
|
||||||
Some((change_set, buffer_snapshot, base_text))
|
Some((change_set, buffer_snapshot, base_text))
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
|
@ -1161,16 +1161,16 @@ impl BufferStore {
|
||||||
Worktree::Local(worktree) => {
|
Worktree::Local(worktree) => {
|
||||||
let worktree = worktree.snapshot();
|
let worktree = worktree.snapshot();
|
||||||
let blame_params = maybe!({
|
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,
|
Some(repo_for_path) => repo_for_path,
|
||||||
None => return Ok(None),
|
None => return Ok(None),
|
||||||
};
|
};
|
||||||
|
|
||||||
let relative_path = repo_entry
|
let relative_path = local_repo
|
||||||
.relativize(&worktree, &file.path)
|
.relativize(&file.path)
|
||||||
.context("failed to relativize buffer path")?;
|
.context("failed to relativize buffer path")?;
|
||||||
|
|
||||||
let repo = local_repo_entry.repo().clone();
|
let repo = local_repo.repo().clone();
|
||||||
|
|
||||||
let content = match version {
|
let content = match version {
|
||||||
Some(version) => buffer.rope_for_version(&version).clone(),
|
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,
|
Ok(RepoPath(path)) => path,
|
||||||
Err(e) => return Task::ready(Err(e)),
|
Err(e) => return Task::ready(Err(e)),
|
||||||
};
|
};
|
||||||
|
|
|
@ -87,9 +87,8 @@ pub use language::Location;
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub use prettier::FORMAT_SUFFIX as TEST_PRETTIER_FORMAT_SUFFIX;
|
pub use prettier::FORMAT_SUFFIX as TEST_PRETTIER_FORMAT_SUFFIX;
|
||||||
pub use worktree::{
|
pub use worktree::{
|
||||||
Entry, EntryKind, File, LocalWorktree, PathChange, ProjectEntryId, RepositoryEntry,
|
Entry, EntryKind, File, LocalWorktree, PathChange, ProjectEntryId, UpdatedEntriesSet,
|
||||||
UpdatedEntriesSet, UpdatedGitRepositoriesSet, Worktree, WorktreeId, WorktreeSettings,
|
UpdatedGitRepositoriesSet, Worktree, WorktreeId, WorktreeSettings, FS_WATCH_LATENCY,
|
||||||
FS_WATCH_LATENCY,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const SERVER_LAUNCHING_BEFORE_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
|
const SERVER_LAUNCHING_BEFORE_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
|
||||||
|
@ -3109,6 +3108,7 @@ impl LspStore {
|
||||||
WorktreeStoreEvent::WorktreeUpdateSent(worktree) => {
|
WorktreeStoreEvent::WorktreeUpdateSent(worktree) => {
|
||||||
worktree.update(cx, |worktree, _cx| self.send_diagnostic_summaries(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};
|
pub use image_store::{ImageItem, ImageStore};
|
||||||
use image_store::{ImageItemEvent, ImageStoreEvent};
|
use image_store::{ImageItemEvent, ImageStoreEvent};
|
||||||
|
|
||||||
use git::{blame::Blame, repository::GitRepository};
|
use git::{
|
||||||
|
blame::Blame,
|
||||||
|
repository::{GitFileStatus, GitRepository},
|
||||||
|
};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyModel, AppContext, AsyncAppContext, BorrowAppContext, Context as _, EventEmitter, Hsla,
|
AnyModel, AppContext, AsyncAppContext, BorrowAppContext, Context as _, EventEmitter, Hsla,
|
||||||
Model, ModelContext, SharedString, Task, WeakModel, WindowContext,
|
Model, ModelContext, SharedString, Task, WeakModel, WindowContext,
|
||||||
|
@ -95,9 +98,8 @@ pub use task_inventory::{
|
||||||
BasicContextProvider, ContextProviderWithTasks, Inventory, TaskSourceKind,
|
BasicContextProvider, ContextProviderWithTasks, Inventory, TaskSourceKind,
|
||||||
};
|
};
|
||||||
pub use worktree::{
|
pub use worktree::{
|
||||||
Entry, EntryKind, File, LocalWorktree, PathChange, ProjectEntryId, RepositoryEntry,
|
Entry, EntryKind, File, LocalWorktree, PathChange, ProjectEntryId, UpdatedEntriesSet,
|
||||||
UpdatedEntriesSet, UpdatedGitRepositoriesSet, Worktree, WorktreeId, WorktreeSettings,
|
UpdatedGitRepositoriesSet, Worktree, WorktreeId, WorktreeSettings, FS_WATCH_LATENCY,
|
||||||
FS_WATCH_LATENCY,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use buffer_store::ProjectTransaction;
|
pub use buffer_store::ProjectTransaction;
|
||||||
|
@ -242,6 +244,7 @@ pub enum Event {
|
||||||
ActivateProjectPanel,
|
ActivateProjectPanel,
|
||||||
WorktreeAdded(WorktreeId),
|
WorktreeAdded(WorktreeId),
|
||||||
WorktreeOrderChanged,
|
WorktreeOrderChanged,
|
||||||
|
GitRepositoryUpdated,
|
||||||
WorktreeRemoved(WorktreeId),
|
WorktreeRemoved(WorktreeId),
|
||||||
WorktreeUpdatedEntries(WorktreeId, UpdatedEntriesSet),
|
WorktreeUpdatedEntries(WorktreeId, UpdatedEntriesSet),
|
||||||
WorktreeUpdatedGitRepositories(WorktreeId),
|
WorktreeUpdatedGitRepositories(WorktreeId),
|
||||||
|
@ -1433,6 +1436,15 @@ impl Project {
|
||||||
.unwrap_or(false)
|
.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> {
|
pub fn visibility_for_paths(&self, paths: &[PathBuf], cx: &AppContext) -> Option<bool> {
|
||||||
paths
|
paths
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -2295,6 +2307,7 @@ impl Project {
|
||||||
}
|
}
|
||||||
WorktreeStoreEvent::WorktreeOrderChanged => cx.emit(Event::WorktreeOrderChanged),
|
WorktreeStoreEvent::WorktreeOrderChanged => cx.emit(Event::WorktreeOrderChanged),
|
||||||
WorktreeStoreEvent::WorktreeUpdateSent(_) => {}
|
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>> {
|
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 worktree = self.visible_worktrees(cx).next()?.read(cx).as_local()?;
|
||||||
let root_entry = worktree.root_git_entry()?;
|
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| {
|
entries.sort_by(|entry_a, entry_b| {
|
||||||
|
let entry_a = entry_a.as_ref();
|
||||||
|
let entry_b = entry_b.as_ref();
|
||||||
compare_paths(
|
compare_paths(
|
||||||
(&entry_a.path, entry_a.is_file()),
|
(&entry_a.path, entry_a.is_file()),
|
||||||
(&entry_b.path, entry_b.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.
|
/// 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,
|
/// 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.
|
/// 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(
|
pub fn used_and_current_resolved_tasks(
|
||||||
&self,
|
&self,
|
||||||
worktree: Option<WorktreeId>,
|
worktree: Option<WorktreeId>,
|
||||||
|
|
|
@ -62,6 +62,7 @@ pub enum WorktreeStoreEvent {
|
||||||
WorktreeReleased(EntityId, WorktreeId),
|
WorktreeReleased(EntityId, WorktreeId),
|
||||||
WorktreeOrderChanged,
|
WorktreeOrderChanged,
|
||||||
WorktreeUpdateSent(Model<Worktree>),
|
WorktreeUpdateSent(Model<Worktree>),
|
||||||
|
GitRepositoryUpdated,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventEmitter<WorktreeStoreEvent> for WorktreeStore {}
|
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::local(path.clone(), visible, fs, next_entry_id, &mut cx).await;
|
||||||
|
|
||||||
let worktree = worktree?;
|
let worktree = worktree?;
|
||||||
|
|
||||||
this.update(&mut cx, |this, cx| this.add(&worktree, cx))?;
|
this.update(&mut cx, |this, cx| this.add(&worktree, cx))?;
|
||||||
|
|
||||||
if visible {
|
if visible {
|
||||||
|
@ -374,6 +376,17 @@ impl WorktreeStore {
|
||||||
this.send_project_updates(cx);
|
this.send_project_updates(cx);
|
||||||
})
|
})
|
||||||
.detach();
|
.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>) {
|
pub fn remove_worktree(&mut self, id_to_remove: WorktreeId, cx: &mut ModelContext<Self>) {
|
||||||
|
@ -583,11 +596,11 @@ impl WorktreeStore {
|
||||||
pub fn shared(
|
pub fn shared(
|
||||||
&mut self,
|
&mut self,
|
||||||
remote_id: u64,
|
remote_id: u64,
|
||||||
downsteam_client: AnyProtoClient,
|
downstream_client: AnyProtoClient,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) {
|
) {
|
||||||
self.retain_worktrees = true;
|
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
|
// When shared, retain all worktrees
|
||||||
for worktree_handle in self.worktrees.iter_mut() {
|
for worktree_handle in self.worktrees.iter_mut() {
|
||||||
|
|
|
@ -63,7 +63,7 @@ use workspace::{
|
||||||
notifications::{DetachAndPromptErr, NotifyTaskExt},
|
notifications::{DetachAndPromptErr, NotifyTaskExt},
|
||||||
DraggedSelection, OpenInTerminal, PreviewTabsSettings, SelectedEntry, Workspace,
|
DraggedSelection, OpenInTerminal, PreviewTabsSettings, SelectedEntry, Workspace,
|
||||||
};
|
};
|
||||||
use worktree::CreatedEntry;
|
use worktree::{CreatedEntry, GitEntry, GitEntryRef};
|
||||||
|
|
||||||
const PROJECT_PANEL_KEY: &str = "ProjectPanel";
|
const PROJECT_PANEL_KEY: &str = "ProjectPanel";
|
||||||
const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX;
|
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
|
// 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.
|
// hovered over the start/end of a list.
|
||||||
hover_scroll_task: Option<Task<()>>,
|
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.
|
/// 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
|
/// 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).
|
/// project entries (and all non-leaf nodes are guaranteed to be directories).
|
||||||
|
@ -311,7 +311,8 @@ impl ProjectPanel {
|
||||||
this.update_visible_entries(None, cx);
|
this.update_visible_entries(None, cx);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
project::Event::WorktreeUpdatedEntries(_, _)
|
project::Event::GitRepositoryUpdated
|
||||||
|
| project::Event::WorktreeUpdatedEntries(_, _)
|
||||||
| project::Event::WorktreeAdded(_)
|
| project::Event::WorktreeAdded(_)
|
||||||
| project::Event::WorktreeOrderChanged => {
|
| project::Event::WorktreeOrderChanged => {
|
||||||
this.update_visible_entries(None, cx);
|
this.update_visible_entries(None, cx);
|
||||||
|
@ -1366,9 +1367,10 @@ impl ProjectPanel {
|
||||||
let parent_entry = worktree.entry_for_path(parent_path)?;
|
let parent_entry = worktree.entry_for_path(parent_path)?;
|
||||||
|
|
||||||
// Remove all siblings that are being deleted except the last marked entry
|
// Remove all siblings that are being deleted except the last marked entry
|
||||||
let mut siblings: Vec<Entry> = worktree
|
let mut siblings: Vec<_> = worktree
|
||||||
.snapshot()
|
.snapshot()
|
||||||
.child_entries(parent_path)
|
.child_entries(parent_path)
|
||||||
|
.with_git_statuses()
|
||||||
.filter(|sibling| {
|
.filter(|sibling| {
|
||||||
sibling.id == latest_entry.id
|
sibling.id == latest_entry.id
|
||||||
|| !marked_entries_in_worktree.contains(&&SelectedEntry {
|
|| !marked_entries_in_worktree.contains(&&SelectedEntry {
|
||||||
|
@ -1376,7 +1378,7 @@ impl ProjectPanel {
|
||||||
entry_id: sibling.id,
|
entry_id: sibling.id,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.cloned()
|
.map(|entry| entry.to_owned())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
project::sort_worktree_entries(&mut siblings);
|
project::sort_worktree_entries(&mut siblings);
|
||||||
|
@ -2334,7 +2336,7 @@ impl ProjectPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut visible_worktree_entries = Vec::new();
|
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![];
|
let mut auto_folded_ancestors = vec![];
|
||||||
while let Some(entry) = entry_iter.entry() {
|
while let Some(entry) = entry_iter.entry() {
|
||||||
if auto_collapse_dirs && entry.kind.is_dir() {
|
if auto_collapse_dirs && entry.kind.is_dir() {
|
||||||
|
@ -2376,7 +2378,7 @@ impl ProjectPanel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
auto_folded_ancestors.clear();
|
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 {
|
let precedes_new_entry = if let Some(new_entry_id) = new_entry_parent_id {
|
||||||
entry.id == new_entry_id || {
|
entry.id == new_entry_id || {
|
||||||
self.ancestors.get(&entry.id).map_or(false, |entries| {
|
self.ancestors.get(&entry.id).map_or(false, |entries| {
|
||||||
|
@ -2390,25 +2392,27 @@ impl ProjectPanel {
|
||||||
false
|
false
|
||||||
};
|
};
|
||||||
if precedes_new_entry {
|
if precedes_new_entry {
|
||||||
visible_worktree_entries.push(Entry {
|
visible_worktree_entries.push(GitEntry {
|
||||||
id: NEW_ENTRY_ID,
|
entry: Entry {
|
||||||
kind: new_entry_kind,
|
id: NEW_ENTRY_ID,
|
||||||
path: entry.path.join("\0").into(),
|
kind: new_entry_kind,
|
||||||
inode: 0,
|
path: entry.path.join("\0").into(),
|
||||||
mtime: entry.mtime,
|
inode: 0,
|
||||||
size: entry.size,
|
mtime: entry.mtime,
|
||||||
is_ignored: entry.is_ignored,
|
size: entry.size,
|
||||||
is_external: false,
|
is_ignored: entry.is_ignored,
|
||||||
is_private: false,
|
is_external: false,
|
||||||
is_always_included: entry.is_always_included,
|
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,
|
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 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
|
let Some(path_name) = worktree_abs_path
|
||||||
.file_name()
|
.file_name()
|
||||||
.with_context(|| {
|
.with_context(|| {
|
||||||
|
@ -2485,8 +2489,8 @@ impl ProjectPanel {
|
||||||
entry_iter.advance();
|
entry_iter.advance();
|
||||||
}
|
}
|
||||||
|
|
||||||
snapshot.propagate_git_statuses(&mut visible_worktree_entries);
|
|
||||||
project::sort_worktree_entries(&mut visible_worktree_entries);
|
project::sort_worktree_entries(&mut visible_worktree_entries);
|
||||||
|
|
||||||
self.visible_entries
|
self.visible_entries
|
||||||
.push((worktree_id, visible_worktree_entries, OnceCell::new()));
|
.push((worktree_id, visible_worktree_entries, OnceCell::new()));
|
||||||
}
|
}
|
||||||
|
@ -2714,13 +2718,13 @@ impl ProjectPanel {
|
||||||
None
|
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;
|
let mut offset = 0;
|
||||||
for (worktree_id, visible_worktree_entries, _) in &self.visible_entries {
|
for (worktree_id, visible_worktree_entries, _) in &self.visible_entries {
|
||||||
if visible_worktree_entries.len() > offset + index {
|
if visible_worktree_entries.len() > offset + index {
|
||||||
return visible_worktree_entries
|
return visible_worktree_entries
|
||||||
.get(index)
|
.get(index)
|
||||||
.map(|entry| (*worktree_id, entry));
|
.map(|entry| (*worktree_id, entry.to_ref()));
|
||||||
}
|
}
|
||||||
offset += visible_worktree_entries.len();
|
offset += visible_worktree_entries.len();
|
||||||
}
|
}
|
||||||
|
@ -2753,7 +2757,7 @@ impl ProjectPanel {
|
||||||
.collect()
|
.collect()
|
||||||
});
|
});
|
||||||
for entry in visible_worktree_entries[entry_range].iter() {
|
for entry in visible_worktree_entries[entry_range].iter() {
|
||||||
callback(entry, entries, cx);
|
callback(&entry, entries, cx);
|
||||||
}
|
}
|
||||||
ix = end_ix;
|
ix = end_ix;
|
||||||
}
|
}
|
||||||
|
@ -2822,7 +2826,7 @@ impl ProjectPanel {
|
||||||
};
|
};
|
||||||
|
|
||||||
let (depth, difference) =
|
let (depth, difference) =
|
||||||
ProjectPanel::calculate_depth_and_difference(entry, entries);
|
ProjectPanel::calculate_depth_and_difference(&entry, entries);
|
||||||
|
|
||||||
let filename = match difference {
|
let filename = match difference {
|
||||||
diff if diff > 1 => entry
|
diff if diff > 1 => entry
|
||||||
|
@ -2951,9 +2955,9 @@ impl ProjectPanel {
|
||||||
worktree_id: WorktreeId,
|
worktree_id: WorktreeId,
|
||||||
reverse_search: bool,
|
reverse_search: bool,
|
||||||
only_visible_entries: bool,
|
only_visible_entries: bool,
|
||||||
predicate: impl Fn(&Entry, WorktreeId) -> bool,
|
predicate: impl Fn(GitEntryRef, WorktreeId) -> bool,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> Option<Entry> {
|
) -> Option<GitEntry> {
|
||||||
if only_visible_entries {
|
if only_visible_entries {
|
||||||
let entries = self
|
let entries = self
|
||||||
.visible_entries
|
.visible_entries
|
||||||
|
@ -2968,15 +2972,18 @@ impl ProjectPanel {
|
||||||
.clone();
|
.clone();
|
||||||
|
|
||||||
return utils::ReversibleIterable::new(entries.iter(), reverse_search)
|
return utils::ReversibleIterable::new(entries.iter(), reverse_search)
|
||||||
.find(|ele| predicate(ele, worktree_id))
|
.find(|ele| predicate(ele.to_ref(), worktree_id))
|
||||||
.cloned();
|
.cloned();
|
||||||
}
|
}
|
||||||
|
|
||||||
let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
|
let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
|
||||||
worktree.update(cx, |tree, _| {
|
worktree.update(cx, |tree, _| {
|
||||||
utils::ReversibleIterable::new(tree.entries(true, 0usize), reverse_search)
|
utils::ReversibleIterable::new(
|
||||||
.find_single_ended(|ele| predicate(ele, worktree_id))
|
tree.entries(true, 0usize).with_git_statuses(),
|
||||||
.cloned()
|
reverse_search,
|
||||||
|
)
|
||||||
|
.find_single_ended(|ele| predicate(*ele, worktree_id))
|
||||||
|
.map(|ele| ele.to_owned())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2984,7 +2991,7 @@ impl ProjectPanel {
|
||||||
&self,
|
&self,
|
||||||
start: Option<&SelectedEntry>,
|
start: Option<&SelectedEntry>,
|
||||||
reverse_search: bool,
|
reverse_search: bool,
|
||||||
predicate: impl Fn(&Entry, WorktreeId) -> bool,
|
predicate: impl Fn(GitEntryRef, WorktreeId) -> bool,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> Option<SelectedEntry> {
|
) -> Option<SelectedEntry> {
|
||||||
let mut worktree_ids: Vec<_> = self
|
let mut worktree_ids: Vec<_> = self
|
||||||
|
@ -3006,7 +3013,9 @@ impl ProjectPanel {
|
||||||
let root_entry = tree.root_entry()?;
|
let root_entry = tree.root_entry()?;
|
||||||
let tree_id = tree.id();
|
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 {
|
if reverse_search {
|
||||||
first_iter.next();
|
first_iter.next();
|
||||||
|
@ -3014,25 +3023,25 @@ impl ProjectPanel {
|
||||||
|
|
||||||
let first = first_iter
|
let first = first_iter
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.take_until(|(count, ele)| *ele == root_entry && *count != 0usize)
|
.take_until(|(count, entry)| entry.entry == root_entry && *count != 0usize)
|
||||||
.map(|(_, ele)| ele)
|
.map(|(_, entry)| entry)
|
||||||
.find(|ele| predicate(ele, tree_id))
|
.find(|ele| predicate(*ele, tree_id))
|
||||||
.cloned();
|
.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 {
|
let second = if reverse_search {
|
||||||
second_iter
|
second_iter
|
||||||
.take_until(|ele| ele.id == start.entry_id)
|
.take_until(|ele| ele.id == start.entry_id)
|
||||||
.filter(|ele| predicate(ele, tree_id))
|
.filter(|ele| predicate(*ele, tree_id))
|
||||||
.last()
|
.last()
|
||||||
.cloned()
|
.map(|ele| ele.to_owned())
|
||||||
} else {
|
} else {
|
||||||
second_iter
|
second_iter
|
||||||
.take_while(|ele| ele.id != start.entry_id)
|
.take_while(|ele| ele.id != start.entry_id)
|
||||||
.filter(|ele| predicate(ele, tree_id))
|
.filter(|ele| predicate(*ele, tree_id))
|
||||||
.last()
|
.last()
|
||||||
.cloned()
|
.map(|ele| ele.to_owned())
|
||||||
};
|
};
|
||||||
|
|
||||||
if reverse_search {
|
if reverse_search {
|
||||||
|
@ -3089,7 +3098,7 @@ impl ProjectPanel {
|
||||||
&self,
|
&self,
|
||||||
start: Option<&SelectedEntry>,
|
start: Option<&SelectedEntry>,
|
||||||
reverse_search: bool,
|
reverse_search: bool,
|
||||||
predicate: impl Fn(&Entry, WorktreeId) -> bool,
|
predicate: impl Fn(GitEntryRef, WorktreeId) -> bool,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> Option<SelectedEntry> {
|
) -> Option<SelectedEntry> {
|
||||||
let mut worktree_ids: Vec<_> = self
|
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 first_search = first_iter.find(|ele| predicate(ele.to_ref(), start.worktree_id));
|
||||||
let second_search = second_iter.find(|ele| predicate(ele, start.worktree_id));
|
let second_search = second_iter.find(|ele| predicate(ele.to_ref(), start.worktree_id));
|
||||||
|
|
||||||
if first_search.is_some() {
|
if first_search.is_some() {
|
||||||
return first_search.map(|entry| SelectedEntry {
|
return first_search.map(|entry| SelectedEntry {
|
||||||
|
|
|
@ -1768,7 +1768,7 @@ message Entry {
|
||||||
bool is_ignored = 7;
|
bool is_ignored = 7;
|
||||||
bool is_external = 8;
|
bool is_external = 8;
|
||||||
reserved 6;
|
reserved 6;
|
||||||
optional GitStatus git_status = 9;
|
reserved 9;
|
||||||
bool is_fifo = 10;
|
bool is_fifo = 10;
|
||||||
optional uint64 size = 11;
|
optional uint64 size = 11;
|
||||||
optional string canonical_path = 12;
|
optional string canonical_path = 12;
|
||||||
|
@ -1777,6 +1777,8 @@ message Entry {
|
||||||
message RepositoryEntry {
|
message RepositoryEntry {
|
||||||
uint64 work_directory_id = 1;
|
uint64 work_directory_id = 1;
|
||||||
optional string branch = 2;
|
optional string branch = 2;
|
||||||
|
repeated StatusEntry updated_statuses = 3;
|
||||||
|
repeated string removed_statuses = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message StatusEntry {
|
message StatusEntry {
|
||||||
|
@ -1788,6 +1790,7 @@ enum GitStatus {
|
||||||
Added = 0;
|
Added = 0;
|
||||||
Modified = 1;
|
Modified = 1;
|
||||||
Conflict = 2;
|
Conflict = 2;
|
||||||
|
Deleted = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message BufferState {
|
message BufferState {
|
||||||
|
|
|
@ -20,6 +20,7 @@ use serde_json::json;
|
||||||
use settings::{initial_server_settings_content, Settings, SettingsLocation, SettingsStore};
|
use settings::{initial_server_settings_content, Settings, SettingsLocation, SettingsStore};
|
||||||
use smol::stream::StreamExt;
|
use smol::stream::StreamExt;
|
||||||
use std::{
|
use std::{
|
||||||
|
collections::HashSet,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::Arc,
|
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 (project, headless_project) = init_test(&fs, cx, server_cx).await;
|
||||||
let branches = ["main", "dev", "feature-1"];
|
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);
|
fs.insert_branches(Path::new("/code/project1/.git"), &branches);
|
||||||
|
|
||||||
let (worktree, _) = project
|
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
|
let remote_branches = remote_branches
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|branch| branch.name)
|
.map(|branch| branch.name.to_string())
|
||||||
.collect::<Vec<_>>();
|
.collect::<HashSet<_>>();
|
||||||
|
|
||||||
assert_eq!(&remote_branches, &branches);
|
assert_eq!(&remote_branches, &branches_set);
|
||||||
|
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
project.update(cx, |project, cx| {
|
project.update(cx, |project, cx| {
|
||||||
|
|
|
@ -39,7 +39,7 @@ pub async fn launch_remote_kernel(
|
||||||
let kernel_launch_request = KernelLaunchRequest {
|
let kernel_launch_request = KernelLaunchRequest {
|
||||||
name: kernel_name.to_string(),
|
name: kernel_name.to_string(),
|
||||||
// Note: since the path we have locally may not be the same as the one on the remote server,
|
// 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,
|
path: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ use collections::HashMap;
|
||||||
// for those users.
|
// for those users.
|
||||||
//
|
//
|
||||||
// The way macOS solves this problem is to move shortcuts around so that they are all reachable,
|
// 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.
|
// 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
|
// 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,
|
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)]
|
#[derive(Clone)]
|
||||||
pub struct Cursor<'a, T: Item, D> {
|
pub struct Cursor<'a, T: Item, D> {
|
||||||
tree: &'a SumTree<T>,
|
tree: &'a SumTree<T>,
|
||||||
|
@ -18,6 +27,21 @@ pub struct Cursor<'a, T: Item, D> {
|
||||||
at_end: bool,
|
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> {
|
pub struct Iter<'a, T: Item> {
|
||||||
tree: &'a SumTree<T>,
|
tree: &'a SumTree<T>,
|
||||||
stack: ArrayVec<StackEntry<'a, T, ()>, 16>,
|
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]
|
#[track_caller]
|
||||||
pub fn item(&self) -> Option<&'a T> {
|
pub fn item(&self) -> Option<&'a T> {
|
||||||
self.assert_did_seek();
|
self.assert_did_seek();
|
||||||
|
|
|
@ -42,6 +42,21 @@ pub trait Summary: Clone {
|
||||||
fn add_summary(&mut self, summary: &Self, cx: &Self::Context);
|
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.
|
/// 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`]
|
/// You can use dimensions to seek to a specific location in the [`SumTree`]
|
||||||
|
@ -761,6 +776,55 @@ impl<T: KeyedItem> SumTree<T> {
|
||||||
None
|
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>
|
impl<T, S> Default for SumTree<T>
|
||||||
|
|
|
@ -358,13 +358,14 @@ impl PickerDelegate for TabSwitcherDelegate {
|
||||||
.item
|
.item
|
||||||
.project_path(cx)
|
.project_path(cx)
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|path| self.project.read(cx).entry_for_path(path, cx))
|
.and_then(|path| {
|
||||||
.map(|entry| {
|
let project = self.project.read(cx);
|
||||||
entry_git_aware_label_color(
|
let entry = project.entry_for_path(path, cx)?;
|
||||||
entry.git_status,
|
let git_status = project.project_path_git_status(path, cx);
|
||||||
entry.is_ignored,
|
Some((entry, git_status))
|
||||||
selected,
|
})
|
||||||
)
|
.map(|(entry, git_status)| {
|
||||||
|
entry_git_aware_label_color(git_status, entry.is_ignored, selected)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.flatten();
|
.flatten();
|
||||||
|
|
|
@ -60,7 +60,7 @@ pub struct SpawnInTerminal {
|
||||||
pub show_command: bool,
|
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)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct ResolvedTask {
|
pub struct ResolvedTask {
|
||||||
/// A way to distinguish tasks produced by the same template, but different contexts.
|
/// 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 truncated_variables = truncate_variables(&task_variables);
|
||||||
let cwd = match self.cwd.as_deref() {
|
let cwd = match self.cwd.as_deref() {
|
||||||
Some(cwd) => {
|
Some(cwd) => {
|
||||||
let substitured_cwd = substitute_all_template_variables_in_str(
|
let substituted_cwd = substitute_all_template_variables_in_str(
|
||||||
cwd,
|
cwd,
|
||||||
&task_variables,
|
&task_variables,
|
||||||
&variable_names,
|
&variable_names,
|
||||||
&mut substituted_variables,
|
&mut substituted_variables,
|
||||||
)?;
|
)?;
|
||||||
Some(PathBuf::from(substitured_cwd))
|
Some(PathBuf::from(substituted_cwd))
|
||||||
}
|
}
|
||||||
None => None,
|
None => None,
|
||||||
}
|
}
|
||||||
|
|
|
@ -100,7 +100,7 @@ impl TerminalPanel {
|
||||||
terminal_panel
|
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;
|
self.assistant_enabled = enabled;
|
||||||
if enabled {
|
if enabled {
|
||||||
let focus_handle = self
|
let focus_handle = self
|
||||||
|
|
|
@ -270,7 +270,7 @@ pub struct ThemeColorsContent {
|
||||||
|
|
||||||
/// Fill Color. Used for the muted or deemphasized fill color of an icon.
|
/// 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")]
|
#[serde(rename = "icon.muted")]
|
||||||
pub icon_muted: Option<String>,
|
pub icon_muted: Option<String>,
|
||||||
|
|
||||||
|
|
|
@ -97,7 +97,7 @@ pub struct ThemeColors {
|
||||||
pub icon: Hsla,
|
pub icon: Hsla,
|
||||||
/// Fill Color. Used for the muted or deemphasized fill color of an icon.
|
/// 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,
|
pub icon_muted: Hsla,
|
||||||
/// Fill Color. Used for the disabled fill color of an icon.
|
/// 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 {
|
pub enum LoadThemes {
|
||||||
/// Only load the base theme.
|
/// Only load the base theme.
|
||||||
///
|
///
|
||||||
|
|
|
@ -21,7 +21,7 @@ use gpui::{
|
||||||
Interactivity, IntoElement, Model, MouseButton, ParentElement, Render, Stateful,
|
Interactivity, IntoElement, Model, MouseButton, ParentElement, Render, Stateful,
|
||||||
StatefulInteractiveElement, Styled, Subscription, View, ViewContext, VisualContext, WeakView,
|
StatefulInteractiveElement, Styled, Subscription, View, ViewContext, VisualContext, WeakView,
|
||||||
};
|
};
|
||||||
use project::{Project, RepositoryEntry};
|
use project::Project;
|
||||||
use rpc::proto;
|
use rpc::proto;
|
||||||
use settings::Settings as _;
|
use settings::Settings as _;
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
|
@ -487,7 +487,7 @@ impl TitleBar {
|
||||||
let workspace = self.workspace.upgrade()?;
|
let workspace = self.workspace.upgrade()?;
|
||||||
let branch_name = entry
|
let branch_name = entry
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(RepositoryEntry::branch)
|
.and_then(|entry| entry.branch())
|
||||||
.map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH))?;
|
.map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH))?;
|
||||||
Some(
|
Some(
|
||||||
Button::new("project_branch_trigger", branch_name)
|
Button::new("project_branch_trigger", branch_name)
|
||||||
|
|
|
@ -179,7 +179,7 @@ define_connection! {
|
||||||
// group_id: usize, // Primary key for pane_groups
|
// group_id: usize, // Primary key for pane_groups
|
||||||
// workspace_id: usize, // References workspaces table
|
// workspace_id: usize, // References workspaces table
|
||||||
// parent_group_id: Option<usize>, // None indicates that this is the root node
|
// 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'
|
// axis: Option<Axis>, // 'Vertical', 'Horizontal'
|
||||||
// flexes: Option<Vec<f32>>, // A JSON array of floats
|
// 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();
|
cx.executor().run_until_parked();
|
||||||
|
|
||||||
let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
|
let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
|
||||||
check_propagated_statuses(
|
|
||||||
|
check_git_statuses(
|
||||||
&snapshot,
|
&snapshot,
|
||||||
&[
|
&[
|
||||||
(Path::new(""), Some(GitFileStatus::Modified)),
|
(Path::new(""), Some(GitFileStatus::Modified)),
|
||||||
|
@ -2178,15 +2179,15 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) {
|
||||||
|
|
||||||
cx.read(|cx| {
|
cx.read(|cx| {
|
||||||
let tree = tree.read(cx);
|
let tree = tree.read(cx);
|
||||||
let (work_dir, _) = tree.repositories().next().unwrap();
|
let repo = tree.repositories().next().unwrap();
|
||||||
assert_eq!(work_dir.as_ref(), Path::new("projects/project1"));
|
assert_eq!(repo.path.as_ref(), Path::new("projects/project1"));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tree.status_for_file(Path::new("projects/project1/a")),
|
tree.status_for_file(Path::new("projects/project1/a")),
|
||||||
Some(GitFileStatus::Modified)
|
Some(GitFileStatus::Modified)
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tree.status_for_file(Path::new("projects/project1/b")),
|
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| {
|
cx.read(|cx| {
|
||||||
let tree = tree.read(cx);
|
let tree = tree.read(cx);
|
||||||
let (work_dir, _) = tree.repositories().next().unwrap();
|
let repo = tree.repositories().next().unwrap();
|
||||||
assert_eq!(work_dir.as_ref(), Path::new("projects/project2"));
|
assert_eq!(repo.path.as_ref(), Path::new("projects/project2"));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tree.status_for_file(Path::new("projects/project2/a")),
|
tree.status_for_file(Path::new("projects/project2/a")),
|
||||||
Some(GitFileStatus::Modified)
|
Some(GitFileStatus::Modified)
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tree.status_for_file(Path::new("projects/project2/b")),
|
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());
|
assert!(tree.repository_for_path("c.txt".as_ref()).is_none());
|
||||||
|
|
||||||
let entry = tree.repository_for_path("dir1/src/b.txt".as_ref()).unwrap();
|
let repo = tree.repository_for_path("dir1/src/b.txt".as_ref()).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(repo.path.as_ref(), Path::new("dir1"));
|
||||||
entry
|
|
||||||
.work_directory(tree)
|
|
||||||
.map(|directory| directory.as_ref().to_owned()),
|
|
||||||
Some(Path::new("dir1").to_owned())
|
|
||||||
);
|
|
||||||
|
|
||||||
let entry = tree
|
let repo = tree
|
||||||
.repository_for_path("dir1/deps/dep1/src/a.txt".as_ref())
|
.repository_for_path("dir1/deps/dep1/src/a.txt".as_ref())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(repo.path.as_ref(), Path::new("dir1/deps/dep1"));
|
||||||
entry
|
|
||||||
.work_directory(tree)
|
|
||||||
.map(|directory| directory.as_ref().to_owned()),
|
|
||||||
Some(Path::new("dir1/deps/dep1").to_owned())
|
|
||||||
);
|
|
||||||
|
|
||||||
let entries = tree.files(false, 0);
|
let entries = tree.files(false, 0);
|
||||||
|
|
||||||
|
@ -2278,10 +2269,7 @@ async fn test_git_repository_for_path(cx: &mut TestAppContext) {
|
||||||
.map(|(entry, repo)| {
|
.map(|(entry, repo)| {
|
||||||
(
|
(
|
||||||
entry.path.as_ref(),
|
entry.path.as_ref(),
|
||||||
repo.and_then(|repo| {
|
repo.map(|repo| repo.path.to_path_buf()),
|
||||||
repo.work_directory(tree)
|
|
||||||
.map(|work_directory| work_directory.0.to_path_buf())
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
@ -2334,7 +2322,7 @@ async fn test_git_repository_for_path(cx: &mut TestAppContext) {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_git_status(cx: &mut TestAppContext) {
|
async fn test_file_status(cx: &mut TestAppContext) {
|
||||||
init_test(cx);
|
init_test(cx);
|
||||||
cx.executor().allow_parking();
|
cx.executor().allow_parking();
|
||||||
const IGNORE_RULE: &str = "**/target";
|
const IGNORE_RULE: &str = "**/target";
|
||||||
|
@ -2393,17 +2381,17 @@ async fn test_git_status(cx: &mut TestAppContext) {
|
||||||
tree.read_with(cx, |tree, _cx| {
|
tree.read_with(cx, |tree, _cx| {
|
||||||
let snapshot = tree.snapshot();
|
let snapshot = tree.snapshot();
|
||||||
assert_eq!(snapshot.repositories().count(), 1);
|
assert_eq!(snapshot.repositories().count(), 1);
|
||||||
let (dir, repo_entry) = snapshot.repositories().next().unwrap();
|
let repo_entry = snapshot.repositories().next().unwrap();
|
||||||
assert_eq!(dir.as_ref(), Path::new("project"));
|
assert_eq!(repo_entry.path.as_ref(), Path::new("project"));
|
||||||
assert!(repo_entry.location_in_repo.is_none());
|
assert!(repo_entry.location_in_repo.is_none());
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
snapshot.status_for_file(project_path.join(B_TXT)),
|
snapshot.status_for_file(project_path.join(B_TXT)),
|
||||||
Some(GitFileStatus::Added)
|
Some(GitFileStatus::Untracked)
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
snapshot.status_for_file(project_path.join(F_TXT)),
|
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();
|
let snapshot = tree.snapshot();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
snapshot.status_for_file(project_path.join(F_TXT)),
|
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(B_TXT)), None);
|
||||||
assert_eq!(snapshot.status_for_file(project_path.join(A_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(A_TXT)), None);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
snapshot.status_for_file(project_path.join(B_TXT)),
|
snapshot.status_for_file(project_path.join(B_TXT)),
|
||||||
Some(GitFileStatus::Added)
|
Some(GitFileStatus::Untracked)
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
snapshot.status_for_file(project_path.join(E_TXT)),
|
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();
|
let snapshot = tree.snapshot();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
snapshot.status_for_file(project_path.join(renamed_dir_name).join(RENAMED_FILE)),
|
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(Path::new(renamed_dir_name))
|
||||||
.join(RENAMED_FILE)
|
.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]
|
#[gpui::test]
|
||||||
async fn test_repository_subfolder_git_status(cx: &mut TestAppContext) {
|
async fn test_repository_subfolder_git_status(cx: &mut TestAppContext) {
|
||||||
init_test(cx);
|
init_test(cx);
|
||||||
|
@ -2575,22 +2677,22 @@ async fn test_repository_subfolder_git_status(cx: &mut TestAppContext) {
|
||||||
tree.read_with(cx, |tree, _cx| {
|
tree.read_with(cx, |tree, _cx| {
|
||||||
let snapshot = tree.snapshot();
|
let snapshot = tree.snapshot();
|
||||||
assert_eq!(snapshot.repositories().count(), 1);
|
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
|
// Path is blank because the working directory of
|
||||||
// the git repository is located at the root of the project
|
// 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
|
// This is the missing path between the root of the project (sub-folder-2) and its
|
||||||
// location relative to the root of the repository.
|
// location relative to the root of the repository.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
repo_entry.location_in_repo,
|
repo.location_in_repo,
|
||||||
Some(Arc::from(Path::new("sub-folder-1/sub-folder-2")))
|
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("c.txt"), None);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
snapshot.status_for_file("d/e.txt"),
|
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]
|
#[gpui::test]
|
||||||
async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
|
async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
|
||||||
init_test(cx);
|
init_test(cx);
|
||||||
|
@ -2638,7 +2827,6 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
|
||||||
"h1.txt": "",
|
"h1.txt": "",
|
||||||
"h2.txt": ""
|
"h2.txt": ""
|
||||||
},
|
},
|
||||||
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
@ -2668,7 +2856,16 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
|
||||||
cx.executor().run_until_parked();
|
cx.executor().run_until_parked();
|
||||||
let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
|
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,
|
&snapshot,
|
||||||
&[
|
&[
|
||||||
(Path::new(""), Some(GitFileStatus::Conflict)),
|
(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,
|
&snapshot,
|
||||||
&[
|
&[
|
||||||
(Path::new("a/b"), Some(GitFileStatus::Added)),
|
(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,
|
&snapshot,
|
||||||
&[
|
&[
|
||||||
(Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
|
(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]
|
#[gpui::test]
|
||||||
async fn test_private_single_file_worktree(cx: &mut TestAppContext) {
|
async fn test_private_single_file_worktree(cx: &mut TestAppContext) {
|
||||||
init_test(cx);
|
init_test(cx);
|
||||||
|
@ -2736,22 +3173,20 @@ async fn test_private_single_file_worktree(cx: &mut TestAppContext) {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
fn check_propagated_statuses(
|
fn check_git_statuses(snapshot: &Snapshot, expected_statuses: &[(&Path, Option<GitFileStatus>)]) {
|
||||||
snapshot: &Snapshot,
|
let mut traversal = snapshot
|
||||||
expected_statuses: &[(&Path, Option<GitFileStatus>)],
|
.traverse_from_path(true, true, false, "".as_ref())
|
||||||
) {
|
.with_git_statuses();
|
||||||
let mut entries = expected_statuses
|
let found_statuses = expected_statuses
|
||||||
.iter()
|
.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<_>>();
|
.collect::<Vec<_>>();
|
||||||
snapshot.propagate_git_statuses(&mut entries);
|
assert_eq!(found_statuses, expected_statuses);
|
||||||
assert_eq!(
|
|
||||||
entries
|
|
||||||
.iter()
|
|
||||||
.map(|e| (e.path.as_ref(), e.git_status))
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
expected_statuses
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
|
@ -2763,14 +3198,14 @@ fn git_init(path: &Path) -> git2::Repository {
|
||||||
fn git_add<P: AsRef<Path>>(path: P, repo: &git2::Repository) {
|
fn git_add<P: AsRef<Path>>(path: P, repo: &git2::Repository) {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
let mut index = repo.index().expect("Failed to get index");
|
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");
|
index.write().expect("Failed to write index");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
fn git_remove_index(path: &Path, repo: &git2::Repository) {
|
fn git_remove_index(path: &Path, repo: &git2::Repository) {
|
||||||
let mut index = repo.index().expect("Failed to get index");
|
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");
|
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");
|
let entry = tree.entry_for_path(path).expect("entry {path} not found");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
entry.git_status, git_status,
|
tree.status_for_file(Path::new(path)),
|
||||||
|
git_status,
|
||||||
"expected {path} to have git status: {git_status:?}"
|
"expected {path} to have git status: {git_status:?}"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
|
@ -70,7 +70,7 @@ use util::load_shell_from_passwd;
|
||||||
#[global_allocator]
|
#[global_allocator]
|
||||||
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
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 message = "Zed failed to launch";
|
||||||
let error_details = errors
|
let error_details = errors
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
@ -179,7 +179,7 @@ fn main() {
|
||||||
|
|
||||||
let file_errors = init_paths();
|
let file_errors = init_paths();
|
||||||
if !file_errors.is_empty() {
|
if !file_errors.is_empty() {
|
||||||
files_not_createad_on_launch(file_errors);
|
files_not_created_on_launch(file_errors);
|
||||||
return;
|
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.
|
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`.
|
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.
|
# This must match the team in the provisioning profile.
|
||||||
IDENTITY="Zed Industries, Inc."
|
IDENTITY="Zed Industries, Inc."
|
||||||
APPLE_NOTORIZATION_TEAM="MQ55VZLNZQ"
|
APPLE_NOTARIZATION_TEAM="MQ55VZLNZQ"
|
||||||
|
|
||||||
# Function for displaying help info
|
# Function for displaying help info
|
||||||
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
|
/usr/bin/codesign --deep --force --timestamp --options runtime --sign "$IDENTITY" "$(pwd)/${dmg_file_path}" -v
|
||||||
|
|
||||||
echo "Notarizing DMG with Apple"
|
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)"
|
echo "Removing temporary DMG (used only for notarization)"
|
||||||
rm "${dmg_file_path}"
|
rm "${dmg_file_path}"
|
||||||
|
@ -344,7 +344,7 @@ function sign_app_binaries() {
|
||||||
if [[ $can_code_sign = true ]]; then
|
if [[ $can_code_sign = true ]]; then
|
||||||
echo "Notarizing DMG with Apple"
|
echo "Notarizing DMG with Apple"
|
||||||
/usr/bin/codesign --deep --force --timestamp --options runtime --sign "$IDENTITY" "$(pwd)/${dmg_file_path}" -v
|
/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}"
|
"${xcode_bin_dir_path}/stapler" staple "${dmg_file_path}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue