Merge pull request #1084 from zed-industries/private-projects

Offline projects
This commit is contained in:
Max Brunsfeld 2022-06-03 17:14:46 -07:00 committed by GitHub
commit ff3e3d0799
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 2301 additions and 1234 deletions

144
Cargo.lock generated
View File

@ -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"
@ -2847,16 +2874,6 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "nom"
version = "5.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af"
dependencies = [
"memchr",
"version_check",
]
[[package]]
name = "nom"
version = "6.1.2"
@ -2870,6 +2887,16 @@ dependencies = [
"version_check",
]
[[package]]
name = "nom"
version = "7.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
name = "ntapi"
version = "0.3.7"
@ -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",
]

3
assets/icons/lock-8.svg Normal file
View File

@ -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>

After

Width:  |  Height:  |  Size: 413 B

View File

@ -67,9 +67,14 @@ 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<
>,
#[cfg(any(test, feature = "test-support"))]
establish_connection: RwLock<
Option<
Box<
dyn 'static
+ Send
@ -80,6 +85,7 @@ pub struct Client {
) -> Task<Result<Connection, EstablishConnectionError>>,
>,
>,
>,
}
#[derive(Error, Debug)]
@ -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(

View File

@ -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

View File

@ -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>,

View File

@ -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"] }

File diff suppressed because it is too large Load Diff

View File

@ -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()

View File

@ -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(())
}

View File

