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:
Mikayla Maki 2025-01-03 17:00:16 -08:00 committed by GitHub
parent 72057e5716
commit 9613084f59
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
57 changed files with 2824 additions and 1254 deletions

4
Cargo.lock generated
View file

@ -2784,7 +2784,8 @@ dependencies = [
name = "collections"
version = "0.1.0"
dependencies = [
"rustc-hash 1.1.0",
"indexmap",
"rustc-hash 2.1.0",
]
[[package]]
@ -5193,6 +5194,7 @@ dependencies = [
"util",
"windows 0.58.0",
"workspace",
"worktree",
]
[[package]]

View file

@ -389,7 +389,7 @@ hyper = "0.14"
http = "1.1"
ignore = "0.4.22"
image = "0.25.1"
indexmap = { version = "2", features = ["serde"] }
indexmap = { version = "2.7.0", features = ["serde"] }
indoc = "2"
itertools = "0.13.0"
jsonwebtoken = "9.3"
@ -440,9 +440,10 @@ runtimelib = { version = "0.24.0", default-features = false, features = [
] }
rustc-demangle = "0.1.23"
rust-embed = { version = "8.4", features = ["include-exclude"] }
rustc-hash = "2.1.0"
rustls = "0.21.12"
rustls-native-certs = "0.8.0"
schemars = { version = "0.8", features = ["impl_json_schema"] }
schemars = { version = "0.8", features = ["impl_json_schema", "indexmap2"] }
semver = "1.0"
serde = { version = "1.0", features = ["derive", "rc"] }
serde_derive = { version = "1.0", features = ["deserialize_in_place"] }

View file

@ -122,7 +122,7 @@ pub fn init(cx: &mut AppContext) {
cx.observe_new_views(
|terminal_panel: &mut TerminalPanel, cx: &mut ViewContext<TerminalPanel>| {
let settings = AssistantSettings::get_global(cx);
terminal_panel.asssistant_enabled(settings.enabled, cx);
terminal_panel.set_assistant_enabled(settings.enabled, cx);
},
)
.detach();

View file

@ -133,7 +133,7 @@ impl InlineAssistant {
};
let enabled = AssistantSettings::get_global(cx).enabled;
terminal_panel.update(cx, |terminal_panel, cx| {
terminal_panel.asssistant_enabled(enabled, cx)
terminal_panel.set_assistant_enabled(enabled, cx)
});
})
.detach();

View file

@ -118,7 +118,7 @@ impl InlineAssistant {
};
let enabled = AssistantSettings::get_global(cx).enabled;
terminal_panel.update(cx, |terminal_panel, cx| {
terminal_panel.asssistant_enabled(enabled, cx)
terminal_panel.set_assistant_enabled(enabled, cx)
});
})
.detach();

View file

