mirror of
https://github.com/zed-industries/zed.git
synced 2024-09-16 17:07:14 +03:00
Handle buffer diff base updates and file renames properly for SSH projects (#14989)
Release Notes: - N/A --------- Co-authored-by: Conrad <conrad@zed.dev>
This commit is contained in:
parent
ec093c390f
commit
38e3182bef
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -8771,6 +8771,7 @@ dependencies = [
|
||||
"settings",
|
||||
"smol",
|
||||
"toml 0.8.10",
|
||||
"util",
|
||||
"worktree",
|
||||
]
|
||||
|
||||
|
@ -2867,7 +2867,7 @@ fn make_lsp_adapter_delegate(
|
||||
project.update(cx, |project, cx| {
|
||||
// TODO: Find the right worktree.
|
||||
let worktree = project
|
||||
.worktrees()
|
||||
.worktrees(cx)
|
||||
.next()
|
||||
.ok_or_else(|| anyhow!("no worktrees when constructing ProjectLspAdapterDelegate"))?;
|
||||
Ok(ProjectLspAdapterDelegate::new(project, &worktree, cx) as Arc<dyn LspAdapterDelegate>)
|
||||
|
@ -284,7 +284,7 @@ fn collect_diagnostics(
|
||||
PathBuf::try_from(path)
|
||||
.ok()
|
||||
.and_then(|path| {
|
||||
project.read(cx).worktrees().find_map(|worktree| {
|
||||
project.read(cx).worktrees(cx).find_map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
let worktree_root_path = Path::new(worktree.root_name());
|
||||
let relative_path = path.strip_prefix(worktree_root_path).ok()?;
|
||||
|
@ -24,7 +24,7 @@ impl DocsSlashCommand {
|
||||
pub const NAME: &'static str = "docs";
|
||||
|
||||
fn path_to_cargo_toml(project: Model<Project>, cx: &mut AppContext) -> Option<Arc<Path>> {
|
||||
let worktree = project.read(cx).worktrees().next()?;
|
||||
let worktree = project.read(cx).worktrees(cx).next()?;
|
||||
let worktree = worktree.read(cx);
|
||||
let entry = worktree.entry_for_path("Cargo.toml")?;
|
||||
let path = ProjectPath {
|
||||
|
@ -188,7 +188,7 @@ fn collect_files(
|
||||
let project_handle = project.downgrade();
|
||||
let snapshots = project
|
||||
.read(cx)
|
||||
.worktrees()
|
||||
.worktrees(cx)
|
||||
.map(|worktree| worktree.read(cx).snapshot())
|
||||
.collect::<Vec<_>>();
|
||||
cx.spawn(|mut cx| async move {
|
||||
|
@ -75,7 +75,7 @@ impl ProjectSlashCommand {
|
||||
}
|
||||
|
||||
fn path_to_cargo_toml(project: Model<Project>, cx: &mut AppContext) -> Option<Arc<Path>> {
|
||||
let worktree = project.read(cx).worktrees().next()?;
|
||||
let worktree = project.read(cx).worktrees(cx).next()?;
|
||||
let worktree = worktree.read(cx);
|
||||
let entry = worktree.entry_for_path("Cargo.toml")?;
|
||||
let path = ProjectPath {
|
||||
|
@ -222,7 +222,7 @@ mod tests {
|
||||
|
||||
let worktree_ids = project.read_with(cx, |project, cx| {
|
||||
project
|
||||
.worktrees()
|
||||
.worktrees(cx)
|
||||
.map(|worktree| worktree.read(cx).id())
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
@ -526,7 +526,7 @@ impl Room {
|
||||
rejoined_projects.push(proto::RejoinProject {
|
||||
id: project_id,
|
||||
worktrees: project
|
||||
.worktrees()
|
||||
.worktrees(cx)
|
||||
.map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
proto::RejoinWorktree {
|
||||
|
@ -52,7 +52,7 @@ async fn test_channel_guests(
|
||||
assert!(project_b.read_with(cx_b, |project, _| project.is_read_only()));
|
||||
assert!(project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
let worktree_id = project.worktrees().next().unwrap().read(cx).id();
|
||||
let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
|
||||
project.create_entry((worktree_id, "b.txt"), false, cx)
|
||||
})
|
||||
.await
|
||||
|
@ -76,7 +76,7 @@ async fn test_host_disconnect(
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
|
||||
|
||||
let worktree_a = project_a.read_with(cx_a, |project, _| project.worktrees().next().unwrap());
|
||||
let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
|
||||
let project_id = active_call_a
|
||||
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||
.await
|
||||
@ -1144,7 +1144,7 @@ async fn test_share_project(
|
||||
});
|
||||
|
||||
project_b.read_with(cx_b, |project, cx| {
|
||||
let worktree = project.worktrees().next().unwrap().read(cx);
|
||||
let worktree = project.worktrees(cx).next().unwrap().read(cx);
|
||||
assert_eq!(
|
||||
worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
|
||||
[
|
||||
@ -1158,7 +1158,7 @@ async fn test_share_project(
|
||||
|
||||
project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
let worktree = project.worktrees().next().unwrap();
|
||||
let worktree = project.worktrees(cx).next().unwrap();
|
||||
let entry = worktree.read(cx).entry_for_path("ignored-dir").unwrap();
|
||||
project.expand_entry(worktree_id, entry.id, cx).unwrap()
|
||||
})
|
||||
@ -1166,7 +1166,7 @@ async fn test_share_project(
|
||||
.unwrap();
|
||||
|
||||
project_b.read_with(cx_b, |project, cx| {
|
||||
let worktree = project.worktrees().next().unwrap().read(cx);
|
||||
let worktree = project.worktrees(cx).next().unwrap().read(cx);
|
||||
assert_eq!(
|
||||
worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
|
||||
[
|
||||
|
@ -1377,7 +1377,7 @@ async fn test_unshare_project(
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let worktree_a = project_a.read_with(cx_a, |project, _| project.worktrees().next().unwrap());
|
||||
let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
|
||||
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
|
||||
executor.run_until_parked();
|
||||
|
||||
@ -1505,7 +1505,8 @@ async fn test_project_reconnect(
|
||||
let (project_a1, _) = client_a.build_local_project("/root-1/dir1", cx_a).await;
|
||||
let (project_a2, _) = client_a.build_local_project("/root-2", cx_a).await;
|
||||
let (project_a3, _) = client_a.build_local_project("/root-3", cx_a).await;
|
||||
let worktree_a1 = project_a1.read_with(cx_a, |project, _| project.worktrees().next().unwrap());
|
||||
let worktree_a1 =
|
||||
project_a1.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
|
||||
let project1_id = active_call_a
|
||||
.update(cx_a, |call, cx| call.share_project(project_a1.clone(), cx))
|
||||
.await
|
||||
@ -2308,7 +2309,7 @@ async fn test_propagate_saves_and_fs_changes(
|
||||
.await;
|
||||
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
|
||||
|
||||
let worktree_a = project_a.read_with(cx_a, |p, _| p.worktrees().next().unwrap());
|
||||
let worktree_a = project_a.read_with(cx_a, |p, cx| p.worktrees(cx).next().unwrap());
|
||||
let project_id = active_call_a
|
||||
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||
.await
|
||||
@ -2318,9 +2319,9 @@ async fn test_propagate_saves_and_fs_changes(
|
||||
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
|
||||
let project_c = client_c.build_dev_server_project(project_id, cx_c).await;
|
||||
|
||||
let worktree_b = project_b.read_with(cx_b, |p, _| p.worktrees().next().unwrap());
|
||||
let worktree_b = project_b.read_with(cx_b, |p, cx| p.worktrees(cx).next().unwrap());
|
||||
|
||||
let worktree_c = project_c.read_with(cx_c, |p, _| p.worktrees().next().unwrap());
|
||||
let worktree_c = project_c.read_with(cx_c, |p, cx| p.worktrees(cx).next().unwrap());
|
||||
|
||||
// Open and edit a buffer as both guests B and C.
|
||||
let buffer_b = project_b
|
||||
@ -3022,8 +3023,8 @@ async fn test_fs_operations(
|
||||
.unwrap();
|
||||
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
|
||||
|
||||
let worktree_a = project_a.read_with(cx_a, |project, _| project.worktrees().next().unwrap());
|
||||
let worktree_b = project_b.read_with(cx_b, |project, _| project.worktrees().next().unwrap());
|
||||
let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
|
||||
let worktree_b = project_b.read_with(cx_b, |project, cx| project.worktrees(cx).next().unwrap());
|
||||
|
||||
let entry = project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
@ -3323,7 +3324,7 @@ async fn test_local_settings(
|
||||
// As client B, join that project and observe the local settings.
|
||||
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
|
||||
|
||||
let worktree_b = project_b.read_with(cx_b, |project, _| project.worktrees().next().unwrap());
|
||||
let worktree_b = project_b.read_with(cx_b, |project, cx| project.worktrees(cx).next().unwrap());
|
||||
executor.run_until_parked();
|
||||
cx_b.read(|cx| {
|
||||
let store = cx.global::<SettingsStore>();
|
||||
@ -3735,7 +3736,7 @@ async fn test_leaving_project(
|
||||
// Client B opens a buffer.
|
||||
let buffer_b1 = project_b1
|
||||
.update(cx_b, |project, cx| {
|
||||
let worktree_id = project.worktrees().next().unwrap().read(cx).id();
|
||||
let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
|
||||
project.open_buffer((worktree_id, "a.txt"), cx)
|
||||
})
|
||||
.await
|
||||
@ -3773,7 +3774,7 @@ async fn test_leaving_project(
|
||||
|
||||
let buffer_b2 = project_b2
|
||||
.update(cx_b, |project, cx| {
|
||||
let worktree_id = project.worktrees().next().unwrap().read(cx).id();
|
||||
let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
|
||||
project.open_buffer((worktree_id, "a.txt"), cx)
|
||||
})
|
||||
.await
|
||||
@ -4627,7 +4628,7 @@ async fn test_definition(
|
||||
.unwrap();
|
||||
cx_b.read(|cx| {
|
||||
assert_eq!(definitions_1.len(), 1);
|
||||
assert_eq!(project_b.read(cx).worktrees().count(), 2);
|
||||
assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
|
||||
let target_buffer = definitions_1[0].target.buffer.read(cx);
|
||||
assert_eq!(
|
||||
target_buffer.text(),
|
||||
@ -4656,7 +4657,7 @@ async fn test_definition(
|
||||
.unwrap();
|
||||
cx_b.read(|cx| {
|
||||
assert_eq!(definitions_2.len(), 1);
|
||||
assert_eq!(project_b.read(cx).worktrees().count(), 2);
|
||||
assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
|
||||
let target_buffer = definitions_2[0].target.buffer.read(cx);
|
||||
assert_eq!(
|
||||
target_buffer.text(),
|
||||
@ -4814,7 +4815,7 @@ async fn test_references(
|
||||
assert!(status.pending_work.is_empty());
|
||||
|
||||
assert_eq!(references.len(), 3);
|
||||
assert_eq!(project.worktrees().count(), 2);
|
||||
assert_eq!(project.worktrees(cx).count(), 2);
|
||||
|
||||
let two_buffer = references[0].buffer.read(cx);
|
||||
let three_buffer = references[2].buffer.read(cx);
|
||||
@ -6199,7 +6200,7 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
|
||||
let project = workspace.update(cx, |workspace, _| workspace.project().clone());
|
||||
|
||||
let worktree_id = project.update(cx, |project, cx| {
|
||||
project.worktrees().next().unwrap().read(cx).id()
|
||||
project.worktrees(cx).next().unwrap().read(cx).id()
|
||||
});
|
||||
|
||||
let path_1 = ProjectPath {
|
||||
|
@ -301,7 +301,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
let is_local = project.read_with(cx, |project, _| project.is_local());
|
||||
let worktree = project.read_with(cx, |project, cx| {
|
||||
project
|
||||
.worktrees()
|
||||
.worktrees(cx)
|
||||
.filter(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
worktree.is_visible()
|
||||
@ -423,7 +423,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
81.. => {
|
||||
let worktree = project.read_with(cx, |project, cx| {
|
||||
project
|
||||
.worktrees()
|
||||
.worktrees(cx)
|
||||
.filter(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
worktree.is_visible()
|
||||
@ -1172,7 +1172,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
let host_worktree_snapshots =
|
||||
host_project.read_with(host_cx, |host_project, cx| {
|
||||
host_project
|
||||
.worktrees()
|
||||
.worktrees(cx)
|
||||
.map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
(worktree.id(), worktree.snapshot())
|
||||
@ -1180,7 +1180,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
.collect::<BTreeMap<_, _>>()
|
||||
});
|
||||
let guest_worktree_snapshots = guest_project
|
||||
.worktrees()
|
||||
.worktrees(cx)
|
||||
.map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
(worktree.id(), worktree.snapshot())
|
||||
@ -1538,7 +1538,7 @@ fn project_path_for_full_path(
|
||||
let root_name = components.next().unwrap().as_os_str().to_str().unwrap();
|
||||
let path = components.as_path().into();
|
||||
let worktree_id = project.read_with(cx, |project, cx| {
|
||||
project.worktrees().find_map(|worktree| {
|
||||
project.worktrees(cx).find_map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
if worktree.root_name() == root_name {
|
||||
Some(worktree.id())
|
||||
|
@ -6253,8 +6253,8 @@ async fn test_multibuffer_format_during_save(cx: &mut gpui::TestAppContext) {
|
||||
},
|
||||
);
|
||||
|
||||
let worktree = project.update(cx, |project, _| {
|
||||
let mut worktrees = project.worktrees().collect::<Vec<_>>();
|
||||
let worktree = project.update(cx, |project, cx| {
|
||||
let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
|
||||
assert_eq!(worktrees.len(), 1);
|
||||
worktrees.pop().unwrap()
|
||||
});
|
||||
@ -9319,7 +9319,7 @@ async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) {
|
||||
let worktree_id = workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.project().update(cx, |project, cx| {
|
||||
project.worktrees().next().unwrap().read(cx).id()
|
||||
project.worktrees(cx).next().unwrap().read(cx).id()
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
|
@ -2581,7 +2581,7 @@ pub mod tests {
|
||||
);
|
||||
|
||||
let worktree_id = project.update(cx, |project, cx| {
|
||||
project.worktrees().next().unwrap().read(cx).id()
|
||||
project.worktrees(cx).next().unwrap().read(cx).id()
|
||||
});
|
||||
|
||||
let buffer_1 = project
|
||||
@ -2931,7 +2931,7 @@ pub mod tests {
|
||||
);
|
||||
|
||||
let worktree_id = project.update(cx, |project, cx| {
|
||||
project.worktrees().next().unwrap().read(cx).id()
|
||||
project.worktrees(cx).next().unwrap().read(cx).id()
|
||||
});
|
||||
|
||||
let buffer_1 = project
|
||||
|
@ -1496,7 +1496,7 @@ async fn test_search_results_refreshed_on_adding_and_removing_worktrees(
|
||||
let project = Project::test(app_state.fs.clone(), ["/test/project_1".as_ref()], cx).await;
|
||||
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let worktree_1_id = project.update(cx, |project, cx| {
|
||||
let worktree = project.worktrees().last().expect("worktree not found");
|
||||
let worktree = project.worktrees(cx).last().expect("worktree not found");
|
||||
worktree.read(cx).id()
|
||||
});
|
||||
|
||||
|
@ -32,7 +32,7 @@ impl Match {
|
||||
path_match.path.join(suffix),
|
||||
)
|
||||
} else {
|
||||
(project.worktrees().next(), PathBuf::from(suffix))
|
||||
(project.worktrees(cx).next(), PathBuf::from(suffix))
|
||||
};
|
||||
|
||||
worktree.and_then(|worktree| worktree.read(cx).entry_for_path(path))
|
||||
@ -72,7 +72,7 @@ impl Match {
|
||||
let worktree_id = if let Some(path_match) = &self.path_match {
|
||||
WorktreeId::from_usize(path_match.worktree_id)
|
||||
} else {
|
||||
project.worktrees().next()?.read(cx).id()
|
||||
project.worktrees(cx).next()?.read(cx).id()
|
||||
};
|
||||
|
||||
let path = PathBuf::from(self.relative_path());
|
||||
@ -84,7 +84,7 @@ impl Match {
|
||||
}
|
||||
|
||||
fn existing_prefix(&self, project: &Project, cx: &WindowContext) -> Option<PathBuf> {
|
||||
let worktree = project.worktrees().next()?.read(cx);
|
||||
let worktree = project.worktrees(cx).next()?.read(cx);
|
||||
let mut prefix = PathBuf::new();
|
||||
let parts = self.suffix.as_ref()?.split('/');
|
||||
for part in parts {
|
||||
|
@ -75,6 +75,9 @@ impl RealGitRepository {
|
||||
}
|
||||
}
|
||||
|
||||
// https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
|
||||
const GIT_MODE_SYMLINK: u32 = 0o120000;
|
||||
|
||||
impl GitRepository for RealGitRepository {
|
||||
fn reload_index(&self) {
|
||||
if let Ok(mut index) = self.repository.lock().index() {
|
||||
@ -91,8 +94,8 @@ impl GitRepository for RealGitRepository {
|
||||
check_path_to_repo_path_errors(relative_file_path)?;
|
||||
|
||||
let oid = match index.get_path(relative_file_path, STAGE_NORMAL) {
|
||||
Some(entry) => entry.id,
|
||||
None => return Ok(None),
|
||||
Some(entry) if entry.mode != GIT_MODE_SYMLINK => entry.id,
|
||||
_ => return Ok(None),
|
||||
};
|
||||
|
||||
let content = repo.find_blob(oid)?.content().to_owned();
|
||||
|
@ -258,7 +258,7 @@ mod tests {
|
||||
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let worktree_id = workspace.update(cx, |workspace, cx| {
|
||||
workspace.project().update(cx, |project, cx| {
|
||||
project.worktrees().next().unwrap().read(cx).id()
|
||||
project.worktrees(cx).next().unwrap().read(cx).id()
|
||||
})
|
||||
});
|
||||
let _buffer = project
|
||||
|
@ -551,7 +551,7 @@ impl LspLogView {
|
||||
self.project
|
||||
.read(cx)
|
||||
.supplementary_language_servers()
|
||||
.filter_map(|(&server_id, (name, _))| {
|
||||
.filter_map(|(&server_id, name)| {
|
||||
let state = log_store.language_servers.get(&server_id)?;
|
||||
Some(LogMenuItem {
|
||||
server_id,
|
||||
|
@ -85,7 +85,7 @@ async fn test_lsp_logs(cx: &mut TestAppContext) {
|
||||
server_name: LanguageServerName("the-rust-language-server".into()),
|
||||
worktree_root_name: project
|
||||
.read(cx)
|
||||
.worktrees()
|
||||
.worktrees(cx)
|
||||
.next()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
|
@ -321,7 +321,7 @@ mod tests {
|
||||
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let worktree_id = workspace.update(cx, |workspace, cx| {
|
||||
workspace.project().update(cx, |project, cx| {
|
||||
project.worktrees().next().unwrap().read(cx).id()
|
||||
project.worktrees(cx).next().unwrap().read(cx).id()
|
||||
})
|
||||
});
|
||||
let _buffer = project
|
||||
|
@ -1,13 +1,16 @@
|
||||
use crate::ProjectPath;
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use crate::{
|
||||
worktree_store::{WorktreeStore, WorktreeStoreEvent},
|
||||
ProjectPath,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use collections::{hash_map, HashMap};
|
||||
use futures::{channel::oneshot, StreamExt as _};
|
||||
use futures::{channel::oneshot, stream::FuturesUnordered, StreamExt as _};
|
||||
use gpui::{
|
||||
AppContext, AsyncAppContext, Context as _, EventEmitter, Model, ModelContext, Task, WeakModel,
|
||||
};
|
||||
use language::{
|
||||
proto::{deserialize_version, serialize_version, split_operations},
|
||||
Buffer, Capability, Language, Operation,
|
||||
proto::{deserialize_line_ending, deserialize_version, serialize_version, split_operations},
|
||||
Buffer, Capability, Event as BufferEvent, Language, Operation,
|
||||
};
|
||||
use rpc::{
|
||||
proto::{self, AnyProtoClient, PeerId},
|
||||
@ -16,11 +19,15 @@ use rpc::{
|
||||
use std::{io, path::Path, sync::Arc};
|
||||
use text::BufferId;
|
||||
use util::{debug_panic, maybe, ResultExt as _};
|
||||
use worktree::{File, ProjectEntryId, RemoteWorktree, Worktree};
|
||||
use worktree::{
|
||||
File, PathChange, ProjectEntryId, RemoteWorktree, UpdatedGitRepositoriesSet, Worktree,
|
||||
};
|
||||
|
||||
/// A set of open buffers.
|
||||
pub struct BufferStore {
|
||||
retain_buffers: bool,
|
||||
#[allow(unused)]
|
||||
worktree_store: Model<WorktreeStore>,
|
||||
opened_buffers: HashMap<BufferId, OpenBuffer>,
|
||||
local_buffer_ids_by_path: HashMap<ProjectPath, BufferId>,
|
||||
local_buffer_ids_by_entry_id: HashMap<ProjectEntryId, BufferId>,
|
||||
@ -51,6 +58,12 @@ pub enum BufferStoreEvent {
|
||||
has_changed_file: bool,
|
||||
saved_version: clock::Global,
|
||||
},
|
||||
LocalBufferUpdated {
|
||||
buffer: Model<Buffer>,
|
||||
},
|
||||
DiffBaseUpdated {
|
||||
buffer: Model<Buffer>,
|
||||
},
|
||||
}
|
||||
|
||||
impl EventEmitter<BufferStoreEvent> for BufferStore {}
|
||||
@ -62,9 +75,22 @@ impl BufferStore {
|
||||
/// and won't be released unless they are explicitly removed, or `retain_buffers`
|
||||
/// is set to `false` via `set_retain_buffers`. Otherwise, buffers are stored as
|
||||
/// weak handles.
|
||||
pub fn new(retain_buffers: bool) -> Self {
|
||||
pub fn new(
|
||||
worktree_store: Model<WorktreeStore>,
|
||||
retain_buffers: bool,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
cx.subscribe(&worktree_store, |this, _, event, cx| match event {
|
||||
WorktreeStoreEvent::WorktreeAdded(worktree) => {
|
||||
this.subscribe_to_worktree(worktree, cx);
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
retain_buffers,
|
||||
worktree_store,
|
||||
opened_buffers: Default::default(),
|
||||
remote_buffer_listeners: Default::default(),
|
||||
loading_remote_buffers_by_id: Default::default(),
|
||||
@ -77,7 +103,6 @@ impl BufferStore {
|
||||
pub fn open_buffer(
|
||||
&mut self,
|
||||
project_path: ProjectPath,
|
||||
worktree: Model<Worktree>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Model<Buffer>>> {
|
||||
let existing_buffer = self.get_by_path(&project_path, cx);
|
||||
@ -85,6 +110,14 @@ impl BufferStore {
|
||||
return Task::ready(Ok(existing_buffer));
|
||||
}
|
||||
|
||||
let Some(worktree) = self
|
||||
.worktree_store
|
||||
.read(cx)
|
||||
.worktree_for_id(project_path.worktree_id, cx)
|
||||
else {
|
||||
return Task::ready(Err(anyhow!("no such worktree")));
|
||||
};
|
||||
|
||||
let loading_watch = match self.loading_buffers_by_path.entry(project_path.clone()) {
|
||||
// If the given path is already being loaded, then wait for that existing
|
||||
// task to complete and return the same buffer.
|
||||
@ -127,6 +160,131 @@ impl BufferStore {
|
||||
})
|
||||
}
|
||||
|
||||
fn subscribe_to_worktree(&mut self, worktree: &Model<Worktree>, cx: &mut ModelContext<Self>) {
|
||||
cx.subscribe(worktree, |this, worktree, event, cx| {
|
||||
if worktree.read(cx).is_local() {
|
||||
match event {
|
||||
worktree::Event::UpdatedEntries(changes) => {
|
||||
this.local_worktree_entries_changed(&worktree, changes, cx);
|
||||
}
|
||||
worktree::Event::UpdatedGitRepositories(updated_repos) => {
|
||||
this.local_worktree_git_repos_changed(worktree.clone(), updated_repos, cx)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn local_worktree_entries_changed(
|
||||
&mut self,
|
||||
worktree_handle: &Model<Worktree>,
|
||||
changes: &[(Arc<Path>, ProjectEntryId, PathChange)],
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let snapshot = worktree_handle.read(cx).snapshot();
|
||||
for (path, entry_id, _) in changes {
|
||||
self.local_worktree_entry_changed(*entry_id, path, worktree_handle, &snapshot, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn local_worktree_git_repos_changed(
|
||||
&mut self,
|
||||
worktree_handle: Model<Worktree>,
|
||||
changed_repos: &UpdatedGitRepositoriesSet,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
debug_assert!(worktree_handle.read(cx).is_local());
|
||||
|
||||
// Identify the loading buffers whose containing repository that has changed.
|
||||
let future_buffers = self
|
||||
.loading_buffers()
|
||||
.filter_map(|(project_path, receiver)| {
|
||||
if project_path.worktree_id != worktree_handle.read(cx).id() {
|
||||
return None;
|
||||
}
|
||||
let path = &project_path.path;
|
||||
changed_repos
|
||||
.iter()
|
||||
.find(|(work_dir, _)| path.starts_with(work_dir))?;
|
||||
let path = path.clone();
|
||||
Some(async move {
|
||||
Self::wait_for_loading_buffer(receiver)
|
||||
.await
|
||||
.ok()
|
||||
.map(|buffer| (buffer, path))
|
||||
})
|
||||
})
|
||||
.collect::<FuturesUnordered<_>>();
|
||||
|
||||
// Identify the current buffers whose containing repository has changed.
|
||||
let current_buffers = self
|
||||
.buffers()
|
||||
.filter_map(|buffer| {
|
||||
let file = File::from_dyn(buffer.read(cx).file())?;
|
||||
if file.worktree != worktree_handle {
|
||||
return None;
|
||||
}
|
||||
changed_repos
|
||||
.iter()
|
||||
.find(|(work_dir, _)| file.path.starts_with(work_dir))?;
|
||||
Some((buffer, file.path.clone()))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if future_buffers.len() + current_buffers.len() == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
// Wait for all of the buffers to load.
|
||||
let future_buffers = future_buffers.collect::<Vec<_>>().await;
|
||||
|
||||
// Reload the diff base for every buffer whose containing git repository has changed.
|
||||
let snapshot =
|
||||
worktree_handle.update(&mut cx, |tree, _| tree.as_local().unwrap().snapshot())?;
|
||||
let diff_bases_by_buffer = cx
|
||||
.background_executor()
|
||||
.spawn(async move {
|
||||
let mut diff_base_tasks = future_buffers
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.chain(current_buffers)
|
||||
.filter_map(|(buffer, path)| {
|
||||
let (repo_entry, local_repo_entry) = snapshot.repo_for_path(&path)?;
|
||||
let relative_path = repo_entry.relativize(&snapshot, &path).ok()?;
|
||||
Some(async move {
|
||||
let base_text =
|
||||
local_repo_entry.repo().load_index_text(&relative_path);
|
||||
Some((buffer, base_text))
|
||||
})
|
||||
})
|
||||
.collect::<FuturesUnordered<_>>();
|
||||
|
||||
let mut diff_bases = Vec::with_capacity(diff_base_tasks.len());
|
||||
while let Some(diff_base) = diff_base_tasks.next().await {
|
||||
if let Some(diff_base) = diff_base {
|
||||
diff_bases.push(diff_base);
|
||||
}
|
||||
}
|
||||
diff_bases
|
||||
})
|
||||
.await;
|
||||
|
||||
this.update(&mut cx, |_, cx| {
|
||||
// Assign the new diff bases on all of the buffers.
|
||||
for (buffer, diff_base) in diff_bases_by_buffer {
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.set_diff_base(diff_base.clone(), cx);
|
||||
});
|
||||
cx.emit(BufferStoreEvent::DiffBaseUpdated { buffer })
|
||||
}
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn open_local_buffer_internal(
|
||||
&mut self,
|
||||
path: Arc<Path>,
|
||||
@ -265,9 +423,16 @@ impl BufferStore {
|
||||
&mut self,
|
||||
buffer: Model<Buffer>,
|
||||
path: ProjectPath,
|
||||
worktree: Model<Worktree>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let Some(worktree) = self
|
||||
.worktree_store
|
||||
.read(cx)
|
||||
.worktree_for_id(path.worktree_id, cx)
|
||||
else {
|
||||
return Task::ready(Err(anyhow!("no such worktree")));
|
||||
};
|
||||
|
||||
let old_file = File::from_dyn(buffer.read(cx).file())
|
||||
.cloned()
|
||||
.map(Arc::new);
|
||||
@ -411,6 +576,7 @@ impl BufferStore {
|
||||
}
|
||||
}
|
||||
|
||||
cx.subscribe(&buffer, Self::on_buffer_event).detach();
|
||||
cx.emit(BufferStoreEvent::BufferAdded(buffer));
|
||||
Ok(())
|
||||
}
|
||||
@ -461,31 +627,6 @@ impl BufferStore {
|
||||
.or_else(|| self.loading_remote_buffers_by_id.get(&buffer_id).cloned())
|
||||
}
|
||||
|
||||
fn get_or_remove_by_path(
|
||||
&mut self,
|
||||
entry_id: ProjectEntryId,
|
||||
project_path: &ProjectPath,
|
||||
) -> Option<(BufferId, Model<Buffer>)> {
|
||||
let buffer_id = match self.local_buffer_ids_by_entry_id.get(&entry_id) {
|
||||
Some(&buffer_id) => buffer_id,
|
||||
None => match self.local_buffer_ids_by_path.get(project_path) {
|
||||
Some(&buffer_id) => buffer_id,
|
||||
None => {
|
||||
return None;
|
||||
}
|
||||
},
|
||||
};
|
||||
let buffer = if let Some(buffer) = self.get(buffer_id) {
|
||||
buffer
|
||||
} else {
|
||||
self.opened_buffers.remove(&buffer_id);
|
||||
self.local_buffer_ids_by_path.remove(project_path);
|
||||
self.local_buffer_ids_by_entry_id.remove(&entry_id);
|
||||
return None;
|
||||
};
|
||||
Some((buffer_id, buffer))
|
||||
}
|
||||
|
||||
pub fn wait_for_remote_buffer(
|
||||
&mut self,
|
||||
id: BufferId,
|
||||
@ -561,25 +702,48 @@ impl BufferStore {
|
||||
.retain(|_, buffer| !matches!(buffer, OpenBuffer::Operations(_)));
|
||||
}
|
||||
|
||||
pub fn file_changed(
|
||||
fn on_buffer_event(
|
||||
&mut self,
|
||||
buffer: Model<Buffer>,
|
||||
event: &BufferEvent,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
BufferEvent::FileHandleChanged => {
|
||||
self.buffer_changed_file(buffer, cx);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn local_worktree_entry_changed(
|
||||
&mut self,
|
||||
path: Arc<Path>,
|
||||
entry_id: ProjectEntryId,
|
||||
worktree_handle: &Model<worktree::Worktree>,
|
||||
path: &Arc<Path>,
|
||||
worktree: &Model<worktree::Worktree>,
|
||||
snapshot: &worktree::Snapshot,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Option<(Model<Buffer>, Arc<File>, Arc<File>)> {
|
||||
let (buffer_id, buffer) = self.get_or_remove_by_path(
|
||||
entry_id,
|
||||
&ProjectPath {
|
||||
) -> Option<()> {
|
||||
let project_path = ProjectPath {
|
||||
worktree_id: snapshot.id(),
|
||||
path,
|
||||
},
|
||||
)?;
|
||||
path: path.clone(),
|
||||
};
|
||||
let buffer_id = match self.local_buffer_ids_by_entry_id.get(&entry_id) {
|
||||
Some(&buffer_id) => buffer_id,
|
||||
None => self.local_buffer_ids_by_path.get(&project_path).copied()?,
|
||||
};
|
||||
let buffer = if let Some(buffer) = self.get(buffer_id) {
|
||||
buffer
|
||||
} else {
|
||||
self.opened_buffers.remove(&buffer_id);
|
||||
self.local_buffer_ids_by_path.remove(&project_path);
|
||||
self.local_buffer_ids_by_entry_id.remove(&entry_id);
|
||||
return None;
|
||||
};
|
||||
|
||||
let result = buffer.update(cx, |buffer, cx| {
|
||||
let (old_file, new_file) = buffer.update(cx, |buffer, cx| {
|
||||
let old_file = File::from_dyn(buffer.file())?;
|
||||
if old_file.worktree != *worktree_handle {
|
||||
if old_file.worktree != *worktree {
|
||||
return None;
|
||||
}
|
||||
|
||||
@ -592,7 +756,7 @@ impl BufferStore {
|
||||
entry_id: Some(entry.id),
|
||||
mtime: entry.mtime,
|
||||
path: entry.path.clone(),
|
||||
worktree: worktree_handle.clone(),
|
||||
worktree: worktree.clone(),
|
||||
is_deleted: false,
|
||||
is_private: entry.is_private,
|
||||
}
|
||||
@ -602,7 +766,7 @@ impl BufferStore {
|
||||
entry_id: Some(entry.id),
|
||||
mtime: entry.mtime,
|
||||
path: entry.path.clone(),
|
||||
worktree: worktree_handle.clone(),
|
||||
worktree: worktree.clone(),
|
||||
is_deleted: false,
|
||||
is_private: entry.is_private,
|
||||
}
|
||||
@ -612,7 +776,7 @@ impl BufferStore {
|
||||
entry_id: old_file.entry_id,
|
||||
path: old_file.path.clone(),
|
||||
mtime: old_file.mtime,
|
||||
worktree: worktree_handle.clone(),
|
||||
worktree: worktree.clone(),
|
||||
is_deleted: true,
|
||||
is_private: old_file.is_private,
|
||||
}
|
||||
@ -625,10 +789,9 @@ impl BufferStore {
|
||||
let old_file = Arc::new(old_file.clone());
|
||||
let new_file = Arc::new(new_file);
|
||||
buffer.file_updated(new_file.clone(), cx);
|
||||
Some((cx.handle(), old_file, new_file))
|
||||
});
|
||||
Some((old_file, new_file))
|
||||
})?;
|
||||
|
||||
if let Some((buffer, old_file, new_file)) = &result {
|
||||
if new_file.path != old_file.path {
|
||||
self.local_buffer_ids_by_path.remove(&ProjectPath {
|
||||
path: old_file.path.clone(),
|
||||
@ -656,16 +819,12 @@ impl BufferStore {
|
||||
.insert(entry_id, buffer_id);
|
||||
}
|
||||
}
|
||||
|
||||
cx.emit(BufferStoreEvent::LocalBufferUpdated { buffer });
|
||||
None
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
pub fn buffer_changed_file(
|
||||
&mut self,
|
||||
buffer: Model<Buffer>,
|
||||
cx: &mut AppContext,
|
||||
) -> Option<()> {
|
||||
fn buffer_changed_file(&mut self, buffer: Model<Buffer>, cx: &mut AppContext) -> Option<()> {
|
||||
let file = File::from_dyn(buffer.read(cx).file())?;
|
||||
|
||||
let remote_id = buffer.read(cx).remote_id();
|
||||
@ -862,7 +1021,6 @@ impl BufferStore {
|
||||
pub async fn handle_save_buffer(
|
||||
this: Model<Self>,
|
||||
project_id: u64,
|
||||
worktree: Option<Model<Worktree>>,
|
||||
envelope: TypedEnvelope<proto::SaveBuffer>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<proto::BufferSaved> {
|
||||
@ -876,10 +1034,9 @@ impl BufferStore {
|
||||
let buffer_id = buffer.update(&mut cx, |buffer, _| buffer.remote_id())?;
|
||||
|
||||
if let Some(new_path) = envelope.payload.new_path {
|
||||
let worktree = worktree.context("no such worktree")?;
|
||||
let new_path = ProjectPath::from_proto(new_path);
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.save_buffer_as(buffer.clone(), new_path, worktree, cx)
|
||||
this.save_buffer_as(buffer.clone(), new_path, cx)
|
||||
})?
|
||||
.await?;
|
||||
} else {
|
||||
@ -895,6 +1052,44 @@ impl BufferStore {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn handle_buffer_saved(
|
||||
this: Model<Self>,
|
||||
envelope: TypedEnvelope<proto::BufferSaved>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
let buffer_id = BufferId::new(envelope.payload.buffer_id)?;
|
||||
let version = deserialize_version(&envelope.payload.version);
|
||||
let mtime = envelope.payload.mtime.map(|time| time.into());
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if let Some(buffer) = this.get_possibly_incomplete(buffer_id) {
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.did_save(version, mtime, cx);
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn handle_buffer_reloaded(
|
||||
this: Model<Self>,
|
||||
envelope: TypedEnvelope<proto::BufferReloaded>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
let buffer_id = BufferId::new(envelope.payload.buffer_id)?;
|
||||
let version = deserialize_version(&envelope.payload.version);
|
||||
let mtime = envelope.payload.mtime.map(|time| time.into());
|
||||
let line_ending = deserialize_line_ending(
|
||||
proto::LineEnding::from_i32(envelope.payload.line_ending)
|
||||
.ok_or_else(|| anyhow!("missing line ending"))?,
|
||||
);
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if let Some(buffer) = this.get_possibly_incomplete(buffer_id) {
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.did_reload(version, line_ending, mtime, cx);
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn wait_for_loading_buffer(
|
||||
mut receiver: postage::watch::Receiver<Option<Result<Model<Buffer>, Arc<anyhow::Error>>>>,
|
||||
) -> Result<Model<Buffer>, Arc<anyhow::Error>> {
|
||||
|
@ -85,7 +85,7 @@ impl Manager {
|
||||
Some(proto::RejoinProject {
|
||||
id: project_id,
|
||||
worktrees: project
|
||||
.worktrees()
|
||||
.worktrees(cx)
|
||||
.map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
proto::RejoinWorktree {
|
||||
|
@ -1,26 +1,25 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use futures::{channel::oneshot, FutureExt};
|
||||
use gpui::{ModelContext, Task};
|
||||
use std::{marker::PhantomData, time::Duration};
|
||||
|
||||
use crate::Project;
|
||||
|
||||
pub struct DebouncedDelay {
|
||||
pub struct DebouncedDelay<E: 'static> {
|
||||
task: Option<Task<()>>,
|
||||
cancel_channel: Option<oneshot::Sender<()>>,
|
||||
_phantom_data: PhantomData<E>,
|
||||
}
|
||||
|
||||
impl DebouncedDelay {
|
||||
pub fn new() -> DebouncedDelay {
|
||||
DebouncedDelay {
|
||||
impl<E: 'static> DebouncedDelay<E> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
task: None,
|
||||
cancel_channel: None,
|
||||
_phantom_data: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fire_new<F>(&mut self, delay: Duration, cx: &mut ModelContext<Project>, func: F)
|
||||
pub fn fire_new<F>(&mut self, delay: Duration, cx: &mut ModelContext<E>, func: F)
|
||||
where
|
||||
F: 'static + Send + FnOnce(&mut Project, &mut ModelContext<Project>) -> Task<()>,
|
||||
F: 'static + Send + FnOnce(&mut E, &mut ModelContext<E>) -> Task<()>,
|
||||
{
|
||||
if let Some(channel) = self.cancel_channel.take() {
|
||||
_ = channel.send(());
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -80,7 +80,7 @@ async fn test_symlinks(cx: &mut gpui::TestAppContext) {
|
||||
let project = Project::test(Arc::new(RealFs::default()), [root_link_path.as_ref()], cx).await;
|
||||
|
||||
project.update(cx, |project, cx| {
|
||||
let tree = project.worktrees().next().unwrap().read(cx);
|
||||
let tree = project.worktrees(cx).next().unwrap().read(cx);
|
||||
assert_eq!(tree.file_count(), 5);
|
||||
assert_eq!(
|
||||
tree.inode_for_path("fennel/grape"),
|
||||
@ -124,13 +124,13 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
|
||||
let worktree = project.update(cx, |project, _| project.worktrees().next().unwrap());
|
||||
let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
|
||||
let task_context = TaskContext::default();
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
let worktree_id = cx.update(|cx| {
|
||||
project.update(cx, |project, cx| {
|
||||
project.worktrees().next().unwrap().read(cx).id()
|
||||
project.worktrees(cx).next().unwrap().read(cx).id()
|
||||
})
|
||||
});
|
||||
let global_task_source_kind = TaskSourceKind::Worktree {
|
||||
@ -734,7 +734,7 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
|
||||
|
||||
// Initially, we don't load ignored files because the language server has not explicitly asked us to watch them.
|
||||
project.update(cx, |project, cx| {
|
||||
let worktree = project.worktrees().next().unwrap();
|
||||
let worktree = project.worktrees(cx).next().unwrap();
|
||||
assert_eq!(
|
||||
worktree
|
||||
.read(cx)
|
||||
@ -808,7 +808,7 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
|
||||
// Now the language server has asked us to watch an ignored directory path,
|
||||
// so we recursively load it.
|
||||
project.update(cx, |project, cx| {
|
||||
let worktree = project.worktrees().next().unwrap();
|
||||
let worktree = project.worktrees(cx).next().unwrap();
|
||||
assert_eq!(
|
||||
worktree
|
||||
.read(cx)
|
||||
@ -1132,7 +1132,7 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
|
||||
},
|
||||
);
|
||||
|
||||
let worktree_id = project.update(cx, |p, cx| p.worktrees().next().unwrap().read(cx).id());
|
||||
let worktree_id = project.update(cx, |p, cx| p.worktrees(cx).next().unwrap().read(cx).id());
|
||||
|
||||
// Cause worktree to start the fake language server
|
||||
let _buffer = project
|
||||
@ -2477,7 +2477,7 @@ async fn test_definition(cx: &mut gpui::TestAppContext) {
|
||||
) -> Vec<(&'a Path, bool)> {
|
||||
project
|
||||
.read(cx)
|
||||
.worktrees()
|
||||
.worktrees(cx)
|
||||
.map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
(
|
||||
@ -2821,7 +2821,7 @@ async fn test_file_changes_multiple_times_on_disk(cx: &mut gpui::TestAppContext)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
|
||||
let worktree = project.read_with(cx, |project, _| project.worktrees().next().unwrap());
|
||||
let worktree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
|
||||
let buffer = project
|
||||
.update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
|
||||
.await
|
||||
@ -2876,7 +2876,7 @@ async fn test_edit_buffer_while_it_reloads(cx: &mut gpui::TestAppContext) {
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
|
||||
let worktree = project.read_with(cx, |project, _| project.worktrees().next().unwrap());
|
||||
let worktree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
|
||||
let buffer = project
|
||||
.update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
|
||||
.await
|
||||
@ -2978,7 +2978,7 @@ async fn test_save_as(cx: &mut gpui::TestAppContext) {
|
||||
});
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
let worktree_id = project.worktrees().next().unwrap().read(cx).id();
|
||||
let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
|
||||
let path = ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from(Path::new("file1.rs")),
|
||||
@ -3038,7 +3038,7 @@ async fn test_rescan_and_remote_updates(cx: &mut gpui::TestAppContext) {
|
||||
};
|
||||
let id_for_path = |path: &'static str, cx: &mut gpui::TestAppContext| {
|
||||
project.update(cx, |project, cx| {
|
||||
let tree = project.worktrees().next().unwrap();
|
||||
let tree = project.worktrees(cx).next().unwrap();
|
||||
tree.read(cx)
|
||||
.entry_for_path(path)
|
||||
.unwrap_or_else(|| panic!("no entry for path {}", path))
|
||||
@ -3056,7 +3056,7 @@ async fn test_rescan_and_remote_updates(cx: &mut gpui::TestAppContext) {
|
||||
let file4_id = id_for_path("b/c/file4", cx);
|
||||
|
||||
// Create a remote copy of this worktree.
|
||||
let tree = project.update(cx, |project, _| project.worktrees().next().unwrap());
|
||||
let tree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
|
||||
let metadata = tree.update(cx, |tree, _| tree.metadata_proto());
|
||||
|
||||
let updates = Arc::new(Mutex::new(Vec::new()));
|
||||
@ -3173,12 +3173,12 @@ async fn test_buffer_identity_across_renames(cx: &mut gpui::TestAppContext) {
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [Path::new("/dir")], cx).await;
|
||||
let tree = project.update(cx, |project, _| project.worktrees().next().unwrap());
|
||||
let tree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
|
||||
let tree_id = tree.update(cx, |tree, _| tree.id());
|
||||
|
||||
let id_for_path = |path: &'static str, cx: &mut gpui::TestAppContext| {
|
||||
project.update(cx, |project, cx| {
|
||||
let tree = project.worktrees().next().unwrap();
|
||||
let tree = project.worktrees(cx).next().unwrap();
|
||||
tree.read(cx)
|
||||
.entry_for_path(path)
|
||||
.unwrap_or_else(|| panic!("no entry for path {}", path))
|
||||
@ -4549,7 +4549,7 @@ async fn test_create_entry(cx: &mut gpui::TestAppContext) {
|
||||
let project = Project::test(fs.clone(), ["/one/two/three".as_ref()], cx).await;
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
let id = project.worktrees().next().unwrap().read(cx).id();
|
||||
let id = project.worktrees(cx).next().unwrap().read(cx).id();
|
||||
project.create_entry((id, "b.."), true, cx)
|
||||
})
|
||||
.unwrap()
|
||||
@ -4560,7 +4560,7 @@ async fn test_create_entry(cx: &mut gpui::TestAppContext) {
|
||||
// Can't create paths outside the project
|
||||
let result = project
|
||||
.update(cx, |project, cx| {
|
||||
let id = project.worktrees().next().unwrap().read(cx).id();
|
||||
let id = project.worktrees(cx).next().unwrap().read(cx).id();
|
||||
project.create_entry((id, "../../boop"), true, cx)
|
||||
})
|
||||
.await;
|
||||
@ -4569,7 +4569,7 @@ async fn test_create_entry(cx: &mut gpui::TestAppContext) {
|
||||
// Can't create paths with '..'
|
||||
let result = project
|
||||
.update(cx, |project, cx| {
|
||||
let id = project.worktrees().next().unwrap().read(cx).id();
|
||||
let id = project.worktrees(cx).next().unwrap().read(cx).id();
|
||||
project.create_entry((id, "four/../beep"), true, cx)
|
||||
})
|
||||
.await;
|
||||
@ -4592,7 +4592,7 @@ async fn test_create_entry(cx: &mut gpui::TestAppContext) {
|
||||
// And we cannot open buffers with '..'
|
||||
let result = project
|
||||
.update(cx, |project, cx| {
|
||||
let id = project.worktrees().next().unwrap().read(cx).id();
|
||||
let id = project.worktrees(cx).next().unwrap().read(cx).id();
|
||||
project.open_buffer((id, "../c.rs"), cx)
|
||||
})
|
||||
.await;
|
||||
|
311
crates/project/src/worktree_store.rs
Normal file
311
crates/project/src/worktree_store.rs
Normal file
@ -0,0 +1,311 @@
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use collections::HashMap;
|
||||
use gpui::{AppContext, AsyncAppContext, EntityId, EventEmitter, Model, ModelContext, WeakModel};
|
||||
use rpc::{
|
||||
proto::{self, AnyProtoClient},
|
||||
TypedEnvelope,
|
||||
};
|
||||
use text::ReplicaId;
|
||||
use worktree::{ProjectEntryId, Worktree, WorktreeId};
|
||||
|
||||
pub struct WorktreeStore {
|
||||
is_shared: bool,
|
||||
worktrees: Vec<WorktreeHandle>,
|
||||
worktrees_reordered: bool,
|
||||
}
|
||||
|
||||
pub enum WorktreeStoreEvent {
|
||||
WorktreeAdded(Model<Worktree>),
|
||||
WorktreeRemoved(EntityId, WorktreeId),
|
||||
WorktreeOrderChanged,
|
||||
}
|
||||
|
||||
impl EventEmitter<WorktreeStoreEvent> for WorktreeStore {}
|
||||
|
||||
impl WorktreeStore {
|
||||
pub fn new(retain_worktrees: bool) -> Self {
|
||||
Self {
|
||||
is_shared: retain_worktrees,
|
||||
worktrees: Vec::new(),
|
||||
worktrees_reordered: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterates through all worktrees, including ones that don't appear in the project panel
|
||||
pub fn worktrees(&self) -> impl '_ + DoubleEndedIterator<Item = Model<Worktree>> {
|
||||
self.worktrees
|
||||
.iter()
|
||||
.filter_map(move |worktree| worktree.upgrade())
|
||||
}
|
||||
|
||||
/// Iterates through all user-visible worktrees, the ones that appear in the project panel.
|
||||
pub fn visible_worktrees<'a>(
|
||||
&'a self,
|
||||
cx: &'a AppContext,
|
||||
) -> impl 'a + DoubleEndedIterator<Item = Model<Worktree>> {
|
||||
self.worktrees()
|
||||
.filter(|worktree| worktree.read(cx).is_visible())
|
||||
}
|
||||
|
||||
pub fn worktree_for_id(&self, id: WorktreeId, cx: &AppContext) -> Option<Model<Worktree>> {
|
||||
self.worktrees()
|
||||
.find(|worktree| worktree.read(cx).id() == id)
|
||||
}
|
||||
|
||||
pub fn worktree_for_entry(
|
||||
&self,
|
||||
entry_id: ProjectEntryId,
|
||||
cx: &AppContext,
|
||||
) -> Option<Model<Worktree>> {
|
||||
self.worktrees()
|
||||
.find(|worktree| worktree.read(cx).contains_entry(entry_id))
|
||||
}
|
||||
|
||||
pub fn add(&mut self, worktree: &Model<Worktree>, cx: &mut ModelContext<Self>) {
|
||||
let push_strong_handle = self.is_shared || worktree.read(cx).is_visible();
|
||||
let handle = if push_strong_handle {
|
||||
WorktreeHandle::Strong(worktree.clone())
|
||||
} else {
|
||||
WorktreeHandle::Weak(worktree.downgrade())
|
||||
};
|
||||
if self.worktrees_reordered {
|
||||
self.worktrees.push(handle);
|
||||
} else {
|
||||
let i = match self
|
||||
.worktrees
|
||||
.binary_search_by_key(&Some(worktree.read(cx).abs_path()), |other| {
|
||||
other.upgrade().map(|worktree| worktree.read(cx).abs_path())
|
||||
}) {
|
||||
Ok(i) | Err(i) => i,
|
||||
};
|
||||
self.worktrees.insert(i, handle);
|
||||
}
|
||||
|
||||
cx.emit(WorktreeStoreEvent::WorktreeAdded(worktree.clone()));
|
||||
|
||||
let handle_id = worktree.entity_id();
|
||||
cx.observe_release(worktree, move |_, worktree, cx| {
|
||||
cx.emit(WorktreeStoreEvent::WorktreeRemoved(
|
||||
handle_id,
|
||||
worktree.id(),
|
||||
));
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn remove_worktree(&mut self, id_to_remove: WorktreeId, cx: &mut ModelContext<Self>) {
|
||||
self.worktrees.retain(|worktree| {
|
||||
if let Some(worktree) = worktree.upgrade() {
|
||||
worktree.read(cx).id() != id_to_remove
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn set_worktrees_reordered(&mut self, worktrees_reordered: bool) {
|
||||
self.worktrees_reordered = worktrees_reordered;
|
||||
}
|
||||
|
||||
pub fn set_worktrees_from_proto(
|
||||
&mut self,
|
||||
worktrees: Vec<proto::WorktreeMetadata>,
|
||||
replica_id: ReplicaId,
|
||||
remote_id: u64,
|
||||
client: AnyProtoClient,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Result<()> {
|
||||
let mut old_worktrees_by_id = self
|
||||
.worktrees
|
||||
.drain(..)
|
||||
.filter_map(|worktree| {
|
||||
let worktree = worktree.upgrade()?;
|
||||
Some((worktree.read(cx).id(), worktree))
|
||||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
for worktree in worktrees {
|
||||
if let Some(old_worktree) =
|
||||
old_worktrees_by_id.remove(&WorktreeId::from_proto(worktree.id))
|
||||
{
|
||||
self.worktrees.push(WorktreeHandle::Strong(old_worktree));
|
||||
} else {
|
||||
self.add(
|
||||
&Worktree::remote(remote_id, replica_id, worktree, client.clone(), cx),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn move_worktree(
|
||||
&mut self,
|
||||
source: WorktreeId,
|
||||
destination: WorktreeId,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Result<()> {
|
||||
if source == destination {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut source_index = None;
|
||||
let mut destination_index = None;
|
||||
for (i, worktree) in self.worktrees.iter().enumerate() {
|
||||
if let Some(worktree) = worktree.upgrade() {
|
||||
let worktree_id = worktree.read(cx).id();
|
||||
if worktree_id == source {
|
||||
source_index = Some(i);
|
||||
if destination_index.is_some() {
|
||||
break;
|
||||
}
|
||||
} else if worktree_id == destination {
|
||||
destination_index = Some(i);
|
||||
if source_index.is_some() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let source_index =
|
||||
source_index.with_context(|| format!("Missing worktree for id {source}"))?;
|
||||
let destination_index =
|
||||
destination_index.with_context(|| format!("Missing worktree for id {destination}"))?;
|
||||
|
||||
if source_index == destination_index {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let worktree_to_move = self.worktrees.remove(source_index);
|
||||
self.worktrees.insert(destination_index, worktree_to_move);
|
||||
self.worktrees_reordered = true;
|
||||
cx.emit(WorktreeStoreEvent::WorktreeOrderChanged);
|
||||
cx.notify();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn disconnected_from_host(&mut self, cx: &mut AppContext) {
|
||||
for worktree in &self.worktrees {
|
||||
if let Some(worktree) = worktree.upgrade() {
|
||||
worktree.update(cx, |worktree, _| {
|
||||
if let Some(worktree) = worktree.as_remote_mut() {
|
||||
worktree.disconnected_from_host();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_shared(&mut self, is_shared: bool, cx: &mut ModelContext<Self>) {
|
||||
self.is_shared = is_shared;
|
||||
|
||||
// When shared, retain all worktrees
|
||||
if is_shared {
|
||||
for worktree_handle in self.worktrees.iter_mut() {
|
||||
match worktree_handle {
|
||||
WorktreeHandle::Strong(_) => {}
|
||||
WorktreeHandle::Weak(worktree) => {
|
||||
if let Some(worktree) = worktree.upgrade() {
|
||||
*worktree_handle = WorktreeHandle::Strong(worktree);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// When not shared, only retain the visible worktrees
|
||||
else {
|
||||
for worktree_handle in self.worktrees.iter_mut() {
|
||||
if let WorktreeHandle::Strong(worktree) = worktree_handle {
|
||||
let is_visible = worktree.update(cx, |worktree, _| {
|
||||
worktree.stop_observing_updates();
|
||||
worktree.is_visible()
|
||||
});
|
||||
if !is_visible {
|
||||
*worktree_handle = WorktreeHandle::Weak(worktree.downgrade());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_create_project_entry(
|
||||
this: Model<Self>,
|
||||
envelope: TypedEnvelope<proto::CreateProjectEntry>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<proto::ProjectEntryResponse> {
|
||||
let worktree = this.update(&mut cx, |this, cx| {
|
||||
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
|
||||
this.worktree_for_id(worktree_id, cx)
|
||||
.ok_or_else(|| anyhow!("worktree not found"))
|
||||
})??;
|
||||
Worktree::handle_create_entry(worktree, envelope.payload, cx).await
|
||||
}
|
||||
|
||||
pub async fn handle_rename_project_entry(
|
||||
this: Model<Self>,
|
||||
envelope: TypedEnvelope<proto::RenameProjectEntry>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<proto::ProjectEntryResponse> {
|
||||
let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id);
|
||||
let worktree = this.update(&mut cx, |this, cx| {
|
||||
this.worktree_for_entry(entry_id, cx)
|
||||
.ok_or_else(|| anyhow!("worktree not found"))
|
||||
})??;
|
||||
Worktree::handle_rename_entry(worktree, envelope.payload, cx).await
|
||||
}
|
||||
|
||||
pub async fn handle_copy_project_entry(
|
||||
this: Model<Self>,
|
||||
envelope: TypedEnvelope<proto::CopyProjectEntry>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<proto::ProjectEntryResponse> {
|
||||
let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id);
|
||||
let worktree = this.update(&mut cx, |this, cx| {
|
||||
this.worktree_for_entry(entry_id, cx)
|
||||
.ok_or_else(|| anyhow!("worktree not found"))
|
||||
})??;
|
||||
Worktree::handle_copy_entry(worktree, envelope.payload, cx).await
|
||||
}
|
||||
|
||||
pub async fn handle_delete_project_entry(
|
||||
this: Model<Self>,
|
||||
envelope: TypedEnvelope<proto::DeleteProjectEntry>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<proto::ProjectEntryResponse> {
|
||||
let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id);
|
||||
let worktree = this.update(&mut cx, |this, cx| {
|
||||
this.worktree_for_entry(entry_id, cx)
|
||||
.ok_or_else(|| anyhow!("worktree not found"))
|
||||
})??;
|
||||
Worktree::handle_delete_entry(worktree, envelope.payload, cx).await
|
||||
}
|
||||
|
||||
pub async fn handle_expand_project_entry(
|
||||
this: Model<Self>,
|
||||
envelope: TypedEnvelope<proto::ExpandProjectEntry>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<proto::ExpandProjectEntryResponse> {
|
||||
let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id);
|
||||
let worktree = this
|
||||
.update(&mut cx, |this, cx| this.worktree_for_entry(entry_id, cx))?
|
||||
.ok_or_else(|| anyhow!("invalid request"))?;
|
||||
Worktree::handle_expand_entry(worktree, envelope.payload, cx).await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum WorktreeHandle {
|
||||
Strong(Model<Worktree>),
|
||||
Weak(WeakModel<Worktree>),
|
||||
}
|
||||
|
||||
impl WorktreeHandle {
|
||||
fn upgrade(&self) -> Option<Model<Worktree>> {
|
||||
match self {
|
||||
WorktreeHandle::Strong(handle) => Some(handle.clone()),
|
||||
WorktreeHandle::Weak(handle) => handle.upgrade(),
|
||||
}
|
||||
}
|
||||
}
|
@ -4173,7 +4173,7 @@ mod tests {
|
||||
|
||||
let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
|
||||
let worktree_id =
|
||||
cx.update(|cx| project.read(cx).worktrees().next().unwrap().read(cx).id());
|
||||
cx.update(|cx| project.read(cx).worktrees(cx).next().unwrap().read(cx).id());
|
||||
let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
let panel = workspace
|
||||
@ -4969,7 +4969,7 @@ mod tests {
|
||||
) {
|
||||
let path = path.as_ref();
|
||||
panel.update(cx, |panel, cx| {
|
||||
for worktree in panel.project.read(cx).worktrees().collect::<Vec<_>>() {
|
||||
for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
|
||||
let worktree = worktree.read(cx);
|
||||
if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
|
||||
let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
|
||||
@ -4984,7 +4984,7 @@ mod tests {
|
||||
fn select_path(panel: &View<ProjectPanel>, path: impl AsRef<Path>, cx: &mut VisualTestContext) {
|
||||
let path = path.as_ref();
|
||||
panel.update(cx, |panel, cx| {
|
||||
for worktree in panel.project.read(cx).worktrees().collect::<Vec<_>>() {
|
||||
for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
|
||||
let worktree = worktree.read(cx);
|
||||
if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
|
||||
let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
|
||||
@ -5006,7 +5006,7 @@ mod tests {
|
||||
) -> Option<ProjectEntryId> {
|
||||
let path = path.as_ref();
|
||||
panel.update(cx, |panel, cx| {
|
||||
for worktree in panel.project.read(cx).worktrees().collect::<Vec<_>>() {
|
||||
for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
|
||||
let worktree = worktree.read(cx);
|
||||
if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
|
||||
return worktree.entry_for_path(relative_path).map(|entry| entry.id);
|
||||
|
@ -534,11 +534,14 @@ impl SshClientState {
|
||||
}
|
||||
|
||||
let mut server_binary_exists = false;
|
||||
if let Ok(installed_version) = run_cmd(self.ssh_command(&dst_path).arg("version")).await {
|
||||
if cfg!(not(debug_assertions)) {
|
||||
if let Ok(installed_version) = run_cmd(self.ssh_command(&dst_path).arg("version")).await
|
||||
{
|
||||
if installed_version.trim() == version.to_string() {
|
||||
server_binary_exists = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if server_binary_exists {
|
||||
log::info!("remote development server already present",);
|
||||
|
@ -32,6 +32,7 @@ remote.workspace = true
|
||||
rpc.workspace = true
|
||||
settings.workspace = true
|
||||
smol.workspace = true
|
||||
util.workspace = true
|
||||
worktree.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
|
@ -1,7 +1,11 @@
|
||||
use anyhow::{Context as _, Result};
|
||||
use anyhow::Result;
|
||||
use fs::Fs;
|
||||
use gpui::{AppContext, AsyncAppContext, Context, Model, ModelContext};
|
||||
use project::{buffer_store::BufferStore, ProjectPath, WorktreeId, WorktreeSettings};
|
||||
use project::{
|
||||
buffer_store::{BufferStore, BufferStoreEvent},
|
||||
worktree_store::WorktreeStore,
|
||||
ProjectPath, WorktreeId, WorktreeSettings,
|
||||
};
|
||||
use remote::SshSession;
|
||||
use rpc::{
|
||||
proto::{self, AnyProtoClient, PeerId},
|
||||
@ -12,6 +16,7 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::{atomic::AtomicUsize, Arc},
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use worktree::Worktree;
|
||||
|
||||
const PEER_ID: PeerId = PeerId { owner_id: 0, id: 0 };
|
||||
@ -20,7 +25,7 @@ const PROJECT_ID: u64 = 0;
|
||||
pub struct HeadlessProject {
|
||||
pub fs: Arc<dyn Fs>,
|
||||
pub session: AnyProtoClient,
|
||||
pub worktrees: Vec<Model<Worktree>>,
|
||||
pub worktree_store: Model<WorktreeStore>,
|
||||
pub buffer_store: Model<BufferStore>,
|
||||
pub next_entry_id: Arc<AtomicUsize>,
|
||||
}
|
||||
@ -34,27 +39,45 @@ impl HeadlessProject {
|
||||
pub fn new(session: Arc<SshSession>, fs: Arc<dyn Fs>, cx: &mut ModelContext<Self>) -> Self {
|
||||
let this = cx.weak_model();
|
||||
|
||||
let worktree_store = cx.new_model(|_| WorktreeStore::new(true));
|
||||
let buffer_store = cx.new_model(|cx| BufferStore::new(worktree_store.clone(), true, cx));
|
||||
cx.subscribe(&buffer_store, Self::on_buffer_store_event)
|
||||
.detach();
|
||||
|
||||
session.add_request_handler(this.clone(), Self::handle_add_worktree);
|
||||
session.add_request_handler(this.clone(), Self::handle_open_buffer_by_path);
|
||||
session.add_request_handler(this.clone(), Self::handle_update_buffer);
|
||||
session.add_request_handler(this.clone(), Self::handle_save_buffer);
|
||||
session.add_request_handler(
|
||||
worktree_store.downgrade(),
|
||||
WorktreeStore::handle_create_project_entry,
|
||||
);
|
||||
session.add_request_handler(
|
||||
worktree_store.downgrade(),
|
||||
WorktreeStore::handle_rename_project_entry,
|
||||
);
|
||||
session.add_request_handler(
|
||||
worktree_store.downgrade(),
|
||||
WorktreeStore::handle_copy_project_entry,
|
||||
);
|
||||
session.add_request_handler(
|
||||
worktree_store.downgrade(),
|
||||
WorktreeStore::handle_delete_project_entry,
|
||||
);
|
||||
session.add_request_handler(
|
||||
worktree_store.downgrade(),
|
||||
WorktreeStore::handle_expand_project_entry,
|
||||
);
|
||||
|
||||
HeadlessProject {
|
||||
session: session.into(),
|
||||
fs,
|
||||
worktrees: Vec::new(),
|
||||
buffer_store: cx.new_model(|_| BufferStore::new(true)),
|
||||
worktree_store,
|
||||
buffer_store,
|
||||
next_entry_id: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn worktree_for_id(&self, id: WorktreeId, cx: &AppContext) -> Option<Model<Worktree>> {
|
||||
self.worktrees
|
||||
.iter()
|
||||
.find(|worktree| worktree.read(cx).id() == id)
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub async fn handle_add_worktree(
|
||||
this: Model<Self>,
|
||||
message: TypedEnvelope<proto::AddWorktree>,
|
||||
@ -74,7 +97,9 @@ impl HeadlessProject {
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let session = this.session.clone();
|
||||
this.worktrees.push(worktree.clone());
|
||||
this.worktree_store.update(cx, |worktree_store, cx| {
|
||||
worktree_store.add(&worktree, cx);
|
||||
});
|
||||
worktree.update(cx, |worktree, cx| {
|
||||
worktree.observe_updates(0, cx, move |update| {
|
||||
session.send(update).ok();
|
||||
@ -104,19 +129,8 @@ impl HeadlessProject {
|
||||
envelope: TypedEnvelope<proto::SaveBuffer>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<proto::BufferSaved> {
|
||||
let (buffer_store, worktree) = this.update(&mut cx, |this, cx| {
|
||||
let buffer_store = this.buffer_store.clone();
|
||||
let worktree = if let Some(path) = &envelope.payload.new_path {
|
||||
Some(
|
||||
this.worktree_for_id(WorktreeId::from_proto(path.worktree_id), cx)
|
||||
.context("worktree does not exist")?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
anyhow::Ok((buffer_store, worktree))
|
||||
})??;
|
||||
BufferStore::handle_save_buffer(buffer_store, PROJECT_ID, worktree, envelope, cx).await
|
||||
let buffer_store = this.update(&mut cx, |this, _| this.buffer_store.clone())?;
|
||||
BufferStore::handle_save_buffer(buffer_store, PROJECT_ID, envelope, cx).await
|
||||
}
|
||||
|
||||
pub async fn handle_open_buffer_by_path(
|
||||
@ -126,9 +140,6 @@ impl HeadlessProject {
|
||||
) -> Result<proto::OpenBufferResponse> {
|
||||
let worktree_id = WorktreeId::from_proto(message.payload.worktree_id);
|
||||
let (buffer_store, buffer, session) = this.update(&mut cx, |this, cx| {
|
||||
let worktree = this
|
||||
.worktree_for_id(worktree_id, cx)
|
||||
.context("no such worktree")?;
|
||||
let buffer_store = this.buffer_store.clone();
|
||||
let buffer = this.buffer_store.update(cx, |buffer_store, cx| {
|
||||
buffer_store.open_buffer(
|
||||
@ -136,7 +147,6 @@ impl HeadlessProject {
|
||||
worktree_id,
|
||||
path: PathBuf::from(message.payload.path).into(),
|
||||
},
|
||||
worktree,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@ -163,4 +173,41 @@ impl HeadlessProject {
|
||||
buffer_id: buffer_id.to_proto(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn on_buffer_store_event(
|
||||
&mut self,
|
||||
_: Model<BufferStore>,
|
||||
event: &BufferStoreEvent,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
BufferStoreEvent::LocalBufferUpdated { buffer } => {
|
||||
let buffer = buffer.read(cx);
|
||||
let buffer_id = buffer.remote_id();
|
||||
let Some(new_file) = buffer.file() else {
|
||||
return;
|
||||
};
|
||||
self.session
|
||||
.send(proto::UpdateBufferFile {
|
||||
project_id: 0,
|
||||
buffer_id: buffer_id.into(),
|
||||
file: Some(new_file.to_proto(cx)),
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
BufferStoreEvent::DiffBaseUpdated { buffer } => {
|
||||
let buffer = buffer.read(cx);
|
||||
let buffer_id = buffer.remote_id();
|
||||
let diff_base = buffer.diff_base();
|
||||
self.session
|
||||
.send(proto::UpdateDiffBase {
|
||||
project_id: 0,
|
||||
buffer_id: buffer_id.to_proto(),
|
||||
diff_base: diff_base.map(|b| b.to_string()),
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,6 @@ use std::{env, io, mem, process, sync::Arc};
|
||||
|
||||
fn main() {
|
||||
env::set_var("RUST_BACKTRACE", "1");
|
||||
env::set_var("RUST_LOG", "remote=trace");
|
||||
|
||||
let subcommand = std::env::args().nth(1);
|
||||
match subcommand.as_deref() {
|
||||
|
@ -12,15 +12,23 @@ use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use std::{path::Path, sync::Arc};
|
||||
|
||||
fn init_logger() {
|
||||
if std::env::var("RUST_LOG").is_ok() {
|
||||
env_logger::try_init().ok();
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
|
||||
let (client_ssh, server_ssh) = SshSession::fake(cx, server_cx);
|
||||
init_logger();
|
||||
|
||||
let fs = FakeFs::new(server_cx.executor());
|
||||
fs.insert_tree(
|
||||
"/code",
|
||||
json!({
|
||||
"project1": {
|
||||
".git": {},
|
||||
"README.md": "# project 1",
|
||||
"src": {
|
||||
"lib.rs": "fn one() -> usize { 1 }"
|
||||
@ -32,6 +40,10 @@ async fn test_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppCon
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
fs.set_index_for_repo(
|
||||
Path::new("/code/project1/.git"),
|
||||
&[(Path::new("src/lib.rs"), "fn one() -> usize { 0 }".into())],
|
||||
);
|
||||
|
||||
server_cx.update(HeadlessProject::init);
|
||||
let _headless_project =
|
||||
@ -52,6 +64,7 @@ async fn test_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppCon
|
||||
assert_eq!(
|
||||
worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
|
||||
vec![
|
||||
Path::new(".git"),
|
||||
Path::new("README.md"),
|
||||
Path::new("src"),
|
||||
Path::new("src/lib.rs"),
|
||||
@ -69,6 +82,10 @@ async fn test_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppCon
|
||||
.unwrap();
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
|
||||
assert_eq!(
|
||||
buffer.diff_base().unwrap().to_string(),
|
||||
"fn one() -> usize { 0 }"
|
||||
);
|
||||
let ix = buffer.text().find('1').unwrap();
|
||||
buffer.edit([(ix..ix + 1, "100")], None, cx);
|
||||
});
|
||||
@ -76,7 +93,7 @@ async fn test_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppCon
|
||||
// The user saves the buffer. The new contents are written to the
|
||||
// remote filesystem.
|
||||
project
|
||||
.update(cx, |project, cx| project.save_buffer(buffer, cx))
|
||||
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
@ -98,6 +115,7 @@ async fn test_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppCon
|
||||
assert_eq!(
|
||||
worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
|
||||
vec![
|
||||
Path::new(".git"),
|
||||
Path::new("README.md"),
|
||||
Path::new("src"),
|
||||
Path::new("src/lib.rs"),
|
||||
@ -105,6 +123,31 @@ async fn test_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppCon
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
// A file that is currently open in a buffer is renamed.
|
||||
fs.rename(
|
||||
"/code/project1/src/lib.rs".as_ref(),
|
||||
"/code/project1/src/lib2.rs".as_ref(),
|
||||
Default::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
cx.executor().run_until_parked();
|
||||
buffer.update(cx, |buffer, _| {
|
||||
assert_eq!(&**buffer.file().unwrap().path(), Path::new("src/lib2.rs"));
|
||||
});
|
||||
|
||||
fs.set_index_for_repo(
|
||||
Path::new("/code/project1/.git"),
|
||||
&[(Path::new("src/lib2.rs"), "fn one() -> usize { 100 }".into())],
|
||||
);
|
||||
cx.executor().run_until_parked();
|
||||
buffer.update(cx, |buffer, _| {
|
||||
assert_eq!(
|
||||
buffer.diff_base().unwrap().to_string(),
|
||||
"fn one() -> usize { 100 }"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn build_project(ssh: Arc<SshSession>, cx: &mut TestAppContext) -> Model<Project> {
|
||||
|
@ -2400,7 +2400,7 @@ pub mod tests {
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
|
||||
let worktree_id = project.read_with(cx, |project, cx| {
|
||||
project.worktrees().next().unwrap().read(cx).id()
|
||||
project.worktrees(cx).next().unwrap().read(cx).id()
|
||||
});
|
||||
let window = cx.add_window(|cx| Workspace::test_new(project, cx));
|
||||
let workspace = window.root(cx).unwrap();
|
||||
@ -2836,7 +2836,7 @@ pub mod tests {
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
|
||||
let worktree_id = project.update(cx, |this, cx| {
|
||||
this.worktrees().next().unwrap().read(cx).id()
|
||||
this.worktrees(cx).next().unwrap().read(cx).id()
|
||||
});
|
||||
|
||||
let window = cx.add_window(|cx| Workspace::test_new(project, cx));
|
||||
@ -3053,7 +3053,7 @@ pub mod tests {
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
|
||||
let worktree_id = project.update(cx, |this, cx| {
|
||||
this.worktrees().next().unwrap().read(cx).id()
|
||||
this.worktrees(cx).next().unwrap().read(cx).id()
|
||||
});
|
||||
let window = cx.add_window(|cx| Workspace::test_new(project, cx));
|
||||
let panes: Vec<_> = window
|
||||
|
@ -273,7 +273,7 @@ async fn open_buffer(
|
||||
) -> Box<dyn ItemHandle> {
|
||||
let project = workspace.update(cx, |workspace, _| workspace.project().clone());
|
||||
let worktree_id = project.update(cx, |project, cx| {
|
||||
let worktree = project.worktrees().last().expect("worktree not found");
|
||||
let worktree = project.worktrees(cx).last().expect("worktree not found");
|
||||
worktree.read(cx).id()
|
||||
});
|
||||
let project_path = ProjectPath {
|
||||
|
@ -256,7 +256,7 @@ mod tests {
|
||||
);
|
||||
|
||||
let worktree_id = project.update(cx, |project, cx| {
|
||||
project.worktrees().next().unwrap().read(cx).id()
|
||||
project.worktrees(cx).next().unwrap().read(cx).id()
|
||||
});
|
||||
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
|
||||
|
||||
|
@ -2229,7 +2229,7 @@ impl Render for Pane {
|
||||
pane.child(self.render_tab_bar(cx))
|
||||
})
|
||||
.child({
|
||||
let has_worktrees = self.project.read(cx).worktrees().next().is_some();
|
||||
let has_worktrees = self.project.read(cx).worktrees(cx).next().is_some();
|
||||
// main content
|
||||
div()
|
||||
.flex_1()
|
||||
|
@ -1077,8 +1077,8 @@ impl Workspace {
|
||||
.collect::<Vec<_>>();
|
||||
if paths_order.iter().enumerate().any(|(i, &j)| i != j) {
|
||||
project_handle
|
||||
.update(&mut cx, |project, _| {
|
||||
project.set_worktrees_reordered(true);
|
||||
.update(&mut cx, |project, cx| {
|
||||
project.set_worktrees_reordered(true, cx);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
@ -1567,7 +1567,7 @@ impl Workspace {
|
||||
}
|
||||
|
||||
pub fn worktrees<'a>(&self, cx: &'a AppContext) -> impl 'a + Iterator<Item = Model<Worktree>> {
|
||||
self.project.read(cx).worktrees()
|
||||
self.project.read(cx).worktrees(cx)
|
||||
}
|
||||
|
||||
pub fn visible_worktrees<'a>(
|
||||
@ -1861,7 +1861,7 @@ impl Workspace {
|
||||
) -> Task<Result<()>> {
|
||||
let window = cx.window_handle().downcast::<Self>();
|
||||
let is_remote = self.project.read(cx).is_remote();
|
||||
let has_worktree = self.project.read(cx).worktrees().next().is_some();
|
||||
let has_worktree = self.project.read(cx).worktrees(cx).next().is_some();
|
||||
let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx));
|
||||
|
||||
let window_to_replace = if replace_current_window {
|
||||
@ -5685,7 +5685,7 @@ mod tests {
|
||||
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
|
||||
let worktree_id = project.update(cx, |project, cx| {
|
||||
project.worktrees().next().unwrap().read(cx).id()
|
||||
project.worktrees(cx).next().unwrap().read(cx).id()
|
||||
});
|
||||
|
||||
let item1 = cx.new_view(|cx| {
|
||||
@ -6809,7 +6809,7 @@ mod tests {
|
||||
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
|
||||
|
||||
let worktree_id = project.update(cx, |project, cx| {
|
||||
project.worktrees().next().unwrap().read(cx).id()
|
||||
project.worktrees(cx).next().unwrap().read(cx).id()
|
||||
});
|
||||
|
||||
let handle = workspace
|
||||
@ -6872,7 +6872,7 @@ mod tests {
|
||||
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
|
||||
|
||||
let worktree_id = project.update(cx, |project, cx| {
|
||||
project.worktrees().next().unwrap().read(cx).id()
|
||||
project.worktrees(cx).next().unwrap().read(cx).id()
|
||||
});
|
||||
|
||||
let handle = workspace
|
||||
|
@ -354,6 +354,7 @@ struct UpdateObservationState {
|
||||
pub enum Event {
|
||||
UpdatedEntries(UpdatedEntriesSet),
|
||||
UpdatedGitRepositories(UpdatedGitRepositoriesSet),
|
||||
DeletedEntry(ProjectEntryId),
|
||||
}
|
||||
|
||||
static EMPTY_PATH: &str = "";
|
||||
@ -738,10 +739,12 @@ impl Worktree {
|
||||
trash: bool,
|
||||
cx: &mut ModelContext<Worktree>,
|
||||
) -> Option<Task<Result<()>>> {
|
||||
match self {
|
||||
let task = match self {
|
||||
Worktree::Local(this) => this.delete_entry(entry_id, trash, cx),
|
||||
Worktree::Remote(this) => this.delete_entry(entry_id, trash, cx),
|
||||
}
|
||||
}?;
|
||||
cx.emit(Event::DeletedEntry(entry_id));
|
||||
Some(task)
|
||||
}
|
||||
|
||||
pub fn rename_entry(
|
||||
@ -1208,25 +1211,10 @@ impl LocalWorktree {
|
||||
if let Some(repo_path) = repo.relativize(&snapshot, &path).log_err() {
|
||||
if let Some(git_repo) = snapshot.git_repositories.get(&*repo.work_directory) {
|
||||
let git_repo = git_repo.repo_ptr.clone();
|
||||
index_task = Some(cx.background_executor().spawn({
|
||||
let fs = fs.clone();
|
||||
let abs_path = abs_path.clone();
|
||||
async move {
|
||||
let metadata = fs
|
||||
.metadata(&abs_path)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("loading file and FS metadata for {abs_path:?}")
|
||||
})
|
||||
.log_err()
|
||||
.flatten()?;
|
||||
if metadata.is_dir || metadata.is_symlink {
|
||||
None
|
||||
} else {
|
||||
git_repo.load_index_text(&repo_path)
|
||||
}
|
||||
}
|
||||
}));
|
||||
index_task = Some(
|
||||
cx.background_executor()
|
||||
.spawn(async move { git_repo.load_index_text(&repo_path) }),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user