@ -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(

View File

@ -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"

View File

@ -9,6 +9,7 @@ doctest = false
[dependencies]
client = { path = "../client" }
collections = { path = "../collections" }
editor = { path = "../editor" }
fuzzy = { path = "../fuzzy" }
gpui = { path = "../gpui" }

View File

@ -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,23 +152,17 @@ 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))
{
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,
)
ContactNotification::new(user.clone(), *kind, user_store, cx)
})
}),
_ => {}
@ -176,28 +174,29 @@ impl ContactsPanel {
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 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 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(&section);
Self::render_header(*section, theme, is_selected, is_collapsed, cx)
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,
&theme.contacts_panel,
true,
is_selected,
cx,
@ -205,36 +204,47 @@ impl ContactsPanel {
ContactEntry::OutgoingRequest(user) => Self::render_contact_request(
user.clone(),
this.user_store.clone(),
theme,
&theme.contacts_panel,
false,
is_selected,
cx,
),
ContactEntry::Contact(contact) => {
Self::render_contact(contact.clone(), theme, is_selected)
Self::render_contact(&contact.user, &theme.contacts_panel, is_selected)
}
ContactEntry::ContactProject(contact, project_ix) => {
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 {
if let ContactEntry::ContactProject(next_contact, _, _) = next {
next_contact.user.id != contact.user.id
} else {
true
}
});
Self::render_contact_project(
Self::render_project(
contact.clone(),
current_user_id,
*project_ix,
theme,
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,15 +368,17 @@ 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(
Stack::new()
.with_child(
Canvas::new(move |bounds, _, cx| {
let start_x =
bounds.min_x() + (bounds.width() / 2.) - (tree_branch.width / 2.);
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.);
@ -397,6 +409,53 @@ impl ContactsPanel {
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(),
@ -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 {
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(
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::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))
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 {
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",
@ -1068,9 +1544,8 @@ mod tests {
});
cx.foreground().run_until_parked();
assert_eq!(
render_to_strings(&panel, cx),
cx.read(|cx| render_to_strings(&panel, cx)),
&[
"+",
"v Online",
" user_four <=== selected",
" dir2",
@ -1083,9 +1558,8 @@ mod tests {
panel.select_next(&Default::default(), cx);
});
assert_eq!(
render_to_strings(&panel, cx),
cx.read(|cx| render_to_strings(&panel, cx)),
&[
"+",
"v Online",
" user_four",
" dir2 <=== selected",
@ -1098,9 +1572,8 @@ mod tests {
panel.select_next(&Default::default(), cx);
});
assert_eq!(
render_to_strings(&panel, cx),
cx.read(|cx| render_to_strings(&panel, cx)),
&[
"+",
"v Online",
" user_four",
" dir2",
@ -1110,10 +1583,9 @@ mod tests {
);
}
fn render_to_strings(panel: &ViewHandle<ContactsPanel>, cx: &TestAppContext) -> Vec<String> {
panel.read_with(cx, |panel, _| {
fn render_to_strings(panel: &ViewHandle<ContactsPanel>, cx: &AppContext) -> Vec<String> {
let panel = panel.read(cx);
let mut entries = Vec::new();
entries.push("+".to_string());
entries.extend(panel.entries.iter().enumerate().map(|(ix, entry)| {
let mut string = match entry {
ContactEntry::Header(name) => {
@ -1133,10 +1605,33 @@ mod tests {
ContactEntry::Contact(contact) => {
format!(" {}", contact.user.github_login)
}
ContactEntry::ContactProject(contact, project_ix) => {
ContactEntry::ContactProject(contact, project_ix, project) => {
let project = project
.and_then(|p| p.upgrade(cx))
.map(|project| project.read(cx));
format!(
" {}",
contact.projects[*project_ix].worktree_root_names.join(", ")
" {}{}",
contact.projects[*project_ix].worktree_root_names.join(", "),
if project.map_or(true, |project| project.is_online()) {
""
} else {
" (going offline...)"
},
)
}
ContactEntry::OfflineProject(project) => {
let project = project.upgrade(cx).unwrap().read(cx);
format!(
" 🔒 {}{}",
project
.worktree_root_names(cx)
.collect::<Vec<_>>()
.join(", "),
if project.is_online() {
" (going online...)"
} else {
""
},
)
}
};
@ -1148,30 +1643,5 @@ mod tests {
string
}));
entries
})
}
async fn init(cx: &mut TestAppContext) -> (Arc<AppState>, FakeServer) {
cx.update(|cx| cx.set_global(Settings::test(cx)));
let themes = ThemeRegistry::new((), cx.font_cache());
let fs = project::FakeFs::new(cx.background().clone());
let languages = Arc::new(LanguageRegistry::test());
let http_client = client::test::FakeHttpClient::with_404_response();
let mut client = Client::new(http_client.clone());
let server = FakeServer::for_client(100, &mut client, &cx).await;
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
(
Arc::new(AppState {
languages,
themes,
client,
user_store: user_store.clone(),
fs,
build_window_options: || Default::default(),
initialize_workspace: |_, _, _| {},
}),
server,
)
}
}

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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)
}

View File

@ -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;

View File

@ -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 = "*"

View File

@ -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"

View File

@ -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"

View File

@ -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"] }

161
crates/project/src/db.rs Normal file
View File

@ -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,]
);
}
}

View File

@ -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?;
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));
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,54 +681,84 @@ impl Project {
&self.fs
}
fn unregister(&mut self, cx: &mut ModelContext<Self>) {
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);
}
}
}
pub fn is_online(&self) -> bool {
match &self.client_state {
ProjectClientState::Local { online_rx, .. } => *online_rx.borrow(),
ProjectClientState::Remote { .. } => true,
}
}
fn unregister(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
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();
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)
});
}
}
if let ProjectClientState::Local { remote_id_tx, .. } = &mut self.client_state {
*remote_id_tx.borrow_mut() = None;
}
self.subscriptions.clear();
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(())
})
})
}
pub fn remote_id(&self) -> Option<u64> {
@ -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
if let Some(project_id) = project_id {
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;
}
}
.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 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: envelope.payload.worktree_id,
root_name: envelope.payload.root_name,
id: worktree.id,
root_name: worktree.root_name,
entries: Default::default(),
diagnostic_summaries: Default::default(),
visible: envelope.payload.visible,
visible: worktree.visible,
scan_id: 0,
};
let (worktree, load_task) =
Worktree::remote(remote_id, replica_id, worktree, client, cx);
Worktree::remote(remote_id, replica_id, worktree, client.clone(), cx);
this.add_worktree(&worktree, cx);
load_task.detach();
Ok(())
})
}
}
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() {

View File

@ -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();
}

View File

@ -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"] }

View File

@ -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;
}

View File

@ -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,
);

View File

@ -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;

View File

@ -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);

View File

@ -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"

View File

@ -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"

View File

@ -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,

View File

@ -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;

View File

@ -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(
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,
),
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,

View File

@ -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"

View File

@ -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,

View File

@ -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,

View File

@ -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"),