@ -79,7 +79,7 @@ fn parse_path_with_position(argument_str: &str) -> anyhow::Result<String> {
Ok(existing_path) => PathWithPosition::from_path(existing_path),
Err(_) => {
let path = PathWithPosition::parse_str(argument_str);
let curdir = env::current_dir().context("reteiving current directory")?;
let curdir = env::current_dir().context("retrieving current directory")?;
path.map_path(|path| match fs::canonicalize(&path) {
Ok(path) => Ok(path),
Err(e) => {

View file

@ -106,6 +106,22 @@ CREATE TABLE "worktree_repositories" (
CREATE INDEX "index_worktree_repositories_on_project_id" ON "worktree_repositories" ("project_id");
CREATE INDEX "index_worktree_repositories_on_project_id_and_worktree_id" ON "worktree_repositories" ("project_id", "worktree_id");
CREATE TABLE "worktree_repository_statuses" (
"project_id" INTEGER NOT NULL,
"worktree_id" INT8 NOT NULL,
"work_directory_id" INT8 NOT NULL,
"repo_path" VARCHAR NOT NULL,
"status" INT8 NOT NULL,
"scan_id" INT8 NOT NULL,
"is_deleted" BOOL NOT NULL,
PRIMARY KEY(project_id, worktree_id, work_directory_id, repo_path),
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE,
FOREIGN KEY(project_id, worktree_id, work_directory_id) REFERENCES worktree_entries (project_id, worktree_id, id) ON DELETE CASCADE
);
CREATE INDEX "index_wt_repos_statuses_on_project_id" ON "worktree_repository_statuses" ("project_id");
CREATE INDEX "index_wt_repos_statuses_on_project_id_and_wt_id" ON "worktree_repository_statuses" ("project_id", "worktree_id");
CREATE INDEX "index_wt_repos_statuses_on_project_id_and_wt_id_and_wd_id" ON "worktree_repository_statuses" ("project_id", "worktree_id", "work_directory_id");
CREATE TABLE "worktree_settings_files" (
"project_id" INTEGER NOT NULL,
"worktree_id" INTEGER NOT NULL,

View file

@ -1,4 +1,5 @@
use anyhow::Context as _;
use util::ResultExt;
use super::*;
@ -274,8 +275,8 @@ impl Database {
mtime_nanos: ActiveValue::set(mtime.nanos as i32),
canonical_path: ActiveValue::set(entry.canonical_path.clone()),
is_ignored: ActiveValue::set(entry.is_ignored),
git_status: ActiveValue::set(None),
is_external: ActiveValue::set(entry.is_external),
git_status: ActiveValue::set(entry.git_status.map(|status| status as i64)),
is_deleted: ActiveValue::set(false),
scan_id: ActiveValue::set(update.scan_id as i64),
is_fifo: ActiveValue::set(entry.is_fifo),
@ -295,7 +296,6 @@ impl Database {
worktree_entry::Column::MtimeNanos,
worktree_entry::Column::CanonicalPath,
worktree_entry::Column::IsIgnored,
worktree_entry::Column::GitStatus,
worktree_entry::Column::ScanId,
])
.to_owned(),
@ -349,6 +349,79 @@ impl Database {
)
.exec(&*tx)
.await?;
let has_any_statuses = update
.updated_repositories
.iter()
.any(|repository| !repository.updated_statuses.is_empty());
if has_any_statuses {
worktree_repository_statuses::Entity::insert_many(
update.updated_repositories.iter().flat_map(
|repository: &proto::RepositoryEntry| {
repository.updated_statuses.iter().map(|status_entry| {
worktree_repository_statuses::ActiveModel {
project_id: ActiveValue::set(project_id),
worktree_id: ActiveValue::set(worktree_id),
work_directory_id: ActiveValue::set(
repository.work_directory_id as i64,
),
scan_id: ActiveValue::set(update.scan_id as i64),
is_deleted: ActiveValue::set(false),
repo_path: ActiveValue::set(status_entry.repo_path.clone()),
status: ActiveValue::set(status_entry.status as i64),
}
})
},
),
)
.on_conflict(
OnConflict::columns([
worktree_repository_statuses::Column::ProjectId,
worktree_repository_statuses::Column::WorktreeId,
worktree_repository_statuses::Column::WorkDirectoryId,
worktree_repository_statuses::Column::RepoPath,
])
.update_columns([
worktree_repository_statuses::Column::ScanId,
worktree_repository_statuses::Column::Status,
])
.to_owned(),
)
.exec(&*tx)
.await?;
}
let has_any_removed_statuses = update
.updated_repositories
.iter()
.any(|repository| !repository.removed_statuses.is_empty());
if has_any_removed_statuses {
worktree_repository_statuses::Entity::update_many()
.filter(
worktree_repository_statuses::Column::ProjectId
.eq(project_id)
.and(
worktree_repository_statuses::Column::WorktreeId
.eq(worktree_id),
)
.and(
worktree_repository_statuses::Column::RepoPath.is_in(
update.updated_repositories.iter().flat_map(|repository| {
repository.removed_statuses.iter()
}),
),
),
)
.set(worktree_repository_statuses::ActiveModel {
is_deleted: ActiveValue::Set(true),
scan_id: ActiveValue::Set(update.scan_id as i64),
..Default::default()
})
.exec(&*tx)
.await?;
}
}
if !update.removed_repositories.is_empty() {
@ -643,7 +716,6 @@ impl Database {
canonical_path: db_entry.canonical_path,
is_ignored: db_entry.is_ignored,
is_external: db_entry.is_external,
git_status: db_entry.git_status.map(|status| status as i32),
// This is only used in the summarization backlog, so if it's None,
// that just means we won't be able to detect when to resummarize
// based on total number of backlogged bytes - instead, we'd go
@ -657,23 +729,49 @@ impl Database {
// Populate repository entries.
{
let mut db_repository_entries = worktree_repository::Entity::find()
let db_repository_entries = worktree_repository::Entity::find()
.filter(
Condition::all()
.add(worktree_repository::Column::ProjectId.eq(project.id))
.add(worktree_repository::Column::IsDeleted.eq(false)),
)
.stream(tx)
.all(tx)
.await?;
while let Some(db_repository_entry) = db_repository_entries.next().await {
let db_repository_entry = db_repository_entry?;
for db_repository_entry in db_repository_entries {
if let Some(worktree) = worktrees.get_mut(&(db_repository_entry.worktree_id as u64))
{
let mut repository_statuses = worktree_repository_statuses::Entity::find()
.filter(
Condition::all()
.add(worktree_repository_statuses::Column::ProjectId.eq(project.id))
.add(
worktree_repository_statuses::Column::WorktreeId
.eq(worktree.id),
)
.add(
worktree_repository_statuses::Column::WorkDirectoryId
.eq(db_repository_entry.work_directory_id),
)
.add(worktree_repository_statuses::Column::IsDeleted.eq(false)),
)
.stream(tx)
.await?;
let mut updated_statuses = Vec::new();
while let Some(status_entry) = repository_statuses.next().await {
let status_entry: worktree_repository_statuses::Model = status_entry?;
updated_statuses.push(proto::StatusEntry {
repo_path: status_entry.repo_path,
status: status_entry.status as i32,
});
}
worktree.repository_entries.insert(
db_repository_entry.work_directory_id as u64,
proto::RepositoryEntry {
work_directory_id: db_repository_entry.work_directory_id as u64,
branch: db_repository_entry.branch,
updated_statuses,
removed_statuses: Vec::new(),
},
);
}

View file

@ -662,7 +662,6 @@ impl Database {
canonical_path: db_entry.canonical_path,
is_ignored: db_entry.is_ignored,
is_external: db_entry.is_external,
git_status: db_entry.git_status.map(|status| status as i32),
// This is only used in the summarization backlog, so if it's None,
// that just means we won't be able to detect when to resummarize
// based on total number of backlogged bytes - instead, we'd go
@ -682,26 +681,69 @@ impl Database {
worktree_repository::Column::IsDeleted.eq(false)
};
let mut db_repositories = worktree_repository::Entity::find()
let db_repositories = worktree_repository::Entity::find()
.filter(
Condition::all()
.add(worktree_repository::Column::ProjectId.eq(project.id))
.add(worktree_repository::Column::WorktreeId.eq(worktree.id))
.add(repository_entry_filter),
)
.stream(tx)
.all(tx)
.await?;
while let Some(db_repository) = db_repositories.next().await {
let db_repository = db_repository?;
for db_repository in db_repositories.into_iter() {
if db_repository.is_deleted {
worktree
.removed_repositories
.push(db_repository.work_directory_id as u64);
} else {
let status_entry_filter = if let Some(rejoined_worktree) = rejoined_worktree
{
worktree_repository_statuses::Column::ScanId
.gt(rejoined_worktree.scan_id)
} else {
worktree_repository_statuses::Column::IsDeleted.eq(false)
};
let mut db_statuses = worktree_repository_statuses::Entity::find()
.filter(
Condition::all()
.add(
worktree_repository_statuses::Column::ProjectId
.eq(project.id),
)
.add(
worktree_repository_statuses::Column::WorktreeId
.eq(worktree.id),
)
.add(
worktree_repository_statuses::Column::WorkDirectoryId
.eq(db_repository.work_directory_id),
)
.add(status_entry_filter),
)
.stream(tx)
.await?;
let mut removed_statuses = Vec::new();
let mut updated_statuses = Vec::new();
while let Some(db_status) = db_statuses.next().await {
let db_status: worktree_repository_statuses::Model = db_status?;
if db_status.is_deleted {
removed_statuses.push(db_status.repo_path);
} else {
updated_statuses.push(proto::StatusEntry {
repo_path: db_status.repo_path,
status: db_status.status as i32,
});
}
}
worktree.updated_repositories.push(proto::RepositoryEntry {
work_directory_id: db_repository.work_directory_id as u64,
branch: db_repository.branch,
updated_statuses,
removed_statuses,
});
}
}

View file

@ -2925,8 +2925,6 @@ async fn test_git_status_sync(
assert_eq!(snapshot.status_for_file(file), status);
}
// Smoke test status reading
project_local.read_with(cx_a, |project, cx| {
assert_status(&Path::new(A_TXT), Some(GitFileStatus::Added), project, cx);
assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), project, cx);
@ -6669,6 +6667,10 @@ async fn test_remote_git_branches(
client_a
.fs()
.insert_branches(Path::new("/project/.git"), &branches);
let branches_set = branches
.into_iter()
.map(ToString::to_string)
.collect::<HashSet<_>>();
let (project_a, worktree_id) = client_a.build_local_project("/project", cx_a).await;
let project_id = active_call_a
@ -6690,10 +6692,10 @@ async fn test_remote_git_branches(
let branches_b = branches_b
.into_iter()
.map(|branch| branch.name)
.collect::<Vec<_>>();
.map(|branch| branch.name.to_string())
.collect::<HashSet<_>>();
assert_eq!(&branches_b, &branches);
assert_eq!(branches_b, branches_set);
cx_b.update(|cx| {
project_b.update(cx, |project, cx| {

View file

@ -229,6 +229,10 @@ async fn test_ssh_collaboration_git_branches(
.await;
let branches = ["main", "dev", "feature-1"];
let branches_set = branches
.iter()
.map(ToString::to_string)
.collect::<HashSet<_>>();
remote_fs.insert_branches(Path::new("/project/.git"), &branches);
// User A connects to the remote project via SSH.
@ -281,10 +285,10 @@ async fn test_ssh_collaboration_git_branches(
let branches_b = branches_b
.into_iter()
.map(|branch| branch.name)
.collect::<Vec<_>>();
.map(|branch| branch.name.to_string())
.collect::<HashSet<_>>();
assert_eq!(&branches_b, &branches);
assert_eq!(&branches_b, &branches_set);
cx_b.update(|cx| {
project_b.update(cx, |project, cx| {

View file

@ -16,4 +16,5 @@ doctest = false
test-support = []
[dependencies]
rustc-hash = "1.1"
indexmap.workspace = true
rustc-hash.workspace = true

View file

@ -4,12 +4,24 @@ pub type HashMap<K, V> = FxHashMap<K, V>;
#[cfg(feature = "test-support")]
pub type HashSet<T> = FxHashSet<T>;
#[cfg(feature = "test-support")]
pub type IndexMap<K, V> = indexmap::IndexMap<K, V, rustc_hash::FxBuildHasher>;
#[cfg(feature = "test-support")]
pub type IndexSet<T> = indexmap::IndexSet<T, rustc_hash::FxBuildHasher>;
#[cfg(not(feature = "test-support"))]
pub type HashMap<K, V> = std::collections::HashMap<K, V>;
#[cfg(not(feature = "test-support"))]
pub type HashSet<T> = std::collections::HashSet<T>;
#[cfg(not(feature = "test-support"))]
pub type IndexMap<K, V> = indexmap::IndexMap<K, V>;
#[cfg(not(feature = "test-support"))]
pub type IndexSet<T> = indexmap::IndexSet<T>;
pub use rustc_hash::FxHasher;
pub use rustc_hash::{FxHashMap, FxHashSet};
pub use std::collections::*;

View file

@ -2748,7 +2748,7 @@ mod tests {
.iter()
.filter(|(_, block)| matches!(block, Block::FoldedBuffer { .. }))
.count(),
"Should have one folded block, prodicing a header of the second buffer"
"Should have one folded block, producing a header of the second buffer"
);
assert_eq!(
blocks_snapshot.text(),
@ -2994,7 +2994,7 @@ mod tests {
}
})
.count(),
"Should have one folded block, prodicing a header of the second buffer"
"Should have one folded block, producing a header of the second buffer"
);
assert_eq!(blocks_snapshot.text(), "\n");
assert_eq!(

View file

@ -11780,7 +11780,7 @@ impl Editor {
}
/// Merges all anchor ranges for all context types ever set, picking the last highlight added in case of a row conflict.
/// Rerturns a map of display rows that are highlighted and their corresponding highlight color.
/// Returns a map of display rows that are highlighted and their corresponding highlight color.
/// Allows to ignore certain kinds of highlights.
pub fn highlighted_display_rows(
&mut self,
@ -12573,7 +12573,7 @@ impl Editor {
.file()
.is_none()
.then(|| {
// Handle file-less buffers separately: those are not really the project items, so won't have a paroject path or entity id,
// Handle file-less buffers separately: those are not really the project items, so won't have a project path or entity id,
// so `workspace.open_project_item` will never find them, always opening a new editor.
// Instead, we try to activate the existing editor in the pane first.
let (editor, pane_item_index) =

View file

@ -194,14 +194,24 @@ impl ProjectDiffEditor {
let open_tasks = project
.update(&mut cx, |project, cx| {
let worktree = project.worktree_for_id(id, cx)?;
let applicable_entries = worktree
.read(cx)
.entries(false, 0)
.filter(|entry| !entry.is_external)
.filter(|entry| entry.is_file())
.filter_map(|entry| Some((entry.git_status?, entry)))
.filter_map(|(git_status, entry)| {
Some((git_status, entry.id, project.path_for_entry(entry.id, cx)?))
let snapshot = worktree.read(cx).snapshot();
let applicable_entries = snapshot
.repositories()
.flat_map(|entry| {
entry.status().map(|git_entry| {
(git_entry.status, entry.join(git_entry.repo_path))
})
})
.filter_map(|(status, path)| {
let id = snapshot.entry_for_path(&path)?.id;
Some((
status,
id,
ProjectPath {
worktree_id: snapshot.id(),
path: path.into(),
},
))
})
.collect::<Vec<_>>();
Some(

View file

@ -615,9 +615,20 @@ impl Item for Editor {
.read(cx)
.as_singleton()
.and_then(|buffer| buffer.read(cx).project_path(cx))
.and_then(|path| self.project.as_ref()?.read(cx).entry_for_path(&path, cx))
.map(|entry| {
entry_git_aware_label_color(entry.git_status, entry.is_ignored, params.selected)
.and_then(|path| {
let project = self.project.as_ref()?.read(cx);
let entry = project.entry_for_path(&path, cx)?;
let git_status = project
.worktree_for_id(path.worktree_id, cx)?
.read(cx)
.snapshot()
.status_for_file(path.path);
Some(entry_git_aware_label_color(
git_status,
entry.is_ignored,
params.selected,
))
})
.unwrap_or_else(|| entry_label_color(params.selected))
} else {
@ -1559,10 +1570,10 @@ pub fn entry_git_aware_label_color(
Color::Ignored
} else {
match git_status {
Some(GitFileStatus::Added) => Color::Created,
Some(GitFileStatus::Added) | Some(GitFileStatus::Untracked) => Color::Created,
Some(GitFileStatus::Modified) => Color::Modified,
Some(GitFileStatus::Conflict) => Color::Conflict,
None => entry_label_color(selected),
Some(GitFileStatus::Deleted) | None => entry_label_color(selected),
}
}
}

View file

@ -257,7 +257,8 @@ impl EditorLspTestContext {
Self::new(language, Default::default(), cx).await
}
// Constructs lsp range using a marked string with '[', ']' range delimiters
/// Constructs lsp range using a marked string with '[', ']' range delimiters
#[track_caller]
pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {
let ranges = self.ranges(marked_text);
self.to_lsp_range(ranges[0].clone())

View file

@ -230,6 +230,7 @@ impl EditorTestContext {
self.cx.background_executor.run_until_parked();
}
#[track_caller]
pub fn ranges(&mut self, marked_text: &str) -> Vec<Range<usize>> {
let (unmarked_text, ranges) = marked_text_ranges(marked_text, false);
assert_eq!(self.buffer_text(), unmarked_text);

View file

@ -16,6 +16,7 @@ use std::sync::LazyLock;
pub use crate::hosting_provider::*;
pub use crate::remote::*;
pub use git2 as libgit;
pub use repository::WORK_DIRECTORY_REPO_PATH;
pub static DOT_GIT: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new(".git"));
pub static COOKIES: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new("cookies"));

View file

@ -7,6 +7,8 @@ use gpui::SharedString;
use parking_lot::Mutex;
use rope::Rope;
use serde::{Deserialize, Serialize};
use std::borrow::Borrow;
use std::sync::LazyLock;
use std::{
cmp::Ordering,
path::{Component, Path, PathBuf},
@ -37,7 +39,8 @@ pub trait GitRepository: Send + Sync {
/// Returns the SHA of the current HEAD.
fn head_sha(&self) -> Option<String>;
fn status(&self, path_prefixes: &[PathBuf]) -> Result<GitStatus>;
/// Returns the list of git statuses, sorted by path
fn status(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus>;
fn branches(&self) -> Result<Vec<Branch>>;
fn change_branch(&self, _: &str) -> Result<()>;
@ -132,7 +135,7 @@ impl GitRepository for RealGitRepository {
Some(self.repository.lock().head().ok()?.target()?.to_string())
}
fn status(&self, path_prefixes: &[PathBuf]) -> Result<GitStatus> {
fn status(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus> {
let working_directory = self
.repository
.lock()
@ -289,8 +292,9 @@ impl GitRepository for FakeGitRepository {
state.dot_git_dir.clone()
}
fn status(&self, path_prefixes: &[PathBuf]) -> Result<GitStatus> {
fn status(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus> {
let state = self.state.lock();
let mut entries = state
.worktree_statuses
.iter()
@ -306,6 +310,7 @@ impl GitRepository for FakeGitRepository {
})
.collect::<Vec<_>>();
entries.sort_unstable_by(|a, b| a.0.cmp(&b.0));
Ok(GitStatus {
entries: entries.into(),
})
@ -394,6 +399,8 @@ pub enum GitFileStatus {
Added,
Modified,
Conflict,
Deleted,
Untracked,
}
impl GitFileStatus {
@ -421,20 +428,34 @@ impl GitFileStatus {
}
}
pub static WORK_DIRECTORY_REPO_PATH: LazyLock<RepoPath> =
LazyLock::new(|| RepoPath(Path::new("").into()));
#[derive(Clone, Debug, Ord, Hash, PartialOrd, Eq, PartialEq)]
pub struct RepoPath(pub PathBuf);
pub struct RepoPath(pub Arc<Path>);
impl RepoPath {
pub fn new(path: PathBuf) -> Self {
debug_assert!(path.is_relative(), "Repo paths must be relative");
RepoPath(path)
RepoPath(path.into())
}
pub fn from_str(path: &str) -> Self {
let path = Path::new(path);
debug_assert!(path.is_relative(), "Repo paths must be relative");
RepoPath(path.into())
}
pub fn to_proto(&self) -> String {
self.0.to_string_lossy().to_string()
}
}
impl From<&Path> for RepoPath {
fn from(value: &Path) -> Self {
RepoPath::new(value.to_path_buf())
RepoPath::new(value.into())
}
}
@ -444,9 +465,15 @@ impl From<PathBuf> for RepoPath {
}
}
impl From<&str> for RepoPath {
fn from(value: &str) -> Self {
Self::from_str(value)
}
}
impl Default for RepoPath {
fn default() -> Self {
RepoPath(PathBuf::new())
RepoPath(Path::new("").into())
}
}
@ -457,13 +484,19 @@ impl AsRef<Path> for RepoPath {
}
impl std::ops::Deref for RepoPath {
type Target = PathBuf;
type Target = Path;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Borrow<Path> for RepoPath {
fn borrow(&self) -> &Path {
self.0.as_ref()
}
}
#[derive(Debug)]
pub struct RepoPathDescendants<'a>(pub &'a Path);

View file

@ -1,10 +1,6 @@
use crate::repository::{GitFileStatus, RepoPath};
use anyhow::{anyhow, Result};
use std::{
path::{Path, PathBuf},
process::Stdio,
sync::Arc,
};
use std::{path::Path, process::Stdio, sync::Arc};
#[derive(Clone)]
pub struct GitStatus {
@ -15,7 +11,7 @@ impl GitStatus {
pub(crate) fn new(
git_binary: &Path,
working_directory: &Path,
path_prefixes: &[PathBuf],
path_prefixes: &[RepoPath],
) -> Result<Self> {
let child = util::command::new_std_command(git_binary)
.current_dir(working_directory)
@ -27,7 +23,7 @@ impl GitStatus {
"-z",
])
.args(path_prefixes.iter().map(|path_prefix| {
if *path_prefix == Path::new("") {
if path_prefix.0.as_ref() == Path::new("") {
Path::new(".")
} else {
path_prefix
@ -55,10 +51,12 @@ impl GitStatus {
let (status, path) = entry.split_at(3);
let status = status.trim();
Some((
RepoPath(PathBuf::from(path)),
RepoPath(Path::new(path).into()),
match status {
"A" | "??" => GitFileStatus::Added,
"A" => GitFileStatus::Added,
"M" => GitFileStatus::Modified,
"D" => GitFileStatus::Deleted,
"??" => GitFileStatus::Untracked,
_ => return None,
},
))
@ -75,7 +73,7 @@ impl GitStatus {
pub fn get(&self, path: &Path) -> Option<GitFileStatus> {
self.entries
.binary_search_by(|(repo_path, _)| repo_path.0.as_path().cmp(path))
.binary_search_by(|(repo_path, _)| repo_path.0.as_ref().cmp(path))
.ok()
.map(|index| self.entries[index].1)
}

View file

@ -14,9 +14,11 @@ path = "src/git_ui.rs"
[dependencies]
anyhow.workspace = true
collections.workspace = true
db.workspace = true
editor.workspace = true
futures.workspace = true
git.workspace = true
gpui.workspace = true
language.workspace = true
menu.workspace = true
@ -29,8 +31,7 @@ settings.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
git.workspace = true
collections.workspace = true
worktree.workspace = true
[target.'cfg(windows)'.dependencies]
windows.workspace = true

View file

@ -1,11 +1,16 @@
use crate::{git_status_icon, settings::GitPanelSettings};
use crate::{CommitAllChanges, CommitStagedChanges, DiscardAll, StageAll, UnstageAll};
use anyhow::{Context as _, Result};
use collections::HashMap;
use db::kvp::KEY_VALUE_STORE;
use editor::{
scroll::{Autoscroll, AutoscrollStrategy},
Editor, MultiBuffer, DEFAULT_MULTIBUFFER_CONTEXT,
};
use git::{diff::DiffHunk, repository::GitFileStatus};
use git::{
diff::DiffHunk,
repository::{GitFileStatus, RepoPath},
};
use gpui::*;
use gpui::{
actions, prelude::*, uniform_list, Action, AppContext, AsyncWindowContext, ClickEvent,
CursorStyle, EventEmitter, FocusHandle, FocusableView, KeyContext,
@ -14,7 +19,7 @@ use gpui::{
};
use language::{Buffer, BufferRow, OffsetRangeExt};
use menu::{SelectNext, SelectPrev};
use project::{Entry, EntryKind, Fs, Project, ProjectEntryId, WorktreeId};
use project::{EntryKind, Fs, Project, ProjectEntryId, ProjectPath, WorktreeId};
use serde::{Deserialize, Serialize};
use settings::Settings as _;
use std::{
@ -22,7 +27,7 @@ use std::{
collections::HashSet,
ffi::OsStr,
ops::{Deref, Range},
path::{Path, PathBuf},
path::PathBuf,
rc::Rc,
sync::Arc,
time::Duration,
@ -37,9 +42,7 @@ use workspace::{
dock::{DockPosition, Panel, PanelEvent},
ItemHandle, Workspace,
};
use crate::{git_status_icon, settings::GitPanelSettings};
use crate::{CommitAllChanges, CommitStagedChanges, DiscardAll, StageAll, UnstageAll};
use worktree::StatusEntry;
actions!(git_panel, [ToggleFocus]);
@ -69,7 +72,7 @@ pub struct GitStatusEntry {}
struct EntryDetails {
filename: String,
display_name: String,
path: Arc<Path>,
path: RepoPath,
kind: EntryKind,
depth: usize,
is_expanded: bool,
@ -101,7 +104,8 @@ pub struct GitPanel {
scrollbar_state: ScrollbarState,
selected_item: Option<usize>,
show_scrollbar: bool,
expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>,
// TODO Reintroduce expanded directories, once we're deriving directories from paths
// expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>,
// The entries that are currently shown in the panel, aka
// not hidden by folding or such
@ -115,18 +119,20 @@ pub struct GitPanel {
#[derive(Debug, Clone)]
struct WorktreeEntries {
worktree_id: WorktreeId,
// TODO support multiple repositories per worktree
work_directory: worktree::WorkDirectory,
visible_entries: Vec<GitPanelEntry>,
paths: Rc<OnceCell<HashSet<Arc<Path>>>>,
paths: Rc<OnceCell<HashSet<RepoPath>>>,
}
#[derive(Debug, Clone)]
struct GitPanelEntry {
entry: Entry,
entry: worktree::StatusEntry,
hunks: Rc<OnceCell<Vec<DiffHunk>>>,
}
impl Deref for GitPanelEntry {
type Target = Entry;
type Target = worktree::StatusEntry;
fn deref(&self) -> &Self::Target {
&self.entry
@ -134,11 +140,11 @@ impl Deref for GitPanelEntry {
}
impl WorktreeEntries {
fn paths(&self) -> &HashSet<Arc<Path>> {
fn paths(&self) -> &HashSet<RepoPath> {
self.paths.get_or_init(|| {
self.visible_entries
.iter()
.map(|e| (e.entry.path.clone()))
.map(|e| (e.entry.repo_path.clone()))
.collect()
})
}
@ -165,8 +171,11 @@ impl GitPanel {
})
.detach();
cx.subscribe(&project, |this, _, event, cx| match event {
project::Event::WorktreeRemoved(id) => {
this.expanded_dir_ids.remove(id);
project::Event::GitRepositoryUpdated => {
this.update_visible_entries(None, None, cx);
}
project::Event::WorktreeRemoved(_id) => {
// this.expanded_dir_ids.remove(id);
this.update_visible_entries(None, None, cx);
cx.notify();
}
@ -183,7 +192,7 @@ impl GitPanel {
project::Event::Closed => {
this.git_diff_editor_updates = Task::ready(());
this.reveal_in_editor = Task::ready(());
this.expanded_dir_ids.clear();
// this.expanded_dir_ids.clear();
this.visible_entries.clear();
this.git_diff_editor = None;
}
@ -200,8 +209,7 @@ impl GitPanel {
pending_serialization: Task::ready(None),
visible_entries: Vec::new(),
current_modifiers: cx.modifiers(),
expanded_dir_ids: Default::default(),
// expanded_dir_ids: Default::default(),
width: Some(px(360.)),
scrollbar_state: ScrollbarState::new(scroll_handle.clone()).parent_view(cx.view()),
scroll_handle,
@ -288,16 +296,16 @@ impl GitPanel {
}
fn calculate_depth_and_difference(
entry: &Entry,
visible_worktree_entries: &HashSet<Arc<Path>>,
entry: &StatusEntry,
visible_worktree_entries: &HashSet<RepoPath>,
) -> (usize, usize) {
let (depth, difference) = entry
.path
.repo_path
.ancestors()
.skip(1) // Skip the entry itself
.find_map(|ancestor| {
if let Some(parent_entry) = visible_worktree_entries.get(ancestor) {
let entry_path_components_count = entry.path.components().count();
let entry_path_components_count = entry.repo_path.components().count();
let parent_path_components_count = parent_entry.components().count();
let difference = entry_path_components_count - parent_path_components_count;
let depth = parent_entry
@ -432,13 +440,7 @@ impl GitPanel {
fn entry_count(&self) -> usize {
self.visible_entries
.iter()
.map(|worktree_entries| {
worktree_entries
.visible_entries
.iter()
.filter(|entry| entry.git_status.is_some())
.count()
})
.map(|worktree_entries| worktree_entries.visible_entries.len())
.sum()
}
@ -446,7 +448,7 @@ impl GitPanel {
&self,
range: Range<usize>,
cx: &mut ViewContext<Self>,
mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut ViewContext<Self>),
mut callback: impl FnMut(usize, EntryDetails, &mut ViewContext<Self>),
) {
let mut ix = 0;
for worktree_entries in &self.visible_entries {
@ -468,11 +470,11 @@ impl GitPanel {
{
let snapshot = worktree.read(cx).snapshot();
let root_name = OsStr::new(snapshot.root_name());
let expanded_entry_ids = self
.expanded_dir_ids
.get(&snapshot.id())
.map(Vec::as_slice)
.unwrap_or(&[]);
// let expanded_entry_ids = self
// .expanded_dir_ids
// .get(&snapshot.id())
// .map(Vec::as_slice)
// .unwrap_or(&[]);
let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
let entries = worktree_entries.paths();
@ -483,22 +485,22 @@ impl GitPanel {
.enumerate()
{
let index = index_start + i;
let status = entry.git_status;
let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok();
let status = entry.status;
let is_expanded = true; //expanded_entry_ids.binary_search(&entry.id).is_ok();
let (depth, difference) = Self::calculate_depth_and_difference(entry, entries);
let filename = match difference {
diff if diff > 1 => entry
.path
.repo_path
.iter()
.skip(entry.path.components().count() - diff)
.skip(entry.repo_path.components().count() - diff)
.collect::<PathBuf>()
.to_str()
.unwrap_or_default()
.to_string(),
_ => entry
.path
.repo_path
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_else(|| root_name.to_string_lossy().to_string()),
@ -506,16 +508,17 @@ impl GitPanel {
let details = EntryDetails {
filename,
display_name: entry.path.to_string_lossy().into_owned(),
kind: entry.kind,
display_name: entry.repo_path.to_string_lossy().into_owned(),
// TODO get it from StatusEntry?
kind: EntryKind::File,
is_expanded,
path: entry.path.clone(),
status,
path: entry.repo_path.clone(),
status: Some(status),
hunks: entry.hunks.clone(),
depth,
index,
};
callback(entry.id, details, cx);
callback(ix, details, cx);
}
}
ix = end_ix;
@ -527,7 +530,7 @@ impl GitPanel {
fn update_visible_entries(
&mut self,
for_worktree: Option<WorktreeId>,
new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
_new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
cx: &mut ViewContext<Self>,
) {
let project = self.project.read(cx);
@ -549,24 +552,36 @@ impl GitPanel {
None => false,
});
for worktree in project.visible_worktrees(cx) {
let worktree_id = worktree.read(cx).id();
let snapshot = worktree.read(cx).snapshot();
let worktree_id = snapshot.id();
if for_worktree.is_some() && for_worktree != Some(worktree_id) {
continue;
}
let snapshot = worktree.read(cx).snapshot();
let mut visible_worktree_entries = snapshot
.entries(false, 0)
.filter(|entry| !entry.is_external)
.filter(|entry| entry.git_status.is_some())
.cloned()
.collect::<Vec<_>>();
snapshot.propagate_git_statuses(&mut visible_worktree_entries);
project::sort_worktree_entries(&mut visible_worktree_entries);
let mut visible_worktree_entries = Vec::new();
// Only use the first repository for now
let repositories = snapshot.repositories().take(1);
let mut work_directory = None;
for repository in repositories {
visible_worktree_entries.extend(repository.status());
work_directory = Some(worktree::WorkDirectory::clone(repository));
}
// TODO use the GitTraversal
// let mut visible_worktree_entries = snapshot
// .entries(false, 0)
// .filter(|entry| !entry.is_external)
// .filter(|entry| entry.git_status.is_some())
// .cloned()
// .collect::<Vec<_>>();
// snapshot.propagate_git_statuses(&mut visible_worktree_entries);
// project::sort_worktree_entries(&mut visible_worktree_entries);
if !visible_worktree_entries.is_empty() {
self.visible_entries.push(WorktreeEntries {
worktree_id,
work_directory: work_directory.unwrap(),
visible_entries: visible_worktree_entries
.into_iter()
.map(|entry| GitPanelEntry {
@ -580,24 +595,25 @@ impl GitPanel {
}
self.visible_entries.extend(after_update);
if let Some((worktree_id, entry_id)) = new_selected_entry {
self.selected_item = self.visible_entries.iter().enumerate().find_map(
|(worktree_index, worktree_entries)| {
if worktree_entries.worktree_id == worktree_id {
worktree_entries
.visible_entries
.iter()
.position(|entry| entry.id == entry_id)
.map(|entry_index| {
worktree_index * worktree_entries.visible_entries.len()
+ entry_index
})
} else {
None
}
},
);
}
// TODO re-implement this
// if let Some((worktree_id, entry_id)) = new_selected_entry {
// self.selected_item = self.visible_entries.iter().enumerate().find_map(
// |(worktree_index, worktree_entries)| {
// if worktree_entries.worktree_id == worktree_id {
// worktree_entries
// .visible_entries
// .iter()
// .position(|entry| entry.id == entry_id)
// .map(|entry_index| {
// worktree_index * worktree_entries.visible_entries.len()
// + entry_index
// })
// } else {
// None
// }
// },
// );
// }
let project = self.project.downgrade();
self.git_diff_editor_updates = cx.spawn(|git_panel, mut cx| async move {
@ -612,12 +628,14 @@ impl GitPanel {
.visible_entries
.iter()
.filter_map(|entry| {
let git_status = entry.git_status()?;
let git_status = entry.status;
let entry_hunks = entry.hunks.clone();
let (entry_path, unstaged_changes_task) =
project.update(cx, |project, cx| {
let entry_path =
project.path_for_entry(entry.id, cx)?;
let entry_path = ProjectPath {
worktree_id: worktree_entries.worktree_id,
path: worktree_entries.work_directory.unrelativize(&entry.repo_path)?,
};
let open_task =
project.open_path(entry_path.clone(), cx);
let unstaged_changes_task =
@ -682,8 +700,8 @@ impl GitPanel {
)
.collect()
}
// TODO support conflicts display
GitFileStatus::Conflict => Vec::new(),
// TODO support these
GitFileStatus::Conflict | GitFileStatus::Deleted | GitFileStatus::Untracked => Vec::new(),
}
}).clone()
})?;
@ -992,18 +1010,17 @@ impl GitPanel {
fn render_entry(
&self,
id: ProjectEntryId,
ix: usize,
selected: bool,
details: EntryDetails,
cx: &ViewContext<Self>,
) -> impl IntoElement {
let id = id.to_proto() as usize;
let checkbox_id = ElementId::Name(format!("checkbox_{}", id).into());
let checkbox_id = ElementId::Name(format!("checkbox_{}", ix).into());
let is_staged = ToggleState::Selected;
let handle = cx.view().downgrade();
h_flex()
.id(id)
.id(("git-panel-entry", ix))
.h(px(28.))
.w_full()
.pl(px(12. + 12. * details.depth as f32))
@ -1019,7 +1036,7 @@ impl GitPanel {
this.child(git_status_icon(status))
})
.child(
ListItem::new(("label", id))
ListItem::new(details.path.0.clone())
.toggle_state(selected)
.child(h_flex().gap_1p5().child(details.display_name.clone()))
.on_click(move |e, cx| {

View file

@ -44,10 +44,13 @@ const REMOVED_COLOR: Hsla = Hsla {
// TODO: Add updated status colors to theme
pub fn git_status_icon(status: GitFileStatus) -> impl IntoElement {
match status {
GitFileStatus::Added => Icon::new(IconName::SquarePlus).color(Color::Custom(ADDED_COLOR)),
GitFileStatus::Added | GitFileStatus::Untracked => {
Icon::new(IconName::SquarePlus).color(Color::Custom(ADDED_COLOR))
}
GitFileStatus::Modified => {
Icon::new(IconName::SquareDot).color(Color::Custom(MODIFIED_COLOR))
}
GitFileStatus::Conflict => Icon::new(IconName::Warning).color(Color::Custom(REMOVED_COLOR)),
GitFileStatus::Deleted => Icon::new(IconName::Warning).color(Color::Custom(REMOVED_COLOR)),
}
}

View file

@ -1580,7 +1580,7 @@ impl LinuxClient for X11Client {
}
}
// Adatpted from:
// Adapted from:
// https://docs.rs/winit/0.29.11/src/winit/platform_impl/linux/x11/monitor.rs.html#103-111
pub fn mode_refresh_rate(mode: &randr::ModeInfo) -> Duration {
if mode.dot_clock == 0 || mode.htotal == 0 || mode.vtotal == 0 {

View file

@ -322,7 +322,7 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
let mut chars_with_shift = chars_for_modified_key(native_event.keyCode(), SHIFT_MOD);
let always_use_cmd_layout = always_use_command_layout();
// Handle Dvorak+QWERTY / Russian / Armeniam
// Handle Dvorak+QWERTY / Russian / Armenian
if command || always_use_cmd_layout {
let chars_with_cmd = chars_for_modified_key(native_event.keyCode(), CMD_MOD);
let chars_with_both =

View file

@ -4880,6 +4880,8 @@ pub enum ElementId {
FocusHandle(FocusId),
/// A combination of a name and an integer.
NamedInteger(SharedString, usize),
/// A path
Path(Arc<std::path::Path>),
}
impl Display for ElementId {
@ -4891,6 +4893,7 @@ impl Display for ElementId {
ElementId::FocusHandle(_) => write!(f, "FocusHandle")?,
ElementId::NamedInteger(s, i) => write!(f, "{}-{}", s, i)?,
ElementId::Uuid(uuid) => write!(f, "{}", uuid)?,
ElementId::Path(path) => write!(f, "{}", path.display())?,
}
Ok(())
@ -4927,6 +4930,12 @@ impl From<SharedString> for ElementId {
}
}
impl From<Arc<std::path::Path>> for ElementId {
fn from(path: Arc<std::path::Path>) -> Self {
ElementId::Path(path)
}
}
impl From<&'static str> for ElementId {
fn from(name: &'static str) -> Self {
ElementId::Name(name.into())

View file

@ -96,12 +96,18 @@ impl Item for ImageView {
fn tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement {
let project_path = self.image_item.read(cx).project_path(cx);
let label_color = if ItemSettings::get_global(cx).git_status {
let git_status = self
.project
.read(cx)
.project_path_git_status(&project_path, cx);
self.project
.read(cx)
.entry_for_path(&project_path, cx)
.map(|entry| {
entry_git_aware_label_color(entry.git_status, entry.is_ignored, params.selected)
entry_git_aware_label_color(git_status, entry.is_ignored, params.selected)
})
.unwrap_or_else(|| params.text_color())
} else {

View file

@ -19,8 +19,8 @@ db.workspace = true
editor.workspace = true
file_icons.workspace = true
fuzzy.workspace = true
itertools.workspace = true
gpui.workspace = true
itertools.workspace = true
language.workspace = true
log.workspace = true
menu.workspace = true
@ -36,8 +36,8 @@ smol.workspace = true
theme.workspace = true
ui.workspace = true
util.workspace = true
worktree.workspace = true
workspace.workspace = true
worktree.workspace = true
[dev-dependencies]
search = { workspace = true, features = ["test-support"] }

File diff suppressed because it is too large Load diff

View file

@ -569,9 +569,9 @@ impl LocalBufferStore {
buffer_change_sets
.into_iter()
.filter_map(|(change_set, buffer_snapshot, path)| {
let (repo_entry, local_repo_entry) = snapshot.repo_for_path(&path)?;
let relative_path = repo_entry.relativize(&snapshot, &path).ok()?;
let base_text = local_repo_entry.repo().load_index_text(&relative_path);
let local_repo = snapshot.local_repo_for_path(&path)?;
let relative_path = local_repo.relativize(&path).ok()?;
let base_text = local_repo.repo().load_index_text(&relative_path);
Some((change_set, buffer_snapshot, base_text))
})
.collect::<Vec<_>>()
@ -1161,16 +1161,16 @@ impl BufferStore {
Worktree::Local(worktree) => {
let worktree = worktree.snapshot();
let blame_params = maybe!({
let (repo_entry, local_repo_entry) = match worktree.repo_for_path(&file.path) {
let local_repo = match worktree.local_repo_for_path(&file.path) {
Some(repo_for_path) => repo_for_path,
None => return Ok(None),
};
let relative_path = repo_entry
.relativize(&worktree, &file.path)
let relative_path = local_repo
.relativize(&file.path)
.context("failed to relativize buffer path")?;
let repo = local_repo_entry.repo().clone();
let repo = local_repo.repo().clone();
let content = match version {
Some(version) => buffer.rope_for_version(&version).clone(),
@ -1247,7 +1247,7 @@ impl BufferStore {
});
};
let path = match repo_entry.relativize(worktree, file.path()) {
let path = match repo_entry.relativize(file.path()) {
Ok(RepoPath(path)) => path,
Err(e) => return Task::ready(Err(e)),
};

View file

@ -87,9 +87,8 @@ pub use language::Location;
#[cfg(any(test, feature = "test-support"))]
pub use prettier::FORMAT_SUFFIX as TEST_PRETTIER_FORMAT_SUFFIX;
pub use worktree::{
Entry, EntryKind, File, LocalWorktree, PathChange, ProjectEntryId, RepositoryEntry,
UpdatedEntriesSet, UpdatedGitRepositoriesSet, Worktree, WorktreeId, WorktreeSettings,
FS_WATCH_LATENCY,
Entry, EntryKind, File, LocalWorktree, PathChange, ProjectEntryId, UpdatedEntriesSet,
UpdatedGitRepositoriesSet, Worktree, WorktreeId, WorktreeSettings, FS_WATCH_LATENCY,
};
const SERVER_LAUNCHING_BEFORE_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
@ -3109,6 +3108,7 @@ impl LspStore {
WorktreeStoreEvent::WorktreeUpdateSent(worktree) => {
worktree.update(cx, |worktree, _cx| self.send_diagnostic_summaries(worktree));
}
WorktreeStoreEvent::GitRepositoryUpdated => {}
}
}

View file

@ -39,7 +39,10 @@ use futures::{
pub use image_store::{ImageItem, ImageStore};
use image_store::{ImageItemEvent, ImageStoreEvent};
use git::{blame::Blame, repository::GitRepository};
use git::{
blame::Blame,
repository::{GitFileStatus, GitRepository},
};
use gpui::{
AnyModel, AppContext, AsyncAppContext, BorrowAppContext, Context as _, EventEmitter, Hsla,
Model, ModelContext, SharedString, Task, WeakModel, WindowContext,
@ -95,9 +98,8 @@ pub use task_inventory::{
BasicContextProvider, ContextProviderWithTasks, Inventory, TaskSourceKind,
};
pub use worktree::{
Entry, EntryKind, File, LocalWorktree, PathChange, ProjectEntryId, RepositoryEntry,
UpdatedEntriesSet, UpdatedGitRepositoriesSet, Worktree, WorktreeId, WorktreeSettings,
FS_WATCH_LATENCY,
Entry, EntryKind, File, LocalWorktree, PathChange, ProjectEntryId, UpdatedEntriesSet,
UpdatedGitRepositoriesSet, Worktree, WorktreeId, WorktreeSettings, FS_WATCH_LATENCY,
};
pub use buffer_store::ProjectTransaction;
@ -242,6 +244,7 @@ pub enum Event {
ActivateProjectPanel,
WorktreeAdded(WorktreeId),
WorktreeOrderChanged,
GitRepositoryUpdated,
WorktreeRemoved(WorktreeId),
WorktreeUpdatedEntries(WorktreeId, UpdatedEntriesSet),
WorktreeUpdatedGitRepositories(WorktreeId),
@ -1433,6 +1436,15 @@ impl Project {
.unwrap_or(false)
}
pub fn project_path_git_status(
&self,
project_path: &ProjectPath,
cx: &AppContext,
) -> Option<GitFileStatus> {
self.worktree_for_id(project_path.worktree_id, cx)
.and_then(|worktree| worktree.read(cx).status_for_file(&project_path.path))
}
pub fn visibility_for_paths(&self, paths: &[PathBuf], cx: &AppContext) -> Option<bool> {
paths
.iter()
@ -2295,6 +2307,7 @@ impl Project {
}
WorktreeStoreEvent::WorktreeOrderChanged => cx.emit(Event::WorktreeOrderChanged),
WorktreeStoreEvent::WorktreeUpdateSent(_) => {}
WorktreeStoreEvent::GitRepositoryUpdated => cx.emit(Event::GitRepositoryUpdated),
}
}
@ -3516,17 +3529,6 @@ impl Project {
)
}
pub fn get_repo(
&self,
project_path: &ProjectPath,
cx: &AppContext,
) -> Option<Arc<dyn GitRepository>> {
self.worktree_for_id(project_path.worktree_id, cx)?
.read(cx)
.as_local()?
.local_git_repo(&project_path.path)
}
pub fn get_first_worktree_root_repo(&self, cx: &AppContext) -> Option<Arc<dyn GitRepository>> {
let worktree = self.visible_worktrees(cx).next()?.read(cx).as_local()?;
let root_entry = worktree.root_git_entry()?;
@ -4426,8 +4428,10 @@ impl Completion {
}
}
pub fn sort_worktree_entries(entries: &mut [Entry]) {
pub fn sort_worktree_entries(entries: &mut [impl AsRef<Entry>]) {
entries.sort_by(|entry_a, entry_b| {
let entry_a = entry_a.as_ref();
let entry_b = entry_b.as_ref();
compare_paths(
(&entry_a.path, entry_a.is_file()),
(&entry_b.path, entry_b.is_file()),

View file

@ -109,7 +109,7 @@ impl Inventory {
/// Pulls its task sources relevant to the worktree and the language given and resolves them with the [`TaskContext`] given.
/// Joins the new resolutions with the resolved tasks that were used (spawned) before,
/// orders them so that the most recently used come first, all equally used ones are ordered so that the most specific tasks come first.
/// Deduplicates the tasks by their labels and contenxt and splits the ordered list into two: used tasks and the rest, newly resolved tasks.
/// Deduplicates the tasks by their labels and context and splits the ordered list into two: used tasks and the rest, newly resolved tasks.
pub fn used_and_current_resolved_tasks(
&self,
worktree: Option<WorktreeId>,

View file

@ -62,6 +62,7 @@ pub enum WorktreeStoreEvent {
WorktreeReleased(EntityId, WorktreeId),
WorktreeOrderChanged,
WorktreeUpdateSent(Model<Worktree>),
GitRepositoryUpdated,
}
impl EventEmitter<WorktreeStoreEvent> for WorktreeStore {}
@ -322,6 +323,7 @@ impl WorktreeStore {
let worktree = Worktree::local(path.clone(), visible, fs, next_entry_id, &mut cx).await;
let worktree = worktree?;
this.update(&mut cx, |this, cx| this.add(&worktree, cx))?;
if visible {
@ -374,6 +376,17 @@ impl WorktreeStore {
this.send_project_updates(cx);
})
.detach();
cx.subscribe(
worktree,
|_this, _, event: &worktree::Event, cx| match event {
worktree::Event::UpdatedGitRepositories(_) => {
cx.emit(WorktreeStoreEvent::GitRepositoryUpdated);
}
worktree::Event::DeletedEntry(_) | worktree::Event::UpdatedEntries(_) => {}
},
)
.detach();
}
pub fn remove_worktree(&mut self, id_to_remove: WorktreeId, cx: &mut ModelContext<Self>) {
@ -583,11 +596,11 @@ impl WorktreeStore {
pub fn shared(
&mut self,
remote_id: u64,
downsteam_client: AnyProtoClient,
downstream_client: AnyProtoClient,
cx: &mut ModelContext<Self>,
) {
self.retain_worktrees = true;
self.downstream_client = Some((downsteam_client, remote_id));
self.downstream_client = Some((downstream_client, remote_id));
// When shared, retain all worktrees
for worktree_handle in self.worktrees.iter_mut() {

View file

@ -63,7 +63,7 @@ use workspace::{
notifications::{DetachAndPromptErr, NotifyTaskExt},
DraggedSelection, OpenInTerminal, PreviewTabsSettings, SelectedEntry, Workspace,
};
use worktree::CreatedEntry;
use worktree::{CreatedEntry, GitEntry, GitEntryRef};
const PROJECT_PANEL_KEY: &str = "ProjectPanel";
const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX;
@ -76,7 +76,7 @@ pub struct ProjectPanel {
// An update loop that keeps incrementing/decrementing scroll offset while there is a dragged entry that's
// hovered over the start/end of a list.
hover_scroll_task: Option<Task<()>>,
visible_entries: Vec<(WorktreeId, Vec<Entry>, OnceCell<HashSet<Arc<Path>>>)>,
visible_entries: Vec<(WorktreeId, Vec<GitEntry>, OnceCell<HashSet<Arc<Path>>>)>,
/// Maps from leaf project entry ID to the currently selected ancestor.
/// Relevant only for auto-fold dirs, where a single project panel entry may actually consist of several
/// project entries (and all non-leaf nodes are guaranteed to be directories).
@ -311,7 +311,8 @@ impl ProjectPanel {
this.update_visible_entries(None, cx);
cx.notify();
}
project::Event::WorktreeUpdatedEntries(_, _)
project::Event::GitRepositoryUpdated
| project::Event::WorktreeUpdatedEntries(_, _)
| project::Event::WorktreeAdded(_)
| project::Event::WorktreeOrderChanged => {
this.update_visible_entries(None, cx);
@ -1366,9 +1367,10 @@ impl ProjectPanel {
let parent_entry = worktree.entry_for_path(parent_path)?;
// Remove all siblings that are being deleted except the last marked entry
let mut siblings: Vec<Entry> = worktree
let mut siblings: Vec<_> = worktree
.snapshot()
.child_entries(parent_path)
.with_git_statuses()
.filter(|sibling| {
sibling.id == latest_entry.id
|| !marked_entries_in_worktree.contains(&&SelectedEntry {
@ -1376,7 +1378,7 @@ impl ProjectPanel {
entry_id: sibling.id,
})
})
.cloned()
.map(|entry| entry.to_owned())
.collect();
project::sort_worktree_entries(&mut siblings);
@ -2334,7 +2336,7 @@ impl ProjectPanel {
}
let mut visible_worktree_entries = Vec::new();
let mut entry_iter = snapshot.entries(true, 0);
let mut entry_iter = snapshot.entries(true, 0).with_git_statuses();
let mut auto_folded_ancestors = vec![];
while let Some(entry) = entry_iter.entry() {
if auto_collapse_dirs && entry.kind.is_dir() {
@ -2376,7 +2378,7 @@ impl ProjectPanel {
}
}
auto_folded_ancestors.clear();
visible_worktree_entries.push(entry.clone());
visible_worktree_entries.push(entry.to_owned());
let precedes_new_entry = if let Some(new_entry_id) = new_entry_parent_id {
entry.id == new_entry_id || {
self.ancestors.get(&entry.id).map_or(false, |entries| {
@ -2390,25 +2392,27 @@ impl ProjectPanel {
false
};
if precedes_new_entry {
visible_worktree_entries.push(Entry {
id: NEW_ENTRY_ID,
kind: new_entry_kind,
path: entry.path.join("\0").into(),
inode: 0,
mtime: entry.mtime,
size: entry.size,
is_ignored: entry.is_ignored,
is_external: false,
is_private: false,
is_always_included: entry.is_always_included,
visible_worktree_entries.push(GitEntry {
entry: Entry {
id: NEW_ENTRY_ID,
kind: new_entry_kind,
path: entry.path.join("\0").into(),
inode: 0,
mtime: entry.mtime,
size: entry.size,
is_ignored: entry.is_ignored,
is_external: false,
is_private: false,
is_always_included: entry.is_always_included,
canonical_path: entry.canonical_path.clone(),
char_bag: entry.char_bag,
is_fifo: entry.is_fifo,
},
git_status: entry.git_status,
canonical_path: entry.canonical_path.clone(),
char_bag: entry.char_bag,
is_fifo: entry.is_fifo,
});
}
let worktree_abs_path = worktree.read(cx).abs_path();
let (depth, path) = if Some(entry) == worktree.read(cx).root_entry() {
let (depth, path) = if Some(entry.entry) == worktree.read(cx).root_entry() {
let Some(path_name) = worktree_abs_path
.file_name()
.with_context(|| {
@ -2485,8 +2489,8 @@ impl ProjectPanel {
entry_iter.advance();
}
snapshot.propagate_git_statuses(&mut visible_worktree_entries);
project::sort_worktree_entries(&mut visible_worktree_entries);
self.visible_entries
.push((worktree_id, visible_worktree_entries, OnceCell::new()));
}
@ -2714,13 +2718,13 @@ impl ProjectPanel {
None
}
fn entry_at_index(&self, index: usize) -> Option<(WorktreeId, &Entry)> {
fn entry_at_index(&self, index: usize) -> Option<(WorktreeId, GitEntryRef)> {
let mut offset = 0;
for (worktree_id, visible_worktree_entries, _) in &self.visible_entries {
if visible_worktree_entries.len() > offset + index {
return visible_worktree_entries
.get(index)
.map(|entry| (*worktree_id, entry));
.map(|entry| (*worktree_id, entry.to_ref()));
}
offset += visible_worktree_entries.len();
}
@ -2753,7 +2757,7 @@ impl ProjectPanel {
.collect()
});
for entry in visible_worktree_entries[entry_range].iter() {
callback(entry, entries, cx);
callback(&entry, entries, cx);
}
ix = end_ix;
}
@ -2822,7 +2826,7 @@ impl ProjectPanel {
};
let (depth, difference) =
ProjectPanel::calculate_depth_and_difference(entry, entries);
ProjectPanel::calculate_depth_and_difference(&entry, entries);
let filename = match difference {
diff if diff > 1 => entry
@ -2951,9 +2955,9 @@ impl ProjectPanel {
worktree_id: WorktreeId,
reverse_search: bool,
only_visible_entries: bool,
predicate: impl Fn(&Entry, WorktreeId) -> bool,
predicate: impl Fn(GitEntryRef, WorktreeId) -> bool,
cx: &mut ViewContext<Self>,
) -> Option<Entry> {
) -> Option<GitEntry> {
if only_visible_entries {
let entries = self
.visible_entries
@ -2968,15 +2972,18 @@ impl ProjectPanel {
.clone();
return utils::ReversibleIterable::new(entries.iter(), reverse_search)
.find(|ele| predicate(ele, worktree_id))
.find(|ele| predicate(ele.to_ref(), worktree_id))
.cloned();
}
let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
worktree.update(cx, |tree, _| {
utils::ReversibleIterable::new(tree.entries(true, 0usize), reverse_search)
.find_single_ended(|ele| predicate(ele, worktree_id))
.cloned()
utils::ReversibleIterable::new(
tree.entries(true, 0usize).with_git_statuses(),
reverse_search,
)
.find_single_ended(|ele| predicate(*ele, worktree_id))
.map(|ele| ele.to_owned())
})
}
@ -2984,7 +2991,7 @@ impl ProjectPanel {
&self,
start: Option<&SelectedEntry>,
reverse_search: bool,
predicate: impl Fn(&Entry, WorktreeId) -> bool,
predicate: impl Fn(GitEntryRef, WorktreeId) -> bool,
cx: &mut ViewContext<Self>,
) -> Option<SelectedEntry> {
let mut worktree_ids: Vec<_> = self
@ -3006,7 +3013,9 @@ impl ProjectPanel {
let root_entry = tree.root_entry()?;
let tree_id = tree.id();
let mut first_iter = tree.traverse_from_path(true, true, true, entry.path.as_ref());
let mut first_iter = tree
.traverse_from_path(true, true, true, entry.path.as_ref())
.with_git_statuses();
if reverse_search {
first_iter.next();
@ -3014,25 +3023,25 @@ impl ProjectPanel {
let first = first_iter
.enumerate()
.take_until(|(count, ele)| *ele == root_entry && *count != 0usize)
.map(|(_, ele)| ele)
.find(|ele| predicate(ele, tree_id))
.cloned();
.take_until(|(count, entry)| entry.entry == root_entry && *count != 0usize)
.map(|(_, entry)| entry)
.find(|ele| predicate(*ele, tree_id))
.map(|ele| ele.to_owned());
let second_iter = tree.entries(true, 0usize);
let second_iter = tree.entries(true, 0usize).with_git_statuses();
let second = if reverse_search {
second_iter
.take_until(|ele| ele.id == start.entry_id)
.filter(|ele| predicate(ele, tree_id))
.filter(|ele| predicate(*ele, tree_id))
.last()
.cloned()
.map(|ele| ele.to_owned())
} else {
second_iter
.take_while(|ele| ele.id != start.entry_id)
.filter(|ele| predicate(ele, tree_id))
.filter(|ele| predicate(*ele, tree_id))
.last()
.cloned()
.map(|ele| ele.to_owned())
};
if reverse_search {
@ -3089,7 +3098,7 @@ impl ProjectPanel {
&self,
start: Option<&SelectedEntry>,
reverse_search: bool,
predicate: impl Fn(&Entry, WorktreeId) -> bool,
predicate: impl Fn(GitEntryRef, WorktreeId) -> bool,
cx: &mut ViewContext<Self>,
) -> Option<SelectedEntry> {
let mut worktree_ids: Vec<_> = self
@ -3131,8 +3140,8 @@ impl ProjectPanel {
)
};
let first_search = first_iter.find(|ele| predicate(ele, start.worktree_id));
let second_search = second_iter.find(|ele| predicate(ele, start.worktree_id));
let first_search = first_iter.find(|ele| predicate(ele.to_ref(), start.worktree_id));
let second_search = second_iter.find(|ele| predicate(ele.to_ref(), start.worktree_id));
if first_search.is_some() {
return first_search.map(|entry| SelectedEntry {

View file

@ -1768,7 +1768,7 @@ message Entry {
bool is_ignored = 7;
bool is_external = 8;
reserved 6;
optional GitStatus git_status = 9;
reserved 9;
bool is_fifo = 10;
optional uint64 size = 11;
optional string canonical_path = 12;
@ -1777,6 +1777,8 @@ message Entry {
message RepositoryEntry {
uint64 work_directory_id = 1;
optional string branch = 2;
repeated StatusEntry updated_statuses = 3;
repeated string removed_statuses = 4;
}
message StatusEntry {
@ -1788,6 +1790,7 @@ enum GitStatus {
Added = 0;
Modified = 1;
Conflict = 2;
Deleted = 3;
}
message BufferState {

View file

@ -20,6 +20,7 @@ use serde_json::json;
use settings::{initial_server_settings_content, Settings, SettingsLocation, SettingsStore};
use smol::stream::StreamExt;
use std::{
collections::HashSet,
path::{Path, PathBuf},
sync::Arc,
};
@ -1150,6 +1151,10 @@ async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestA
let (project, headless_project) = init_test(&fs, cx, server_cx).await;
let branches = ["main", "dev", "feature-1"];
let branches_set = branches
.iter()
.map(ToString::to_string)
.collect::<HashSet<_>>();
fs.insert_branches(Path::new("/code/project1/.git"), &branches);
let (worktree, _) = project
@ -1173,10 +1178,10 @@ async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestA
let remote_branches = remote_branches
.into_iter()
.map(|branch| branch.name)
.collect::<Vec<_>>();
.map(|branch| branch.name.to_string())
.collect::<HashSet<_>>();
assert_eq!(&remote_branches, &branches);
assert_eq!(&remote_branches, &branches_set);
cx.update(|cx| {
project.update(cx, |project, cx| {

View file

@ -39,7 +39,7 @@ pub async fn launch_remote_kernel(
let kernel_launch_request = KernelLaunchRequest {
name: kernel_name.to_string(),
// Note: since the path we have locally may not be the same as the one on the remote server,
// we don't send it. We'll have to evaluate this decisiion along the way.
// we don't send it. We'll have to evaluate this decision along the way.
path: None,
};

View file

@ -5,7 +5,7 @@ use collections::HashMap;
// for those users.
//
// The way macOS solves this problem is to move shortcuts around so that they are all reachable,
// even if the mnemoic changes. https://developer.apple.com/documentation/swiftui/keyboardshortcut/localization-swift.struct
// even if the mnemonic changes. https://developer.apple.com/documentation/swiftui/keyboardshortcut/localization-swift.struct
//
// For example, cmd-> is the "switch window" shortcut because the > key is right above tab.
// To ensure this doesn't cause problems for shortcuts defined for a QWERTY layout, apple moves

View file

@ -9,6 +9,15 @@ struct StackEntry<'a, T: Item, D> {
position: D,
}
impl<'a, T: Item + fmt::Debug, D: fmt::Debug> fmt::Debug for StackEntry<'a, T, D> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("StackEntry")
.field("index", &self.index)
.field("position", &self.position)
.finish()
}
}
#[derive(Clone)]
pub struct Cursor<'a, T: Item, D> {
tree: &'a SumTree<T>,
@ -18,6 +27,21 @@ pub struct Cursor<'a, T: Item, D> {
at_end: bool,
}
impl<'a, T: Item + fmt::Debug, D: fmt::Debug> fmt::Debug for Cursor<'a, T, D>
where
T::Summary: fmt::Debug,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Cursor")
.field("tree", &self.tree)
.field("stack", &self.stack)
.field("position", &self.position)
.field("did_seek", &self.did_seek)
.field("at_end", &self.at_end)
.finish()
}
}
pub struct Iter<'a, T: Item> {
tree: &'a SumTree<T>,
stack: ArrayVec<StackEntry<'a, T, ()>, 16>,
@ -60,6 +84,7 @@ where
}
}
/// Item is None, when the list is empty, or this cursor is at the end of the list.
#[track_caller]
pub fn item(&self) -> Option<&'a T> {
self.assert_did_seek();

View file

@ -42,6 +42,21 @@ pub trait Summary: Clone {
fn add_summary(&mut self, summary: &Self, cx: &Self::Context);
}
/// This type exists because we can't implement Summary for () without causing
/// type resolution errors
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub struct Unit;
impl Summary for Unit {
type Context = ();
fn zero(_: &()) -> Self {
Unit
}
fn add_summary(&mut self, _: &Self, _: &()) {}
}
/// Each [`Summary`] type can have more than one [`Dimension`] type that it measures.
///
/// You can use dimensions to seek to a specific location in the [`SumTree`]
@ -761,6 +776,55 @@ impl<T: KeyedItem> SumTree<T> {
None
}
}
#[inline]
pub fn contains(&self, key: &T::Key, cx: &<T::Summary as Summary>::Context) -> bool {
self.get(key, cx).is_some()
}
pub fn update<F, R>(
&mut self,
key: &T::Key,
cx: &<T::Summary as Summary>::Context,
f: F,
) -> Option<R>
where
F: FnOnce(&mut T) -> R,
{
let mut cursor = self.cursor::<T::Key>(cx);
let mut new_tree = cursor.slice(key, Bias::Left, cx);
let mut result = None;
if Ord::cmp(key, &cursor.end(cx)) == Ordering::Equal {
let mut updated = cursor.item().unwrap().clone();
result = Some(f(&mut updated));
new_tree.push(updated, cx);
cursor.next(cx);
}
new_tree.append(cursor.suffix(cx), cx);
drop(cursor);
*self = new_tree;
result
}
pub fn retain<F: FnMut(&T) -> bool>(
&mut self,
cx: &<T::Summary as Summary>::Context,
mut predicate: F,
) {
let mut new_map = SumTree::new(cx);
let mut cursor = self.cursor::<T::Key>(cx);
cursor.next(cx);
while let Some(item) = cursor.item() {
if predicate(&item) {
new_map.push(item.clone(), cx);
}
cursor.next(cx);
}
drop(cursor);
*self = new_map;
}
}
impl<T, S> Default for SumTree<T>

View file

@ -358,13 +358,14 @@ impl PickerDelegate for TabSwitcherDelegate {
.item
.project_path(cx)
.as_ref()
.and_then(|path| self.project.read(cx).entry_for_path(path, cx))
.map(|entry| {
entry_git_aware_label_color(
entry.git_status,
entry.is_ignored,
selected,
)
.and_then(|path| {
let project = self.project.read(cx);
let entry = project.entry_for_path(path, cx)?;
let git_status = project.project_path_git_status(path, cx);
Some((entry, git_status))
})
.map(|(entry, git_status)| {
entry_git_aware_label_color(git_status, entry.is_ignored, selected)
})
})
.flatten();

View file

@ -60,7 +60,7 @@ pub struct SpawnInTerminal {
pub show_command: bool,
}
/// A final form of the [`TaskTemplate`], that got resolved with a particualar [`TaskContext`] and now is ready to spawn the actual task.
/// A final form of the [`TaskTemplate`], that got resolved with a particular [`TaskContext`] and now is ready to spawn the actual task.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ResolvedTask {
/// A way to distinguish tasks produced by the same template, but different contexts.

View file

@ -143,13 +143,13 @@ impl TaskTemplate {
let truncated_variables = truncate_variables(&task_variables);
let cwd = match self.cwd.as_deref() {
Some(cwd) => {
let substitured_cwd = substitute_all_template_variables_in_str(
let substituted_cwd = substitute_all_template_variables_in_str(
cwd,
&task_variables,
&variable_names,
&mut substituted_variables,
)?;
Some(PathBuf::from(substitured_cwd))
Some(PathBuf::from(substituted_cwd))
}
None => None,
}

View file

@ -100,7 +100,7 @@ impl TerminalPanel {
terminal_panel
}
pub fn asssistant_enabled(&mut self, enabled: bool, cx: &mut ViewContext<Self>) {
pub fn set_assistant_enabled(&mut self, enabled: bool, cx: &mut ViewContext<Self>) {
self.assistant_enabled = enabled;
if enabled {
let focus_handle = self

View file

@ -270,7 +270,7 @@ pub struct ThemeColorsContent {
/// Fill Color. Used for the muted or deemphasized fill color of an icon.
///
/// This might be used to show an icon in an inactive pane, or to demphasize a series of icons to give them less visual weight.
/// This might be used to show an icon in an inactive pane, or to deemphasize a series of icons to give them less visual weight.
#[serde(rename = "icon.muted")]
pub icon_muted: Option<String>,

View file

@ -97,7 +97,7 @@ pub struct ThemeColors {
pub icon: Hsla,
/// Fill Color. Used for the muted or deemphasized fill color of an icon.
///
/// This might be used to show an icon in an inactive pane, or to demphasize a series of icons to give them less visual weight.
/// This might be used to show an icon in an inactive pane, or to deemphasize a series of icons to give them less visual weight.
pub icon_muted: Hsla,
/// Fill Color. Used for the disabled fill color of an icon.
///

View file

@ -71,7 +71,7 @@ impl From<WindowAppearance> for Appearance {
}
}
/// Which themes should be loaded. This is used primarlily for testing.
/// Which themes should be loaded. This is used primarily for testing.
pub enum LoadThemes {
/// Only load the base theme.
///

View file

@ -21,7 +21,7 @@ use gpui::{
Interactivity, IntoElement, Model, MouseButton, ParentElement, Render, Stateful,
StatefulInteractiveElement, Styled, Subscription, View, ViewContext, VisualContext, WeakView,
};
use project::{Project, RepositoryEntry};
use project::Project;
use rpc::proto;
use settings::Settings as _;
use smallvec::SmallVec;
@ -487,7 +487,7 @@ impl TitleBar {
let workspace = self.workspace.upgrade()?;
let branch_name = entry
.as_ref()
.and_then(RepositoryEntry::branch)
.and_then(|entry| entry.branch())
.map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH))?;
Some(
Button::new("project_branch_trigger", branch_name)

View file

@ -179,7 +179,7 @@ define_connection! {
// group_id: usize, // Primary key for pane_groups
// workspace_id: usize, // References workspaces table
// parent_group_id: Option<usize>, // None indicates that this is the root node
// position: Optiopn<usize>, // None indicates that this is the root node
// position: Option<usize>, // None indicates that this is the root node
// axis: Option<Axis>, // 'Vertical', 'Horizontal'
// flexes: Option<Vec<f32>>, // A JSON array of floats
// )

File diff suppressed because it is too large Load diff

View file

@ -1497,7 +1497,8 @@ async fn test_bump_mtime_of_git_repo_workdir(cx: &mut TestAppContext) {
cx.executor().run_until_parked();
let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
check_propagated_statuses(
check_git_statuses(
&snapshot,
&[
(Path::new(""), Some(GitFileStatus::Modified)),
@ -2178,15 +2179,15 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) {
cx.read(|cx| {
let tree = tree.read(cx);
let (work_dir, _) = tree.repositories().next().unwrap();
assert_eq!(work_dir.as_ref(), Path::new("projects/project1"));
let repo = tree.repositories().next().unwrap();
assert_eq!(repo.path.as_ref(), Path::new("projects/project1"));
assert_eq!(
tree.status_for_file(Path::new("projects/project1/a")),
Some(GitFileStatus::Modified)
);
assert_eq!(
tree.status_for_file(Path::new("projects/project1/b")),
Some(GitFileStatus::Added)
Some(GitFileStatus::Untracked)
);
});
@ -2199,15 +2200,15 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) {
cx.read(|cx| {
let tree = tree.read(cx);
let (work_dir, _) = tree.repositories().next().unwrap();
assert_eq!(work_dir.as_ref(), Path::new("projects/project2"));
let repo = tree.repositories().next().unwrap();
assert_eq!(repo.path.as_ref(), Path::new("projects/project2"));
assert_eq!(
tree.status_for_file(Path::new("projects/project2/a")),
Some(GitFileStatus::Modified)
);
assert_eq!(
tree.status_for_file(Path::new("projects/project2/b")),
Some(GitFileStatus::Added)
Some(GitFileStatus::Untracked)
);
});
}
@ -2253,23 +2254,13 @@ async fn test_git_repository_for_path(cx: &mut TestAppContext) {
assert!(tree.repository_for_path("c.txt".as_ref()).is_none());
let entry = tree.repository_for_path("dir1/src/b.txt".as_ref()).unwrap();
assert_eq!(
entry
.work_directory(tree)
.map(|directory| directory.as_ref().to_owned()),
Some(Path::new("dir1").to_owned())
);
let repo = tree.repository_for_path("dir1/src/b.txt".as_ref()).unwrap();
assert_eq!(repo.path.as_ref(), Path::new("dir1"));
let entry = tree
let repo = tree
.repository_for_path("dir1/deps/dep1/src/a.txt".as_ref())
.unwrap();
assert_eq!(
entry
.work_directory(tree)
.map(|directory| directory.as_ref().to_owned()),
Some(Path::new("dir1/deps/dep1").to_owned())
);
assert_eq!(repo.path.as_ref(), Path::new("dir1/deps/dep1"));
let entries = tree.files(false, 0);
@ -2278,10 +2269,7 @@ async fn test_git_repository_for_path(cx: &mut TestAppContext) {
.map(|(entry, repo)| {
(
entry.path.as_ref(),
repo.and_then(|repo| {
repo.work_directory(tree)
.map(|work_directory| work_directory.0.to_path_buf())
}),
repo.map(|repo| repo.path.to_path_buf()),
)
})
.collect::<Vec<_>>();
@ -2334,7 +2322,7 @@ async fn test_git_repository_for_path(cx: &mut TestAppContext) {
}
#[gpui::test]
async fn test_git_status(cx: &mut TestAppContext) {
async fn test_file_status(cx: &mut TestAppContext) {
init_test(cx);
cx.executor().allow_parking();
const IGNORE_RULE: &str = "**/target";
@ -2393,17 +2381,17 @@ async fn test_git_status(cx: &mut TestAppContext) {
tree.read_with(cx, |tree, _cx| {
let snapshot = tree.snapshot();
assert_eq!(snapshot.repositories().count(), 1);
let (dir, repo_entry) = snapshot.repositories().next().unwrap();
assert_eq!(dir.as_ref(), Path::new("project"));
let repo_entry = snapshot.repositories().next().unwrap();
assert_eq!(repo_entry.path.as_ref(), Path::new("project"));
assert!(repo_entry.location_in_repo.is_none());
assert_eq!(
snapshot.status_for_file(project_path.join(B_TXT)),
Some(GitFileStatus::Added)
Some(GitFileStatus::Untracked)
);
assert_eq!(
snapshot.status_for_file(project_path.join(F_TXT)),
Some(GitFileStatus::Added)
Some(GitFileStatus::Untracked)
);
});
@ -2433,7 +2421,7 @@ async fn test_git_status(cx: &mut TestAppContext) {
let snapshot = tree.snapshot();
assert_eq!(
snapshot.status_for_file(project_path.join(F_TXT)),
Some(GitFileStatus::Added)
Some(GitFileStatus::Untracked)
);
assert_eq!(snapshot.status_for_file(project_path.join(B_TXT)), None);
assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None);
@ -2455,7 +2443,7 @@ async fn test_git_status(cx: &mut TestAppContext) {
assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None);
assert_eq!(
snapshot.status_for_file(project_path.join(B_TXT)),
Some(GitFileStatus::Added)
Some(GitFileStatus::Untracked)
);
assert_eq!(
snapshot.status_for_file(project_path.join(E_TXT)),
@ -2494,7 +2482,7 @@ async fn test_git_status(cx: &mut TestAppContext) {
let snapshot = tree.snapshot();
assert_eq!(
snapshot.status_for_file(project_path.join(renamed_dir_name).join(RENAMED_FILE)),
Some(GitFileStatus::Added)
Some(GitFileStatus::Untracked)
);
});
@ -2518,11 +2506,125 @@ async fn test_git_status(cx: &mut TestAppContext) {
.join(Path::new(renamed_dir_name))
.join(RENAMED_FILE)
),
Some(GitFileStatus::Added)
Some(GitFileStatus::Untracked)
);
});
}
#[gpui::test]
async fn test_git_repository_status(cx: &mut TestAppContext) {
init_test(cx);
cx.executor().allow_parking();
let root = temp_tree(json!({
"project": {
"a.txt": "a", // Modified
"b.txt": "bb", // Added
"c.txt": "ccc", // Unchanged
"d.txt": "dddd", // Deleted
},
}));
// Set up git repository before creating the worktree.
let work_dir = root.path().join("project");
let repo = git_init(work_dir.as_path());
git_add("a.txt", &repo);
git_add("c.txt", &repo);
git_add("d.txt", &repo);
git_commit("Initial commit", &repo);
std::fs::remove_file(work_dir.join("d.txt")).unwrap();
std::fs::write(work_dir.join("a.txt"), "aa").unwrap();
let tree = Worktree::local(
root.path(),
true,
Arc::new(RealFs::default()),
Default::default(),
&mut cx.to_async(),
)
.await
.unwrap();
tree.flush_fs_events(cx).await;
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
.await;
cx.executor().run_until_parked();
// Check that the right git state is observed on startup
tree.read_with(cx, |tree, _cx| {
let snapshot = tree.snapshot();
let repo = snapshot.repositories().next().unwrap();
let entries = repo.status().collect::<Vec<_>>();
assert_eq!(entries.len(), 3);
assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
assert_eq!(entries[0].status, GitFileStatus::Modified);
assert_eq!(entries[1].repo_path.as_ref(), Path::new("b.txt"));
assert_eq!(entries[1].status, GitFileStatus::Untracked);
assert_eq!(entries[2].repo_path.as_ref(), Path::new("d.txt"));
assert_eq!(entries[2].status, GitFileStatus::Deleted);
});
std::fs::write(work_dir.join("c.txt"), "some changes").unwrap();
eprintln!("File c.txt has been modified");
tree.flush_fs_events(cx).await;
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
.await;
cx.executor().run_until_parked();
tree.read_with(cx, |tree, _cx| {
let snapshot = tree.snapshot();
let repository = snapshot.repositories().next().unwrap();
let entries = repository.status().collect::<Vec<_>>();
std::assert_eq!(entries.len(), 4, "entries: {entries:?}");
assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
assert_eq!(entries[0].status, GitFileStatus::Modified);
assert_eq!(entries[1].repo_path.as_ref(), Path::new("b.txt"));
assert_eq!(entries[1].status, GitFileStatus::Untracked);
// Status updated
assert_eq!(entries[2].repo_path.as_ref(), Path::new("c.txt"));
assert_eq!(entries[2].status, GitFileStatus::Modified);
assert_eq!(entries[3].repo_path.as_ref(), Path::new("d.txt"));
assert_eq!(entries[3].status, GitFileStatus::Deleted);
});
git_add("a.txt", &repo);
git_add("c.txt", &repo);
git_remove_index(Path::new("d.txt"), &repo);
git_commit("Another commit", &repo);
tree.flush_fs_events(cx).await;
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
.await;
cx.executor().run_until_parked();
std::fs::remove_file(work_dir.join("a.txt")).unwrap();
std::fs::remove_file(work_dir.join("b.txt")).unwrap();
tree.flush_fs_events(cx).await;
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
.await;
cx.executor().run_until_parked();
tree.read_with(cx, |tree, _cx| {
let snapshot = tree.snapshot();
let repo = snapshot.repositories().next().unwrap();
let entries = repo.status().collect::<Vec<_>>();
// Deleting an untracked entry, b.txt, should leave no status
// a.txt was tracked, and so should have a status
assert_eq!(
entries.len(),
1,
"Entries length was incorrect\n{:#?}",
&entries
);
assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
assert_eq!(entries[0].status, GitFileStatus::Deleted);
});
}
#[gpui::test]
async fn test_repository_subfolder_git_status(cx: &mut TestAppContext) {
init_test(cx);
@ -2575,22 +2677,22 @@ async fn test_repository_subfolder_git_status(cx: &mut TestAppContext) {
tree.read_with(cx, |tree, _cx| {
let snapshot = tree.snapshot();
assert_eq!(snapshot.repositories().count(), 1);
let (dir, repo_entry) = snapshot.repositories().next().unwrap();
let repo = snapshot.repositories().next().unwrap();
// Path is blank because the working directory of
// the git repository is located at the root of the project
assert_eq!(dir.as_ref(), Path::new(""));
assert_eq!(repo.path.as_ref(), Path::new(""));
// This is the missing path between the root of the project (sub-folder-2) and its
// location relative to the root of the repository.
assert_eq!(
repo_entry.location_in_repo,
repo.location_in_repo,
Some(Arc::from(Path::new("sub-folder-1/sub-folder-2")))
);
assert_eq!(snapshot.status_for_file("c.txt"), None);
assert_eq!(
snapshot.status_for_file("d/e.txt"),
Some(GitFileStatus::Added)
Some(GitFileStatus::Untracked)
);
});
@ -2612,6 +2714,93 @@ async fn test_repository_subfolder_git_status(cx: &mut TestAppContext) {
});
}
#[gpui::test]
async fn test_traverse_with_git_status(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.background_executor.clone());
fs.insert_tree(
"/root",
json!({
"x": {
".git": {},
"x1.txt": "foo",
"x2.txt": "bar",
"y": {
".git": {},
"y1.txt": "baz",
"y2.txt": "qux"
},
"z.txt": "sneaky..."
},
"z": {
".git": {},
"z1.txt": "quux",
"z2.txt": "quuux"
}
}),
)
.await;
fs.set_status_for_repo_via_git_operation(
Path::new("/root/x/.git"),
&[
(Path::new("x2.txt"), GitFileStatus::Modified),
(Path::new("z.txt"), GitFileStatus::Added),
],
);
fs.set_status_for_repo_via_git_operation(
Path::new("/root/x/y/.git"),
&[(Path::new("y1.txt"), GitFileStatus::Conflict)],
);
fs.set_status_for_repo_via_git_operation(
Path::new("/root/z/.git"),
&[(Path::new("z2.txt"), GitFileStatus::Added)],
);
let tree = Worktree::local(
Path::new("/root"),
true,
fs.clone(),
Default::default(),
&mut cx.to_async(),
)
.await
.unwrap();
tree.flush_fs_events(cx).await;
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
.await;
cx.executor().run_until_parked();
let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
let mut traversal = snapshot
.traverse_from_path(true, false, true, Path::new("x"))
.with_git_statuses();
let entry = traversal.next().unwrap();
assert_eq!(entry.path.as_ref(), Path::new("x/x1.txt"));
assert_eq!(entry.git_status, None);
let entry = traversal.next().unwrap();
assert_eq!(entry.path.as_ref(), Path::new("x/x2.txt"));
assert_eq!(entry.git_status, Some(GitFileStatus::Modified));
let entry = traversal.next().unwrap();
assert_eq!(entry.path.as_ref(), Path::new("x/y/y1.txt"));
assert_eq!(entry.git_status, Some(GitFileStatus::Conflict));
let entry = traversal.next().unwrap();
assert_eq!(entry.path.as_ref(), Path::new("x/y/y2.txt"));
assert_eq!(entry.git_status, None);
let entry = traversal.next().unwrap();
assert_eq!(entry.path.as_ref(), Path::new("x/z.txt"));
assert_eq!(entry.git_status, Some(GitFileStatus::Added));
let entry = traversal.next().unwrap();
assert_eq!(entry.path.as_ref(), Path::new("z/z1.txt"));
assert_eq!(entry.git_status, None);
let entry = traversal.next().unwrap();
assert_eq!(entry.path.as_ref(), Path::new("z/z2.txt"));
assert_eq!(entry.git_status, Some(GitFileStatus::Added));
}
#[gpui::test]
async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
init_test(cx);
@ -2638,7 +2827,6 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
"h1.txt": "",
"h2.txt": ""
},
}),
)
.await;
@ -2668,7 +2856,16 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
cx.executor().run_until_parked();
let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
check_propagated_statuses(
check_git_statuses(
&snapshot,
&[
(Path::new(""), Some(GitFileStatus::Conflict)),
(Path::new("g"), Some(GitFileStatus::Conflict)),
(Path::new("g/h2.txt"), Some(GitFileStatus::Conflict)),
],
);
check_git_statuses(
&snapshot,
&[
(Path::new(""), Some(GitFileStatus::Conflict)),
@ -2685,7 +2882,7 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
],
);
check_propagated_statuses(
check_git_statuses(
&snapshot,
&[
(Path::new("a/b"), Some(GitFileStatus::Added)),
@ -2700,7 +2897,7 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
],
);
check_propagated_statuses(
check_git_statuses(
&snapshot,
&[
(Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
@ -2712,6 +2909,246 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
);
}
#[gpui::test]
async fn test_propagate_statuses_for_repos_under_project(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.background_executor.clone());
fs.insert_tree(
"/root",
json!({
"x": {
".git": {},
"x1.txt": "foo",
"x2.txt": "bar"
},
"y": {
".git": {},
"y1.txt": "baz",
"y2.txt": "qux"
},
"z": {
".git": {},
"z1.txt": "quux",
"z2.txt": "quuux"
}
}),
)
.await;
fs.set_status_for_repo_via_git_operation(
Path::new("/root/x/.git"),
&[(Path::new("x1.txt"), GitFileStatus::Added)],
);
fs.set_status_for_repo_via_git_operation(
Path::new("/root/y/.git"),
&[
(Path::new("y1.txt"), GitFileStatus::Conflict),
(Path::new("y2.txt"), GitFileStatus::Modified),
],
);
fs.set_status_for_repo_via_git_operation(
Path::new("/root/z/.git"),
&[(Path::new("z2.txt"), GitFileStatus::Modified)],
);
let tree = Worktree::local(
Path::new("/root"),
true,
fs.clone(),
Default::default(),
&mut cx.to_async(),
)
.await
.unwrap();
tree.flush_fs_events(cx).await;
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
.await;
cx.executor().run_until_parked();
let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
check_git_statuses(
&snapshot,
&[
(Path::new("x"), Some(GitFileStatus::Added)),
(Path::new("x/x1.txt"), Some(GitFileStatus::Added)),
],
);
check_git_statuses(
&snapshot,
&[
(Path::new("y"), Some(GitFileStatus::Conflict)),
(Path::new("y/y1.txt"), Some(GitFileStatus::Conflict)),
(Path::new("y/y2.txt"), Some(GitFileStatus::Modified)),
],
);
check_git_statuses(
&snapshot,
&[
(Path::new("z"), Some(GitFileStatus::Modified)),
(Path::new("z/z2.txt"), Some(GitFileStatus::Modified)),
],
);
check_git_statuses(
&snapshot,
&[
(Path::new("x"), Some(GitFileStatus::Added)),
(Path::new("x/x1.txt"), Some(GitFileStatus::Added)),
],
);
check_git_statuses(
&snapshot,
&[
(Path::new("x"), Some(GitFileStatus::Added)),
(Path::new("x/x1.txt"), Some(GitFileStatus::Added)),
(Path::new("x/x2.txt"), None),
(Path::new("y"), Some(GitFileStatus::Conflict)),
(Path::new("y/y1.txt"), Some(GitFileStatus::Conflict)),
(Path::new("y/y2.txt"), Some(GitFileStatus::Modified)),
(Path::new("z"), Some(GitFileStatus::Modified)),
(Path::new("z/z1.txt"), None),
(Path::new("z/z2.txt"), Some(GitFileStatus::Modified)),
],
);
}
#[gpui::test]
async fn test_propagate_statuses_for_nested_repos(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.background_executor.clone());
fs.insert_tree(
"/root",
json!({
"x": {
".git": {},
"x1.txt": "foo",
"x2.txt": "bar",
"y": {
".git": {},
"y1.txt": "baz",
"y2.txt": "qux"
},
"z.txt": "sneaky..."
},
"z": {
".git": {},
"z1.txt": "quux",
"z2.txt": "quuux"
}
}),
)
.await;
fs.set_status_for_repo_via_git_operation(
Path::new("/root/x/.git"),
&[
(Path::new("x2.txt"), GitFileStatus::Modified),
(Path::new("z.txt"), GitFileStatus::Added),
],
);
fs.set_status_for_repo_via_git_operation(
Path::new("/root/x/y/.git"),
&[(Path::new("y1.txt"), GitFileStatus::Conflict)],
);
fs.set_status_for_repo_via_git_operation(
Path::new("/root/z/.git"),
&[(Path::new("z2.txt"), GitFileStatus::Added)],
);
let tree = Worktree::local(
Path::new("/root"),
true,
fs.clone(),
Default::default(),
&mut cx.to_async(),
)
.await
.unwrap();
tree.flush_fs_events(cx).await;
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
.await;
cx.executor().run_until_parked();
let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
// Sanity check the propagation for x/y and z
check_git_statuses(
&snapshot,
&[
(Path::new("x/y"), Some(GitFileStatus::Conflict)), // the y git repository has conflict file in it, and so should have a conflict status
(Path::new("x/y/y1.txt"), Some(GitFileStatus::Conflict)),
(Path::new("x/y/y2.txt"), None),
],
);
check_git_statuses(
&snapshot,
&[
(Path::new("z"), Some(GitFileStatus::Added)),
(Path::new("z/z1.txt"), None),
(Path::new("z/z2.txt"), Some(GitFileStatus::Added)),
],
);
// Test one of the fundamental cases of propagation blocking, the transition from one git repository to another
check_git_statuses(
&snapshot,
&[
(Path::new("x"), Some(GitFileStatus::Modified)),
(Path::new("x/y"), Some(GitFileStatus::Conflict)),
(Path::new("x/y/y1.txt"), Some(GitFileStatus::Conflict)),
],
);
// Sanity check everything around it
check_git_statuses(
&snapshot,
&[
(Path::new("x"), Some(GitFileStatus::Modified)),
(Path::new("x/x1.txt"), None),
(Path::new("x/x2.txt"), Some(GitFileStatus::Modified)),
(Path::new("x/y"), Some(GitFileStatus::Conflict)),
(Path::new("x/y/y1.txt"), Some(GitFileStatus::Conflict)),
(Path::new("x/y/y2.txt"), None),
(Path::new("x/z.txt"), Some(GitFileStatus::Added)),
],
);
// Test the other fundamental case, transitioning from git repository to non-git repository
check_git_statuses(
&snapshot,
&[
(Path::new(""), None),
(Path::new("x"), Some(GitFileStatus::Modified)),
(Path::new("x/x1.txt"), None),
],
);
// And all together now
check_git_statuses(
&snapshot,
&[
(Path::new(""), None),
(Path::new("x"), Some(GitFileStatus::Modified)),
(Path::new("x/x1.txt"), None),
(Path::new("x/x2.txt"), Some(GitFileStatus::Modified)),
(Path::new("x/y"), Some(GitFileStatus::Conflict)),
(Path::new("x/y/y1.txt"), Some(GitFileStatus::Conflict)),
(Path::new("x/y/y2.txt"), None),
(Path::new("x/z.txt"), Some(GitFileStatus::Added)),
(Path::new("z"), Some(GitFileStatus::Added)),
(Path::new("z/z1.txt"), None),
(Path::new("z/z2.txt"), Some(GitFileStatus::Added)),
],
);
}
#[gpui::test]
async fn test_private_single_file_worktree(cx: &mut TestAppContext) {
init_test(cx);
@ -2736,22 +3173,20 @@ async fn test_private_single_file_worktree(cx: &mut TestAppContext) {
}
#[track_caller]
fn check_propagated_statuses(
snapshot: &Snapshot,
expected_statuses: &[(&Path, Option<GitFileStatus>)],
) {
let mut entries = expected_statuses
fn check_git_statuses(snapshot: &Snapshot, expected_statuses: &[(&Path, Option<GitFileStatus>)]) {
let mut traversal = snapshot
.traverse_from_path(true, true, false, "".as_ref())
.with_git_statuses();
let found_statuses = expected_statuses
.iter()
.map(|(path, _)| snapshot.entry_for_path(path).unwrap().clone())
.map(|&(path, _)| {
let git_entry = traversal
.find(|git_entry| &*git_entry.path == path)
.expect("Traversal has no entry for {path:?}");
(path, git_entry.git_status)
})
.collect::<Vec<_>>();
snapshot.propagate_git_statuses(&mut entries);
assert_eq!(
entries
.iter()
.map(|e| (e.path.as_ref(), e.git_status))
.collect::<Vec<_>>(),
expected_statuses
);
assert_eq!(found_statuses, expected_statuses);
}
#[track_caller]
@ -2763,14 +3198,14 @@ fn git_init(path: &Path) -> git2::Repository {
fn git_add<P: AsRef<Path>>(path: P, repo: &git2::Repository) {
let path = path.as_ref();
let mut index = repo.index().expect("Failed to get index");
index.add_path(path).expect("Failed to add a.txt");
index.add_path(path).expect("Failed to add file");
index.write().expect("Failed to write index");
}
#[track_caller]
fn git_remove_index(path: &Path, repo: &git2::Repository) {
let mut index = repo.index().expect("Failed to get index");
index.remove_path(path).expect("Failed to add a.txt");
index.remove_path(path).expect("Failed to add file");
index.write().expect("Failed to write index");
}
@ -2900,7 +3335,8 @@ fn assert_entry_git_state(
) {
let entry = tree.entry_for_path(path).expect("entry {path} not found");
assert_eq!(
entry.git_status, git_status,
tree.status_for_file(Path::new(path)),
git_status,
"expected {path} to have git status: {git_status:?}"
);
assert_eq!(

View file

@ -70,7 +70,7 @@ use util::load_shell_from_passwd;
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
fn files_not_createad_on_launch(errors: HashMap<io::ErrorKind, Vec<&Path>>) {
fn files_not_created_on_launch(errors: HashMap<io::ErrorKind, Vec<&Path>>) {
let message = "Zed failed to launch";
let error_details = errors
.into_iter()
@ -179,7 +179,7 @@ fn main() {
let file_errors = init_paths();
if !file_errors.is_empty() {
files_not_createad_on_launch(file_errors);
files_not_created_on_launch(file_errors);
return;
}

View file

@ -38,6 +38,6 @@ Open the AI Assistant panel (`cmd-r` or `ctrl-r`) and enter:
This extension requires a Perplexity API key to be available via the `PERPLEXITY_API_KEY` environment variable.
To onbtain a Perplexity.ai API token, login to your Perplexity.ai account and go [Settings->API](https://www.perplexity.ai/settings/api) and under "API Keys" click "Generate". This will require you to have [Perplexity Pro](https://www.perplexity.ai/pro) or to buy API credits. By default the extension uses `llama-3.1-sonar-small-128k-online`, currently cheapest model available which is roughly half a penny per request + a penny per 50,000 tokens. So most requests will cost less than $0.01 USD.
To obtain a Perplexity.ai API token, login to your Perplexity.ai account and go [Settings->API](https://www.perplexity.ai/settings/api) and under "API Keys" click "Generate". This will require you to have [Perplexity Pro](https://www.perplexity.ai/pro) or to buy API credits. By default the extension uses `llama-3.1-sonar-small-128k-online`, currently cheapest model available which is roughly half a penny per request + a penny per 50,000 tokens. So most requests will cost less than $0.01 USD.
Take your API key and add it to your environment by adding `export PERPLEXITY_API_KEY="pplx-0123456789abcdef..."` to your `~/.zshrc` or `~/.bashrc`. Reload close and reopen your terminal session. Check with `env |grep PERPLEXITY_API_KEY`.

View file

@ -14,7 +14,7 @@ can_code_sign=false
# This must match the team in the provisioning profile.
IDENTITY="Zed Industries, Inc."
APPLE_NOTORIZATION_TEAM="MQ55VZLNZQ"
APPLE_NOTARIZATION_TEAM="MQ55VZLNZQ"
# Function for displaying help info
help_info() {
@ -317,7 +317,7 @@ function sign_app_binaries() {
/usr/bin/codesign --deep --force --timestamp --options runtime --sign "$IDENTITY" "$(pwd)/${dmg_file_path}" -v
echo "Notarizing DMG with Apple"
"${xcode_bin_dir_path}/notarytool" submit --wait --apple-id "$APPLE_NOTARIZATION_USERNAME" --password "$APPLE_NOTARIZATION_PASSWORD" --team-id "$APPLE_NOTORIZATION_TEAM" "${dmg_file_path}"
"${xcode_bin_dir_path}/notarytool" submit --wait --apple-id "$APPLE_NOTARIZATION_USERNAME" --password "$APPLE_NOTARIZATION_PASSWORD" --team-id "$APPLE_NOTARIZATION_TEAM" "${dmg_file_path}"
echo "Removing temporary DMG (used only for notarization)"
rm "${dmg_file_path}"
@ -344,7 +344,7 @@ function sign_app_binaries() {
if [[ $can_code_sign = true ]]; then
echo "Notarizing DMG with Apple"
/usr/bin/codesign --deep --force --timestamp --options runtime --sign "$IDENTITY" "$(pwd)/${dmg_file_path}" -v
"${xcode_bin_dir_path}/notarytool" submit --wait --apple-id "$APPLE_NOTARIZATION_USERNAME" --password "$APPLE_NOTARIZATION_PASSWORD" --team-id "$APPLE_NOTORIZATION_TEAM" "${dmg_file_path}"
"${xcode_bin_dir_path}/notarytool" submit --wait --apple-id "$APPLE_NOTARIZATION_USERNAME" --password "$APPLE_NOTARIZATION_PASSWORD" --team-id "$APPLE_NOTARIZATION_TEAM" "${dmg_file_path}"
"${xcode_bin_dir_path}/stapler" staple "${dmg_file_path}"
fi