diff --git a/Cargo.lock b/Cargo.lock index cd79c19630..4a9c4616b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1507,7 +1507,7 @@ dependencies = [ "parking_lot 0.11.2", "postage", "rand 0.8.5", - "rpc", + "rpc2", "schemars", "serde", "serde_derive", @@ -1836,6 +1836,7 @@ dependencies = [ "log", "lsp2", "node_runtime", + "parking_lot 0.11.2", "rpc", "serde", "serde_derive", @@ -4285,7 +4286,6 @@ dependencies = [ "collections", "ctor", "env_logger 0.9.3", - "fs", "futures 0.3.28", "fuzzy2", "git", @@ -4299,7 +4299,7 @@ dependencies = [ "postage", "rand 0.8.5", "regex", - "rpc", + "rpc2", "schemars", "serde", "serde_derive", @@ -5907,6 +5907,7 @@ dependencies = [ "log", "lsp2", "node_runtime", + "parking_lot 0.11.2", "serde", "serde_derive", "serde_json", @@ -6087,7 +6088,7 @@ dependencies = [ "pretty_assertions", "rand 0.8.5", "regex", - "rpc", + "rpc2", "schemars", "serde", "serde_derive", @@ -6839,6 +6840,35 @@ dependencies = [ "zstd", ] +[[package]] +name = "rpc2" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-lock", + "async-tungstenite", + "base64 0.13.1", + "clock", + "collections", + "ctor", + "env_logger 0.9.3", + "futures 0.3.28", + "gpui2", + "parking_lot 0.11.2", + "prost 0.8.0", + "prost-build", + "rand 0.8.5", + "rsa 0.4.0", + "serde", + "serde_derive", + "smol", + "smol-timeout", + "tempdir", + "tracing", + "util", + "zstd", +] + [[package]] name = "rsa" version = "0.4.0" @@ -10843,7 +10873,7 @@ dependencies = [ "project2", "rand 0.8.5", "regex", - "rpc", + "rpc2", "rsa 0.4.0", "rust-embed", "schemars", diff --git a/Cargo.toml b/Cargo.toml index 0bb710fc1d..ed0c09cc26 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,6 +73,7 @@ members = [ "crates/recent_projects", "crates/rope", "crates/rpc", + "crates/rpc2", "crates/search", "crates/settings", "crates/settings2", diff --git a/crates/client2/Cargo.toml b/crates/client2/Cargo.toml index 3203b71819..8a6edbb428 100644 --- a/crates/client2/Cargo.toml +++ b/crates/client2/Cargo.toml @@ -9,14 +9,14 @@ path = "src/client2.rs" doctest = false [features] -test-support = ["collections/test-support", "gpui2/test-support", "rpc/test-support"] +test-support = ["collections/test-support", "gpui2/test-support", "rpc2/test-support"] [dependencies] collections = { path = "../collections" } db2 = { path = "../db2" } gpui2 = { path = "../gpui2" } util = { path = "../util" } -rpc = { path = "../rpc" } +rpc2 = { path = "../rpc2" } text = { path = "../text" } settings2 = { path = "../settings2" } feature_flags2 = { path = "../feature_flags2" } @@ -47,6 +47,6 @@ url = "2.2" [dev-dependencies] collections = { path = "../collections", features = ["test-support"] } gpui2 = { path = "../gpui2", features = ["test-support"] } -rpc = { path = "../rpc", features = ["test-support"] } +rpc2 = { path = "../rpc2", features = ["test-support"] } settings = { path = "../settings", features = ["test-support"] } util = { path = "../util", features = ["test-support"] } diff --git a/crates/client2/src/client2.rs b/crates/client2/src/client2.rs index 6072f47397..79b0205c91 100644 --- a/crates/client2/src/client2.rs +++ b/crates/client2/src/client2.rs @@ -21,7 +21,7 @@ use lazy_static::lazy_static; use parking_lot::RwLock; use postage::watch; use rand::prelude::*; -use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, PeerId, RequestMessage}; +use rpc2::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, PeerId, RequestMessage}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings2::Settings; @@ -43,7 +43,7 @@ use util::channel::ReleaseChannel; use util::http::HttpClient; use util::{ResultExt, TryFutureExt}; -pub use rpc::*; +pub use rpc2::*; pub use telemetry::ClickhouseEvent; pub use user::*; @@ -975,7 +975,7 @@ impl Client { "Authorization", format!("{} {}", credentials.user_id, credentials.access_token), ) - .header("x-zed-protocol-version", rpc::PROTOCOL_VERSION); + .header("x-zed-protocol-version", rpc2::PROTOCOL_VERSION); let http = self.http.clone(); cx.executor().spawn(async move { @@ -1025,7 +1025,7 @@ impl Client { // zed server to encrypt the user's access token, so that it can'be intercepted by // any other app running on the user's device. let (public_key, private_key) = - rpc::auth::keypair().expect("failed to generate keypair for auth"); + rpc2::auth::keypair().expect("failed to generate keypair for auth"); let public_key_string = String::try_from(public_key).expect("failed to serialize public key for auth"); @@ -1377,290 +1377,275 @@ pub fn decode_worktree_url(url: &str) -> Option<(u64, String)> { Some((id, access_token.to_string())) } -// #[cfg(test)] -// mod tests { -// use super::*; -// use crate::test::FakeServer; -// use gpui::{executor::Deterministic, TestAppContext}; -// use parking_lot::Mutex; -// use std::future; -// use util::http::FakeHttpClient; +#[cfg(test)] +mod tests { + use super::*; + use crate::test::FakeServer; -// #[gpui::test(iterations = 10)] -// async fn test_reconnection(cx: &mut TestAppContext) { -// cx.foreground().forbid_parking(); + use gpui2::{Context, Executor, TestAppContext}; + use parking_lot::Mutex; + use std::future; + use util::http::FakeHttpClient; -// let user_id = 5; -// let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); -// let server = FakeServer::for_client(user_id, &client, cx).await; -// let mut status = client.status(); -// assert!(matches!( -// status.next().await, -// Some(Status::Connected { .. }) -// )); -// assert_eq!(server.auth_count(), 1); + #[gpui2::test(iterations = 10)] + async fn test_reconnection(cx: &mut TestAppContext) { + let user_id = 5; + let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); + let server = FakeServer::for_client(user_id, &client, cx).await; + let mut status = client.status(); + assert!(matches!( + status.next().await, + Some(Status::Connected { .. }) + )); + assert_eq!(server.auth_count(), 1); -// server.forbid_connections(); -// server.disconnect(); -// while !matches!(status.next().await, Some(Status::ReconnectionError { .. })) {} + server.forbid_connections(); + server.disconnect(); + while !matches!(status.next().await, Some(Status::ReconnectionError { .. })) {} -// server.allow_connections(); -// cx.foreground().advance_clock(Duration::from_secs(10)); -// while !matches!(status.next().await, Some(Status::Connected { .. })) {} -// assert_eq!(server.auth_count(), 1); // Client reused the cached credentials when reconnecting + server.allow_connections(); + cx.executor().advance_clock(Duration::from_secs(10)); + while !matches!(status.next().await, Some(Status::Connected { .. })) {} + assert_eq!(server.auth_count(), 1); // Client reused the cached credentials when reconnecting -// server.forbid_connections(); -// server.disconnect(); -// while !matches!(status.next().await, Some(Status::ReconnectionError { .. })) {} + server.forbid_connections(); + server.disconnect(); + while !matches!(status.next().await, Some(Status::ReconnectionError { .. })) {} -// // Clear cached credentials after authentication fails -// server.roll_access_token(); -// server.allow_connections(); -// cx.foreground().advance_clock(Duration::from_secs(10)); -// while !matches!(status.next().await, Some(Status::Connected { .. })) {} -// assert_eq!(server.auth_count(), 2); // Client re-authenticated due to an invalid token -// } + // Clear cached credentials after authentication fails + server.roll_access_token(); + server.allow_connections(); + cx.executor().run_until_parked(); + cx.executor().advance_clock(Duration::from_secs(10)); + while !matches!(status.next().await, Some(Status::Connected { .. })) {} + assert_eq!(server.auth_count(), 2); // Client re-authenticated due to an invalid token + } -// #[gpui::test(iterations = 10)] -// async fn test_connection_timeout(deterministic: Arc, cx: &mut TestAppContext) { -// deterministic.forbid_parking(); + #[gpui2::test(iterations = 10)] + async fn test_connection_timeout(executor: Executor, cx: &mut TestAppContext) { + let user_id = 5; + let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); + let mut status = client.status(); -// let user_id = 5; -// let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); -// let mut status = client.status(); + // Time out when client tries to connect. + client.override_authenticate(move |cx| { + cx.executor().spawn(async move { + Ok(Credentials { + user_id, + access_token: "token".into(), + }) + }) + }); + client.override_establish_connection(|_, cx| { + cx.executor().spawn(async move { + future::pending::<()>().await; + unreachable!() + }) + }); + let auth_and_connect = cx.spawn({ + let client = client.clone(); + |cx| async move { client.authenticate_and_connect(false, &cx).await } + }); + executor.run_until_parked(); + assert!(matches!(status.next().await, Some(Status::Connecting))); -// // Time out when client tries to connect. -// client.override_authenticate(move |cx| { -// cx.foreground().spawn(async move { -// Ok(Credentials { -// user_id, -// access_token: "token".into(), -// }) -// }) -// }); -// client.override_establish_connection(|_, cx| { -// cx.foreground().spawn(async move { -// future::pending::<()>().await; -// unreachable!() -// }) -// }); -// let auth_and_connect = cx.spawn({ -// let client = client.clone(); -// |cx| async move { client.authenticate_and_connect(false, &cx).await } -// }); -// deterministic.run_until_parked(); -// assert!(matches!(status.next().await, Some(Status::Connecting))); + executor.advance_clock(CONNECTION_TIMEOUT); + assert!(matches!( + status.next().await, + Some(Status::ConnectionError { .. }) + )); + auth_and_connect.await.unwrap_err(); -// deterministic.advance_clock(CONNECTION_TIMEOUT); -// assert!(matches!( -// status.next().await, -// Some(Status::ConnectionError { .. }) -// )); -// auth_and_connect.await.unwrap_err(); + // Allow the connection to be established. + let server = FakeServer::for_client(user_id, &client, cx).await; + assert!(matches!( + status.next().await, + Some(Status::Connected { .. }) + )); -// // Allow the connection to be established. -// let server = FakeServer::for_client(user_id, &client, cx).await; -// assert!(matches!( -// status.next().await, -// Some(Status::Connected { .. }) -// )); + // Disconnect client. + server.forbid_connections(); + server.disconnect(); + while !matches!(status.next().await, Some(Status::ReconnectionError { .. })) {} -// // Disconnect client. -// server.forbid_connections(); -// server.disconnect(); -// while !matches!(status.next().await, Some(Status::ReconnectionError { .. })) {} + // Time out when re-establishing the connection. + server.allow_connections(); + client.override_establish_connection(|_, cx| { + cx.executor().spawn(async move { + future::pending::<()>().await; + unreachable!() + }) + }); + executor.advance_clock(2 * INITIAL_RECONNECTION_DELAY); + assert!(matches!( + status.next().await, + Some(Status::Reconnecting { .. }) + )); -// // Time out when re-establishing the connection. -// server.allow_connections(); -// client.override_establish_connection(|_, cx| { -// cx.foreground().spawn(async move { -// future::pending::<()>().await; -// unreachable!() -// }) -// }); -// deterministic.advance_clock(2 * INITIAL_RECONNECTION_DELAY); -// assert!(matches!( -// status.next().await, -// Some(Status::Reconnecting { .. }) -// )); + executor.advance_clock(CONNECTION_TIMEOUT); + assert!(matches!( + status.next().await, + Some(Status::ReconnectionError { .. }) + )); + } -// deterministic.advance_clock(CONNECTION_TIMEOUT); -// assert!(matches!( -// status.next().await, -// Some(Status::ReconnectionError { .. }) -// )); -// } + #[gpui2::test(iterations = 10)] + async fn test_authenticating_more_than_once(cx: &mut TestAppContext, executor: Executor) { + let auth_count = Arc::new(Mutex::new(0)); + let dropped_auth_count = Arc::new(Mutex::new(0)); + let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); + client.override_authenticate({ + let auth_count = auth_count.clone(); + let dropped_auth_count = dropped_auth_count.clone(); + move |cx| { + let auth_count = auth_count.clone(); + let dropped_auth_count = dropped_auth_count.clone(); + cx.executor().spawn(async move { + *auth_count.lock() += 1; + let _drop = util::defer(move || *dropped_auth_count.lock() += 1); + future::pending::<()>().await; + unreachable!() + }) + } + }); -// #[gpui::test(iterations = 10)] -// async fn test_authenticating_more_than_once( -// cx: &mut TestAppContext, -// deterministic: Arc, -// ) { -// cx.foreground().forbid_parking(); + let _authenticate = cx.spawn({ + let client = client.clone(); + move |cx| async move { client.authenticate_and_connect(false, &cx).await } + }); + executor.run_until_parked(); + assert_eq!(*auth_count.lock(), 1); + assert_eq!(*dropped_auth_count.lock(), 0); -// let auth_count = Arc::new(Mutex::new(0)); -// let dropped_auth_count = Arc::new(Mutex::new(0)); -// let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); -// client.override_authenticate({ -// let auth_count = auth_count.clone(); -// let dropped_auth_count = dropped_auth_count.clone(); -// move |cx| { -// let auth_count = auth_count.clone(); -// let dropped_auth_count = dropped_auth_count.clone(); -// cx.foreground().spawn(async move { -// *auth_count.lock() += 1; -// let _drop = util::defer(move || *dropped_auth_count.lock() += 1); -// future::pending::<()>().await; -// unreachable!() -// }) -// } -// }); + let _authenticate = cx.spawn({ + let client = client.clone(); + |cx| async move { client.authenticate_and_connect(false, &cx).await } + }); + executor.run_until_parked(); + assert_eq!(*auth_count.lock(), 2); + assert_eq!(*dropped_auth_count.lock(), 1); + } -// let _authenticate = cx.spawn(|cx| { -// let client = client.clone(); -// async move { client.authenticate_and_connect(false, &cx).await } -// }); -// deterministic.run_until_parked(); -// assert_eq!(*auth_count.lock(), 1); -// assert_eq!(*dropped_auth_count.lock(), 0); + #[test] + fn test_encode_and_decode_worktree_url() { + let url = encode_worktree_url(5, "deadbeef"); + assert_eq!(decode_worktree_url(&url), Some((5, "deadbeef".to_string()))); + assert_eq!( + decode_worktree_url(&format!("\n {}\t", url)), + Some((5, "deadbeef".to_string())) + ); + assert_eq!(decode_worktree_url("not://the-right-format"), None); + } -// let _authenticate = cx.spawn(|cx| { -// let client = client.clone(); -// async move { client.authenticate_and_connect(false, &cx).await } -// }); -// deterministic.run_until_parked(); -// assert_eq!(*auth_count.lock(), 2); -// assert_eq!(*dropped_auth_count.lock(), 1); -// } + #[gpui2::test] + async fn test_subscribing_to_entity(cx: &mut TestAppContext) { + let user_id = 5; + let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); + let server = FakeServer::for_client(user_id, &client, cx).await; -// #[test] -// fn test_encode_and_decode_worktree_url() { -// let url = encode_worktree_url(5, "deadbeef"); -// assert_eq!(decode_worktree_url(&url), Some((5, "deadbeef".to_string()))); -// assert_eq!( -// decode_worktree_url(&format!("\n {}\t", url)), -// Some((5, "deadbeef".to_string())) -// ); -// assert_eq!(decode_worktree_url("not://the-right-format"), None); -// } + let (done_tx1, mut done_rx1) = smol::channel::unbounded(); + let (done_tx2, mut done_rx2) = smol::channel::unbounded(); + client.add_model_message_handler( + move |model: Handle, _: TypedEnvelope, _, mut cx| { + match model.update(&mut cx, |model, _| model.id).unwrap() { + 1 => done_tx1.try_send(()).unwrap(), + 2 => done_tx2.try_send(()).unwrap(), + _ => unreachable!(), + } + async { Ok(()) } + }, + ); + let model1 = cx.entity(|_| Model { + id: 1, + subscription: None, + }); + let model2 = cx.entity(|_| Model { + id: 2, + subscription: None, + }); + let model3 = cx.entity(|_| Model { + id: 3, + subscription: None, + }); -// #[gpui::test] -// async fn test_subscribing_to_entity(cx: &mut TestAppContext) { -// cx.foreground().forbid_parking(); + let _subscription1 = client + .subscribe_to_entity(1) + .unwrap() + .set_model(&model1, &mut cx.to_async()); + let _subscription2 = client + .subscribe_to_entity(2) + .unwrap() + .set_model(&model2, &mut cx.to_async()); + // Ensure dropping a subscription for the same entity type still allows receiving of + // messages for other entity IDs of the same type. + let subscription3 = client + .subscribe_to_entity(3) + .unwrap() + .set_model(&model3, &mut cx.to_async()); + drop(subscription3); -// let user_id = 5; -// let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); -// let server = FakeServer::for_client(user_id, &client, cx).await; + server.send(proto::JoinProject { project_id: 1 }); + server.send(proto::JoinProject { project_id: 2 }); + done_rx1.next().await.unwrap(); + done_rx2.next().await.unwrap(); + } -// let (done_tx1, mut done_rx1) = smol::channel::unbounded(); -// let (done_tx2, mut done_rx2) = smol::channel::unbounded(); -// client.add_model_message_handler( -// move |model: ModelHandle, _: TypedEnvelope, _, cx| { -// match model.read_with(&cx, |model, _| model.id) { -// 1 => done_tx1.try_send(()).unwrap(), -// 2 => done_tx2.try_send(()).unwrap(), -// _ => unreachable!(), -// } -// async { Ok(()) } -// }, -// ); -// let model1 = cx.add_model(|_| Model { -// id: 1, -// subscription: None, -// }); -// let model2 = cx.add_model(|_| Model { -// id: 2, -// subscription: None, -// }); -// let model3 = cx.add_model(|_| Model { -// id: 3, -// subscription: None, -// }); + #[gpui2::test] + async fn test_subscribing_after_dropping_subscription(cx: &mut TestAppContext) { + let user_id = 5; + let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); + let server = FakeServer::for_client(user_id, &client, cx).await; -// let _subscription1 = client -// .subscribe_to_entity(1) -// .unwrap() -// .set_model(&model1, &mut cx.to_async()); -// let _subscription2 = client -// .subscribe_to_entity(2) -// .unwrap() -// .set_model(&model2, &mut cx.to_async()); -// // Ensure dropping a subscription for the same entity type still allows receiving of -// // messages for other entity IDs of the same type. -// let subscription3 = client -// .subscribe_to_entity(3) -// .unwrap() -// .set_model(&model3, &mut cx.to_async()); -// drop(subscription3); + let model = cx.entity(|_| Model::default()); + let (done_tx1, _done_rx1) = smol::channel::unbounded(); + let (done_tx2, mut done_rx2) = smol::channel::unbounded(); + let subscription1 = client.add_message_handler( + model.downgrade(), + move |_, _: TypedEnvelope, _, _| { + done_tx1.try_send(()).unwrap(); + async { Ok(()) } + }, + ); + drop(subscription1); + let _subscription2 = client.add_message_handler( + model.downgrade(), + move |_, _: TypedEnvelope, _, _| { + done_tx2.try_send(()).unwrap(); + async { Ok(()) } + }, + ); + server.send(proto::Ping {}); + done_rx2.next().await.unwrap(); + } -// server.send(proto::JoinProject { project_id: 1 }); -// server.send(proto::JoinProject { project_id: 2 }); -// done_rx1.next().await.unwrap(); -// done_rx2.next().await.unwrap(); -// } + #[gpui2::test] + async fn test_dropping_subscription_in_handler(cx: &mut TestAppContext) { + let user_id = 5; + let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); + let server = FakeServer::for_client(user_id, &client, cx).await; -// #[gpui::test] -// async fn test_subscribing_after_dropping_subscription(cx: &mut TestAppContext) { -// cx.foreground().forbid_parking(); + let model = cx.entity(|_| Model::default()); + let (done_tx, mut done_rx) = smol::channel::unbounded(); + let subscription = client.add_message_handler( + model.clone().downgrade(), + move |model: Handle, _: TypedEnvelope, _, mut cx| { + model + .update(&mut cx, |model, _| model.subscription.take()) + .unwrap(); + done_tx.try_send(()).unwrap(); + async { Ok(()) } + }, + ); + model.update(cx, |model, _| { + model.subscription = Some(subscription); + }); + server.send(proto::Ping {}); + done_rx.next().await.unwrap(); + } -// let user_id = 5; -// let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); -// let server = FakeServer::for_client(user_id, &client, cx).await; - -// let model = cx.add_model(|_| Model::default()); -// let (done_tx1, _done_rx1) = smol::channel::unbounded(); -// let (done_tx2, mut done_rx2) = smol::channel::unbounded(); -// let subscription1 = client.add_message_handler( -// model.clone(), -// move |_, _: TypedEnvelope, _, _| { -// done_tx1.try_send(()).unwrap(); -// async { Ok(()) } -// }, -// ); -// drop(subscription1); -// let _subscription2 = client.add_message_handler( -// model.clone(), -// move |_, _: TypedEnvelope, _, _| { -// done_tx2.try_send(()).unwrap(); -// async { Ok(()) } -// }, -// ); -// server.send(proto::Ping {}); -// done_rx2.next().await.unwrap(); -// } - -// #[gpui::test] -// async fn test_dropping_subscription_in_handler(cx: &mut TestAppContext) { -// cx.foreground().forbid_parking(); - -// let user_id = 5; -// let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); -// let server = FakeServer::for_client(user_id, &client, cx).await; - -// let model = cx.add_model(|_| Model::default()); -// let (done_tx, mut done_rx) = smol::channel::unbounded(); -// let subscription = client.add_message_handler( -// model.clone(), -// move |model, _: TypedEnvelope, _, mut cx| { -// model.update(&mut cx, |model, _| model.subscription.take()); -// done_tx.try_send(()).unwrap(); -// async { Ok(()) } -// }, -// ); -// model.update(cx, |model, _| { -// model.subscription = Some(subscription); -// }); -// server.send(proto::Ping {}); -// done_rx.next().await.unwrap(); -// } - -// #[derive(Default)] -// struct Model { -// id: usize, -// subscription: Option, -// } - -// impl Entity for Model { -// type Event = (); -// } -// } + #[derive(Default)] + struct Model { + id: usize, + subscription: Option, + } +} diff --git a/crates/client2/src/test.rs b/crates/client2/src/test.rs index 96c20791ec..1b32d35092 100644 --- a/crates/client2/src/test.rs +++ b/crates/client2/src/test.rs @@ -1,215 +1,216 @@ -// use crate::{Client, Connection, Credentials, EstablishConnectionError, UserStore}; -// use anyhow::{anyhow, Result}; -// use futures::{stream::BoxStream, StreamExt}; -// use gpui2::{Executor, Handle, TestAppContext}; -// use parking_lot::Mutex; -// use rpc::{ -// proto::{self, GetPrivateUserInfo, GetPrivateUserInfoResponse}, -// ConnectionId, Peer, Receipt, TypedEnvelope, -// }; -// use std::{rc::Rc, sync::Arc}; -// use util::http::FakeHttpClient; +use crate::{Client, Connection, Credentials, EstablishConnectionError, UserStore}; +use anyhow::{anyhow, Result}; +use futures::{stream::BoxStream, StreamExt}; +use gpui2::{Context, Executor, Handle, TestAppContext}; +use parking_lot::Mutex; +use rpc2::{ + proto::{self, GetPrivateUserInfo, GetPrivateUserInfoResponse}, + ConnectionId, Peer, Receipt, TypedEnvelope, +}; +use std::sync::Arc; +use util::http::FakeHttpClient; -// pub struct FakeServer { -// peer: Arc, -// state: Arc>, -// user_id: u64, -// executor: Executor, -// } +pub struct FakeServer { + peer: Arc, + state: Arc>, + user_id: u64, + executor: Executor, +} -// #[derive(Default)] -// struct FakeServerState { -// incoming: Option>>, -// connection_id: Option, -// forbid_connections: bool, -// auth_count: usize, -// access_token: usize, -// } +#[derive(Default)] +struct FakeServerState { + incoming: Option>>, + connection_id: Option, + forbid_connections: bool, + auth_count: usize, + access_token: usize, +} -// impl FakeServer { -// pub async fn for_client( -// client_user_id: u64, -// client: &Arc, -// cx: &TestAppContext, -// ) -> Self { -// let server = Self { -// peer: Peer::new(0), -// state: Default::default(), -// user_id: client_user_id, -// executor: cx.foreground(), -// }; +impl FakeServer { + pub async fn for_client( + client_user_id: u64, + client: &Arc, + cx: &TestAppContext, + ) -> Self { + let server = Self { + peer: Peer::new(0), + state: Default::default(), + user_id: client_user_id, + executor: cx.executor().clone(), + }; -// client -// .override_authenticate({ -// let state = Arc::downgrade(&server.state); -// move |cx| { -// let state = state.clone(); -// cx.spawn(move |_| async move { -// let state = state.upgrade().ok_or_else(|| anyhow!("server dropped"))?; -// let mut state = state.lock(); -// state.auth_count += 1; -// let access_token = state.access_token.to_string(); -// Ok(Credentials { -// user_id: client_user_id, -// access_token, -// }) -// }) -// } -// }) -// .override_establish_connection({ -// let peer = Arc::downgrade(&server.peer); -// let state = Arc::downgrade(&server.state); -// move |credentials, cx| { -// let peer = peer.clone(); -// let state = state.clone(); -// let credentials = credentials.clone(); -// cx.spawn(move |cx| async move { -// let state = state.upgrade().ok_or_else(|| anyhow!("server dropped"))?; -// let peer = peer.upgrade().ok_or_else(|| anyhow!("server dropped"))?; -// if state.lock().forbid_connections { -// Err(EstablishConnectionError::Other(anyhow!( -// "server is forbidding connections" -// )))? -// } + client + .override_authenticate({ + let state = Arc::downgrade(&server.state); + move |cx| { + let state = state.clone(); + cx.spawn(move |_| async move { + let state = state.upgrade().ok_or_else(|| anyhow!("server dropped"))?; + let mut state = state.lock(); + state.auth_count += 1; + let access_token = state.access_token.to_string(); + Ok(Credentials { + user_id: client_user_id, + access_token, + }) + }) + } + }) + .override_establish_connection({ + let peer = Arc::downgrade(&server.peer); + let state = Arc::downgrade(&server.state); + move |credentials, cx| { + let peer = peer.clone(); + let state = state.clone(); + let credentials = credentials.clone(); + cx.spawn(move |cx| async move { + let state = state.upgrade().ok_or_else(|| anyhow!("server dropped"))?; + let peer = peer.upgrade().ok_or_else(|| anyhow!("server dropped"))?; + if state.lock().forbid_connections { + Err(EstablishConnectionError::Other(anyhow!( + "server is forbidding connections" + )))? + } -// assert_eq!(credentials.user_id, client_user_id); + assert_eq!(credentials.user_id, client_user_id); -// if credentials.access_token != state.lock().access_token.to_string() { -// Err(EstablishConnectionError::Unauthorized)? -// } + if credentials.access_token != state.lock().access_token.to_string() { + Err(EstablishConnectionError::Unauthorized)? + } -// let (client_conn, server_conn, _) = Connection::in_memory(cx.background()); -// let (connection_id, io, incoming) = -// peer.add_test_connection(server_conn, cx.background()); -// cx.background().spawn(io).detach(); -// { -// let mut state = state.lock(); -// state.connection_id = Some(connection_id); -// state.incoming = Some(incoming); -// } -// peer.send( -// connection_id, -// proto::Hello { -// peer_id: Some(connection_id.into()), -// }, -// ) -// .unwrap(); + let (client_conn, server_conn, _) = + Connection::in_memory(cx.executor().clone()); + let (connection_id, io, incoming) = + peer.add_test_connection(server_conn, cx.executor().clone()); + cx.executor().spawn(io).detach(); + { + let mut state = state.lock(); + state.connection_id = Some(connection_id); + state.incoming = Some(incoming); + } + peer.send( + connection_id, + proto::Hello { + peer_id: Some(connection_id.into()), + }, + ) + .unwrap(); -// Ok(client_conn) -// }) -// } -// }); + Ok(client_conn) + }) + } + }); -// client -// .authenticate_and_connect(false, &cx.to_async()) -// .await -// .unwrap(); + client + .authenticate_and_connect(false, &cx.to_async()) + .await + .unwrap(); -// server -// } + server + } -// pub fn disconnect(&self) { -// if self.state.lock().connection_id.is_some() { -// self.peer.disconnect(self.connection_id()); -// let mut state = self.state.lock(); -// state.connection_id.take(); -// state.incoming.take(); -// } -// } + pub fn disconnect(&self) { + if self.state.lock().connection_id.is_some() { + self.peer.disconnect(self.connection_id()); + let mut state = self.state.lock(); + state.connection_id.take(); + state.incoming.take(); + } + } -// pub fn auth_count(&self) -> usize { -// self.state.lock().auth_count -// } + pub fn auth_count(&self) -> usize { + self.state.lock().auth_count + } -// pub fn roll_access_token(&self) { -// self.state.lock().access_token += 1; -// } + pub fn roll_access_token(&self) { + self.state.lock().access_token += 1; + } -// pub fn forbid_connections(&self) { -// self.state.lock().forbid_connections = true; -// } + pub fn forbid_connections(&self) { + self.state.lock().forbid_connections = true; + } -// pub fn allow_connections(&self) { -// self.state.lock().forbid_connections = false; -// } + pub fn allow_connections(&self) { + self.state.lock().forbid_connections = false; + } -// pub fn send(&self, message: T) { -// self.peer.send(self.connection_id(), message).unwrap(); -// } + pub fn send(&self, message: T) { + self.peer.send(self.connection_id(), message).unwrap(); + } -// #[allow(clippy::await_holding_lock)] -// pub async fn receive(&self) -> Result> { -// self.executor.start_waiting(); + #[allow(clippy::await_holding_lock)] + pub async fn receive(&self) -> Result> { + self.executor.start_waiting(); -// loop { -// let message = self -// .state -// .lock() -// .incoming -// .as_mut() -// .expect("not connected") -// .next() -// .await -// .ok_or_else(|| anyhow!("other half hung up"))?; -// self.executor.finish_waiting(); -// let type_name = message.payload_type_name(); -// let message = message.into_any(); + loop { + let message = self + .state + .lock() + .incoming + .as_mut() + .expect("not connected") + .next() + .await + .ok_or_else(|| anyhow!("other half hung up"))?; + self.executor.finish_waiting(); + let type_name = message.payload_type_name(); + let message = message.into_any(); -// if message.is::>() { -// return Ok(*message.downcast().unwrap()); -// } + if message.is::>() { + return Ok(*message.downcast().unwrap()); + } -// if message.is::>() { -// self.respond( -// message -// .downcast::>() -// .unwrap() -// .receipt(), -// GetPrivateUserInfoResponse { -// metrics_id: "the-metrics-id".into(), -// staff: false, -// flags: Default::default(), -// }, -// ); -// continue; -// } + if message.is::>() { + self.respond( + message + .downcast::>() + .unwrap() + .receipt(), + GetPrivateUserInfoResponse { + metrics_id: "the-metrics-id".into(), + staff: false, + flags: Default::default(), + }, + ); + continue; + } -// panic!( -// "fake server received unexpected message type: {:?}", -// type_name -// ); -// } -// } + panic!( + "fake server received unexpected message type: {:?}", + type_name + ); + } + } -// pub fn respond(&self, receipt: Receipt, response: T::Response) { -// self.peer.respond(receipt, response).unwrap() -// } + pub fn respond(&self, receipt: Receipt, response: T::Response) { + self.peer.respond(receipt, response).unwrap() + } -// fn connection_id(&self) -> ConnectionId { -// self.state.lock().connection_id.expect("not connected") -// } + fn connection_id(&self) -> ConnectionId { + self.state.lock().connection_id.expect("not connected") + } -// pub async fn build_user_store( -// &self, -// client: Arc, -// cx: &mut TestAppContext, -// ) -> ModelHandle { -// let http_client = FakeHttpClient::with_404_response(); -// let user_store = cx.add_model(|cx| UserStore::new(client, http_client, cx)); -// assert_eq!( -// self.receive::() -// .await -// .unwrap() -// .payload -// .user_ids, -// &[self.user_id] -// ); -// user_store -// } -// } + pub async fn build_user_store( + &self, + client: Arc, + cx: &mut TestAppContext, + ) -> Handle { + let http_client = FakeHttpClient::with_404_response(); + let user_store = cx.entity(|cx| UserStore::new(client, http_client, cx)); + assert_eq!( + self.receive::() + .await + .unwrap() + .payload + .user_ids, + &[self.user_id] + ); + user_store + } +} -// impl Drop for FakeServer { -// fn drop(&mut self) { -// self.disconnect(); -// } -// } +impl Drop for FakeServer { + fn drop(&mut self) { + self.disconnect(); + } +} diff --git a/crates/client2/src/user.rs b/crates/client2/src/user.rs index 2aaae6dc85..41cf46ea8f 100644 --- a/crates/client2/src/user.rs +++ b/crates/client2/src/user.rs @@ -5,7 +5,7 @@ use feature_flags2::FeatureFlagAppExt; use futures::{channel::mpsc, future, AsyncReadExt, Future, StreamExt}; use gpui2::{AsyncAppContext, EventEmitter, Handle, ImageData, ModelContext, Task}; use postage::{sink::Sink, watch}; -use rpc::proto::{RequestMessage, UsersResponse}; +use rpc2::proto::{RequestMessage, UsersResponse}; use std::sync::{Arc, Weak}; use text::ReplicaId; use util::http::HttpClient; diff --git a/crates/copilot2/Cargo.toml b/crates/copilot2/Cargo.toml index 161a9f3bd8..f83824d808 100644 --- a/crates/copilot2/Cargo.toml +++ b/crates/copilot2/Cargo.toml @@ -36,6 +36,7 @@ serde.workspace = true serde_derive.workspace = true smol.workspace = true futures.workspace = true +parking_lot.workspace = true [dev-dependencies] clock = { path = "../clock" } diff --git a/crates/copilot2/src/copilot2.rs b/crates/copilot2/src/copilot2.rs index 834750b25d..149b01aa82 100644 --- a/crates/copilot2/src/copilot2.rs +++ b/crates/copilot2/src/copilot2.rs @@ -17,6 +17,7 @@ use language2::{ }; use lsp2::{LanguageServer, LanguageServerBinary, LanguageServerId}; use node_runtime::NodeRuntime; +use parking_lot::Mutex; use request::StatusNotification; use settings2::SettingsStore; use smol::{fs, io::BufReader, stream::StreamExt}; @@ -394,8 +395,15 @@ impl Copilot { path: node_path, arguments, }; - let server = - LanguageServer::new(new_server_id, binary, Path::new("/"), None, cx.clone())?; + + let server = LanguageServer::new( + Arc::new(Mutex::new(None)), + new_server_id, + binary, + Path::new("/"), + None, + cx.clone(), + )?; server .on_notification::( diff --git a/crates/copilot2/src/sign_in.rs b/crates/copilot2/src/sign_in.rs index ab326a8819..57f248aa52 100644 --- a/crates/copilot2/src/sign_in.rs +++ b/crates/copilot2/src/sign_in.rs @@ -106,7 +106,7 @@ // data: &PromptUserDeviceFlow, // style: &theme::Copilot, // cx: &mut ViewContext, -// ) -> impl Element { +// ) -> impl IntoAnyElement { // let copied = cx // .read_from_clipboard() // .map(|item| item.text() == &data.user_code) diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index 8b287d9b88..abb047c5ef 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/crates/gpui2/src/app/test_context.rs @@ -143,7 +143,7 @@ impl TestAppContext { lock.update_global(update) } - fn to_async(&self) -> AsyncAppContext { + pub fn to_async(&self) -> AsyncAppContext { AsyncAppContext { app: Arc::downgrade(&self.app), executor: self.executor.clone(), diff --git a/crates/gpui2/src/color.rs b/crates/gpui2/src/color.rs index f8ae1567e3..db07259476 100644 --- a/crates/gpui2/src/color.rs +++ b/crates/gpui2/src/color.rs @@ -60,7 +60,6 @@ impl From for u32 { } } - struct RgbaVisitor; impl<'de> Visitor<'de> for RgbaVisitor { @@ -157,10 +156,8 @@ impl Hsla { } } - impl Eq for Hsla {} - pub fn hsla(h: f32, s: f32, l: f32, a: f32) -> Hsla { Hsla { h: h.clamp(0., 1.), diff --git a/crates/gpui2/src/element.rs b/crates/gpui2/src/element.rs index a6efbe52eb..15d29a7c42 100644 --- a/crates/gpui2/src/element.rs +++ b/crates/gpui2/src/element.rs @@ -3,36 +3,37 @@ use derive_more::{Deref, DerefMut}; pub(crate) use smallvec::SmallVec; use std::{any::Any, mem}; -pub trait Element: IntoAnyElement { - type ViewState: 'static; +pub trait Element { type ElementState: 'static; fn id(&self) -> Option; + /// Called to initialize this element for the current frame. If this + /// element had state in a previous frame, it will be passed in for the 3rd argument. fn initialize( &mut self, - view_state: &mut Self::ViewState, + view_state: &mut V, element_state: Option, - cx: &mut ViewContext, + cx: &mut ViewContext, ) -> Self::ElementState; // where - // Self::ViewState: Any + Send + Sync; + // V: Any + Send + Sync; fn layout( &mut self, - view_state: &mut Self::ViewState, + view_state: &mut V, element_state: &mut Self::ElementState, - cx: &mut ViewContext, + cx: &mut ViewContext, ) -> LayoutId; // where - // Self::ViewState: Any + Send + Sync; + // V: Any + Send + Sync; fn paint( &mut self, bounds: Bounds, - view_state: &mut Self::ViewState, + view_state: &mut V, element_state: &mut Self::ElementState, - cx: &mut ViewContext, + cx: &mut ViewContext, ); // where @@ -42,26 +43,23 @@ pub trait Element: IntoAnyElement { #[derive(Deref, DerefMut, Default, Clone, Debug, Eq, PartialEq, Hash)] pub struct GlobalElementId(SmallVec<[ElementId; 32]>); -pub trait ParentElement: Element { - fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]>; +pub trait ParentElement { + fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]>; - fn child(mut self, child: impl IntoAnyElement) -> Self + fn child(mut self, child: impl Component) -> Self where Self: Sized, { - self.children_mut().push(child.into_any()); + self.children_mut().push(child.render()); self } - fn children( - mut self, - iter: impl IntoIterator>, - ) -> Self + fn children(mut self, iter: impl IntoIterator>) -> Self where Self: Sized, { self.children_mut() - .extend(iter.into_iter().map(|item| item.into_any())); + .extend(iter.into_iter().map(|item| item.render())); self } } @@ -72,7 +70,7 @@ trait ElementObject { fn paint(&mut self, view_state: &mut V, cx: &mut ViewContext); } -struct RenderedElement { +struct RenderedElement> { element: E, phase: ElementRenderPhase, } @@ -94,7 +92,7 @@ enum ElementRenderPhase { /// Internal struct that wraps an element to store Layout and ElementState after the element is rendered. /// It's allocated as a trait object to erase the element type and wrapped in AnyElement for /// improved usability. -impl RenderedElement { +impl> RenderedElement { fn new(element: E) -> Self { RenderedElement { element, @@ -103,13 +101,13 @@ impl RenderedElement { } } -impl ElementObject for RenderedElement +impl ElementObject for RenderedElement where - E: Element, + E: Element, // E::ViewState: Any + Send + Sync, E::ElementState: Any + Send + Sync, { - fn initialize(&mut self, view_state: &mut E::ViewState, cx: &mut ViewContext) { + fn initialize(&mut self, view_state: &mut V, cx: &mut ViewContext) { let frame_state = if let Some(id) = self.element.id() { cx.with_element_state(id, |element_state, cx| { let element_state = self.element.initialize(view_state, element_state, cx); @@ -124,7 +122,7 @@ where self.phase = ElementRenderPhase::Initialized { frame_state }; } - fn layout(&mut self, state: &mut E::ViewState, cx: &mut ViewContext) -> LayoutId { + fn layout(&mut self, state: &mut V, cx: &mut ViewContext) -> LayoutId { let layout_id; let mut frame_state; match mem::take(&mut self.phase) { @@ -154,7 +152,7 @@ where layout_id } - fn paint(&mut self, view_state: &mut E::ViewState, cx: &mut ViewContext) { + fn paint(&mut self, view_state: &mut V, cx: &mut ViewContext) { self.phase = match mem::take(&mut self.phase) { ElementRenderPhase::LayoutRequested { layout_id, @@ -182,11 +180,15 @@ where pub struct AnyElement(Box + Send + Sync>); +unsafe impl Send for AnyElement {} +unsafe impl Sync for AnyElement {} + impl AnyElement { pub fn new(element: E) -> Self where + V: 'static, E: 'static + Send + Sync, - E: Element, + E: Element, E::ElementState: Any + Send + Sync, { AnyElement(Box::new(RenderedElement::new(element))) @@ -205,12 +207,88 @@ impl AnyElement { } } -pub trait IntoAnyElement { - fn into_any(self) -> AnyElement; -} +pub trait Component { + fn render(self) -> AnyElement; -impl IntoAnyElement for AnyElement { - fn into_any(self) -> AnyElement { + fn when(mut self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self + where + Self: Sized, + { + if condition { + self = then(self); + } self } } + +impl Component for AnyElement { + fn render(self) -> AnyElement { + self + } +} + +impl Element for Option +where + V: 'static, + E: 'static + Component + Send + Sync, + F: FnOnce(&mut V, &mut ViewContext<'_, '_, V>) -> E + Send + Sync + 'static, +{ + type ElementState = AnyElement; + + fn id(&self) -> Option { + None + } + + fn initialize( + &mut self, + view_state: &mut V, + _rendered_element: Option, + cx: &mut ViewContext, + ) -> Self::ElementState { + let render = self.take().unwrap(); + let mut rendered_element = (render)(view_state, cx).render(); + rendered_element.initialize(view_state, cx); + rendered_element + } + + fn layout( + &mut self, + view_state: &mut V, + rendered_element: &mut Self::ElementState, + cx: &mut ViewContext, + ) -> LayoutId { + rendered_element.layout(view_state, cx) + } + + fn paint( + &mut self, + _bounds: Bounds, + view_state: &mut V, + rendered_element: &mut Self::ElementState, + cx: &mut ViewContext, + ) { + rendered_element.paint(view_state, cx) + } +} + +impl Component for Option +where + V: 'static, + E: 'static + Component + Send + Sync, + F: FnOnce(&mut V, &mut ViewContext<'_, '_, V>) -> E + Send + Sync + 'static, +{ + fn render(self) -> AnyElement { + AnyElement::new(self) + } +} + +impl Component for F +where + V: 'static, + E: 'static + Component + Send + Sync, + F: FnOnce(&mut V, &mut ViewContext<'_, '_, V>) -> E + Send + Sync + 'static, +{ + fn render(self) -> AnyElement { + AnyElement::new(Some(self)) + } +} diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index 71196eadca..6fe10d94a3 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -1,7 +1,7 @@ use crate::{ - point, AnyElement, BorrowWindow, Bounds, Element, ElementFocus, ElementId, ElementInteraction, - FocusDisabled, FocusEnabled, FocusHandle, FocusListeners, Focusable, GlobalElementId, - GroupBounds, InteractiveElementState, IntoAnyElement, LayoutId, Overflow, ParentElement, + point, AnyElement, BorrowWindow, Bounds, Component, Element, ElementFocus, ElementId, + ElementInteraction, FocusDisabled, FocusEnabled, FocusHandle, FocusListeners, Focusable, + GlobalElementId, GroupBounds, InteractiveElementState, LayoutId, Overflow, ParentElement, Pixels, Point, SharedString, StatefulInteraction, StatefulInteractive, StatelessInteraction, StatelessInteractive, Style, StyleRefinement, Styled, ViewContext, }; @@ -160,7 +160,7 @@ impl Div, FocusDisabled> { } } -impl Focusable for Div> +impl Focusable for Div> where V: 'static, I: ElementInteraction, @@ -189,12 +189,11 @@ pub struct DivState { child_layout_ids: SmallVec<[LayoutId; 4]>, } -impl Element for Div +impl Element for Div where I: ElementInteraction, F: ElementFocus, { - type ViewState = V; type ElementState = DivState; fn id(&self) -> Option { @@ -205,9 +204,9 @@ where fn initialize( &mut self, - view_state: &mut Self::ViewState, + view_state: &mut V, element_state: Option, - cx: &mut ViewContext, + cx: &mut ViewContext, ) -> Self::ElementState { let mut element_state = element_state.unwrap_or_default(); self.focus @@ -224,9 +223,9 @@ where fn layout( &mut self, - view_state: &mut Self::ViewState, + view_state: &mut V, element_state: &mut Self::ElementState, - cx: &mut ViewContext, + cx: &mut ViewContext, ) -> LayoutId { let style = self.compute_style(Bounds::default(), element_state, cx); style.apply_text_style(cx, |cx| { @@ -245,9 +244,9 @@ where fn paint( &mut self, bounds: Bounds, - view_state: &mut Self::ViewState, + view_state: &mut V, element_state: &mut Self::ElementState, - cx: &mut ViewContext, + cx: &mut ViewContext, ) { self.with_element_id(cx, |this, _global_id, cx| { if let Some(group) = this.group.clone() { @@ -304,23 +303,23 @@ where } } -impl IntoAnyElement for Div +impl Component for Div where // V: Any + Send + Sync, I: ElementInteraction, F: ElementFocus, { - fn into_any(self) -> AnyElement { + fn render(self) -> AnyElement { AnyElement::new(self) } } -impl ParentElement for Div +impl ParentElement for Div where I: ElementInteraction, F: ElementFocus, { - fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { + fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { &mut self.children } } @@ -335,7 +334,7 @@ where } } -impl StatelessInteractive for Div +impl StatelessInteractive for Div where I: ElementInteraction, F: ElementFocus, @@ -345,11 +344,11 @@ where } } -impl StatefulInteractive for Div, F> +impl StatefulInteractive for Div, F> where F: ElementFocus, { - fn stateful_interaction(&mut self) -> &mut StatefulInteraction { + fn stateful_interaction(&mut self) -> &mut StatefulInteraction { &mut self.interaction } } diff --git a/crates/gpui2/src/elements/img.rs b/crates/gpui2/src/elements/img.rs index adce6aea6b..747e573ea5 100644 --- a/crates/gpui2/src/elements/img.rs +++ b/crates/gpui2/src/elements/img.rs @@ -1,6 +1,6 @@ use crate::{ - div, AnyElement, BorrowWindow, Bounds, Div, DivState, Element, ElementFocus, ElementId, - ElementInteraction, FocusDisabled, FocusEnabled, FocusListeners, Focusable, IntoAnyElement, + div, AnyElement, BorrowWindow, Bounds, Component, Div, DivState, Element, ElementFocus, + ElementId, ElementInteraction, FocusDisabled, FocusEnabled, FocusListeners, Focusable, LayoutId, Pixels, SharedString, StatefulInteraction, StatefulInteractive, StatelessInteraction, StatelessInteractive, StyleRefinement, Styled, ViewContext, }; @@ -55,22 +55,21 @@ where } } -impl IntoAnyElement for Img +impl Component for Img where I: ElementInteraction, F: ElementFocus, { - fn into_any(self) -> AnyElement { + fn render(self) -> AnyElement { AnyElement::new(self) } } -impl Element for Img +impl Element for Img where I: ElementInteraction, F: ElementFocus, { - type ViewState = V; type ElementState = DivState; fn id(&self) -> Option { @@ -90,7 +89,7 @@ where &mut self, view_state: &mut V, element_state: &mut Self::ElementState, - cx: &mut ViewContext, + cx: &mut ViewContext, ) -> LayoutId { self.base.layout(view_state, element_state, cx) } @@ -143,7 +142,7 @@ where } } -impl StatelessInteractive for Img +impl StatelessInteractive for Img where I: ElementInteraction, F: ElementFocus, @@ -153,21 +152,21 @@ where } } -impl StatefulInteractive for Img, F> +impl StatefulInteractive for Img, F> where F: ElementFocus, { - fn stateful_interaction(&mut self) -> &mut StatefulInteraction { + fn stateful_interaction(&mut self) -> &mut StatefulInteraction { self.base.stateful_interaction() } } -impl Focusable for Img> +impl Focusable for Img> where V: 'static, I: ElementInteraction, { - fn focus_listeners(&mut self) -> &mut FocusListeners { + fn focus_listeners(&mut self) -> &mut FocusListeners { self.base.focus_listeners() } diff --git a/crates/gpui2/src/elements/svg.rs b/crates/gpui2/src/elements/svg.rs index 7e9017264f..7db4c5cf6d 100644 --- a/crates/gpui2/src/elements/svg.rs +++ b/crates/gpui2/src/elements/svg.rs @@ -1,6 +1,6 @@ use crate::{ - div, AnyElement, Bounds, Div, DivState, Element, ElementFocus, ElementId, ElementInteraction, - FocusDisabled, FocusEnabled, FocusListeners, Focusable, IntoAnyElement, LayoutId, Pixels, + div, AnyElement, Bounds, Component, Div, DivState, Element, ElementFocus, ElementId, + ElementInteraction, FocusDisabled, FocusEnabled, FocusListeners, Focusable, LayoutId, Pixels, SharedString, StatefulInteraction, StatefulInteractive, StatelessInteraction, StatelessInteractive, StyleRefinement, Styled, ViewContext, }; @@ -45,22 +45,21 @@ where } } -impl IntoAnyElement for Svg +impl Component for Svg where I: ElementInteraction, F: ElementFocus, { - fn into_any(self) -> AnyElement { + fn render(self) -> AnyElement { AnyElement::new(self) } } -impl Element for Svg +impl Element for Svg where I: ElementInteraction, F: ElementFocus, { - type ViewState = V; type ElementState = DivState; fn id(&self) -> Option { @@ -80,7 +79,7 @@ where &mut self, view_state: &mut V, element_state: &mut Self::ElementState, - cx: &mut ViewContext, + cx: &mut ViewContext, ) -> LayoutId { self.base.layout(view_state, element_state, cx) } @@ -88,7 +87,7 @@ where fn paint( &mut self, bounds: Bounds, - view: &mut Self::ViewState, + view: &mut V, element_state: &mut Self::ElementState, cx: &mut ViewContext, ) where @@ -116,7 +115,7 @@ where } } -impl StatelessInteractive for Svg +impl StatelessInteractive for Svg where I: ElementInteraction, F: ElementFocus, @@ -126,21 +125,21 @@ where } } -impl StatefulInteractive for Svg, F> +impl StatefulInteractive for Svg, F> where V: 'static, F: ElementFocus, { - fn stateful_interaction(&mut self) -> &mut StatefulInteraction { + fn stateful_interaction(&mut self) -> &mut StatefulInteraction { self.base.stateful_interaction() } } -impl Focusable for Svg> +impl Focusable for Svg> where I: ElementInteraction, { - fn focus_listeners(&mut self) -> &mut FocusListeners { + fn focus_listeners(&mut self) -> &mut FocusListeners { self.base.focus_listeners() } diff --git a/crates/gpui2/src/elements/text.rs b/crates/gpui2/src/elements/text.rs index 7b0052826c..3aff568c4c 100644 --- a/crates/gpui2/src/elements/text.rs +++ b/crates/gpui2/src/elements/text.rs @@ -1,41 +1,41 @@ use crate::{ - AnyElement, BorrowWindow, Bounds, Element, IntoAnyElement, LayoutId, Line, Pixels, - SharedString, Size, ViewContext, + AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId, Line, Pixels, SharedString, + Size, ViewContext, }; use parking_lot::Mutex; use smallvec::SmallVec; use std::{marker::PhantomData, sync::Arc}; use util::ResultExt; -impl IntoAnyElement for SharedString { - fn into_any(self) -> AnyElement { +impl Component for SharedString { + fn render(self) -> AnyElement { Text { text: self, state_type: PhantomData, } - .into_any() + .render() } } -impl IntoAnyElement for &'static str { - fn into_any(self) -> AnyElement { +impl Component for &'static str { + fn render(self) -> AnyElement { Text { text: self.into(), state_type: PhantomData, } - .into_any() + .render() } } // TODO: Figure out how to pass `String` to `child` without this. // This impl doesn't exist in the `gpui2` crate. -impl IntoAnyElement for String { - fn into_any(self) -> AnyElement { +impl Component for String { + fn render(self) -> AnyElement { Text { text: self.into(), state_type: PhantomData, } - .into_any() + .render() } } @@ -47,14 +47,13 @@ pub struct Text { unsafe impl Send for Text {} unsafe impl Sync for Text {} -impl IntoAnyElement for Text { - fn into_any(self) -> AnyElement { +impl Component for Text { + fn render(self) -> AnyElement { AnyElement::new(self) } } -impl Element for Text { - type ViewState = V; +impl Element for Text { type ElementState = Arc>>; fn id(&self) -> Option { diff --git a/crates/gpui2/src/executor.rs b/crates/gpui2/src/executor.rs index 261912b085..28d4b6d117 100644 --- a/crates/gpui2/src/executor.rs +++ b/crates/gpui2/src/executor.rs @@ -146,7 +146,10 @@ impl Executor { Poll::Ready(result) => return result, Poll::Pending => { if !self.dispatcher.poll() { - // todo!("forbid_parking") + #[cfg(any(test, feature = "test-support"))] + if let Some(_) = self.dispatcher.as_test() { + panic!("blocked with nothing left to run") + } parker.park(); } } @@ -206,11 +209,26 @@ impl Executor { todo!("start_waiting") } + #[cfg(any(test, feature = "test-support"))] + pub fn finish_waiting(&self) { + todo!("finish_waiting") + } + #[cfg(any(test, feature = "test-support"))] pub fn simulate_random_delay(&self) -> impl Future { self.dispatcher.as_test().unwrap().simulate_random_delay() } + #[cfg(any(test, feature = "test-support"))] + pub fn advance_clock(&self, duration: Duration) { + self.dispatcher.as_test().unwrap().advance_clock(duration) + } + + #[cfg(any(test, feature = "test-support"))] + pub fn run_until_parked(&self) { + self.dispatcher.as_test().unwrap().run_until_parked() + } + pub fn num_cpus(&self) -> usize { num_cpus::get() } diff --git a/crates/gpui2/src/focusable.rs b/crates/gpui2/src/focusable.rs index 3345b4c6c6..c283998ca2 100644 --- a/crates/gpui2/src/focusable.rs +++ b/crates/gpui2/src/focusable.rs @@ -11,8 +11,8 @@ pub type FocusListeners = SmallVec<[FocusListener; 2]>; pub type FocusListener = Arc) + Send + Sync + 'static>; -pub trait Focusable: Element { - fn focus_listeners(&mut self) -> &mut FocusListeners; +pub trait Focusable: Element { + fn focus_listeners(&mut self) -> &mut FocusListeners; fn set_focus_style(&mut self, style: StyleRefinement); fn set_focus_in_style(&mut self, style: StyleRefinement); fn set_in_focus_style(&mut self, style: StyleRefinement); @@ -43,10 +43,7 @@ pub trait Focusable: Element { fn on_focus( mut self, - listener: impl Fn(&mut Self::ViewState, &FocusEvent, &mut ViewContext) - + Send - + Sync - + 'static, + listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext) + Send + Sync + 'static, ) -> Self where Self: Sized, @@ -62,10 +59,7 @@ pub trait Focusable: Element { fn on_blur( mut self, - listener: impl Fn(&mut Self::ViewState, &FocusEvent, &mut ViewContext) - + Send - + Sync - + 'static, + listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext) + Send + Sync + 'static, ) -> Self where Self: Sized, @@ -81,10 +75,7 @@ pub trait Focusable: Element { fn on_focus_in( mut self, - listener: impl Fn(&mut Self::ViewState, &FocusEvent, &mut ViewContext) - + Send - + Sync - + 'static, + listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext) + Send + Sync + 'static, ) -> Self where Self: Sized, @@ -109,10 +100,7 @@ pub trait Focusable: Element { fn on_focus_out( mut self, - listener: impl Fn(&mut Self::ViewState, &FocusEvent, &mut ViewContext) - + Send - + Sync - + 'static, + listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext) + Send + Sync + 'static, ) -> Self where Self: Sized, diff --git a/crates/gpui2/src/interactive.rs b/crates/gpui2/src/interactive.rs index fd603eda46..9ec6c38dfe 100644 --- a/crates/gpui2/src/interactive.rs +++ b/crates/gpui2/src/interactive.rs @@ -1,7 +1,7 @@ use crate::{ - point, px, view, Action, AnyBox, AnyDrag, AppContext, BorrowWindow, Bounds, DispatchContext, - DispatchPhase, Element, ElementId, FocusHandle, KeyMatch, Keystroke, Modifiers, Overflow, - Pixels, Point, SharedString, Size, Style, StyleRefinement, ViewContext, + point, px, view, Action, AnyBox, AnyDrag, AppContext, BorrowWindow, Bounds, Component, + DispatchContext, DispatchPhase, Element, ElementId, FocusHandle, KeyMatch, Keystroke, + Modifiers, Overflow, Pixels, Point, SharedString, Size, Style, StyleRefinement, ViewContext, }; use collections::HashMap; use derive_more::{Deref, DerefMut}; @@ -19,8 +19,8 @@ use std::{ const DRAG_THRESHOLD: f64 = 2.; -pub trait StatelessInteractive: Element { - fn stateless_interaction(&mut self) -> &mut StatelessInteraction; +pub trait StatelessInteractive: Element { + fn stateless_interaction(&mut self) -> &mut StatelessInteraction; fn hover(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self where @@ -48,10 +48,7 @@ pub trait StatelessInteractive: Element { fn on_mouse_down( mut self, button: MouseButton, - handler: impl Fn(&mut Self::ViewState, &MouseDownEvent, &mut ViewContext) - + Send - + Sync - + 'static, + handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext) + Send + Sync + 'static, ) -> Self where Self: Sized, @@ -72,10 +69,7 @@ pub trait StatelessInteractive: Element { fn on_mouse_up( mut self, button: MouseButton, - handler: impl Fn(&mut Self::ViewState, &MouseUpEvent, &mut ViewContext) - + Send - + Sync - + 'static, + handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext) + Send + Sync + 'static, ) -> Self where Self: Sized, @@ -96,10 +90,7 @@ pub trait StatelessInteractive: Element { fn on_mouse_down_out( mut self, button: MouseButton, - handler: impl Fn(&mut Self::ViewState, &MouseDownEvent, &mut ViewContext) - + Send - + Sync - + 'static, + handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext) + Send + Sync + 'static, ) -> Self where Self: Sized, @@ -120,10 +111,7 @@ pub trait StatelessInteractive: Element { fn on_mouse_up_out( mut self, button: MouseButton, - handler: impl Fn(&mut Self::ViewState, &MouseUpEvent, &mut ViewContext) - + Send - + Sync - + 'static, + handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext) + Send + Sync + 'static, ) -> Self where Self: Sized, @@ -143,10 +131,7 @@ pub trait StatelessInteractive: Element { fn on_mouse_move( mut self, - handler: impl Fn(&mut Self::ViewState, &MouseMoveEvent, &mut ViewContext) - + Send - + Sync - + 'static, + handler: impl Fn(&mut V, &MouseMoveEvent, &mut ViewContext) + Send + Sync + 'static, ) -> Self where Self: Sized, @@ -163,10 +148,7 @@ pub trait StatelessInteractive: Element { fn on_scroll_wheel( mut self, - handler: impl Fn(&mut Self::ViewState, &ScrollWheelEvent, &mut ViewContext) - + Send - + Sync - + 'static, + handler: impl Fn(&mut V, &ScrollWheelEvent, &mut ViewContext) + Send + Sync + 'static, ) -> Self where Self: Sized, @@ -194,10 +176,7 @@ pub trait StatelessInteractive: Element { fn on_action( mut self, - listener: impl Fn(&mut Self::ViewState, &A, DispatchPhase, &mut ViewContext) - + Send - + Sync - + 'static, + listener: impl Fn(&mut V, &A, DispatchPhase, &mut ViewContext) + Send + Sync + 'static, ) -> Self where Self: Sized, @@ -215,12 +194,8 @@ pub trait StatelessInteractive: Element { fn on_key_down( mut self, - listener: impl Fn( - &mut Self::ViewState, - &KeyDownEvent, - DispatchPhase, - &mut ViewContext, - ) + Send + listener: impl Fn(&mut V, &KeyDownEvent, DispatchPhase, &mut ViewContext) + + Send + Sync + 'static, ) -> Self @@ -240,7 +215,7 @@ pub trait StatelessInteractive: Element { fn on_key_up( mut self, - listener: impl Fn(&mut Self::ViewState, &KeyUpEvent, DispatchPhase, &mut ViewContext) + listener: impl Fn(&mut V, &KeyUpEvent, DispatchPhase, &mut ViewContext) + Send + Sync + 'static, @@ -289,10 +264,7 @@ pub trait StatelessInteractive: Element { fn on_drop( mut self, - listener: impl Fn(&mut Self::ViewState, S, &mut ViewContext) - + Send - + Sync - + 'static, + listener: impl Fn(&mut V, S, &mut ViewContext) + Send + Sync + 'static, ) -> Self where Self: Sized, @@ -307,8 +279,8 @@ pub trait StatelessInteractive: Element { } } -pub trait StatefulInteractive: StatelessInteractive { - fn stateful_interaction(&mut self) -> &mut StatefulInteraction; +pub trait StatefulInteractive: StatelessInteractive { + fn stateful_interaction(&mut self) -> &mut StatefulInteraction; fn active(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self where @@ -335,10 +307,7 @@ pub trait StatefulInteractive: StatelessInteractive { fn on_click( mut self, - listener: impl Fn(&mut Self::ViewState, &ClickEvent, &mut ViewContext) - + Send - + Sync - + 'static, + listener: impl Fn(&mut V, &ClickEvent, &mut ViewContext) + Send + Sync + 'static, ) -> Self where Self: Sized, @@ -351,20 +320,14 @@ pub trait StatefulInteractive: StatelessInteractive { fn on_drag( mut self, - listener: impl Fn( - &mut Self::ViewState, - &mut ViewContext, - ) -> Drag - + Send - + Sync - + 'static, + listener: impl Fn(&mut V, &mut ViewContext) -> Drag + Send + Sync + 'static, ) -> Self where Self: Sized, S: Any + Send + Sync, - R: Fn(&mut Self::ViewState, &mut ViewContext) -> E, + R: Fn(&mut V, &mut ViewContext) -> E, R: 'static + Send + Sync, - E: Element, + E: Component, { debug_assert!( self.stateful_interaction().drag_listener.is_none(), @@ -907,7 +870,8 @@ pub struct ClickEvent { pub struct Drag where R: Fn(&mut V, &mut ViewContext) -> E, - E: Element, + V: 'static, + E: Component, { pub state: S, pub render_drag_handle: R, @@ -917,7 +881,8 @@ where impl Drag where R: Fn(&mut V, &mut ViewContext) -> E, - E: Element, + V: 'static, + E: Component, { pub fn new(state: S, render_drag_handle: R) -> Self { Drag { diff --git a/crates/gpui2/src/platform/test/dispatcher.rs b/crates/gpui2/src/platform/test/dispatcher.rs index 746a5ed0c0..0ed5638a8b 100644 --- a/crates/gpui2/src/platform/test/dispatcher.rs +++ b/crates/gpui2/src/platform/test/dispatcher.rs @@ -1,6 +1,6 @@ use crate::PlatformDispatcher; use async_task::Runnable; -use collections::{BTreeMap, HashMap, VecDeque}; +use collections::{HashMap, VecDeque}; use parking_lot::Mutex; use rand::prelude::*; use std::{ @@ -8,7 +8,7 @@ use std::{ pin::Pin, sync::Arc, task::{Context, Poll}, - time::{Duration, Instant}, + time::Duration, }; use util::post_inc; @@ -24,8 +24,8 @@ struct TestDispatcherState { random: StdRng, foreground: HashMap>, background: Vec, - delayed: BTreeMap, - time: Instant, + delayed: Vec<(Duration, Runnable)>, + time: Duration, is_main_thread: bool, next_id: TestDispatcherId, } @@ -36,8 +36,8 @@ impl TestDispatcher { random, foreground: HashMap::default(), background: Vec::new(), - delayed: BTreeMap::new(), - time: Instant::now(), + delayed: Vec::new(), + time: Duration::ZERO, is_main_thread: true, next_id: TestDispatcherId(1), }; @@ -49,7 +49,21 @@ impl TestDispatcher { } pub fn advance_clock(&self, by: Duration) { - self.state.lock().time += by; + let new_now = self.state.lock().time + by; + loop { + self.run_until_parked(); + let state = self.state.lock(); + let next_due_time = state.delayed.first().map(|(time, _)| *time); + drop(state); + if let Some(due_time) = next_due_time { + if due_time <= new_now { + self.state.lock().time = due_time; + continue; + } + } + break; + } + self.state.lock().time = new_now; } pub fn simulate_random_delay(&self) -> impl Future { @@ -112,17 +126,20 @@ impl PlatformDispatcher for TestDispatcher { fn dispatch_after(&self, duration: std::time::Duration, runnable: Runnable) { let mut state = self.state.lock(); let next_time = state.time + duration; - state.delayed.insert(next_time, runnable); + let ix = match state.delayed.binary_search_by_key(&next_time, |e| e.0) { + Ok(ix) | Err(ix) => ix, + }; + state.delayed.insert(ix, (next_time, runnable)); } fn poll(&self) -> bool { let mut state = self.state.lock(); - while let Some((deadline, _)) = state.delayed.first_key_value() { + while let Some((deadline, _)) = state.delayed.first() { if *deadline > state.time { break; } - let (_, runnable) = state.delayed.pop_first().unwrap(); + let (_, runnable) = state.delayed.remove(0); state.background.push(runnable); } diff --git a/crates/gpui2/src/platform/test/platform.rs b/crates/gpui2/src/platform/test/platform.rs index 9cc7d959e0..4d86c434d0 100644 --- a/crates/gpui2/src/platform/test/platform.rs +++ b/crates/gpui2/src/platform/test/platform.rs @@ -173,14 +173,14 @@ impl Platform for TestPlatform { } fn write_credentials(&self, _url: &str, _username: &str, _password: &[u8]) -> Result<()> { - unimplemented!() + Ok(()) } fn read_credentials(&self, _url: &str) -> Result)>> { - unimplemented!() + Ok(None) } fn delete_credentials(&self, _url: &str) -> Result<()> { - unimplemented!() + Ok(()) } } diff --git a/crates/gpui2/src/svg_renderer.rs b/crates/gpui2/src/svg_renderer.rs index 8fc68ede45..96e6658ab1 100644 --- a/crates/gpui2/src/svg_renderer.rs +++ b/crates/gpui2/src/svg_renderer.rs @@ -1,4 +1,4 @@ -use crate::{DevicePixels, IsZero, Result, SharedString, Size, AssetSource}; +use crate::{AssetSource, DevicePixels, IsZero, Result, SharedString, Size}; use anyhow::anyhow; use std::{hash::Hash, sync::Arc}; diff --git a/crates/gpui2/src/view.rs b/crates/gpui2/src/view.rs index 66f1a14869..36f1f5d9cd 100644 --- a/crates/gpui2/src/view.rs +++ b/crates/gpui2/src/view.rs @@ -1,8 +1,8 @@ use parking_lot::Mutex; use crate::{ - AnyBox, AnyElement, AnyHandle, BorrowWindow, Bounds, Element, ElementId, Handle, - IntoAnyElement, LayoutId, Pixels, ViewContext, WindowContext, + AnyBox, AnyElement, AnyHandle, BorrowWindow, Bounds, Component, Element, ElementId, Handle, + LayoutId, Pixels, ViewContext, WindowContext, }; use std::{marker::PhantomData, sync::Arc}; @@ -33,16 +33,16 @@ pub fn view( render: impl Fn(&mut V, &mut ViewContext) -> E + Send + Sync + 'static, ) -> View where - E: IntoAnyElement, + E: Component, { View { state, - render: Arc::new(move |state, cx| render(state, cx).into_any()), + render: Arc::new(move |state, cx| render(state, cx).render()), } } -impl IntoAnyElement for View { - fn into_any(self) -> AnyElement { +impl Component for View { + fn render(self) -> AnyElement { AnyElement::new(EraseViewState { view: self, parent_view_state_type: PhantomData, @@ -50,8 +50,7 @@ impl IntoAnyElement for V } } -impl Element for View { - type ViewState = (); +impl Element<()> for View { type ElementState = AnyElement; fn id(&self) -> Option { @@ -99,14 +98,13 @@ struct EraseViewState { unsafe impl Send for EraseViewState {} unsafe impl Sync for EraseViewState {} -impl IntoAnyElement for EraseViewState { - fn into_any(self) -> AnyElement { +impl Component for EraseViewState { + fn render(self) -> AnyElement { AnyElement::new(self) } } -impl Element for EraseViewState { - type ViewState = ParentV; +impl Element for EraseViewState { type ElementState = AnyBox; fn id(&self) -> Option { @@ -115,18 +113,18 @@ impl Element for EraseViewState { fn initialize( &mut self, - _: &mut Self::ViewState, + _: &mut ParentV, _: Option, - cx: &mut ViewContext, + cx: &mut ViewContext, ) -> Self::ElementState { ViewObject::initialize(&mut self.view, cx) } fn layout( &mut self, - _: &mut Self::ViewState, + _: &mut ParentV, element: &mut Self::ElementState, - cx: &mut ViewContext, + cx: &mut ViewContext, ) -> LayoutId { ViewObject::layout(&mut self.view, element, cx) } @@ -134,9 +132,9 @@ impl Element for EraseViewState { fn paint( &mut self, bounds: Bounds, - _: &mut Self::ViewState, + _: &mut ParentV, element: &mut Self::ElementState, - cx: &mut ViewContext, + cx: &mut ViewContext, ) { ViewObject::paint(&mut self.view, bounds, element, cx) } @@ -193,8 +191,8 @@ impl AnyView { } } -impl IntoAnyElement for AnyView { - fn into_any(self) -> AnyElement { +impl Component for AnyView { + fn render(self) -> AnyElement { AnyElement::new(EraseAnyViewState { view: self, parent_view_state_type: PhantomData, @@ -202,8 +200,7 @@ impl IntoAnyElement for AnyView { } } -impl Element for AnyView { - type ViewState = (); +impl Element<()> for AnyView { type ElementState = AnyBox; fn id(&self) -> Option { @@ -212,18 +209,18 @@ impl Element for AnyView { fn initialize( &mut self, - _: &mut Self::ViewState, + _: &mut (), _: Option, - cx: &mut ViewContext, + cx: &mut ViewContext<()>, ) -> Self::ElementState { self.view.lock().initialize(cx) } fn layout( &mut self, - _: &mut Self::ViewState, + _: &mut (), element: &mut Self::ElementState, - cx: &mut ViewContext, + cx: &mut ViewContext<()>, ) -> LayoutId { self.view.lock().layout(element, cx) } @@ -233,7 +230,7 @@ impl Element for AnyView { bounds: Bounds, _: &mut (), element: &mut AnyBox, - cx: &mut ViewContext, + cx: &mut ViewContext<()>, ) { self.view.lock().paint(bounds, element, cx) } @@ -247,14 +244,13 @@ struct EraseAnyViewState { unsafe impl Send for EraseAnyViewState {} unsafe impl Sync for EraseAnyViewState {} -impl IntoAnyElement for EraseAnyViewState { - fn into_any(self) -> AnyElement { +impl Component for EraseAnyViewState { + fn render(self) -> AnyElement { AnyElement::new(self) } } -impl Element for EraseAnyViewState { - type ViewState = ParentV; +impl Element for EraseAnyViewState { type ElementState = AnyBox; fn id(&self) -> Option { @@ -263,18 +259,18 @@ impl Element for EraseAnyViewState { fn initialize( &mut self, - _: &mut Self::ViewState, + _: &mut ParentV, _: Option, - cx: &mut ViewContext, + cx: &mut ViewContext, ) -> Self::ElementState { self.view.view.lock().initialize(cx) } fn layout( &mut self, - _: &mut Self::ViewState, + _: &mut ParentV, element: &mut Self::ElementState, - cx: &mut ViewContext, + cx: &mut ViewContext, ) -> LayoutId { self.view.view.lock().layout(element, cx) } @@ -282,9 +278,9 @@ impl Element for EraseAnyViewState { fn paint( &mut self, bounds: Bounds, - _: &mut Self::ViewState, + _: &mut ParentV, element: &mut Self::ElementState, - cx: &mut ViewContext, + cx: &mut ViewContext, ) { self.view.view.lock().paint(bounds, element, cx) } diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index cdd83265b2..c23f60de0c 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -901,6 +901,7 @@ impl<'a, 'w> WindowContext<'a, 'w> { } InputEvent::FileDrop(file_drop) => match file_drop { FileDropEvent::Entered { position, files } => { + self.window.mouse_position = position; self.active_drag.get_or_insert_with(|| AnyDrag { drag_handle_view: None, cursor_offset: position, @@ -914,17 +915,23 @@ impl<'a, 'w> WindowContext<'a, 'w> { modifiers: Modifiers::default(), }) } - FileDropEvent::Pending { position } => InputEvent::MouseMove(MouseMoveEvent { - position, - pressed_button: Some(MouseButton::Left), - modifiers: Modifiers::default(), - }), - FileDropEvent::Submit { position } => InputEvent::MouseUp(MouseUpEvent { - button: MouseButton::Left, - position, - modifiers: Modifiers::default(), - click_count: 1, - }), + FileDropEvent::Pending { position } => { + self.window.mouse_position = position; + InputEvent::MouseMove(MouseMoveEvent { + position, + pressed_button: Some(MouseButton::Left), + modifiers: Modifiers::default(), + }) + } + FileDropEvent::Submit { position } => { + self.window.mouse_position = position; + InputEvent::MouseUp(MouseUpEvent { + button: MouseButton::Left, + position, + modifiers: Modifiers::default(), + click_count: 1, + }) + } FileDropEvent::Exited => InputEvent::MouseUp(MouseUpEvent { button: MouseButton::Left, position: Point::default(), diff --git a/crates/gpui2_macros/src/derive_component.rs b/crates/gpui2_macros/src/derive_component.rs new file mode 100644 index 0000000000..d1919c8bc4 --- /dev/null +++ b/crates/gpui2_macros/src/derive_component.rs @@ -0,0 +1,66 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, parse_quote, DeriveInput}; + +pub fn derive_component(input: TokenStream) -> TokenStream { + let ast = parse_macro_input!(input as DeriveInput); + let name = &ast.ident; + + let mut trait_generics = ast.generics.clone(); + let view_type = if let Some(view_type) = specified_view_type(&ast) { + quote! { #view_type } + } else { + if let Some(first_type_param) = ast.generics.params.iter().find_map(|param| { + if let syn::GenericParam::Type(type_param) = param { + Some(type_param.ident.clone()) + } else { + None + } + }) { + quote! { #first_type_param } + } else { + trait_generics.params.push(parse_quote! { V: 'static }); + quote! { V } + } + }; + + let (impl_generics, _, where_clause) = trait_generics.split_for_impl(); + let (_, ty_generics, _) = ast.generics.split_for_impl(); + + let expanded = quote! { + impl #impl_generics gpui2::Component<#view_type> for #name #ty_generics #where_clause { + fn render(self) -> gpui2::AnyElement<#view_type> { + (move |view_state: &mut #view_type, cx: &mut gpui2::ViewContext<'_, '_, #view_type>| self.render(view_state, cx)) + .render() + } + } + }; + + TokenStream::from(expanded) +} + +fn specified_view_type(ast: &DeriveInput) -> Option { + let component_attr = ast + .attrs + .iter() + .find(|attr| attr.path.is_ident("component"))?; + + if let Ok(syn::Meta::List(meta_list)) = component_attr.parse_meta() { + meta_list.nested.iter().find_map(|nested| { + if let syn::NestedMeta::Meta(syn::Meta::NameValue(nv)) = nested { + if nv.path.is_ident("view_type") { + if let syn::Lit::Str(lit_str) = &nv.lit { + return Some( + lit_str + .parse::() + .expect("Failed to parse view_type"), + ); + } + } + } + None + }) + } else { + None + } +} diff --git a/crates/gpui2_macros/src/derive_element.rs b/crates/gpui2_macros/src/derive_element.rs deleted file mode 100644 index 3f6b053aa0..0000000000 --- a/crates/gpui2_macros/src/derive_element.rs +++ /dev/null @@ -1,95 +0,0 @@ -use proc_macro::TokenStream; -use quote::quote; -use syn::{parse_macro_input, DeriveInput, GenericParam}; - -pub fn derive_element(input: TokenStream) -> TokenStream { - let ast = parse_macro_input!(input as DeriveInput); - let type_name = ast.ident; - - let mut state_type = quote! { () }; - - for param in &ast.generics.params { - if let GenericParam::Type(type_param) = param { - let type_ident = &type_param.ident; - state_type = quote! {#type_ident}; - break; - } - } - - let attrs = &ast.attrs; - for attr in attrs { - if attr.path.is_ident("element") { - match attr.parse_meta() { - Ok(syn::Meta::List(i)) => { - for nested_meta in i.nested { - if let syn::NestedMeta::Meta(syn::Meta::NameValue(nv)) = nested_meta { - if nv.path.is_ident("view_state") { - if let syn::Lit::Str(lit_str) = nv.lit { - state_type = lit_str.value().parse().unwrap(); - } - } - } - } - } - _ => (), - } - } - } - - let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl(); - - let gen = quote! { - impl #impl_generics gpui2::IntoAnyElement<#state_type> for #type_name #ty_generics - #where_clause - { - fn into_any(self) -> gpui2::AnyElement<#state_type> { - gpui2::AnyElement::new(self) - } - } - - impl #impl_generics gpui2::Element for #type_name #ty_generics - #where_clause - { - type ViewState = #state_type; - type ElementState = gpui2::AnyElement<#state_type>; - - fn id(&self) -> Option { - None - } - - fn initialize( - &mut self, - view_state: &mut Self::ViewState, - _: Option, - cx: &mut gpui2::ViewContext - ) -> Self::ElementState { - use gpui2::IntoAnyElement; - - let mut element = self.render(view_state, cx).into_any(); - element.initialize(view_state, cx); - element - } - - fn layout( - &mut self, - view_state: &mut Self::ViewState, - rendered_element: &mut Self::ElementState, - cx: &mut gpui2::ViewContext, - ) -> gpui2::LayoutId { - rendered_element.layout(view_state, cx) - } - - fn paint( - &mut self, - bounds: gpui2::Bounds, - view_state: &mut Self::ViewState, - rendered_element: &mut Self::ElementState, - cx: &mut gpui2::ViewContext, - ) { - rendered_element.paint(view_state, cx) - } - } - }; - - gen.into() -} diff --git a/crates/gpui2_macros/src/gpui2_macros.rs b/crates/gpui2_macros/src/gpui2_macros.rs index 59fd046c83..2e0c0547f7 100644 --- a/crates/gpui2_macros/src/gpui2_macros.rs +++ b/crates/gpui2_macros/src/gpui2_macros.rs @@ -1,6 +1,6 @@ use proc_macro::TokenStream; -mod derive_element; +mod derive_component; mod style_helpers; mod test; @@ -9,9 +9,9 @@ pub fn style_helpers(args: TokenStream) -> TokenStream { style_helpers::style_helpers(args) } -#[proc_macro_derive(Element, attributes(element))] -pub fn derive_element(input: TokenStream) -> TokenStream { - derive_element::derive_element(input) +#[proc_macro_derive(Component, attributes(component))] +pub fn derive_component(input: TokenStream) -> TokenStream { + derive_component::derive_component(input) } #[proc_macro_attribute] diff --git a/crates/language2/Cargo.toml b/crates/language2/Cargo.toml index 77b195293b..a3e1bf3e2a 100644 --- a/crates/language2/Cargo.toml +++ b/crates/language2/Cargo.toml @@ -25,11 +25,10 @@ test-support = [ clock = { path = "../clock" } collections = { path = "../collections" } fuzzy2 = { path = "../fuzzy2" } -fs = { path = "../fs" } git = { path = "../git" } gpui2 = { path = "../gpui2" } lsp2 = { path = "../lsp2" } -rpc = { path = "../rpc" } +rpc2 = { path = "../rpc2" } settings2 = { path = "../settings2" } sum_tree = { path = "../sum_tree" } text = { path = "../text" } diff --git a/crates/language2/src/buffer.rs b/crates/language2/src/buffer.rs index a8b764abc9..54425cec47 100644 --- a/crates/language2/src/buffer.rs +++ b/crates/language2/src/buffer.rs @@ -226,7 +226,7 @@ pub trait File: Send + Sync { fn as_any(&self) -> &dyn Any; - fn to_proto(&self) -> rpc::proto::File; + fn to_proto(&self) -> rpc2::proto::File; } pub trait LocalFile: File { @@ -375,7 +375,7 @@ impl Buffer { file, ); this.text.set_line_ending(proto::deserialize_line_ending( - rpc::proto::LineEnding::from_i32(message.line_ending) + rpc2::proto::LineEnding::from_i32(message.line_ending) .ok_or_else(|| anyhow!("missing line_ending"))?, )); this.saved_version = proto::deserialize_version(&message.saved_version); diff --git a/crates/language2/src/language2.rs b/crates/language2/src/language2.rs index d21ebd572a..0a267e5e1a 100644 --- a/crates/language2/src/language2.rs +++ b/crates/language2/src/language2.rs @@ -667,7 +667,7 @@ struct LanguageRegistryState { pub struct PendingLanguageServer { pub server_id: LanguageServerId, - pub task: Task>>, + pub task: Task>, pub container_dir: Option>, } @@ -906,6 +906,7 @@ impl LanguageRegistry { pub fn create_pending_language_server( self: &Arc, + stderr_capture: Arc>>, language: Arc, adapter: Arc, root_path: Arc, @@ -945,7 +946,7 @@ impl LanguageRegistry { }) .detach(); - Ok(Some(server)) + Ok(server) }); return Some(PendingLanguageServer { @@ -996,24 +997,23 @@ impl LanguageRegistry { }) .clone(); - let binary = match entry.await.log_err() { - Some(binary) => binary, - None => return Ok(None), + let binary = match entry.await { + Ok(binary) => binary, + Err(err) => anyhow::bail!("{err}"), }; if let Some(task) = adapter.will_start_server(&delegate, &mut cx) { - if task.await.log_err().is_none() { - return Ok(None); - } + task.await?; } - Ok(Some(lsp2::LanguageServer::new( + lsp2::LanguageServer::new( + stderr_capture, server_id, binary, &root_path, adapter.code_action_kinds(), cx, - )?)) + ) }) }; @@ -1862,111 +1862,112 @@ pub fn range_from_lsp(range: lsp2::Range) -> Range> { start..end } -// #[cfg(test)] -// mod tests { -// use super::*; -// use gpui::TestAppContext; +#[cfg(test)] +mod tests { + use super::*; + use gpui2::TestAppContext; -// #[gpui::test(iterations = 10)] -// async fn test_first_line_pattern(cx: &mut TestAppContext) { -// let mut languages = LanguageRegistry::test(); -// languages.set_executor(cx.background()); -// let languages = Arc::new(languages); -// languages.register( -// "/javascript", -// LanguageConfig { -// name: "JavaScript".into(), -// path_suffixes: vec!["js".into()], -// first_line_pattern: Some(Regex::new(r"\bnode\b").unwrap()), -// ..Default::default() -// }, -// tree_sitter_typescript::language_tsx(), -// vec![], -// |_| Default::default(), -// ); + #[gpui2::test(iterations = 10)] + async fn test_first_line_pattern(cx: &mut TestAppContext) { + let mut languages = LanguageRegistry::test(); -// languages -// .language_for_file("the/script", None) -// .await -// .unwrap_err(); -// languages -// .language_for_file("the/script", Some(&"nothing".into())) -// .await -// .unwrap_err(); -// assert_eq!( -// languages -// .language_for_file("the/script", Some(&"#!/bin/env node".into())) -// .await -// .unwrap() -// .name() -// .as_ref(), -// "JavaScript" -// ); -// } + languages.set_executor(cx.executor().clone()); + let languages = Arc::new(languages); + languages.register( + "/javascript", + LanguageConfig { + name: "JavaScript".into(), + path_suffixes: vec!["js".into()], + first_line_pattern: Some(Regex::new(r"\bnode\b").unwrap()), + ..Default::default() + }, + tree_sitter_typescript::language_tsx(), + vec![], + |_| Default::default(), + ); -// #[gpui::test(iterations = 10)] -// async fn test_language_loading(cx: &mut TestAppContext) { -// let mut languages = LanguageRegistry::test(); -// languages.set_executor(cx.background()); -// let languages = Arc::new(languages); -// languages.register( -// "/JSON", -// LanguageConfig { -// name: "JSON".into(), -// path_suffixes: vec!["json".into()], -// ..Default::default() -// }, -// tree_sitter_json::language(), -// vec![], -// |_| Default::default(), -// ); -// languages.register( -// "/rust", -// LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".into()], -// ..Default::default() -// }, -// tree_sitter_rust::language(), -// vec![], -// |_| Default::default(), -// ); -// assert_eq!( -// languages.language_names(), -// &[ -// "JSON".to_string(), -// "Plain Text".to_string(), -// "Rust".to_string(), -// ] -// ); + languages + .language_for_file("the/script", None) + .await + .unwrap_err(); + languages + .language_for_file("the/script", Some(&"nothing".into())) + .await + .unwrap_err(); + assert_eq!( + languages + .language_for_file("the/script", Some(&"#!/bin/env node".into())) + .await + .unwrap() + .name() + .as_ref(), + "JavaScript" + ); + } -// let rust1 = languages.language_for_name("Rust"); -// let rust2 = languages.language_for_name("Rust"); + #[gpui2::test(iterations = 10)] + async fn test_language_loading(cx: &mut TestAppContext) { + let mut languages = LanguageRegistry::test(); + languages.set_executor(cx.executor().clone()); + let languages = Arc::new(languages); + languages.register( + "/JSON", + LanguageConfig { + name: "JSON".into(), + path_suffixes: vec!["json".into()], + ..Default::default() + }, + tree_sitter_json::language(), + vec![], + |_| Default::default(), + ); + languages.register( + "/rust", + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".into()], + ..Default::default() + }, + tree_sitter_rust::language(), + vec![], + |_| Default::default(), + ); + assert_eq!( + languages.language_names(), + &[ + "JSON".to_string(), + "Plain Text".to_string(), + "Rust".to_string(), + ] + ); -// // Ensure language is still listed even if it's being loaded. -// assert_eq!( -// languages.language_names(), -// &[ -// "JSON".to_string(), -// "Plain Text".to_string(), -// "Rust".to_string(), -// ] -// ); + let rust1 = languages.language_for_name("Rust"); + let rust2 = languages.language_for_name("Rust"); -// let (rust1, rust2) = futures::join!(rust1, rust2); -// assert!(Arc::ptr_eq(&rust1.unwrap(), &rust2.unwrap())); + // Ensure language is still listed even if it's being loaded. + assert_eq!( + languages.language_names(), + &[ + "JSON".to_string(), + "Plain Text".to_string(), + "Rust".to_string(), + ] + ); -// // Ensure language is still listed even after loading it. -// assert_eq!( -// languages.language_names(), -// &[ -// "JSON".to_string(), -// "Plain Text".to_string(), -// "Rust".to_string(), -// ] -// ); + let (rust1, rust2) = futures::join!(rust1, rust2); + assert!(Arc::ptr_eq(&rust1.unwrap(), &rust2.unwrap())); -// // Loading an unknown language returns an error. -// assert!(languages.language_for_name("Unknown").await.is_err()); -// } -// } + // Ensure language is still listed even after loading it. + assert_eq!( + languages.language_names(), + &[ + "JSON".to_string(), + "Plain Text".to_string(), + "Rust".to_string(), + ] + ); + + // Loading an unknown language returns an error. + assert!(languages.language_for_name("Unknown").await.is_err()); + } +} diff --git a/crates/language2/src/proto.rs b/crates/language2/src/proto.rs index e23711e328..f90bb94742 100644 --- a/crates/language2/src/proto.rs +++ b/crates/language2/src/proto.rs @@ -5,7 +5,7 @@ use crate::{ use anyhow::{anyhow, Result}; use clock::ReplicaId; use lsp2::{DiagnosticSeverity, LanguageServerId}; -use rpc::proto; +use rpc2::proto; use std::{ops::Range, sync::Arc}; use text::*; diff --git a/crates/lsp2/src/lsp2.rs b/crates/lsp2/src/lsp2.rs index f874d9f118..27b7b36e73 100644 --- a/crates/lsp2/src/lsp2.rs +++ b/crates/lsp2/src/lsp2.rs @@ -136,6 +136,7 @@ struct Error { impl LanguageServer { pub fn new( + stderr_capture: Arc>>, server_id: LanguageServerId, binary: LanguageServerBinary, root_path: &Path, @@ -165,6 +166,7 @@ impl LanguageServer { stdin, stdout, Some(stderr), + stderr_capture, Some(server), root_path, code_action_kinds, @@ -197,6 +199,7 @@ impl LanguageServer { stdin: Stdin, stdout: Stdout, stderr: Option, + stderr_capture: Arc>>, server: Option, root_path: &Path, code_action_kinds: Option>, @@ -237,7 +240,8 @@ impl LanguageServer { let stderr_input_task = stderr .map(|stderr| { let io_handlers = io_handlers.clone(); - cx.spawn(|_| Self::handle_stderr(stderr, io_handlers).log_err()) + let stderr_captures = stderr_capture.clone(); + cx.spawn(|_| Self::handle_stderr(stderr, io_handlers, stderr_captures).log_err()) }) .unwrap_or_else(|| Task::Ready(Some(None))); let input_task = cx.spawn(|_| async move { @@ -360,12 +364,14 @@ impl LanguageServer { async fn handle_stderr( stderr: Stderr, io_handlers: Arc>>, + stderr_capture: Arc>>, ) -> anyhow::Result<()> where Stderr: AsyncRead + Unpin + Send + 'static, { let mut stderr = BufReader::new(stderr); let mut buffer = Vec::new(); + loop { buffer.clear(); stderr.read_until(b'\n', &mut buffer).await?; @@ -374,6 +380,10 @@ impl LanguageServer { for handler in io_handlers.lock().values_mut() { handler(IoKind::StdErr, message); } + + if let Some(stderr) = stderr_capture.lock().as_mut() { + stderr.push_str(message); + } } // Don't starve the main thread when receiving lots of messages at once. @@ -933,6 +943,7 @@ impl LanguageServer { stdin_writer, stdout_reader, None::, + Arc::new(Mutex::new(None)), None, Path::new("/"), None, @@ -945,6 +956,7 @@ impl LanguageServer { stdout_writer, stdin_reader, None::, + Arc::new(Mutex::new(None)), None, Path::new("/"), None, diff --git a/crates/prettier2/Cargo.toml b/crates/prettier2/Cargo.toml index 0fca004344..8defd40262 100644 --- a/crates/prettier2/Cargo.toml +++ b/crates/prettier2/Cargo.toml @@ -27,6 +27,7 @@ serde_derive.workspace = true serde_json.workspace = true anyhow.workspace = true futures.workspace = true +parking_lot.workspace = true [dev-dependencies] language2 = { path = "../language2", features = ["test-support"] } diff --git a/crates/prettier2/src/prettier2.rs b/crates/prettier2/src/prettier2.rs index 4e8d8b9070..ed22d23157 100644 --- a/crates/prettier2/src/prettier2.rs +++ b/crates/prettier2/src/prettier2.rs @@ -210,6 +210,7 @@ impl Prettier { .spawn(async move { node.binary_path().await }) .await?; let server = LanguageServer::new( + Arc::new(parking_lot::Mutex::new(None)), server_id, LanguageServerBinary { path: node_path, diff --git a/crates/project2/Cargo.toml b/crates/project2/Cargo.toml index 28b9826bd4..98bf9b62be 100644 --- a/crates/project2/Cargo.toml +++ b/crates/project2/Cargo.toml @@ -34,7 +34,7 @@ language2 = { path = "../language2" } lsp2 = { path = "../lsp2" } node_runtime = { path = "../node_runtime" } prettier2 = { path = "../prettier2" } -rpc = { path = "../rpc" } +rpc2 = { path = "../rpc2" } settings2 = { path = "../settings2" } sum_tree = { path = "../sum_tree" } terminal2 = { path = "../terminal2" } @@ -78,7 +78,7 @@ lsp2 = { path = "../lsp2", features = ["test-support"] } settings2 = { path = "../settings2", features = ["test-support"] } prettier2 = { path = "../prettier2", features = ["test-support"] } util = { path = "../util", features = ["test-support"] } -rpc = { path = "../rpc", features = ["test-support"] } +rpc2 = { path = "../rpc2", features = ["test-support"] } git2.workspace = true tempdir.workspace = true unindent.workspace = true diff --git a/crates/project2/src/project2.rs b/crates/project2/src/project2.rs index a49a17a3b6..b0fde1c486 100644 --- a/crates/project2/src/project2.rs +++ b/crates/project2/src/project2.rs @@ -52,6 +52,7 @@ use lsp2::{ }; use lsp_command::*; use node_runtime::NodeRuntime; +use parking_lot::Mutex; use postage::watch; use prettier2::{LocateStart, Prettier, PRETTIER_SERVER_FILE, PRETTIER_SERVER_JS}; use project_settings::{LspSettings, ProjectSettings}; @@ -2778,7 +2779,9 @@ impl Project { return; } + let stderr_capture = Arc::new(Mutex::new(Some(String::new()))); let pending_server = match self.languages.create_pending_language_server( + stderr_capture.clone(), language.clone(), adapter.clone(), worktree_path, @@ -2824,10 +2827,14 @@ impl Project { .await; match result { - Ok(server) => server, + Ok(server) => { + stderr_capture.lock().take(); + server + } Err(err) => { log::error!("failed to start language server {:?}: {}", server_name, err); + log::error!("server stderr: {:?}", stderr_capture.lock().take()); if let Some(this) = this.upgrade() { if let Some(container_dir) = container_dir { @@ -2931,19 +2938,16 @@ impl Project { key: (WorktreeId, LanguageServerName), cx: &mut AsyncAppContext, ) -> Result>> { - let setup = Self::setup_pending_language_server( + let language_server = Self::setup_pending_language_server( this.clone(), initialization_options, pending_server, adapter.clone(), server_id, cx, - ); + ) + .await?; - let language_server = match setup.await? { - Some(language_server) => language_server, - None => return Ok(None), - }; let this = match this.upgrade() { Some(this) => this, None => return Err(anyhow!("failed to upgrade project handle")), @@ -2970,12 +2974,9 @@ impl Project { adapter: Arc, server_id: LanguageServerId, cx: &mut AsyncAppContext, - ) -> Result>> { + ) -> Result> { let workspace_config = cx.update(|cx| adapter.workspace_configuration(cx))?.await; - let language_server = match pending_server.task.await? { - Some(server) => server, - None => return Ok(None), - }; + let language_server = pending_server.task.await?; language_server .on_notification::({ @@ -3050,6 +3051,7 @@ impl Project { } }) .detach(); + language_server .on_request::({ let this = this.clone(); @@ -3138,7 +3140,7 @@ impl Project { ) .ok(); - Ok(Some(language_server)) + Ok(language_server) } fn insert_newly_running_language_server( diff --git a/crates/project2/src/worktree.rs b/crates/project2/src/worktree.rs index c094f21db4..c1b762640b 100644 --- a/crates/project2/src/worktree.rs +++ b/crates/project2/src/worktree.rs @@ -2646,8 +2646,8 @@ impl language2::File for File { self } - fn to_proto(&self) -> rpc::proto::File { - rpc::proto::File { + fn to_proto(&self) -> rpc2::proto::File { + rpc2::proto::File { worktree_id: self.worktree.entity_id().as_u64(), entry_id: self.entry_id.to_proto(), path: self.path.to_string_lossy().into(), @@ -2713,7 +2713,7 @@ impl File { } pub fn from_proto( - proto: rpc::proto::File, + proto: rpc2::proto::File, worktree: Handle, cx: &AppContext, ) -> Result { diff --git a/crates/rpc2/Cargo.toml b/crates/rpc2/Cargo.toml new file mode 100644 index 0000000000..f108af3d3f --- /dev/null +++ b/crates/rpc2/Cargo.toml @@ -0,0 +1,44 @@ +[package] +description = "Shared logic for communication between the Zed app and the zed.dev server" +edition = "2021" +name = "rpc2" +version = "0.1.0" +publish = false + +[lib] +path = "src/rpc.rs" +doctest = false + +[features] +test-support = ["collections/test-support", "gpui2/test-support"] + +[dependencies] +clock = { path = "../clock" } +collections = { path = "../collections" } +gpui2 = { path = "../gpui2", optional = true } +util = { path = "../util" } +anyhow.workspace = true +async-lock = "2.4" +async-tungstenite = "0.16" +base64 = "0.13" +futures.workspace = true +parking_lot.workspace = true +prost.workspace = true +rand.workspace = true +rsa = "0.4" +serde.workspace = true +serde_derive.workspace = true +smol-timeout = "0.6" +tracing = { version = "0.1.34", features = ["log"] } +zstd = "0.11" + +[build-dependencies] +prost-build = "0.9" + +[dev-dependencies] +collections = { path = "../collections", features = ["test-support"] } +gpui2 = { path = "../gpui2", features = ["test-support"] } +smol.workspace = true +tempdir.workspace = true +ctor.workspace = true +env_logger.workspace = true diff --git a/crates/rpc2/build.rs b/crates/rpc2/build.rs new file mode 100644 index 0000000000..66b289f1db --- /dev/null +++ b/crates/rpc2/build.rs @@ -0,0 +1,8 @@ +fn main() { + let mut build = prost_build::Config::new(); + // build.protoc_arg("--experimental_allow_proto3_optional"); + build + .type_attribute(".", "#[derive(serde::Serialize)]") + .compile_protos(&["proto/zed.proto"], &["proto"]) + .unwrap(); +} diff --git a/crates/rpc2/proto/zed.proto b/crates/rpc2/proto/zed.proto new file mode 100644 index 0000000000..3501e70e6a --- /dev/null +++ b/crates/rpc2/proto/zed.proto @@ -0,0 +1,1559 @@ +syntax = "proto3"; +package zed.messages; + +// Looking for a number? Search "// Current max" + +message PeerId { + uint32 owner_id = 1; + uint32 id = 2; +} + +message Envelope { + uint32 id = 1; + optional uint32 responding_to = 2; + optional PeerId original_sender_id = 3; + oneof payload { + Hello hello = 4; + Ack ack = 5; + Error error = 6; + Ping ping = 7; + Test test = 8; + + CreateRoom create_room = 9; + CreateRoomResponse create_room_response = 10; + JoinRoom join_room = 11; + JoinRoomResponse join_room_response = 12; + RejoinRoom rejoin_room = 13; + RejoinRoomResponse rejoin_room_response = 14; + LeaveRoom leave_room = 15; + Call call = 16; + IncomingCall incoming_call = 17; + CallCanceled call_canceled = 18; + CancelCall cancel_call = 19; + DeclineCall decline_call = 20; + UpdateParticipantLocation update_participant_location = 21; + RoomUpdated room_updated = 22; + + ShareProject share_project = 23; + ShareProjectResponse share_project_response = 24; + UnshareProject unshare_project = 25; + JoinProject join_project = 26; + JoinProjectResponse join_project_response = 27; + LeaveProject leave_project = 28; + AddProjectCollaborator add_project_collaborator = 29; + UpdateProjectCollaborator update_project_collaborator = 30; + RemoveProjectCollaborator remove_project_collaborator = 31; + + GetDefinition get_definition = 32; + GetDefinitionResponse get_definition_response = 33; + GetTypeDefinition get_type_definition = 34; + GetTypeDefinitionResponse get_type_definition_response = 35; + GetReferences get_references = 36; + GetReferencesResponse get_references_response = 37; + GetDocumentHighlights get_document_highlights = 38; + GetDocumentHighlightsResponse get_document_highlights_response = 39; + GetProjectSymbols get_project_symbols = 40; + GetProjectSymbolsResponse get_project_symbols_response = 41; + OpenBufferForSymbol open_buffer_for_symbol = 42; + OpenBufferForSymbolResponse open_buffer_for_symbol_response = 43; + + UpdateProject update_project = 44; + UpdateWorktree update_worktree = 45; + + CreateProjectEntry create_project_entry = 46; + RenameProjectEntry rename_project_entry = 47; + CopyProjectEntry copy_project_entry = 48; + DeleteProjectEntry delete_project_entry = 49; + ProjectEntryResponse project_entry_response = 50; + ExpandProjectEntry expand_project_entry = 51; + ExpandProjectEntryResponse expand_project_entry_response = 52; + + UpdateDiagnosticSummary update_diagnostic_summary = 53; + StartLanguageServer start_language_server = 54; + UpdateLanguageServer update_language_server = 55; + + OpenBufferById open_buffer_by_id = 56; + OpenBufferByPath open_buffer_by_path = 57; + OpenBufferResponse open_buffer_response = 58; + CreateBufferForPeer create_buffer_for_peer = 59; + UpdateBuffer update_buffer = 60; + UpdateBufferFile update_buffer_file = 61; + SaveBuffer save_buffer = 62; + BufferSaved buffer_saved = 63; + BufferReloaded buffer_reloaded = 64; + ReloadBuffers reload_buffers = 65; + ReloadBuffersResponse reload_buffers_response = 66; + SynchronizeBuffers synchronize_buffers = 67; + SynchronizeBuffersResponse synchronize_buffers_response = 68; + FormatBuffers format_buffers = 69; + FormatBuffersResponse format_buffers_response = 70; + GetCompletions get_completions = 71; + GetCompletionsResponse get_completions_response = 72; + ApplyCompletionAdditionalEdits apply_completion_additional_edits = 73; + ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 74; + GetCodeActions get_code_actions = 75; + GetCodeActionsResponse get_code_actions_response = 76; + GetHover get_hover = 77; + GetHoverResponse get_hover_response = 78; + ApplyCodeAction apply_code_action = 79; + ApplyCodeActionResponse apply_code_action_response = 80; + PrepareRename prepare_rename = 81; + PrepareRenameResponse prepare_rename_response = 82; + PerformRename perform_rename = 83; + PerformRenameResponse perform_rename_response = 84; + SearchProject search_project = 85; + SearchProjectResponse search_project_response = 86; + + UpdateContacts update_contacts = 87; + UpdateInviteInfo update_invite_info = 88; + ShowContacts show_contacts = 89; + + GetUsers get_users = 90; + FuzzySearchUsers fuzzy_search_users = 91; + UsersResponse users_response = 92; + RequestContact request_contact = 93; + RespondToContactRequest respond_to_contact_request = 94; + RemoveContact remove_contact = 95; + + Follow follow = 96; + FollowResponse follow_response = 97; + UpdateFollowers update_followers = 98; + Unfollow unfollow = 99; + GetPrivateUserInfo get_private_user_info = 100; + GetPrivateUserInfoResponse get_private_user_info_response = 101; + UpdateDiffBase update_diff_base = 102; + + OnTypeFormatting on_type_formatting = 103; + OnTypeFormattingResponse on_type_formatting_response = 104; + + UpdateWorktreeSettings update_worktree_settings = 105; + + InlayHints inlay_hints = 106; + InlayHintsResponse inlay_hints_response = 107; + ResolveInlayHint resolve_inlay_hint = 108; + ResolveInlayHintResponse resolve_inlay_hint_response = 109; + RefreshInlayHints refresh_inlay_hints = 110; + + CreateChannel create_channel = 111; + CreateChannelResponse create_channel_response = 112; + InviteChannelMember invite_channel_member = 113; + RemoveChannelMember remove_channel_member = 114; + RespondToChannelInvite respond_to_channel_invite = 115; + UpdateChannels update_channels = 116; + JoinChannel join_channel = 117; + DeleteChannel delete_channel = 118; + GetChannelMembers get_channel_members = 119; + GetChannelMembersResponse get_channel_members_response = 120; + SetChannelMemberAdmin set_channel_member_admin = 121; + RenameChannel rename_channel = 122; + RenameChannelResponse rename_channel_response = 123; + + JoinChannelBuffer join_channel_buffer = 124; + JoinChannelBufferResponse join_channel_buffer_response = 125; + UpdateChannelBuffer update_channel_buffer = 126; + LeaveChannelBuffer leave_channel_buffer = 127; + UpdateChannelBufferCollaborators update_channel_buffer_collaborators = 128; + RejoinChannelBuffers rejoin_channel_buffers = 129; + RejoinChannelBuffersResponse rejoin_channel_buffers_response = 130; + AckBufferOperation ack_buffer_operation = 143; + + JoinChannelChat join_channel_chat = 131; + JoinChannelChatResponse join_channel_chat_response = 132; + LeaveChannelChat leave_channel_chat = 133; + SendChannelMessage send_channel_message = 134; + SendChannelMessageResponse send_channel_message_response = 135; + ChannelMessageSent channel_message_sent = 136; + GetChannelMessages get_channel_messages = 137; + GetChannelMessagesResponse get_channel_messages_response = 138; + RemoveChannelMessage remove_channel_message = 139; + AckChannelMessage ack_channel_message = 144; + + LinkChannel link_channel = 140; + UnlinkChannel unlink_channel = 141; + MoveChannel move_channel = 142; // current max: 144 + } +} + +// Messages + +message Hello { + PeerId peer_id = 1; +} + +message Ping {} + +message Ack {} + +message Error { + string message = 1; +} + +message Test { + uint64 id = 1; +} + +message CreateRoom {} + +message CreateRoomResponse { + Room room = 1; + optional LiveKitConnectionInfo live_kit_connection_info = 2; +} + +message JoinRoom { + uint64 id = 1; +} + +message JoinRoomResponse { + Room room = 1; + optional uint64 channel_id = 2; + optional LiveKitConnectionInfo live_kit_connection_info = 3; +} + +message RejoinRoom { + uint64 id = 1; + repeated UpdateProject reshared_projects = 2; + repeated RejoinProject rejoined_projects = 3; +} + +message RejoinProject { + uint64 id = 1; + repeated RejoinWorktree worktrees = 2; +} + +message RejoinWorktree { + uint64 id = 1; + uint64 scan_id = 2; +} + +message RejoinRoomResponse { + Room room = 1; + repeated ResharedProject reshared_projects = 2; + repeated RejoinedProject rejoined_projects = 3; +} + +message ResharedProject { + uint64 id = 1; + repeated Collaborator collaborators = 2; +} + +message RejoinedProject { + uint64 id = 1; + repeated WorktreeMetadata worktrees = 2; + repeated Collaborator collaborators = 3; + repeated LanguageServer language_servers = 4; +} + +message LeaveRoom {} + +message Room { + uint64 id = 1; + repeated Participant participants = 2; + repeated PendingParticipant pending_participants = 3; + repeated Follower followers = 4; + string live_kit_room = 5; +} + +message Participant { + uint64 user_id = 1; + PeerId peer_id = 2; + repeated ParticipantProject projects = 3; + ParticipantLocation location = 4; + uint32 participant_index = 5; +} + +message PendingParticipant { + uint64 user_id = 1; + uint64 calling_user_id = 2; + optional uint64 initial_project_id = 3; +} + +message ParticipantProject { + uint64 id = 1; + repeated string worktree_root_names = 2; +} + +message Follower { + PeerId leader_id = 1; + PeerId follower_id = 2; + uint64 project_id = 3; +} + +message ParticipantLocation { + oneof variant { + SharedProject shared_project = 1; + UnsharedProject unshared_project = 2; + External external = 3; + } + + message SharedProject { + uint64 id = 1; + } + + message UnsharedProject {} + + message External {} +} + +message Call { + uint64 room_id = 1; + uint64 called_user_id = 2; + optional uint64 initial_project_id = 3; +} + +message IncomingCall { + uint64 room_id = 1; + uint64 calling_user_id = 2; + repeated uint64 participant_user_ids = 3; + optional ParticipantProject initial_project = 4; +} + +message CallCanceled { + uint64 room_id = 1; +} + +message CancelCall { + uint64 room_id = 1; + uint64 called_user_id = 2; +} + +message DeclineCall { + uint64 room_id = 1; +} + +message UpdateParticipantLocation { + uint64 room_id = 1; + ParticipantLocation location = 2; +} + +message RoomUpdated { + Room room = 1; +} + +message LiveKitConnectionInfo { + string server_url = 1; + string token = 2; +} + +message ShareProject { + uint64 room_id = 1; + repeated WorktreeMetadata worktrees = 2; +} + +message ShareProjectResponse { + uint64 project_id = 1; +} + +message UnshareProject { + uint64 project_id = 1; +} + +message UpdateProject { + uint64 project_id = 1; + repeated WorktreeMetadata worktrees = 2; +} + +message JoinProject { + uint64 project_id = 1; +} + +message JoinProjectResponse { + uint32 replica_id = 1; + repeated WorktreeMetadata worktrees = 2; + repeated Collaborator collaborators = 3; + repeated LanguageServer language_servers = 4; +} + +message LeaveProject { + uint64 project_id = 1; +} + +message UpdateWorktree { + uint64 project_id = 1; + uint64 worktree_id = 2; + string root_name = 3; + repeated Entry updated_entries = 4; + repeated uint64 removed_entries = 5; + repeated RepositoryEntry updated_repositories = 6; + repeated uint64 removed_repositories = 7; + uint64 scan_id = 8; + bool is_last_update = 9; + string abs_path = 10; +} + +message UpdateWorktreeSettings { + uint64 project_id = 1; + uint64 worktree_id = 2; + string path = 3; + optional string content = 4; +} + +message CreateProjectEntry { + uint64 project_id = 1; + uint64 worktree_id = 2; + string path = 3; + bool is_directory = 4; +} + +message RenameProjectEntry { + uint64 project_id = 1; + uint64 entry_id = 2; + string new_path = 3; +} + +message CopyProjectEntry { + uint64 project_id = 1; + uint64 entry_id = 2; + string new_path = 3; +} + +message DeleteProjectEntry { + uint64 project_id = 1; + uint64 entry_id = 2; +} + +message ExpandProjectEntry { + uint64 project_id = 1; + uint64 entry_id = 2; +} + +message ExpandProjectEntryResponse { + uint64 worktree_scan_id = 1; +} + +message ProjectEntryResponse { + Entry entry = 1; + uint64 worktree_scan_id = 2; +} + +message AddProjectCollaborator { + uint64 project_id = 1; + Collaborator collaborator = 2; +} + +message UpdateProjectCollaborator { + uint64 project_id = 1; + PeerId old_peer_id = 2; + PeerId new_peer_id = 3; +} + +message RemoveProjectCollaborator { + uint64 project_id = 1; + PeerId peer_id = 2; +} + +message UpdateChannelBufferCollaborators { + uint64 channel_id = 1; + repeated Collaborator collaborators = 2; +} + +message GetDefinition { + uint64 project_id = 1; + uint64 buffer_id = 2; + Anchor position = 3; + repeated VectorClockEntry version = 4; + } + +message GetDefinitionResponse { + repeated LocationLink links = 1; +} + +message GetTypeDefinition { + uint64 project_id = 1; + uint64 buffer_id = 2; + Anchor position = 3; + repeated VectorClockEntry version = 4; + } + +message GetTypeDefinitionResponse { + repeated LocationLink links = 1; +} + +message GetReferences { + uint64 project_id = 1; + uint64 buffer_id = 2; + Anchor position = 3; + repeated VectorClockEntry version = 4; + } + +message GetReferencesResponse { + repeated Location locations = 1; +} + +message GetDocumentHighlights { + uint64 project_id = 1; + uint64 buffer_id = 2; + Anchor position = 3; + repeated VectorClockEntry version = 4; + } + +message GetDocumentHighlightsResponse { + repeated DocumentHighlight highlights = 1; +} + +message Location { + uint64 buffer_id = 1; + Anchor start = 2; + Anchor end = 3; +} + +message LocationLink { + optional Location origin = 1; + Location target = 2; +} + +message DocumentHighlight { + Kind kind = 1; + Anchor start = 2; + Anchor end = 3; + + enum Kind { + Text = 0; + Read = 1; + Write = 2; + } +} + +message GetProjectSymbols { + uint64 project_id = 1; + string query = 2; +} + +message GetProjectSymbolsResponse { + repeated Symbol symbols = 4; +} + +message Symbol { + uint64 source_worktree_id = 1; + uint64 worktree_id = 2; + string language_server_name = 3; + string name = 4; + int32 kind = 5; + string path = 6; + // Cannot use generate anchors for unopened files, + // so we are forced to use point coords instead + PointUtf16 start = 7; + PointUtf16 end = 8; + bytes signature = 9; +} + +message OpenBufferForSymbol { + uint64 project_id = 1; + Symbol symbol = 2; +} + +message OpenBufferForSymbolResponse { + uint64 buffer_id = 1; +} + +message OpenBufferByPath { + uint64 project_id = 1; + uint64 worktree_id = 2; + string path = 3; +} + +message OpenBufferById { + uint64 project_id = 1; + uint64 id = 2; +} + +message OpenBufferResponse { + uint64 buffer_id = 1; +} + +message CreateBufferForPeer { + uint64 project_id = 1; + PeerId peer_id = 2; + oneof variant { + BufferState state = 3; + BufferChunk chunk = 4; + } +} + +message UpdateBuffer { + uint64 project_id = 1; + uint64 buffer_id = 2; + repeated Operation operations = 3; +} + +message UpdateChannelBuffer { + uint64 channel_id = 1; + repeated Operation operations = 2; +} + +message UpdateBufferFile { + uint64 project_id = 1; + uint64 buffer_id = 2; + File file = 3; +} + +message SaveBuffer { + uint64 project_id = 1; + uint64 buffer_id = 2; + repeated VectorClockEntry version = 3; +} + +message BufferSaved { + uint64 project_id = 1; + uint64 buffer_id = 2; + repeated VectorClockEntry version = 3; + Timestamp mtime = 4; + string fingerprint = 5; +} + +message BufferReloaded { + uint64 project_id = 1; + uint64 buffer_id = 2; + repeated VectorClockEntry version = 3; + Timestamp mtime = 4; + string fingerprint = 5; + LineEnding line_ending = 6; +} + +message ReloadBuffers { + uint64 project_id = 1; + repeated uint64 buffer_ids = 2; +} + +message ReloadBuffersResponse { + ProjectTransaction transaction = 1; +} + +message SynchronizeBuffers { + uint64 project_id = 1; + repeated BufferVersion buffers = 2; +} + +message SynchronizeBuffersResponse { + repeated BufferVersion buffers = 1; +} + +message BufferVersion { + uint64 id = 1; + repeated VectorClockEntry version = 2; +} + +message ChannelBufferVersion { + uint64 channel_id = 1; + repeated VectorClockEntry version = 2; + uint64 epoch = 3; +} + +enum FormatTrigger { + Save = 0; + Manual = 1; +} + +message FormatBuffers { + uint64 project_id = 1; + FormatTrigger trigger = 2; + repeated uint64 buffer_ids = 3; +} + +message FormatBuffersResponse { + ProjectTransaction transaction = 1; +} + +message GetCompletions { + uint64 project_id = 1; + uint64 buffer_id = 2; + Anchor position = 3; + repeated VectorClockEntry version = 4; +} + +message GetCompletionsResponse { + repeated Completion completions = 1; + repeated VectorClockEntry version = 2; +} + +message ApplyCompletionAdditionalEdits { + uint64 project_id = 1; + uint64 buffer_id = 2; + Completion completion = 3; +} + +message ApplyCompletionAdditionalEditsResponse { + Transaction transaction = 1; +} + +message Completion { + Anchor old_start = 1; + Anchor old_end = 2; + string new_text = 3; + uint64 server_id = 4; + bytes lsp_completion = 5; +} + +message GetCodeActions { + uint64 project_id = 1; + uint64 buffer_id = 2; + Anchor start = 3; + Anchor end = 4; + repeated VectorClockEntry version = 5; +} + +message GetCodeActionsResponse { + repeated CodeAction actions = 1; + repeated VectorClockEntry version = 2; +} + +message GetHover { + uint64 project_id = 1; + uint64 buffer_id = 2; + Anchor position = 3; + repeated VectorClockEntry version = 5; +} + +message GetHoverResponse { + optional Anchor start = 1; + optional Anchor end = 2; + repeated HoverBlock contents = 3; +} + +message HoverBlock { + string text = 1; + optional string language = 2; + bool is_markdown = 3; +} + +message ApplyCodeAction { + uint64 project_id = 1; + uint64 buffer_id = 2; + CodeAction action = 3; +} + +message ApplyCodeActionResponse { + ProjectTransaction transaction = 1; +} + +message PrepareRename { + uint64 project_id = 1; + uint64 buffer_id = 2; + Anchor position = 3; + repeated VectorClockEntry version = 4; +} + +message PrepareRenameResponse { + bool can_rename = 1; + Anchor start = 2; + Anchor end = 3; + repeated VectorClockEntry version = 4; +} + +message PerformRename { + uint64 project_id = 1; + uint64 buffer_id = 2; + Anchor position = 3; + string new_name = 4; + repeated VectorClockEntry version = 5; +} + +message OnTypeFormatting { + uint64 project_id = 1; + uint64 buffer_id = 2; + Anchor position = 3; + string trigger = 4; + repeated VectorClockEntry version = 5; +} + +message OnTypeFormattingResponse { + Transaction transaction = 1; +} + +message InlayHints { + uint64 project_id = 1; + uint64 buffer_id = 2; + Anchor start = 3; + Anchor end = 4; + repeated VectorClockEntry version = 5; +} + +message InlayHintsResponse { + repeated InlayHint hints = 1; + repeated VectorClockEntry version = 2; +} + +message InlayHint { + Anchor position = 1; + InlayHintLabel label = 2; + optional string kind = 3; + bool padding_left = 4; + bool padding_right = 5; + InlayHintTooltip tooltip = 6; + ResolveState resolve_state = 7; +} + +message InlayHintLabel { + oneof label { + string value = 1; + InlayHintLabelParts label_parts = 2; + } +} + +message InlayHintLabelParts { + repeated InlayHintLabelPart parts = 1; +} + +message InlayHintLabelPart { + string value = 1; + InlayHintLabelPartTooltip tooltip = 2; + optional string location_url = 3; + PointUtf16 location_range_start = 4; + PointUtf16 location_range_end = 5; + optional uint64 language_server_id = 6; +} + +message InlayHintTooltip { + oneof content { + string value = 1; + MarkupContent markup_content = 2; + } +} + +message InlayHintLabelPartTooltip { + oneof content { + string value = 1; + MarkupContent markup_content = 2; + } +} + +message ResolveState { + State state = 1; + LspResolveState lsp_resolve_state = 2; + + enum State { + Resolved = 0; + CanResolve = 1; + Resolving = 2; + } + + message LspResolveState { + string value = 1; + uint64 server_id = 2; + } +} + +message ResolveInlayHint { + uint64 project_id = 1; + uint64 buffer_id = 2; + uint64 language_server_id = 3; + InlayHint hint = 4; +} + +message ResolveInlayHintResponse { + InlayHint hint = 1; +} + +message RefreshInlayHints { + uint64 project_id = 1; +} + +message MarkupContent { + bool is_markdown = 1; + string value = 2; +} + +message PerformRenameResponse { + ProjectTransaction transaction = 2; +} + +message SearchProject { + uint64 project_id = 1; + string query = 2; + bool regex = 3; + bool whole_word = 4; + bool case_sensitive = 5; + string files_to_include = 6; + string files_to_exclude = 7; +} + +message SearchProjectResponse { + repeated Location locations = 1; +} + +message CodeAction { + uint64 server_id = 1; + Anchor start = 2; + Anchor end = 3; + bytes lsp_action = 4; +} + +message ProjectTransaction { + repeated uint64 buffer_ids = 1; + repeated Transaction transactions = 2; +} + +message Transaction { + LamportTimestamp id = 1; + repeated LamportTimestamp edit_ids = 2; + repeated VectorClockEntry start = 3; +} + +message LamportTimestamp { + uint32 replica_id = 1; + uint32 value = 2; +} + +message LanguageServer { + uint64 id = 1; + string name = 2; +} + +message StartLanguageServer { + uint64 project_id = 1; + LanguageServer server = 2; +} + +message UpdateDiagnosticSummary { + uint64 project_id = 1; + uint64 worktree_id = 2; + DiagnosticSummary summary = 3; +} + +message DiagnosticSummary { + string path = 1; + uint64 language_server_id = 2; + uint32 error_count = 3; + uint32 warning_count = 4; +} + +message UpdateLanguageServer { + uint64 project_id = 1; + uint64 language_server_id = 2; + oneof variant { + LspWorkStart work_start = 3; + LspWorkProgress work_progress = 4; + LspWorkEnd work_end = 5; + LspDiskBasedDiagnosticsUpdating disk_based_diagnostics_updating = 6; + LspDiskBasedDiagnosticsUpdated disk_based_diagnostics_updated = 7; + } +} + +message LspWorkStart { + string token = 1; + optional string message = 2; + optional uint32 percentage = 3; +} + +message LspWorkProgress { + string token = 1; + optional string message = 2; + optional uint32 percentage = 3; +} + +message LspWorkEnd { + string token = 1; +} + +message LspDiskBasedDiagnosticsUpdating {} + +message LspDiskBasedDiagnosticsUpdated {} + +message UpdateChannels { + repeated Channel channels = 1; + repeated ChannelEdge insert_edge = 2; + repeated ChannelEdge delete_edge = 3; + repeated uint64 delete_channels = 4; + repeated Channel channel_invitations = 5; + repeated uint64 remove_channel_invitations = 6; + repeated ChannelParticipants channel_participants = 7; + repeated ChannelPermission channel_permissions = 8; + repeated UnseenChannelMessage unseen_channel_messages = 9; + repeated UnseenChannelBufferChange unseen_channel_buffer_changes = 10; +} + +message UnseenChannelMessage { + uint64 channel_id = 1; + uint64 message_id = 2; +} + +message UnseenChannelBufferChange { + uint64 channel_id = 1; + uint64 epoch = 2; + repeated VectorClockEntry version = 3; +} + +message ChannelEdge { + uint64 channel_id = 1; + uint64 parent_id = 2; +} + +message ChannelPermission { + uint64 channel_id = 1; + bool is_admin = 2; +} + +message ChannelParticipants { + uint64 channel_id = 1; + repeated uint64 participant_user_ids = 2; +} + +message JoinChannel { + uint64 channel_id = 1; +} + +message DeleteChannel { + uint64 channel_id = 1; +} + +message GetChannelMembers { + uint64 channel_id = 1; +} + +message GetChannelMembersResponse { + repeated ChannelMember members = 1; +} + +message ChannelMember { + uint64 user_id = 1; + bool admin = 2; + Kind kind = 3; + + enum Kind { + Member = 0; + Invitee = 1; + AncestorMember = 2; + } +} + +message CreateChannel { + string name = 1; + optional uint64 parent_id = 2; +} + +message CreateChannelResponse { + Channel channel = 1; + optional uint64 parent_id = 2; +} + +message InviteChannelMember { + uint64 channel_id = 1; + uint64 user_id = 2; + bool admin = 3; +} + +message RemoveChannelMember { + uint64 channel_id = 1; + uint64 user_id = 2; +} + +message SetChannelMemberAdmin { + uint64 channel_id = 1; + uint64 user_id = 2; + bool admin = 3; +} + +message RenameChannel { + uint64 channel_id = 1; + string name = 2; +} + +message RenameChannelResponse { + Channel channel = 1; +} + +message JoinChannelChat { + uint64 channel_id = 1; +} + +message JoinChannelChatResponse { + repeated ChannelMessage messages = 1; + bool done = 2; +} + +message LeaveChannelChat { + uint64 channel_id = 1; +} + +message SendChannelMessage { + uint64 channel_id = 1; + string body = 2; + Nonce nonce = 3; +} + +message RemoveChannelMessage { + uint64 channel_id = 1; + uint64 message_id = 2; +} + +message AckChannelMessage { + uint64 channel_id = 1; + uint64 message_id = 2; +} + +message SendChannelMessageResponse { + ChannelMessage message = 1; +} + +message ChannelMessageSent { + uint64 channel_id = 1; + ChannelMessage message = 2; +} + +message GetChannelMessages { + uint64 channel_id = 1; + uint64 before_message_id = 2; +} + +message GetChannelMessagesResponse { + repeated ChannelMessage messages = 1; + bool done = 2; +} + +message LinkChannel { + uint64 channel_id = 1; + uint64 to = 2; +} + +message UnlinkChannel { + uint64 channel_id = 1; + uint64 from = 2; +} + +message MoveChannel { + uint64 channel_id = 1; + uint64 from = 2; + uint64 to = 3; +} + +message JoinChannelBuffer { + uint64 channel_id = 1; +} + +message ChannelMessage { + uint64 id = 1; + string body = 2; + uint64 timestamp = 3; + uint64 sender_id = 4; + Nonce nonce = 5; +} + +message RejoinChannelBuffers { + repeated ChannelBufferVersion buffers = 1; +} + +message RejoinChannelBuffersResponse { + repeated RejoinedChannelBuffer buffers = 1; +} + +message AckBufferOperation { + uint64 buffer_id = 1; + uint64 epoch = 2; + repeated VectorClockEntry version = 3; +} + +message JoinChannelBufferResponse { + uint64 buffer_id = 1; + uint32 replica_id = 2; + string base_text = 3; + repeated Operation operations = 4; + repeated Collaborator collaborators = 5; + uint64 epoch = 6; +} + +message RejoinedChannelBuffer { + uint64 channel_id = 1; + repeated VectorClockEntry version = 2; + repeated Operation operations = 3; + repeated Collaborator collaborators = 4; +} + +message LeaveChannelBuffer { + uint64 channel_id = 1; +} + +message RespondToChannelInvite { + uint64 channel_id = 1; + bool accept = 2; +} + +message GetUsers { + repeated uint64 user_ids = 1; +} + +message FuzzySearchUsers { + string query = 1; +} + +message UsersResponse { + repeated User users = 1; +} + +message RequestContact { + uint64 responder_id = 1; +} + +message RemoveContact { + uint64 user_id = 1; +} + +message RespondToContactRequest { + uint64 requester_id = 1; + ContactRequestResponse response = 2; +} + +enum ContactRequestResponse { + Accept = 0; + Decline = 1; + Block = 2; + Dismiss = 3; +} + +message UpdateContacts { + repeated Contact contacts = 1; + repeated uint64 remove_contacts = 2; + repeated IncomingContactRequest incoming_requests = 3; + repeated uint64 remove_incoming_requests = 4; + repeated uint64 outgoing_requests = 5; + repeated uint64 remove_outgoing_requests = 6; +} + +message UpdateInviteInfo { + string url = 1; + uint32 count = 2; +} + +message ShowContacts {} + +message IncomingContactRequest { + uint64 requester_id = 1; + bool should_notify = 2; +} + +message UpdateDiagnostics { + uint32 replica_id = 1; + uint32 lamport_timestamp = 2; + uint64 server_id = 3; + repeated Diagnostic diagnostics = 4; +} + +message Follow { + uint64 room_id = 1; + optional uint64 project_id = 2; + PeerId leader_id = 3; +} + +message FollowResponse { + optional ViewId active_view_id = 1; + repeated View views = 2; +} + +message UpdateFollowers { + uint64 room_id = 1; + optional uint64 project_id = 2; + repeated PeerId follower_ids = 3; + oneof variant { + UpdateActiveView update_active_view = 4; + View create_view = 5; + UpdateView update_view = 6; + } +} + +message Unfollow { + uint64 room_id = 1; + optional uint64 project_id = 2; + PeerId leader_id = 3; +} + +message GetPrivateUserInfo {} + +message GetPrivateUserInfoResponse { + string metrics_id = 1; + bool staff = 2; + repeated string flags = 3; +} + +// Entities + +message ViewId { + PeerId creator = 1; + uint64 id = 2; +} + +message UpdateActiveView { + optional ViewId id = 1; + optional PeerId leader_id = 2; +} + +message UpdateView { + ViewId id = 1; + optional PeerId leader_id = 2; + + oneof variant { + Editor editor = 3; + } + + message Editor { + repeated ExcerptInsertion inserted_excerpts = 1; + repeated uint64 deleted_excerpts = 2; + repeated Selection selections = 3; + optional Selection pending_selection = 4; + EditorAnchor scroll_top_anchor = 5; + float scroll_x = 6; + float scroll_y = 7; + } +} + +message View { + ViewId id = 1; + optional PeerId leader_id = 2; + + oneof variant { + Editor editor = 3; + ChannelView channel_view = 4; + } + + message Editor { + bool singleton = 1; + optional string title = 2; + repeated Excerpt excerpts = 3; + repeated Selection selections = 4; + optional Selection pending_selection = 5; + EditorAnchor scroll_top_anchor = 6; + float scroll_x = 7; + float scroll_y = 8; + } + + message ChannelView { + uint64 channel_id = 1; + Editor editor = 2; + } +} + +message Collaborator { + PeerId peer_id = 1; + uint32 replica_id = 2; + uint64 user_id = 3; +} + +message User { + uint64 id = 1; + string github_login = 2; + string avatar_url = 3; +} + +message File { + uint64 worktree_id = 1; + uint64 entry_id = 2; + string path = 3; + Timestamp mtime = 4; + bool is_deleted = 5; +} + +message Entry { + uint64 id = 1; + bool is_dir = 2; + string path = 3; + uint64 inode = 4; + Timestamp mtime = 5; + bool is_symlink = 6; + bool is_ignored = 7; + bool is_external = 8; + optional GitStatus git_status = 9; +} + +message RepositoryEntry { + uint64 work_directory_id = 1; + optional string branch = 2; +} + +message StatusEntry { + string repo_path = 1; + GitStatus status = 2; +} + +enum GitStatus { + Added = 0; + Modified = 1; + Conflict = 2; +} + +message BufferState { + uint64 id = 1; + optional File file = 2; + string base_text = 3; + optional string diff_base = 4; + LineEnding line_ending = 5; + repeated VectorClockEntry saved_version = 6; + string saved_version_fingerprint = 7; + Timestamp saved_mtime = 8; +} + +message BufferChunk { + uint64 buffer_id = 1; + repeated Operation operations = 2; + bool is_last = 3; +} + +enum LineEnding { + Unix = 0; + Windows = 1; +} + +message Selection { + uint64 id = 1; + EditorAnchor start = 2; + EditorAnchor end = 3; + bool reversed = 4; +} + +message EditorAnchor { + uint64 excerpt_id = 1; + Anchor anchor = 2; +} + +enum CursorShape { + CursorBar = 0; + CursorBlock = 1; + CursorUnderscore = 2; + CursorHollow = 3; +} + +message ExcerptInsertion { + Excerpt excerpt = 1; + optional uint64 previous_excerpt_id = 2; +} + +message Excerpt { + uint64 id = 1; + uint64 buffer_id = 2; + Anchor context_start = 3; + Anchor context_end = 4; + Anchor primary_start = 5; + Anchor primary_end = 6; +} + +message Anchor { + uint32 replica_id = 1; + uint32 timestamp = 2; + uint64 offset = 3; + Bias bias = 4; + optional uint64 buffer_id = 5; +} + +enum Bias { + Left = 0; + Right = 1; +} + +message Diagnostic { + Anchor start = 1; + Anchor end = 2; + optional string source = 3; + Severity severity = 4; + string message = 5; + optional string code = 6; + uint64 group_id = 7; + bool is_primary = 8; + bool is_valid = 9; + bool is_disk_based = 10; + bool is_unnecessary = 11; + + enum Severity { + None = 0; + Error = 1; + Warning = 2; + Information = 3; + Hint = 4; + } +} + +message Operation { + oneof variant { + Edit edit = 1; + Undo undo = 2; + UpdateSelections update_selections = 3; + UpdateDiagnostics update_diagnostics = 4; + UpdateCompletionTriggers update_completion_triggers = 5; + } + + message Edit { + uint32 replica_id = 1; + uint32 lamport_timestamp = 2; + repeated VectorClockEntry version = 3; + repeated Range ranges = 4; + repeated string new_text = 5; + } + + message Undo { + uint32 replica_id = 1; + uint32 lamport_timestamp = 2; + repeated VectorClockEntry version = 3; + repeated UndoCount counts = 4; + } + + message UpdateSelections { + uint32 replica_id = 1; + uint32 lamport_timestamp = 2; + repeated Selection selections = 3; + bool line_mode = 4; + CursorShape cursor_shape = 5; + } + + message UpdateCompletionTriggers { + uint32 replica_id = 1; + uint32 lamport_timestamp = 2; + repeated string triggers = 3; + } +} + +message UndoMapEntry { + uint32 replica_id = 1; + uint32 local_timestamp = 2; + repeated UndoCount counts = 3; +} + +message UndoCount { + uint32 replica_id = 1; + uint32 lamport_timestamp = 2; + uint32 count = 3; +} + +message VectorClockEntry { + uint32 replica_id = 1; + uint32 timestamp = 2; +} + +message Timestamp { + uint64 seconds = 1; + uint32 nanos = 2; +} + +message Range { + uint64 start = 1; + uint64 end = 2; +} + +message PointUtf16 { + uint32 row = 1; + uint32 column = 2; +} + +message Nonce { + uint64 upper_half = 1; + uint64 lower_half = 2; +} + +message Channel { + uint64 id = 1; + string name = 2; +} + +message Contact { + uint64 user_id = 1; + bool online = 2; + bool busy = 3; + bool should_notify = 4; +} + +message WorktreeMetadata { + uint64 id = 1; + string root_name = 2; + bool visible = 3; + string abs_path = 4; +} + +message UpdateDiffBase { + uint64 project_id = 1; + uint64 buffer_id = 2; + optional string diff_base = 3; +} diff --git a/crates/rpc2/src/auth.rs b/crates/rpc2/src/auth.rs new file mode 100644 index 0000000000..ac7bbcebec --- /dev/null +++ b/crates/rpc2/src/auth.rs @@ -0,0 +1,136 @@ +use anyhow::{Context, Result}; +use rand::{thread_rng, Rng as _}; +use rsa::{PublicKey as _, PublicKeyEncoding, RSAPrivateKey, RSAPublicKey}; +use std::convert::TryFrom; + +pub struct PublicKey(RSAPublicKey); + +pub struct PrivateKey(RSAPrivateKey); + +/// Generate a public and private key for asymmetric encryption. +pub fn keypair() -> Result<(PublicKey, PrivateKey)> { + let mut rng = thread_rng(); + let bits = 1024; + let private_key = RSAPrivateKey::new(&mut rng, bits)?; + let public_key = RSAPublicKey::from(&private_key); + Ok((PublicKey(public_key), PrivateKey(private_key))) +} + +/// Generate a random 64-character base64 string. +pub fn random_token() -> String { + let mut rng = thread_rng(); + let mut token_bytes = [0; 48]; + for byte in token_bytes.iter_mut() { + *byte = rng.gen(); + } + base64::encode_config(token_bytes, base64::URL_SAFE) +} + +impl PublicKey { + /// Convert a string to a base64-encoded string that can only be decoded with the corresponding + /// private key. + pub fn encrypt_string(&self, string: &str) -> Result { + let mut rng = thread_rng(); + let bytes = string.as_bytes(); + let encrypted_bytes = self + .0 + .encrypt(&mut rng, PADDING_SCHEME, bytes) + .context("failed to encrypt string with public key")?; + let encrypted_string = base64::encode_config(&encrypted_bytes, base64::URL_SAFE); + Ok(encrypted_string) + } +} + +impl PrivateKey { + /// Decrypt a base64-encoded string that was encrypted by the corresponding public key. + pub fn decrypt_string(&self, encrypted_string: &str) -> Result { + let encrypted_bytes = base64::decode_config(encrypted_string, base64::URL_SAFE) + .context("failed to base64-decode encrypted string")?; + let bytes = self + .0 + .decrypt(PADDING_SCHEME, &encrypted_bytes) + .context("failed to decrypt string with private key")?; + let string = String::from_utf8(bytes).context("decrypted content was not valid utf8")?; + Ok(string) + } +} + +impl TryFrom for String { + type Error = anyhow::Error; + fn try_from(key: PublicKey) -> Result { + let bytes = key.0.to_pkcs1().context("failed to serialize public key")?; + let string = base64::encode_config(&bytes, base64::URL_SAFE); + Ok(string) + } +} + +impl TryFrom for PublicKey { + type Error = anyhow::Error; + fn try_from(value: String) -> Result { + let bytes = base64::decode_config(&value, base64::URL_SAFE) + .context("failed to base64-decode public key string")?; + let key = Self(RSAPublicKey::from_pkcs1(&bytes).context("failed to parse public key")?); + Ok(key) + } +} + +const PADDING_SCHEME: rsa::PaddingScheme = rsa::PaddingScheme::PKCS1v15Encrypt; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_generate_encrypt_and_decrypt_token() { + // CLIENT: + // * generate a keypair for asymmetric encryption + // * serialize the public key to send it to the server. + let (public, private) = keypair().unwrap(); + let public_string = String::try_from(public).unwrap(); + assert_printable(&public_string); + + // SERVER: + // * parse the public key + // * generate a random token. + // * encrypt the token using the public key. + let public = PublicKey::try_from(public_string).unwrap(); + let token = random_token(); + let encrypted_token = public.encrypt_string(&token).unwrap(); + assert_eq!(token.len(), 64); + assert_ne!(encrypted_token, token); + assert_printable(&token); + assert_printable(&encrypted_token); + + // CLIENT: + // * decrypt the token using the private key. + let decrypted_token = private.decrypt_string(&encrypted_token).unwrap(); + assert_eq!(decrypted_token, token); + } + + #[test] + fn test_tokens_are_always_url_safe() { + for _ in 0..5 { + let token = random_token(); + let (public_key, _) = keypair().unwrap(); + let encrypted_token = public_key.encrypt_string(&token).unwrap(); + let public_key_str = String::try_from(public_key).unwrap(); + + assert_printable(&token); + assert_printable(&public_key_str); + assert_printable(&encrypted_token); + } + } + + fn assert_printable(token: &str) { + for c in token.chars() { + assert!( + c.is_ascii_graphic(), + "token {:?} has non-printable char {}", + token, + c + ); + assert_ne!(c, '/', "token {:?} is not URL-safe", token); + assert_ne!(c, '&', "token {:?} is not URL-safe", token); + } + } +} diff --git a/crates/rpc2/src/conn.rs b/crates/rpc2/src/conn.rs new file mode 100644 index 0000000000..902e9822d5 --- /dev/null +++ b/crates/rpc2/src/conn.rs @@ -0,0 +1,108 @@ +use async_tungstenite::tungstenite::Message as WebSocketMessage; +use futures::{SinkExt as _, StreamExt as _}; + +pub struct Connection { + pub(crate) tx: + Box>, + pub(crate) rx: Box< + dyn 'static + + Send + + Unpin + + futures::Stream>, + >, +} + +impl Connection { + pub fn new(stream: S) -> Self + where + S: 'static + + Send + + Unpin + + futures::Sink + + futures::Stream>, + { + let (tx, rx) = stream.split(); + Self { + tx: Box::new(tx), + rx: Box::new(rx), + } + } + + pub async fn send(&mut self, message: WebSocketMessage) -> Result<(), anyhow::Error> { + self.tx.send(message).await + } + + #[cfg(any(test, feature = "test-support"))] + pub fn in_memory( + executor: gpui2::Executor, + ) -> (Self, Self, std::sync::Arc) { + use std::sync::{ + atomic::{AtomicBool, Ordering::SeqCst}, + Arc, + }; + + let killed = Arc::new(AtomicBool::new(false)); + let (a_tx, a_rx) = channel(killed.clone(), executor.clone()); + let (b_tx, b_rx) = channel(killed.clone(), executor); + return ( + Self { tx: a_tx, rx: b_rx }, + Self { tx: b_tx, rx: a_rx }, + killed, + ); + + #[allow(clippy::type_complexity)] + fn channel( + killed: Arc, + executor: gpui2::Executor, + ) -> ( + Box>, + Box>>, + ) { + use anyhow::anyhow; + use futures::channel::mpsc; + use std::io::{Error, ErrorKind}; + + let (tx, rx) = mpsc::unbounded::(); + + let tx = tx.sink_map_err(|error| anyhow!(error)).with({ + let killed = killed.clone(); + let executor = executor.clone(); + move |msg| { + let killed = killed.clone(); + let executor = executor.clone(); + Box::pin(async move { + executor.simulate_random_delay().await; + + // Writes to a half-open TCP connection will error. + if killed.load(SeqCst) { + std::io::Result::Err(Error::new(ErrorKind::Other, "connection lost"))?; + } + + Ok(msg) + }) + } + }); + + let rx = rx.then({ + let killed = killed; + let executor = executor.clone(); + move |msg| { + let killed = killed.clone(); + let executor = executor.clone(); + Box::pin(async move { + executor.simulate_random_delay().await; + + // Reads from a half-open TCP connection will hang. + if killed.load(SeqCst) { + futures::future::pending::<()>().await; + } + + Ok(msg) + }) + } + }); + + (Box::new(tx), Box::new(rx)) + } + } +} diff --git a/crates/rpc2/src/macros.rs b/crates/rpc2/src/macros.rs new file mode 100644 index 0000000000..89e605540d --- /dev/null +++ b/crates/rpc2/src/macros.rs @@ -0,0 +1,70 @@ +#[macro_export] +macro_rules! messages { + ($(($name:ident, $priority:ident)),* $(,)?) => { + pub fn build_typed_envelope(sender_id: ConnectionId, envelope: Envelope) -> Option> { + match envelope.payload { + $(Some(envelope::Payload::$name(payload)) => { + Some(Box::new(TypedEnvelope { + sender_id, + original_sender_id: envelope.original_sender_id.map(|original_sender| PeerId { + owner_id: original_sender.owner_id, + id: original_sender.id + }), + message_id: envelope.id, + payload, + })) + }, )* + _ => None + } + } + + $( + impl EnvelopedMessage for $name { + const NAME: &'static str = std::stringify!($name); + const PRIORITY: MessagePriority = MessagePriority::$priority; + + fn into_envelope( + self, + id: u32, + responding_to: Option, + original_sender_id: Option, + ) -> Envelope { + Envelope { + id, + responding_to, + original_sender_id, + payload: Some(envelope::Payload::$name(self)), + } + } + + fn from_envelope(envelope: Envelope) -> Option { + if let Some(envelope::Payload::$name(msg)) = envelope.payload { + Some(msg) + } else { + None + } + } + } + )* + }; +} + +#[macro_export] +macro_rules! request_messages { + ($(($request_name:ident, $response_name:ident)),* $(,)?) => { + $(impl RequestMessage for $request_name { + type Response = $response_name; + })* + }; +} + +#[macro_export] +macro_rules! entity_messages { + ($id_field:ident, $($name:ident),* $(,)?) => { + $(impl EntityMessage for $name { + fn remote_entity_id(&self) -> u64 { + self.$id_field + } + })* + }; +} diff --git a/crates/rpc2/src/peer.rs b/crates/rpc2/src/peer.rs new file mode 100644 index 0000000000..6dfb170f4c --- /dev/null +++ b/crates/rpc2/src/peer.rs @@ -0,0 +1,933 @@ +use super::{ + proto::{self, AnyTypedEnvelope, EnvelopedMessage, MessageStream, PeerId, RequestMessage}, + Connection, +}; +use anyhow::{anyhow, Context, Result}; +use collections::HashMap; +use futures::{ + channel::{mpsc, oneshot}, + stream::BoxStream, + FutureExt, SinkExt, StreamExt, TryFutureExt, +}; +use parking_lot::{Mutex, RwLock}; +use serde::{ser::SerializeStruct, Serialize}; +use std::{fmt, sync::atomic::Ordering::SeqCst}; +use std::{ + future::Future, + marker::PhantomData, + sync::{ + atomic::{self, AtomicU32}, + Arc, + }, + time::Duration, +}; +use tracing::instrument; + +#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize)] +pub struct ConnectionId { + pub owner_id: u32, + pub id: u32, +} + +impl Into for ConnectionId { + fn into(self) -> PeerId { + PeerId { + owner_id: self.owner_id, + id: self.id, + } + } +} + +impl From for ConnectionId { + fn from(peer_id: PeerId) -> Self { + Self { + owner_id: peer_id.owner_id, + id: peer_id.id, + } + } +} + +impl fmt::Display for ConnectionId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}/{}", self.owner_id, self.id) + } +} + +pub struct Receipt { + pub sender_id: ConnectionId, + pub message_id: u32, + payload_type: PhantomData, +} + +impl Clone for Receipt { + fn clone(&self) -> Self { + Self { + sender_id: self.sender_id, + message_id: self.message_id, + payload_type: PhantomData, + } + } +} + +impl Copy for Receipt {} + +#[derive(Clone, Debug)] +pub struct TypedEnvelope { + pub sender_id: ConnectionId, + pub original_sender_id: Option, + pub message_id: u32, + pub payload: T, +} + +impl TypedEnvelope { + pub fn original_sender_id(&self) -> Result { + self.original_sender_id + .ok_or_else(|| anyhow!("missing original_sender_id")) + } +} + +impl TypedEnvelope { + pub fn receipt(&self) -> Receipt { + Receipt { + sender_id: self.sender_id, + message_id: self.message_id, + payload_type: PhantomData, + } + } +} + +pub struct Peer { + epoch: AtomicU32, + pub connections: RwLock>, + next_connection_id: AtomicU32, +} + +#[derive(Clone, Serialize)] +pub struct ConnectionState { + #[serde(skip)] + outgoing_tx: mpsc::UnboundedSender, + next_message_id: Arc, + #[allow(clippy::type_complexity)] + #[serde(skip)] + response_channels: + Arc)>>>>>, +} + +const KEEPALIVE_INTERVAL: Duration = Duration::from_secs(1); +const WRITE_TIMEOUT: Duration = Duration::from_secs(2); +pub const RECEIVE_TIMEOUT: Duration = Duration::from_secs(10); + +impl Peer { + pub fn new(epoch: u32) -> Arc { + Arc::new(Self { + epoch: AtomicU32::new(epoch), + connections: Default::default(), + next_connection_id: Default::default(), + }) + } + + pub fn epoch(&self) -> u32 { + self.epoch.load(SeqCst) + } + + #[instrument(skip_all)] + pub fn add_connection( + self: &Arc, + connection: Connection, + create_timer: F, + ) -> ( + ConnectionId, + impl Future> + Send, + BoxStream<'static, Box>, + ) + where + F: Send + Fn(Duration) -> Fut, + Fut: Send + Future, + Out: Send, + { + // For outgoing messages, use an unbounded channel so that application code + // can always send messages without yielding. For incoming messages, use a + // bounded channel so that other peers will receive backpressure if they send + // messages faster than this peer can process them. + #[cfg(any(test, feature = "test-support"))] + const INCOMING_BUFFER_SIZE: usize = 1; + #[cfg(not(any(test, feature = "test-support")))] + const INCOMING_BUFFER_SIZE: usize = 64; + let (mut incoming_tx, incoming_rx) = mpsc::channel(INCOMING_BUFFER_SIZE); + let (outgoing_tx, mut outgoing_rx) = mpsc::unbounded(); + + let connection_id = ConnectionId { + owner_id: self.epoch.load(SeqCst), + id: self.next_connection_id.fetch_add(1, SeqCst), + }; + let connection_state = ConnectionState { + outgoing_tx, + next_message_id: Default::default(), + response_channels: Arc::new(Mutex::new(Some(Default::default()))), + }; + let mut writer = MessageStream::new(connection.tx); + let mut reader = MessageStream::new(connection.rx); + + let this = self.clone(); + let response_channels = connection_state.response_channels.clone(); + let handle_io = async move { + tracing::trace!(%connection_id, "handle io future: start"); + + let _end_connection = util::defer(|| { + response_channels.lock().take(); + this.connections.write().remove(&connection_id); + tracing::trace!(%connection_id, "handle io future: end"); + }); + + // Send messages on this frequency so the connection isn't closed. + let keepalive_timer = create_timer(KEEPALIVE_INTERVAL).fuse(); + futures::pin_mut!(keepalive_timer); + + // Disconnect if we don't receive messages at least this frequently. + let receive_timeout = create_timer(RECEIVE_TIMEOUT).fuse(); + futures::pin_mut!(receive_timeout); + + loop { + tracing::trace!(%connection_id, "outer loop iteration start"); + let read_message = reader.read().fuse(); + futures::pin_mut!(read_message); + + loop { + tracing::trace!(%connection_id, "inner loop iteration start"); + futures::select_biased! { + outgoing = outgoing_rx.next().fuse() => match outgoing { + Some(outgoing) => { + tracing::trace!(%connection_id, "outgoing rpc message: writing"); + futures::select_biased! { + result = writer.write(outgoing).fuse() => { + tracing::trace!(%connection_id, "outgoing rpc message: done writing"); + result.context("failed to write RPC message")?; + tracing::trace!(%connection_id, "keepalive interval: resetting after sending message"); + keepalive_timer.set(create_timer(KEEPALIVE_INTERVAL).fuse()); + } + _ = create_timer(WRITE_TIMEOUT).fuse() => { + tracing::trace!(%connection_id, "outgoing rpc message: writing timed out"); + Err(anyhow!("timed out writing message"))?; + } + } + } + None => { + tracing::trace!(%connection_id, "outgoing rpc message: channel closed"); + return Ok(()) + }, + }, + _ = keepalive_timer => { + tracing::trace!(%connection_id, "keepalive interval: pinging"); + futures::select_biased! { + result = writer.write(proto::Message::Ping).fuse() => { + tracing::trace!(%connection_id, "keepalive interval: done pinging"); + result.context("failed to send keepalive")?; + tracing::trace!(%connection_id, "keepalive interval: resetting after pinging"); + keepalive_timer.set(create_timer(KEEPALIVE_INTERVAL).fuse()); + } + _ = create_timer(WRITE_TIMEOUT).fuse() => { + tracing::trace!(%connection_id, "keepalive interval: pinging timed out"); + Err(anyhow!("timed out sending keepalive"))?; + } + } + } + incoming = read_message => { + let incoming = incoming.context("error reading rpc message from socket")?; + tracing::trace!(%connection_id, "incoming rpc message: received"); + tracing::trace!(%connection_id, "receive timeout: resetting"); + receive_timeout.set(create_timer(RECEIVE_TIMEOUT).fuse()); + if let proto::Message::Envelope(incoming) = incoming { + tracing::trace!(%connection_id, "incoming rpc message: processing"); + futures::select_biased! { + result = incoming_tx.send(incoming).fuse() => match result { + Ok(_) => { + tracing::trace!(%connection_id, "incoming rpc message: processed"); + } + Err(_) => { + tracing::trace!(%connection_id, "incoming rpc message: channel closed"); + return Ok(()) + } + }, + _ = create_timer(WRITE_TIMEOUT).fuse() => { + tracing::trace!(%connection_id, "incoming rpc message: processing timed out"); + Err(anyhow!("timed out processing incoming message"))? + } + } + } + break; + }, + _ = receive_timeout => { + tracing::trace!(%connection_id, "receive timeout: delay between messages too long"); + Err(anyhow!("delay between messages too long"))? + } + } + } + } + }; + + let response_channels = connection_state.response_channels.clone(); + self.connections + .write() + .insert(connection_id, connection_state); + + let incoming_rx = incoming_rx.filter_map(move |incoming| { + let response_channels = response_channels.clone(); + async move { + let message_id = incoming.id; + tracing::trace!(?incoming, "incoming message future: start"); + let _end = util::defer(move || { + tracing::trace!(%connection_id, message_id, "incoming message future: end"); + }); + + if let Some(responding_to) = incoming.responding_to { + tracing::trace!( + %connection_id, + message_id, + responding_to, + "incoming response: received" + ); + let channel = response_channels.lock().as_mut()?.remove(&responding_to); + if let Some(tx) = channel { + let requester_resumed = oneshot::channel(); + if let Err(error) = tx.send((incoming, requester_resumed.0)) { + tracing::trace!( + %connection_id, + message_id, + responding_to = responding_to, + ?error, + "incoming response: request future dropped", + ); + } + + tracing::trace!( + %connection_id, + message_id, + responding_to, + "incoming response: waiting to resume requester" + ); + let _ = requester_resumed.1.await; + tracing::trace!( + %connection_id, + message_id, + responding_to, + "incoming response: requester resumed" + ); + } else { + tracing::warn!( + %connection_id, + message_id, + responding_to, + "incoming response: unknown request" + ); + } + + None + } else { + tracing::trace!(%connection_id, message_id, "incoming message: received"); + proto::build_typed_envelope(connection_id, incoming).or_else(|| { + tracing::error!( + %connection_id, + message_id, + "unable to construct a typed envelope" + ); + None + }) + } + } + }); + (connection_id, handle_io, incoming_rx.boxed()) + } + + #[cfg(any(test, feature = "test-support"))] + pub fn add_test_connection( + self: &Arc, + connection: Connection, + executor: gpui2::Executor, + ) -> ( + ConnectionId, + impl Future> + Send, + BoxStream<'static, Box>, + ) { + let executor = executor.clone(); + self.add_connection(connection, move |duration| executor.timer(duration)) + } + + pub fn disconnect(&self, connection_id: ConnectionId) { + self.connections.write().remove(&connection_id); + } + + pub fn reset(&self, epoch: u32) { + self.teardown(); + self.next_connection_id.store(0, SeqCst); + self.epoch.store(epoch, SeqCst); + } + + pub fn teardown(&self) { + self.connections.write().clear(); + } + + pub fn request( + &self, + receiver_id: ConnectionId, + request: T, + ) -> impl Future> { + self.request_internal(None, receiver_id, request) + .map_ok(|envelope| envelope.payload) + } + + pub fn request_envelope( + &self, + receiver_id: ConnectionId, + request: T, + ) -> impl Future>> { + self.request_internal(None, receiver_id, request) + } + + pub fn forward_request( + &self, + sender_id: ConnectionId, + receiver_id: ConnectionId, + request: T, + ) -> impl Future> { + self.request_internal(Some(sender_id), receiver_id, request) + .map_ok(|envelope| envelope.payload) + } + + pub fn request_internal( + &self, + original_sender_id: Option, + receiver_id: ConnectionId, + request: T, + ) -> impl Future>> { + let (tx, rx) = oneshot::channel(); + let send = self.connection_state(receiver_id).and_then(|connection| { + let message_id = connection.next_message_id.fetch_add(1, SeqCst); + connection + .response_channels + .lock() + .as_mut() + .ok_or_else(|| anyhow!("connection was closed"))? + .insert(message_id, tx); + connection + .outgoing_tx + .unbounded_send(proto::Message::Envelope(request.into_envelope( + message_id, + None, + original_sender_id.map(Into::into), + ))) + .map_err(|_| anyhow!("connection was closed"))?; + Ok(()) + }); + async move { + send?; + let (response, _barrier) = rx.await.map_err(|_| anyhow!("connection was closed"))?; + + if let Some(proto::envelope::Payload::Error(error)) = &response.payload { + Err(anyhow!( + "RPC request {} failed - {}", + T::NAME, + error.message + )) + } else { + Ok(TypedEnvelope { + message_id: response.id, + sender_id: receiver_id, + original_sender_id: response.original_sender_id, + payload: T::Response::from_envelope(response) + .ok_or_else(|| anyhow!("received response of the wrong type"))?, + }) + } + } + } + + pub fn send(&self, receiver_id: ConnectionId, message: T) -> Result<()> { + let connection = self.connection_state(receiver_id)?; + let message_id = connection + .next_message_id + .fetch_add(1, atomic::Ordering::SeqCst); + connection + .outgoing_tx + .unbounded_send(proto::Message::Envelope( + message.into_envelope(message_id, None, None), + ))?; + Ok(()) + } + + pub fn forward_send( + &self, + sender_id: ConnectionId, + receiver_id: ConnectionId, + message: T, + ) -> Result<()> { + let connection = self.connection_state(receiver_id)?; + let message_id = connection + .next_message_id + .fetch_add(1, atomic::Ordering::SeqCst); + connection + .outgoing_tx + .unbounded_send(proto::Message::Envelope(message.into_envelope( + message_id, + None, + Some(sender_id.into()), + )))?; + Ok(()) + } + + pub fn respond( + &self, + receipt: Receipt, + response: T::Response, + ) -> Result<()> { + let connection = self.connection_state(receipt.sender_id)?; + let message_id = connection + .next_message_id + .fetch_add(1, atomic::Ordering::SeqCst); + connection + .outgoing_tx + .unbounded_send(proto::Message::Envelope(response.into_envelope( + message_id, + Some(receipt.message_id), + None, + )))?; + Ok(()) + } + + pub fn respond_with_error( + &self, + receipt: Receipt, + response: proto::Error, + ) -> Result<()> { + let connection = self.connection_state(receipt.sender_id)?; + let message_id = connection + .next_message_id + .fetch_add(1, atomic::Ordering::SeqCst); + connection + .outgoing_tx + .unbounded_send(proto::Message::Envelope(response.into_envelope( + message_id, + Some(receipt.message_id), + None, + )))?; + Ok(()) + } + + pub fn respond_with_unhandled_message( + &self, + envelope: Box, + ) -> Result<()> { + let connection = self.connection_state(envelope.sender_id())?; + let response = proto::Error { + message: format!("message {} was not handled", envelope.payload_type_name()), + }; + let message_id = connection + .next_message_id + .fetch_add(1, atomic::Ordering::SeqCst); + connection + .outgoing_tx + .unbounded_send(proto::Message::Envelope(response.into_envelope( + message_id, + Some(envelope.message_id()), + None, + )))?; + Ok(()) + } + + fn connection_state(&self, connection_id: ConnectionId) -> Result { + let connections = self.connections.read(); + let connection = connections + .get(&connection_id) + .ok_or_else(|| anyhow!("no such connection: {}", connection_id))?; + Ok(connection.clone()) + } +} + +impl Serialize for Peer { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut state = serializer.serialize_struct("Peer", 2)?; + state.serialize_field("connections", &*self.connections.read())?; + state.end() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::TypedEnvelope; + use async_tungstenite::tungstenite::Message as WebSocketMessage; + use gpui2::TestAppContext; + + #[ctor::ctor] + fn init_logger() { + if std::env::var("RUST_LOG").is_ok() { + env_logger::init(); + } + } + + #[gpui2::test(iterations = 50)] + async fn test_request_response(cx: &mut TestAppContext) { + let executor = cx.executor(); + + // create 2 clients connected to 1 server + let server = Peer::new(0); + let client1 = Peer::new(0); + let client2 = Peer::new(0); + + let (client1_to_server_conn, server_to_client_1_conn, _kill) = + Connection::in_memory(cx.executor().clone()); + let (client1_conn_id, io_task1, client1_incoming) = + client1.add_test_connection(client1_to_server_conn, cx.executor().clone()); + let (_, io_task2, server_incoming1) = + server.add_test_connection(server_to_client_1_conn, cx.executor().clone()); + + let (client2_to_server_conn, server_to_client_2_conn, _kill) = + Connection::in_memory(cx.executor().clone()); + let (client2_conn_id, io_task3, client2_incoming) = + client2.add_test_connection(client2_to_server_conn, cx.executor().clone()); + let (_, io_task4, server_incoming2) = + server.add_test_connection(server_to_client_2_conn, cx.executor().clone()); + + executor.spawn(io_task1).detach(); + executor.spawn(io_task2).detach(); + executor.spawn(io_task3).detach(); + executor.spawn(io_task4).detach(); + executor + .spawn(handle_messages(server_incoming1, server.clone())) + .detach(); + executor + .spawn(handle_messages(client1_incoming, client1.clone())) + .detach(); + executor + .spawn(handle_messages(server_incoming2, server.clone())) + .detach(); + executor + .spawn(handle_messages(client2_incoming, client2.clone())) + .detach(); + + assert_eq!( + client1 + .request(client1_conn_id, proto::Ping {},) + .await + .unwrap(), + proto::Ack {} + ); + + assert_eq!( + client2 + .request(client2_conn_id, proto::Ping {},) + .await + .unwrap(), + proto::Ack {} + ); + + assert_eq!( + client1 + .request(client1_conn_id, proto::Test { id: 1 },) + .await + .unwrap(), + proto::Test { id: 1 } + ); + + assert_eq!( + client2 + .request(client2_conn_id, proto::Test { id: 2 }) + .await + .unwrap(), + proto::Test { id: 2 } + ); + + client1.disconnect(client1_conn_id); + client2.disconnect(client1_conn_id); + + async fn handle_messages( + mut messages: BoxStream<'static, Box>, + peer: Arc, + ) -> Result<()> { + while let Some(envelope) = messages.next().await { + let envelope = envelope.into_any(); + if let Some(envelope) = envelope.downcast_ref::>() { + let receipt = envelope.receipt(); + peer.respond(receipt, proto::Ack {})? + } else if let Some(envelope) = envelope.downcast_ref::>() + { + peer.respond(envelope.receipt(), envelope.payload.clone())? + } else { + panic!("unknown message type"); + } + } + + Ok(()) + } + } + + #[gpui2::test(iterations = 50)] + async fn test_order_of_response_and_incoming(cx: &mut TestAppContext) { + let executor = cx.executor(); + let server = Peer::new(0); + let client = Peer::new(0); + + let (client_to_server_conn, server_to_client_conn, _kill) = + Connection::in_memory(executor.clone()); + let (client_to_server_conn_id, io_task1, mut client_incoming) = + client.add_test_connection(client_to_server_conn, executor.clone()); + + let (server_to_client_conn_id, io_task2, mut server_incoming) = + server.add_test_connection(server_to_client_conn, executor.clone()); + + executor.spawn(io_task1).detach(); + executor.spawn(io_task2).detach(); + + executor + .spawn(async move { + let future = server_incoming.next().await; + let request = future + .unwrap() + .into_any() + .downcast::>() + .unwrap(); + + server + .send( + server_to_client_conn_id, + proto::Error { + message: "message 1".to_string(), + }, + ) + .unwrap(); + server + .send( + server_to_client_conn_id, + proto::Error { + message: "message 2".to_string(), + }, + ) + .unwrap(); + server.respond(request.receipt(), proto::Ack {}).unwrap(); + + // Prevent the connection from being dropped + server_incoming.next().await; + }) + .detach(); + + let events = Arc::new(Mutex::new(Vec::new())); + + let response = client.request(client_to_server_conn_id, proto::Ping {}); + let response_task = executor.spawn({ + let events = events.clone(); + async move { + response.await.unwrap(); + events.lock().push("response".to_string()); + } + }); + + executor + .spawn({ + let events = events.clone(); + async move { + let incoming1 = client_incoming + .next() + .await + .unwrap() + .into_any() + .downcast::>() + .unwrap(); + events.lock().push(incoming1.payload.message); + let incoming2 = client_incoming + .next() + .await + .unwrap() + .into_any() + .downcast::>() + .unwrap(); + events.lock().push(incoming2.payload.message); + + // Prevent the connection from being dropped + client_incoming.next().await; + } + }) + .detach(); + + response_task.await; + assert_eq!( + &*events.lock(), + &[ + "message 1".to_string(), + "message 2".to_string(), + "response".to_string() + ] + ); + } + + #[gpui2::test(iterations = 50)] + async fn test_dropping_request_before_completion(cx: &mut TestAppContext) { + let executor = cx.executor().clone(); + let server = Peer::new(0); + let client = Peer::new(0); + + let (client_to_server_conn, server_to_client_conn, _kill) = + Connection::in_memory(cx.executor().clone()); + let (client_to_server_conn_id, io_task1, mut client_incoming) = + client.add_test_connection(client_to_server_conn, cx.executor().clone()); + let (server_to_client_conn_id, io_task2, mut server_incoming) = + server.add_test_connection(server_to_client_conn, cx.executor().clone()); + + executor.spawn(io_task1).detach(); + executor.spawn(io_task2).detach(); + + executor + .spawn(async move { + let request1 = server_incoming + .next() + .await + .unwrap() + .into_any() + .downcast::>() + .unwrap(); + let request2 = server_incoming + .next() + .await + .unwrap() + .into_any() + .downcast::>() + .unwrap(); + + server + .send( + server_to_client_conn_id, + proto::Error { + message: "message 1".to_string(), + }, + ) + .unwrap(); + server + .send( + server_to_client_conn_id, + proto::Error { + message: "message 2".to_string(), + }, + ) + .unwrap(); + server.respond(request1.receipt(), proto::Ack {}).unwrap(); + server.respond(request2.receipt(), proto::Ack {}).unwrap(); + + // Prevent the connection from being dropped + server_incoming.next().await; + }) + .detach(); + + let events = Arc::new(Mutex::new(Vec::new())); + + let request1 = client.request(client_to_server_conn_id, proto::Ping {}); + let request1_task = executor.spawn(request1); + let request2 = client.request(client_to_server_conn_id, proto::Ping {}); + let request2_task = executor.spawn({ + let events = events.clone(); + async move { + request2.await.unwrap(); + events.lock().push("response 2".to_string()); + } + }); + + executor + .spawn({ + let events = events.clone(); + async move { + let incoming1 = client_incoming + .next() + .await + .unwrap() + .into_any() + .downcast::>() + .unwrap(); + events.lock().push(incoming1.payload.message); + let incoming2 = client_incoming + .next() + .await + .unwrap() + .into_any() + .downcast::>() + .unwrap(); + events.lock().push(incoming2.payload.message); + + // Prevent the connection from being dropped + client_incoming.next().await; + } + }) + .detach(); + + // Allow the request to make some progress before dropping it. + cx.executor().simulate_random_delay().await; + drop(request1_task); + + request2_task.await; + assert_eq!( + &*events.lock(), + &[ + "message 1".to_string(), + "message 2".to_string(), + "response 2".to_string() + ] + ); + } + + #[gpui2::test(iterations = 50)] + async fn test_disconnect(cx: &mut TestAppContext) { + let executor = cx.executor(); + + let (client_conn, mut server_conn, _kill) = Connection::in_memory(executor.clone()); + + let client = Peer::new(0); + let (connection_id, io_handler, mut incoming) = + client.add_test_connection(client_conn, executor.clone()); + + let (io_ended_tx, io_ended_rx) = oneshot::channel(); + executor + .spawn(async move { + io_handler.await.ok(); + io_ended_tx.send(()).unwrap(); + }) + .detach(); + + let (messages_ended_tx, messages_ended_rx) = oneshot::channel(); + executor + .spawn(async move { + incoming.next().await; + messages_ended_tx.send(()).unwrap(); + }) + .detach(); + + client.disconnect(connection_id); + + let _ = io_ended_rx.await; + let _ = messages_ended_rx.await; + assert!(server_conn + .send(WebSocketMessage::Binary(vec![])) + .await + .is_err()); + } + + #[gpui2::test(iterations = 50)] + async fn test_io_error(cx: &mut TestAppContext) { + let executor = cx.executor(); + let (client_conn, mut server_conn, _kill) = Connection::in_memory(executor.clone()); + + let client = Peer::new(0); + let (connection_id, io_handler, mut incoming) = + client.add_test_connection(client_conn, executor.clone()); + executor.spawn(io_handler).detach(); + executor + .spawn(async move { incoming.next().await }) + .detach(); + + let response = executor.spawn(client.request(connection_id, proto::Ping {})); + let _request = server_conn.rx.next().await.unwrap().unwrap(); + + drop(server_conn); + assert_eq!( + response.await.unwrap_err().to_string(), + "connection was closed" + ); + } +} diff --git a/crates/rpc2/src/proto.rs b/crates/rpc2/src/proto.rs new file mode 100644 index 0000000000..c1a7af3e4d --- /dev/null +++ b/crates/rpc2/src/proto.rs @@ -0,0 +1,674 @@ +#![allow(non_snake_case)] + +use super::{entity_messages, messages, request_messages, ConnectionId, TypedEnvelope}; +use anyhow::{anyhow, Result}; +use async_tungstenite::tungstenite::Message as WebSocketMessage; +use collections::HashMap; +use futures::{SinkExt as _, StreamExt as _}; +use prost::Message as _; +use serde::Serialize; +use std::any::{Any, TypeId}; +use std::{ + cmp, + fmt::Debug, + io, iter, + time::{Duration, SystemTime, UNIX_EPOCH}, +}; +use std::{fmt, mem}; + +include!(concat!(env!("OUT_DIR"), "/zed.messages.rs")); + +pub trait EnvelopedMessage: Clone + Debug + Serialize + Sized + Send + Sync + 'static { + const NAME: &'static str; + const PRIORITY: MessagePriority; + fn into_envelope( + self, + id: u32, + responding_to: Option, + original_sender_id: Option, + ) -> Envelope; + fn from_envelope(envelope: Envelope) -> Option; +} + +pub trait EntityMessage: EnvelopedMessage { + fn remote_entity_id(&self) -> u64; +} + +pub trait RequestMessage: EnvelopedMessage { + type Response: EnvelopedMessage; +} + +pub trait AnyTypedEnvelope: 'static + Send + Sync { + fn payload_type_id(&self) -> TypeId; + fn payload_type_name(&self) -> &'static str; + fn as_any(&self) -> &dyn Any; + fn into_any(self: Box) -> Box; + fn is_background(&self) -> bool; + fn original_sender_id(&self) -> Option; + fn sender_id(&self) -> ConnectionId; + fn message_id(&self) -> u32; +} + +pub enum MessagePriority { + Foreground, + Background, +} + +impl AnyTypedEnvelope for TypedEnvelope { + fn payload_type_id(&self) -> TypeId { + TypeId::of::() + } + + fn payload_type_name(&self) -> &'static str { + T::NAME + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn into_any(self: Box) -> Box { + self + } + + fn is_background(&self) -> bool { + matches!(T::PRIORITY, MessagePriority::Background) + } + + fn original_sender_id(&self) -> Option { + self.original_sender_id + } + + fn sender_id(&self) -> ConnectionId { + self.sender_id + } + + fn message_id(&self) -> u32 { + self.message_id + } +} + +impl PeerId { + pub fn from_u64(peer_id: u64) -> Self { + let owner_id = (peer_id >> 32) as u32; + let id = peer_id as u32; + Self { owner_id, id } + } + + pub fn as_u64(self) -> u64 { + ((self.owner_id as u64) << 32) | (self.id as u64) + } +} + +impl Copy for PeerId {} + +impl Eq for PeerId {} + +impl Ord for PeerId { + fn cmp(&self, other: &Self) -> cmp::Ordering { + self.owner_id + .cmp(&other.owner_id) + .then_with(|| self.id.cmp(&other.id)) + } +} + +impl PartialOrd for PeerId { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl std::hash::Hash for PeerId { + fn hash(&self, state: &mut H) { + self.owner_id.hash(state); + self.id.hash(state); + } +} + +impl fmt::Display for PeerId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}/{}", self.owner_id, self.id) + } +} + +messages!( + (Ack, Foreground), + (AddProjectCollaborator, Foreground), + (ApplyCodeAction, Background), + (ApplyCodeActionResponse, Background), + (ApplyCompletionAdditionalEdits, Background), + (ApplyCompletionAdditionalEditsResponse, Background), + (BufferReloaded, Foreground), + (BufferSaved, Foreground), + (Call, Foreground), + (CallCanceled, Foreground), + (CancelCall, Foreground), + (CopyProjectEntry, Foreground), + (CreateBufferForPeer, Foreground), + (CreateChannel, Foreground), + (CreateChannelResponse, Foreground), + (ChannelMessageSent, Foreground), + (CreateProjectEntry, Foreground), + (CreateRoom, Foreground), + (CreateRoomResponse, Foreground), + (DeclineCall, Foreground), + (DeleteProjectEntry, Foreground), + (Error, Foreground), + (ExpandProjectEntry, Foreground), + (Follow, Foreground), + (FollowResponse, Foreground), + (FormatBuffers, Foreground), + (FormatBuffersResponse, Foreground), + (FuzzySearchUsers, Foreground), + (GetCodeActions, Background), + (GetCodeActionsResponse, Background), + (GetHover, Background), + (GetHoverResponse, Background), + (GetChannelMessages, Background), + (GetChannelMessagesResponse, Background), + (SendChannelMessage, Background), + (SendChannelMessageResponse, Background), + (GetCompletions, Background), + (GetCompletionsResponse, Background), + (GetDefinition, Background), + (GetDefinitionResponse, Background), + (GetTypeDefinition, Background), + (GetTypeDefinitionResponse, Background), + (GetDocumentHighlights, Background), + (GetDocumentHighlightsResponse, Background), + (GetReferences, Background), + (GetReferencesResponse, Background), + (GetProjectSymbols, Background), + (GetProjectSymbolsResponse, Background), + (GetUsers, Foreground), + (Hello, Foreground), + (IncomingCall, Foreground), + (InviteChannelMember, Foreground), + (UsersResponse, Foreground), + (JoinProject, Foreground), + (JoinProjectResponse, Foreground), + (JoinRoom, Foreground), + (JoinRoomResponse, Foreground), + (JoinChannelChat, Foreground), + (JoinChannelChatResponse, Foreground), + (LeaveChannelChat, Foreground), + (LeaveProject, Foreground), + (LeaveRoom, Foreground), + (OpenBufferById, Background), + (OpenBufferByPath, Background), + (OpenBufferForSymbol, Background), + (OpenBufferForSymbolResponse, Background), + (OpenBufferResponse, Background), + (PerformRename, Background), + (PerformRenameResponse, Background), + (OnTypeFormatting, Background), + (OnTypeFormattingResponse, Background), + (InlayHints, Background), + (InlayHintsResponse, Background), + (ResolveInlayHint, Background), + (ResolveInlayHintResponse, Background), + (RefreshInlayHints, Foreground), + (Ping, Foreground), + (PrepareRename, Background), + (PrepareRenameResponse, Background), + (ExpandProjectEntryResponse, Foreground), + (ProjectEntryResponse, Foreground), + (RejoinRoom, Foreground), + (RejoinRoomResponse, Foreground), + (RemoveContact, Foreground), + (RemoveChannelMember, Foreground), + (RemoveChannelMessage, Foreground), + (ReloadBuffers, Foreground), + (ReloadBuffersResponse, Foreground), + (RemoveProjectCollaborator, Foreground), + (RenameProjectEntry, Foreground), + (RequestContact, Foreground), + (RespondToContactRequest, Foreground), + (RespondToChannelInvite, Foreground), + (JoinChannel, Foreground), + (RoomUpdated, Foreground), + (SaveBuffer, Foreground), + (RenameChannel, Foreground), + (RenameChannelResponse, Foreground), + (SetChannelMemberAdmin, Foreground), + (SearchProject, Background), + (SearchProjectResponse, Background), + (ShareProject, Foreground), + (ShareProjectResponse, Foreground), + (ShowContacts, Foreground), + (StartLanguageServer, Foreground), + (SynchronizeBuffers, Foreground), + (SynchronizeBuffersResponse, Foreground), + (RejoinChannelBuffers, Foreground), + (RejoinChannelBuffersResponse, Foreground), + (Test, Foreground), + (Unfollow, Foreground), + (UnshareProject, Foreground), + (UpdateBuffer, Foreground), + (UpdateBufferFile, Foreground), + (UpdateContacts, Foreground), + (DeleteChannel, Foreground), + (MoveChannel, Foreground), + (LinkChannel, Foreground), + (UnlinkChannel, Foreground), + (UpdateChannels, Foreground), + (UpdateDiagnosticSummary, Foreground), + (UpdateFollowers, Foreground), + (UpdateInviteInfo, Foreground), + (UpdateLanguageServer, Foreground), + (UpdateParticipantLocation, Foreground), + (UpdateProject, Foreground), + (UpdateProjectCollaborator, Foreground), + (UpdateWorktree, Foreground), + (UpdateWorktreeSettings, Foreground), + (UpdateDiffBase, Foreground), + (GetPrivateUserInfo, Foreground), + (GetPrivateUserInfoResponse, Foreground), + (GetChannelMembers, Foreground), + (GetChannelMembersResponse, Foreground), + (JoinChannelBuffer, Foreground), + (JoinChannelBufferResponse, Foreground), + (LeaveChannelBuffer, Background), + (UpdateChannelBuffer, Foreground), + (UpdateChannelBufferCollaborators, Foreground), + (AckBufferOperation, Background), + (AckChannelMessage, Background), +); + +request_messages!( + (ApplyCodeAction, ApplyCodeActionResponse), + ( + ApplyCompletionAdditionalEdits, + ApplyCompletionAdditionalEditsResponse + ), + (Call, Ack), + (CancelCall, Ack), + (CopyProjectEntry, ProjectEntryResponse), + (CreateProjectEntry, ProjectEntryResponse), + (CreateRoom, CreateRoomResponse), + (CreateChannel, CreateChannelResponse), + (DeclineCall, Ack), + (DeleteProjectEntry, ProjectEntryResponse), + (ExpandProjectEntry, ExpandProjectEntryResponse), + (Follow, FollowResponse), + (FormatBuffers, FormatBuffersResponse), + (GetCodeActions, GetCodeActionsResponse), + (GetHover, GetHoverResponse), + (GetCompletions, GetCompletionsResponse), + (GetDefinition, GetDefinitionResponse), + (GetTypeDefinition, GetTypeDefinitionResponse), + (GetDocumentHighlights, GetDocumentHighlightsResponse), + (GetReferences, GetReferencesResponse), + (GetPrivateUserInfo, GetPrivateUserInfoResponse), + (GetProjectSymbols, GetProjectSymbolsResponse), + (FuzzySearchUsers, UsersResponse), + (GetUsers, UsersResponse), + (InviteChannelMember, Ack), + (JoinProject, JoinProjectResponse), + (JoinRoom, JoinRoomResponse), + (JoinChannelChat, JoinChannelChatResponse), + (LeaveRoom, Ack), + (RejoinRoom, RejoinRoomResponse), + (IncomingCall, Ack), + (OpenBufferById, OpenBufferResponse), + (OpenBufferByPath, OpenBufferResponse), + (OpenBufferForSymbol, OpenBufferForSymbolResponse), + (Ping, Ack), + (PerformRename, PerformRenameResponse), + (PrepareRename, PrepareRenameResponse), + (OnTypeFormatting, OnTypeFormattingResponse), + (InlayHints, InlayHintsResponse), + (ResolveInlayHint, ResolveInlayHintResponse), + (RefreshInlayHints, Ack), + (ReloadBuffers, ReloadBuffersResponse), + (RequestContact, Ack), + (RemoveChannelMember, Ack), + (RemoveContact, Ack), + (RespondToContactRequest, Ack), + (RespondToChannelInvite, Ack), + (SetChannelMemberAdmin, Ack), + (SendChannelMessage, SendChannelMessageResponse), + (GetChannelMessages, GetChannelMessagesResponse), + (GetChannelMembers, GetChannelMembersResponse), + (JoinChannel, JoinRoomResponse), + (RemoveChannelMessage, Ack), + (DeleteChannel, Ack), + (RenameProjectEntry, ProjectEntryResponse), + (RenameChannel, RenameChannelResponse), + (LinkChannel, Ack), + (UnlinkChannel, Ack), + (MoveChannel, Ack), + (SaveBuffer, BufferSaved), + (SearchProject, SearchProjectResponse), + (ShareProject, ShareProjectResponse), + (SynchronizeBuffers, SynchronizeBuffersResponse), + (RejoinChannelBuffers, RejoinChannelBuffersResponse), + (Test, Test), + (UpdateBuffer, Ack), + (UpdateParticipantLocation, Ack), + (UpdateProject, Ack), + (UpdateWorktree, Ack), + (JoinChannelBuffer, JoinChannelBufferResponse), + (LeaveChannelBuffer, Ack) +); + +entity_messages!( + project_id, + AddProjectCollaborator, + ApplyCodeAction, + ApplyCompletionAdditionalEdits, + BufferReloaded, + BufferSaved, + CopyProjectEntry, + CreateBufferForPeer, + CreateProjectEntry, + DeleteProjectEntry, + ExpandProjectEntry, + FormatBuffers, + GetCodeActions, + GetCompletions, + GetDefinition, + GetTypeDefinition, + GetDocumentHighlights, + GetHover, + GetReferences, + GetProjectSymbols, + JoinProject, + LeaveProject, + OpenBufferById, + OpenBufferByPath, + OpenBufferForSymbol, + PerformRename, + OnTypeFormatting, + InlayHints, + ResolveInlayHint, + RefreshInlayHints, + PrepareRename, + ReloadBuffers, + RemoveProjectCollaborator, + RenameProjectEntry, + SaveBuffer, + SearchProject, + StartLanguageServer, + SynchronizeBuffers, + UnshareProject, + UpdateBuffer, + UpdateBufferFile, + UpdateDiagnosticSummary, + UpdateLanguageServer, + UpdateProject, + UpdateProjectCollaborator, + UpdateWorktree, + UpdateWorktreeSettings, + UpdateDiffBase +); + +entity_messages!( + channel_id, + ChannelMessageSent, + UpdateChannelBuffer, + RemoveChannelMessage, + UpdateChannelBufferCollaborators, +); + +const KIB: usize = 1024; +const MIB: usize = KIB * 1024; +const MAX_BUFFER_LEN: usize = MIB; + +/// A stream of protobuf messages. +pub struct MessageStream { + stream: S, + encoding_buffer: Vec, +} + +#[allow(clippy::large_enum_variant)] +#[derive(Debug)] +pub enum Message { + Envelope(Envelope), + Ping, + Pong, +} + +impl MessageStream { + pub fn new(stream: S) -> Self { + Self { + stream, + encoding_buffer: Vec::new(), + } + } + + pub fn inner_mut(&mut self) -> &mut S { + &mut self.stream + } +} + +impl MessageStream +where + S: futures::Sink + Unpin, +{ + pub async fn write(&mut self, message: Message) -> Result<(), anyhow::Error> { + #[cfg(any(test, feature = "test-support"))] + const COMPRESSION_LEVEL: i32 = -7; + + #[cfg(not(any(test, feature = "test-support")))] + const COMPRESSION_LEVEL: i32 = 4; + + match message { + Message::Envelope(message) => { + self.encoding_buffer.reserve(message.encoded_len()); + message + .encode(&mut self.encoding_buffer) + .map_err(io::Error::from)?; + let buffer = + zstd::stream::encode_all(self.encoding_buffer.as_slice(), COMPRESSION_LEVEL) + .unwrap(); + + self.encoding_buffer.clear(); + self.encoding_buffer.shrink_to(MAX_BUFFER_LEN); + self.stream.send(WebSocketMessage::Binary(buffer)).await?; + } + Message::Ping => { + self.stream + .send(WebSocketMessage::Ping(Default::default())) + .await?; + } + Message::Pong => { + self.stream + .send(WebSocketMessage::Pong(Default::default())) + .await?; + } + } + + Ok(()) + } +} + +impl MessageStream +where + S: futures::Stream> + Unpin, +{ + pub async fn read(&mut self) -> Result { + while let Some(bytes) = self.stream.next().await { + match bytes? { + WebSocketMessage::Binary(bytes) => { + zstd::stream::copy_decode(bytes.as_slice(), &mut self.encoding_buffer).unwrap(); + let envelope = Envelope::decode(self.encoding_buffer.as_slice()) + .map_err(io::Error::from)?; + + self.encoding_buffer.clear(); + self.encoding_buffer.shrink_to(MAX_BUFFER_LEN); + return Ok(Message::Envelope(envelope)); + } + WebSocketMessage::Ping(_) => return Ok(Message::Ping), + WebSocketMessage::Pong(_) => return Ok(Message::Pong), + WebSocketMessage::Close(_) => break, + _ => {} + } + } + Err(anyhow!("connection closed")) + } +} + +impl From for SystemTime { + fn from(val: Timestamp) -> Self { + UNIX_EPOCH + .checked_add(Duration::new(val.seconds, val.nanos)) + .unwrap() + } +} + +impl From for Timestamp { + fn from(time: SystemTime) -> Self { + let duration = time.duration_since(UNIX_EPOCH).unwrap(); + Self { + seconds: duration.as_secs(), + nanos: duration.subsec_nanos(), + } + } +} + +impl From for Nonce { + fn from(nonce: u128) -> Self { + let upper_half = (nonce >> 64) as u64; + let lower_half = nonce as u64; + Self { + upper_half, + lower_half, + } + } +} + +impl From for u128 { + fn from(nonce: Nonce) -> Self { + let upper_half = (nonce.upper_half as u128) << 64; + let lower_half = nonce.lower_half as u128; + upper_half | lower_half + } +} + +pub fn split_worktree_update( + mut message: UpdateWorktree, + max_chunk_size: usize, +) -> impl Iterator { + let mut done_files = false; + + let mut repository_map = message + .updated_repositories + .into_iter() + .map(|repo| (repo.work_directory_id, repo)) + .collect::>(); + + iter::from_fn(move || { + if done_files { + return None; + } + + let updated_entries_chunk_size = cmp::min(message.updated_entries.len(), max_chunk_size); + let updated_entries: Vec<_> = message + .updated_entries + .drain(..updated_entries_chunk_size) + .collect(); + + let removed_entries_chunk_size = cmp::min(message.removed_entries.len(), max_chunk_size); + let removed_entries = message + .removed_entries + .drain(..removed_entries_chunk_size) + .collect(); + + done_files = message.updated_entries.is_empty() && message.removed_entries.is_empty(); + + let mut updated_repositories = Vec::new(); + + if !repository_map.is_empty() { + for entry in &updated_entries { + if let Some(repo) = repository_map.remove(&entry.id) { + updated_repositories.push(repo) + } + } + } + + let removed_repositories = if done_files { + mem::take(&mut message.removed_repositories) + } else { + Default::default() + }; + + if done_files { + updated_repositories.extend(mem::take(&mut repository_map).into_values()); + } + + Some(UpdateWorktree { + project_id: message.project_id, + worktree_id: message.worktree_id, + root_name: message.root_name.clone(), + abs_path: message.abs_path.clone(), + updated_entries, + removed_entries, + scan_id: message.scan_id, + is_last_update: done_files && message.is_last_update, + updated_repositories, + removed_repositories, + }) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[gpui2::test] + async fn test_buffer_size() { + let (tx, rx) = futures::channel::mpsc::unbounded(); + let mut sink = MessageStream::new(tx.sink_map_err(|_| anyhow!(""))); + sink.write(Message::Envelope(Envelope { + payload: Some(envelope::Payload::UpdateWorktree(UpdateWorktree { + root_name: "abcdefg".repeat(10), + ..Default::default() + })), + ..Default::default() + })) + .await + .unwrap(); + assert!(sink.encoding_buffer.capacity() <= MAX_BUFFER_LEN); + sink.write(Message::Envelope(Envelope { + payload: Some(envelope::Payload::UpdateWorktree(UpdateWorktree { + root_name: "abcdefg".repeat(1000000), + ..Default::default() + })), + ..Default::default() + })) + .await + .unwrap(); + assert!(sink.encoding_buffer.capacity() <= MAX_BUFFER_LEN); + + let mut stream = MessageStream::new(rx.map(anyhow::Ok)); + stream.read().await.unwrap(); + assert!(stream.encoding_buffer.capacity() <= MAX_BUFFER_LEN); + stream.read().await.unwrap(); + assert!(stream.encoding_buffer.capacity() <= MAX_BUFFER_LEN); + } + + #[gpui2::test] + fn test_converting_peer_id_from_and_to_u64() { + let peer_id = PeerId { + owner_id: 10, + id: 3, + }; + assert_eq!(PeerId::from_u64(peer_id.as_u64()), peer_id); + let peer_id = PeerId { + owner_id: u32::MAX, + id: 3, + }; + assert_eq!(PeerId::from_u64(peer_id.as_u64()), peer_id); + let peer_id = PeerId { + owner_id: 10, + id: u32::MAX, + }; + assert_eq!(PeerId::from_u64(peer_id.as_u64()), peer_id); + let peer_id = PeerId { + owner_id: u32::MAX, + id: u32::MAX, + }; + assert_eq!(PeerId::from_u64(peer_id.as_u64()), peer_id); + } +} diff --git a/crates/rpc2/src/rpc.rs b/crates/rpc2/src/rpc.rs new file mode 100644 index 0000000000..942672b94b --- /dev/null +++ b/crates/rpc2/src/rpc.rs @@ -0,0 +1,9 @@ +pub mod auth; +mod conn; +mod peer; +pub mod proto; +pub use conn::Connection; +pub use peer::*; +mod macros; + +pub const PROTOCOL_VERSION: u32 = 64; diff --git a/crates/storybook2/src/components.rs b/crates/storybook2/src/components.rs index c39ccaa3d9..a3dca51adc 100644 --- a/crates/storybook2/src/components.rs +++ b/crates/storybook2/src/components.rs @@ -14,7 +14,7 @@ impl Default for ButtonHandlers { } } -#[derive(Element)] +#[derive(Component)] pub struct Button { handlers: ButtonHandlers, label: Option>, diff --git a/crates/storybook2/src/stories/kitchen_sink.rs b/crates/storybook2/src/stories/kitchen_sink.rs index e7c4e752f6..ec89238ac4 100644 --- a/crates/storybook2/src/stories/kitchen_sink.rs +++ b/crates/storybook2/src/stories/kitchen_sink.rs @@ -16,7 +16,7 @@ impl KitchenSinkStory { view(cx.entity(|cx| Self::new()), Self::render) } - fn render(&mut self, cx: &mut ViewContext) -> impl Element { + fn render(&mut self, cx: &mut ViewContext) -> impl Component { let element_stories = ElementStory::iter() .map(|selector| selector.story(cx)) .collect::>(); diff --git a/crates/storybook2/src/stories/scroll.rs b/crates/storybook2/src/stories/scroll.rs index c5df3bd3d3..a1e3c6700e 100644 --- a/crates/storybook2/src/stories/scroll.rs +++ b/crates/storybook2/src/stories/scroll.rs @@ -1,8 +1,7 @@ use crate::themes::rose_pine; use gpui2::{ - div, px, view, Context, Element, ParentElement, SharedString, Styled, View, WindowContext, + div, px, view, Component, Context, ParentElement, SharedString, Styled, View, WindowContext, }; -use ui::ElementExt; pub struct ScrollStory { text: View<()>, @@ -16,7 +15,7 @@ impl ScrollStory { } } -fn checkerboard(depth: usize) -> impl Element +fn checkerboard(depth: usize) -> impl Component where S: 'static + Send + Sync, { diff --git a/crates/storybook2/src/stories/z_index.rs b/crates/storybook2/src/stories/z_index.rs index 9dd74b6884..7fa078c4e3 100644 --- a/crates/storybook2/src/stories/z_index.rs +++ b/crates/storybook2/src/stories/z_index.rs @@ -1,4 +1,3 @@ -use std::marker::PhantomData; use gpui2::{px, rgb, Div, Hsla}; use ui::prelude::*; @@ -7,19 +6,15 @@ use crate::story::Story; /// A reimplementation of the MDN `z-index` example, found here: /// [https://developer.mozilla.org/en-US/docs/Web/CSS/z-index](https://developer.mozilla.org/en-US/docs/Web/CSS/z-index). -#[derive(Element)] -pub struct ZIndexStory { - state_type: PhantomData, -} +#[derive(Component)] +pub struct ZIndexStory; -impl ZIndexStory { +impl ZIndexStory { pub fn new() -> Self { - Self { - state_type: PhantomData, - } + Self } - fn render(&mut self, _view: &mut S, cx: &mut ViewContext) -> impl Element { + fn render(self, _view: &mut S, cx: &mut ViewContext) -> impl Component { Story::container(cx) .child(Story::title(cx, "z-index")) .child( @@ -86,23 +81,19 @@ trait Styles: Styled + Sized { } } -impl Styles for Div {} +impl Styles for Div {} -#[derive(Element)] -struct ZIndexExample { - state_type: PhantomData, +#[derive(Component)] +struct ZIndexExample { z_index: u32, } -impl ZIndexExample { +impl ZIndexExample { pub fn new(z_index: u32) -> Self { - Self { - state_type: PhantomData, - z_index, - } + Self { z_index } } - fn render(&mut self, _view: &mut S, cx: &mut ViewContext) -> impl Element { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { div() .relative() .size_full() diff --git a/crates/storybook2/src/story_selector.rs b/crates/storybook2/src/story_selector.rs index 8cd3f9e329..5f60ad14ea 100644 --- a/crates/storybook2/src/story_selector.rs +++ b/crates/storybook2/src/story_selector.rs @@ -28,29 +28,29 @@ impl ElementStory { pub fn story(&self, cx: &mut WindowContext) -> AnyView { match self { Self::Avatar => { - view(cx.entity(|cx| ()), |_, _| ui::AvatarStory::new().into_any()).into_any() + view(cx.entity(|cx| ()), |_, _| ui::AvatarStory::new().render()).into_any() } Self::Button => { - view(cx.entity(|cx| ()), |_, _| ui::ButtonStory::new().into_any()).into_any() + view(cx.entity(|cx| ()), |_, _| ui::ButtonStory::new().render()).into_any() } Self::Details => view(cx.entity(|cx| ()), |_, _| { - ui::DetailsStory::new().into_any() + ui::DetailsStory::new().render() }) .into_any(), Self::Focus => FocusStory::view(cx).into_any(), Self::Icon => { - view(cx.entity(|cx| ()), |_, _| ui::IconStory::new().into_any()).into_any() + view(cx.entity(|cx| ()), |_, _| ui::IconStory::new().render()).into_any() } Self::Input => { - view(cx.entity(|cx| ()), |_, _| ui::InputStory::new().into_any()).into_any() + view(cx.entity(|cx| ()), |_, _| ui::InputStory::new().render()).into_any() } Self::Label => { - view(cx.entity(|cx| ()), |_, _| ui::LabelStory::new().into_any()).into_any() + view(cx.entity(|cx| ()), |_, _| ui::LabelStory::new().render()).into_any() } Self::Scroll => ScrollStory::view(cx).into_any(), Self::Text => TextStory::view(cx).into_any(), Self::ZIndex => { - view(cx.entity(|cx| ()), |_, _| ZIndexStory::new().into_any()).into_any() + view(cx.entity(|cx| ()), |_, _| ZIndexStory::new().render()).into_any() } } } @@ -91,93 +91,93 @@ impl ComponentStory { pub fn story(&self, cx: &mut WindowContext) -> AnyView { match self { Self::AssistantPanel => view(cx.entity(|cx| ()), |_, _| { - ui::AssistantPanelStory::new().into_any() + ui::AssistantPanelStory::new().render() }) .into_any(), Self::Buffer => { - view(cx.entity(|cx| ()), |_, _| ui::BufferStory::new().into_any()).into_any() + view(cx.entity(|cx| ()), |_, _| ui::BufferStory::new().render()).into_any() } Self::Breadcrumb => view(cx.entity(|cx| ()), |_, _| { - ui::BreadcrumbStory::new().into_any() + ui::BreadcrumbStory::new().render() }) .into_any(), Self::ChatPanel => view(cx.entity(|cx| ()), |_, _| { - ui::ChatPanelStory::new().into_any() + ui::ChatPanelStory::new().render() }) .into_any(), Self::CollabPanel => view(cx.entity(|cx| ()), |_, _| { - ui::CollabPanelStory::new().into_any() + ui::CollabPanelStory::new().render() }) .into_any(), Self::CommandPalette => view(cx.entity(|cx| ()), |_, _| { - ui::CommandPaletteStory::new().into_any() + ui::CommandPaletteStory::new().render() }) .into_any(), Self::ContextMenu => view(cx.entity(|cx| ()), |_, _| { - ui::ContextMenuStory::new().into_any() + ui::ContextMenuStory::new().render() }) .into_any(), Self::Facepile => view(cx.entity(|cx| ()), |_, _| { - ui::FacepileStory::new().into_any() + ui::FacepileStory::new().render() }) .into_any(), Self::Keybinding => view(cx.entity(|cx| ()), |_, _| { - ui::KeybindingStory::new().into_any() + ui::KeybindingStory::new().render() }) .into_any(), Self::LanguageSelector => view(cx.entity(|cx| ()), |_, _| { - ui::LanguageSelectorStory::new().into_any() + ui::LanguageSelectorStory::new().render() }) .into_any(), Self::MultiBuffer => view(cx.entity(|cx| ()), |_, _| { - ui::MultiBufferStory::new().into_any() + ui::MultiBufferStory::new().render() }) .into_any(), Self::NotificationsPanel => view(cx.entity(|cx| ()), |_, _| { - ui::NotificationsPanelStory::new().into_any() + ui::NotificationsPanelStory::new().render() }) .into_any(), Self::Palette => view(cx.entity(|cx| ()), |_, _| { - ui::PaletteStory::new().into_any() + ui::PaletteStory::new().render() }) .into_any(), Self::Panel => { - view(cx.entity(|cx| ()), |_, _| ui::PanelStory::new().into_any()).into_any() + view(cx.entity(|cx| ()), |_, _| ui::PanelStory::new().render()).into_any() } Self::ProjectPanel => view(cx.entity(|cx| ()), |_, _| { - ui::ProjectPanelStory::new().into_any() + ui::ProjectPanelStory::new().render() }) .into_any(), Self::RecentProjects => view(cx.entity(|cx| ()), |_, _| { - ui::RecentProjectsStory::new().into_any() + ui::RecentProjectsStory::new().render() }) .into_any(), - Self::Tab => view(cx.entity(|cx| ()), |_, _| ui::TabStory::new().into_any()).into_any(), + Self::Tab => view(cx.entity(|cx| ()), |_, _| ui::TabStory::new().render()).into_any(), Self::TabBar => { - view(cx.entity(|cx| ()), |_, _| ui::TabBarStory::new().into_any()).into_any() + view(cx.entity(|cx| ()), |_, _| ui::TabBarStory::new().render()).into_any() } Self::Terminal => view(cx.entity(|cx| ()), |_, _| { - ui::TerminalStory::new().into_any() + ui::TerminalStory::new().render() }) .into_any(), Self::ThemeSelector => view(cx.entity(|cx| ()), |_, _| { - ui::ThemeSelectorStory::new().into_any() + ui::ThemeSelectorStory::new().render() }) .into_any(), Self::TitleBar => ui::TitleBarStory::view(cx).into_any(), Self::Toast => { - view(cx.entity(|cx| ()), |_, _| ui::ToastStory::new().into_any()).into_any() + view(cx.entity(|cx| ()), |_, _| ui::ToastStory::new().render()).into_any() } Self::Toolbar => view(cx.entity(|cx| ()), |_, _| { - ui::ToolbarStory::new().into_any() + ui::ToolbarStory::new().render() }) .into_any(), Self::TrafficLights => view(cx.entity(|cx| ()), |_, _| { - ui::TrafficLightsStory::new().into_any() + ui::TrafficLightsStory::new().render() }) .into_any(), Self::Copilot => view(cx.entity(|cx| ()), |_, _| { - ui::CopilotModalStory::new().into_any() + ui::CopilotModalStory::new().render() }) .into_any(), Self::Workspace => ui::WorkspaceStory::view(cx).into_any(), diff --git a/crates/storybook2/src/storybook2.rs b/crates/storybook2/src/storybook2.rs index c6d71f079d..29ff5eaa98 100644 --- a/crates/storybook2/src/storybook2.rs +++ b/crates/storybook2/src/storybook2.rs @@ -10,7 +10,7 @@ use std::sync::Arc; use clap::Parser; use gpui2::{ - div, px, size, view, AnyView, AppContext, Bounds, Context, Element, ViewContext, WindowBounds, + div, px, size, view, AnyView, AppContext, Bounds, Context, ViewContext, WindowBounds, WindowOptions, }; use log::LevelFilter; @@ -107,7 +107,7 @@ impl StoryWrapper { Self { story, theme } } - fn render(&mut self, cx: &mut ViewContext) -> impl Element { + fn render(&mut self, cx: &mut ViewContext) -> impl Component { themed(self.theme.clone(), cx, |cx| { div() .flex() diff --git a/crates/ui2/src/components/assistant_panel.rs b/crates/ui2/src/components/assistant_panel.rs index 31f299de91..defd05f500 100644 --- a/crates/ui2/src/components/assistant_panel.rs +++ b/crates/ui2/src/components/assistant_panel.rs @@ -1,22 +1,18 @@ -use std::marker::PhantomData; - use gpui2::{rems, AbsoluteLength}; use crate::prelude::*; use crate::{Icon, IconButton, Label, Panel, PanelSide}; -#[derive(Element)] -pub struct AssistantPanel { +#[derive(Component)] +pub struct AssistantPanel { id: ElementId, - state_type: PhantomData, current_side: PanelSide, } -impl AssistantPanel { +impl AssistantPanel { pub fn new(id: impl Into) -> Self { Self { id: id.into(), - state_type: PhantomData, current_side: PanelSide::default(), } } @@ -26,7 +22,7 @@ impl AssistantPanel { self } - fn render(&mut self, view: &mut S, cx: &mut ViewContext) -> impl Element { + fn render(self, view: &mut V, cx: &mut ViewContext) -> impl Component { Panel::new(self.id.clone(), cx) .children(vec![div() .flex() @@ -69,7 +65,7 @@ impl AssistantPanel { .overflow_y_scroll() .child(Label::new("Is this thing on?")), ) - .into_any()]) + .render()]) .side(self.current_side) .width(AbsoluteLength::Rems(rems(32.))) } @@ -84,25 +80,17 @@ mod stories { use super::*; - #[derive(Element)] - pub struct AssistantPanelStory { - state_type: PhantomData, - } + #[derive(Component)] + pub struct AssistantPanelStory {} - impl AssistantPanelStory { + impl AssistantPanelStory { pub fn new() -> Self { - Self { - state_type: PhantomData, - } + Self {} } - fn render( - &mut self, - _view: &mut S, - cx: &mut ViewContext, - ) -> impl Element { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { Story::container(cx) - .child(Story::title_for::<_, AssistantPanel>(cx)) + .child(Story::title_for::<_, AssistantPanel>(cx)) .child(Story::label(cx, "Default")) .child(AssistantPanel::new("assistant-panel")) } diff --git a/crates/ui2/src/components/breadcrumb.rs b/crates/ui2/src/components/breadcrumb.rs index e67ceb1751..3cba3b7248 100644 --- a/crates/ui2/src/components/breadcrumb.rs +++ b/crates/ui2/src/components/breadcrumb.rs @@ -1,41 +1,33 @@ -use std::marker::PhantomData; use std::path::PathBuf; use gpui2::Div; - use crate::prelude::*; use crate::{h_stack, HighlightedText}; #[derive(Clone)] pub struct Symbol(pub Vec); -#[derive(Element)] -pub struct Breadcrumb { - state_type: PhantomData, +#[derive(Component)] +pub struct Breadcrumb { path: PathBuf, symbols: Vec, } -impl Breadcrumb { +impl Breadcrumb { pub fn new(path: PathBuf, symbols: Vec) -> Self { Self { - state_type: PhantomData, path, symbols, } } - fn render_separator(&self, cx: &WindowContext) -> Div { + fn render_separator(&self, cx: &WindowContext) -> Div { let theme = theme(cx); div().child(" › ").text_color(theme.text_muted) } - fn render( - &mut self, - view_state: &mut S, - cx: &mut ViewContext, - ) -> impl Element { + fn render(self, view_state: &mut V, cx: &mut ViewContext) -> impl Component { let theme = theme(cx); let symbols_len = self.symbols.len(); @@ -90,27 +82,19 @@ mod stories { use super::*; - #[derive(Element)] - pub struct BreadcrumbStory { - state_type: PhantomData, - } + #[derive(Component)] + pub struct BreadcrumbStory; - impl BreadcrumbStory { + impl BreadcrumbStory { pub fn new() -> Self { - Self { - state_type: PhantomData, - } + Self } - fn render( - &mut self, - view_state: &mut S, - cx: &mut ViewContext, - ) -> impl Element { + fn render(self, view_state: &mut V, cx: &mut ViewContext) -> impl Component { let theme = theme(cx); Story::container(cx) - .child(Story::title_for::<_, Breadcrumb>(cx)) + .child(Story::title_for::<_, Breadcrumb>(cx)) .child(Story::label(cx, "Default")) .child(Breadcrumb::new( PathBuf::from_str("crates/ui/src/components/toolbar.rs").unwrap(), diff --git a/crates/ui2/src/components/buffer.rs b/crates/ui2/src/components/buffer.rs index 2e0e07e16c..b4ccb89b5c 100644 --- a/crates/ui2/src/components/buffer.rs +++ b/crates/ui2/src/components/buffer.rs @@ -1,5 +1,3 @@ -use std::marker::PhantomData; - use gpui2::{Hsla, WindowContext}; use crate::prelude::*; @@ -109,10 +107,9 @@ impl BufferRow { } } -#[derive(Element, Clone)] -pub struct Buffer { +#[derive(Component, Clone)] +pub struct Buffer { id: ElementId, - state_type: PhantomData, rows: Option, readonly: bool, language: Option, @@ -120,11 +117,10 @@ pub struct Buffer { path: Option, } -impl Buffer { +impl Buffer { pub fn new(id: impl Into) -> Self { Self { id: id.into(), - state_type: PhantomData, rows: Some(BufferRows::default()), readonly: false, language: None, @@ -158,7 +154,7 @@ impl Buffer { self } - fn render_row(row: BufferRow, cx: &WindowContext) -> impl Element { + fn render_row(row: BufferRow, cx: &WindowContext) -> impl Component { let theme = theme(cx); let line_background = if row.current { @@ -208,7 +204,7 @@ impl Buffer { })) } - fn render_rows(&self, cx: &WindowContext) -> Vec> { + fn render_rows(&self, cx: &WindowContext) -> Vec> { match &self.rows { Some(rows) => rows .rows @@ -219,7 +215,7 @@ impl Buffer { } } - fn render(&mut self, _view: &mut S, cx: &mut ViewContext) -> impl Element { + fn render(self, _view: &mut S, cx: &mut ViewContext) -> impl Component { let theme = theme(cx); let rows = self.render_rows(cx); @@ -246,27 +242,19 @@ mod stories { use super::*; - #[derive(Element)] - pub struct BufferStory { - state_type: PhantomData, - } + #[derive(Component)] + pub struct BufferStory; - impl BufferStory { + impl BufferStory { pub fn new() -> Self { - Self { - state_type: PhantomData, - } + Self } - fn render( - &mut self, - _view: &mut S, - cx: &mut ViewContext, - ) -> impl Element { + fn render(self, _view: &mut S, cx: &mut ViewContext) -> impl Component { let theme = theme(cx); Story::container(cx) - .child(Story::title_for::<_, Buffer>(cx)) + .child(Story::title_for::<_, Buffer>(cx)) .child(Story::label(cx, "Default")) .child(div().w(rems(64.)).h_96().child(empty_buffer_example())) .child(Story::label(cx, "Hello World (Rust)")) diff --git a/crates/ui2/src/components/buffer_search.rs b/crates/ui2/src/components/buffer_search.rs index 1edbd58c87..b5e74a4810 100644 --- a/crates/ui2/src/components/buffer_search.rs +++ b/crates/ui2/src/components/buffer_search.rs @@ -25,7 +25,7 @@ impl BufferSearch { view(cx.entity(|cx| Self::new()), Self::render) } - fn render(&mut self, cx: &mut ViewContext) -> impl Element { + fn render(&mut self, cx: &mut ViewContext) -> impl Component { let theme = theme(cx); h_stack().bg(theme.toolbar).p_2().child( diff --git a/crates/ui2/src/components/chat_panel.rs b/crates/ui2/src/components/chat_panel.rs index 7afccd6d9d..e4494f4614 100644 --- a/crates/ui2/src/components/chat_panel.rs +++ b/crates/ui2/src/components/chat_panel.rs @@ -1,17 +1,15 @@ -use std::marker::PhantomData; - use chrono::NaiveDateTime; use crate::prelude::*; use crate::{Icon, IconButton, Input, Label, LabelColor}; -#[derive(Element)] -pub struct ChatPanel { +#[derive(Component)] +pub struct ChatPanel { element_id: ElementId, - messages: Vec>, + messages: Vec, } -impl ChatPanel { +impl ChatPanel { pub fn new(element_id: impl Into) -> Self { Self { element_id: element_id.into(), @@ -19,12 +17,12 @@ impl ChatPanel { } } - pub fn messages(mut self, messages: Vec>) -> Self { + pub fn messages(mut self, messages: Vec) -> Self { self.messages = messages; self } - fn render(&mut self, _view: &mut S, cx: &mut ViewContext) -> impl Element { + fn render(mut self, _view: &mut S, cx: &mut ViewContext) -> impl Component { div() .id(self.element_id.clone()) .flex() @@ -70,25 +68,23 @@ impl ChatPanel { } } -#[derive(Element)] -pub struct ChatMessage { - state_type: PhantomData, +#[derive(Component)] +pub struct ChatMessage { author: String, text: String, sent_at: NaiveDateTime, } -impl ChatMessage { +impl ChatMessage { pub fn new(author: String, text: String, sent_at: NaiveDateTime) -> Self { Self { - state_type: PhantomData, author, text, sent_at, } } - fn render(&mut self, _view: &mut S, cx: &mut ViewContext) -> impl Element { + fn render(self, _view: &mut S, cx: &mut ViewContext) -> impl Component { div() .flex() .flex_col() @@ -117,25 +113,17 @@ mod stories { use super::*; - #[derive(Element)] - pub struct ChatPanelStory { - state_type: PhantomData, - } + #[derive(Component)] + pub struct ChatPanelStory; - impl ChatPanelStory { + impl ChatPanelStory { pub fn new() -> Self { - Self { - state_type: PhantomData, - } + Self } - fn render( - &mut self, - _view: &mut S, - cx: &mut ViewContext, - ) -> impl Element { + fn render(self, _view: &mut S, cx: &mut ViewContext) -> impl Component { Story::container(cx) - .child(Story::title_for::<_, ChatPanel>(cx)) + .child(Story::title_for::<_, ChatPanel>(cx)) .child(Story::label(cx, "Default")) .child( Panel::new("chat-panel-1-outer", cx) diff --git a/crates/ui2/src/components/collab_panel.rs b/crates/ui2/src/components/collab_panel.rs index 6414e0e5ff..6d2cbd8e14 100644 --- a/crates/ui2/src/components/collab_panel.rs +++ b/crates/ui2/src/components/collab_panel.rs @@ -3,23 +3,18 @@ use crate::{ static_collab_panel_channels, static_collab_panel_current_call, v_stack, Icon, List, ListHeader, ToggleState, }; -use std::marker::PhantomData; -#[derive(Element)] -pub struct CollabPanel { +#[derive(Component)] +pub struct CollabPanel { id: ElementId, - state_type: PhantomData, } -impl CollabPanel { +impl CollabPanel { pub fn new(id: impl Into) -> Self { - Self { - id: id.into(), - state_type: PhantomData, - } + Self { id: id.into() } } - fn render(&mut self, _view: &mut S, cx: &mut ViewContext) -> impl Element { + fn render(self, _view: &mut S, cx: &mut ViewContext) -> impl Component { let theme = theme(cx); v_stack() @@ -98,25 +93,17 @@ mod stories { use super::*; - #[derive(Element)] - pub struct CollabPanelStory { - state_type: PhantomData, - } + #[derive(Component)] + pub struct CollabPanelStory; - impl CollabPanelStory { + impl CollabPanelStory { pub fn new() -> Self { - Self { - state_type: PhantomData, - } + Self } - fn render( - &mut self, - _view: &mut S, - cx: &mut ViewContext, - ) -> impl Element { + fn render(self, _view: &mut S, cx: &mut ViewContext) -> impl Component { Story::container(cx) - .child(Story::title_for::<_, CollabPanel>(cx)) + .child(Story::title_for::<_, CollabPanel>(cx)) .child(Story::label(cx, "Default")) .child(CollabPanel::new("collab-panel")) } diff --git a/crates/ui2/src/components/command_palette.rs b/crates/ui2/src/components/command_palette.rs index 45194e843c..065ee9a052 100644 --- a/crates/ui2/src/components/command_palette.rs +++ b/crates/ui2/src/components/command_palette.rs @@ -1,23 +1,17 @@ -use std::marker::PhantomData; - use crate::prelude::*; use crate::{example_editor_actions, OrderMethod, Palette}; -#[derive(Element)] -pub struct CommandPalette { +#[derive(Component)] +pub struct CommandPalette { id: ElementId, - state_type: PhantomData, } -impl CommandPalette { +impl CommandPalette { pub fn new(id: impl Into) -> Self { - Self { - id: id.into(), - state_type: PhantomData, - } + Self { id: id.into() } } - fn render(&mut self, _view: &mut S, cx: &mut ViewContext) -> impl Element { + fn render(self, _view: &mut S, cx: &mut ViewContext) -> impl Component { div().id(self.id.clone()).child( Palette::new("palette") .items(example_editor_actions()) @@ -37,25 +31,17 @@ mod stories { use super::*; - #[derive(Element)] - pub struct CommandPaletteStory { - state_type: PhantomData, - } + #[derive(Component)] + pub struct CommandPaletteStory; - impl CommandPaletteStory { + impl CommandPaletteStory { pub fn new() -> Self { - Self { - state_type: PhantomData, - } + Self } - fn render( - &mut self, - _view: &mut S, - cx: &mut ViewContext, - ) -> impl Element { + fn render(self, _view: &mut S, cx: &mut ViewContext) -> impl Component { Story::container(cx) - .child(Story::title_for::<_, CommandPalette>(cx)) + .child(Story::title_for::<_, CommandPalette>(cx)) .child(Story::label(cx, "Default")) .child(CommandPalette::new("command-palette")) } diff --git a/crates/ui2/src/components/context_menu.rs b/crates/ui2/src/components/context_menu.rs index 73813ed613..3e0323d942 100644 --- a/crates/ui2/src/components/context_menu.rs +++ b/crates/ui2/src/components/context_menu.rs @@ -1,14 +1,14 @@ use crate::{prelude::*, ListItemVariant}; use crate::{v_stack, Label, List, ListEntry, ListItem, ListSeparator, ListSubHeader}; -pub enum ContextMenuItem { +pub enum ContextMenuItem { Header(SharedString), - Entry(Label), + Entry(Label), Separator, } -impl ContextMenuItem { - fn to_list_item(self) -> ListItem { +impl ContextMenuItem { + fn to_list_item(self) -> ListItem { match self { ContextMenuItem::Header(label) => ListSubHeader::new(label).into(), ContextMenuItem::Entry(label) => { @@ -26,23 +26,23 @@ impl ContextMenuItem { Self::Separator } - pub fn entry(label: Label) -> Self { + pub fn entry(label: Label) -> Self { Self::Entry(label) } } -#[derive(Element)] -pub struct ContextMenu { - items: Vec>, +#[derive(Component)] +pub struct ContextMenu { + items: Vec, } -impl ContextMenu { - pub fn new(items: impl IntoIterator>) -> Self { +impl ContextMenu { + pub fn new(items: impl IntoIterator) -> Self { Self { items: items.into_iter().collect(), } } - fn render(&mut self, _view: &mut S, cx: &mut ViewContext) -> impl Element { + fn render(mut self, _view: &mut S, cx: &mut ViewContext) -> impl Component { let theme = theme(cx); v_stack() @@ -67,31 +67,21 @@ pub use stories::*; #[cfg(feature = "stories")] mod stories { - use std::marker::PhantomData; - use crate::story::Story; use super::*; - #[derive(Element)] - pub struct ContextMenuStory { - state_type: PhantomData, - } + #[derive(Component)] + pub struct ContextMenuStory; - impl ContextMenuStory { + impl ContextMenuStory { pub fn new() -> Self { - Self { - state_type: PhantomData, - } + Self } - fn render( - &mut self, - _view: &mut S, - cx: &mut ViewContext, - ) -> impl Element { + fn render(self, _view: &mut S, cx: &mut ViewContext) -> impl Component { Story::container(cx) - .child(Story::title_for::<_, ContextMenu>(cx)) + .child(Story::title_for::<_, ContextMenu>(cx)) .child(Story::label(cx, "Default")) .child(ContextMenu::new([ ContextMenuItem::header("Section header"), diff --git a/crates/ui2/src/components/copilot.rs b/crates/ui2/src/components/copilot.rs index b0f20cfa0d..3c48ef20d0 100644 --- a/crates/ui2/src/components/copilot.rs +++ b/crates/ui2/src/components/copilot.rs @@ -1,22 +1,16 @@ -use std::marker::PhantomData; - use crate::{prelude::*, Button, Label, LabelColor, Modal}; -#[derive(Element)] -pub struct CopilotModal { +#[derive(Component)] +pub struct CopilotModal { id: ElementId, - state_type: PhantomData, } -impl CopilotModal { +impl CopilotModal { pub fn new(id: impl Into) -> Self { - Self { - id: id.into(), - state_type: PhantomData, - } + Self { id: id.into() } } - fn render(&mut self, _view: &mut S, cx: &mut ViewContext) -> impl Element { + fn render(self, _view: &mut S, cx: &mut ViewContext) -> impl Component { div().id(self.id.clone()).child( Modal::new("some-id") .title("Connect Copilot to Zed") @@ -35,25 +29,17 @@ mod stories { use super::*; - #[derive(Element)] - pub struct CopilotModalStory { - state_type: PhantomData, - } + #[derive(Component)] + pub struct CopilotModalStory; - impl CopilotModalStory { + impl CopilotModalStory { pub fn new() -> Self { - Self { - state_type: PhantomData, - } + Self } - fn render( - &mut self, - _view: &mut S, - cx: &mut ViewContext, - ) -> impl Element { + fn render(self, _view: &mut S, cx: &mut ViewContext) -> impl Component { Story::container(cx) - .child(Story::title_for::<_, CopilotModal>(cx)) + .child(Story::title_for::<_, CopilotModal>(cx)) .child(Story::label(cx, "Default")) .child(CopilotModal::new("copilot-modal")) } diff --git a/crates/ui2/src/components/editor_pane.rs b/crates/ui2/src/components/editor_pane.rs index 7489b3e47d..9c9638d057 100644 --- a/crates/ui2/src/components/editor_pane.rs +++ b/crates/ui2/src/components/editor_pane.rs @@ -10,10 +10,10 @@ use crate::{ #[derive(Clone)] pub struct EditorPane { - tabs: Vec>, + tabs: Vec, path: PathBuf, symbols: Vec, - buffer: Buffer, + buffer: Buffer, buffer_search: View, is_buffer_search_open: bool, } @@ -21,10 +21,10 @@ pub struct EditorPane { impl EditorPane { pub fn new( cx: &mut WindowContext, - tabs: Vec>, + tabs: Vec, path: PathBuf, symbols: Vec, - buffer: Buffer, + buffer: Buffer, ) -> Self { Self { tabs, @@ -49,7 +49,7 @@ impl EditorPane { ) } - fn render(&mut self, cx: &mut ViewContext) -> impl Element { + fn render(&mut self, cx: &mut ViewContext) -> impl Component { v_stack() .w_full() .h_full() diff --git a/crates/ui2/src/components/facepile.rs b/crates/ui2/src/components/facepile.rs index 9a96d80010..898489f1ed 100644 --- a/crates/ui2/src/components/facepile.rs +++ b/crates/ui2/src/components/facepile.rs @@ -1,23 +1,19 @@ -use std::marker::PhantomData; - use crate::prelude::*; use crate::{Avatar, Player}; -#[derive(Element)] -pub struct Facepile { - state_type: PhantomData, +#[derive(Component)] +pub struct Facepile { players: Vec, } -impl Facepile { +impl Facepile { pub fn new>(players: P) -> Self { Self { - state_type: PhantomData, players: players.collect(), } } - fn render(&mut self, _view: &mut S, cx: &mut ViewContext) -> impl Element { + fn render(self, _view: &mut S, cx: &mut ViewContext) -> impl Component { let player_count = self.players.len(); let player_list = self.players.iter().enumerate().map(|(ix, player)| { let isnt_last = ix < player_count - 1; @@ -39,27 +35,19 @@ mod stories { use super::*; - #[derive(Element)] - pub struct FacepileStory { - state_type: PhantomData, - } + #[derive(Component)] + pub struct FacepileStory; - impl FacepileStory { + impl FacepileStory { pub fn new() -> Self { - Self { - state_type: PhantomData, - } + Self } - fn render( - &mut self, - _view: &mut S, - cx: &mut ViewContext, - ) -> impl Element { + fn render(self, _view: &mut S, cx: &mut ViewContext) -> impl Component { let players = static_players(); Story::container(cx) - .child(Story::title_for::<_, Facepile>(cx)) + .child(Story::title_for::<_, Facepile>(cx)) .child(Story::label(cx, "Default")) .child( div() diff --git a/crates/ui2/src/components/icon_button.rs b/crates/ui2/src/components/icon_button.rs index b74165720d..2891ce6bd8 100644 --- a/crates/ui2/src/components/icon_button.rs +++ b/crates/ui2/src/components/icon_button.rs @@ -1,4 +1,3 @@ -use std::marker::PhantomData; use std::sync::Arc; use gpui2::MouseButton; @@ -6,19 +5,18 @@ use gpui2::MouseButton; use crate::{h_stack, prelude::*}; use crate::{ClickHandler, Icon, IconColor, IconElement}; -struct IconButtonHandlers { +struct IconButtonHandlers { click: Option>, } -impl Default for IconButtonHandlers { +impl Default for IconButtonHandlers { fn default() -> Self { Self { click: None } } } -#[derive(Element)] -pub struct IconButton { - state_type: PhantomData, +#[derive(Component)] +pub struct IconButton { id: ElementId, icon: Icon, color: IconColor, @@ -27,10 +25,9 @@ pub struct IconButton { handlers: IconButtonHandlers, } -impl IconButton { +impl IconButton { pub fn new(id: impl Into, icon: Icon) -> Self { Self { - state_type: PhantomData, id: id.into(), icon, color: IconColor::default(), @@ -60,15 +57,12 @@ impl IconButton { self } - pub fn on_click( - mut self, - handler: impl Fn(&mut S, &mut ViewContext) + 'static + Send + Sync, - ) -> Self { + pub fn on_click(mut self, handler: impl 'static + Fn(&mut S, &mut ViewContext) + Send + Sync) -> Self { self.handlers.click = Some(Arc::new(handler)); self } - fn render(&mut self, _view: &mut S, cx: &mut ViewContext) -> impl Element { + fn render(self, _view: &mut S, cx: &mut ViewContext) -> impl Component { let theme = theme(cx); let icon_color = match (self.state, self.color) { diff --git a/crates/ui2/src/components/keybinding.rs b/crates/ui2/src/components/keybinding.rs index 2fb40adac6..2b0eefa4ea 100644 --- a/crates/ui2/src/components/keybinding.rs +++ b/crates/ui2/src/components/keybinding.rs @@ -1,14 +1,11 @@ use std::collections::HashSet; -use std::marker::PhantomData; use strum::{EnumIter, IntoEnumIterator}; use crate::prelude::*; -#[derive(Element)] -pub struct Keybinding { - state_type: PhantomData, - +#[derive(Component)] +pub struct Keybinding { /// A keybinding consists of a key and a set of modifier keys. /// More then one keybinding produces a chord. /// @@ -16,10 +13,9 @@ pub struct Keybinding { keybinding: Vec<(String, ModifierKeys)>, } -impl Keybinding { +impl Keybinding { pub fn new(key: String, modifiers: ModifierKeys) -> Self { Self { - state_type: PhantomData, keybinding: vec![(key, modifiers)], } } @@ -29,12 +25,11 @@ impl Keybinding { second_note: (String, ModifierKeys), ) -> Self { Self { - state_type: PhantomData, keybinding: vec![first_note, second_note], } } - fn render(&mut self, _view: &mut S, cx: &mut ViewContext) -> impl Element { + fn render(self, _view: &mut S, cx: &mut ViewContext) -> impl Component { div() .flex() .gap_2() @@ -54,21 +49,17 @@ impl Keybinding { } } -#[derive(Element)] -pub struct Key { - state_type: PhantomData, +#[derive(Component)] +pub struct Key { key: SharedString, } -impl Key { +impl Key { pub fn new(key: impl Into) -> Self { - Self { - state_type: PhantomData, - key: key.into(), - } + Self { key: key.into() } } - fn render(&mut self, _view: &mut S, cx: &mut ViewContext) -> impl Element { + fn render(self, _view: &mut S, cx: &mut ViewContext) -> impl Component { let theme = theme(cx); div() @@ -173,27 +164,19 @@ mod stories { use super::*; - #[derive(Element)] - pub struct KeybindingStory { - state_type: PhantomData, - } + #[derive(Component)] + pub struct KeybindingStory; - impl KeybindingStory { + impl KeybindingStory { pub fn new() -> Self { - Self { - state_type: PhantomData, - } + Self } - fn render( - &mut self, - _view: &mut S, - cx: &mut ViewContext, - ) -> impl Element { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { let all_modifier_permutations = ModifierKey::iter().permutations(2); Story::container(cx) - .child(Story::title_for::<_, Keybinding>(cx)) + .child(Story::title_for::<_, Keybinding>(cx)) .child(Story::label(cx, "Single Key")) .child(Keybinding::new("Z".to_string(), ModifierKeys::new())) .child(Story::label(cx, "Single Key with Modifier")) diff --git a/crates/ui2/src/components/language_selector.rs b/crates/ui2/src/components/language_selector.rs index 224db69754..db42b28281 100644 --- a/crates/ui2/src/components/language_selector.rs +++ b/crates/ui2/src/components/language_selector.rs @@ -1,23 +1,17 @@ -use std::marker::PhantomData; - use crate::prelude::*; use crate::{OrderMethod, Palette, PaletteItem}; -#[derive(Element)] -pub struct LanguageSelector { +#[derive(Component)] +pub struct LanguageSelector { id: ElementId, - state_type: PhantomData, } -impl LanguageSelector { +impl LanguageSelector { pub fn new(id: impl Into) -> Self { - Self { - id: id.into(), - state_type: PhantomData, - } + Self { id: id.into() } } - fn render(&mut self, _view: &mut S, cx: &mut ViewContext) -> impl Element { + fn render(self, _view: &mut S, cx: &mut ViewContext) -> impl Component { div().id(self.id.clone()).child( Palette::new("palette") .items(vec![ @@ -48,25 +42,17 @@ mod stories { use super::*; - #[derive(Element)] - pub struct LanguageSelectorStory { - state_type: PhantomData, - } + #[derive(Component)] + pub struct LanguageSelectorStory; - impl LanguageSelectorStory { + impl LanguageSelectorStory { pub fn new() -> Self { - Self { - state_type: PhantomData, - } + Self } - fn render( - &mut self, - _view: &mut S, - cx: &mut ViewContext, - ) -> impl Element { + fn render(self, _view: &mut S, cx: &mut ViewContext) -> impl Component { Story::container(cx) - .child(Story::title_for::<_, LanguageSelector>(cx)) + .child(Story::title_for::<_, LanguageSelector>(cx)) .child(Story::label(cx, "Default")) .child(LanguageSelector::new("language-selector")) } diff --git a/crates/ui2/src/components/list.rs b/crates/ui2/src/components/list.rs index 99282fa06d..0bf413e217 100644 --- a/crates/ui2/src/components/list.rs +++ b/crates/ui2/src/components/list.rs @@ -1,5 +1,3 @@ -use std::marker::PhantomData; - use gpui2::{div, relative, Div}; use crate::settings::user_settings; @@ -17,9 +15,8 @@ pub enum ListItemVariant { Inset, } -#[derive(Element)] -pub struct ListHeader { - state_type: PhantomData, +#[derive(Component)] +pub struct ListHeader { label: SharedString, left_icon: Option, variant: ListItemVariant, @@ -27,10 +24,9 @@ pub struct ListHeader { toggleable: Toggleable, } -impl ListHeader { +impl ListHeader { pub fn new(label: impl Into) -> Self { Self { - state_type: PhantomData, label: label.into(), left_icon: None, variant: ListItemVariant::default(), @@ -59,7 +55,7 @@ impl ListHeader { self } - fn disclosure_control(&self) -> Div { + fn disclosure_control(&self) -> Div { let is_toggleable = self.toggleable != Toggleable::NotToggleable; let is_toggled = Toggleable::is_toggled(&self.toggleable); @@ -92,7 +88,7 @@ impl ListHeader { } } - fn render(&mut self, _view: &mut S, cx: &mut ViewContext) -> impl Element { + fn render(self, _view: &mut S, cx: &mut ViewContext) -> impl Component { let theme = theme(cx); let is_toggleable = self.toggleable != Toggleable::NotToggleable; @@ -134,18 +130,16 @@ impl ListHeader { } } -#[derive(Element)] -pub struct ListSubHeader { - state_type: PhantomData, +#[derive(Component)] +pub struct ListSubHeader { label: SharedString, left_icon: Option, variant: ListItemVariant, } -impl ListSubHeader { +impl ListSubHeader { pub fn new(label: impl Into) -> Self { Self { - state_type: PhantomData, label: label.into(), left_icon: None, variant: ListItemVariant::default(), @@ -157,7 +151,7 @@ impl ListSubHeader { self } - fn render(&mut self, _view: &mut S, cx: &mut ViewContext) -> impl Element { + fn render(self, _view: &mut S, cx: &mut ViewContext) -> impl Component { h_stack().flex_1().w_full().relative().py_1().child( div() .h_6() @@ -197,40 +191,40 @@ pub enum ListEntrySize { Medium, } -#[derive(Element)] -pub enum ListItem { - Entry(ListEntry), +#[derive(Component)] +pub enum ListItem { + Entry(ListEntry), Details(ListDetailsEntry), - Separator(ListSeparator), - Header(ListSubHeader), + Separator(ListSeparator), + Header(ListSubHeader), } -impl From> for ListItem { - fn from(entry: ListEntry) -> Self { +impl From for ListItem { + fn from(entry: ListEntry) -> Self { Self::Entry(entry) } } -impl From> for ListItem { +impl From> for ListItem { fn from(entry: ListDetailsEntry) -> Self { Self::Details(entry) } } -impl From> for ListItem { - fn from(entry: ListSeparator) -> Self { +impl From for ListItem { + fn from(entry: ListSeparator) -> Self { Self::Separator(entry) } } -impl From> for ListItem { - fn from(entry: ListSubHeader) -> Self { +impl From for ListItem { + fn from(entry: ListSubHeader) -> Self { Self::Header(entry) } } -impl ListItem { - fn render(&mut self, view: &mut S, cx: &mut ViewContext) -> impl Element { +impl ListItem { + fn render(self, view: &mut S, cx: &mut ViewContext) -> impl Component { match self { ListItem::Entry(entry) => div().child(entry.render(view, cx)), ListItem::Separator(separator) => div().child(separator.render(view, cx)), @@ -239,11 +233,11 @@ impl ListItem { } } - pub fn new(label: Label) -> Self { + pub fn new(label: Label) -> Self { Self::Entry(ListEntry::new(label)) } - pub fn as_entry(&mut self) -> Option<&mut ListEntry> { + pub fn as_entry(&mut self) -> Option<&mut ListEntry> { if let Self::Entry(entry) = self { Some(entry) } else { @@ -252,11 +246,11 @@ impl ListItem { } } -#[derive(Element)] -pub struct ListEntry { +#[derive(Component)] +pub struct ListEntry { disclosure_control_style: DisclosureControlVisibility, indent_level: u32, - label: Option>, + label: Option