Detailed changes
@@ -485,9 +485,9 @@ dependencies = [
[[package]]
name = "bindgen"
-version = "0.58.1"
+version = "0.59.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0f8523b410d7187a43085e7e064416ea32ded16bd0a4e6fc025e21616d01258f"
+checksum = "2bd2a9a458e8f4304c52c43ebb0cfbd520289f8379a52e329a38afda99bf8eb8"
dependencies = [
"bitflags",
"cexpr",
@@ -503,7 +503,7 @@ dependencies = [
"regex",
"rustc-hash",
"shlex",
- "which 3.1.1",
+ "which",
]
[[package]]
@@ -616,6 +616,17 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
+[[package]]
+name = "bzip2-sys"
+version = "0.1.11+1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+]
+
[[package]]
name = "cache-padded"
version = "1.1.1"
@@ -639,11 +650,11 @@ dependencies = [
[[package]]
name = "cexpr"
-version = "0.4.0"
+version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f4aedb84272dbe89af497cf81375129abda4fc0a9e7c5d317498c15cc30c0d27"
+checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
- "nom 5.1.2",
+ "nom 7.1.1",
]
[[package]]
@@ -956,6 +967,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"client",
+ "collections",
"editor",
"futures",
"fuzzy",
@@ -1444,9 +1456,9 @@ dependencies = [
[[package]]
name = "env_logger"
-version = "0.8.3"
+version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "17392a012ea30ef05a610aa97dfb49496e71c9f676b27879922ea5bdf60d9d3f"
+checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3"
dependencies = [
"atty",
"humantime",
@@ -1539,9 +1551,9 @@ dependencies = [
[[package]]
name = "fixedbitset"
-version = "0.2.0"
+version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "37ab347416e802de484e4d03c7316c48f1ecb56574dfd4a46a80f173ce1de04d"
+checksum = "279fb028e20b3c4c320317955b77c5e0c9701f05a1d309905d6fc702cdc5053e"
[[package]]
name = "flate2"
@@ -1987,12 +1999,6 @@ dependencies = [
"tracing",
]
-[[package]]
-name = "hashbrown"
-version = "0.9.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04"
-
[[package]]
name = "hashbrown"
version = "0.11.2"
@@ -2008,7 +2014,7 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf"
dependencies = [
- "hashbrown 0.11.2",
+ "hashbrown",
]
[[package]]
@@ -2240,12 +2246,12 @@ dependencies = [
[[package]]
name = "indexmap"
-version = "1.6.2"
+version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3"
+checksum = "e6012d540c5baa3589337a98ce73408de9b5a25ec9fc2c6fd6be8f0d39e0ca5a"
dependencies = [
"autocfg 1.0.1",
- "hashbrown 0.9.1",
+ "hashbrown",
]
[[package]]
@@ -2481,9 +2487,9 @@ dependencies = [
[[package]]
name = "libc"
-version = "0.2.119"
+version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1bf2e165bb3457c8e098ea76f3e3bc9db55f87aa90d52d0e6be741470916aaa4"
+checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836"
[[package]]
name = "libloading"
@@ -2511,6 +2517,21 @@ dependencies = [
"libc",
]
+[[package]]
+name = "librocksdb-sys"
+version = "0.6.1+6.28.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81bc587013734dadb7cf23468e531aa120788b87243648be42e2d3a072186291"
+dependencies = [
+ "bindgen",
+ "bzip2-sys",
+ "cc",
+ "glob",
+ "libc",
+ "libz-sys",
+ "zstd-sys",
+]
+
[[package]]
name = "libz-sys"
version = "1.1.3"
@@ -2721,6 +2742,12 @@ version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
[[package]]
name = "miniz_oxide"
version = "0.3.7"
@@ -2849,25 +2876,25 @@ dependencies = [
[[package]]
name = "nom"
-version = "5.1.2"
+version = "6.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af"
+checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2"
dependencies = [
+ "bitvec",
+ "funty",
+ "lexical-core",
"memchr",
"version_check",
]
[[package]]
name = "nom"
-version = "6.1.2"
+version = "7.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2"
+checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36"
dependencies = [
- "bitvec",
- "funty",
- "lexical-core",
"memchr",
- "version_check",
+ "minimal-lexical",
]
[[package]]
@@ -3190,9 +3217,9 @@ checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
[[package]]
name = "petgraph"
-version = "0.5.1"
+version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "467d164a6de56270bd7c4d070df81d07beace25012d5103ced4e9ff08d6afdb7"
+checksum = "e6d5014253a1331579ce62aa67443b4a658c5e7dd03d4bc6d302b94474888143"
dependencies = [
"fixedbitset",
"indexmap",
@@ -3394,6 +3421,7 @@ dependencies = [
"postage",
"rand 0.8.3",
"regex",
+ "rocksdb",
"rpc",
"serde",
"serde_json",
@@ -3473,20 +3501,22 @@ dependencies = [
[[package]]
name = "prost-build"
-version = "0.8.0"
+version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "355f634b43cdd80724ee7848f95770e7e70eefa6dcf14fea676216573b8fd603"
+checksum = "62941722fb675d463659e49c4f3fe1fe792ff24fe5bbaa9c08cd3b98a1c354f5"
dependencies = [
"bytes",
"heck 0.3.3",
"itertools",
+ "lazy_static",
"log",
"multimap",
"petgraph",
- "prost 0.8.0",
+ "prost 0.9.0",
"prost-types",
+ "regex",
"tempfile",
- "which 4.1.0",
+ "which",
]
[[package]]
@@ -3517,12 +3547,12 @@ dependencies = [
[[package]]
name = "prost-types"
-version = "0.8.0"
+version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "603bbd6394701d13f3f25aada59c7de9d35a6a5887cfc156181234a44002771b"
+checksum = "534b7a0e836e3c482d2693070f982e39e7611da9695d4d1f5a4b186b51faef0a"
dependencies = [
"bytes",
- "prost 0.8.0",
+ "prost 0.9.0",
]
[[package]]
@@ -3710,9 +3740,9 @@ dependencies = [
[[package]]
name = "regex"
-version = "1.5.4"
+version = "1.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
+checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1"
dependencies = [
"aho-corasick",
"memchr",
@@ -3730,9 +3760,9 @@ dependencies = [
[[package]]
name = "regex-syntax"
-version = "0.6.25"
+version = "0.6.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
+checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64"
[[package]]
name = "remove_dir_all"
@@ -3819,6 +3849,16 @@ dependencies = [
"winapi 0.3.9",
]
+[[package]]
+name = "rocksdb"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "620f4129485ff1a7128d184bc687470c21c7951b64779ebc9cfdad3dcd920290"
+dependencies = [
+ "libc",
+ "librocksdb-sys",
+]
+
[[package]]
name = "roxmltree"
version = "0.14.1"
@@ -5821,20 +5861,12 @@ dependencies = [
[[package]]
name = "which"
-version = "3.1.1"
+version = "4.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d011071ae14a2f6671d0b74080ae0cd8ebf3a6f8c9589a2cd45f23126fe29724"
-dependencies = [
- "libc",
-]
-
-[[package]]
-name = "which"
-version = "4.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b55551e42cbdf2ce2bedd2203d0cc08dba002c27510f86dab6d0ce304cba3dfe"
+checksum = "5c4fb54e6113b6a8772ee41c3404fb0301ac79604489467e0a9ce1f3e97c24ae"
dependencies = [
"either",
+ "lazy_static",
"libc",
]
@@ -0,0 +1,3 @@
+<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M1.75 3V2.25C1.75 1.00734 2.75781 0 4 0C5.24219 0 6.25 1.00734 6.25 2.25V3H6.5C7.05156 3 7.5 3.44844 7.5 4V7C7.5 7.55156 7.05156 8 6.5 8H1.5C0.947656 8 0.5 7.55156 0.5 7V4C0.5 3.44844 0.947656 3 1.5 3H1.75ZM2.75 3H5.25V2.25C5.25 1.55969 4.69063 1 4 1C3.30938 1 2.75 1.55969 2.75 2.25V3Z" fill="#8B8792"/>
+</svg>
@@ -67,17 +67,23 @@ pub struct Client {
peer: Arc<Peer>,
http: Arc<dyn HttpClient>,
state: RwLock<ClientState>,
- authenticate:
+
+ #[cfg(any(test, feature = "test-support"))]
+ authenticate: RwLock<
Option<Box<dyn 'static + Send + Sync + Fn(&AsyncAppContext) -> Task<Result<Credentials>>>>,
- establish_connection: Option<
- Box<
- dyn 'static
- + Send
- + Sync
- + Fn(
- &Credentials,
- &AsyncAppContext,
- ) -> Task<Result<Connection, EstablishConnectionError>>,
+ >,
+ #[cfg(any(test, feature = "test-support"))]
+ establish_connection: RwLock<
+ Option<
+ Box<
+ dyn 'static
+ + Send
+ + Sync
+ + Fn(
+ &Credentials,
+ &AsyncAppContext,
+ ) -> Task<Result<Connection, EstablishConnectionError>>,
+ >,
>,
>,
}
@@ -235,8 +241,11 @@ impl Client {
peer: Peer::new(),
http,
state: Default::default(),
- authenticate: None,
- establish_connection: None,
+
+ #[cfg(any(test, feature = "test-support"))]
+ authenticate: Default::default(),
+ #[cfg(any(test, feature = "test-support"))]
+ establish_connection: Default::default(),
})
}
@@ -260,23 +269,23 @@ impl Client {
}
#[cfg(any(test, feature = "test-support"))]
- pub fn override_authenticate<F>(&mut self, authenticate: F) -> &mut Self
+ pub fn override_authenticate<F>(&self, authenticate: F) -> &Self
where
F: 'static + Send + Sync + Fn(&AsyncAppContext) -> Task<Result<Credentials>>,
{
- self.authenticate = Some(Box::new(authenticate));
+ *self.authenticate.write() = Some(Box::new(authenticate));
self
}
#[cfg(any(test, feature = "test-support"))]
- pub fn override_establish_connection<F>(&mut self, connect: F) -> &mut Self
+ pub fn override_establish_connection<F>(&self, connect: F) -> &Self
where
F: 'static
+ Send
+ Sync
+ Fn(&Credentials, &AsyncAppContext) -> Task<Result<Connection, EstablishConnectionError>>,
{
- self.establish_connection = Some(Box::new(connect));
+ *self.establish_connection.write() = Some(Box::new(connect));
self
}
@@ -755,11 +764,12 @@ impl Client {
}
fn authenticate(self: &Arc<Self>, cx: &AsyncAppContext) -> Task<Result<Credentials>> {
- if let Some(callback) = self.authenticate.as_ref() {
- callback(cx)
- } else {
- self.authenticate_with_browser(cx)
+ #[cfg(any(test, feature = "test-support"))]
+ if let Some(callback) = self.authenticate.read().as_ref() {
+ return callback(cx);
}
+
+ self.authenticate_with_browser(cx)
}
fn establish_connection(
@@ -767,11 +777,12 @@ impl Client {
credentials: &Credentials,
cx: &AsyncAppContext,
) -> Task<Result<Connection, EstablishConnectionError>> {
- if let Some(callback) = self.establish_connection.as_ref() {
- callback(credentials, cx)
- } else {
- self.establish_websocket_connection(credentials, cx)
+ #[cfg(any(test, feature = "test-support"))]
+ if let Some(callback) = self.establish_connection.read().as_ref() {
+ return callback(credentials, cx);
}
+
+ self.establish_websocket_connection(credentials, cx)
}
fn establish_websocket_connection(
@@ -28,7 +28,7 @@ struct FakeServerState {
impl FakeServer {
pub async fn for_client(
client_user_id: u64,
- client: &mut Arc<Client>,
+ client: &Arc<Client>,
cx: &TestAppContext,
) -> Self {
let server = Self {
@@ -38,8 +38,7 @@ impl FakeServer {
executor: cx.foreground(),
};
- Arc::get_mut(client)
- .unwrap()
+ client
.override_authenticate({
let state = Arc::downgrade(&server.state);
move |cx| {
@@ -179,6 +178,12 @@ impl FakeServer {
}
}
+impl Drop for FakeServer {
+ fn drop(&mut self) {
+ self.disconnect();
+ }
+}
+
pub struct FakeHttpClient {
handler: Box<
dyn 'static
@@ -42,7 +42,7 @@ pub struct Contact {
pub projects: Vec<ProjectMetadata>,
}
-#[derive(Debug)]
+#[derive(Clone, Debug, PartialEq)]
pub struct ProjectMetadata {
pub id: u64,
pub worktree_root_names: Vec<String>,
@@ -99,6 +99,7 @@ impl Entity for UserStore {
enum UpdateContacts {
Update(proto::UpdateContacts),
+ Wait(postage::barrier::Sender),
Clear(postage::barrier::Sender),
}
@@ -217,6 +218,10 @@ impl UserStore {
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
match message {
+ UpdateContacts::Wait(barrier) => {
+ drop(barrier);
+ Task::ready(Ok(()))
+ }
UpdateContacts::Clear(barrier) => {
self.contacts.clear();
self.incoming_contact_requests.clear();
@@ -497,6 +502,16 @@ impl UserStore {
}
}
+ pub fn contact_updates_done(&mut self) -> impl Future<Output = ()> {
+ let (tx, mut rx) = postage::barrier::channel();
+ self.update_contacts_tx
+ .unbounded_send(UpdateContacts::Wait(tx))
+ .unwrap();
+ async move {
+ rx.recv().await;
+ }
+ }
+
pub fn get_users(
&mut self,
mut user_ids: Vec<u64>,
@@ -65,7 +65,7 @@ settings = { path = "../settings", features = ["test-support"] }
theme = { path = "../theme" }
workspace = { path = "../workspace", features = ["test-support"] }
ctor = "0.1"
-env_logger = "0.8"
+env_logger = "0.9"
util = { path = "../util" }
lazy_static = "1.4"
serde_json = { version = "1.0", features = ["preserve_order"] }
@@ -7,7 +7,7 @@ use ::rpc::Peer;
use anyhow::anyhow;
use client::{
self, proto, test::FakeHttpClient, Channel, ChannelDetails, ChannelList, Client, Connection,
- Credentials, EstablishConnectionError, UserStore, RECEIVE_TIMEOUT,
+ Credentials, EstablishConnectionError, ProjectMetadata, UserStore, RECEIVE_TIMEOUT,
};
use collections::{BTreeMap, HashMap, HashSet};
use editor::{
@@ -30,7 +30,7 @@ use project::{
fs::{FakeFs, Fs as _},
search::SearchQuery,
worktree::WorktreeHandle,
- DiagnosticSummary, Project, ProjectPath, WorktreeId,
+ DiagnosticSummary, Project, ProjectPath, ProjectStore, WorktreeId,
};
use rand::prelude::*;
use rpc::PeerId;
@@ -70,28 +70,29 @@ async fn test_share_project(
cx_a.foreground().forbid_parking();
let (window_b, _) = cx_b.add_window(|_| EmptyView);
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
- let mut client_a = server.create_client(cx_a, "user_a").await;
- let mut client_b = server.create_client(cx_b, "user_b").await;
+ let client_a = server.create_client(cx_a, "user_a").await;
+ let client_b = server.create_client(cx_b, "user_b").await;
server
.make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
.await;
- let fs = FakeFs::new(cx_a.background());
- fs.insert_tree(
- "/a",
- json!({
- ".gitignore": "ignored-dir",
- "a.txt": "a-contents",
- "b.txt": "b-contents",
- "ignored-dir": {
- "c.txt": "",
- "d.txt": "",
- }
- }),
- )
- .await;
+ client_a
+ .fs
+ .insert_tree(
+ "/a",
+ json!({
+ ".gitignore": "ignored-dir",
+ "a.txt": "a-contents",
+ "b.txt": "b-contents",
+ "ignored-dir": {
+ "c.txt": "",
+ "d.txt": "",
+ }
+ }),
+ )
+ .await;
- let (project_a, worktree_id) = client_a.build_local_project(fs, "/a", cx_a).await;
+ let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
let project_id = project_a.read_with(cx_a, |project, _| project.remote_id().unwrap());
// Join that project as client B
@@ -174,9 +175,10 @@ async fn test_share_project(
project_id,
client_b2.client.clone(),
client_b2.user_store.clone(),
+ client_b2.project_store.clone(),
client_b2.language_registry.clone(),
FakeFs::new(cx_b2.background()),
- &mut cx_b2.to_async(),
+ cx_b2.to_async(),
)
.await
.unwrap();
@@ -192,10 +194,7 @@ async fn test_share_project(
});
// Dropping client B's first project removes only that from client A's collaborators.
- cx_b.update(move |_| {
- drop(client_b.project.take());
- drop(project_b);
- });
+ cx_b.update(move |_| drop(project_b));
deterministic.run_until_parked();
project_a.read_with(cx_a, |project, _| {
assert_eq!(project.collaborators().len(), 1);
@@ -213,23 +212,24 @@ async fn test_unshare_project(
) {
cx_a.foreground().forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
- let mut client_a = server.create_client(cx_a, "user_a").await;
- let mut client_b = server.create_client(cx_b, "user_b").await;
+ let client_a = server.create_client(cx_a, "user_a").await;
+ let client_b = server.create_client(cx_b, "user_b").await;
server
.make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
.await;
- let fs = FakeFs::new(cx_a.background());
- fs.insert_tree(
- "/a",
- json!({
- "a.txt": "a-contents",
- "b.txt": "b-contents",
- }),
- )
- .await;
+ client_a
+ .fs
+ .insert_tree(
+ "/a",
+ json!({
+ "a.txt": "a-contents",
+ "b.txt": "b-contents",
+ }),
+ )
+ .await;
- let (project_a, worktree_id) = client_a.build_local_project(fs, "/a", cx_a).await;
+ let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
@@ -240,10 +240,7 @@ async fn test_unshare_project(
.unwrap();
// When client B leaves the project, it gets automatically unshared.
- cx_b.update(|_| {
- drop(client_b.project.take());
- drop(project_b);
- });
+ cx_b.update(|_| drop(project_b));
deterministic.run_until_parked();
assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared()));
@@ -256,10 +253,7 @@ async fn test_unshare_project(
.unwrap();
// When client A (the host) leaves, the project gets unshared and guests are notified.
- cx_a.update(|_| {
- drop(project_a);
- client_a.project.take();
- });
+ cx_a.update(|_| drop(project_a));
deterministic.run_until_parked();
project_b2.read_with(cx_b, |project, _| {
assert!(project.is_read_only());
@@ -276,8 +270,8 @@ async fn test_host_disconnect(
) {
cx_a.foreground().forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
- let mut client_a = server.create_client(cx_a, "user_a").await;
- let mut client_b = server.create_client(cx_b, "user_b").await;
+ let client_a = server.create_client(cx_a, "user_a").await;
+ let client_b = server.create_client(cx_b, "user_b").await;
let client_c = server.create_client(cx_c, "user_c").await;
server
.make_contacts(vec![
@@ -287,17 +281,18 @@ async fn test_host_disconnect(
])
.await;
- let fs = FakeFs::new(cx_a.background());
- fs.insert_tree(
- "/a",
- json!({
- "a.txt": "a-contents",
- "b.txt": "b-contents",
- }),
- )
- .await;
+ client_a
+ .fs
+ .insert_tree(
+ "/a",
+ json!({
+ "a.txt": "a-contents",
+ "b.txt": "b-contents",
+ }),
+ )
+ .await;
- let (project_a, worktree_id) = client_a.build_local_project(fs, "/a", cx_a).await;
+ let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
let project_id = project_a.read_with(cx_a, |project, _| project.remote_id().unwrap());
@@ -310,16 +305,16 @@ async fn test_host_disconnect(
.unwrap();
// Request to join that project as client C
- let project_c = cx_c.spawn(|mut cx| async move {
+ let project_c = cx_c.spawn(|cx| {
Project::remote(
project_id,
client_c.client.clone(),
client_c.user_store.clone(),
+ client_c.project_store.clone(),
client_c.language_registry.clone(),
FakeFs::new(cx.background()),
- &mut cx,
+ cx,
)
- .await
});
deterministic.run_until_parked();
@@ -359,34 +354,28 @@ async fn test_decline_join_request(
) {
cx_a.foreground().forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
- let mut client_a = server.create_client(cx_a, "user_a").await;
+ let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
.make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
.await;
- let fs = FakeFs::new(cx_a.background());
- fs.insert_tree("/a", json!({})).await;
+ client_a.fs.insert_tree("/a", json!({})).await;
- let (project_a, _) = client_a.build_local_project(fs, "/a", cx_a).await;
+ let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
let project_id = project_a.read_with(cx_a, |project, _| project.remote_id().unwrap());
// Request to join that project as client B
- let project_b = cx_b.spawn(|mut cx| {
- let client = client_b.client.clone();
- let user_store = client_b.user_store.clone();
- let language_registry = client_b.language_registry.clone();
- async move {
- Project::remote(
- project_id,
- client,
- user_store,
- language_registry,
- FakeFs::new(cx.background()),
- &mut cx,
- )
- .await
- }
+ let project_b = cx_b.spawn(|cx| {
+ Project::remote(
+ project_id,
+ client_b.client.clone(),
+ client_b.user_store.clone(),
+ client_b.project_store.clone(),
+ client_b.language_registry.clone(),
+ FakeFs::new(cx.background()),
+ cx,
+ )
});
deterministic.run_until_parked();
project_a.update(cx_a, |project, cx| {
@@ -398,28 +387,21 @@ async fn test_decline_join_request(
));
// Request to join the project again as client B
- let project_b = cx_b.spawn(|mut cx| {
- let client = client_b.client.clone();
- let user_store = client_b.user_store.clone();
- async move {
- Project::remote(
- project_id,
- client,
- user_store,
- client_b.language_registry.clone(),
- FakeFs::new(cx.background()),
- &mut cx,
- )
- .await
- }
+ let project_b = cx_b.spawn(|cx| {
+ Project::remote(
+ project_id,
+ client_b.client.clone(),
+ client_b.user_store.clone(),
+ client_b.project_store.clone(),
+ client_b.language_registry.clone(),
+ FakeFs::new(cx.background()),
+ cx,
+ )
});
// Close the project on the host
deterministic.run_until_parked();
- cx_a.update(|_| {
- drop(project_a);
- client_a.project.take();
- });
+ cx_a.update(|_| drop(project_a));
deterministic.run_until_parked();
assert!(matches!(
project_b.await.unwrap_err(),
@@ -435,16 +417,14 @@ async fn test_cancel_join_request(
) {
cx_a.foreground().forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
- let mut client_a = server.create_client(cx_a, "user_a").await;
+ let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
.make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
.await;
- let fs = FakeFs::new(cx_a.background());
- fs.insert_tree("/a", json!({})).await;
-
- let (project_a, _) = client_a.build_local_project(fs, "/a", cx_a).await;
+ client_a.fs.insert_tree("/a", json!({})).await;
+ let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
let project_id = project_a.read_with(cx_a, |project, _| project.remote_id().unwrap());
let user_b = client_a
@@ -467,21 +447,16 @@ async fn test_cancel_join_request(
});
// Request to join that project as client B
- let project_b = cx_b.spawn(|mut cx| {
- let client = client_b.client.clone();
- let user_store = client_b.user_store.clone();
- let language_registry = client_b.language_registry.clone();
- async move {
- Project::remote(
- project_id,
- client,
- user_store,
- language_registry.clone(),
- FakeFs::new(cx.background()),
- &mut cx,
- )
- .await
- }
+ let project_b = cx_b.spawn(|cx| {
+ Project::remote(
+ project_id,
+ client_b.client.clone(),
+ client_b.user_store.clone(),
+ client_b.project_store.clone(),
+ client_b.language_registry.clone().clone(),
+ FakeFs::new(cx.background()),
+ cx,
+ )
});
deterministic.run_until_parked();
assert_eq!(
@@ -504,6 +479,241 @@ async fn test_cancel_join_request(
);
}
+#[gpui::test(iterations = 10)]
+async fn test_offline_projects(
+ deterministic: Arc<Deterministic>,
+ cx_a: &mut TestAppContext,
+ cx_b: &mut TestAppContext,
+) {
+ cx_a.foreground().forbid_parking();
+ let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
+ let client_a = server.create_client(cx_a, "user_a").await;
+ let client_b = server.create_client(cx_b, "user_b").await;
+ let user_a = UserId::from_proto(client_a.user_id().unwrap());
+ server
+ .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
+ .await;
+
+ // Set up observers of the project and user stores. Any time either of
+ // these models update, they should be in a consistent state with each
+ // other. There should not be an observable moment where the current
+ // user's contact entry contains a project that does not match one of
+ // the current open projects. That would cause a duplicate entry to be
+ // shown in the contacts panel.
+ let mut subscriptions = vec![];
+ let (window_id, view) = cx_a.add_window(|cx| {
+ subscriptions.push(cx.observe(&client_a.user_store, {
+ let project_store = client_a.project_store.clone();
+ let user_store = client_a.user_store.clone();
+ move |_, _, cx| check_project_list(project_store.clone(), user_store.clone(), cx)
+ }));
+
+ subscriptions.push(cx.observe(&client_a.project_store, {
+ let project_store = client_a.project_store.clone();
+ let user_store = client_a.user_store.clone();
+ move |_, _, cx| check_project_list(project_store.clone(), user_store.clone(), cx)
+ }));
+
+ fn check_project_list(
+ project_store: ModelHandle<ProjectStore>,
+ user_store: ModelHandle<UserStore>,
+ cx: &mut gpui::MutableAppContext,
+ ) {
+ let open_project_ids = project_store
+ .read(cx)
+ .projects(cx)
+ .filter_map(|project| project.read(cx).remote_id())
+ .collect::<Vec<_>>();
+
+ let user_store = user_store.read(cx);
+ for contact in user_store.contacts() {
+ if contact.user.id == user_store.current_user().unwrap().id {
+ for project in &contact.projects {
+ if !open_project_ids.contains(&project.id) {
+ panic!(
+ concat!(
+ "current user's contact data has a project",
+ "that doesn't match any open project {:?}",
+ ),
+ project
+ );
+ }
+ }
+ }
+ }
+ }
+
+ EmptyView
+ });
+
+ // Build an offline project with two worktrees.
+ client_a
+ .fs
+ .insert_tree(
+ "/code",
+ json!({
+ "crate1": { "a.rs": "" },
+ "crate2": { "b.rs": "" },
+ }),
+ )
+ .await;
+ let project = cx_a.update(|cx| {
+ Project::local(
+ false,
+ client_a.client.clone(),
+ client_a.user_store.clone(),
+ client_a.project_store.clone(),
+ client_a.language_registry.clone(),
+ client_a.fs.clone(),
+ cx,
+ )
+ });
+ project
+ .update(cx_a, |p, cx| {
+ p.find_or_create_local_worktree("/code/crate1", true, cx)
+ })
+ .await
+ .unwrap();
+ project
+ .update(cx_a, |p, cx| {
+ p.find_or_create_local_worktree("/code/crate2", true, cx)
+ })
+ .await
+ .unwrap();
+ project
+ .update(cx_a, |p, cx| p.restore_state(cx))
+ .await
+ .unwrap();
+
+ // When a project is offline, no information about it is sent to the server.
+ deterministic.run_until_parked();
+ assert!(server
+ .store
+ .read()
+ .await
+ .project_metadata_for_user(user_a)
+ .is_empty());
+ assert!(project.read_with(cx_a, |project, _| project.remote_id().is_none()));
+ assert!(client_b
+ .user_store
+ .read_with(cx_b, |store, _| { store.contacts()[0].projects.is_empty() }));
+
+ // When the project is taken online, its metadata is sent to the server
+ // and broadcasted to other users.
+ project.update(cx_a, |p, cx| p.set_online(true, cx));
+ deterministic.run_until_parked();
+ let project_id = project.read_with(cx_a, |p, _| p.remote_id()).unwrap();
+ client_b.user_store.read_with(cx_b, |store, _| {
+ assert_eq!(
+ store.contacts()[0].projects,
+ &[ProjectMetadata {
+ id: project_id,
+ worktree_root_names: vec!["crate1".into(), "crate2".into()],
+ guests: Default::default(),
+ }]
+ );
+ });
+
+ // The project is registered again when the host loses and regains connection.
+ server.disconnect_client(user_a);
+ server.forbid_connections();
+ cx_a.foreground().advance_clock(rpc::RECEIVE_TIMEOUT);
+ assert!(server
+ .store
+ .read()
+ .await
+ .project_metadata_for_user(user_a)
+ .is_empty());
+ assert!(project.read_with(cx_a, |p, _| p.remote_id().is_none()));
+ assert!(client_b
+ .user_store
+ .read_with(cx_b, |store, _| { store.contacts()[0].projects.is_empty() }));
+
+ server.allow_connections();
+ cx_b.foreground().advance_clock(Duration::from_secs(10));
+ let project_id = project.read_with(cx_a, |p, _| p.remote_id()).unwrap();
+ client_b.user_store.read_with(cx_b, |store, _| {
+ assert_eq!(
+ store.contacts()[0].projects,
+ &[ProjectMetadata {
+ id: project_id,
+ worktree_root_names: vec!["crate1".into(), "crate2".into()],
+ guests: Default::default(),
+ }]
+ );
+ });
+
+ project
+ .update(cx_a, |p, cx| {
+ p.find_or_create_local_worktree("/code/crate3", true, cx)
+ })
+ .await
+ .unwrap();
+ deterministic.run_until_parked();
+ client_b.user_store.read_with(cx_b, |store, _| {
+ assert_eq!(
+ store.contacts()[0].projects,
+ &[ProjectMetadata {
+ id: project_id,
+ worktree_root_names: vec!["crate1".into(), "crate2".into(), "crate3".into()],
+ guests: Default::default(),
+ }]
+ );
+ });
+
+ // Build another project using a directory which was previously part of
+ // an online project. Restore the project's state from the host's database.
+ let project2 = cx_a.update(|cx| {
+ Project::local(
+ false,
+ client_a.client.clone(),
+ client_a.user_store.clone(),
+ client_a.project_store.clone(),
+ client_a.language_registry.clone(),
+ client_a.fs.clone(),
+ cx,
+ )
+ });
+ project2
+ .update(cx_a, |p, cx| {
+ p.find_or_create_local_worktree("/code/crate3", true, cx)
+ })
+ .await
+ .unwrap();
+ project2
+ .update(cx_a, |project, cx| project.restore_state(cx))
+ .await
+ .unwrap();
+
+ // This project is now online, because its directory was previously online.
+ project2.read_with(cx_a, |project, _| assert!(project.is_online()));
+ deterministic.run_until_parked();
+ let project2_id = project2.read_with(cx_a, |p, _| p.remote_id()).unwrap();
+ client_b.user_store.read_with(cx_b, |store, _| {
+ assert_eq!(
+ store.contacts()[0].projects,
+ &[
+ ProjectMetadata {
+ id: project_id,
+ worktree_root_names: vec!["crate1".into(), "crate2".into(), "crate3".into()],
+ guests: Default::default(),
+ },
+ ProjectMetadata {
+ id: project2_id,
+ worktree_root_names: vec!["crate3".into()],
+ guests: Default::default(),
+ }
+ ]
+ );
+ });
+
+ cx_a.update(|cx| {
+ drop(subscriptions);
+ drop(view);
+ cx.remove_window(window_id);
+ });
+}
+
#[gpui::test(iterations = 10)]
async fn test_propagate_saves_and_fs_changes(
cx_a: &mut TestAppContext,
@@ -512,9 +722,9 @@ async fn test_propagate_saves_and_fs_changes(
) {
cx_a.foreground().forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
- let mut client_a = server.create_client(cx_a, "user_a").await;
- let mut client_b = server.create_client(cx_b, "user_b").await;
- let mut client_c = server.create_client(cx_c, "user_c").await;
+ let client_a = server.create_client(cx_a, "user_a").await;
+ let client_b = server.create_client(cx_b, "user_b").await;
+ let client_c = server.create_client(cx_c, "user_c").await;
server
.make_contacts(vec![
(&client_a, cx_a),
@@ -523,17 +733,17 @@ async fn test_propagate_saves_and_fs_changes(
])
.await;
- let fs = FakeFs::new(cx_a.background());
- fs.insert_tree(
- "/a",
- json!({
- "file1": "",
- "file2": ""
- }),
- )
- .await;
-
- let (project_a, worktree_id) = client_a.build_local_project(fs.clone(), "/a", cx_a).await;
+ client_a
+ .fs
+ .insert_tree(
+ "/a",
+ json!({
+ "file1": "",
+ "file2": ""
+ }),
+ )
+ .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, cx| p.worktrees(cx).next().unwrap());
// Join that worktree as clients B and C.
@@ -583,7 +793,7 @@ async fn test_propagate_saves_and_fs_changes(
buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "hi-a, ")], cx));
save_b.await.unwrap();
assert_eq!(
- fs.load("/a/file1".as_ref()).await.unwrap(),
+ client_a.fs.load("/a/file1".as_ref()).await.unwrap(),
"hi-a, i-am-c, i-am-b, i-am-a"
);
buffer_a.read_with(cx_a, |buf, _| assert!(!buf.is_dirty()));
@@ -593,18 +803,22 @@ async fn test_propagate_saves_and_fs_changes(
worktree_a.flush_fs_events(cx_a).await;
// Make changes on host's file system, see those changes on guest worktrees.
- fs.rename(
- "/a/file1".as_ref(),
- "/a/file1-renamed".as_ref(),
- Default::default(),
- )
- .await
- .unwrap();
+ client_a
+ .fs
+ .rename(
+ "/a/file1".as_ref(),
+ "/a/file1-renamed".as_ref(),
+ Default::default(),
+ )
+ .await
+ .unwrap();
- fs.rename("/a/file2".as_ref(), "/a/file3".as_ref(), Default::default())
+ client_a
+ .fs
+ .rename("/a/file2".as_ref(), "/a/file3".as_ref(), Default::default())
.await
.unwrap();
- fs.insert_file(Path::new("/a/file4"), "4".into()).await;
+ client_a.fs.insert_file("/a/file4", "4".into()).await;
worktree_a
.condition(&cx_a, |tree, _| {
@@ -656,27 +870,24 @@ async fn test_fs_operations(
cx_b: &mut TestAppContext,
) {
executor.forbid_parking();
- let fs = FakeFs::new(cx_a.background());
-
- // Connect to a server as 2 clients.
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
- let mut client_a = server.create_client(cx_a, "user_a").await;
- let mut client_b = server.create_client(cx_b, "user_b").await;
+ let client_a = server.create_client(cx_a, "user_a").await;
+ let client_b = server.create_client(cx_b, "user_b").await;
server
.make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
.await;
- // Share a project as client A
- fs.insert_tree(
- "/dir",
- json!({
- "a.txt": "a-contents",
- "b.txt": "b-contents",
- }),
- )
- .await;
-
- let (project_a, worktree_id) = client_a.build_local_project(fs, "/dir", cx_a).await;
+ client_a
+ .fs
+ .insert_tree(
+ "/dir",
+ json!({
+ "a.txt": "a-contents",
+ "b.txt": "b-contents",
+ }),
+ )
+ .await;
+ let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
@@ -921,22 +1132,22 @@ async fn test_fs_operations(
async fn test_buffer_conflict_after_save(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
cx_a.foreground().forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
- let mut client_a = server.create_client(cx_a, "user_a").await;
- let mut client_b = server.create_client(cx_b, "user_b").await;
+ let client_a = server.create_client(cx_a, "user_a").await;
+ let client_b = server.create_client(cx_b, "user_b").await;
server
.make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
.await;
- let fs = FakeFs::new(cx_a.background());
- fs.insert_tree(
- "/dir",
- json!({
- "a.txt": "a-contents",
- }),
- )
- .await;
-
- let (project_a, worktree_id) = client_a.build_local_project(fs, "/dir", cx_a).await;
+ client_a
+ .fs
+ .insert_tree(
+ "/dir",
+ json!({
+ "a.txt": "a-contents",
+ }),
+ )
+ .await;
+ let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
// Open a buffer as client B
@@ -970,22 +1181,22 @@ async fn test_buffer_conflict_after_save(cx_a: &mut TestAppContext, cx_b: &mut T
async fn test_buffer_reloading(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
cx_a.foreground().forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
- let mut client_a = server.create_client(cx_a, "user_a").await;
- let mut client_b = server.create_client(cx_b, "user_b").await;
+ let client_a = server.create_client(cx_a, "user_a").await;
+ let client_b = server.create_client(cx_b, "user_b").await;
server
.make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
.await;
- let fs = FakeFs::new(cx_a.background());
- fs.insert_tree(
- "/dir",
- json!({
- "a.txt": "a-contents",
- }),
- )
- .await;
-
- let (project_a, worktree_id) = client_a.build_local_project(fs.clone(), "/dir", cx_a).await;
+ client_a
+ .fs
+ .insert_tree(
+ "/dir",
+ json!({
+ "a.txt": "a-contents",
+ }),
+ )
+ .await;
+ let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
// Open a buffer as client B
@@ -998,7 +1209,9 @@ async fn test_buffer_reloading(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
assert!(!buf.has_conflict());
});
- fs.save(Path::new("/dir/a.txt"), &"new contents".into())
+ client_a
+ .fs
+ .save("/dir/a.txt".as_ref(), &"new contents".into())
.await
.unwrap();
buffer_b
@@ -1006,9 +1219,7 @@ async fn test_buffer_reloading(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
buf.text() == "new contents" && !buf.is_dirty()
})
.await;
- buffer_b.read_with(cx_b, |buf, _| {
- assert!(!buf.has_conflict());
- });
+ buffer_b.read_with(cx_b, |buf, _| assert!(!buf.has_conflict()));
}
#[gpui::test(iterations = 10)]
@@ -1018,22 +1229,17 @@ async fn test_editing_while_guest_opens_buffer(
) {
cx_a.foreground().forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
- let mut client_a = server.create_client(cx_a, "user_a").await;
- let mut client_b = server.create_client(cx_b, "user_b").await;
+ let client_a = server.create_client(cx_a, "user_a").await;
+ let client_b = server.create_client(cx_b, "user_b").await;
server
.make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
.await;
- let fs = FakeFs::new(cx_a.background());
- fs.insert_tree(
- "/dir",
- json!({
- "a.txt": "a-contents",
- }),
- )
- .await;
-
- let (project_a, worktree_id) = client_a.build_local_project(fs, "/dir", cx_a).await;
+ client_a
+ .fs
+ .insert_tree("/dir", json!({ "a.txt": "a-contents" }))
+ .await;
+ let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
// Open a buffer as client A
@@ -1065,22 +1271,17 @@ async fn test_leaving_worktree_while_opening_buffer(
) {
cx_a.foreground().forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
- let mut client_a = server.create_client(cx_a, "user_a").await;
- let mut client_b = server.create_client(cx_b, "user_b").await;
+ let client_a = server.create_client(cx_a, "user_a").await;
+ let client_b = server.create_client(cx_b, "user_b").await;
server
.make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
.await;
- let fs = FakeFs::new(cx_a.background());
- fs.insert_tree(
- "/dir",
- json!({
- "a.txt": "a-contents",
- }),
- )
- .await;
-
- let (project_a, worktree_id) = client_a.build_local_project(fs, "/dir", cx_a).await;
+ client_a
+ .fs
+ .insert_tree("/dir", json!({ "a.txt": "a-contents" }))
+ .await;
+ let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
// See that a guest has joined as client A.
@@ -1092,10 +1293,7 @@ async fn test_leaving_worktree_while_opening_buffer(
let buffer_b = cx_b
.background()
.spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)));
- cx_b.update(|_| {
- drop(client_b.project.take());
- drop(project_b);
- });
+ cx_b.update(|_| drop(project_b));
drop(buffer_b);
// See that the guest has left.
@@ -1108,23 +1306,23 @@ async fn test_leaving_worktree_while_opening_buffer(
async fn test_leaving_project(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
cx_a.foreground().forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
- let mut client_a = server.create_client(cx_a, "user_a").await;
- let mut client_b = server.create_client(cx_b, "user_b").await;
+ let client_a = server.create_client(cx_a, "user_a").await;
+ let client_b = server.create_client(cx_b, "user_b").await;
server
.make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
.await;
- let fs = FakeFs::new(cx_a.background());
- fs.insert_tree(
- "/a",
- json!({
- "a.txt": "a-contents",
- "b.txt": "b-contents",
- }),
- )
- .await;
-
- let (project_a, _) = client_a.build_local_project(fs, "/a", cx_a).await;
+ client_a
+ .fs
+ .insert_tree(
+ "/a",
+ json!({
+ "a.txt": "a-contents",
+ "b.txt": "b-contents",
+ }),
+ )
+ .await;
+ let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
let _project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
// Client A sees that a guest has joined.
@@ -1164,9 +1362,9 @@ async fn test_collaborating_with_diagnostics(
) {
deterministic.forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
- let mut client_a = server.create_client(cx_a, "user_a").await;
- let mut client_b = server.create_client(cx_b, "user_b").await;
- let mut client_c = server.create_client(cx_c, "user_c").await;
+ let client_a = server.create_client(cx_a, "user_a").await;
+ let client_b = server.create_client(cx_b, "user_b").await;
+ let client_c = server.create_client(cx_c, "user_c").await;
server
.make_contacts(vec![
(&client_a, cx_a),
@@ -1187,19 +1385,18 @@ async fn test_collaborating_with_diagnostics(
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
client_a.language_registry.add(Arc::new(language));
- // Connect to a server as 2 clients.
-
// Share a project as client A
- let fs = FakeFs::new(cx_a.background());
- fs.insert_tree(
- "/a",
- json!({
- "a.rs": "let one = two",
- "other.rs": "",
- }),
- )
- .await;
- let (project_a, worktree_id) = client_a.build_local_project(fs, "/a", cx_a).await;
+ client_a
+ .fs
+ .insert_tree(
+ "/a",
+ json!({
+ "a.rs": "let one = two",
+ "other.rs": "",
+ }),
+ )
+ .await;
+ let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await;
// Cause the language server to start.
@@ -1404,8 +1601,8 @@ async fn test_collaborating_with_diagnostics(
async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
cx_a.foreground().forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
- let mut client_a = server.create_client(cx_a, "user_a").await;
- let mut client_b = server.create_client(cx_b, "user_b").await;
+ let client_a = server.create_client(cx_a, "user_a").await;
+ let client_b = server.create_client(cx_b, "user_b").await;
server
.make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
.await;
@@ -1431,17 +1628,17 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
});
client_a.language_registry.add(Arc::new(language));
- let fs = FakeFs::new(cx_a.background());
- fs.insert_tree(
- "/a",
- json!({
- "main.rs": "fn main() { a }",
- "other.rs": "",
- }),
- )
- .await;
-
- let (project_a, worktree_id) = client_a.build_local_project(fs, "/a", cx_a).await;
+ client_a
+ .fs
+ .insert_tree(
+ "/a",
+ json!({
+ "main.rs": "fn main() { a }",
+ "other.rs": "",
+ }),
+ )
+ .await;
+ let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
// Open a file in an editor as the guest.
@@ -82,7 +82,6 @@ pub fn init_tracing(config: &Config) -> Option<()> {
use tracing_subscriber::layer::SubscriberExt;
let rust_log = config.rust_log.clone()?;
- println!("HEY!");
LogTracer::init().log_err()?;
let subscriber = tracing_subscriber::Registry::default()
@@ -141,12 +141,11 @@ impl Server {
server
.add_request_handler(Server::ping)
.add_request_handler(Server::register_project)
- .add_message_handler(Server::unregister_project)
+ .add_request_handler(Server::unregister_project)
.add_request_handler(Server::join_project)
.add_message_handler(Server::leave_project)
.add_message_handler(Server::respond_to_join_project_request)
- .add_request_handler(Server::register_worktree)
- .add_message_handler(Server::unregister_worktree)
+ .add_message_handler(Server::update_project)
.add_request_handler(Server::update_worktree)
.add_message_handler(Server::start_language_server)
.add_message_handler(Server::update_language_server)
@@ -477,21 +476,22 @@ impl Server {
request: TypedEnvelope<proto::RegisterProject>,
response: Response<proto::RegisterProject>,
) -> Result<()> {
- let user_id;
let project_id;
{
let mut state = self.store_mut().await;
- user_id = state.user_id_for_connection(request.sender_id)?;
+ let user_id = state.user_id_for_connection(request.sender_id)?;
project_id = state.register_project(request.sender_id, user_id);
};
- self.update_user_contacts(user_id).await?;
+
response.send(proto::RegisterProjectResponse { project_id })?;
+
Ok(())
}
async fn unregister_project(
self: Arc<Server>,
request: TypedEnvelope<proto::UnregisterProject>,
+ response: Response<proto::UnregisterProject>,
) -> Result<()> {
let (user_id, project) = {
let mut state = self.store_mut().await;
@@ -528,7 +528,13 @@ impl Server {
}
}
+ // Send out the `UpdateContacts` message before responding to the unregister
+ // request. This way, when the project's host can keep track of the project's
+ // remote id until after they've received the `UpdateContacts` message for
+ // themself.
self.update_user_contacts(user_id).await?;
+ response.send(proto::Ack {})?;
+
Ok(())
}
@@ -568,6 +574,7 @@ impl Server {
response: Response<proto::JoinProject>,
) -> Result<()> {
let project_id = request.payload.project_id;
+
let host_user_id;
let guest_user_id;
let host_connection_id;
@@ -768,63 +775,28 @@ impl Server {
Ok(())
}
- async fn register_worktree(
+ async fn update_project(
self: Arc<Server>,
- request: TypedEnvelope<proto::RegisterWorktree>,
- response: Response<proto::RegisterWorktree>,
+ request: TypedEnvelope<proto::UpdateProject>,
) -> Result<()> {
- let host_user_id;
+ let user_id;
{
let mut state = self.store_mut().await;
- host_user_id = state.user_id_for_connection(request.sender_id)?;
-
+ user_id = state.user_id_for_connection(request.sender_id)?;
let guest_connection_ids = state
.read_project(request.payload.project_id, request.sender_id)?
.guest_connection_ids();
- state.register_worktree(
+ state.update_project(
request.payload.project_id,
- request.payload.worktree_id,
+ &request.payload.worktrees,
request.sender_id,
- Worktree {
- root_name: request.payload.root_name.clone(),
- visible: request.payload.visible,
- ..Default::default()
- },
)?;
-
broadcast(request.sender_id, guest_connection_ids, |connection_id| {
self.peer
.forward_send(request.sender_id, connection_id, request.payload.clone())
});
- }
- self.update_user_contacts(host_user_id).await?;
- response.send(proto::Ack {})?;
- Ok(())
- }
-
- async fn unregister_worktree(
- self: Arc<Server>,
- request: TypedEnvelope<proto::UnregisterWorktree>,
- ) -> Result<()> {
- let host_user_id;
- let project_id = request.payload.project_id;
- let worktree_id = request.payload.worktree_id;
- {
- let mut state = self.store_mut().await;
- let (_, guest_connection_ids) =
- state.unregister_worktree(project_id, worktree_id, request.sender_id)?;
- host_user_id = state.user_id_for_connection(request.sender_id)?;
- broadcast(request.sender_id, guest_connection_ids, |conn_id| {
- self.peer.send(
- conn_id,
- proto::UnregisterWorktree {
- project_id,
- worktree_id,
- },
- )
- });
- }
- self.update_user_contacts(host_user_id).await?;
+ };
+ self.update_user_contacts(user_id).await?;
Ok(())
}
@@ -833,10 +805,11 @@ impl Server {
request: TypedEnvelope<proto::UpdateWorktree>,
response: Response<proto::UpdateWorktree>,
) -> Result<()> {
- let connection_ids = self.store_mut().await.update_worktree(
+ let (connection_ids, metadata_changed) = self.store_mut().await.update_worktree(
request.sender_id,
request.payload.project_id,
request.payload.worktree_id,
+ &request.payload.root_name,
&request.payload.removed_entries,
&request.payload.updated_entries,
request.payload.scan_id,
@@ -846,6 +819,13 @@ impl Server {
self.peer
.forward_send(request.sender_id, connection_id, request.payload.clone())
});
+ if metadata_changed {
+ let user_id = self
+ .store()
+ .await
+ .user_id_for_connection(request.sender_id)?;
+ self.update_user_contacts(user_id).await?;
+ }
response.send(proto::Ack {})?;
Ok(())
}
@@ -32,7 +32,7 @@ pub struct Project {
#[serde(skip)]
pub join_requests: HashMap<UserId, Vec<Receipt<proto::JoinProject>>>,
pub active_replica_ids: HashSet<ReplicaId>,
- pub worktrees: HashMap<u64, Worktree>,
+ pub worktrees: BTreeMap<u64, Worktree>,
pub language_servers: Vec<proto::LanguageServer>,
}
@@ -312,19 +312,32 @@ impl Store {
project_id
}
- pub fn register_worktree(
+ pub fn update_project(
&mut self,
project_id: u64,
- worktree_id: u64,
+ worktrees: &[proto::WorktreeMetadata],
connection_id: ConnectionId,
- worktree: Worktree,
) -> Result<()> {
let project = self
.projects
.get_mut(&project_id)
.ok_or_else(|| anyhow!("no such project"))?;
if project.host_connection_id == connection_id {
- project.worktrees.insert(worktree_id, worktree);
+ let mut old_worktrees = mem::take(&mut project.worktrees);
+ for worktree in worktrees {
+ if let Some(old_worktree) = old_worktrees.remove(&worktree.id) {
+ project.worktrees.insert(worktree.id, old_worktree);
+ } else {
+ project.worktrees.insert(
+ worktree.id,
+ Worktree {
+ root_name: worktree.root_name.clone(),
+ visible: worktree.visible,
+ ..Default::default()
+ },
+ );
+ }
+ }
Ok(())
} else {
Err(anyhow!("no such project"))?
@@ -374,27 +387,6 @@ impl Store {
}
}
- pub fn unregister_worktree(
- &mut self,
- project_id: u64,
- worktree_id: u64,
- acting_connection_id: ConnectionId,
- ) -> Result<(Worktree, Vec<ConnectionId>)> {
- let project = self
- .projects
- .get_mut(&project_id)
- .ok_or_else(|| anyhow!("no such project"))?;
- if project.host_connection_id != acting_connection_id {
- Err(anyhow!("not your worktree"))?;
- }
-
- let worktree = project
- .worktrees
- .remove(&worktree_id)
- .ok_or_else(|| anyhow!("no such worktree"))?;
- Ok((worktree, project.guest_connection_ids()))
- }
-
pub fn update_diagnostic_summary(
&mut self,
project_id: u64,
@@ -573,15 +565,15 @@ impl Store {
connection_id: ConnectionId,
project_id: u64,
worktree_id: u64,
+ worktree_root_name: &str,
removed_entries: &[u64],
updated_entries: &[proto::Entry],
scan_id: u64,
- ) -> Result<Vec<ConnectionId>> {
+ ) -> Result<(Vec<ConnectionId>, bool)> {
let project = self.write_project(project_id, connection_id)?;
- let worktree = project
- .worktrees
- .get_mut(&worktree_id)
- .ok_or_else(|| anyhow!("no such worktree"))?;
+ let mut worktree = project.worktrees.entry(worktree_id).or_default();
+ let metadata_changed = worktree_root_name != worktree.root_name;
+ worktree.root_name = worktree_root_name.to_string();
for entry_id in removed_entries {
worktree.entries.remove(&entry_id);
}
@@ -590,7 +582,7 @@ impl Store {
}
worktree.scan_id = scan_id;
let connection_ids = project.connection_ids();
- Ok(connection_ids)
+ Ok((connection_ids, metadata_changed))
}
pub fn project_connection_ids(
@@ -25,4 +25,4 @@ project = { path = "../project", features = ["test-support"] }
serde_json = { version = "1.0", features = ["preserve_order"] }
workspace = { path = "../workspace", features = ["test-support"] }
ctor = "0.1"
-env_logger = "0.8"
+env_logger = "0.9"
@@ -9,6 +9,7 @@ doctest = false
[dependencies]
client = { path = "../client" }
+collections = { path = "../collections" }
editor = { path = "../editor" }
fuzzy = { path = "../fuzzy" }
gpui = { path = "../gpui" }
@@ -13,15 +13,16 @@ use gpui::{
impl_actions, impl_internal_actions,
platform::CursorStyle,
AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle, MutableAppContext,
- RenderContext, Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
+ RenderContext, Subscription, View, ViewContext, ViewHandle, WeakModelHandle, WeakViewHandle,
};
use join_project_notification::JoinProjectNotification;
use menu::{Confirm, SelectNext, SelectPrev};
+use project::{Project, ProjectStore};
use serde::Deserialize;
use settings::Settings;
-use std::sync::Arc;
+use std::{ops::DerefMut, sync::Arc};
use theme::IconButton;
-use workspace::{sidebar::SidebarItem, JoinProject, Workspace};
+use workspace::{sidebar::SidebarItem, JoinProject, ToggleProjectOnline, Workspace};
impl_actions!(
contacts_panel,
@@ -37,13 +38,14 @@ enum Section {
Offline,
}
-#[derive(Clone, Debug)]
+#[derive(Clone)]
enum ContactEntry {
Header(Section),
IncomingRequest(Arc<User>),
OutgoingRequest(Arc<User>),
Contact(Arc<Contact>),
- ContactProject(Arc<Contact>, usize),
+ ContactProject(Arc<Contact>, usize, Option<WeakModelHandle<Project>>),
+ OfflineProject(WeakModelHandle<Project>),
}
#[derive(Clone)]
@@ -54,6 +56,7 @@ pub struct ContactsPanel {
match_candidates: Vec<StringMatchCandidate>,
list_state: ListState,
user_store: ModelHandle<UserStore>,
+ project_store: ModelHandle<ProjectStore>,
filter_editor: ViewHandle<Editor>,
collapsed_sections: Vec<Section>,
selection: Option<usize>,
@@ -89,6 +92,7 @@ pub fn init(cx: &mut MutableAppContext) {
impl ContactsPanel {
pub fn new(
user_store: ModelHandle<UserStore>,
+ project_store: ModelHandle<ProjectStore>,
workspace: WeakViewHandle<Workspace>,
cx: &mut ViewContext<Self>,
) -> Self {
@@ -148,93 +152,99 @@ impl ContactsPanel {
}
});
- cx.subscribe(&user_store, {
- let user_store = user_store.downgrade();
- move |_, _, event, cx| {
- if let Some((workspace, user_store)) =
- workspace.upgrade(cx).zip(user_store.upgrade(cx))
- {
- workspace.update(cx, |workspace, cx| match event {
- client::Event::Contact { user, kind } => match kind {
- ContactEventKind::Requested | ContactEventKind::Accepted => workspace
- .show_notification(user.id as usize, cx, |cx| {
- cx.add_view(|cx| {
- ContactNotification::new(
- user.clone(),
- *kind,
- user_store,
- cx,
- )
- })
- }),
- _ => {}
- },
+ cx.observe(&project_store, |this, _, cx| this.update_entries(cx))
+ .detach();
+
+ cx.subscribe(&user_store, move |_, user_store, event, cx| {
+ if let Some(workspace) = workspace.upgrade(cx) {
+ workspace.update(cx, |workspace, cx| match event {
+ client::Event::Contact { user, kind } => match kind {
+ ContactEventKind::Requested | ContactEventKind::Accepted => workspace
+ .show_notification(user.id as usize, cx, |cx| {
+ cx.add_view(|cx| {
+ ContactNotification::new(user.clone(), *kind, user_store, cx)
+ })
+ }),
_ => {}
- });
- }
+ },
+ _ => {}
+ });
+ }
- if let client::Event::ShowContacts = event {
- cx.emit(Event::Activate);
- }
+ if let client::Event::ShowContacts = event {
+ cx.emit(Event::Activate);
}
})
.detach();
- let mut this = Self {
- list_state: ListState::new(0, Orientation::Top, 1000., cx, {
- move |this, ix, cx| {
- let theme = cx.global::<Settings>().theme.clone();
- let theme = &theme.contacts_panel;
- let current_user_id =
- this.user_store.read(cx).current_user().map(|user| user.id);
- let is_selected = this.selection == Some(ix);
-
- match &this.entries[ix] {
- ContactEntry::Header(section) => {
- let is_collapsed = this.collapsed_sections.contains(§ion);
- Self::render_header(*section, theme, is_selected, is_collapsed, cx)
- }
- ContactEntry::IncomingRequest(user) => Self::render_contact_request(
- user.clone(),
- this.user_store.clone(),
- theme,
- true,
- is_selected,
- cx,
- ),
- ContactEntry::OutgoingRequest(user) => Self::render_contact_request(
- user.clone(),
- this.user_store.clone(),
- theme,
- false,
- is_selected,
- cx,
- ),
- ContactEntry::Contact(contact) => {
- Self::render_contact(contact.clone(), theme, is_selected)
- }
- ContactEntry::ContactProject(contact, project_ix) => {
- let is_last_project_for_contact =
- this.entries.get(ix + 1).map_or(true, |next| {
- if let ContactEntry::ContactProject(next_contact, _) = next {
- next_contact.user.id != contact.user.id
- } else {
- true
- }
- });
- Self::render_contact_project(
- contact.clone(),
- current_user_id,
- *project_ix,
- theme,
- is_last_project_for_contact,
- is_selected,
- cx,
- )
- }
- }
+ let list_state = ListState::new(0, Orientation::Top, 1000., cx, move |this, ix, cx| {
+ let theme = cx.global::<Settings>().theme.clone();
+ let current_user_id = this.user_store.read(cx).current_user().map(|user| user.id);
+ let is_selected = this.selection == Some(ix);
+
+ match &this.entries[ix] {
+ ContactEntry::Header(section) => {
+ let is_collapsed = this.collapsed_sections.contains(§ion);
+ Self::render_header(
+ *section,
+ &theme.contacts_panel,
+ is_selected,
+ is_collapsed,
+ cx,
+ )
}
- }),
+ ContactEntry::IncomingRequest(user) => Self::render_contact_request(
+ user.clone(),
+ this.user_store.clone(),
+ &theme.contacts_panel,
+ true,
+ is_selected,
+ cx,
+ ),
+ ContactEntry::OutgoingRequest(user) => Self::render_contact_request(
+ user.clone(),
+ this.user_store.clone(),
+ &theme.contacts_panel,
+ false,
+ is_selected,
+ cx,
+ ),
+ ContactEntry::Contact(contact) => {
+ Self::render_contact(&contact.user, &theme.contacts_panel, is_selected)
+ }
+ ContactEntry::ContactProject(contact, project_ix, open_project) => {
+ let is_last_project_for_contact =
+ this.entries.get(ix + 1).map_or(true, |next| {
+ if let ContactEntry::ContactProject(next_contact, _, _) = next {
+ next_contact.user.id != contact.user.id
+ } else {
+ true
+ }
+ });
+ Self::render_project(
+ contact.clone(),
+ current_user_id,
+ *project_ix,
+ open_project.clone(),
+ &theme.contacts_panel,
+ &theme.tooltip,
+ is_last_project_for_contact,
+ is_selected,
+ cx,
+ )
+ }
+ ContactEntry::OfflineProject(project) => Self::render_offline_project(
+ project.clone(),
+ &theme.contacts_panel,
+ &theme.tooltip,
+ is_selected,
+ cx,
+ ),
+ }
+ });
+
+ let mut this = Self {
+ list_state,
selection: None,
collapsed_sections: Default::default(),
entries: Default::default(),
@@ -242,6 +252,7 @@ impl ContactsPanel {
filter_editor,
_maintain_contacts: cx.observe(&user_store, |this, _, cx| this.update_entries(cx)),
user_store,
+ project_store,
};
this.update_entries(cx);
this
@@ -300,13 +311,9 @@ impl ContactsPanel {
.boxed()
}
- fn render_contact(
- contact: Arc<Contact>,
- theme: &theme::ContactsPanel,
- is_selected: bool,
- ) -> ElementBox {
+ fn render_contact(user: &User, theme: &theme::ContactsPanel, is_selected: bool) -> ElementBox {
Flex::row()
- .with_children(contact.user.avatar.clone().map(|avatar| {
+ .with_children(user.avatar.clone().map(|avatar| {
Image::new(avatar)
.with_style(theme.contact_avatar)
.aligned()
@@ -315,7 +322,7 @@ impl ContactsPanel {
}))
.with_child(
Label::new(
- contact.user.github_login.clone(),
+ user.github_login.clone(),
theme.contact_username.text.clone(),
)
.contained()
@@ -332,11 +339,13 @@ impl ContactsPanel {
.boxed()
}
- fn render_contact_project(
+ fn render_project(
contact: Arc<Contact>,
current_user_id: Option<u64>,
project_index: usize,
+ open_project: Option<WeakModelHandle<Project>>,
theme: &theme::ContactsPanel,
+ tooltip_style: &TooltipStyle,
is_last_project: bool,
is_selected: bool,
cx: &mut RenderContext<Self>,
@@ -344,6 +353,7 @@ impl ContactsPanel {
let project = &contact.projects[project_index];
let project_id = project.id;
let is_host = Some(contact.user.id) == current_user_id;
+ let open_project = open_project.and_then(|p| p.upgrade(cx.deref_mut()));
let font_cache = cx.font_cache();
let host_avatar_height = theme
@@ -358,48 +368,97 @@ impl ContactsPanel {
let baseline_offset =
row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.;
- MouseEventHandler::new::<JoinProject, _, _>(project_id as usize, cx, |mouse_state, _| {
+ MouseEventHandler::new::<JoinProject, _, _>(project_id as usize, cx, |mouse_state, cx| {
let tree_branch = *tree_branch.style_for(mouse_state, is_selected);
let row = theme.project_row.style_for(mouse_state, is_selected);
Flex::row()
.with_child(
- Canvas::new(move |bounds, _, cx| {
- let start_x =
- bounds.min_x() + (bounds.width() / 2.) - (tree_branch.width / 2.);
- let end_x = bounds.max_x();
- let start_y = bounds.min_y();
- let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.);
-
- cx.scene.push_quad(gpui::Quad {
- bounds: RectF::from_points(
- vec2f(start_x, start_y),
- vec2f(
- start_x + tree_branch.width,
- if is_last_project {
- end_y
- } else {
- bounds.max_y()
- },
- ),
- ),
- background: Some(tree_branch.color),
- border: gpui::Border::default(),
- corner_radius: 0.,
- });
- cx.scene.push_quad(gpui::Quad {
- bounds: RectF::from_points(
- vec2f(start_x, end_y),
- vec2f(end_x, end_y + tree_branch.width),
- ),
- background: Some(tree_branch.color),
- border: gpui::Border::default(),
- corner_radius: 0.,
- });
- })
- .constrained()
- .with_width(host_avatar_height)
- .boxed(),
+ Stack::new()
+ .with_child(
+ Canvas::new(move |bounds, _, cx| {
+ let start_x = bounds.min_x() + (bounds.width() / 2.)
+ - (tree_branch.width / 2.);
+ let end_x = bounds.max_x();
+ let start_y = bounds.min_y();
+ let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.);
+
+ cx.scene.push_quad(gpui::Quad {
+ bounds: RectF::from_points(
+ vec2f(start_x, start_y),
+ vec2f(
+ start_x + tree_branch.width,
+ if is_last_project {
+ end_y
+ } else {
+ bounds.max_y()
+ },
+ ),
+ ),
+ background: Some(tree_branch.color),
+ border: gpui::Border::default(),
+ corner_radius: 0.,
+ });
+ cx.scene.push_quad(gpui::Quad {
+ bounds: RectF::from_points(
+ vec2f(start_x, end_y),
+ vec2f(end_x, end_y + tree_branch.width),
+ ),
+ background: Some(tree_branch.color),
+ border: gpui::Border::default(),
+ corner_radius: 0.,
+ });
+ })
+ .boxed(),
+ )
+ .with_children(open_project.and_then(|open_project| {
+ let is_going_offline = !open_project.read(cx).is_online();
+ if !mouse_state.hovered && !is_going_offline {
+ return None;
+ }
+
+ let button = MouseEventHandler::new::<ToggleProjectOnline, _, _>(
+ project_id as usize,
+ cx,
+ |state, _| {
+ let mut icon_style =
+ *theme.private_button.style_for(state, false);
+ icon_style.container.background_color =
+ row.container.background_color;
+ if is_going_offline {
+ icon_style.color = theme.disabled_button.color;
+ }
+ render_icon_button(&icon_style, "icons/lock-8.svg")
+ .aligned()
+ .boxed()
+ },
+ );
+
+ if is_going_offline {
+ Some(button.boxed())
+ } else {
+ Some(
+ button
+ .with_cursor_style(CursorStyle::PointingHand)
+ .on_click(move |_, _, cx| {
+ cx.dispatch_action(ToggleProjectOnline {
+ project: Some(open_project.clone()),
+ })
+ })
+ .with_tooltip(
+ project_id as usize,
+ "Take project offline".to_string(),
+ None,
+ tooltip_style.clone(),
+ cx,
+ )
+ .boxed(),
+ )
+ }
+ }))
+ .constrained()
+ .with_width(host_avatar_height)
+ .boxed(),
)
.with_child(
Label::new(
@@ -446,6 +505,94 @@ impl ContactsPanel {
.boxed()
}
+ fn render_offline_project(
+ project: WeakModelHandle<Project>,
+ theme: &theme::ContactsPanel,
+ tooltip_style: &TooltipStyle,
+ is_selected: bool,
+ cx: &mut RenderContext<Self>,
+ ) -> ElementBox {
+ let project = if let Some(project) = project.upgrade(cx.deref_mut()) {
+ project
+ } else {
+ return Empty::new().boxed();
+ };
+
+ let host_avatar_height = theme
+ .contact_avatar
+ .width
+ .or(theme.contact_avatar.height)
+ .unwrap_or(0.);
+
+ enum LocalProject {}
+ enum ToggleOnline {}
+
+ let project_id = project.id();
+ MouseEventHandler::new::<LocalProject, _, _>(project_id, cx, |state, cx| {
+ let row = theme.project_row.style_for(state, is_selected);
+ let mut worktree_root_names = String::new();
+ let project_ = project.read(cx);
+ let is_going_online = project_.is_online();
+ for tree in project_.visible_worktrees(cx) {
+ if !worktree_root_names.is_empty() {
+ worktree_root_names.push_str(", ");
+ }
+ worktree_root_names.push_str(tree.read(cx).root_name());
+ }
+
+ Flex::row()
+ .with_child({
+ let button =
+ MouseEventHandler::new::<ToggleOnline, _, _>(project_id, cx, |state, _| {
+ let mut style = *theme.private_button.style_for(state, false);
+ if is_going_online {
+ style.color = theme.disabled_button.color;
+ }
+ render_icon_button(&style, "icons/lock-8.svg")
+ .aligned()
+ .constrained()
+ .with_width(host_avatar_height)
+ .boxed()
+ });
+
+ if is_going_online {
+ button.boxed()
+ } else {
+ button
+ .with_cursor_style(CursorStyle::PointingHand)
+ .on_click(move |_, _, cx| {
+ cx.dispatch_action(ToggleProjectOnline {
+ project: Some(project.clone()),
+ })
+ })
+ .with_tooltip(
+ project_id,
+ "Take project online".to_string(),
+ None,
+ tooltip_style.clone(),
+ cx,
+ )
+ .boxed()
+ }
+ })
+ .with_child(
+ Label::new(worktree_root_names, row.name.text.clone())
+ .aligned()
+ .left()
+ .contained()
+ .with_style(row.name.container)
+ .flex(1., false)
+ .boxed(),
+ )
+ .constrained()
+ .with_height(theme.row_height)
+ .contained()
+ .with_style(row.container)
+ .boxed()
+ })
+ .boxed()
+ }
+
fn render_contact_request(
user: Arc<User>,
user_store: ModelHandle<UserStore>,
@@ -487,7 +634,7 @@ impl ContactsPanel {
row.add_children([
MouseEventHandler::new::<Decline, _, _>(user.id as usize, cx, |mouse_state, _| {
let button_style = if is_contact_request_pending {
- &theme.disabled_contact_button
+ &theme.disabled_button
} else {
&theme.contact_button.style_for(mouse_state, false)
};
@@ -509,7 +656,7 @@ impl ContactsPanel {
.boxed(),
MouseEventHandler::new::<Accept, _, _>(user.id as usize, cx, |mouse_state, _| {
let button_style = if is_contact_request_pending {
- &theme.disabled_contact_button
+ &theme.disabled_button
} else {
&theme.contact_button.style_for(mouse_state, false)
};
@@ -531,7 +678,7 @@ impl ContactsPanel {
row.add_child(
MouseEventHandler::new::<Cancel, _, _>(user.id as usize, cx, |mouse_state, _| {
let button_style = if is_contact_request_pending {
- &theme.disabled_contact_button
+ &theme.disabled_button
} else {
&theme.contact_button.style_for(mouse_state, false)
};
@@ -557,6 +704,7 @@ impl ContactsPanel {
fn update_entries(&mut self, cx: &mut ViewContext<Self>) {
let user_store = self.user_store.read(cx);
+ let project_store = self.project_store.read(cx);
let query = self.filter_editor.read(cx).text(cx);
let executor = cx.background().clone();
@@ -629,20 +777,37 @@ impl ContactsPanel {
}
}
+ let current_user = user_store.current_user();
+
let contacts = user_store.contacts();
if !contacts.is_empty() {
+ // Always put the current user first.
self.match_candidates.clear();
- self.match_candidates
- .extend(
- contacts
- .iter()
- .enumerate()
- .map(|(ix, contact)| StringMatchCandidate {
- id: ix,
- string: contact.user.github_login.clone(),
- char_bag: contact.user.github_login.chars().collect(),
- }),
- );
+ self.match_candidates.reserve(contacts.len());
+ self.match_candidates.push(StringMatchCandidate {
+ id: 0,
+ string: Default::default(),
+ char_bag: Default::default(),
+ });
+ for (ix, contact) in contacts.iter().enumerate() {
+ let candidate = StringMatchCandidate {
+ id: ix,
+ string: contact.user.github_login.clone(),
+ char_bag: contact.user.github_login.chars().collect(),
+ };
+ if current_user
+ .as_ref()
+ .map_or(false, |current_user| current_user.id == contact.user.id)
+ {
+ self.match_candidates[0] = candidate;
+ } else {
+ self.match_candidates.push(candidate);
+ }
+ }
+ if self.match_candidates[0].string.is_empty() {
+ self.match_candidates.remove(0);
+ }
+
let matches = executor.block(match_strings(
&self.match_candidates,
&query,
@@ -666,16 +831,60 @@ impl ContactsPanel {
for mat in matches {
let contact = &contacts[mat.candidate_id];
self.entries.push(ContactEntry::Contact(contact.clone()));
- self.entries
- .extend(contact.projects.iter().enumerate().filter_map(
- |(ix, project)| {
- if project.worktree_root_names.is_empty() {
+
+ let is_current_user = current_user
+ .as_ref()
+ .map_or(false, |user| user.id == contact.user.id);
+ if is_current_user {
+ let mut open_projects =
+ project_store.projects(cx).collect::<Vec<_>>();
+ self.entries.extend(
+ contact.projects.iter().enumerate().filter_map(
+ |(ix, project)| {
+ let open_project = open_projects
+ .iter()
+ .position(|p| {
+ p.read(cx).remote_id() == Some(project.id)
+ })
+ .map(|ix| open_projects.remove(ix).downgrade());
+ if project.worktree_root_names.is_empty() {
+ None
+ } else {
+ Some(ContactEntry::ContactProject(
+ contact.clone(),
+ ix,
+ open_project,
+ ))
+ }
+ },
+ ),
+ );
+ self.entries.extend(open_projects.into_iter().filter_map(
+ |project| {
+ if project.read(cx).visible_worktrees(cx).next().is_none() {
None
} else {
- Some(ContactEntry::ContactProject(contact.clone(), ix))
+ Some(ContactEntry::OfflineProject(project.downgrade()))
}
},
));
+ } else {
+ self.entries.extend(
+ contact.projects.iter().enumerate().filter_map(
+ |(ix, project)| {
+ if project.worktree_root_names.is_empty() {
+ None
+ } else {
+ Some(ContactEntry::ContactProject(
+ contact.clone(),
+ ix,
+ None,
+ ))
+ }
+ },
+ ),
+ );
+ }
}
}
}
@@ -757,11 +966,18 @@ impl ContactsPanel {
let section = *section;
self.toggle_expanded(&ToggleExpanded(section), cx);
}
- ContactEntry::ContactProject(contact, project_index) => cx
- .dispatch_global_action(JoinProject {
- contact: contact.clone(),
- project_index: *project_index,
- }),
+ ContactEntry::ContactProject(contact, project_index, open_project) => {
+ if let Some(open_project) = open_project {
+ workspace::activate_workspace_for_project(cx, |_, cx| {
+ cx.model_id() == open_project.id()
+ });
+ } else {
+ cx.dispatch_global_action(JoinProject {
+ contact: contact.clone(),
+ project_index: *project_index,
+ })
+ }
+ }
_ => {}
}
}
@@ -952,11 +1168,16 @@ impl PartialEq for ContactEntry {
return contact_1.user.id == contact_2.user.id;
}
}
- ContactEntry::ContactProject(contact_1, ix_1) => {
- if let ContactEntry::ContactProject(contact_2, ix_2) = other {
+ ContactEntry::ContactProject(contact_1, ix_1, _) => {
+ if let ContactEntry::ContactProject(contact_2, ix_2, _) = other {
return contact_1.user.id == contact_2.user.id && ix_1 == ix_2;
}
}
+ ContactEntry::OfflineProject(project_1) => {
+ if let ContactEntry::OfflineProject(project_2) = other {
+ return project_1.id() == project_2.id();
+ }
+ }
}
false
}
@@ -965,20 +1186,70 @@ impl PartialEq for ContactEntry {
#[cfg(test)]
mod tests {
use super::*;
- use client::{proto, test::FakeServer, Client};
- use gpui::TestAppContext;
+ use client::{
+ proto,
+ test::{FakeHttpClient, FakeServer},
+ Client,
+ };
+ use collections::HashSet;
+ use gpui::{serde_json::json, TestAppContext};
use language::LanguageRegistry;
- use project::Project;
- use theme::ThemeRegistry;
- use workspace::AppState;
+ use project::{FakeFs, Project};
#[gpui::test]
async fn test_contact_panel(cx: &mut TestAppContext) {
- let (app_state, server) = init(cx).await;
- let project = Project::test(app_state.fs.clone(), [], cx).await;
- let workspace = cx.add_view(0, |cx| Workspace::new(project, cx));
+ Settings::test_async(cx);
+ let current_user_id = 100;
+
+ let languages = Arc::new(LanguageRegistry::test());
+ let http_client = FakeHttpClient::with_404_response();
+ let client = Client::new(http_client.clone());
+ let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
+ let project_store = cx.add_model(|_| ProjectStore::new(project::Db::open_fake()));
+ let server = FakeServer::for_client(current_user_id, &client, &cx).await;
+ let fs = FakeFs::new(cx.background());
+ fs.insert_tree("/private_dir", json!({ "one.rs": "" }))
+ .await;
+ let project = cx.update(|cx| {
+ Project::local(
+ false,
+ client.clone(),
+ user_store.clone(),
+ project_store.clone(),
+ languages,
+ fs,
+ cx,
+ )
+ });
+ let worktree_id = project
+ .update(cx, |project, cx| {
+ project.find_or_create_local_worktree("/private_dir", true, cx)
+ })
+ .await
+ .unwrap()
+ .0
+ .read_with(cx, |worktree, _| worktree.id().to_proto());
+
+ let workspace = cx.add_view(0, |cx| Workspace::new(project.clone(), cx));
let panel = cx.add_view(0, |cx| {
- ContactsPanel::new(app_state.user_store.clone(), workspace.downgrade(), cx)
+ ContactsPanel::new(
+ user_store.clone(),
+ project_store.clone(),
+ workspace.downgrade(),
+ cx,
+ )
+ });
+
+ workspace.update(cx, |_, cx| {
+ cx.observe(&panel, |_, panel, cx| {
+ let entries = render_to_strings(&panel, cx);
+ assert!(
+ entries.iter().collect::<HashSet<_>>().len() == entries.len(),
+ "Duplicate contact panel entries {:?}",
+ entries
+ )
+ })
+ .detach();
});
let get_users_request = server.receive::<proto::GetUsers>().await.unwrap();
@@ -1001,6 +1272,11 @@ mod tests {
github_login: name.to_string(),
..Default::default()
})
+ .chain([proto::User {
+ id: current_user_id,
+ github_login: "the_current_user".to_string(),
+ ..Default::default()
+ }])
.collect(),
},
)
@@ -1039,19 +1315,219 @@ mod tests {
should_notify: false,
projects: vec![],
},
+ proto::Contact {
+ user_id: current_user_id,
+ online: true,
+ should_notify: false,
+ projects: vec![proto::ProjectMetadata {
+ id: 103,
+ worktree_root_names: vec!["dir3".to_string()],
+ guests: vec![3],
+ }],
+ },
],
..Default::default()
});
cx.foreground().run_until_parked();
assert_eq!(
- render_to_strings(&panel, cx),
+ cx.read(|cx| render_to_strings(&panel, cx)),
+ &[
+ "v Requests",
+ " incoming user_one",
+ " outgoing user_two",
+ "v Online",
+ " the_current_user",
+ " dir3",
+ " 🔒 private_dir",
+ " user_four",
+ " dir2",
+ " user_three",
+ " dir1",
+ "v Offline",
+ " user_five",
+ ]
+ );
+
+ // Take a project online. It appears as loading, since the project
+ // isn't yet visible to other contacts.
+ project.update(cx, |project, cx| project.set_online(true, cx));
+ cx.foreground().run_until_parked();
+ assert_eq!(
+ cx.read(|cx| render_to_strings(&panel, cx)),
+ &[
+ "v Requests",
+ " incoming user_one",
+ " outgoing user_two",
+ "v Online",
+ " the_current_user",
+ " dir3",
+ " 🔒 private_dir (going online...)",
+ " user_four",
+ " dir2",
+ " user_three",
+ " dir1",
+ "v Offline",
+ " user_five",
+ ]
+ );
+
+ // The server responds, assigning the project a remote id. It still appears
+ // as loading, because the server hasn't yet sent out the updated contact
+ // state for the current user.
+ let request = server.receive::<proto::RegisterProject>().await.unwrap();
+ server
+ .respond(
+ request.receipt(),
+ proto::RegisterProjectResponse { project_id: 200 },
+ )
+ .await;
+ cx.foreground().run_until_parked();
+ assert_eq!(
+ cx.read(|cx| render_to_strings(&panel, cx)),
+ &[
+ "v Requests",
+ " incoming user_one",
+ " outgoing user_two",
+ "v Online",
+ " the_current_user",
+ " dir3",
+ " 🔒 private_dir (going online...)",
+ " user_four",
+ " dir2",
+ " user_three",
+ " dir1",
+ "v Offline",
+ " user_five",
+ ]
+ );
+
+ // The server receives the project's metadata and updates the contact metadata
+ // for the current user. Now the project appears as online.
+ assert_eq!(
+ server
+ .receive::<proto::UpdateProject>()
+ .await
+ .unwrap()
+ .payload
+ .worktrees,
+ &[proto::WorktreeMetadata {
+ id: worktree_id,
+ root_name: "private_dir".to_string(),
+ visible: true,
+ }],
+ );
+ server.send(proto::UpdateContacts {
+ contacts: vec![proto::Contact {
+ user_id: current_user_id,
+ online: true,
+ should_notify: false,
+ projects: vec![
+ proto::ProjectMetadata {
+ id: 103,
+ worktree_root_names: vec!["dir3".to_string()],
+ guests: vec![3],
+ },
+ proto::ProjectMetadata {
+ id: 200,
+ worktree_root_names: vec!["private_dir".to_string()],
+ guests: vec![3],
+ },
+ ],
+ }],
+ ..Default::default()
+ });
+ cx.foreground().run_until_parked();
+ assert_eq!(
+ cx.read(|cx| render_to_strings(&panel, cx)),
&[
- "+",
"v Requests",
" incoming user_one",
" outgoing user_two",
"v Online",
+ " the_current_user",
+ " dir3",
+ " private_dir",
+ " user_four",
+ " dir2",
+ " user_three",
+ " dir1",
+ "v Offline",
+ " user_five",
+ ]
+ );
+
+ // Take the project offline. It appears as loading.
+ project.update(cx, |project, cx| project.set_online(false, cx));
+ cx.foreground().run_until_parked();
+ assert_eq!(
+ cx.read(|cx| render_to_strings(&panel, cx)),
+ &[
+ "v Requests",
+ " incoming user_one",
+ " outgoing user_two",
+ "v Online",
+ " the_current_user",
+ " dir3",
+ " private_dir (going offline...)",
+ " user_four",
+ " dir2",
+ " user_three",
+ " dir1",
+ "v Offline",
+ " user_five",
+ ]
+ );
+
+ // The server receives the unregister request and updates the contact
+ // metadata for the current user. The project is now offline.
+ let request = server.receive::<proto::UnregisterProject>().await.unwrap();
+ server.send(proto::UpdateContacts {
+ contacts: vec![proto::Contact {
+ user_id: current_user_id,
+ online: true,
+ should_notify: false,
+ projects: vec![proto::ProjectMetadata {
+ id: 103,
+ worktree_root_names: vec!["dir3".to_string()],
+ guests: vec![3],
+ }],
+ }],
+ ..Default::default()
+ });
+ cx.foreground().run_until_parked();
+ assert_eq!(
+ cx.read(|cx| render_to_strings(&panel, cx)),
+ &[
+ "v Requests",
+ " incoming user_one",
+ " outgoing user_two",
+ "v Online",
+ " the_current_user",
+ " dir3",
+ " 🔒 private_dir",
+ " user_four",
+ " dir2",
+ " user_three",
+ " dir1",
+ "v Offline",
+ " user_five",
+ ]
+ );
+
+ // The server responds to the unregister request.
+ server.respond(request.receipt(), proto::Ack {}).await;
+ cx.foreground().run_until_parked();
+ assert_eq!(
+ cx.read(|cx| render_to_strings(&panel, cx)),
+ &[
+ "v Requests",
+ " incoming user_one",
+ " outgoing user_two",
+ "v Online",
+ " the_current_user",
+ " dir3",
+ " 🔒 private_dir",
" user_four",
" dir2",
" user_three",
@@ -59,7 +59,7 @@ project = { path = "../project", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] }
ctor = "0.1"
-env_logger = "0.8"
+env_logger = "0.9"
rand = "0.8"
unindent = "0.1.7"
tree-sitter = "0.20"
@@ -25,4 +25,4 @@ gpui = { path = "../gpui", features = ["test-support"] }
serde_json = { version = "1.0", features = ["preserve_order"] }
workspace = { path = "../workspace", features = ["test-support"] }
ctor = "0.1"
-env_logger = "0.8"
+env_logger = "0.9"
@@ -20,7 +20,7 @@ async-task = "4.0.3"
backtrace = { version = "0.3", optional = true }
ctor = "0.1"
dhat = { version = "0.3", optional = true }
-env_logger = { version = "0.8", optional = true }
+env_logger = { version = "0.9", optional = true }
etagere = "0.2"
futures = "0.3"
image = "0.23"
@@ -47,14 +47,14 @@ usvg = "0.14"
waker-fn = "1.1.0"
[build-dependencies]
-bindgen = "0.58.1"
+bindgen = "0.59.2"
cc = "1.0.67"
[dev-dependencies]
backtrace = "0.3"
collections = { path = "../collections", features = ["test-support"] }
dhat = "0.3"
-env_logger = "0.8"
+env_logger = "0.9"
png = "0.16"
simplelog = "0.9"
@@ -499,7 +499,14 @@ impl TestAppContext {
Fut: 'static + Future<Output = T>,
T: 'static,
{
- self.cx.borrow_mut().spawn(f)
+ let foreground = self.foreground();
+ let future = f(self.to_async());
+ let cx = self.to_async();
+ foreground.spawn(async move {
+ let result = future.await;
+ cx.0.borrow_mut().flush_effects();
+ result
+ })
}
pub fn simulate_new_path_selection(&self, result: impl FnOnce(PathBuf) -> Option<PathBuf>) {
@@ -4604,6 +4611,10 @@ impl<T: View> WeakViewHandle<T> {
self.view_id
}
+ pub fn window_id(&self) -> usize {
+ self.window_id
+ }
+
pub fn upgrade(&self, cx: &impl UpgradeViewHandle) -> Option<ViewHandle<T>> {
cx.upgrade_view_handle(self)
}
@@ -147,6 +147,12 @@ pub struct AppVersion {
patch: usize,
}
+impl Default for CursorStyle {
+ fn default() -> Self {
+ Self::Arrow
+ }
+}
+
impl FromStr for AppVersion {
type Err = anyhow::Error;
@@ -57,7 +57,7 @@ lsp = { path = "../lsp", features = ["test-support"] }
text = { path = "../text", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }
ctor = "0.1"
-env_logger = "0.8"
+env_logger = "0.9"
rand = "0.8.3"
tree-sitter-json = "*"
tree-sitter-rust = "*"
@@ -30,5 +30,5 @@ gpui = { path = "../gpui", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }
async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553" }
ctor = "0.1"
-env_logger = "0.8"
+env_logger = "0.9"
unindent = "0.1.7"
@@ -21,4 +21,4 @@ gpui = { path = "../gpui", features = ["test-support"] }
serde_json = { version = "1.0", features = ["preserve_order"] }
workspace = { path = "../workspace", features = ["test-support"] }
ctor = "0.1"
-env_logger = "0.8"
+env_logger = "0.9"
@@ -47,6 +47,7 @@ similar = "1.3"
smol = "1.2.5"
thiserror = "1.0.29"
toml = "0.5"
+rocksdb = "0.18"
[dev-dependencies]
client = { path = "../client", features = ["test-support"] }
@@ -0,0 +1,161 @@
+use anyhow::Result;
+use std::path::PathBuf;
+use std::sync::Arc;
+
+pub struct Db(DbStore);
+
+enum DbStore {
+ Null,
+ Real(rocksdb::DB),
+
+ #[cfg(any(test, feature = "test-support"))]
+ Fake {
+ data: parking_lot::Mutex<collections::HashMap<Vec<u8>, Vec<u8>>>,
+ },
+}
+
+impl Db {
+ /// Open or create a database at the given file path.
+ pub fn open(path: PathBuf) -> Result<Arc<Self>> {
+ let db = rocksdb::DB::open_default(&path)?;
+ Ok(Arc::new(Self(DbStore::Real(db))))
+ }
+
+ /// Open a null database that stores no data, for use as a fallback
+ /// when there is an error opening the real database.
+ pub fn null() -> Arc<Self> {
+ Arc::new(Self(DbStore::Null))
+ }
+
+ /// Open a fake database for testing.
+ #[cfg(any(test, feature = "test-support"))]
+ pub fn open_fake() -> Arc<Self> {
+ Arc::new(Self(DbStore::Fake {
+ data: Default::default(),
+ }))
+ }
+
+ pub fn read<K, I>(&self, keys: I) -> Result<Vec<Option<Vec<u8>>>>
+ where
+ K: AsRef<[u8]>,
+ I: IntoIterator<Item = K>,
+ {
+ match &self.0 {
+ DbStore::Real(db) => db
+ .multi_get(keys)
+ .into_iter()
+ .map(|e| e.map_err(Into::into))
+ .collect(),
+
+ DbStore::Null => Ok(keys.into_iter().map(|_| None).collect()),
+
+ #[cfg(any(test, feature = "test-support"))]
+ DbStore::Fake { data: db } => {
+ let db = db.lock();
+ Ok(keys
+ .into_iter()
+ .map(|key| db.get(key.as_ref()).cloned())
+ .collect())
+ }
+ }
+ }
+
+ pub fn delete<K, I>(&self, keys: I) -> Result<()>
+ where
+ K: AsRef<[u8]>,
+ I: IntoIterator<Item = K>,
+ {
+ match &self.0 {
+ DbStore::Real(db) => {
+ let mut batch = rocksdb::WriteBatch::default();
+ for key in keys {
+ batch.delete(key);
+ }
+ db.write(batch)?;
+ }
+
+ DbStore::Null => {}
+
+ #[cfg(any(test, feature = "test-support"))]
+ DbStore::Fake { data: db } => {
+ let mut db = db.lock();
+ for key in keys {
+ db.remove(key.as_ref());
+ }
+ }
+ }
+ Ok(())
+ }
+
+ pub fn write<K, V, I>(&self, entries: I) -> Result<()>
+ where
+ K: AsRef<[u8]>,
+ V: AsRef<[u8]>,
+ I: IntoIterator<Item = (K, V)>,
+ {
+ match &self.0 {
+ DbStore::Real(db) => {
+ let mut batch = rocksdb::WriteBatch::default();
+ for (key, value) in entries {
+ batch.put(key, value);
+ }
+ db.write(batch)?;
+ }
+
+ DbStore::Null => {}
+
+ #[cfg(any(test, feature = "test-support"))]
+ DbStore::Fake { data: db } => {
+ let mut db = db.lock();
+ for (key, value) in entries {
+ db.insert(key.as_ref().into(), value.as_ref().into());
+ }
+ }
+ }
+ Ok(())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use tempdir::TempDir;
+
+ #[gpui::test]
+ fn test_db() {
+ let dir = TempDir::new("db-test").unwrap();
+ let fake_db = Db::open_fake();
+ let real_db = Db::open(dir.path().join("test.db")).unwrap();
+
+ for db in [&real_db, &fake_db] {
+ assert_eq!(
+ db.read(["key-1", "key-2", "key-3"]).unwrap(),
+ &[None, None, None]
+ );
+
+ db.write([("key-1", "one"), ("key-3", "three")]).unwrap();
+ assert_eq!(
+ db.read(["key-1", "key-2", "key-3"]).unwrap(),
+ &[
+ Some("one".as_bytes().to_vec()),
+ None,
+ Some("three".as_bytes().to_vec())
+ ]
+ );
+
+ db.delete(["key-3", "key-4"]).unwrap();
+ assert_eq!(
+ db.read(["key-1", "key-2", "key-3"]).unwrap(),
+ &[Some("one".as_bytes().to_vec()), None, None,]
+ );
+ }
+
+ drop(real_db);
+
+ let real_db = Db::open(dir.path().join("test.db")).unwrap();
+ assert_eq!(
+ real_db.read(["key-1", "key-2", "key-3"]).unwrap(),
+ &[Some("one".as_bytes().to_vec()), None, None,]
+ );
+ }
+}
@@ -1,3 +1,4 @@
+mod db;
pub mod fs;
mod ignore;
mod lsp_command;
@@ -25,6 +26,7 @@ use language::{
use lsp::{DiagnosticSeverity, DiagnosticTag, DocumentHighlightKind, LanguageServer};
use lsp_command::*;
use parking_lot::Mutex;
+use postage::stream::Stream;
use postage::watch;
use rand::prelude::*;
use search::SearchQuery;
@@ -52,6 +54,7 @@ use std::{
use thiserror::Error;
use util::{post_inc, ResultExt, TryFutureExt as _};
+pub use db::Db;
pub use fs::*;
pub use worktree::*;
@@ -59,6 +62,11 @@ pub trait Item: Entity {
fn entry_id(&self, cx: &AppContext) -> Option<ProjectEntryId>;
}
+pub struct ProjectStore {
+ db: Arc<Db>,
+ projects: Vec<WeakModelHandle<Project>>,
+}
+
pub struct Project {
worktrees: Vec<WorktreeHandle>,
active_entry: Option<ProjectEntryId>,
@@ -75,6 +83,7 @@ pub struct Project {
next_entry_id: Arc<AtomicUsize>,
next_diagnostic_group_id: usize,
user_store: ModelHandle<UserStore>,
+ project_store: ModelHandle<ProjectStore>,
fs: Arc<dyn Fs>,
client_state: ProjectClientState,
collaborators: HashMap<PeerId, Collaborator>,
@@ -90,6 +99,7 @@ pub struct Project {
opened_buffers: HashMap<u64, OpenBuffer>,
buffer_snapshots: HashMap<u64, Vec<(i32, TextBufferSnapshot)>>,
nonce: u128,
+ initialized_persistent_state: bool,
}
#[derive(Error, Debug)]
@@ -120,6 +130,8 @@ enum ProjectClientState {
is_shared: bool,
remote_id_tx: watch::Sender<Option<u64>>,
remote_id_rx: watch::Receiver<Option<u64>>,
+ online_tx: watch::Sender<bool>,
+ online_rx: watch::Receiver<bool>,
_maintain_remote_id_task: Task<Option<()>>,
},
Remote {
@@ -273,8 +285,7 @@ impl Project {
client.add_model_message_handler(Self::handle_update_language_server);
client.add_model_message_handler(Self::handle_remove_collaborator);
client.add_model_message_handler(Self::handle_join_project_request_cancelled);
- client.add_model_message_handler(Self::handle_register_worktree);
- client.add_model_message_handler(Self::handle_unregister_worktree);
+ client.add_model_message_handler(Self::handle_update_project);
client.add_model_message_handler(Self::handle_unregister_project);
client.add_model_message_handler(Self::handle_project_unshared);
client.add_model_message_handler(Self::handle_update_buffer_file);
@@ -305,34 +316,42 @@ impl Project {
}
pub fn local(
+ online: bool,
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
+ project_store: ModelHandle<ProjectStore>,
languages: Arc<LanguageRegistry>,
fs: Arc<dyn Fs>,
cx: &mut MutableAppContext,
) -> ModelHandle<Self> {
cx.add_model(|cx: &mut ModelContext<Self>| {
+ let (online_tx, online_rx) = watch::channel_with(online);
let (remote_id_tx, remote_id_rx) = watch::channel();
let _maintain_remote_id_task = cx.spawn_weak({
- let rpc = client.clone();
- move |this, mut cx| {
- async move {
- let mut status = rpc.status();
- while let Some(status) = status.next().await {
- if let Some(this) = this.upgrade(&cx) {
- if status.is_connected() {
- this.update(&mut cx, |this, cx| this.register(cx)).await?;
- } else {
- this.update(&mut cx, |this, cx| this.unregister(cx));
- }
- }
+ let status_rx = client.clone().status();
+ let online_rx = online_rx.clone();
+ move |this, mut cx| async move {
+ let mut stream = Stream::map(status_rx.clone(), drop)
+ .merge(Stream::map(online_rx.clone(), drop));
+ while stream.recv().await.is_some() {
+ let this = this.upgrade(&cx)?;
+ if status_rx.borrow().is_connected() && *online_rx.borrow() {
+ this.update(&mut cx, |this, cx| this.register(cx))
+ .await
+ .log_err()?;
+ } else {
+ this.update(&mut cx, |this, cx| this.unregister(cx))
+ .await
+ .log_err();
}
- Ok(())
}
- .log_err()
+ None
}
});
+ let handle = cx.weak_handle();
+ project_store.update(cx, |store, cx| store.add_project(handle, cx));
+
let (opened_buffer_tx, opened_buffer_rx) = watch::channel();
Self {
worktrees: Default::default(),
@@ -346,6 +365,8 @@ impl Project {
is_shared: false,
remote_id_tx,
remote_id_rx,
+ online_tx,
+ online_rx,
_maintain_remote_id_task,
},
opened_buffer: (Rc::new(RefCell::new(opened_buffer_tx)), opened_buffer_rx),
@@ -354,6 +375,7 @@ impl Project {
languages,
client,
user_store,
+ project_store,
fs,
next_entry_id: Default::default(),
next_diagnostic_group_id: Default::default(),
@@ -364,6 +386,7 @@ impl Project {
language_server_settings: Default::default(),
next_language_server_id: 0,
nonce: StdRng::from_entropy().gen(),
+ initialized_persistent_state: false,
}
})
}
@@ -372,9 +395,10 @@ impl Project {
remote_id: u64,
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
+ project_store: ModelHandle<ProjectStore>,
languages: Arc<LanguageRegistry>,
fs: Arc<dyn Fs>,
- cx: &mut AsyncAppContext,
+ mut cx: AsyncAppContext,
) -> Result<ModelHandle<Self>, JoinProjectError> {
client.authenticate_and_connect(true, &cx).await?;
@@ -414,6 +438,9 @@ impl Project {
let (opened_buffer_tx, opened_buffer_rx) = watch::channel();
let this = cx.add_model(|cx: &mut ModelContext<Self>| {
+ let handle = cx.weak_handle();
+ project_store.update(cx, |store, cx| store.add_project(handle, cx));
+
let mut this = Self {
worktrees: Vec::new(),
loading_buffers: Default::default(),
@@ -424,6 +451,7 @@ impl Project {
collaborators: Default::default(),
languages,
user_store: user_store.clone(),
+ project_store,
fs,
next_entry_id: Default::default(),
next_diagnostic_group_id: Default::default(),
@@ -471,6 +499,7 @@ impl Project {
opened_buffers: Default::default(),
buffer_snapshots: Default::default(),
nonce: StdRng::from_entropy().gen(),
+ initialized_persistent_state: false,
};
for worktree in worktrees {
this.add_worktree(&worktree, cx);
@@ -484,15 +513,15 @@ impl Project {
.map(|peer| peer.user_id)
.collect();
user_store
- .update(cx, |user_store, cx| user_store.get_users(user_ids, cx))
+ .update(&mut cx, |user_store, cx| user_store.get_users(user_ids, cx))
.await?;
let mut collaborators = HashMap::default();
for message in response.collaborators {
- let collaborator = Collaborator::from_proto(message, &user_store, cx).await?;
+ let collaborator = Collaborator::from_proto(message, &user_store, &mut cx).await?;
collaborators.insert(collaborator.peer_id, collaborator);
}
- this.update(cx, |this, _| {
+ this.update(&mut cx, |this, _| {
this.collaborators = collaborators;
});
@@ -509,7 +538,10 @@ impl Project {
let http_client = client::test::FakeHttpClient::with_404_response();
let client = client::Client::new(http_client.clone());
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
- let project = cx.update(|cx| Project::local(client, user_store, languages, fs, cx));
+ let project_store = cx.add_model(|_| ProjectStore::new(Db::open_fake()));
+ let project = cx.update(|cx| {
+ Project::local(true, client, user_store, project_store, languages, fs, cx)
+ });
for path in root_paths {
let (tree, _) = project
.update(cx, |project, cx| {
@@ -523,6 +555,53 @@ impl Project {
project
}
+ pub fn restore_state(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
+ if self.is_remote() {
+ return Task::ready(Ok(()));
+ }
+
+ let db = self.project_store.read(cx).db.clone();
+ let keys = self.db_keys_for_online_state(cx);
+ let online_by_default = cx.global::<Settings>().projects_online_by_default;
+ let read_online = cx.background().spawn(async move {
+ let values = db.read(keys)?;
+ anyhow::Ok(
+ values
+ .into_iter()
+ .all(|e| e.map_or(online_by_default, |e| e == [true as u8])),
+ )
+ });
+ cx.spawn(|this, mut cx| async move {
+ let online = read_online.await.log_err().unwrap_or(false);
+ this.update(&mut cx, |this, cx| {
+ this.initialized_persistent_state = true;
+ if let ProjectClientState::Local { online_tx, .. } = &mut this.client_state {
+ let mut online_tx = online_tx.borrow_mut();
+ if *online_tx != online {
+ *online_tx = online;
+ drop(online_tx);
+ this.metadata_changed(false, cx);
+ }
+ }
+ });
+ Ok(())
+ })
+ }
+
+ fn persist_state(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
+ if self.is_remote() || !self.initialized_persistent_state {
+ return Task::ready(Ok(()));
+ }
+
+ let db = self.project_store.read(cx).db.clone();
+ let keys = self.db_keys_for_online_state(cx);
+ let is_online = self.is_online();
+ cx.background().spawn(async move {
+ let value = &[is_online as u8];
+ db.write(keys.into_iter().map(|key| (key, value)))
+ })
+ }
+
pub fn buffer_for_id(&self, remote_id: u64, cx: &AppContext) -> Option<ModelHandle<Buffer>> {
self.opened_buffers
.get(&remote_id)
@@ -541,6 +620,10 @@ impl Project {
self.user_store.clone()
}
+ pub fn project_store(&self) -> ModelHandle<ProjectStore> {
+ self.project_store.clone()
+ }
+
#[cfg(any(test, feature = "test-support"))]
pub fn check_invariants(&self, cx: &AppContext) {
if self.is_local() {
@@ -598,53 +681,83 @@ impl Project {
&self.fs
}
- fn unregister(&mut self, cx: &mut ModelContext<Self>) {
- self.unshared(cx);
- for worktree in &self.worktrees {
- if let Some(worktree) = worktree.upgrade(cx) {
- worktree.update(cx, |worktree, _| {
- worktree.as_local_mut().unwrap().unregister();
- });
+ pub fn set_online(&mut self, online: bool, cx: &mut ModelContext<Self>) {
+ if let ProjectClientState::Local { online_tx, .. } = &mut self.client_state {
+ let mut online_tx = online_tx.borrow_mut();
+ if *online_tx != online {
+ *online_tx = online;
+ drop(online_tx);
+ self.metadata_changed(true, cx);
}
}
+ }
- if let ProjectClientState::Local { remote_id_tx, .. } = &mut self.client_state {
- *remote_id_tx.borrow_mut() = None;
+ pub fn is_online(&self) -> bool {
+ match &self.client_state {
+ ProjectClientState::Local { online_rx, .. } => *online_rx.borrow(),
+ ProjectClientState::Remote { .. } => true,
}
+ }
- self.subscriptions.clear();
+ fn unregister(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
+ self.unshared(cx);
+ if let ProjectClientState::Local { remote_id_rx, .. } = &mut self.client_state {
+ if let Some(remote_id) = *remote_id_rx.borrow() {
+ let request = self.client.request(proto::UnregisterProject {
+ project_id: remote_id,
+ });
+ return cx.spawn(|this, mut cx| async move {
+ let response = request.await;
+
+ // Unregistering the project causes the server to send out a
+ // contact update removing this project from the host's list
+ // of online projects. Wait until this contact update has been
+ // processed before clearing out this project's remote id, so
+ // that there is no moment where this project appears in the
+ // contact metadata and *also* has no remote id.
+ this.update(&mut cx, |this, cx| {
+ this.user_store()
+ .update(cx, |store, _| store.contact_updates_done())
+ })
+ .await;
+
+ this.update(&mut cx, |this, cx| {
+ if let ProjectClientState::Local { remote_id_tx, .. } =
+ &mut this.client_state
+ {
+ *remote_id_tx.borrow_mut() = None;
+ }
+ this.subscriptions.clear();
+ this.metadata_changed(false, cx);
+ });
+ response.map(drop)
+ });
+ }
+ }
+ Task::ready(Ok(()))
}
fn register(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
- self.unregister(cx);
+ if let ProjectClientState::Local { remote_id_rx, .. } = &self.client_state {
+ if remote_id_rx.borrow().is_some() {
+ return Task::ready(Ok(()));
+ }
+ }
let response = self.client.request(proto::RegisterProject {});
cx.spawn(|this, mut cx| async move {
let remote_id = response.await?.project_id;
-
- let mut registrations = Vec::new();
this.update(&mut cx, |this, cx| {
if let ProjectClientState::Local { remote_id_tx, .. } = &mut this.client_state {
*remote_id_tx.borrow_mut() = Some(remote_id);
}
+ this.metadata_changed(false, cx);
cx.emit(Event::RemoteIdChanged(Some(remote_id)));
-
this.subscriptions
.push(this.client.add_model_for_remote_entity(remote_id, cx));
-
- for worktree in &this.worktrees {
- if let Some(worktree) = worktree.upgrade(cx) {
- registrations.push(worktree.update(cx, |worktree, cx| {
- let worktree = worktree.as_local_mut().unwrap();
- worktree.register(remote_id, cx)
- }));
- }
- }
- });
-
- futures::future::try_join_all(registrations).await?;
- Ok(())
+ Ok(())
+ })
})
}
@@ -702,6 +815,38 @@ impl Project {
}
}
+ fn metadata_changed(&mut self, persist: bool, cx: &mut ModelContext<Self>) {
+ if let ProjectClientState::Local {
+ remote_id_rx,
+ online_rx,
+ ..
+ } = &self.client_state
+ {
+ if let (Some(project_id), true) = (*remote_id_rx.borrow(), *online_rx.borrow()) {
+ self.client
+ .send(proto::UpdateProject {
+ project_id,
+ worktrees: self
+ .worktrees
+ .iter()
+ .filter_map(|worktree| {
+ worktree.upgrade(&cx).map(|worktree| {
+ worktree.read(cx).as_local().unwrap().metadata_proto()
+ })
+ })
+ .collect(),
+ })
+ .log_err();
+ }
+
+ self.project_store.update(cx, |_, cx| cx.notify());
+ if persist {
+ self.persist_state(cx).detach_and_log_err(cx);
+ }
+ cx.notify();
+ }
+ }
+
pub fn collaborators(&self) -> &HashMap<PeerId, Collaborator> {
&self.collaborators
}
@@ -730,6 +875,28 @@ impl Project {
})
}
+ pub fn worktree_root_names<'a>(&'a self, cx: &'a AppContext) -> impl Iterator<Item = &'a str> {
+ self.visible_worktrees(cx)
+ .map(|tree| tree.read(cx).root_name())
+ }
+
+ fn db_keys_for_online_state(&self, cx: &AppContext) -> Vec<String> {
+ self.worktrees
+ .iter()
+ .filter_map(|worktree| {
+ let worktree = worktree.upgrade(&cx)?.read(cx);
+ if worktree.is_visible() {
+ Some(format!(
+ "project-path-online:{}",
+ worktree.as_local().unwrap().abs_path().to_string_lossy()
+ ))
+ } else {
+ None
+ }
+ })
+ .collect::<Vec<_>>()
+ }
+
pub fn worktree_for_id(
&self,
id: WorktreeId,
@@ -757,6 +924,20 @@ impl Project {
.map(|worktree| worktree.read(cx).id())
}
+ pub fn contains_paths(&self, paths: &[PathBuf], cx: &AppContext) -> bool {
+ paths.iter().all(|path| self.contains_path(&path, cx))
+ }
+
+ pub fn contains_path(&self, path: &Path, cx: &AppContext) -> bool {
+ for worktree in self.worktrees(cx) {
+ let worktree = worktree.read(cx).as_local();
+ if worktree.map_or(false, |w| w.contains_abs_path(path)) {
+ return true;
+ }
+ }
+ false
+ }
+
pub fn create_entry(
&mut self,
project_path: impl Into<ProjectPath>,
@@ -3619,37 +3800,18 @@ impl Project {
});
let worktree = worktree?;
- let remote_project_id = project.update(&mut cx, |project, cx| {
+ let project_id = project.update(&mut cx, |project, cx| {
project.add_worktree(&worktree, cx);
- project.remote_id()
+ project.shared_remote_id()
});
- if let Some(project_id) = remote_project_id {
- // Because sharing is async, we may have *unshared* the project by the time it completes,
- // in which case we need to register the worktree instead.
- loop {
- if project.read_with(&cx, |project, _| project.is_shared()) {
- if worktree
- .update(&mut cx, |worktree, cx| {
- worktree.as_local_mut().unwrap().share(project_id, cx)
- })
- .await
- .is_ok()
- {
- break;
- }
- } else {
- worktree
- .update(&mut cx, |worktree, cx| {
- worktree
- .as_local_mut()
- .unwrap()
- .register(project_id, cx)
- })
- .await?;
- break;
- }
- }
+ if let Some(project_id) = project_id {
+ worktree
+ .update(&mut cx, |worktree, cx| {
+ worktree.as_local_mut().unwrap().share(project_id, cx)
+ })
+ .await
+ .log_err();
}
Ok(worktree)
@@ -3681,6 +3843,7 @@ impl Project {
false
}
});
+ self.metadata_changed(true, cx);
cx.notify();
}
@@ -3710,6 +3873,7 @@ impl Project {
self.worktrees
.push(WorktreeHandle::Weak(worktree.downgrade()));
}
+ self.metadata_changed(true, cx);
cx.emit(Event::WorktreeAdded);
cx.notify();
}
@@ -3992,40 +4156,51 @@ impl Project {
Ok(())
}
- async fn handle_register_worktree(
+ async fn handle_update_project(
this: ModelHandle<Self>,
- envelope: TypedEnvelope<proto::RegisterWorktree>,
+ envelope: TypedEnvelope<proto::UpdateProject>,
client: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, cx| {
- let remote_id = this.remote_id().ok_or_else(|| anyhow!("invalid project"))?;
let replica_id = this.replica_id();
- let worktree = proto::Worktree {
- id: envelope.payload.worktree_id,
- root_name: envelope.payload.root_name,
- entries: Default::default(),
- diagnostic_summaries: Default::default(),
- visible: envelope.payload.visible,
- scan_id: 0,
- };
- let (worktree, load_task) =
- Worktree::remote(remote_id, replica_id, worktree, client, cx);
- this.add_worktree(&worktree, cx);
- load_task.detach();
- Ok(())
- })
- }
+ let remote_id = this.remote_id().ok_or_else(|| anyhow!("invalid project"))?;
+
+ let mut old_worktrees_by_id = this
+ .worktrees
+ .drain(..)
+ .filter_map(|worktree| {
+ let worktree = worktree.upgrade(cx)?;
+ Some((worktree.read(cx).id(), worktree))
+ })
+ .collect::<HashMap<_, _>>();
+
+ for worktree in envelope.payload.worktrees {
+ if let Some(old_worktree) =
+ old_worktrees_by_id.remove(&WorktreeId::from_proto(worktree.id))
+ {
+ this.worktrees.push(WorktreeHandle::Strong(old_worktree));
+ } else {
+ let worktree = proto::Worktree {
+ id: worktree.id,
+ root_name: worktree.root_name,
+ entries: Default::default(),
+ diagnostic_summaries: Default::default(),
+ visible: worktree.visible,
+ scan_id: 0,
+ };
+ let (worktree, load_task) =
+ Worktree::remote(remote_id, replica_id, worktree, client.clone(), cx);
+ this.add_worktree(&worktree, cx);
+ load_task.detach();
+ }
+ }
+
+ this.metadata_changed(true, cx);
+ for (id, _) in old_worktrees_by_id {
+ cx.emit(Event::WorktreeRemoved(id));
+ }
- async fn handle_unregister_worktree(
- this: ModelHandle<Self>,
- envelope: TypedEnvelope<proto::UnregisterWorktree>,
- _: Arc<Client>,
- mut cx: AsyncAppContext,
- ) -> Result<()> {
- this.update(&mut cx, |this, cx| {
- let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
- this.remove_worktree(worktree_id, cx);
Ok(())
})
}
@@ -5132,6 +5307,49 @@ impl Project {
}
}
+impl ProjectStore {
+ pub fn new(db: Arc<Db>) -> Self {
+ Self {
+ db,
+ projects: Default::default(),
+ }
+ }
+
+ pub fn projects<'a>(
+ &'a self,
+ cx: &'a AppContext,
+ ) -> impl 'a + Iterator<Item = ModelHandle<Project>> {
+ self.projects
+ .iter()
+ .filter_map(|project| project.upgrade(cx))
+ }
+
+ fn add_project(&mut self, project: WeakModelHandle<Project>, cx: &mut ModelContext<Self>) {
+ if let Err(ix) = self
+ .projects
+ .binary_search_by_key(&project.id(), WeakModelHandle::id)
+ {
+ self.projects.insert(ix, project);
+ }
+ cx.notify();
+ }
+
+ fn prune_projects(&mut self, cx: &mut ModelContext<Self>) {
+ let mut did_change = false;
+ self.projects.retain(|project| {
+ if project.is_upgradable(cx) {
+ true
+ } else {
+ did_change = true;
+ false
+ }
+ });
+ if did_change {
+ cx.notify();
+ }
+ }
+}
+
impl WorktreeHandle {
pub fn upgrade(&self, cx: &AppContext) -> Option<ModelHandle<Worktree>> {
match self {
@@ -5210,10 +5428,16 @@ impl<'a> Iterator for CandidateSetIter<'a> {
}
}
+impl Entity for ProjectStore {
+ type Event = ();
+}
+
impl Entity for Project {
type Event = Event;
- fn release(&mut self, _: &mut gpui::MutableAppContext) {
+ fn release(&mut self, cx: &mut gpui::MutableAppContext) {
+ self.project_store.update(cx, ProjectStore::prune_projects);
+
match &self.client_state {
ProjectClientState::Local { remote_id_rx, .. } => {
if let Some(project_id) = *remote_id_rx.borrow() {
@@ -68,7 +68,6 @@ pub struct LocalWorktree {
last_scan_state_rx: watch::Receiver<ScanState>,
_background_scanner_task: Option<Task<()>>,
poll_task: Option<Task<()>>,
- registration: Registration,
share: Option<ShareState>,
diagnostics: HashMap<Arc<Path>, Vec<DiagnosticEntry<PointUtf16>>>,
diagnostic_summaries: TreeMap<PathKey, DiagnosticSummary>,
@@ -129,13 +128,6 @@ enum ScanState {
Err(Arc<anyhow::Error>),
}
-#[derive(Debug, Eq, PartialEq)]
-enum Registration {
- None,
- Pending,
- Done { project_id: u64 },
-}
-
struct ShareState {
project_id: u64,
snapshots_tx: Sender<LocalSnapshot>,
@@ -148,19 +140,6 @@ pub enum Event {
impl Entity for Worktree {
type Event = Event;
-
- fn release(&mut self, _: &mut MutableAppContext) {
- if let Some(worktree) = self.as_local_mut() {
- if let Registration::Done { project_id } = worktree.registration {
- let client = worktree.client.clone();
- let unregister_message = proto::UnregisterWorktree {
- project_id,
- worktree_id: worktree.id().to_proto(),
- };
- client.send(unregister_message).log_err();
- }
- }
- }
}
impl Worktree {
@@ -486,7 +465,6 @@ impl LocalWorktree {
background_snapshot: Arc::new(Mutex::new(snapshot)),
last_scan_state_rx,
_background_scanner_task: None,
- registration: Registration::None,
share: None,
poll_task: None,
diagnostics: Default::default(),
@@ -608,6 +586,14 @@ impl LocalWorktree {
self.snapshot.clone()
}
+ pub fn metadata_proto(&self) -> proto::WorktreeMetadata {
+ proto::WorktreeMetadata {
+ id: self.id().to_proto(),
+ root_name: self.root_name().to_string(),
+ visible: self.visible,
+ }
+ }
+
fn load(&self, path: &Path, cx: &mut ModelContext<Worktree>) -> Task<Result<(File, String)>> {
let handle = cx.handle();
let path = Arc::from(path);
@@ -904,46 +890,7 @@ impl LocalWorktree {
})
}
- pub fn register(
- &mut self,
- project_id: u64,
- cx: &mut ModelContext<Worktree>,
- ) -> Task<anyhow::Result<()>> {
- if self.registration != Registration::None {
- return Task::ready(Ok(()));
- }
-
- self.registration = Registration::Pending;
- let client = self.client.clone();
- let register_message = proto::RegisterWorktree {
- project_id,
- worktree_id: self.id().to_proto(),
- root_name: self.root_name().to_string(),
- visible: self.visible,
- };
- let request = client.request(register_message);
- cx.spawn(|this, mut cx| async move {
- let response = request.await;
- this.update(&mut cx, |this, _| {
- let worktree = this.as_local_mut().unwrap();
- match response {
- Ok(_) => {
- if worktree.registration == Registration::Pending {
- worktree.registration = Registration::Done { project_id };
- }
- Ok(())
- }
- Err(error) => {
- worktree.registration = Registration::None;
- Err(error)
- }
- }
- })
- })
- }
-
pub fn share(&mut self, project_id: u64, cx: &mut ModelContext<Worktree>) -> Task<Result<()>> {
- let register = self.register(project_id, cx);
let (share_tx, share_rx) = oneshot::channel();
let (snapshots_to_send_tx, snapshots_to_send_rx) =
smol::channel::unbounded::<LocalSnapshot>();
@@ -1048,7 +995,6 @@ impl LocalWorktree {
}
cx.spawn_weak(|this, cx| async move {
- register.await?;
if let Some(this) = this.upgrade(&cx) {
this.read_with(&cx, |this, _| {
let this = this.as_local().unwrap();
@@ -1061,11 +1007,6 @@ impl LocalWorktree {
})
}
- pub fn unregister(&mut self) {
- self.unshare();
- self.registration = Registration::None;
- }
-
pub fn unshare(&mut self) {
self.share.take();
}
@@ -31,7 +31,7 @@ tracing = { version = "0.1.34", features = ["log"] }
zstd = "0.9"
[build-dependencies]
-prost-build = "0.8"
+prost-build = "0.9"
[dev-dependencies]
collections = { path = "../collections", features = ["test-support"] }
@@ -35,8 +35,7 @@ message Envelope {
OpenBufferForSymbol open_buffer_for_symbol = 28;
OpenBufferForSymbolResponse open_buffer_for_symbol_response = 29;
- RegisterWorktree register_worktree = 30;
- UnregisterWorktree unregister_worktree = 31;
+ UpdateProject update_project = 30;
UpdateWorktree update_worktree = 32;
CreateProjectEntry create_project_entry = 33;
@@ -129,6 +128,11 @@ message UnregisterProject {
uint64 project_id = 1;
}
+message UpdateProject {
+ uint64 project_id = 1;
+ repeated WorktreeMetadata worktrees = 2;
+}
+
message RequestJoinProject {
uint64 requester_id = 1;
uint64 project_id = 2;
@@ -177,18 +181,6 @@ message LeaveProject {
uint64 project_id = 1;
}
-message RegisterWorktree {
- uint64 project_id = 1;
- uint64 worktree_id = 2;
- string root_name = 3;
- bool visible = 4;
-}
-
-message UnregisterWorktree {
- uint64 project_id = 1;
- uint64 worktree_id = 2;
-}
-
message UpdateWorktree {
uint64 project_id = 1;
uint64 worktree_id = 2;
@@ -934,3 +926,9 @@ message ProjectMetadata {
repeated string worktree_root_names = 3;
repeated uint64 guests = 4;
}
+
+message WorktreeMetadata {
+ uint64 id = 1;
+ string root_name = 2;
+ bool visible = 3;
+}
@@ -132,7 +132,6 @@ messages!(
(Ping, Foreground),
(ProjectUnshared, Foreground),
(RegisterProject, Foreground),
- (RegisterWorktree, Foreground),
(ReloadBuffers, Foreground),
(ReloadBuffersResponse, Foreground),
(RemoveProjectCollaborator, Foreground),
@@ -151,7 +150,6 @@ messages!(
(Test, Foreground),
(Unfollow, Foreground),
(UnregisterProject, Foreground),
- (UnregisterWorktree, Foreground),
(UpdateBuffer, Foreground),
(UpdateBufferFile, Foreground),
(UpdateContacts, Foreground),
@@ -159,6 +157,7 @@ messages!(
(UpdateFollowers, Foreground),
(UpdateInviteInfo, Foreground),
(UpdateLanguageServer, Foreground),
+ (UpdateProject, Foreground),
(UpdateWorktree, Foreground),
);
@@ -192,7 +191,6 @@ request_messages!(
(PerformRename, PerformRenameResponse),
(PrepareRename, PrepareRenameResponse),
(RegisterProject, RegisterProjectResponse),
- (RegisterWorktree, Ack),
(ReloadBuffers, ReloadBuffersResponse),
(RequestContact, Ack),
(RemoveContact, Ack),
@@ -202,6 +200,7 @@ request_messages!(
(SearchProject, SearchProjectResponse),
(SendChannelMessage, SendChannelMessageResponse),
(Test, Test),
+ (UnregisterProject, Ack),
(UpdateBuffer, Ack),
(UpdateWorktree, Ack),
);
@@ -242,13 +241,12 @@ entity_messages!(
StartLanguageServer,
Unfollow,
UnregisterProject,
- UnregisterWorktree,
UpdateBuffer,
UpdateBufferFile,
UpdateDiagnosticSummary,
UpdateFollowers,
UpdateLanguageServer,
- RegisterWorktree,
+ UpdateProject,
UpdateWorktree,
);
@@ -6,4 +6,4 @@ pub use conn::Connection;
pub use peer::*;
mod macros;
-pub const PROTOCOL_VERSION: u32 = 21;
+pub const PROTOCOL_VERSION: u32 = 22;
@@ -19,6 +19,7 @@ pub use keymap_file::{keymap_file_json_schema, KeymapFileContent};
#[derive(Clone)]
pub struct Settings {
+ pub projects_online_by_default: bool,
pub buffer_font_family: FamilyId,
pub buffer_font_size: f32,
pub default_buffer_font_size: f32,
@@ -49,6 +50,8 @@ pub enum SoftWrap {
#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
pub struct SettingsFileContent {
+ #[serde(default)]
+ pub projects_online_by_default: Option<bool>,
#[serde(default)]
pub buffer_font_family: Option<String>,
#[serde(default)]
@@ -81,6 +84,7 @@ impl Settings {
preferred_line_length: 80,
language_overrides: Default::default(),
format_on_save: true,
+ projects_online_by_default: true,
theme,
})
}
@@ -135,6 +139,7 @@ impl Settings {
preferred_line_length: 80,
format_on_save: true,
language_overrides: Default::default(),
+ projects_online_by_default: true,
theme: gpui::fonts::with_font_cache(cx.font_cache().clone(), || Default::default()),
}
}
@@ -164,6 +169,10 @@ impl Settings {
}
}
+ merge(
+ &mut self.projects_online_by_default,
+ data.projects_online_by_default,
+ );
merge(&mut self.buffer_font_size, data.buffer_font_size);
merge(&mut self.default_buffer_font_size, data.buffer_font_size);
merge(&mut self.vim_mode, data.vim_mode);
@@ -13,5 +13,5 @@ log = { version = "0.4.16", features = ["kv_unstable_serde"] }
[dev-dependencies]
ctor = "0.1"
-env_logger = "0.8"
+env_logger = "0.9"
rand = "0.8.3"
@@ -28,5 +28,5 @@ collections = { path = "../collections", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }
ctor = "0.1"
-env_logger = "0.8"
+env_logger = "0.9"
rand = "0.8.3"
@@ -279,8 +279,9 @@ pub struct ContactsPanel {
pub contact_username: ContainedText,
pub contact_button: Interactive<IconButton>,
pub contact_button_spacing: f32,
- pub disabled_contact_button: IconButton,
+ pub disabled_button: IconButton,
pub tree_branch: Interactive<TreeBranch>,
+ pub private_button: Interactive<IconButton>,
pub section_icon_size: f32,
pub invite_row: Interactive<ContainedLabel>,
}
@@ -318,7 +319,7 @@ pub struct Icon {
pub path: String,
}
-#[derive(Clone, Deserialize, Default)]
+#[derive(Deserialize, Clone, Copy, Default)]
pub struct IconButton {
#[serde(flatten)]
pub container: ContainerStyle,
@@ -85,9 +85,10 @@ impl WaitingRoom {
project_id,
app_state.client.clone(),
app_state.user_store.clone(),
+ app_state.project_store.clone(),
app_state.languages.clone(),
app_state.fs.clone(),
- &mut cx,
+ cx.clone(),
)
.await;
@@ -17,19 +17,20 @@ use gpui::{
color::Color,
elements::*,
geometry::{rect::RectF, vector::vec2f, PathBuilder},
- impl_internal_actions,
+ impl_actions, impl_internal_actions,
json::{self, ToJson},
platform::{CursorStyle, WindowOptions},
AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Border, Entity, ImageData,
- ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View,
- ViewContext, ViewHandle, WeakViewHandle,
+ ModelContext, ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext,
+ Task, View, ViewContext, ViewHandle, WeakViewHandle,
};
use language::LanguageRegistry;
use log::error;
pub use pane::*;
pub use pane_group::*;
use postage::prelude::Stream;
-use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
+use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, ProjectStore, Worktree, WorktreeId};
+use serde::Deserialize;
use settings::Settings;
use sidebar::{Side, Sidebar, SidebarButtons, ToggleSidebarItem, ToggleSidebarItemFocus};
use smallvec::SmallVec;
@@ -98,6 +99,12 @@ pub struct OpenPaths {
pub paths: Vec<PathBuf>,
}
+#[derive(Clone, Deserialize)]
+pub struct ToggleProjectOnline {
+ #[serde(skip_deserializing)]
+ pub project: Option<ModelHandle<Project>>,
+}
+
#[derive(Clone)]
pub struct ToggleFollow(pub PeerId);
@@ -116,6 +123,7 @@ impl_internal_actions!(
RemoveFolderFromProject
]
);
+impl_actions!(workspace, [ToggleProjectOnline]);
pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
pane::init(cx);
@@ -160,6 +168,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
cx.add_async_action(Workspace::save_all);
cx.add_action(Workspace::add_folder_to_project);
cx.add_action(Workspace::remove_folder_from_project);
+ cx.add_action(Workspace::toggle_project_online);
cx.add_action(
|workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext<Workspace>| {
let pane = workspace.active_pane().clone();
@@ -222,6 +231,7 @@ pub struct AppState {
pub themes: Arc<ThemeRegistry>,
pub client: Arc<client::Client>,
pub user_store: ModelHandle<client::UserStore>,
+ pub project_store: ModelHandle<ProjectStore>,
pub fs: Arc<dyn fs::Fs>,
pub build_window_options: fn() -> WindowOptions<'static>,
pub initialize_workspace: fn(&mut Workspace, &Arc<AppState>, &mut ViewContext<Workspace>),
@@ -682,6 +692,7 @@ impl AppState {
let languages = Arc::new(LanguageRegistry::test());
let http_client = client::test::FakeHttpClient::with_404_response();
let client = Client::new(http_client.clone());
+ let project_store = cx.add_model(|_| ProjectStore::new(project::Db::open_fake()));
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
let themes = ThemeRegistry::new((), cx.font_cache().clone());
Arc::new(Self {
@@ -690,6 +701,7 @@ impl AppState {
fs,
languages,
user_store,
+ project_store,
initialize_workspace: |_, _, _| {},
build_window_options: || Default::default(),
})
@@ -837,10 +849,7 @@ impl Workspace {
_observe_current_user,
};
this.project_remote_id_changed(this.project.read(cx).remote_id(), cx);
-
- cx.defer(|this, cx| {
- this.update_window_title(cx);
- });
+ cx.defer(|this, cx| this.update_window_title(cx));
this
}
@@ -876,20 +885,6 @@ impl Workspace {
self.project.read(cx).worktrees(cx)
}
- pub fn contains_paths(&self, paths: &[PathBuf], cx: &AppContext) -> bool {
- paths.iter().all(|path| self.contains_path(&path, cx))
- }
-
- pub fn contains_path(&self, path: &Path, cx: &AppContext) -> bool {
- for worktree in self.worktrees(cx) {
- let worktree = worktree.read(cx).as_local();
- if worktree.map_or(false, |w| w.contains_abs_path(path)) {
- return true;
- }
- }
- false
- }
-
pub fn worktree_scans_complete(&self, cx: &AppContext) -> impl Future<Output = ()> + 'static {
let futures = self
.worktrees(cx)
@@ -1054,6 +1049,17 @@ impl Workspace {
.update(cx, |project, cx| project.remove_worktree(*worktree_id, cx));
}
+ fn toggle_project_online(&mut self, action: &ToggleProjectOnline, cx: &mut ViewContext<Self>) {
+ let project = action
+ .project
+ .clone()
+ .unwrap_or_else(|| self.project.clone());
+ project.update(cx, |project, cx| {
+ let public = !project.is_online();
+ project.set_online(public, cx);
+ });
+ }
+
fn project_path_for_path(
&self,
abs_path: &Path,
@@ -1668,8 +1674,15 @@ impl Workspace {
}
fn render_titlebar(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
+ let project = &self.project.read(cx);
+ let replica_id = project.replica_id();
let mut worktree_root_names = String::new();
- self.worktree_root_names(&mut worktree_root_names, cx);
+ for (i, name) in project.worktree_root_names(cx).enumerate() {
+ if i > 0 {
+ worktree_root_names.push_str(", ");
+ }
+ worktree_root_names.push_str(name);
+ }
ConstrainedBox::new(
Container::new(
@@ -1686,7 +1699,7 @@ impl Workspace {
.with_children(self.render_collaborators(theme, cx))
.with_children(self.render_current_user(
self.user_store.read(cx).current_user().as_ref(),
- self.project.read(cx).replica_id(),
+ replica_id,
theme,
cx,
))
@@ -1714,6 +1727,7 @@ impl Workspace {
fn update_window_title(&mut self, cx: &mut ViewContext<Self>) {
let mut title = String::new();
+ let project = self.project().read(cx);
if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
let filename = path
.path
@@ -1721,8 +1735,7 @@ impl Workspace {
.map(|s| s.to_string_lossy())
.or_else(|| {
Some(Cow::Borrowed(
- self.project()
- .read(cx)
+ project
.worktree_for_id(path.worktree_id, cx)?
.read(cx)
.root_name(),
@@ -1733,22 +1746,18 @@ impl Workspace {
title.push_str(" — ");
}
}
- self.worktree_root_names(&mut title, cx);
+ for (i, name) in project.worktree_root_names(cx).enumerate() {
+ if i > 0 {
+ title.push_str(", ");
+ }
+ title.push_str(name);
+ }
if title.is_empty() {
title = "empty project".to_string();
}
cx.set_window_title(&title);
}
- fn worktree_root_names(&self, string: &mut String, cx: &mut MutableAppContext) {
- for (i, worktree) in self.project.read(cx).visible_worktrees(cx).enumerate() {
- if i != 0 {
- string.push_str(", ");
- }
- string.push_str(worktree.read(cx).root_name());
- }
- }
-
fn render_collaborators(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> Vec<ElementBox> {
let mut collaborators = self
.project
@@ -2365,6 +2374,22 @@ fn open(_: &Open, cx: &mut MutableAppContext) {
pub struct WorkspaceCreated(WeakViewHandle<Workspace>);
+pub fn activate_workspace_for_project(
+ cx: &mut MutableAppContext,
+ predicate: impl Fn(&mut Project, &mut ModelContext<Project>) -> bool,
+) -> Option<ViewHandle<Workspace>> {
+ for window_id in cx.window_ids().collect::<Vec<_>>() {
+ if let Some(workspace_handle) = cx.root_view::<Workspace>(window_id) {
+ let project = workspace_handle.read(cx).project.clone();
+ if project.update(cx, &predicate) {
+ cx.activate_window(window_id);
+ return Some(workspace_handle);
+ }
+ }
+ }
+ None
+}
+
pub fn open_paths(
abs_paths: &[PathBuf],
app_state: &Arc<AppState>,
@@ -2376,26 +2401,13 @@ pub fn open_paths(
log::info!("open paths {:?}", abs_paths);
// Open paths in existing workspace if possible
- let mut existing = None;
- for window_id in cx.window_ids().collect::<Vec<_>>() {
- if let Some(workspace_handle) = cx.root_view::<Workspace>(window_id) {
- if workspace_handle.update(cx, |workspace, cx| {
- if workspace.contains_paths(abs_paths, cx.as_ref()) {
- cx.activate_window(window_id);
- existing = Some(workspace_handle.clone());
- true
- } else {
- false
- }
- }) {
- break;
- }
- }
- }
+ let existing =
+ activate_workspace_for_project(cx, |project, cx| project.contains_paths(abs_paths, cx));
let app_state = app_state.clone();
let abs_paths = abs_paths.to_vec();
cx.spawn(|mut cx| async move {
+ let mut new_project = None;
let workspace = if let Some(existing) = existing {
existing
} else {
@@ -2405,16 +2417,17 @@ pub fn open_paths(
.contains(&false);
cx.add_window((app_state.build_window_options)(), |cx| {
- let mut workspace = Workspace::new(
- Project::local(
- app_state.client.clone(),
- app_state.user_store.clone(),
- app_state.languages.clone(),
- app_state.fs.clone(),
- cx,
- ),
+ let project = Project::local(
+ false,
+ app_state.client.clone(),
+ app_state.user_store.clone(),
+ app_state.project_store.clone(),
+ app_state.languages.clone(),
+ app_state.fs.clone(),
cx,
);
+ new_project = Some(project.clone());
+ let mut workspace = Workspace::new(project, cx);
(app_state.initialize_workspace)(&mut workspace, &app_state, cx);
if contains_directory {
workspace.toggle_sidebar_item(
@@ -2433,6 +2446,14 @@ pub fn open_paths(
let items = workspace
.update(&mut cx, |workspace, cx| workspace.open_paths(abs_paths, cx))
.await;
+
+ if let Some(project) = new_project {
+ project
+ .update(&mut cx, |project, cx| project.restore_state(cx))
+ .await
+ .log_err();
+ }
+
(workspace, items)
})
}
@@ -2463,8 +2484,10 @@ fn open_new(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
let (window_id, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
let mut workspace = Workspace::new(
Project::local(
+ false,
app_state.client.clone(),
app_state.user_store.clone(),
+ app_state.project_store.clone(),
app_state.languages.clone(),
app_state.fs.clone(),
cx,
@@ -59,7 +59,7 @@ chrono = "0.4"
ctor = "0.1.20"
dirs = "3.0"
easy-parallel = "3.1.0"
-env_logger = "0.8"
+env_logger = "0.9"
futures = "0.3"
http-auth-basic = "0.1.3"
ignore = "0.4"
@@ -108,7 +108,7 @@ client = { path = "../client", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] }
-env_logger = "0.8"
+env_logger = "0.9"
serde_json = { version = "1.0", features = ["preserve_order"] }
unindent = "0.1.7"
@@ -23,7 +23,7 @@ use gpui::{executor::Background, App, AssetSource, AsyncAppContext, Task};
use isahc::{config::Configurable, AsyncBody, Request};
use log::LevelFilter;
use parking_lot::Mutex;
-use project::Fs;
+use project::{Fs, ProjectStore};
use serde_json::json;
use settings::{self, KeymapFileContent, Settings, SettingsFileContent};
use smol::process::Command;
@@ -48,9 +48,10 @@ use zed::{
fn main() {
let http = http::client();
- let logs_dir_path = dirs::home_dir()
- .expect("could not find home dir")
- .join("Library/Logs/Zed");
+ let home_dir = dirs::home_dir().expect("could not find home dir");
+ let db_dir_path = home_dir.join("Library/Application Support/Zed");
+ let logs_dir_path = home_dir.join("Library/Logs/Zed");
+ fs::create_dir_all(&db_dir_path).expect("could not create database path");
fs::create_dir_all(&logs_dir_path).expect("could not create logs path");
init_logger(&logs_dir_path);
@@ -59,6 +60,11 @@ fn main() {
.or_else(|| app.platform().app_version().ok())
.map_or("dev".to_string(), |v| v.to_string());
init_panic_hook(logs_dir_path, app_version, http.clone(), app.background());
+ let db = app.background().spawn(async move {
+ project::Db::open(db_dir_path.join("zed.db"))
+ .log_err()
+ .unwrap_or(project::Db::null())
+ });
load_embedded_fonts(&app);
@@ -169,6 +175,7 @@ fn main() {
search::init(cx);
vim::init(cx);
+ let db = cx.background().block(db);
let (settings_file, keymap_file) = cx.background().block(config_files).unwrap();
let mut settings_rx = settings_from_files(
default_settings,
@@ -204,11 +211,13 @@ fn main() {
.detach();
cx.set_global(settings);
+ let project_store = cx.add_model(|_| ProjectStore::new(db));
let app_state = Arc::new(AppState {
languages,
themes,
client: client.clone(),
user_store,
+ project_store,
fs,
build_window_options,
initialize_workspace,
@@ -181,7 +181,12 @@ pub fn initialize_workspace(
let project_panel = ProjectPanel::new(workspace.project().clone(), cx);
let contact_panel = cx.add_view(|cx| {
- ContactsPanel::new(app_state.user_store.clone(), workspace.weak_handle(), cx)
+ ContactsPanel::new(
+ app_state.user_store.clone(),
+ app_state.project_store.clone(),
+ workspace.weak_handle(),
+ cx,
+ )
});
workspace.left_sidebar().update(cx, |sidebar, cx| {
@@ -295,8 +300,10 @@ fn open_config_file(
let (_, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
let mut workspace = Workspace::new(
Project::local(
+ false,
app_state.client.clone(),
app_state.user_store.clone(),
+ app_state.project_store.clone(),
app_state.languages.clone(),
app_state.fs.clone(),
cx,
@@ -68,6 +68,12 @@ export default function contactsPanel(theme: Theme) {
buttonWidth: 8,
iconWidth: 8,
},
+ privateButton: {
+ iconWidth: 8,
+ color: iconColor(theme, "primary"),
+ cornerRadius: 5,
+ buttonWidth: 12,
+ },
rowHeight: 28,
sectionIconSize: 8,
headerRow: {
@@ -118,7 +124,7 @@ export default function contactsPanel(theme: Theme) {
background: backgroundColor(theme, 100, "hovered"),
},
},
- disabledContactButton: {
+ disabledButton: {
...contactButton,
background: backgroundColor(theme, 100),
color: iconColor(theme, "muted"),