Compare commits
13 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
7b721fd6df | ||
![]() |
624e62b8d6 | ||
![]() |
8910f7970b | ||
![]() |
16012fd800 | ||
![]() |
0b095f28ef | ||
![]() |
23a81e3d93 | ||
![]() |
d238e1867e | ||
![]() |
40b6c657ef | ||
![]() |
9d32e933e8 | ||
![]() |
ca356108f0 | ||
![]() |
1c820ea3c6 | ||
![]() |
0824bc5700 | ||
![]() |
89d9d2d2b9 |
36 changed files with 1571 additions and 1303 deletions
3
Cargo.lock
generated
3
Cargo.lock
generated
|
@ -2440,6 +2440,7 @@ dependencies = [
|
|||
"parking_lot 0.11.2",
|
||||
"regex",
|
||||
"rope",
|
||||
"rpc",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
|
@ -8782,7 +8783,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.90.0"
|
||||
version = "0.90.2"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"ai",
|
||||
|
|
4
assets/icons/robot_14.svg
Normal file
4
assets/icons/robot_14.svg
Normal file
|
@ -0,0 +1,4 @@
|
|||
<svg width="14" height="16" viewBox="0 0 14 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.5 4C2.5 2.89531 3.39688 2 4.5 2H9.5C10.6031 2 11.5 2.89531 11.5 4V8C11.5 9.10312 10.6031 10 9.5 10H4.5C3.39688 10 2.5 9.10312 2.5 8V4ZM5 4C4.44687 4 4 4.44687 4 5C4 5.55313 4.44687 6 5 6C5.55313 6 6 5.55313 6 5C6 4.44687 5.55313 4 5 4ZM9 6C9.55313 6 10 5.55313 10 5C10 4.44687 9.55313 4 9 4C8.44687 4 8 4.44687 8 5C8 5.55313 8.44687 6 9 6ZM5 8.5C5.275 8.5 5.5 8.275 5.5 8C5.5 7.725 5.275 7.5 5 7.5C4.725 7.5 4.5 7.725 4.5 8C4.5 8.275 4.725 8.5 5 8.5ZM7 7.5C6.725 7.5 6.5 7.725 6.5 8C6.5 8.275 6.725 8.5 7 8.5C7.275 8.5 7.5 8.275 7.5 8C7.5 7.725 7.275 7.5 7 7.5ZM9 8.5C9.275 8.5 9.5 8.275 9.5 8C9.5 7.725 9.275 7.5 9 7.5C8.725 7.5 8.5 7.725 8.5 8C8.5 8.275 8.725 8.5 9 8.5ZM0 14C0 12.3156 1.34312 11 3 11H11C12.6562 11 14 12.3156 14 14V15C14 15.5531 13.5531 16 13 16H11V14C11 13.4469 10.5531 13 10 13H4C3.44687 13 3 13.4469 3 14V16H1C0.447812 16 0 15.5531 0 15V14Z" fill="#808080"/>
|
||||
<path d="M7.5 2H6.5V0.5C6.5 0.22375 6.725 0 7 0C7.275 0 7.5 0.22375 7.5 0.5V2ZM1.5 4.5V7.5C1.5 7.775 1.27625 8 1 8C0.72375 8 0.5 7.775 0.5 7.5V4.5C0.5 4.225 0.72375 4 1 4C1.27625 4 1.5 4.225 1.5 4.5ZM5.5 16H4.5V14.5C4.5 14.225 4.725 14 5 14C5.275 14 5.5 14.225 5.5 14.5V16ZM7.5 16H6.5V14.5C6.5 14.225 6.725 14 7 14C7.275 14 7.5 14.225 7.5 14.5V16ZM9 14C9.275 14 9.5 14.225 9.5 14.5V16H8.5V14.5C8.5 14.225 8.725 14 9 14ZM13.5 7.5C13.5 7.775 13.275 8 13 8C12.725 8 12.5 7.775 12.5 7.5V4.5C12.5 4.225 12.725 4 13 4C13.275 4 13.5 4.225 13.5 4.5V7.5Z" fill="#808080"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -108,6 +108,8 @@
|
|||
// Whether or not to remove any trailing whitespace from lines of a buffer
|
||||
// before saving it.
|
||||
"remove_trailing_whitespace_on_save": true,
|
||||
// Whether to start a new line with a comment when a previous line is a comment as well.
|
||||
"extend_comment_on_newline": true,
|
||||
// Whether or not to ensure there's a single newline at the end of a buffer
|
||||
// when saving it.
|
||||
"ensure_final_newline_on_save": true,
|
||||
|
|
|
@ -1,11 +1,5 @@
|
|||
// Folder-specific Zed settings
|
||||
// Folder-specific settings
|
||||
//
|
||||
// A subset of Zed's settings can be configured on a per-folder basis.
|
||||
//
|
||||
// For information on how to configure Zed, see the Zed
|
||||
// documentation: https://zed.dev/docs/configuring-zed
|
||||
//
|
||||
// To see all of Zed's default settings without changing your
|
||||
// custom settings, run the `open default settings` command
|
||||
// from the command palette or from `Zed` application menu.
|
||||
// For a full list of overridable settings, and general information on folder-specific settings,
|
||||
// see the documentation: https://docs.zed.dev/configuration/configuring-zed#folder-specific-settings
|
||||
{}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Folder-specific settings
|
||||
// Zed settings
|
||||
//
|
||||
// For a full list of overridable settings, and general information on folder-specific settings, see the documentation:
|
||||
// https://docs.zed.dev/configuration/configuring-zed#folder-specific-settings
|
||||
// For information on how to configure Zed, see the Zed
|
||||
// documentation: https://zed.dev/docs/configuring-zed
|
||||
//
|
||||
// To see all of Zed's default settings without changing your
|
||||
// custom settings, run the `open default settings` command
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -66,6 +66,7 @@ impl<'a> AddAssign<&'a Local> for Local {
|
|||
}
|
||||
}
|
||||
|
||||
/// A vector clock
|
||||
#[derive(Clone, Default, Hash, Eq, PartialEq)]
|
||||
pub struct Global(SmallVec<[u32; 8]>);
|
||||
|
||||
|
|
|
@ -76,6 +76,7 @@ CREATE TABLE "worktree_entries" (
|
|||
"is_symlink" BOOL NOT NULL,
|
||||
"is_ignored" BOOL NOT NULL,
|
||||
"is_deleted" BOOL NOT NULL,
|
||||
"git_status" INTEGER,
|
||||
PRIMARY KEY(project_id, worktree_id, id),
|
||||
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE
|
||||
);
|
||||
|
@ -96,22 +97,6 @@ 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" INTEGER NOT NULL,
|
||||
"work_directory_id" INTEGER NOT NULL,
|
||||
"repo_path" VARCHAR NOT NULL,
|
||||
"status" INTEGER NOT NULL,
|
||||
"scan_id" INTEGER 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_worktree_repository_statuses_on_project_id" ON "worktree_repository_statuses" ("project_id");
|
||||
CREATE INDEX "index_worktree_repository_statuses_on_project_id_and_worktree_id" ON "worktree_repository_statuses" ("project_id", "worktree_id");
|
||||
CREATE INDEX "index_worktree_repository_statuses_on_project_id_and_worktree_id_and_work_directory_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,
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
DROP TABLE "worktree_repository_statuses";
|
||||
|
||||
ALTER TABLE "worktree_entries"
|
||||
ADD "git_status" INT8;
|
|
@ -1539,6 +1539,7 @@ impl Database {
|
|||
}),
|
||||
is_symlink: db_entry.is_symlink,
|
||||
is_ignored: db_entry.is_ignored,
|
||||
git_status: db_entry.git_status.map(|status| status as i32),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1573,54 +1574,6 @@ impl Database {
|
|||
worktree.updated_repositories.push(proto::RepositoryEntry {
|
||||
work_directory_id: db_repository.work_directory_id as u64,
|
||||
branch: db_repository.branch,
|
||||
removed_repo_paths: Default::default(),
|
||||
updated_statuses: Default::default(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Repository Status Entries
|
||||
for repository in worktree.updated_repositories.iter_mut() {
|
||||
let repository_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_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(repository.work_directory_id),
|
||||
)
|
||||
.add(repository_status_entry_filter),
|
||||
)
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
|
||||
while let Some(db_status_entry) = db_repository_statuses.next().await {
|
||||
let db_status_entry = db_status_entry?;
|
||||
if db_status_entry.is_deleted {
|
||||
repository
|
||||
.removed_repo_paths
|
||||
.push(db_status_entry.repo_path);
|
||||
} else {
|
||||
repository.updated_statuses.push(proto::StatusEntry {
|
||||
repo_path: db_status_entry.repo_path,
|
||||
status: db_status_entry.status as i32,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -2396,6 +2349,7 @@ impl Database {
|
|||
mtime_nanos: ActiveValue::set(mtime.nanos as i32),
|
||||
is_symlink: ActiveValue::set(entry.is_symlink),
|
||||
is_ignored: ActiveValue::set(entry.is_ignored),
|
||||
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),
|
||||
}
|
||||
|
@ -2414,6 +2368,7 @@ impl Database {
|
|||
worktree_entry::Column::MtimeNanos,
|
||||
worktree_entry::Column::IsSymlink,
|
||||
worktree_entry::Column::IsIgnored,
|
||||
worktree_entry::Column::GitStatus,
|
||||
worktree_entry::Column::ScanId,
|
||||
])
|
||||
.to_owned(),
|
||||
|
@ -2467,68 +2422,6 @@ impl Database {
|
|||
)
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
|
||||
for repository in update.updated_repositories.iter() {
|
||||
if !repository.updated_statuses.is_empty() {
|
||||
worktree_repository_statuses::Entity::insert_many(
|
||||
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,
|
||||
),
|
||||
repo_path: ActiveValue::set(status_entry.repo_path.clone()),
|
||||
status: ActiveValue::set(status_entry.status as i64),
|
||||
scan_id: ActiveValue::set(update.scan_id as i64),
|
||||
is_deleted: ActiveValue::set(false),
|
||||
}
|
||||
}),
|
||||
)
|
||||
.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,
|
||||
worktree_repository_statuses::Column::IsDeleted,
|
||||
])
|
||||
.to_owned(),
|
||||
)
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if !repository.removed_repo_paths.is_empty() {
|
||||
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::WorkDirectoryId
|
||||
.eq(repository.work_directory_id as i64),
|
||||
)
|
||||
.and(worktree_repository_statuses::Column::RepoPath.is_in(
|
||||
repository.removed_repo_paths.iter().map(String::as_str),
|
||||
)),
|
||||
)
|
||||
.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() {
|
||||
|
@ -2812,6 +2705,7 @@ impl Database {
|
|||
}),
|
||||
is_symlink: db_entry.is_symlink,
|
||||
is_ignored: db_entry.is_ignored,
|
||||
git_status: db_entry.git_status.map(|status| status as i32),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -2837,41 +2731,12 @@ impl Database {
|
|||
proto::RepositoryEntry {
|
||||
work_directory_id: db_repository_entry.work_directory_id as u64,
|
||||
branch: db_repository_entry.branch,
|
||||
removed_repo_paths: Default::default(),
|
||||
updated_statuses: Default::default(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let mut db_status_entries = worktree_repository_statuses::Entity::find()
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(worktree_repository_statuses::Column::ProjectId.eq(project_id))
|
||||
.add(worktree_repository_statuses::Column::IsDeleted.eq(false)),
|
||||
)
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
|
||||
while let Some(db_status_entry) = db_status_entries.next().await {
|
||||
let db_status_entry = db_status_entry?;
|
||||
if let Some(worktree) = worktrees.get_mut(&(db_status_entry.worktree_id as u64))
|
||||
{
|
||||
if let Some(repository_entry) = worktree
|
||||
.repository_entries
|
||||
.get_mut(&(db_status_entry.work_directory_id as u64))
|
||||
{
|
||||
repository_entry.updated_statuses.push(proto::StatusEntry {
|
||||
repo_path: db_status_entry.repo_path,
|
||||
status: db_status_entry.status as i32,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Populate worktree diagnostic summaries.
|
||||
{
|
||||
let mut db_summaries = worktree_diagnostic_summary::Entity::find()
|
||||
|
|
|
@ -15,6 +15,7 @@ pub struct Model {
|
|||
pub inode: i64,
|
||||
pub mtime_seconds: i64,
|
||||
pub mtime_nanos: i32,
|
||||
pub git_status: Option<i64>,
|
||||
pub is_symlink: bool,
|
||||
pub is_ignored: bool,
|
||||
pub is_deleted: bool,
|
||||
|
|
|
@ -2415,14 +2415,10 @@ async fn test_git_diff_base_change(
|
|||
"
|
||||
.unindent();
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.as_fake()
|
||||
.set_index_for_repo(
|
||||
Path::new("/dir/.git"),
|
||||
&[(Path::new("a.txt"), diff_base.clone())],
|
||||
)
|
||||
.await;
|
||||
client_a.fs.as_fake().set_index_for_repo(
|
||||
Path::new("/dir/.git"),
|
||||
&[(Path::new("a.txt"), diff_base.clone())],
|
||||
);
|
||||
|
||||
// Create the buffer
|
||||
let buffer_local_a = project_local
|
||||
|
@ -2464,14 +2460,10 @@ async fn test_git_diff_base_change(
|
|||
);
|
||||
});
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.as_fake()
|
||||
.set_index_for_repo(
|
||||
Path::new("/dir/.git"),
|
||||
&[(Path::new("a.txt"), new_diff_base.clone())],
|
||||
)
|
||||
.await;
|
||||
client_a.fs.as_fake().set_index_for_repo(
|
||||
Path::new("/dir/.git"),
|
||||
&[(Path::new("a.txt"), new_diff_base.clone())],
|
||||
);
|
||||
|
||||
// Wait for buffer_local_a to receive it
|
||||
deterministic.run_until_parked();
|
||||
|
@ -2513,14 +2505,10 @@ async fn test_git_diff_base_change(
|
|||
"
|
||||
.unindent();
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.as_fake()
|
||||
.set_index_for_repo(
|
||||
Path::new("/dir/sub/.git"),
|
||||
&[(Path::new("b.txt"), diff_base.clone())],
|
||||
)
|
||||
.await;
|
||||
client_a.fs.as_fake().set_index_for_repo(
|
||||
Path::new("/dir/sub/.git"),
|
||||
&[(Path::new("b.txt"), diff_base.clone())],
|
||||
);
|
||||
|
||||
// Create the buffer
|
||||
let buffer_local_b = project_local
|
||||
|
@ -2562,14 +2550,10 @@ async fn test_git_diff_base_change(
|
|||
);
|
||||
});
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.as_fake()
|
||||
.set_index_for_repo(
|
||||
Path::new("/dir/sub/.git"),
|
||||
&[(Path::new("b.txt"), new_diff_base.clone())],
|
||||
)
|
||||
.await;
|
||||
client_a.fs.as_fake().set_index_for_repo(
|
||||
Path::new("/dir/sub/.git"),
|
||||
&[(Path::new("b.txt"), new_diff_base.clone())],
|
||||
);
|
||||
|
||||
// Wait for buffer_local_b to receive it
|
||||
deterministic.run_until_parked();
|
||||
|
@ -2646,8 +2630,7 @@ async fn test_git_branch_name(
|
|||
client_a
|
||||
.fs
|
||||
.as_fake()
|
||||
.set_branch_name(Path::new("/dir/.git"), Some("branch-1"))
|
||||
.await;
|
||||
.set_branch_name(Path::new("/dir/.git"), Some("branch-1"));
|
||||
|
||||
// Wait for it to catch up to the new branch
|
||||
deterministic.run_until_parked();
|
||||
|
@ -2673,8 +2656,7 @@ async fn test_git_branch_name(
|
|||
client_a
|
||||
.fs
|
||||
.as_fake()
|
||||
.set_branch_name(Path::new("/dir/.git"), Some("branch-2"))
|
||||
.await;
|
||||
.set_branch_name(Path::new("/dir/.git"), Some("branch-2"));
|
||||
|
||||
// Wait for buffer_local_a to receive it
|
||||
deterministic.run_until_parked();
|
||||
|
@ -2726,17 +2708,13 @@ async fn test_git_status_sync(
|
|||
const A_TXT: &'static str = "a.txt";
|
||||
const B_TXT: &'static str = "b.txt";
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.as_fake()
|
||||
.set_status_for_repo(
|
||||
Path::new("/dir/.git"),
|
||||
&[
|
||||
(&Path::new(A_TXT), GitFileStatus::Added),
|
||||
(&Path::new(B_TXT), GitFileStatus::Added),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
client_a.fs.as_fake().set_status_for_repo_via_git_operation(
|
||||
Path::new("/dir/.git"),
|
||||
&[
|
||||
(&Path::new(A_TXT), GitFileStatus::Added),
|
||||
(&Path::new(B_TXT), GitFileStatus::Added),
|
||||
],
|
||||
);
|
||||
|
||||
let (project_local, _worktree_id) = client_a.build_local_project("/dir", cx_a).await;
|
||||
let project_id = active_call_a
|
||||
|
@ -2763,8 +2741,7 @@ async fn test_git_status_sync(
|
|||
assert_eq!(worktrees.len(), 1);
|
||||
let worktree = worktrees[0].clone();
|
||||
let snapshot = worktree.read(cx).snapshot();
|
||||
let root_entry = snapshot.root_git_entry().unwrap();
|
||||
assert_eq!(root_entry.status_for_file(&snapshot, file), status);
|
||||
assert_eq!(snapshot.status_for_file(file), status);
|
||||
}
|
||||
|
||||
// Smoke test status reading
|
||||
|
@ -2780,14 +2757,13 @@ async fn test_git_status_sync(
|
|||
client_a
|
||||
.fs
|
||||
.as_fake()
|
||||
.set_status_for_repo(
|
||||
.set_status_for_repo_via_working_copy_change(
|
||||
Path::new("/dir/.git"),
|
||||
&[
|
||||
(&Path::new(A_TXT), GitFileStatus::Modified),
|
||||
(&Path::new(B_TXT), GitFileStatus::Modified),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
);
|
||||
|
||||
// Wait for buffer_local_a to receive it
|
||||
deterministic.run_until_parked();
|
||||
|
|
|
@ -422,7 +422,7 @@ async fn apply_client_operation(
|
|||
);
|
||||
|
||||
ensure_project_shared(&project, client, cx).await;
|
||||
if !client.fs.paths().contains(&new_root_path) {
|
||||
if !client.fs.paths(false).contains(&new_root_path) {
|
||||
client.fs.create_dir(&new_root_path).await.unwrap();
|
||||
}
|
||||
project
|
||||
|
@ -628,12 +628,13 @@ async fn apply_client_operation(
|
|||
|
||||
ensure_project_shared(&project, client, cx).await;
|
||||
let requested_version = buffer.read_with(cx, |buffer, _| buffer.version());
|
||||
let save = project.update(cx, |project, cx| project.save_buffer(buffer, cx));
|
||||
let save = cx.background().spawn(async move {
|
||||
let (saved_version, _, _) = save
|
||||
.await
|
||||
let save = project.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx));
|
||||
let save = cx.spawn(|cx| async move {
|
||||
save.await
|
||||
.map_err(|err| anyhow!("save request failed: {:?}", err))?;
|
||||
assert!(saved_version.observed_all(&requested_version));
|
||||
assert!(buffer
|
||||
.read_with(&cx, |buffer, _| { buffer.saved_version().to_owned() })
|
||||
.observed_all(&requested_version));
|
||||
anyhow::Ok(())
|
||||
});
|
||||
if detach {
|
||||
|
@ -743,7 +744,7 @@ async fn apply_client_operation(
|
|||
} => {
|
||||
if !client
|
||||
.fs
|
||||
.directories()
|
||||
.directories(false)
|
||||
.contains(&path.parent().unwrap().to_owned())
|
||||
{
|
||||
return Err(TestError::Inapplicable);
|
||||
|
@ -770,10 +771,16 @@ async fn apply_client_operation(
|
|||
repo_path,
|
||||
contents,
|
||||
} => {
|
||||
if !client.fs.directories().contains(&repo_path) {
|
||||
if !client.fs.directories(false).contains(&repo_path) {
|
||||
return Err(TestError::Inapplicable);
|
||||
}
|
||||
|
||||
for (path, _) in contents.iter() {
|
||||
if !client.fs.files().contains(&repo_path.join(path)) {
|
||||
return Err(TestError::Inapplicable);
|
||||
}
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"{}: writing git index for repo {:?}: {:?}",
|
||||
client.username,
|
||||
|
@ -789,13 +796,13 @@ async fn apply_client_operation(
|
|||
if client.fs.metadata(&dot_git_dir).await?.is_none() {
|
||||
client.fs.create_dir(&dot_git_dir).await?;
|
||||
}
|
||||
client.fs.set_index_for_repo(&dot_git_dir, &contents).await;
|
||||
client.fs.set_index_for_repo(&dot_git_dir, &contents);
|
||||
}
|
||||
GitOperation::WriteGitBranch {
|
||||
repo_path,
|
||||
new_branch,
|
||||
} => {
|
||||
if !client.fs.directories().contains(&repo_path) {
|
||||
if !client.fs.directories(false).contains(&repo_path) {
|
||||
return Err(TestError::Inapplicable);
|
||||
}
|
||||
|
||||
|
@ -810,15 +817,21 @@ async fn apply_client_operation(
|
|||
if client.fs.metadata(&dot_git_dir).await?.is_none() {
|
||||
client.fs.create_dir(&dot_git_dir).await?;
|
||||
}
|
||||
client.fs.set_branch_name(&dot_git_dir, new_branch).await;
|
||||
client.fs.set_branch_name(&dot_git_dir, new_branch);
|
||||
}
|
||||
GitOperation::WriteGitStatuses {
|
||||
repo_path,
|
||||
statuses,
|
||||
git_operation,
|
||||
} => {
|
||||
if !client.fs.directories().contains(&repo_path) {
|
||||
if !client.fs.directories(false).contains(&repo_path) {
|
||||
return Err(TestError::Inapplicable);
|
||||
}
|
||||
for (path, _) in statuses.iter() {
|
||||
if !client.fs.files().contains(&repo_path.join(path)) {
|
||||
return Err(TestError::Inapplicable);
|
||||
}
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"{}: writing git statuses for repo {:?}: {:?}",
|
||||
|
@ -838,10 +851,16 @@ async fn apply_client_operation(
|
|||
client.fs.create_dir(&dot_git_dir).await?;
|
||||
}
|
||||
|
||||
client
|
||||
.fs
|
||||
.set_status_for_repo(&dot_git_dir, statuses.as_slice())
|
||||
.await;
|
||||
if git_operation {
|
||||
client
|
||||
.fs
|
||||
.set_status_for_repo_via_git_operation(&dot_git_dir, statuses.as_slice());
|
||||
} else {
|
||||
client.fs.set_status_for_repo_via_working_copy_change(
|
||||
&dot_git_dir,
|
||||
statuses.as_slice(),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@ -913,9 +932,10 @@ fn check_consistency_between_clients(clients: &[(Rc<TestClient>, TestAppContext)
|
|||
assert_eq!(
|
||||
guest_snapshot.entries(false).collect::<Vec<_>>(),
|
||||
host_snapshot.entries(false).collect::<Vec<_>>(),
|
||||
"{} has different snapshot than the host for worktree {:?} and project {:?}",
|
||||
"{} has different snapshot than the host for worktree {:?} ({:?}) and project {:?}",
|
||||
client.username,
|
||||
host_snapshot.abs_path(),
|
||||
id,
|
||||
guest_project.remote_id(),
|
||||
);
|
||||
assert_eq!(guest_snapshot.repositories().collect::<Vec<_>>(), host_snapshot.repositories().collect::<Vec<_>>(),
|
||||
|
@ -1230,6 +1250,7 @@ enum GitOperation {
|
|||
WriteGitStatuses {
|
||||
repo_path: PathBuf,
|
||||
statuses: Vec<(PathBuf, GitFileStatus)>,
|
||||
git_operation: bool,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -1575,7 +1596,7 @@ impl TestPlan {
|
|||
.choose(&mut self.rng)
|
||||
.cloned() else { continue };
|
||||
let project_root_name = root_name_for_project(&project, cx);
|
||||
let mut paths = client.fs.paths();
|
||||
let mut paths = client.fs.paths(false);
|
||||
paths.remove(0);
|
||||
let new_root_path = if paths.is_empty() || self.rng.gen() {
|
||||
Path::new("/").join(&self.next_root_dir_name(user_id))
|
||||
|
@ -1755,7 +1776,7 @@ impl TestPlan {
|
|||
let is_dir = self.rng.gen::<bool>();
|
||||
let content;
|
||||
let mut path;
|
||||
let dir_paths = client.fs.directories();
|
||||
let dir_paths = client.fs.directories(false);
|
||||
|
||||
if is_dir {
|
||||
content = String::new();
|
||||
|
@ -1809,7 +1830,7 @@ impl TestPlan {
|
|||
|
||||
let repo_path = client
|
||||
.fs
|
||||
.directories()
|
||||
.directories(false)
|
||||
.choose(&mut self.rng)
|
||||
.unwrap()
|
||||
.clone();
|
||||
|
@ -1855,9 +1876,12 @@ impl TestPlan {
|
|||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let git_operation = self.rng.gen::<bool>();
|
||||
|
||||
GitOperation::WriteGitStatuses {
|
||||
repo_path,
|
||||
statuses,
|
||||
git_operation,
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
|
|
|
@ -430,7 +430,7 @@ impl ProjectDiagnosticsEditor {
|
|||
});
|
||||
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.remove_blocks(blocks_to_remove, cx);
|
||||
editor.remove_blocks(blocks_to_remove, None, cx);
|
||||
let block_ids = editor.insert_blocks(
|
||||
blocks_to_add.into_iter().map(|block| {
|
||||
let (excerpt_id, text_anchor) = block.position;
|
||||
|
@ -442,6 +442,7 @@ impl ProjectDiagnosticsEditor {
|
|||
disposition: block.disposition,
|
||||
}
|
||||
}),
|
||||
Some(Autoscroll::fit()),
|
||||
cx,
|
||||
);
|
||||
|
||||
|
|
|
@ -31,13 +31,11 @@ use copilot::Copilot;
|
|||
pub use display_map::DisplayPoint;
|
||||
use display_map::*;
|
||||
pub use editor_settings::EditorSettings;
|
||||
pub use element::RenderExcerptHeaderParams;
|
||||
pub use element::{
|
||||
Cursor, EditorElement, HighlightedRange, HighlightedRangeLine, LineWithInvisibles,
|
||||
};
|
||||
use futures::FutureExt;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::LayoutContext;
|
||||
use gpui::{
|
||||
actions,
|
||||
color::Color,
|
||||
|
@ -511,7 +509,6 @@ pub struct Editor {
|
|||
mode: EditorMode,
|
||||
show_gutter: bool,
|
||||
placeholder_text: Option<Arc<str>>,
|
||||
render_excerpt_header: Option<element::RenderExcerptHeader>,
|
||||
highlighted_rows: Option<Range<u32>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
background_highlights: BTreeMap<TypeId, (fn(&Theme) -> Color, Vec<Range<Anchor>>)>,
|
||||
|
@ -1317,7 +1314,6 @@ impl Editor {
|
|||
mode,
|
||||
show_gutter: mode == EditorMode::Full,
|
||||
placeholder_text: None,
|
||||
render_excerpt_header: None,
|
||||
highlighted_rows: None,
|
||||
background_highlights: Default::default(),
|
||||
nav_history: None,
|
||||
|
@ -2169,8 +2165,8 @@ impl Editor {
|
|||
self.transact(cx, |this, cx| {
|
||||
let (edits, selection_fixup_info): (Vec<_>, Vec<_>) = {
|
||||
let selections = this.selections.all::<usize>(cx);
|
||||
|
||||
let buffer = this.buffer.read(cx).snapshot(cx);
|
||||
let multi_buffer = this.buffer.read(cx);
|
||||
let buffer = multi_buffer.snapshot(cx);
|
||||
selections
|
||||
.iter()
|
||||
.map(|selection| {
|
||||
|
@ -2181,58 +2177,74 @@ impl Editor {
|
|||
let end = selection.end;
|
||||
let is_cursor = start == end;
|
||||
let language_scope = buffer.language_scope_at(start);
|
||||
let (comment_delimiter, insert_extra_newline) =
|
||||
if let Some(language) = &language_scope {
|
||||
let leading_whitespace_len = buffer
|
||||
.reversed_chars_at(start)
|
||||
.take_while(|c| c.is_whitespace() && *c != '\n')
|
||||
.map(|c| c.len_utf8())
|
||||
.sum::<usize>();
|
||||
let (comment_delimiter, insert_extra_newline) = if let Some(language) =
|
||||
&language_scope
|
||||
{
|
||||
let leading_whitespace_len = buffer
|
||||
.reversed_chars_at(start)
|
||||
.take_while(|c| c.is_whitespace() && *c != '\n')
|
||||
.map(|c| c.len_utf8())
|
||||
.sum::<usize>();
|
||||
|
||||
let trailing_whitespace_len = buffer
|
||||
.chars_at(end)
|
||||
.take_while(|c| c.is_whitespace() && *c != '\n')
|
||||
.map(|c| c.len_utf8())
|
||||
.sum::<usize>();
|
||||
let trailing_whitespace_len = buffer
|
||||
.chars_at(end)
|
||||
.take_while(|c| c.is_whitespace() && *c != '\n')
|
||||
.map(|c| c.len_utf8())
|
||||
.sum::<usize>();
|
||||
|
||||
let insert_extra_newline =
|
||||
language.brackets().any(|(pair, enabled)| {
|
||||
let pair_start = pair.start.trim_end();
|
||||
let pair_end = pair.end.trim_start();
|
||||
let insert_extra_newline =
|
||||
language.brackets().any(|(pair, enabled)| {
|
||||
let pair_start = pair.start.trim_end();
|
||||
let pair_end = pair.end.trim_start();
|
||||
|
||||
enabled
|
||||
&& pair.newline
|
||||
&& buffer.contains_str_at(
|
||||
end + trailing_whitespace_len,
|
||||
pair_end,
|
||||
)
|
||||
&& buffer.contains_str_at(
|
||||
(start - leading_whitespace_len)
|
||||
.saturating_sub(pair_start.len()),
|
||||
pair_start,
|
||||
)
|
||||
});
|
||||
// Comment extension on newline is allowed only for cursor selections
|
||||
let comment_delimiter =
|
||||
language.line_comment_prefix().filter(|_| is_cursor);
|
||||
let comment_delimiter = if let Some(delimiter) = comment_delimiter {
|
||||
buffer
|
||||
.buffer_line_for_row(start_point.row)
|
||||
.is_some_and(|(snapshot, range)| {
|
||||
snapshot
|
||||
.chars_for_range(range)
|
||||
.skip_while(|c| c.is_whitespace())
|
||||
.take(delimiter.len())
|
||||
.eq(delimiter.chars())
|
||||
})
|
||||
.then(|| delimiter.clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
(comment_delimiter, insert_extra_newline)
|
||||
enabled
|
||||
&& pair.newline
|
||||
&& buffer.contains_str_at(
|
||||
end + trailing_whitespace_len,
|
||||
pair_end,
|
||||
)
|
||||
&& buffer.contains_str_at(
|
||||
(start - leading_whitespace_len)
|
||||
.saturating_sub(pair_start.len()),
|
||||
pair_start,
|
||||
)
|
||||
});
|
||||
// Comment extension on newline is allowed only for cursor selections
|
||||
let comment_delimiter = language.line_comment_prefix().filter(|_| {
|
||||
let is_comment_extension_enabled =
|
||||
multi_buffer.settings_at(0, cx).extend_comment_on_newline;
|
||||
is_cursor && is_comment_extension_enabled
|
||||
});
|
||||
let comment_delimiter = if let Some(delimiter) = comment_delimiter {
|
||||
buffer
|
||||
.buffer_line_for_row(start_point.row)
|
||||
.is_some_and(|(snapshot, range)| {
|
||||
let mut index_of_first_non_whitespace = 0;
|
||||
let line_starts_with_comment = snapshot
|
||||
.chars_for_range(range)
|
||||
.skip_while(|c| {
|
||||
let should_skip = c.is_whitespace();
|
||||
if should_skip {
|
||||
index_of_first_non_whitespace += 1;
|
||||
}
|
||||
should_skip
|
||||
})
|
||||
.take(delimiter.len())
|
||||
.eq(delimiter.chars());
|
||||
let cursor_is_placed_after_comment_marker =
|
||||
index_of_first_non_whitespace + delimiter.len()
|
||||
<= start_point.column as usize;
|
||||
line_starts_with_comment
|
||||
&& cursor_is_placed_after_comment_marker
|
||||
})
|
||||
.then(|| delimiter.clone())
|
||||
} else {
|
||||
(None, false)
|
||||
None
|
||||
};
|
||||
(comment_delimiter, insert_extra_newline)
|
||||
} else {
|
||||
(None, false)
|
||||
};
|
||||
|
||||
let capacity_for_delimiter = comment_delimiter
|
||||
.as_deref()
|
||||
|
@ -6256,6 +6268,7 @@ impl Editor {
|
|||
}),
|
||||
disposition: BlockDisposition::Below,
|
||||
}],
|
||||
Some(Autoscroll::fit()),
|
||||
cx,
|
||||
)[0];
|
||||
this.pending_rename = Some(RenameState {
|
||||
|
@ -6322,7 +6335,11 @@ impl Editor {
|
|||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<RenameState> {
|
||||
let rename = self.pending_rename.take()?;
|
||||
self.remove_blocks([rename.block_id].into_iter().collect(), cx);
|
||||
self.remove_blocks(
|
||||
[rename.block_id].into_iter().collect(),
|
||||
Some(Autoscroll::fit()),
|
||||
cx,
|
||||
);
|
||||
self.clear_text_highlights::<Rename>(cx);
|
||||
self.show_local_selections = true;
|
||||
|
||||
|
@ -6708,29 +6725,43 @@ impl Editor {
|
|||
pub fn insert_blocks(
|
||||
&mut self,
|
||||
blocks: impl IntoIterator<Item = BlockProperties<Anchor>>,
|
||||
autoscroll: Option<Autoscroll>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Vec<BlockId> {
|
||||
let blocks = self
|
||||
.display_map
|
||||
.update(cx, |display_map, cx| display_map.insert_blocks(blocks, cx));
|
||||
self.request_autoscroll(Autoscroll::fit(), cx);
|
||||
if let Some(autoscroll) = autoscroll {
|
||||
self.request_autoscroll(autoscroll, cx);
|
||||
}
|
||||
blocks
|
||||
}
|
||||
|
||||
pub fn replace_blocks(
|
||||
&mut self,
|
||||
blocks: HashMap<BlockId, RenderBlock>,
|
||||
autoscroll: Option<Autoscroll>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.display_map
|
||||
.update(cx, |display_map, _| display_map.replace_blocks(blocks));
|
||||
self.request_autoscroll(Autoscroll::fit(), cx);
|
||||
if let Some(autoscroll) = autoscroll {
|
||||
self.request_autoscroll(autoscroll, cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_blocks(&mut self, block_ids: HashSet<BlockId>, cx: &mut ViewContext<Self>) {
|
||||
pub fn remove_blocks(
|
||||
&mut self,
|
||||
block_ids: HashSet<BlockId>,
|
||||
autoscroll: Option<Autoscroll>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.display_map.update(cx, |display_map, cx| {
|
||||
display_map.remove_blocks(block_ids, cx)
|
||||
});
|
||||
if let Some(autoscroll) = autoscroll {
|
||||
self.request_autoscroll(autoscroll, cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn longest_row(&self, cx: &mut AppContext) -> u32 {
|
||||
|
@ -6811,20 +6842,6 @@ impl Editor {
|
|||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_render_excerpt_header(
|
||||
&mut self,
|
||||
render_excerpt_header: impl 'static
|
||||
+ Fn(
|
||||
&mut Editor,
|
||||
RenderExcerptHeaderParams,
|
||||
&mut LayoutContext<Editor>,
|
||||
) -> AnyElement<Editor>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.render_excerpt_header = Some(Arc::new(render_excerpt_header));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn reveal_in_finder(&mut self, _: &RevealInFinder, cx: &mut ViewContext<Self>) {
|
||||
if let Some(buffer) = self.buffer().read(cx).as_singleton() {
|
||||
if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) {
|
||||
|
@ -7432,6 +7449,7 @@ pub enum Event {
|
|||
},
|
||||
ScrollPositionChanged {
|
||||
local: bool,
|
||||
autoscroll: bool,
|
||||
},
|
||||
Closed,
|
||||
}
|
||||
|
@ -7463,12 +7481,8 @@ impl View for Editor {
|
|||
});
|
||||
}
|
||||
|
||||
let mut editor = EditorElement::new(style.clone());
|
||||
if let Some(render_excerpt_header) = self.render_excerpt_header.clone() {
|
||||
editor = editor.with_render_excerpt_header(render_excerpt_header);
|
||||
}
|
||||
Stack::new()
|
||||
.with_child(editor)
|
||||
.with_child(EditorElement::new(style.clone()))
|
||||
.with_child(ChildView::new(&self.mouse_context_menu, cx))
|
||||
.into_any()
|
||||
}
|
||||
|
|
|
@ -1732,17 +1732,40 @@ async fn test_newline_comments(cx: &mut gpui::TestAppContext) {
|
|||
},
|
||||
None,
|
||||
));
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
|
||||
cx.set_state(indoc! {"
|
||||
{
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
|
||||
cx.set_state(indoc! {"
|
||||
// Fooˇ
|
||||
"});
|
||||
|
||||
cx.update_editor(|e, cx| e.newline(&Newline, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
// Foo
|
||||
//ˇ
|
||||
"});
|
||||
// Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
|
||||
cx.set_state(indoc! {"
|
||||
ˇ// Foo
|
||||
"});
|
||||
cx.update_editor(|e, cx| e.newline(&Newline, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
|
||||
ˇ// Foo
|
||||
"});
|
||||
}
|
||||
// Ensure that comment continuations can be disabled.
|
||||
update_test_settings(cx, |settings| {
|
||||
settings.defaults.extend_comment_on_newline = Some(false);
|
||||
});
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.set_state(indoc! {"
|
||||
// Fooˇ
|
||||
"});
|
||||
cx.update_editor(|e, cx| e.newline(&Newline, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
// Foo
|
||||
//ˇ
|
||||
ˇ
|
||||
"});
|
||||
}
|
||||
|
||||
|
@ -2472,6 +2495,7 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
|
|||
height: 1,
|
||||
render: Arc::new(|_| Empty::new().into_any()),
|
||||
}],
|
||||
Some(Autoscroll::fit()),
|
||||
cx,
|
||||
);
|
||||
editor.change_selections(None, cx, |s| {
|
||||
|
|
|
@ -91,41 +91,17 @@ impl SelectionLayout {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct RenderExcerptHeaderParams<'a> {
|
||||
pub id: crate::ExcerptId,
|
||||
pub buffer: &'a language::BufferSnapshot,
|
||||
pub range: &'a crate::ExcerptRange<text::Anchor>,
|
||||
pub starts_new_buffer: bool,
|
||||
pub gutter_padding: f32,
|
||||
pub editor_style: &'a EditorStyle,
|
||||
}
|
||||
|
||||
pub type RenderExcerptHeader = Arc<
|
||||
dyn Fn(
|
||||
&mut Editor,
|
||||
RenderExcerptHeaderParams,
|
||||
&mut LayoutContext<Editor>,
|
||||
) -> AnyElement<Editor>,
|
||||
>;
|
||||
|
||||
pub struct EditorElement {
|
||||
style: Arc<EditorStyle>,
|
||||
render_excerpt_header: RenderExcerptHeader,
|
||||
}
|
||||
|
||||
impl EditorElement {
|
||||
pub fn new(style: EditorStyle) -> Self {
|
||||
Self {
|
||||
style: Arc::new(style),
|
||||
render_excerpt_header: Arc::new(render_excerpt_header),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_render_excerpt_header(mut self, render: RenderExcerptHeader) -> Self {
|
||||
self.render_excerpt_header = render;
|
||||
self
|
||||
}
|
||||
|
||||
fn attach_mouse_handlers(
|
||||
scene: &mut SceneBuilder,
|
||||
position_map: &Arc<PositionMap>,
|
||||
|
@ -1531,18 +1507,117 @@ impl EditorElement {
|
|||
range,
|
||||
starts_new_buffer,
|
||||
..
|
||||
} => (self.render_excerpt_header)(
|
||||
editor,
|
||||
RenderExcerptHeaderParams {
|
||||
id: *id,
|
||||
buffer,
|
||||
range,
|
||||
starts_new_buffer: *starts_new_buffer,
|
||||
gutter_padding,
|
||||
editor_style: style,
|
||||
},
|
||||
cx,
|
||||
),
|
||||
} => {
|
||||
let tooltip_style = theme::current(cx).tooltip.clone();
|
||||
let include_root = editor
|
||||
.project
|
||||
.as_ref()
|
||||
.map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
|
||||
.unwrap_or_default();
|
||||
let jump_icon = project::File::from_dyn(buffer.file()).map(|file| {
|
||||
let jump_path = ProjectPath {
|
||||
worktree_id: file.worktree_id(cx),
|
||||
path: file.path.clone(),
|
||||
};
|
||||
let jump_anchor = range
|
||||
.primary
|
||||
.as_ref()
|
||||
.map_or(range.context.start, |primary| primary.start);
|
||||
let jump_position = language::ToPoint::to_point(&jump_anchor, buffer);
|
||||
|
||||
enum JumpIcon {}
|
||||
MouseEventHandler::<JumpIcon, _>::new((*id).into(), cx, |state, _| {
|
||||
let style = style.jump_icon.style_for(state, false);
|
||||
Svg::new("icons/arrow_up_right_8.svg")
|
||||
.with_color(style.color)
|
||||
.constrained()
|
||||
.with_width(style.icon_width)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.constrained()
|
||||
.with_width(style.button_width)
|
||||
.with_height(style.button_width)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, editor, cx| {
|
||||
if let Some(workspace) = editor
|
||||
.workspace
|
||||
.as_ref()
|
||||
.and_then(|(workspace, _)| workspace.upgrade(cx))
|
||||
{
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
Editor::jump(
|
||||
workspace,
|
||||
jump_path.clone(),
|
||||
jump_position,
|
||||
jump_anchor,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
})
|
||||
.with_tooltip::<JumpIcon>(
|
||||
(*id).into(),
|
||||
"Jump to Buffer".to_string(),
|
||||
Some(Box::new(crate::OpenExcerpts)),
|
||||
tooltip_style.clone(),
|
||||
cx,
|
||||
)
|
||||
.aligned()
|
||||
.flex_float()
|
||||
});
|
||||
|
||||
if *starts_new_buffer {
|
||||
let editor_font_size = style.text.font_size;
|
||||
let style = &style.diagnostic_path_header;
|
||||
let font_size = (style.text_scale_factor * editor_font_size).round();
|
||||
|
||||
let path = buffer.resolve_file_path(cx, include_root);
|
||||
let mut filename = None;
|
||||
let mut parent_path = None;
|
||||
// Can't use .and_then() because `.file_name()` and `.parent()` return references :(
|
||||
if let Some(path) = path {
|
||||
filename = path.file_name().map(|f| f.to_string_lossy().to_string());
|
||||
parent_path =
|
||||
path.parent().map(|p| p.to_string_lossy().to_string() + "/");
|
||||
}
|
||||
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Label::new(
|
||||
filename.unwrap_or_else(|| "untitled".to_string()),
|
||||
style.filename.text.clone().with_font_size(font_size),
|
||||
)
|
||||
.contained()
|
||||
.with_style(style.filename.container)
|
||||
.aligned(),
|
||||
)
|
||||
.with_children(parent_path.map(|path| {
|
||||
Label::new(path, style.path.text.clone().with_font_size(font_size))
|
||||
.contained()
|
||||
.with_style(style.path.container)
|
||||
.aligned()
|
||||
}))
|
||||
.with_children(jump_icon)
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.with_padding_left(gutter_padding)
|
||||
.with_padding_right(gutter_padding)
|
||||
.expanded()
|
||||
.into_any_named("path header block")
|
||||
} else {
|
||||
let text_style = style.text.clone();
|
||||
Flex::row()
|
||||
.with_child(Label::new("⋯", text_style))
|
||||
.with_children(jump_icon)
|
||||
.contained()
|
||||
.with_padding_left(gutter_padding)
|
||||
.with_padding_right(gutter_padding)
|
||||
.expanded()
|
||||
.into_any_named("collapsed context")
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
element.layout(
|
||||
|
@ -2679,121 +2754,6 @@ impl HighlightedRange {
|
|||
}
|
||||
}
|
||||
|
||||
fn render_excerpt_header(
|
||||
editor: &mut Editor,
|
||||
RenderExcerptHeaderParams {
|
||||
id,
|
||||
buffer,
|
||||
range,
|
||||
starts_new_buffer,
|
||||
gutter_padding,
|
||||
editor_style,
|
||||
}: RenderExcerptHeaderParams,
|
||||
cx: &mut LayoutContext<Editor>,
|
||||
) -> AnyElement<Editor> {
|
||||
let tooltip_style = theme::current(cx).tooltip.clone();
|
||||
let include_root = editor
|
||||
.project
|
||||
.as_ref()
|
||||
.map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
|
||||
.unwrap_or_default();
|
||||
let jump_icon = project::File::from_dyn(buffer.file()).map(|file| {
|
||||
let jump_path = ProjectPath {
|
||||
worktree_id: file.worktree_id(cx),
|
||||
path: file.path.clone(),
|
||||
};
|
||||
let jump_anchor = range
|
||||
.primary
|
||||
.as_ref()
|
||||
.map_or(range.context.start, |primary| primary.start);
|
||||
let jump_position = language::ToPoint::to_point(&jump_anchor, buffer);
|
||||
|
||||
enum JumpIcon {}
|
||||
MouseEventHandler::<JumpIcon, _>::new(id.into(), cx, |state, _| {
|
||||
let style = editor_style.jump_icon.style_for(state, false);
|
||||
Svg::new("icons/arrow_up_right_8.svg")
|
||||
.with_color(style.color)
|
||||
.constrained()
|
||||
.with_width(style.icon_width)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.constrained()
|
||||
.with_width(style.button_width)
|
||||
.with_height(style.button_width)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, editor, cx| {
|
||||
if let Some(workspace) = editor
|
||||
.workspace
|
||||
.as_ref()
|
||||
.and_then(|(workspace, _)| workspace.upgrade(cx))
|
||||
{
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
Editor::jump(workspace, jump_path.clone(), jump_position, jump_anchor, cx);
|
||||
});
|
||||
}
|
||||
})
|
||||
.with_tooltip::<JumpIcon>(
|
||||
id.into(),
|
||||
"Jump to Buffer".to_string(),
|
||||
Some(Box::new(crate::OpenExcerpts)),
|
||||
tooltip_style.clone(),
|
||||
cx,
|
||||
)
|
||||
.aligned()
|
||||
.flex_float()
|
||||
});
|
||||
|
||||
if starts_new_buffer {
|
||||
let style = &editor_style.diagnostic_path_header;
|
||||
let font_size = (style.text_scale_factor * editor_style.text.font_size).round();
|
||||
|
||||
let path = buffer.resolve_file_path(cx, include_root);
|
||||
let mut filename = None;
|
||||
let mut parent_path = None;
|
||||
// Can't use .and_then() because `.file_name()` and `.parent()` return references :(
|
||||
if let Some(path) = path {
|
||||
filename = path.file_name().map(|f| f.to_string_lossy().to_string());
|
||||
parent_path = path.parent().map(|p| p.to_string_lossy().to_string() + "/");
|
||||
}
|
||||
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Label::new(
|
||||
filename.unwrap_or_else(|| "untitled".to_string()),
|
||||
style.filename.text.clone().with_font_size(font_size),
|
||||
)
|
||||
.contained()
|
||||
.with_style(style.filename.container)
|
||||
.aligned(),
|
||||
)
|
||||
.with_children(parent_path.map(|path| {
|
||||
Label::new(path, style.path.text.clone().with_font_size(font_size))
|
||||
.contained()
|
||||
.with_style(style.path.container)
|
||||
.aligned()
|
||||
}))
|
||||
.with_children(jump_icon)
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.with_padding_left(gutter_padding)
|
||||
.with_padding_right(gutter_padding)
|
||||
.expanded()
|
||||
.into_any_named("path header block")
|
||||
} else {
|
||||
let text_style = editor_style.text.clone();
|
||||
Flex::row()
|
||||
.with_child(Label::new("⋯", text_style))
|
||||
.with_children(jump_icon)
|
||||
.contained()
|
||||
.with_padding_left(gutter_padding)
|
||||
.with_padding_right(gutter_padding)
|
||||
.expanded()
|
||||
.into_any_named("collapsed context")
|
||||
}
|
||||
}
|
||||
|
||||
fn position_to_display_point(
|
||||
position: Vector2F,
|
||||
text_bounds: RectF,
|
||||
|
@ -2923,6 +2883,7 @@ mod tests {
|
|||
position: Anchor::min(),
|
||||
render: Arc::new(|_| Empty::new().into_any()),
|
||||
}],
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
|
||||
|
|
|
@ -294,7 +294,7 @@ impl FollowableItem for Editor {
|
|||
match event {
|
||||
Event::Edited => true,
|
||||
Event::SelectionsChanged { local } => *local,
|
||||
Event::ScrollPositionChanged { local } => *local,
|
||||
Event::ScrollPositionChanged { local, .. } => *local,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -173,6 +173,7 @@ impl ScrollManager {
|
|||
scroll_position: Vector2F,
|
||||
map: &DisplaySnapshot,
|
||||
local: bool,
|
||||
autoscroll: bool,
|
||||
workspace_id: Option<i64>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
|
@ -203,7 +204,7 @@ impl ScrollManager {
|
|||
)
|
||||
};
|
||||
|
||||
self.set_anchor(new_anchor, top_row, local, workspace_id, cx);
|
||||
self.set_anchor(new_anchor, top_row, local, autoscroll, workspace_id, cx);
|
||||
}
|
||||
|
||||
fn set_anchor(
|
||||
|
@ -211,11 +212,12 @@ impl ScrollManager {
|
|||
anchor: ScrollAnchor,
|
||||
top_row: u32,
|
||||
local: bool,
|
||||
autoscroll: bool,
|
||||
workspace_id: Option<i64>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
self.anchor = anchor;
|
||||
cx.emit(Event::ScrollPositionChanged { local });
|
||||
cx.emit(Event::ScrollPositionChanged { local, autoscroll });
|
||||
self.show_scrollbar(cx);
|
||||
self.autoscroll_request.take();
|
||||
if let Some(workspace_id) = workspace_id {
|
||||
|
@ -296,21 +298,28 @@ impl Editor {
|
|||
}
|
||||
|
||||
pub fn set_scroll_position(&mut self, scroll_position: Vector2F, cx: &mut ViewContext<Self>) {
|
||||
self.set_scroll_position_internal(scroll_position, true, cx);
|
||||
self.set_scroll_position_internal(scroll_position, true, false, cx);
|
||||
}
|
||||
|
||||
pub(crate) fn set_scroll_position_internal(
|
||||
&mut self,
|
||||
scroll_position: Vector2F,
|
||||
local: bool,
|
||||
autoscroll: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
|
||||
hide_hover(self, cx);
|
||||
let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1);
|
||||
self.scroll_manager
|
||||
.set_scroll_position(scroll_position, &map, local, workspace_id, cx);
|
||||
self.scroll_manager.set_scroll_position(
|
||||
scroll_position,
|
||||
&map,
|
||||
local,
|
||||
autoscroll,
|
||||
workspace_id,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn scroll_position(&self, cx: &mut ViewContext<Self>) -> Vector2F {
|
||||
|
@ -326,7 +335,7 @@ impl Editor {
|
|||
.to_point(&self.buffer().read(cx).snapshot(cx))
|
||||
.row;
|
||||
self.scroll_manager
|
||||
.set_anchor(scroll_anchor, top_row, true, workspace_id, cx);
|
||||
.set_anchor(scroll_anchor, top_row, true, false, workspace_id, cx);
|
||||
}
|
||||
|
||||
pub(crate) fn set_scroll_anchor_remote(
|
||||
|
@ -341,7 +350,7 @@ impl Editor {
|
|||
.to_point(&self.buffer().read(cx).snapshot(cx))
|
||||
.row;
|
||||
self.scroll_manager
|
||||
.set_anchor(scroll_anchor, top_row, false, workspace_id, cx);
|
||||
.set_anchor(scroll_anchor, top_row, false, false, workspace_id, cx);
|
||||
}
|
||||
|
||||
pub fn scroll_screen(&mut self, amount: &ScrollAmount, cx: &mut ViewContext<Self>) {
|
||||
|
|
|
@ -136,23 +136,23 @@ impl Editor {
|
|||
|
||||
if target_top < start_row {
|
||||
scroll_position.set_y(target_top);
|
||||
self.set_scroll_position_internal(scroll_position, local, cx);
|
||||
self.set_scroll_position_internal(scroll_position, local, true, cx);
|
||||
} else if target_bottom >= end_row {
|
||||
scroll_position.set_y(target_bottom - visible_lines);
|
||||
self.set_scroll_position_internal(scroll_position, local, cx);
|
||||
self.set_scroll_position_internal(scroll_position, local, true, cx);
|
||||
}
|
||||
}
|
||||
AutoscrollStrategy::Center => {
|
||||
scroll_position.set_y((first_cursor_top - margin).max(0.0));
|
||||
self.set_scroll_position_internal(scroll_position, local, cx);
|
||||
self.set_scroll_position_internal(scroll_position, local, true, cx);
|
||||
}
|
||||
AutoscrollStrategy::Top => {
|
||||
scroll_position.set_y((first_cursor_top).max(0.0));
|
||||
self.set_scroll_position_internal(scroll_position, local, cx);
|
||||
self.set_scroll_position_internal(scroll_position, local, true, cx);
|
||||
}
|
||||
AutoscrollStrategy::Bottom => {
|
||||
scroll_position.set_y((last_cursor_bottom - visible_lines).max(0.0));
|
||||
self.set_scroll_position_internal(scroll_position, local, cx);
|
||||
self.set_scroll_position_internal(scroll_position, local, true, cx);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,8 @@ lsp = { path = "../lsp" }
|
|||
rope = { path = "../rope" }
|
||||
util = { path = "../util" }
|
||||
sum_tree = { path = "../sum_tree" }
|
||||
rpc = { path = "../rpc" }
|
||||
|
||||
anyhow.workspace = true
|
||||
async-trait.workspace = true
|
||||
futures.workspace = true
|
||||
|
|
|
@ -29,6 +29,8 @@ use collections::{btree_map, BTreeMap};
|
|||
#[cfg(any(test, feature = "test-support"))]
|
||||
use repository::{FakeGitRepositoryState, GitFileStatus};
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use std::ffi::OsStr;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use std::sync::Weak;
|
||||
|
||||
lazy_static! {
|
||||
|
@ -501,6 +503,11 @@ impl FakeFsState {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
lazy_static! {
|
||||
pub static ref FS_DOT_GIT: &'static OsStr = OsStr::new(".git");
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl FakeFs {
|
||||
pub fn new(executor: Arc<gpui::executor::Background>) -> Arc<Self> {
|
||||
|
@ -619,7 +626,7 @@ impl FakeFs {
|
|||
.boxed()
|
||||
}
|
||||
|
||||
pub fn with_git_state<F>(&self, dot_git: &Path, f: F)
|
||||
pub fn with_git_state<F>(&self, dot_git: &Path, emit_git_event: bool, f: F)
|
||||
where
|
||||
F: FnOnce(&mut FakeGitRepositoryState),
|
||||
{
|
||||
|
@ -633,18 +640,22 @@ impl FakeFs {
|
|||
|
||||
f(&mut repo_state);
|
||||
|
||||
state.emit_event([dot_git]);
|
||||
if emit_git_event {
|
||||
state.emit_event([dot_git]);
|
||||
}
|
||||
} else {
|
||||
panic!("not a directory");
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn set_branch_name(&self, dot_git: &Path, branch: Option<impl Into<String>>) {
|
||||
self.with_git_state(dot_git, |state| state.branch_name = branch.map(Into::into))
|
||||
pub fn set_branch_name(&self, dot_git: &Path, branch: Option<impl Into<String>>) {
|
||||
self.with_git_state(dot_git, true, |state| {
|
||||
state.branch_name = branch.map(Into::into)
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn set_index_for_repo(&self, dot_git: &Path, head_state: &[(&Path, String)]) {
|
||||
self.with_git_state(dot_git, |state| {
|
||||
pub fn set_index_for_repo(&self, dot_git: &Path, head_state: &[(&Path, String)]) {
|
||||
self.with_git_state(dot_git, true, |state| {
|
||||
state.index_contents.clear();
|
||||
state.index_contents.extend(
|
||||
head_state
|
||||
|
@ -654,8 +665,32 @@ impl FakeFs {
|
|||
});
|
||||
}
|
||||
|
||||
pub async fn set_status_for_repo(&self, dot_git: &Path, statuses: &[(&Path, GitFileStatus)]) {
|
||||
self.with_git_state(dot_git, |state| {
|
||||
pub fn set_status_for_repo_via_working_copy_change(
|
||||
&self,
|
||||
dot_git: &Path,
|
||||
statuses: &[(&Path, GitFileStatus)],
|
||||
) {
|
||||
self.with_git_state(dot_git, false, |state| {
|
||||
state.worktree_statuses.clear();
|
||||
state.worktree_statuses.extend(
|
||||
statuses
|
||||
.iter()
|
||||
.map(|(path, content)| ((**path).into(), content.clone())),
|
||||
);
|
||||
});
|
||||
self.state.lock().emit_event(
|
||||
statuses
|
||||
.iter()
|
||||
.map(|(path, _)| dot_git.parent().unwrap().join(path)),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn set_status_for_repo_via_git_operation(
|
||||
&self,
|
||||
dot_git: &Path,
|
||||
statuses: &[(&Path, GitFileStatus)],
|
||||
) {
|
||||
self.with_git_state(dot_git, true, |state| {
|
||||
state.worktree_statuses.clear();
|
||||
state.worktree_statuses.extend(
|
||||
statuses
|
||||
|
@ -665,7 +700,7 @@ impl FakeFs {
|
|||
});
|
||||
}
|
||||
|
||||
pub fn paths(&self) -> Vec<PathBuf> {
|
||||
pub fn paths(&self, include_dot_git: bool) -> Vec<PathBuf> {
|
||||
let mut result = Vec::new();
|
||||
let mut queue = collections::VecDeque::new();
|
||||
queue.push_back((PathBuf::from("/"), self.state.lock().root.clone()));
|
||||
|
@ -675,12 +710,18 @@ impl FakeFs {
|
|||
queue.push_back((path.join(name), entry.clone()));
|
||||
}
|
||||
}
|
||||
result.push(path);
|
||||
if include_dot_git
|
||||
|| !path
|
||||
.components()
|
||||
.any(|component| component.as_os_str() == *FS_DOT_GIT)
|
||||
{
|
||||
result.push(path);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub fn directories(&self) -> Vec<PathBuf> {
|
||||
pub fn directories(&self, include_dot_git: bool) -> Vec<PathBuf> {
|
||||
let mut result = Vec::new();
|
||||
let mut queue = collections::VecDeque::new();
|
||||
queue.push_back((PathBuf::from("/"), self.state.lock().root.clone()));
|
||||
|
@ -689,7 +730,13 @@ impl FakeFs {
|
|||
for (name, entry) in entries {
|
||||
queue.push_back((path.join(name), entry.clone()));
|
||||
}
|
||||
result.push(path);
|
||||
if include_dot_git
|
||||
|| !path
|
||||
.components()
|
||||
.any(|component| component.as_os_str() == *FS_DOT_GIT)
|
||||
{
|
||||
result.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use git2::ErrorCode;
|
||||
use parking_lot::Mutex;
|
||||
use rpc::proto;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
|
@ -24,7 +26,7 @@ pub trait GitRepository: Send {
|
|||
|
||||
fn statuses(&self) -> Option<TreeMap<RepoPath, GitFileStatus>>;
|
||||
|
||||
fn status(&self, path: &RepoPath) -> Option<GitFileStatus>;
|
||||
fn status(&self, path: &RepoPath) -> Result<Option<GitFileStatus>>;
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for dyn GitRepository {
|
||||
|
@ -91,9 +93,18 @@ impl GitRepository for LibGitRepository {
|
|||
Some(map)
|
||||
}
|
||||
|
||||
fn status(&self, path: &RepoPath) -> Option<GitFileStatus> {
|
||||
let status = self.status_file(path).log_err()?;
|
||||
read_status(status)
|
||||
fn status(&self, path: &RepoPath) -> Result<Option<GitFileStatus>> {
|
||||
let status = self.status_file(path);
|
||||
match status {
|
||||
Ok(status) => Ok(read_status(status)),
|
||||
Err(e) => {
|
||||
if e.code() == ErrorCode::NotFound {
|
||||
Ok(None)
|
||||
} else {
|
||||
Err(e.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -155,9 +166,9 @@ impl GitRepository for FakeGitRepository {
|
|||
Some(map)
|
||||
}
|
||||
|
||||
fn status(&self, path: &RepoPath) -> Option<GitFileStatus> {
|
||||
fn status(&self, path: &RepoPath) -> Result<Option<GitFileStatus>> {
|
||||
let state = self.state.lock();
|
||||
state.worktree_statuses.get(path).cloned()
|
||||
Ok(state.worktree_statuses.get(path).cloned())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -197,8 +208,51 @@ pub enum GitFileStatus {
|
|||
Conflict,
|
||||
}
|
||||
|
||||
impl GitFileStatus {
|
||||
pub fn merge(
|
||||
this: Option<GitFileStatus>,
|
||||
other: Option<GitFileStatus>,
|
||||
prefer_other: bool,
|
||||
) -> Option<GitFileStatus> {
|
||||
if prefer_other {
|
||||
return other;
|
||||
} else {
|
||||
match (this, other) {
|
||||
(Some(GitFileStatus::Conflict), _) | (_, Some(GitFileStatus::Conflict)) => {
|
||||
Some(GitFileStatus::Conflict)
|
||||
}
|
||||
(Some(GitFileStatus::Modified), _) | (_, Some(GitFileStatus::Modified)) => {
|
||||
Some(GitFileStatus::Modified)
|
||||
}
|
||||
(Some(GitFileStatus::Added), _) | (_, Some(GitFileStatus::Added)) => {
|
||||
Some(GitFileStatus::Added)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_proto(git_status: Option<i32>) -> Option<GitFileStatus> {
|
||||
git_status.and_then(|status| {
|
||||
proto::GitStatus::from_i32(status).map(|status| match status {
|
||||
proto::GitStatus::Added => GitFileStatus::Added,
|
||||
proto::GitStatus::Modified => GitFileStatus::Modified,
|
||||
proto::GitStatus::Conflict => GitFileStatus::Conflict,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_proto(self) -> i32 {
|
||||
match self {
|
||||
GitFileStatus::Added => proto::GitStatus::Added as i32,
|
||||
GitFileStatus::Modified => proto::GitStatus::Modified as i32,
|
||||
GitFileStatus::Conflict => proto::GitStatus::Conflict as i32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Ord, Hash, PartialOrd, Eq, PartialEq)]
|
||||
pub struct RepoPath(PathBuf);
|
||||
pub struct RepoPath(pub PathBuf);
|
||||
|
||||
impl RepoPath {
|
||||
pub fn new(path: PathBuf) -> Self {
|
||||
|
|
|
@ -51,6 +51,7 @@ pub struct LanguageSettings {
|
|||
pub enable_language_server: bool,
|
||||
pub show_copilot_suggestions: bool,
|
||||
pub show_whitespaces: ShowWhitespaceSetting,
|
||||
pub extend_comment_on_newline: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
|
@ -95,6 +96,8 @@ pub struct LanguageSettingsContent {
|
|||
pub show_copilot_suggestions: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub show_whitespaces: Option<ShowWhitespaceSetting>,
|
||||
#[serde(default)]
|
||||
pub extend_comment_on_newline: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
|
@ -340,7 +343,10 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent
|
|||
src.show_copilot_suggestions,
|
||||
);
|
||||
merge(&mut settings.show_whitespaces, src.show_whitespaces);
|
||||
|
||||
merge(
|
||||
&mut settings.extend_comment_on_newline,
|
||||
src.extend_comment_on_newline,
|
||||
);
|
||||
fn merge<T>(target: &mut T, value: Option<T>) {
|
||||
if let Some(value) = value {
|
||||
*target = value;
|
||||
|
|
|
@ -37,8 +37,8 @@ use language::{
|
|||
range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, CachedLspAdapter, CodeAction, CodeLabel,
|
||||
Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Event as BufferEvent, File as _,
|
||||
Language, LanguageRegistry, LanguageServerName, LocalFile, OffsetRangeExt, Operation, Patch,
|
||||
PendingLanguageServer, PointUtf16, RopeFingerprint, TextBufferSnapshot, ToOffset, ToPointUtf16,
|
||||
Transaction, Unclipped,
|
||||
PendingLanguageServer, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction,
|
||||
Unclipped,
|
||||
};
|
||||
use log::error;
|
||||
use lsp::{
|
||||
|
@ -69,7 +69,7 @@ use std::{
|
|||
atomic::{AtomicUsize, Ordering::SeqCst},
|
||||
Arc,
|
||||
},
|
||||
time::{Duration, Instant, SystemTime},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use terminals::Terminals;
|
||||
use util::{
|
||||
|
@ -1616,7 +1616,7 @@ impl Project {
|
|||
&self,
|
||||
buffer: ModelHandle<Buffer>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<(clock::Global, RopeFingerprint, SystemTime)>> {
|
||||
) -> Task<Result<()>> {
|
||||
let Some(file) = File::from_dyn(buffer.read(cx).file()) else {
|
||||
return Task::ready(Err(anyhow!("buffer doesn't have a file")));
|
||||
};
|
||||
|
@ -5156,9 +5156,9 @@ impl Project {
|
|||
return None;
|
||||
}
|
||||
let path = &project_path.path;
|
||||
changed_repos.iter().find(|(work_dir, change)| {
|
||||
path.starts_with(work_dir) && change.git_dir_changed
|
||||
})?;
|
||||
changed_repos
|
||||
.iter()
|
||||
.find(|(work_dir, _)| path.starts_with(work_dir))?;
|
||||
let receiver = receiver.clone();
|
||||
let path = path.clone();
|
||||
Some(async move {
|
||||
|
@ -5181,9 +5181,9 @@ impl Project {
|
|||
return None;
|
||||
}
|
||||
let path = file.path();
|
||||
changed_repos.iter().find(|(work_dir, change)| {
|
||||
path.starts_with(work_dir) && change.git_dir_changed
|
||||
})?;
|
||||
changed_repos
|
||||
.iter()
|
||||
.find(|(work_dir, _)| path.starts_with(work_dir))?;
|
||||
Some((buffer, path.clone()))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
@ -5984,16 +5984,15 @@ impl Project {
|
|||
.await?;
|
||||
let buffer_id = buffer.read_with(&cx, |buffer, _| buffer.remote_id());
|
||||
|
||||
let (saved_version, fingerprint, mtime) = this
|
||||
.update(&mut cx, |this, cx| this.save_buffer(buffer, cx))
|
||||
this.update(&mut cx, |this, cx| this.save_buffer(buffer.clone(), cx))
|
||||
.await?;
|
||||
Ok(proto::BufferSaved {
|
||||
Ok(buffer.read_with(&cx, |buffer, _| proto::BufferSaved {
|
||||
project_id,
|
||||
buffer_id,
|
||||
version: serialize_version(&saved_version),
|
||||
mtime: Some(mtime.into()),
|
||||
fingerprint: language::proto::serialize_fingerprint(fingerprint),
|
||||
})
|
||||
version: serialize_version(buffer.saved_version()),
|
||||
mtime: Some(buffer.saved_mtime().into()),
|
||||
fingerprint: language::proto::serialize_fingerprint(buffer.saved_version_fingerprint()),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn handle_reload_buffers(
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1002,6 +1002,7 @@ impl ProjectPanel {
|
|||
mtime: entry.mtime,
|
||||
is_symlink: false,
|
||||
is_ignored: false,
|
||||
git_status: entry.git_status,
|
||||
});
|
||||
}
|
||||
if expanded_dir_ids.binary_search(&entry.id).is_err()
|
||||
|
@ -1011,6 +1012,9 @@ impl ProjectPanel {
|
|||
}
|
||||
entry_iter.advance();
|
||||
}
|
||||
|
||||
snapshot.propagate_git_statuses(&mut visible_worktree_entries);
|
||||
|
||||
visible_worktree_entries.sort_by(|entry_a, entry_b| {
|
||||
let mut components_a = entry_a.path.components().peekable();
|
||||
let mut components_b = entry_b.path.components().peekable();
|
||||
|
@ -1108,14 +1112,8 @@ impl ProjectPanel {
|
|||
.unwrap_or(&[]);
|
||||
|
||||
let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
|
||||
for (entry, repo) in
|
||||
snapshot.entries_with_repositories(visible_worktree_entries[entry_range].iter())
|
||||
{
|
||||
let status = (git_status_setting
|
||||
&& entry.path.parent().is_some()
|
||||
&& !entry.is_ignored)
|
||||
.then(|| repo.and_then(|repo| repo.status_for_path(&snapshot, &entry.path)))
|
||||
.flatten();
|
||||
for entry in visible_worktree_entries[entry_range].iter() {
|
||||
let status = git_status_setting.then(|| entry.git_status).flatten();
|
||||
|
||||
let mut details = EntryDetails {
|
||||
filename: entry
|
||||
|
|
|
@ -1005,13 +1005,12 @@ message Entry {
|
|||
Timestamp mtime = 5;
|
||||
bool is_symlink = 6;
|
||||
bool is_ignored = 7;
|
||||
optional GitStatus git_status = 8;
|
||||
}
|
||||
|
||||
message RepositoryEntry {
|
||||
uint64 work_directory_id = 1;
|
||||
optional string branch = 2;
|
||||
repeated string removed_repo_paths = 3;
|
||||
repeated StatusEntry updated_statuses = 4;
|
||||
}
|
||||
|
||||
message StatusEntry {
|
||||
|
|
|
@ -6,4 +6,4 @@ pub use conn::Connection;
|
|||
pub use peer::*;
|
||||
mod macros;
|
||||
|
||||
pub const PROTOCOL_VERSION: u32 = 57;
|
||||
pub const PROTOCOL_VERSION: u32 = 58;
|
||||
|
|
|
@ -480,6 +480,11 @@ impl<T: Item> SumTree<T> {
|
|||
} => child_trees.last().unwrap().rightmost_leaf(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
pub fn _debug_entries(&self) -> Vec<&T> {
|
||||
self.iter().collect::<Vec<_>>()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Item + PartialEq> PartialEq for SumTree<T> {
|
||||
|
|
|
@ -57,7 +57,7 @@ pub fn post_inc<T: From<u8> + AddAssign<T> + Copy>(value: &mut T) -> T {
|
|||
}
|
||||
|
||||
/// Extend a sorted vector with a sorted sequence of items, maintaining the vector's sort order and
|
||||
/// enforcing a maximum length. Sort the items according to the given callback. Before calling this,
|
||||
/// enforcing a maximum length. This also de-duplicates items. Sort the items according to the given callback. Before calling this,
|
||||
/// both `vec` and `new_items` should already be sorted according to the `cmp` comparator.
|
||||
pub fn extend_sorted<T, I, F>(vec: &mut Vec<T>, new_items: I, limit: usize, mut cmp: F)
|
||||
where
|
||||
|
|
|
@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
|
|||
description = "The fast, collaborative code editor."
|
||||
edition = "2021"
|
||||
name = "zed"
|
||||
version = "0.90.0"
|
||||
version = "0.90.2"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
|
|
|
@ -1 +1 @@
|
|||
dev
|
||||
stable
|
|
@ -31,7 +31,6 @@ use std::{
|
|||
ffi::OsStr,
|
||||
fs::OpenOptions,
|
||||
io::Write as _,
|
||||
ops::Not,
|
||||
os::unix::prelude::OsStrExt,
|
||||
panic,
|
||||
path::{Path, PathBuf},
|
||||
|
@ -373,7 +372,6 @@ struct Panic {
|
|||
os_version: Option<String>,
|
||||
architecture: String,
|
||||
panicked_on: u128,
|
||||
identifying_backtrace: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
|
@ -401,61 +399,18 @@ fn init_panic_hook(app: &App) {
|
|||
.unwrap_or_else(|| "Box<Any>".to_string());
|
||||
|
||||
let backtrace = Backtrace::new();
|
||||
let backtrace = backtrace
|
||||
let mut backtrace = backtrace
|
||||
.frames()
|
||||
.iter()
|
||||
.filter_map(|frame| {
|
||||
let symbol = frame.symbols().first()?;
|
||||
let path = symbol.filename()?;
|
||||
Some((path, symbol.lineno(), format!("{:#}", symbol.name()?)))
|
||||
})
|
||||
.filter_map(|frame| Some(format!("{:#}", frame.symbols().first()?.name()?)))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let this_file_path = Path::new(file!());
|
||||
|
||||
// Find the first frame in the backtrace for this panic hook itself. Exclude
|
||||
// that frame and all frames before it.
|
||||
let mut start_frame_ix = 0;
|
||||
let mut codebase_root_path = None;
|
||||
for (ix, (path, _, _)) in backtrace.iter().enumerate() {
|
||||
if path.ends_with(this_file_path) {
|
||||
start_frame_ix = ix + 1;
|
||||
codebase_root_path = path.ancestors().nth(this_file_path.components().count());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Exclude any subsequent frames inside of rust's panic handling system.
|
||||
while let Some((path, _, _)) = backtrace.get(start_frame_ix) {
|
||||
if path.starts_with("/rustc") {
|
||||
start_frame_ix += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Build two backtraces:
|
||||
// * one for display, which includes symbol names for all frames, and files
|
||||
// and line numbers for symbols in this codebase
|
||||
// * one for identification and de-duplication, which only includes symbol
|
||||
// names for symbols in this codebase.
|
||||
let mut display_backtrace = Vec::new();
|
||||
let mut identifying_backtrace = Vec::new();
|
||||
for (path, line, symbol) in &backtrace[start_frame_ix..] {
|
||||
display_backtrace.push(symbol.clone());
|
||||
|
||||
if let Some(codebase_root_path) = &codebase_root_path {
|
||||
if let Ok(suffix) = path.strip_prefix(&codebase_root_path) {
|
||||
identifying_backtrace.push(symbol.clone());
|
||||
|
||||
let display_path = suffix.to_string_lossy();
|
||||
if let Some(line) = line {
|
||||
display_backtrace.push(format!(" {display_path}:{line}"));
|
||||
} else {
|
||||
display_backtrace.push(format!(" {display_path}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Strip out leading stack frames for rust panic-handling.
|
||||
if let Some(ix) = backtrace
|
||||
.iter()
|
||||
.position(|name| name == "rust_begin_unwind")
|
||||
{
|
||||
backtrace.drain(0..=ix);
|
||||
}
|
||||
|
||||
let panic_data = Panic {
|
||||
|
@ -477,29 +432,27 @@ fn init_panic_hook(app: &App) {
|
|||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis(),
|
||||
backtrace: display_backtrace,
|
||||
identifying_backtrace: identifying_backtrace
|
||||
.is_empty()
|
||||
.not()
|
||||
.then_some(identifying_backtrace),
|
||||
backtrace,
|
||||
};
|
||||
|
||||
if let Some(panic_data_json) = serde_json::to_string_pretty(&panic_data).log_err() {
|
||||
if is_pty {
|
||||
if is_pty {
|
||||
if let Some(panic_data_json) = serde_json::to_string_pretty(&panic_data).log_err() {
|
||||
eprintln!("{}", panic_data_json);
|
||||
return;
|
||||
}
|
||||
|
||||
let timestamp = chrono::Utc::now().format("%Y_%m_%d %H_%M_%S").to_string();
|
||||
let panic_file_path = paths::LOGS_DIR.join(format!("zed-{}.panic", timestamp));
|
||||
let panic_file = std::fs::OpenOptions::new()
|
||||
.append(true)
|
||||
.create(true)
|
||||
.open(&panic_file_path)
|
||||
.log_err();
|
||||
if let Some(mut panic_file) = panic_file {
|
||||
write!(&mut panic_file, "{}", panic_data_json).log_err();
|
||||
panic_file.flush().log_err();
|
||||
} else {
|
||||
if let Some(panic_data_json) = serde_json::to_string(&panic_data).log_err() {
|
||||
let timestamp = chrono::Utc::now().format("%Y_%m_%d %H_%M_%S").to_string();
|
||||
let panic_file_path = paths::LOGS_DIR.join(format!("zed-{}.panic", timestamp));
|
||||
let panic_file = std::fs::OpenOptions::new()
|
||||
.append(true)
|
||||
.create(true)
|
||||
.open(&panic_file_path)
|
||||
.log_err();
|
||||
if let Some(mut panic_file) = panic_file {
|
||||
writeln!(&mut panic_file, "{}", panic_data_json).log_err();
|
||||
panic_file.flush().log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
@ -531,23 +484,45 @@ fn upload_previous_panics(http: Arc<dyn HttpClient>, cx: &mut AppContext) {
|
|||
}
|
||||
|
||||
if telemetry_settings.diagnostics {
|
||||
let panic_data_text = smol::fs::read_to_string(&child_path)
|
||||
let panic_file_content = smol::fs::read_to_string(&child_path)
|
||||
.await
|
||||
.context("error reading panic file")?;
|
||||
|
||||
let body = serde_json::to_string(&PanicRequest {
|
||||
panic: serde_json::from_str(&panic_data_text)?,
|
||||
token: ZED_SECRET_CLIENT_TOKEN.into(),
|
||||
})
|
||||
.unwrap();
|
||||
let panic = serde_json::from_str(&panic_file_content)
|
||||
.ok()
|
||||
.or_else(|| {
|
||||
panic_file_content
|
||||
.lines()
|
||||
.next()
|
||||
.and_then(|line| serde_json::from_str(line).ok())
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
log::error!(
|
||||
"failed to deserialize panic file {:?}",
|
||||
panic_file_content
|
||||
);
|
||||
None
|
||||
});
|
||||
|
||||
let request = Request::post(&panic_report_url)
|
||||
.redirect_policy(isahc::config::RedirectPolicy::Follow)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(body.into())?;
|
||||
let response = http.send(request).await.context("error sending panic")?;
|
||||
if !response.status().is_success() {
|
||||
log::error!("Error uploading panic to server: {}", response.status());
|
||||
if let Some(panic) = panic {
|
||||
let body = serde_json::to_string(&PanicRequest {
|
||||
panic,
|
||||
token: ZED_SECRET_CLIENT_TOKEN.into(),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let request = Request::post(&panic_report_url)
|
||||
.redirect_policy(isahc::config::RedirectPolicy::Follow)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(body.into())?;
|
||||
let response =
|
||||
http.send(request).await.context("error sending panic")?;
|
||||
if !response.status().is_success() {
|
||||
log::error!(
|
||||
"Error uploading panic to server: {}",
|
||||
response.status()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -254,13 +254,6 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
|
|||
workspace.toggle_panel_focus::<TerminalPanel>(cx);
|
||||
},
|
||||
);
|
||||
cx.add_action(
|
||||
|workspace: &mut Workspace,
|
||||
_: &ai::assistant::ToggleFocus,
|
||||
cx: &mut ViewContext<Workspace>| {
|
||||
workspace.toggle_panel_focus::<AssistantPanel>(cx);
|
||||
},
|
||||
);
|
||||
cx.add_global_action({
|
||||
let app_state = Arc::downgrade(&app_state);
|
||||
move |_: &NewWindow, cx: &mut AppContext| {
|
||||
|
@ -366,9 +359,12 @@ pub fn initialize_workspace(
|
|||
|
||||
let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
|
||||
let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone());
|
||||
let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone());
|
||||
let (project_panel, terminal_panel, assistant_panel) =
|
||||
futures::try_join!(project_panel, terminal_panel, assistant_panel)?;
|
||||
let assistant_panel = if *util::channel::RELEASE_CHANNEL == ReleaseChannel::Stable {
|
||||
None
|
||||
} else {
|
||||
Some(AssistantPanel::load(workspace_handle.clone(), cx.clone()).await?)
|
||||
};
|
||||
let (project_panel, terminal_panel) = futures::try_join!(project_panel, terminal_panel)?;
|
||||
workspace_handle.update(&mut cx, |workspace, cx| {
|
||||
let project_panel_position = project_panel.position(cx);
|
||||
workspace.add_panel(project_panel, cx);
|
||||
|
@ -387,7 +383,9 @@ pub fn initialize_workspace(
|
|||
}
|
||||
|
||||
workspace.add_panel(terminal_panel, cx);
|
||||
workspace.add_panel(assistant_panel, cx);
|
||||
if let Some(assistant_panel) = assistant_panel {
|
||||
workspace.add_panel(assistant_panel, cx);
|
||||
}
|
||||
})?;
|
||||
Ok(())
|
||||
})
|
||||
|
|
|
@ -23,7 +23,7 @@ done
|
|||
export ZED_BUNDLE=true
|
||||
export MACOSX_DEPLOYMENT_TARGET=10.15.7
|
||||
|
||||
which cargo-bundle > /dev/null || cargo install cargo-bundle --version 0.5.0
|
||||
cargo install cargo-bundle --version 0.5.0
|
||||
rustup target add wasm32-wasi
|
||||
|
||||
# Deal with versions of macOS that don't include libstdc++ headers
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue