Merge pull request #661 from zed-industries/follow
Introduce basic following experience
This commit is contained in:
commit
004f98cc6d
35 changed files with 3061 additions and 510 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1629,6 +1629,7 @@ dependencies = [
|
||||||
"postage",
|
"postage",
|
||||||
"project",
|
"project",
|
||||||
"rand 0.8.3",
|
"rand 0.8.3",
|
||||||
|
"rpc",
|
||||||
"serde",
|
"serde",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"smol",
|
"smol",
|
||||||
|
|
|
@ -181,7 +181,7 @@ impl Entity for Channel {
|
||||||
|
|
||||||
impl Channel {
|
impl Channel {
|
||||||
pub fn init(rpc: &Arc<Client>) {
|
pub fn init(rpc: &Arc<Client>) {
|
||||||
rpc.add_entity_message_handler(Self::handle_message_sent);
|
rpc.add_model_message_handler(Self::handle_message_sent);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(
|
pub fn new(
|
||||||
|
|
|
@ -13,8 +13,8 @@ use async_tungstenite::tungstenite::{
|
||||||
};
|
};
|
||||||
use futures::{future::LocalBoxFuture, FutureExt, StreamExt};
|
use futures::{future::LocalBoxFuture, FutureExt, StreamExt};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
action, AnyModelHandle, AnyWeakModelHandle, AsyncAppContext, Entity, ModelContext, ModelHandle,
|
action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, AsyncAppContext,
|
||||||
MutableAppContext, Task,
|
Entity, ModelContext, ModelHandle, MutableAppContext, Task, View, ViewContext, ViewHandle,
|
||||||
};
|
};
|
||||||
use http::HttpClient;
|
use http::HttpClient;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
|
@ -136,26 +136,37 @@ impl Status {
|
||||||
struct ClientState {
|
struct ClientState {
|
||||||
credentials: Option<Credentials>,
|
credentials: Option<Credentials>,
|
||||||
status: (watch::Sender<Status>, watch::Receiver<Status>),
|
status: (watch::Sender<Status>, watch::Receiver<Status>),
|
||||||
entity_id_extractors: HashMap<TypeId, Box<dyn Send + Sync + Fn(&dyn AnyTypedEnvelope) -> u64>>,
|
entity_id_extractors: HashMap<TypeId, fn(&dyn AnyTypedEnvelope) -> u64>,
|
||||||
_reconnect_task: Option<Task<()>>,
|
_reconnect_task: Option<Task<()>>,
|
||||||
reconnect_interval: Duration,
|
reconnect_interval: Duration,
|
||||||
models_by_entity_type_and_remote_id: HashMap<(TypeId, u64), AnyWeakModelHandle>,
|
entities_by_type_and_remote_id: HashMap<(TypeId, u64), AnyWeakEntityHandle>,
|
||||||
models_by_message_type: HashMap<TypeId, AnyWeakModelHandle>,
|
models_by_message_type: HashMap<TypeId, AnyWeakModelHandle>,
|
||||||
model_types_by_message_type: HashMap<TypeId, TypeId>,
|
entity_types_by_message_type: HashMap<TypeId, TypeId>,
|
||||||
message_handlers: HashMap<
|
message_handlers: HashMap<
|
||||||
TypeId,
|
TypeId,
|
||||||
Arc<
|
Arc<
|
||||||
dyn Send
|
dyn Send
|
||||||
+ Sync
|
+ Sync
|
||||||
+ Fn(
|
+ Fn(
|
||||||
AnyModelHandle,
|
AnyEntityHandle,
|
||||||
Box<dyn AnyTypedEnvelope>,
|
Box<dyn AnyTypedEnvelope>,
|
||||||
|
&Arc<Client>,
|
||||||
AsyncAppContext,
|
AsyncAppContext,
|
||||||
) -> LocalBoxFuture<'static, Result<()>>,
|
) -> LocalBoxFuture<'static, Result<()>>,
|
||||||
>,
|
>,
|
||||||
>,
|
>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum AnyWeakEntityHandle {
|
||||||
|
Model(AnyWeakModelHandle),
|
||||||
|
View(AnyWeakViewHandle),
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AnyEntityHandle {
|
||||||
|
Model(AnyModelHandle),
|
||||||
|
View(AnyViewHandle),
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Credentials {
|
pub struct Credentials {
|
||||||
pub user_id: u64,
|
pub user_id: u64,
|
||||||
|
@ -171,8 +182,8 @@ impl Default for ClientState {
|
||||||
_reconnect_task: None,
|
_reconnect_task: None,
|
||||||
reconnect_interval: Duration::from_secs(5),
|
reconnect_interval: Duration::from_secs(5),
|
||||||
models_by_message_type: Default::default(),
|
models_by_message_type: Default::default(),
|
||||||
models_by_entity_type_and_remote_id: Default::default(),
|
entities_by_type_and_remote_id: Default::default(),
|
||||||
model_types_by_message_type: Default::default(),
|
entity_types_by_message_type: Default::default(),
|
||||||
message_handlers: Default::default(),
|
message_handlers: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -195,13 +206,13 @@ impl Drop for Subscription {
|
||||||
Subscription::Entity { client, id } => {
|
Subscription::Entity { client, id } => {
|
||||||
if let Some(client) = client.upgrade() {
|
if let Some(client) = client.upgrade() {
|
||||||
let mut state = client.state.write();
|
let mut state = client.state.write();
|
||||||
let _ = state.models_by_entity_type_and_remote_id.remove(id);
|
let _ = state.entities_by_type_and_remote_id.remove(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Subscription::Message { client, id } => {
|
Subscription::Message { client, id } => {
|
||||||
if let Some(client) = client.upgrade() {
|
if let Some(client) = client.upgrade() {
|
||||||
let mut state = client.state.write();
|
let mut state = client.state.write();
|
||||||
let _ = state.model_types_by_message_type.remove(id);
|
let _ = state.entity_types_by_message_type.remove(id);
|
||||||
let _ = state.message_handlers.remove(id);
|
let _ = state.message_handlers.remove(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -239,7 +250,7 @@ impl Client {
|
||||||
state._reconnect_task.take();
|
state._reconnect_task.take();
|
||||||
state.message_handlers.clear();
|
state.message_handlers.clear();
|
||||||
state.models_by_message_type.clear();
|
state.models_by_message_type.clear();
|
||||||
state.models_by_entity_type_and_remote_id.clear();
|
state.entities_by_type_and_remote_id.clear();
|
||||||
state.entity_id_extractors.clear();
|
state.entity_id_extractors.clear();
|
||||||
self.peer.reset();
|
self.peer.reset();
|
||||||
}
|
}
|
||||||
|
@ -313,17 +324,32 @@ impl Client {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn add_view_for_remote_entity<T: View>(
|
||||||
|
self: &Arc<Self>,
|
||||||
|
remote_id: u64,
|
||||||
|
cx: &mut ViewContext<T>,
|
||||||
|
) -> Subscription {
|
||||||
|
let id = (TypeId::of::<T>(), remote_id);
|
||||||
|
self.state
|
||||||
|
.write()
|
||||||
|
.entities_by_type_and_remote_id
|
||||||
|
.insert(id, AnyWeakEntityHandle::View(cx.weak_handle().into()));
|
||||||
|
Subscription::Entity {
|
||||||
|
client: Arc::downgrade(self),
|
||||||
|
id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn add_model_for_remote_entity<T: Entity>(
|
pub fn add_model_for_remote_entity<T: Entity>(
|
||||||
self: &Arc<Self>,
|
self: &Arc<Self>,
|
||||||
remote_id: u64,
|
remote_id: u64,
|
||||||
cx: &mut ModelContext<T>,
|
cx: &mut ModelContext<T>,
|
||||||
) -> Subscription {
|
) -> Subscription {
|
||||||
let handle = AnyModelHandle::from(cx.handle());
|
|
||||||
let mut state = self.state.write();
|
|
||||||
let id = (TypeId::of::<T>(), remote_id);
|
let id = (TypeId::of::<T>(), remote_id);
|
||||||
state
|
self.state
|
||||||
.models_by_entity_type_and_remote_id
|
.write()
|
||||||
.insert(id, handle.downgrade());
|
.entities_by_type_and_remote_id
|
||||||
|
.insert(id, AnyWeakEntityHandle::Model(cx.weak_handle().into()));
|
||||||
Subscription::Entity {
|
Subscription::Entity {
|
||||||
client: Arc::downgrade(self),
|
client: Arc::downgrade(self),
|
||||||
id,
|
id,
|
||||||
|
@ -346,7 +372,6 @@ impl Client {
|
||||||
{
|
{
|
||||||
let message_type_id = TypeId::of::<M>();
|
let message_type_id = TypeId::of::<M>();
|
||||||
|
|
||||||
let client = Arc::downgrade(self);
|
|
||||||
let mut state = self.state.write();
|
let mut state = self.state.write();
|
||||||
state
|
state
|
||||||
.models_by_message_type
|
.models_by_message_type
|
||||||
|
@ -354,14 +379,15 @@ impl Client {
|
||||||
|
|
||||||
let prev_handler = state.message_handlers.insert(
|
let prev_handler = state.message_handlers.insert(
|
||||||
message_type_id,
|
message_type_id,
|
||||||
Arc::new(move |handle, envelope, cx| {
|
Arc::new(move |handle, envelope, client, cx| {
|
||||||
|
let handle = if let AnyEntityHandle::Model(handle) = handle {
|
||||||
|
handle
|
||||||
|
} else {
|
||||||
|
unreachable!();
|
||||||
|
};
|
||||||
let model = handle.downcast::<E>().unwrap();
|
let model = handle.downcast::<E>().unwrap();
|
||||||
let envelope = envelope.into_any().downcast::<TypedEnvelope<M>>().unwrap();
|
let envelope = envelope.into_any().downcast::<TypedEnvelope<M>>().unwrap();
|
||||||
if let Some(client) = client.upgrade() {
|
|
||||||
handler(model, *envelope, client.clone(), cx).boxed_local()
|
handler(model, *envelope, client.clone(), cx).boxed_local()
|
||||||
} else {
|
|
||||||
async move { Ok(()) }.boxed_local()
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
if prev_handler.is_some() {
|
if prev_handler.is_some() {
|
||||||
|
@ -374,7 +400,26 @@ impl Client {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_entity_message_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
|
pub fn add_view_message_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
|
||||||
|
where
|
||||||
|
M: EntityMessage,
|
||||||
|
E: View,
|
||||||
|
H: 'static
|
||||||
|
+ Send
|
||||||
|
+ Sync
|
||||||
|
+ Fn(ViewHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
|
||||||
|
F: 'static + Future<Output = Result<()>>,
|
||||||
|
{
|
||||||
|
self.add_entity_message_handler::<M, E, _, _>(move |handle, message, client, cx| {
|
||||||
|
if let AnyEntityHandle::View(handle) = handle {
|
||||||
|
handler(handle.downcast::<E>().unwrap(), message, client, cx)
|
||||||
|
} else {
|
||||||
|
unreachable!();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_model_message_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
|
||||||
where
|
where
|
||||||
M: EntityMessage,
|
M: EntityMessage,
|
||||||
E: Entity,
|
E: Entity,
|
||||||
|
@ -383,38 +428,51 @@ impl Client {
|
||||||
+ Sync
|
+ Sync
|
||||||
+ Fn(ModelHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
|
+ Fn(ModelHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
|
||||||
F: 'static + Future<Output = Result<()>>,
|
F: 'static + Future<Output = Result<()>>,
|
||||||
|
{
|
||||||
|
self.add_entity_message_handler::<M, E, _, _>(move |handle, message, client, cx| {
|
||||||
|
if let AnyEntityHandle::Model(handle) = handle {
|
||||||
|
handler(handle.downcast::<E>().unwrap(), message, client, cx)
|
||||||
|
} else {
|
||||||
|
unreachable!();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_entity_message_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
|
||||||
|
where
|
||||||
|
M: EntityMessage,
|
||||||
|
E: Entity,
|
||||||
|
H: 'static
|
||||||
|
+ Send
|
||||||
|
+ Sync
|
||||||
|
+ Fn(AnyEntityHandle, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
|
||||||
|
F: 'static + Future<Output = Result<()>>,
|
||||||
{
|
{
|
||||||
let model_type_id = TypeId::of::<E>();
|
let model_type_id = TypeId::of::<E>();
|
||||||
let message_type_id = TypeId::of::<M>();
|
let message_type_id = TypeId::of::<M>();
|
||||||
|
|
||||||
let client = Arc::downgrade(self);
|
|
||||||
let mut state = self.state.write();
|
let mut state = self.state.write();
|
||||||
state
|
state
|
||||||
.model_types_by_message_type
|
.entity_types_by_message_type
|
||||||
.insert(message_type_id, model_type_id);
|
.insert(message_type_id, model_type_id);
|
||||||
state
|
state
|
||||||
.entity_id_extractors
|
.entity_id_extractors
|
||||||
.entry(message_type_id)
|
.entry(message_type_id)
|
||||||
.or_insert_with(|| {
|
.or_insert_with(|| {
|
||||||
Box::new(|envelope| {
|
|envelope| {
|
||||||
let envelope = envelope
|
envelope
|
||||||
.as_any()
|
.as_any()
|
||||||
.downcast_ref::<TypedEnvelope<M>>()
|
.downcast_ref::<TypedEnvelope<M>>()
|
||||||
.unwrap();
|
.unwrap()
|
||||||
envelope.payload.remote_entity_id()
|
.payload
|
||||||
})
|
.remote_entity_id()
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let prev_handler = state.message_handlers.insert(
|
let prev_handler = state.message_handlers.insert(
|
||||||
message_type_id,
|
message_type_id,
|
||||||
Arc::new(move |handle, envelope, cx| {
|
Arc::new(move |handle, envelope, client, cx| {
|
||||||
let model = handle.downcast::<E>().unwrap();
|
|
||||||
let envelope = envelope.into_any().downcast::<TypedEnvelope<M>>().unwrap();
|
let envelope = envelope.into_any().downcast::<TypedEnvelope<M>>().unwrap();
|
||||||
if let Some(client) = client.upgrade() {
|
handler(handle, *envelope, client.clone(), cx).boxed_local()
|
||||||
handler(model, *envelope, client.clone(), cx).boxed_local()
|
|
||||||
} else {
|
|
||||||
async move { Ok(()) }.boxed_local()
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
if prev_handler.is_some() {
|
if prev_handler.is_some() {
|
||||||
|
@ -422,7 +480,7 @@ impl Client {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_entity_request_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
|
pub fn add_model_request_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
|
||||||
where
|
where
|
||||||
M: EntityMessage + RequestMessage,
|
M: EntityMessage + RequestMessage,
|
||||||
E: Entity,
|
E: Entity,
|
||||||
|
@ -432,10 +490,39 @@ impl Client {
|
||||||
+ Fn(ModelHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
|
+ Fn(ModelHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
|
||||||
F: 'static + Future<Output = Result<M::Response>>,
|
F: 'static + Future<Output = Result<M::Response>>,
|
||||||
{
|
{
|
||||||
self.add_entity_message_handler(move |model, envelope, client, cx| {
|
self.add_model_message_handler(move |entity, envelope, client, cx| {
|
||||||
let receipt = envelope.receipt();
|
Self::respond_to_request::<M, _>(
|
||||||
let response = handler(model, envelope, client.clone(), cx);
|
envelope.receipt(),
|
||||||
async move {
|
handler(entity, envelope, client.clone(), cx),
|
||||||
|
client,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_view_request_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
|
||||||
|
where
|
||||||
|
M: EntityMessage + RequestMessage,
|
||||||
|
E: View,
|
||||||
|
H: 'static
|
||||||
|
+ Send
|
||||||
|
+ Sync
|
||||||
|
+ Fn(ViewHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
|
||||||
|
F: 'static + Future<Output = Result<M::Response>>,
|
||||||
|
{
|
||||||
|
self.add_view_message_handler(move |entity, envelope, client, cx| {
|
||||||
|
Self::respond_to_request::<M, _>(
|
||||||
|
envelope.receipt(),
|
||||||
|
handler(entity, envelope, client.clone(), cx),
|
||||||
|
client,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn respond_to_request<T: RequestMessage, F: Future<Output = Result<T::Response>>>(
|
||||||
|
receipt: Receipt<T>,
|
||||||
|
response: F,
|
||||||
|
client: Arc<Self>,
|
||||||
|
) -> Result<()> {
|
||||||
match response.await {
|
match response.await {
|
||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
client.respond(receipt, response)?;
|
client.respond(receipt, response)?;
|
||||||
|
@ -452,8 +539,6 @@ impl Client {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn has_keychain_credentials(&self, cx: &AsyncAppContext) -> bool {
|
pub fn has_keychain_credentials(&self, cx: &AsyncAppContext) -> bool {
|
||||||
read_credentials_from_keychain(cx).is_some()
|
read_credentials_from_keychain(cx).is_some()
|
||||||
|
@ -561,24 +646,26 @@ impl Client {
|
||||||
.models_by_message_type
|
.models_by_message_type
|
||||||
.get(&payload_type_id)
|
.get(&payload_type_id)
|
||||||
.and_then(|model| model.upgrade(&cx))
|
.and_then(|model| model.upgrade(&cx))
|
||||||
|
.map(AnyEntityHandle::Model)
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
let model_type_id =
|
let entity_type_id =
|
||||||
*state.model_types_by_message_type.get(&payload_type_id)?;
|
*state.entity_types_by_message_type.get(&payload_type_id)?;
|
||||||
let entity_id = state
|
let entity_id = state
|
||||||
.entity_id_extractors
|
.entity_id_extractors
|
||||||
.get(&message.payload_type_id())
|
.get(&message.payload_type_id())
|
||||||
.map(|extract_entity_id| {
|
.map(|extract_entity_id| {
|
||||||
(extract_entity_id)(message.as_ref())
|
(extract_entity_id)(message.as_ref())
|
||||||
})?;
|
})?;
|
||||||
let model = state
|
|
||||||
.models_by_entity_type_and_remote_id
|
let entity = state
|
||||||
.get(&(model_type_id, entity_id))?;
|
.entities_by_type_and_remote_id
|
||||||
if let Some(model) = model.upgrade(&cx) {
|
.get(&(entity_type_id, entity_id))?;
|
||||||
Some(model)
|
if let Some(entity) = entity.upgrade(&cx) {
|
||||||
|
Some(entity)
|
||||||
} else {
|
} else {
|
||||||
state
|
state
|
||||||
.models_by_entity_type_and_remote_id
|
.entities_by_type_and_remote_id
|
||||||
.remove(&(model_type_id, entity_id));
|
.remove(&(entity_type_id, entity_id));
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -593,7 +680,7 @@ impl Client {
|
||||||
if let Some(handler) = state.message_handlers.get(&payload_type_id).cloned()
|
if let Some(handler) = state.message_handlers.get(&payload_type_id).cloned()
|
||||||
{
|
{
|
||||||
drop(state); // Avoid deadlocks if the handler interacts with rpc::Client
|
drop(state); // Avoid deadlocks if the handler interacts with rpc::Client
|
||||||
let future = handler(model, message, cx.clone());
|
let future = handler(model, message, &this, cx.clone());
|
||||||
|
|
||||||
let client_id = this.id;
|
let client_id = this.id;
|
||||||
log::debug!(
|
log::debug!(
|
||||||
|
@ -891,6 +978,15 @@ impl Client {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AnyWeakEntityHandle {
|
||||||
|
fn upgrade(&self, cx: &AsyncAppContext) -> Option<AnyEntityHandle> {
|
||||||
|
match self {
|
||||||
|
AnyWeakEntityHandle::Model(handle) => handle.upgrade(cx).map(AnyEntityHandle::Model),
|
||||||
|
AnyWeakEntityHandle::View(handle) => handle.upgrade(cx).map(AnyEntityHandle::View),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option<Credentials> {
|
fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option<Credentials> {
|
||||||
if IMPERSONATE_LOGIN.is_some() {
|
if IMPERSONATE_LOGIN.is_some() {
|
||||||
return None;
|
return None;
|
||||||
|
@ -994,7 +1090,7 @@ mod tests {
|
||||||
|
|
||||||
let (done_tx1, mut done_rx1) = smol::channel::unbounded();
|
let (done_tx1, mut done_rx1) = smol::channel::unbounded();
|
||||||
let (done_tx2, mut done_rx2) = smol::channel::unbounded();
|
let (done_tx2, mut done_rx2) = smol::channel::unbounded();
|
||||||
client.add_entity_message_handler(
|
client.add_model_message_handler(
|
||||||
move |model: ModelHandle<Model>, _: TypedEnvelope<proto::UnshareProject>, _, cx| {
|
move |model: ModelHandle<Model>, _: TypedEnvelope<proto::UnshareProject>, _, cx| {
|
||||||
match model.read_with(&cx, |model, _| model.id) {
|
match model.read_with(&cx, |model, _| model.id) {
|
||||||
1 => done_tx1.try_send(()).unwrap(),
|
1 => done_tx1.try_send(()).unwrap(),
|
||||||
|
|
|
@ -450,6 +450,10 @@ impl workspace::Item for ProjectDiagnosticsEditor {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn project_entry_id(&self, _: &AppContext) -> Option<project::ProjectEntryId> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) {
|
fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) {
|
||||||
self.editor
|
self.editor
|
||||||
.update(cx, |editor, cx| editor.navigate(data, cx));
|
.update(cx, |editor, cx| editor.navigate(data, cx));
|
||||||
|
|
|
@ -27,6 +27,7 @@ gpui = { path = "../gpui" }
|
||||||
language = { path = "../language" }
|
language = { path = "../language" }
|
||||||
lsp = { path = "../lsp" }
|
lsp = { path = "../lsp" }
|
||||||
project = { path = "../project" }
|
project = { path = "../project" }
|
||||||
|
rpc = { path = "../rpc" }
|
||||||
snippet = { path = "../snippet" }
|
snippet = { path = "../snippet" }
|
||||||
sum_tree = { path = "../sum_tree" }
|
sum_tree = { path = "../sum_tree" }
|
||||||
theme = { path = "../theme" }
|
theme = { path = "../theme" }
|
||||||
|
|
|
@ -340,9 +340,8 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||||
cx.add_async_action(Editor::confirm_rename);
|
cx.add_async_action(Editor::confirm_rename);
|
||||||
cx.add_async_action(Editor::find_all_references);
|
cx.add_async_action(Editor::find_all_references);
|
||||||
|
|
||||||
workspace::register_project_item(cx, |project, buffer, cx| {
|
workspace::register_project_item::<Editor>(cx);
|
||||||
Editor::for_buffer(buffer, Some(project), cx)
|
workspace::register_followable_item::<Editor>(cx);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
trait InvalidationRegion {
|
trait InvalidationRegion {
|
||||||
|
@ -431,8 +430,8 @@ pub struct Editor {
|
||||||
select_larger_syntax_node_stack: Vec<Box<[Selection<usize>]>>,
|
select_larger_syntax_node_stack: Vec<Box<[Selection<usize>]>>,
|
||||||
active_diagnostics: Option<ActiveDiagnosticGroup>,
|
active_diagnostics: Option<ActiveDiagnosticGroup>,
|
||||||
scroll_position: Vector2F,
|
scroll_position: Vector2F,
|
||||||
scroll_top_anchor: Option<Anchor>,
|
scroll_top_anchor: Anchor,
|
||||||
autoscroll_request: Option<Autoscroll>,
|
autoscroll_request: Option<(Autoscroll, bool)>,
|
||||||
soft_wrap_mode_override: Option<settings::SoftWrap>,
|
soft_wrap_mode_override: Option<settings::SoftWrap>,
|
||||||
get_field_editor_theme: Option<GetFieldEditorTheme>,
|
get_field_editor_theme: Option<GetFieldEditorTheme>,
|
||||||
override_text_style: Option<Box<OverrideTextStyle>>,
|
override_text_style: Option<Box<OverrideTextStyle>>,
|
||||||
|
@ -457,6 +456,7 @@ pub struct Editor {
|
||||||
pending_rename: Option<RenameState>,
|
pending_rename: Option<RenameState>,
|
||||||
searchable: bool,
|
searchable: bool,
|
||||||
cursor_shape: CursorShape,
|
cursor_shape: CursorShape,
|
||||||
|
leader_replica_id: Option<u16>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct EditorSnapshot {
|
pub struct EditorSnapshot {
|
||||||
|
@ -465,7 +465,7 @@ pub struct EditorSnapshot {
|
||||||
pub placeholder_text: Option<Arc<str>>,
|
pub placeholder_text: Option<Arc<str>>,
|
||||||
is_focused: bool,
|
is_focused: bool,
|
||||||
scroll_position: Vector2F,
|
scroll_position: Vector2F,
|
||||||
scroll_top_anchor: Option<Anchor>,
|
scroll_top_anchor: Anchor,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -909,7 +909,7 @@ impl Editor {
|
||||||
get_field_editor_theme,
|
get_field_editor_theme,
|
||||||
project,
|
project,
|
||||||
scroll_position: Vector2F::zero(),
|
scroll_position: Vector2F::zero(),
|
||||||
scroll_top_anchor: None,
|
scroll_top_anchor: Anchor::min(),
|
||||||
autoscroll_request: None,
|
autoscroll_request: None,
|
||||||
focused: false,
|
focused: false,
|
||||||
show_local_cursors: false,
|
show_local_cursors: false,
|
||||||
|
@ -932,6 +932,7 @@ impl Editor {
|
||||||
searchable: true,
|
searchable: true,
|
||||||
override_text_style: None,
|
override_text_style: None,
|
||||||
cursor_shape: Default::default(),
|
cursor_shape: Default::default(),
|
||||||
|
leader_replica_id: None,
|
||||||
};
|
};
|
||||||
this.end_selection(cx);
|
this.end_selection(cx);
|
||||||
|
|
||||||
|
@ -1014,10 +1015,19 @@ impl Editor {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_scroll_position(&mut self, scroll_position: Vector2F, cx: &mut ViewContext<Self>) {
|
pub fn set_scroll_position(&mut self, scroll_position: Vector2F, cx: &mut ViewContext<Self>) {
|
||||||
|
self.set_scroll_position_internal(scroll_position, true, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_scroll_position_internal(
|
||||||
|
&mut self,
|
||||||
|
scroll_position: Vector2F,
|
||||||
|
local: bool,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
let map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
let map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||||
|
|
||||||
if scroll_position.y() == 0. {
|
if scroll_position.y() == 0. {
|
||||||
self.scroll_top_anchor = None;
|
self.scroll_top_anchor = Anchor::min();
|
||||||
self.scroll_position = scroll_position;
|
self.scroll_position = scroll_position;
|
||||||
} else {
|
} else {
|
||||||
let scroll_top_buffer_offset =
|
let scroll_top_buffer_offset =
|
||||||
|
@ -1029,9 +1039,22 @@ impl Editor {
|
||||||
scroll_position.x(),
|
scroll_position.x(),
|
||||||
scroll_position.y() - anchor.to_display_point(&map).row() as f32,
|
scroll_position.y() - anchor.to_display_point(&map).row() as f32,
|
||||||
);
|
);
|
||||||
self.scroll_top_anchor = Some(anchor);
|
self.scroll_top_anchor = anchor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cx.emit(Event::ScrollPositionChanged { local });
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_scroll_top_anchor(
|
||||||
|
&mut self,
|
||||||
|
anchor: Anchor,
|
||||||
|
position: Vector2F,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
self.scroll_top_anchor = anchor;
|
||||||
|
self.scroll_position = position;
|
||||||
|
cx.emit(Event::ScrollPositionChanged { local: false });
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1074,7 +1097,7 @@ impl Editor {
|
||||||
self.set_scroll_position(scroll_position, cx);
|
self.set_scroll_position(scroll_position, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
let autoscroll = if let Some(autoscroll) = self.autoscroll_request.take() {
|
let (autoscroll, local) = if let Some(autoscroll) = self.autoscroll_request.take() {
|
||||||
autoscroll
|
autoscroll
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
|
@ -1126,15 +1149,15 @@ impl Editor {
|
||||||
|
|
||||||
if target_top < start_row {
|
if target_top < start_row {
|
||||||
scroll_position.set_y(target_top);
|
scroll_position.set_y(target_top);
|
||||||
self.set_scroll_position(scroll_position, cx);
|
self.set_scroll_position_internal(scroll_position, local, cx);
|
||||||
} else if target_bottom >= end_row {
|
} else if target_bottom >= end_row {
|
||||||
scroll_position.set_y(target_bottom - visible_lines);
|
scroll_position.set_y(target_bottom - visible_lines);
|
||||||
self.set_scroll_position(scroll_position, cx);
|
self.set_scroll_position_internal(scroll_position, local, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Autoscroll::Center => {
|
Autoscroll::Center => {
|
||||||
scroll_position.set_y((first_cursor_top - margin).max(0.0));
|
scroll_position.set_y((first_cursor_top - margin).max(0.0));
|
||||||
self.set_scroll_position(scroll_position, cx);
|
self.set_scroll_position_internal(scroll_position, local, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1316,7 +1339,7 @@ impl Editor {
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.set_selections(self.selections.clone(), Some(pending), cx);
|
self.set_selections(self.selections.clone(), Some(pending), true, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn begin_selection(
|
fn begin_selection(
|
||||||
|
@ -1396,7 +1419,12 @@ impl Editor {
|
||||||
} else {
|
} else {
|
||||||
selections = Arc::from([]);
|
selections = Arc::from([]);
|
||||||
}
|
}
|
||||||
self.set_selections(selections, Some(PendingSelection { selection, mode }), cx);
|
self.set_selections(
|
||||||
|
selections,
|
||||||
|
Some(PendingSelection { selection, mode }),
|
||||||
|
true,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
@ -1510,7 +1538,7 @@ impl Editor {
|
||||||
pending.selection.end = buffer.anchor_before(head);
|
pending.selection.end = buffer.anchor_before(head);
|
||||||
pending.selection.reversed = false;
|
pending.selection.reversed = false;
|
||||||
}
|
}
|
||||||
self.set_selections(self.selections.clone(), Some(pending), cx);
|
self.set_selections(self.selections.clone(), Some(pending), true, cx);
|
||||||
} else {
|
} else {
|
||||||
log::error!("update_selection dispatched with no pending selection");
|
log::error!("update_selection dispatched with no pending selection");
|
||||||
return;
|
return;
|
||||||
|
@ -1597,7 +1625,7 @@ impl Editor {
|
||||||
if selections.is_empty() {
|
if selections.is_empty() {
|
||||||
selections = Arc::from([pending.selection]);
|
selections = Arc::from([pending.selection]);
|
||||||
}
|
}
|
||||||
self.set_selections(selections, None, cx);
|
self.set_selections(selections, None, true, cx);
|
||||||
self.request_autoscroll(Autoscroll::Fit, cx);
|
self.request_autoscroll(Autoscroll::Fit, cx);
|
||||||
} else {
|
} else {
|
||||||
let mut oldest_selection = self.oldest_selection::<usize>(&cx);
|
let mut oldest_selection = self.oldest_selection::<usize>(&cx);
|
||||||
|
@ -1617,7 +1645,7 @@ impl Editor {
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub fn selected_ranges<D: TextDimension + Ord + Sub<D, Output = D>>(
|
pub fn selected_ranges<D: TextDimension + Ord + Sub<D, Output = D>>(
|
||||||
&self,
|
&self,
|
||||||
cx: &mut MutableAppContext,
|
cx: &AppContext,
|
||||||
) -> Vec<Range<D>> {
|
) -> Vec<Range<D>> {
|
||||||
self.local_selections::<D>(cx)
|
self.local_selections::<D>(cx)
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -1944,7 +1972,7 @@ impl Editor {
|
||||||
}
|
}
|
||||||
drop(snapshot);
|
drop(snapshot);
|
||||||
|
|
||||||
self.set_selections(selections.into(), None, cx);
|
self.set_selections(selections.into(), None, true, cx);
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -3334,7 +3362,7 @@ impl Editor {
|
||||||
pub fn undo(&mut self, _: &Undo, cx: &mut ViewContext<Self>) {
|
pub fn undo(&mut self, _: &Undo, cx: &mut ViewContext<Self>) {
|
||||||
if let Some(tx_id) = self.buffer.update(cx, |buffer, cx| buffer.undo(cx)) {
|
if let Some(tx_id) = self.buffer.update(cx, |buffer, cx| buffer.undo(cx)) {
|
||||||
if let Some((selections, _)) = self.selection_history.get(&tx_id).cloned() {
|
if let Some((selections, _)) = self.selection_history.get(&tx_id).cloned() {
|
||||||
self.set_selections(selections, None, cx);
|
self.set_selections(selections, None, true, cx);
|
||||||
}
|
}
|
||||||
self.request_autoscroll(Autoscroll::Fit, cx);
|
self.request_autoscroll(Autoscroll::Fit, cx);
|
||||||
}
|
}
|
||||||
|
@ -3343,7 +3371,7 @@ impl Editor {
|
||||||
pub fn redo(&mut self, _: &Redo, cx: &mut ViewContext<Self>) {
|
pub fn redo(&mut self, _: &Redo, cx: &mut ViewContext<Self>) {
|
||||||
if let Some(tx_id) = self.buffer.update(cx, |buffer, cx| buffer.redo(cx)) {
|
if let Some(tx_id) = self.buffer.update(cx, |buffer, cx| buffer.redo(cx)) {
|
||||||
if let Some((_, Some(selections))) = self.selection_history.get(&tx_id).cloned() {
|
if let Some((_, Some(selections))) = self.selection_history.get(&tx_id).cloned() {
|
||||||
self.set_selections(selections, None, cx);
|
self.set_selections(selections, None, true, cx);
|
||||||
}
|
}
|
||||||
self.request_autoscroll(Autoscroll::Fit, cx);
|
self.request_autoscroll(Autoscroll::Fit, cx);
|
||||||
}
|
}
|
||||||
|
@ -4870,6 +4898,7 @@ impl Editor {
|
||||||
}
|
}
|
||||||
})),
|
})),
|
||||||
None,
|
None,
|
||||||
|
true,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -4930,6 +4959,7 @@ impl Editor {
|
||||||
&mut self,
|
&mut self,
|
||||||
selections: Arc<[Selection<Anchor>]>,
|
selections: Arc<[Selection<Anchor>]>,
|
||||||
pending_selection: Option<PendingSelection>,
|
pending_selection: Option<PendingSelection>,
|
||||||
|
local: bool,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
assert!(
|
assert!(
|
||||||
|
@ -4941,7 +4971,7 @@ impl Editor {
|
||||||
|
|
||||||
self.selections = selections;
|
self.selections = selections;
|
||||||
self.pending_selection = pending_selection;
|
self.pending_selection = pending_selection;
|
||||||
if self.focused {
|
if self.focused && self.leader_replica_id.is_none() {
|
||||||
self.buffer.update(cx, |buffer, cx| {
|
self.buffer.update(cx, |buffer, cx| {
|
||||||
buffer.set_active_selections(&self.selections, cx)
|
buffer.set_active_selections(&self.selections, cx)
|
||||||
});
|
});
|
||||||
|
@ -4998,11 +5028,16 @@ impl Editor {
|
||||||
self.refresh_document_highlights(cx);
|
self.refresh_document_highlights(cx);
|
||||||
|
|
||||||
self.pause_cursor_blinking(cx);
|
self.pause_cursor_blinking(cx);
|
||||||
cx.emit(Event::SelectionsChanged);
|
cx.emit(Event::SelectionsChanged { local });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn request_autoscroll(&mut self, autoscroll: Autoscroll, cx: &mut ViewContext<Self>) {
|
pub fn request_autoscroll(&mut self, autoscroll: Autoscroll, cx: &mut ViewContext<Self>) {
|
||||||
self.autoscroll_request = Some(autoscroll);
|
self.autoscroll_request = Some((autoscroll, true));
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_autoscroll_remotely(&mut self, autoscroll: Autoscroll, cx: &mut ViewContext<Self>) {
|
||||||
|
self.autoscroll_request = Some((autoscroll, false));
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5407,7 +5442,7 @@ impl Editor {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn show_local_cursors(&self) -> bool {
|
pub fn show_local_cursors(&self) -> bool {
|
||||||
self.show_local_cursors
|
self.show_local_cursors && self.focused
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_buffer_changed(&mut self, _: ModelHandle<MultiBuffer>, cx: &mut ViewContext<Self>) {
|
fn on_buffer_changed(&mut self, _: ModelHandle<MultiBuffer>, cx: &mut ViewContext<Self>) {
|
||||||
|
@ -5421,10 +5456,10 @@ impl Editor {
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
match event {
|
match event {
|
||||||
language::Event::Edited => {
|
language::Event::Edited { local } => {
|
||||||
self.refresh_active_diagnostics(cx);
|
self.refresh_active_diagnostics(cx);
|
||||||
self.refresh_code_actions(cx);
|
self.refresh_code_actions(cx);
|
||||||
cx.emit(Event::Edited);
|
cx.emit(Event::Edited { local: *local });
|
||||||
}
|
}
|
||||||
language::Event::Dirtied => cx.emit(Event::Dirtied),
|
language::Event::Dirtied => cx.emit(Event::Dirtied),
|
||||||
language::Event::Saved => cx.emit(Event::Saved),
|
language::Event::Saved => cx.emit(Event::Saved),
|
||||||
|
@ -5537,10 +5572,10 @@ impl Deref for EditorSnapshot {
|
||||||
fn compute_scroll_position(
|
fn compute_scroll_position(
|
||||||
snapshot: &DisplaySnapshot,
|
snapshot: &DisplaySnapshot,
|
||||||
mut scroll_position: Vector2F,
|
mut scroll_position: Vector2F,
|
||||||
scroll_top_anchor: &Option<Anchor>,
|
scroll_top_anchor: &Anchor,
|
||||||
) -> Vector2F {
|
) -> Vector2F {
|
||||||
if let Some(anchor) = scroll_top_anchor {
|
if *scroll_top_anchor != Anchor::min() {
|
||||||
let scroll_top = anchor.to_display_point(snapshot).row() as f32;
|
let scroll_top = scroll_top_anchor.to_display_point(snapshot).row() as f32;
|
||||||
scroll_position.set_y(scroll_top + scroll_position.y());
|
scroll_position.set_y(scroll_top + scroll_position.y());
|
||||||
} else {
|
} else {
|
||||||
scroll_position.set_y(0.);
|
scroll_position.set_y(0.);
|
||||||
|
@ -5551,12 +5586,13 @@ fn compute_scroll_position(
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone)]
|
||||||
pub enum Event {
|
pub enum Event {
|
||||||
Activate,
|
Activate,
|
||||||
Edited,
|
Edited { local: bool },
|
||||||
Blurred,
|
Blurred,
|
||||||
Dirtied,
|
Dirtied,
|
||||||
Saved,
|
Saved,
|
||||||
TitleChanged,
|
TitleChanged,
|
||||||
SelectionsChanged,
|
SelectionsChanged { local: bool },
|
||||||
|
ScrollPositionChanged { local: bool },
|
||||||
Closed,
|
Closed,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5595,7 +5631,9 @@ impl View for Editor {
|
||||||
self.blink_cursors(self.blink_epoch, cx);
|
self.blink_cursors(self.blink_epoch, cx);
|
||||||
self.buffer.update(cx, |buffer, cx| {
|
self.buffer.update(cx, |buffer, cx| {
|
||||||
buffer.finalize_last_transaction(cx);
|
buffer.finalize_last_transaction(cx);
|
||||||
buffer.set_active_selections(&self.selections, cx)
|
if self.leader_replica_id.is_none() {
|
||||||
|
buffer.set_active_selections(&self.selections, cx);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6013,6 +6051,10 @@ mod tests {
|
||||||
use crate::test::marked_text_by;
|
use crate::test::marked_text_by;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use gpui::{
|
||||||
|
geometry::rect::RectF,
|
||||||
|
platform::{WindowBounds, WindowOptions},
|
||||||
|
};
|
||||||
use language::{LanguageConfig, LanguageServerConfig};
|
use language::{LanguageConfig, LanguageServerConfig};
|
||||||
use lsp::FakeLanguageServer;
|
use lsp::FakeLanguageServer;
|
||||||
use project::FakeFs;
|
use project::FakeFs;
|
||||||
|
@ -6021,6 +6063,7 @@ mod tests {
|
||||||
use text::Point;
|
use text::Point;
|
||||||
use unindent::Unindent;
|
use unindent::Unindent;
|
||||||
use util::test::sample_text;
|
use util::test::sample_text;
|
||||||
|
use workspace::FollowableItem;
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_undo_redo_with_selection_restoration(cx: &mut MutableAppContext) {
|
fn test_undo_redo_with_selection_restoration(cx: &mut MutableAppContext) {
|
||||||
|
@ -8921,6 +8964,75 @@ mod tests {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn test_following(cx: &mut gpui::MutableAppContext) {
|
||||||
|
let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
|
||||||
|
populate_settings(cx);
|
||||||
|
|
||||||
|
let (_, leader) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx));
|
||||||
|
let (_, follower) = cx.add_window(
|
||||||
|
WindowOptions {
|
||||||
|
bounds: WindowBounds::Fixed(RectF::from_points(vec2f(0., 0.), vec2f(10., 80.))),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
|cx| build_editor(buffer.clone(), cx),
|
||||||
|
);
|
||||||
|
|
||||||
|
let pending_update = Rc::new(RefCell::new(None));
|
||||||
|
follower.update(cx, {
|
||||||
|
let update = pending_update.clone();
|
||||||
|
|_, cx| {
|
||||||
|
cx.subscribe(&leader, move |_, leader, event, cx| {
|
||||||
|
leader
|
||||||
|
.read(cx)
|
||||||
|
.add_event_to_update_proto(event, &mut *update.borrow_mut(), cx);
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update the selections only
|
||||||
|
leader.update(cx, |leader, cx| {
|
||||||
|
leader.select_ranges([1..1], None, cx);
|
||||||
|
});
|
||||||
|
follower.update(cx, |follower, cx| {
|
||||||
|
follower
|
||||||
|
.apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx)
|
||||||
|
.unwrap();
|
||||||
|
});
|
||||||
|
assert_eq!(follower.read(cx).selected_ranges(cx), vec![1..1]);
|
||||||
|
|
||||||
|
// Update the scroll position only
|
||||||
|
leader.update(cx, |leader, cx| {
|
||||||
|
leader.set_scroll_position(vec2f(1.5, 3.5), cx);
|
||||||
|
});
|
||||||
|
follower.update(cx, |follower, cx| {
|
||||||
|
follower
|
||||||
|
.apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx)
|
||||||
|
.unwrap();
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
follower.update(cx, |follower, cx| follower.scroll_position(cx)),
|
||||||
|
vec2f(1.5, 3.5)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update the selections and scroll position
|
||||||
|
leader.update(cx, |leader, cx| {
|
||||||
|
leader.select_ranges([0..0], None, cx);
|
||||||
|
leader.request_autoscroll(Autoscroll::Newest, cx);
|
||||||
|
leader.set_scroll_position(vec2f(1.5, 3.5), cx);
|
||||||
|
});
|
||||||
|
follower.update(cx, |follower, cx| {
|
||||||
|
let initial_scroll_position = follower.scroll_position(cx);
|
||||||
|
follower
|
||||||
|
.apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(follower.scroll_position(cx), initial_scroll_position);
|
||||||
|
assert!(follower.autoscroll_request.is_some());
|
||||||
|
});
|
||||||
|
assert_eq!(follower.read(cx).selected_ranges(cx), vec![0..0]);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_combine_syntax_and_fuzzy_match_highlights() {
|
fn test_combine_syntax_and_fuzzy_match_highlights() {
|
||||||
let string = "abcdefghijklmnop";
|
let string = "abcdefghijklmnop";
|
||||||
|
|
|
@ -909,7 +909,7 @@ impl Element for EditorElement {
|
||||||
.anchor_before(DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right))
|
.anchor_before(DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right))
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut selections = HashMap::default();
|
let mut selections = Vec::new();
|
||||||
let mut active_rows = BTreeMap::new();
|
let mut active_rows = BTreeMap::new();
|
||||||
let mut highlighted_rows = None;
|
let mut highlighted_rows = None;
|
||||||
let mut highlighted_ranges = Vec::new();
|
let mut highlighted_ranges = Vec::new();
|
||||||
|
@ -922,11 +922,32 @@ impl Element for EditorElement {
|
||||||
&display_map,
|
&display_map,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let mut remote_selections = HashMap::default();
|
||||||
|
for (replica_id, selection) in display_map
|
||||||
|
.buffer_snapshot
|
||||||
|
.remote_selections_in_range(&(start_anchor.clone()..end_anchor.clone()))
|
||||||
|
{
|
||||||
|
// The local selections match the leader's selections.
|
||||||
|
if Some(replica_id) == view.leader_replica_id {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
remote_selections
|
||||||
|
.entry(replica_id)
|
||||||
|
.or_insert(Vec::new())
|
||||||
|
.push(crate::Selection {
|
||||||
|
id: selection.id,
|
||||||
|
goal: selection.goal,
|
||||||
|
reversed: selection.reversed,
|
||||||
|
start: selection.start.to_display_point(&display_map),
|
||||||
|
end: selection.end.to_display_point(&display_map),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
selections.extend(remote_selections);
|
||||||
|
|
||||||
if view.show_local_selections {
|
if view.show_local_selections {
|
||||||
let local_selections = view.local_selections_in_range(
|
let local_selections =
|
||||||
start_anchor.clone()..end_anchor.clone(),
|
view.local_selections_in_range(start_anchor..end_anchor, &display_map);
|
||||||
&display_map,
|
|
||||||
);
|
|
||||||
for selection in &local_selections {
|
for selection in &local_selections {
|
||||||
let is_empty = selection.start == selection.end;
|
let is_empty = selection.start == selection.end;
|
||||||
let selection_start = snapshot.prev_line_boundary(selection.start).1;
|
let selection_start = snapshot.prev_line_boundary(selection.start).1;
|
||||||
|
@ -939,8 +960,12 @@ impl Element for EditorElement {
|
||||||
*contains_non_empty_selection |= !is_empty;
|
*contains_non_empty_selection |= !is_empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
selections.insert(
|
|
||||||
view.replica_id(cx),
|
// Render the local selections in the leader's color when following.
|
||||||
|
let local_replica_id = view.leader_replica_id.unwrap_or(view.replica_id(cx));
|
||||||
|
|
||||||
|
selections.push((
|
||||||
|
local_replica_id,
|
||||||
local_selections
|
local_selections
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|selection| crate::Selection {
|
.map(|selection| crate::Selection {
|
||||||
|
@ -951,23 +976,7 @@ impl Element for EditorElement {
|
||||||
end: selection.end.to_display_point(&display_map),
|
end: selection.end.to_display_point(&display_map),
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
);
|
));
|
||||||
}
|
|
||||||
|
|
||||||
for (replica_id, selection) in display_map
|
|
||||||
.buffer_snapshot
|
|
||||||
.remote_selections_in_range(&(start_anchor..end_anchor))
|
|
||||||
{
|
|
||||||
selections
|
|
||||||
.entry(replica_id)
|
|
||||||
.or_insert(Vec::new())
|
|
||||||
.push(crate::Selection {
|
|
||||||
id: selection.id,
|
|
||||||
goal: selection.goal,
|
|
||||||
reversed: selection.reversed,
|
|
||||||
start: selection.start.to_display_point(&display_map),
|
|
||||||
end: selection.end.to_display_point(&display_map),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1213,7 +1222,7 @@ pub struct LayoutState {
|
||||||
em_width: f32,
|
em_width: f32,
|
||||||
em_advance: f32,
|
em_advance: f32,
|
||||||
highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
|
highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
|
||||||
selections: HashMap<ReplicaId, Vec<text::Selection<DisplayPoint>>>,
|
selections: Vec<(ReplicaId, Vec<text::Selection<DisplayPoint>>)>,
|
||||||
context_menu: Option<(DisplayPoint, ElementBox)>,
|
context_menu: Option<(DisplayPoint, ElementBox)>,
|
||||||
code_actions_indicator: Option<(u32, ElementBox)>,
|
code_actions_indicator: Option<(u32, ElementBox)>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,244 @@
|
||||||
use crate::{Autoscroll, Editor, Event, NavigationData, ToOffset, ToPoint as _};
|
use crate::{Anchor, Autoscroll, Editor, Event, ExcerptId, NavigationData, ToOffset, ToPoint as _};
|
||||||
use anyhow::Result;
|
use anyhow::{anyhow, Result};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
elements::*, AppContext, Entity, ModelHandle, RenderContext, Subscription, Task, View,
|
elements::*, geometry::vector::vec2f, AppContext, Entity, ModelHandle, MutableAppContext,
|
||||||
ViewContext, ViewHandle,
|
RenderContext, Subscription, Task, View, ViewContext, ViewHandle,
|
||||||
};
|
};
|
||||||
use language::{Bias, Buffer, Diagnostic, File as _};
|
use language::{Bias, Buffer, Diagnostic, File as _, SelectionGoal};
|
||||||
use project::{File, Project, ProjectPath};
|
use project::{File, Project, ProjectEntryId, ProjectPath};
|
||||||
use std::fmt::Write;
|
use rpc::proto::{self, update_view};
|
||||||
use std::path::PathBuf;
|
use std::{fmt::Write, path::PathBuf};
|
||||||
use text::{Point, Selection};
|
use text::{Point, Selection};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use workspace::{Item, ItemHandle, ItemNavHistory, ProjectItem, Settings, StatusItemView};
|
use workspace::{
|
||||||
|
FollowableItem, Item, ItemHandle, ItemNavHistory, ProjectItem, Settings, StatusItemView,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl FollowableItem for Editor {
|
||||||
|
fn from_state_proto(
|
||||||
|
pane: ViewHandle<workspace::Pane>,
|
||||||
|
project: ModelHandle<Project>,
|
||||||
|
state: &mut Option<proto::view::Variant>,
|
||||||
|
cx: &mut MutableAppContext,
|
||||||
|
) -> Option<Task<Result<ViewHandle<Self>>>> {
|
||||||
|
let state = if matches!(state, Some(proto::view::Variant::Editor(_))) {
|
||||||
|
if let Some(proto::view::Variant::Editor(state)) = state.take() {
|
||||||
|
state
|
||||||
|
} else {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
let buffer = project.update(cx, |project, cx| {
|
||||||
|
project.open_buffer_by_id(state.buffer_id, cx)
|
||||||
|
});
|
||||||
|
Some(cx.spawn(|mut cx| async move {
|
||||||
|
let buffer = buffer.await?;
|
||||||
|
let editor = pane
|
||||||
|
.read_with(&cx, |pane, cx| {
|
||||||
|
pane.items_of_type::<Self>().find(|editor| {
|
||||||
|
editor.read(cx).buffer.read(cx).as_singleton().as_ref() == Some(&buffer)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
cx.add_view(pane.window_id(), |cx| {
|
||||||
|
Editor::for_buffer(buffer, Some(project), cx)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
editor.update(&mut cx, |editor, cx| {
|
||||||
|
let excerpt_id;
|
||||||
|
let buffer_id;
|
||||||
|
{
|
||||||
|
let buffer = editor.buffer.read(cx).read(cx);
|
||||||
|
let singleton = buffer.as_singleton().unwrap();
|
||||||
|
excerpt_id = singleton.0.clone();
|
||||||
|
buffer_id = singleton.1;
|
||||||
|
}
|
||||||
|
let selections = state
|
||||||
|
.selections
|
||||||
|
.into_iter()
|
||||||
|
.map(|selection| {
|
||||||
|
deserialize_selection(&excerpt_id, buffer_id, selection)
|
||||||
|
.ok_or_else(|| anyhow!("invalid selection"))
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>>>()?;
|
||||||
|
if !selections.is_empty() {
|
||||||
|
editor.set_selections(selections.into(), None, false, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(anchor) = state.scroll_top_anchor {
|
||||||
|
editor.set_scroll_top_anchor(
|
||||||
|
Anchor {
|
||||||
|
buffer_id: Some(state.buffer_id as usize),
|
||||||
|
excerpt_id: excerpt_id.clone(),
|
||||||
|
text_anchor: language::proto::deserialize_anchor(anchor)
|
||||||
|
.ok_or_else(|| anyhow!("invalid scroll top"))?,
|
||||||
|
},
|
||||||
|
vec2f(state.scroll_x, state.scroll_y),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok::<_, anyhow::Error>(())
|
||||||
|
})?;
|
||||||
|
Ok(editor)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_leader_replica_id(
|
||||||
|
&mut self,
|
||||||
|
leader_replica_id: Option<u16>,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
self.leader_replica_id = leader_replica_id;
|
||||||
|
if self.leader_replica_id.is_some() {
|
||||||
|
self.buffer.update(cx, |buffer, cx| {
|
||||||
|
buffer.remove_active_selections(cx);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
self.buffer.update(cx, |buffer, cx| {
|
||||||
|
if self.focused {
|
||||||
|
buffer.set_active_selections(&self.selections, cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
|
||||||
|
let buffer_id = self.buffer.read(cx).as_singleton()?.read(cx).remote_id();
|
||||||
|
Some(proto::view::Variant::Editor(proto::view::Editor {
|
||||||
|
buffer_id,
|
||||||
|
scroll_top_anchor: Some(language::proto::serialize_anchor(
|
||||||
|
&self.scroll_top_anchor.text_anchor,
|
||||||
|
)),
|
||||||
|
scroll_x: self.scroll_position.x(),
|
||||||
|
scroll_y: self.scroll_position.y(),
|
||||||
|
selections: self.selections.iter().map(serialize_selection).collect(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_event_to_update_proto(
|
||||||
|
&self,
|
||||||
|
event: &Self::Event,
|
||||||
|
update: &mut Option<proto::update_view::Variant>,
|
||||||
|
_: &AppContext,
|
||||||
|
) -> bool {
|
||||||
|
let update =
|
||||||
|
update.get_or_insert_with(|| proto::update_view::Variant::Editor(Default::default()));
|
||||||
|
|
||||||
|
match update {
|
||||||
|
proto::update_view::Variant::Editor(update) => match event {
|
||||||
|
Event::ScrollPositionChanged { .. } => {
|
||||||
|
update.scroll_top_anchor = Some(language::proto::serialize_anchor(
|
||||||
|
&self.scroll_top_anchor.text_anchor,
|
||||||
|
));
|
||||||
|
update.scroll_x = self.scroll_position.x();
|
||||||
|
update.scroll_y = self.scroll_position.y();
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Event::SelectionsChanged { .. } => {
|
||||||
|
update.selections = self
|
||||||
|
.selections
|
||||||
|
.iter()
|
||||||
|
.chain(self.pending_selection.as_ref().map(|p| &p.selection))
|
||||||
|
.map(serialize_selection)
|
||||||
|
.collect();
|
||||||
|
true
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_update_proto(
|
||||||
|
&mut self,
|
||||||
|
message: update_view::Variant,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> Result<()> {
|
||||||
|
match message {
|
||||||
|
update_view::Variant::Editor(message) => {
|
||||||
|
let buffer = self.buffer.read(cx);
|
||||||
|
let buffer = buffer.read(cx);
|
||||||
|
let (excerpt_id, buffer_id, _) = buffer.as_singleton().unwrap();
|
||||||
|
let excerpt_id = excerpt_id.clone();
|
||||||
|
drop(buffer);
|
||||||
|
|
||||||
|
let selections = message
|
||||||
|
.selections
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|selection| {
|
||||||
|
deserialize_selection(&excerpt_id, buffer_id, selection)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if !selections.is_empty() {
|
||||||
|
self.set_selections(selections.into(), None, false, cx);
|
||||||
|
self.request_autoscroll_remotely(Autoscroll::Newest, cx);
|
||||||
|
} else {
|
||||||
|
if let Some(anchor) = message.scroll_top_anchor {
|
||||||
|
self.set_scroll_top_anchor(
|
||||||
|
Anchor {
|
||||||
|
buffer_id: Some(buffer_id),
|
||||||
|
excerpt_id: excerpt_id.clone(),
|
||||||
|
text_anchor: language::proto::deserialize_anchor(anchor)
|
||||||
|
.ok_or_else(|| anyhow!("invalid scroll top"))?,
|
||||||
|
},
|
||||||
|
vec2f(message.scroll_x, message.scroll_y),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_unfollow_on_event(event: &Self::Event, _: &AppContext) -> bool {
|
||||||
|
match event {
|
||||||
|
Event::Edited { local } => *local,
|
||||||
|
Event::SelectionsChanged { local } => *local,
|
||||||
|
Event::ScrollPositionChanged { local } => *local,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serialize_selection(selection: &Selection<Anchor>) -> proto::Selection {
|
||||||
|
proto::Selection {
|
||||||
|
id: selection.id as u64,
|
||||||
|
start: Some(language::proto::serialize_anchor(
|
||||||
|
&selection.start.text_anchor,
|
||||||
|
)),
|
||||||
|
end: Some(language::proto::serialize_anchor(
|
||||||
|
&selection.end.text_anchor,
|
||||||
|
)),
|
||||||
|
reversed: selection.reversed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_selection(
|
||||||
|
excerpt_id: &ExcerptId,
|
||||||
|
buffer_id: usize,
|
||||||
|
selection: proto::Selection,
|
||||||
|
) -> Option<Selection<Anchor>> {
|
||||||
|
Some(Selection {
|
||||||
|
id: selection.id as usize,
|
||||||
|
start: Anchor {
|
||||||
|
buffer_id: Some(buffer_id),
|
||||||
|
excerpt_id: excerpt_id.clone(),
|
||||||
|
text_anchor: language::proto::deserialize_anchor(selection.start?)?,
|
||||||
|
},
|
||||||
|
end: Anchor {
|
||||||
|
buffer_id: Some(buffer_id),
|
||||||
|
excerpt_id: excerpt_id.clone(),
|
||||||
|
text_anchor: language::proto::deserialize_anchor(selection.end?)?,
|
||||||
|
},
|
||||||
|
reversed: selection.reversed,
|
||||||
|
goal: SelectionGoal::None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
impl Item for Editor {
|
impl Item for Editor {
|
||||||
fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) {
|
fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) {
|
||||||
|
@ -41,6 +269,10 @@ impl Item for Editor {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn project_entry_id(&self, cx: &AppContext) -> Option<ProjectEntryId> {
|
||||||
|
File::from_dyn(self.buffer().read(cx).file(cx)).and_then(|file| file.project_entry_id(cx))
|
||||||
|
}
|
||||||
|
|
||||||
fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
|
fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
|
||||||
where
|
where
|
||||||
Self: Sized,
|
Self: Sized,
|
||||||
|
|
|
@ -821,6 +821,14 @@ impl MultiBuffer {
|
||||||
.map_or(Vec::new(), |state| state.excerpts.clone())
|
.map_or(Vec::new(), |state| state.excerpts.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn excerpt_ids(&self) -> Vec<ExcerptId> {
|
||||||
|
self.buffers
|
||||||
|
.borrow()
|
||||||
|
.values()
|
||||||
|
.flat_map(|state| state.excerpts.iter().cloned())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn excerpt_containing(
|
pub fn excerpt_containing(
|
||||||
&self,
|
&self,
|
||||||
position: impl ToOffset,
|
position: impl ToOffset,
|
||||||
|
|
|
@ -291,7 +291,7 @@ impl FileFinder {
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
match event {
|
match event {
|
||||||
editor::Event::Edited => {
|
editor::Event::Edited { .. } => {
|
||||||
let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx));
|
let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx));
|
||||||
if query.is_empty() {
|
if query.is_empty() {
|
||||||
self.latest_search_id = post_inc(&mut self.search_count);
|
self.latest_search_id = post_inc(&mut self.search_count);
|
||||||
|
|
|
@ -102,7 +102,7 @@ impl GoToLine {
|
||||||
) {
|
) {
|
||||||
match event {
|
match event {
|
||||||
editor::Event::Blurred => cx.emit(Event::Dismissed),
|
editor::Event::Blurred => cx.emit(Event::Dismissed),
|
||||||
editor::Event::Edited => {
|
editor::Event::Edited { .. } => {
|
||||||
let line_editor = self.line_editor.read(cx).buffer().read(cx).read(cx).text();
|
let line_editor = self.line_editor.read(cx).buffer().read(cx).read(cx).text();
|
||||||
let mut components = line_editor.trim().split(&[',', ':'][..]);
|
let mut components = line_editor.trim().split(&[',', ':'][..]);
|
||||||
let row = components.next().and_then(|row| row.parse::<u32>().ok());
|
let row = components.next().and_then(|row| row.parse::<u32>().ok());
|
||||||
|
|
|
@ -8,6 +8,7 @@ use crate::{
|
||||||
AssetCache, AssetSource, ClipboardItem, FontCache, PathPromptOptions, TextLayoutCache,
|
AssetCache, AssetSource, ClipboardItem, FontCache, PathPromptOptions, TextLayoutCache,
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
|
use collections::btree_map;
|
||||||
use keymap::MatchResult;
|
use keymap::MatchResult;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
@ -93,6 +94,8 @@ pub trait UpgradeModelHandle {
|
||||||
|
|
||||||
pub trait UpgradeViewHandle {
|
pub trait UpgradeViewHandle {
|
||||||
fn upgrade_view_handle<T: View>(&self, handle: &WeakViewHandle<T>) -> Option<ViewHandle<T>>;
|
fn upgrade_view_handle<T: View>(&self, handle: &WeakViewHandle<T>) -> Option<ViewHandle<T>>;
|
||||||
|
|
||||||
|
fn upgrade_any_view_handle(&self, handle: &AnyWeakViewHandle) -> Option<AnyViewHandle>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait ReadView {
|
pub trait ReadView {
|
||||||
|
@ -182,6 +185,12 @@ macro_rules! action {
|
||||||
Box::new(self.clone())
|
Box::new(self.clone())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<$arg> for $name {
|
||||||
|
fn from(arg: $arg) -> Self {
|
||||||
|
Self(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
($name:ident) => {
|
($name:ident) => {
|
||||||
|
@ -647,6 +656,10 @@ impl UpgradeViewHandle for AsyncAppContext {
|
||||||
fn upgrade_view_handle<T: View>(&self, handle: &WeakViewHandle<T>) -> Option<ViewHandle<T>> {
|
fn upgrade_view_handle<T: View>(&self, handle: &WeakViewHandle<T>) -> Option<ViewHandle<T>> {
|
||||||
self.0.borrow_mut().upgrade_view_handle(handle)
|
self.0.borrow_mut().upgrade_view_handle(handle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn upgrade_any_view_handle(&self, handle: &AnyWeakViewHandle) -> Option<AnyViewHandle> {
|
||||||
|
self.0.borrow_mut().upgrade_any_view_handle(handle)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ReadModelWith for AsyncAppContext {
|
impl ReadModelWith for AsyncAppContext {
|
||||||
|
@ -1098,21 +1111,18 @@ impl MutableAppContext {
|
||||||
E: Any,
|
E: Any,
|
||||||
F: 'static + FnMut(&E, &mut Self),
|
F: 'static + FnMut(&E, &mut Self),
|
||||||
{
|
{
|
||||||
let id = post_inc(&mut self.next_subscription_id);
|
let subscription_id = post_inc(&mut self.next_subscription_id);
|
||||||
let type_id = TypeId::of::<E>();
|
let type_id = TypeId::of::<E>();
|
||||||
self.global_subscriptions
|
self.pending_effects.push_back(Effect::GlobalSubscription {
|
||||||
.lock()
|
type_id,
|
||||||
.entry(type_id)
|
subscription_id,
|
||||||
.or_default()
|
callback: Box::new(move |payload, cx| {
|
||||||
.insert(
|
|
||||||
id,
|
|
||||||
Some(Box::new(move |payload, cx| {
|
|
||||||
let payload = payload.downcast_ref().expect("downcast is type safe");
|
let payload = payload.downcast_ref().expect("downcast is type safe");
|
||||||
callback(payload, cx)
|
callback(payload, cx)
|
||||||
})),
|
}),
|
||||||
);
|
});
|
||||||
Subscription::GlobalSubscription {
|
Subscription::GlobalSubscription {
|
||||||
id,
|
id: subscription_id,
|
||||||
type_id,
|
type_id,
|
||||||
subscriptions: Some(Arc::downgrade(&self.global_subscriptions)),
|
subscriptions: Some(Arc::downgrade(&self.global_subscriptions)),
|
||||||
}
|
}
|
||||||
|
@ -1138,25 +1148,22 @@ impl MutableAppContext {
|
||||||
H: Handle<E>,
|
H: Handle<E>,
|
||||||
F: 'static + FnMut(H, &E::Event, &mut Self) -> bool,
|
F: 'static + FnMut(H, &E::Event, &mut Self) -> bool,
|
||||||
{
|
{
|
||||||
let id = post_inc(&mut self.next_subscription_id);
|
let subscription_id = post_inc(&mut self.next_subscription_id);
|
||||||
let emitter = handle.downgrade();
|
let emitter = handle.downgrade();
|
||||||
self.subscriptions
|
self.pending_effects.push_back(Effect::Subscription {
|
||||||
.lock()
|
entity_id: handle.id(),
|
||||||
.entry(handle.id())
|
subscription_id,
|
||||||
.or_default()
|
callback: Box::new(move |payload, cx| {
|
||||||
.insert(
|
|
||||||
id,
|
|
||||||
Some(Box::new(move |payload, cx| {
|
|
||||||
if let Some(emitter) = H::upgrade_from(&emitter, cx.as_ref()) {
|
if let Some(emitter) = H::upgrade_from(&emitter, cx.as_ref()) {
|
||||||
let payload = payload.downcast_ref().expect("downcast is type safe");
|
let payload = payload.downcast_ref().expect("downcast is type safe");
|
||||||
callback(emitter, payload, cx)
|
callback(emitter, payload, cx)
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
})),
|
}),
|
||||||
);
|
});
|
||||||
Subscription::Subscription {
|
Subscription::Subscription {
|
||||||
id,
|
id: subscription_id,
|
||||||
entity_id: handle.id(),
|
entity_id: handle.id(),
|
||||||
subscriptions: Some(Arc::downgrade(&self.subscriptions)),
|
subscriptions: Some(Arc::downgrade(&self.subscriptions)),
|
||||||
}
|
}
|
||||||
|
@ -1169,25 +1176,23 @@ impl MutableAppContext {
|
||||||
H: Handle<E>,
|
H: Handle<E>,
|
||||||
F: 'static + FnMut(H, &mut Self) -> bool,
|
F: 'static + FnMut(H, &mut Self) -> bool,
|
||||||
{
|
{
|
||||||
let id = post_inc(&mut self.next_subscription_id);
|
let subscription_id = post_inc(&mut self.next_subscription_id);
|
||||||
let observed = handle.downgrade();
|
let observed = handle.downgrade();
|
||||||
self.observations
|
let entity_id = handle.id();
|
||||||
.lock()
|
self.pending_effects.push_back(Effect::Observation {
|
||||||
.entry(handle.id())
|
entity_id,
|
||||||
.or_default()
|
subscription_id,
|
||||||
.insert(
|
callback: Box::new(move |cx| {
|
||||||
id,
|
|
||||||
Some(Box::new(move |cx| {
|
|
||||||
if let Some(observed) = H::upgrade_from(&observed, cx) {
|
if let Some(observed) = H::upgrade_from(&observed, cx) {
|
||||||
callback(observed, cx)
|
callback(observed, cx)
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
})),
|
}),
|
||||||
);
|
});
|
||||||
Subscription::Observation {
|
Subscription::Observation {
|
||||||
id,
|
id: subscription_id,
|
||||||
entity_id: handle.id(),
|
entity_id,
|
||||||
observations: Some(Arc::downgrade(&self.observations)),
|
observations: Some(Arc::downgrade(&self.observations)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1219,7 +1224,17 @@ impl MutableAppContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn defer(&mut self, callback: Box<dyn FnOnce(&mut MutableAppContext)>) {
|
fn defer(&mut self, callback: Box<dyn FnOnce(&mut MutableAppContext)>) {
|
||||||
self.pending_effects.push_back(Effect::Deferred(callback))
|
self.pending_effects.push_back(Effect::Deferred {
|
||||||
|
callback,
|
||||||
|
after_window_update: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn after_window_update(&mut self, callback: impl 'static + FnOnce(&mut MutableAppContext)) {
|
||||||
|
self.pending_effects.push_back(Effect::Deferred {
|
||||||
|
callback: Box::new(callback),
|
||||||
|
after_window_update: true,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn notify_model(&mut self, model_id: usize) {
|
pub(crate) fn notify_model(&mut self, model_id: usize) {
|
||||||
|
@ -1635,6 +1650,7 @@ impl MutableAppContext {
|
||||||
|
|
||||||
fn flush_effects(&mut self) {
|
fn flush_effects(&mut self) {
|
||||||
self.pending_flushes = self.pending_flushes.saturating_sub(1);
|
self.pending_flushes = self.pending_flushes.saturating_sub(1);
|
||||||
|
let mut after_window_update_callbacks = Vec::new();
|
||||||
|
|
||||||
if !self.flushing_effects && self.pending_flushes == 0 {
|
if !self.flushing_effects && self.pending_flushes == 0 {
|
||||||
self.flushing_effects = true;
|
self.flushing_effects = true;
|
||||||
|
@ -1643,15 +1659,43 @@ impl MutableAppContext {
|
||||||
loop {
|
loop {
|
||||||
if let Some(effect) = self.pending_effects.pop_front() {
|
if let Some(effect) = self.pending_effects.pop_front() {
|
||||||
match effect {
|
match effect {
|
||||||
|
Effect::Subscription {
|
||||||
|
entity_id,
|
||||||
|
subscription_id,
|
||||||
|
callback,
|
||||||
|
} => self.handle_subscription_effect(entity_id, subscription_id, callback),
|
||||||
Effect::Event { entity_id, payload } => self.emit_event(entity_id, payload),
|
Effect::Event { entity_id, payload } => self.emit_event(entity_id, payload),
|
||||||
|
Effect::GlobalSubscription {
|
||||||
|
type_id,
|
||||||
|
subscription_id,
|
||||||
|
callback,
|
||||||
|
} => self.handle_global_subscription_effect(
|
||||||
|
type_id,
|
||||||
|
subscription_id,
|
||||||
|
callback,
|
||||||
|
),
|
||||||
Effect::GlobalEvent { payload } => self.emit_global_event(payload),
|
Effect::GlobalEvent { payload } => self.emit_global_event(payload),
|
||||||
|
Effect::Observation {
|
||||||
|
entity_id,
|
||||||
|
subscription_id,
|
||||||
|
callback,
|
||||||
|
} => self.handle_observation_effect(entity_id, subscription_id, callback),
|
||||||
Effect::ModelNotification { model_id } => {
|
Effect::ModelNotification { model_id } => {
|
||||||
self.notify_model_observers(model_id)
|
self.notify_model_observers(model_id)
|
||||||
}
|
}
|
||||||
Effect::ViewNotification { window_id, view_id } => {
|
Effect::ViewNotification { window_id, view_id } => {
|
||||||
self.notify_view_observers(window_id, view_id)
|
self.notify_view_observers(window_id, view_id)
|
||||||
}
|
}
|
||||||
Effect::Deferred(callback) => callback(self),
|
Effect::Deferred {
|
||||||
|
callback,
|
||||||
|
after_window_update,
|
||||||
|
} => {
|
||||||
|
if after_window_update {
|
||||||
|
after_window_update_callbacks.push(callback);
|
||||||
|
} else {
|
||||||
|
callback(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
Effect::ModelRelease { model_id, model } => {
|
Effect::ModelRelease { model_id, model } => {
|
||||||
self.notify_release_observers(model_id, model.as_any())
|
self.notify_release_observers(model_id, model.as_any())
|
||||||
}
|
}
|
||||||
|
@ -1682,14 +1726,20 @@ impl MutableAppContext {
|
||||||
self.update_windows();
|
self.update_windows();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.pending_effects.is_empty() {
|
||||||
|
for callback in after_window_update_callbacks.drain(..) {
|
||||||
|
callback(self);
|
||||||
|
}
|
||||||
|
|
||||||
if self.pending_effects.is_empty() {
|
if self.pending_effects.is_empty() {
|
||||||
self.flushing_effects = false;
|
self.flushing_effects = false;
|
||||||
self.pending_notifications.clear();
|
self.pending_notifications.clear();
|
||||||
break;
|
break;
|
||||||
} else {
|
|
||||||
refreshing = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
refreshing = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1759,6 +1809,30 @@ impl MutableAppContext {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_subscription_effect(
|
||||||
|
&mut self,
|
||||||
|
entity_id: usize,
|
||||||
|
subscription_id: usize,
|
||||||
|
callback: SubscriptionCallback,
|
||||||
|
) {
|
||||||
|
match self
|
||||||
|
.subscriptions
|
||||||
|
.lock()
|
||||||
|
.entry(entity_id)
|
||||||
|
.or_default()
|
||||||
|
.entry(subscription_id)
|
||||||
|
{
|
||||||
|
btree_map::Entry::Vacant(entry) => {
|
||||||
|
entry.insert(Some(callback));
|
||||||
|
}
|
||||||
|
// Subscription was dropped before effect was processed
|
||||||
|
btree_map::Entry::Occupied(entry) => {
|
||||||
|
debug_assert!(entry.get().is_none());
|
||||||
|
entry.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn emit_event(&mut self, entity_id: usize, payload: Box<dyn Any>) {
|
fn emit_event(&mut self, entity_id: usize, payload: Box<dyn Any>) {
|
||||||
let callbacks = self.subscriptions.lock().remove(&entity_id);
|
let callbacks = self.subscriptions.lock().remove(&entity_id);
|
||||||
if let Some(callbacks) = callbacks {
|
if let Some(callbacks) = callbacks {
|
||||||
|
@ -1773,10 +1847,10 @@ impl MutableAppContext {
|
||||||
.or_default()
|
.or_default()
|
||||||
.entry(id)
|
.entry(id)
|
||||||
{
|
{
|
||||||
collections::btree_map::Entry::Vacant(entry) => {
|
btree_map::Entry::Vacant(entry) => {
|
||||||
entry.insert(Some(callback));
|
entry.insert(Some(callback));
|
||||||
}
|
}
|
||||||
collections::btree_map::Entry::Occupied(entry) => {
|
btree_map::Entry::Occupied(entry) => {
|
||||||
entry.remove();
|
entry.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1786,6 +1860,30 @@ impl MutableAppContext {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_global_subscription_effect(
|
||||||
|
&mut self,
|
||||||
|
type_id: TypeId,
|
||||||
|
subscription_id: usize,
|
||||||
|
callback: GlobalSubscriptionCallback,
|
||||||
|
) {
|
||||||
|
match self
|
||||||
|
.global_subscriptions
|
||||||
|
.lock()
|
||||||
|
.entry(type_id)
|
||||||
|
.or_default()
|
||||||
|
.entry(subscription_id)
|
||||||
|
{
|
||||||
|
btree_map::Entry::Vacant(entry) => {
|
||||||
|
entry.insert(Some(callback));
|
||||||
|
}
|
||||||
|
// Subscription was dropped before effect was processed
|
||||||
|
btree_map::Entry::Occupied(entry) => {
|
||||||
|
debug_assert!(entry.get().is_none());
|
||||||
|
entry.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn emit_global_event(&mut self, payload: Box<dyn Any>) {
|
fn emit_global_event(&mut self, payload: Box<dyn Any>) {
|
||||||
let type_id = (&*payload).type_id();
|
let type_id = (&*payload).type_id();
|
||||||
let callbacks = self.global_subscriptions.lock().remove(&type_id);
|
let callbacks = self.global_subscriptions.lock().remove(&type_id);
|
||||||
|
@ -1800,10 +1898,10 @@ impl MutableAppContext {
|
||||||
.or_default()
|
.or_default()
|
||||||
.entry(id)
|
.entry(id)
|
||||||
{
|
{
|
||||||
collections::btree_map::Entry::Vacant(entry) => {
|
btree_map::Entry::Vacant(entry) => {
|
||||||
entry.insert(Some(callback));
|
entry.insert(Some(callback));
|
||||||
}
|
}
|
||||||
collections::btree_map::Entry::Occupied(entry) => {
|
btree_map::Entry::Occupied(entry) => {
|
||||||
entry.remove();
|
entry.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1812,6 +1910,30 @@ impl MutableAppContext {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_observation_effect(
|
||||||
|
&mut self,
|
||||||
|
entity_id: usize,
|
||||||
|
subscription_id: usize,
|
||||||
|
callback: ObservationCallback,
|
||||||
|
) {
|
||||||
|
match self
|
||||||
|
.observations
|
||||||
|
.lock()
|
||||||
|
.entry(entity_id)
|
||||||
|
.or_default()
|
||||||
|
.entry(subscription_id)
|
||||||
|
{
|
||||||
|
btree_map::Entry::Vacant(entry) => {
|
||||||
|
entry.insert(Some(callback));
|
||||||
|
}
|
||||||
|
// Observation was dropped before effect was processed
|
||||||
|
btree_map::Entry::Occupied(entry) => {
|
||||||
|
debug_assert!(entry.get().is_none());
|
||||||
|
entry.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn notify_model_observers(&mut self, observed_id: usize) {
|
fn notify_model_observers(&mut self, observed_id: usize) {
|
||||||
let callbacks = self.observations.lock().remove(&observed_id);
|
let callbacks = self.observations.lock().remove(&observed_id);
|
||||||
if let Some(callbacks) = callbacks {
|
if let Some(callbacks) = callbacks {
|
||||||
|
@ -1827,10 +1949,10 @@ impl MutableAppContext {
|
||||||
.or_default()
|
.or_default()
|
||||||
.entry(id)
|
.entry(id)
|
||||||
{
|
{
|
||||||
collections::btree_map::Entry::Vacant(entry) => {
|
btree_map::Entry::Vacant(entry) => {
|
||||||
entry.insert(Some(callback));
|
entry.insert(Some(callback));
|
||||||
}
|
}
|
||||||
collections::btree_map::Entry::Occupied(entry) => {
|
btree_map::Entry::Occupied(entry) => {
|
||||||
entry.remove();
|
entry.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1868,10 +1990,10 @@ impl MutableAppContext {
|
||||||
.or_default()
|
.or_default()
|
||||||
.entry(id)
|
.entry(id)
|
||||||
{
|
{
|
||||||
collections::btree_map::Entry::Vacant(entry) => {
|
btree_map::Entry::Vacant(entry) => {
|
||||||
entry.insert(Some(callback));
|
entry.insert(Some(callback));
|
||||||
}
|
}
|
||||||
collections::btree_map::Entry::Occupied(entry) => {
|
btree_map::Entry::Occupied(entry) => {
|
||||||
entry.remove();
|
entry.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2017,6 +2139,10 @@ impl UpgradeViewHandle for MutableAppContext {
|
||||||
fn upgrade_view_handle<T: View>(&self, handle: &WeakViewHandle<T>) -> Option<ViewHandle<T>> {
|
fn upgrade_view_handle<T: View>(&self, handle: &WeakViewHandle<T>) -> Option<ViewHandle<T>> {
|
||||||
self.cx.upgrade_view_handle(handle)
|
self.cx.upgrade_view_handle(handle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn upgrade_any_view_handle(&self, handle: &AnyWeakViewHandle) -> Option<AnyViewHandle> {
|
||||||
|
self.cx.upgrade_any_view_handle(handle)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ReadView for MutableAppContext {
|
impl ReadView for MutableAppContext {
|
||||||
|
@ -2111,6 +2237,10 @@ impl AppContext {
|
||||||
&self.platform
|
&self.platform
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn has_global<T: 'static>(&self) -> bool {
|
||||||
|
self.globals.contains_key(&TypeId::of::<T>())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn global<T: 'static>(&self) -> &T {
|
pub fn global<T: 'static>(&self) -> &T {
|
||||||
self.globals
|
self.globals
|
||||||
.get(&TypeId::of::<T>())
|
.get(&TypeId::of::<T>())
|
||||||
|
@ -2174,6 +2304,19 @@ impl UpgradeViewHandle for AppContext {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn upgrade_any_view_handle(&self, handle: &AnyWeakViewHandle) -> Option<AnyViewHandle> {
|
||||||
|
if self.ref_counts.lock().is_entity_alive(handle.view_id) {
|
||||||
|
Some(AnyViewHandle::new(
|
||||||
|
handle.window_id,
|
||||||
|
handle.view_id,
|
||||||
|
handle.view_type,
|
||||||
|
self.ref_counts.clone(),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ReadView for AppContext {
|
impl ReadView for AppContext {
|
||||||
|
@ -2201,13 +2344,28 @@ pub struct WindowInvalidation {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum Effect {
|
pub enum Effect {
|
||||||
|
Subscription {
|
||||||
|
entity_id: usize,
|
||||||
|
subscription_id: usize,
|
||||||
|
callback: SubscriptionCallback,
|
||||||
|
},
|
||||||
Event {
|
Event {
|
||||||
entity_id: usize,
|
entity_id: usize,
|
||||||
payload: Box<dyn Any>,
|
payload: Box<dyn Any>,
|
||||||
},
|
},
|
||||||
|
GlobalSubscription {
|
||||||
|
type_id: TypeId,
|
||||||
|
subscription_id: usize,
|
||||||
|
callback: GlobalSubscriptionCallback,
|
||||||
|
},
|
||||||
GlobalEvent {
|
GlobalEvent {
|
||||||
payload: Box<dyn Any>,
|
payload: Box<dyn Any>,
|
||||||
},
|
},
|
||||||
|
Observation {
|
||||||
|
entity_id: usize,
|
||||||
|
subscription_id: usize,
|
||||||
|
callback: ObservationCallback,
|
||||||
|
},
|
||||||
ModelNotification {
|
ModelNotification {
|
||||||
model_id: usize,
|
model_id: usize,
|
||||||
},
|
},
|
||||||
|
@ -2215,7 +2373,10 @@ pub enum Effect {
|
||||||
window_id: usize,
|
window_id: usize,
|
||||||
view_id: usize,
|
view_id: usize,
|
||||||
},
|
},
|
||||||
Deferred(Box<dyn FnOnce(&mut MutableAppContext)>),
|
Deferred {
|
||||||
|
callback: Box<dyn FnOnce(&mut MutableAppContext)>,
|
||||||
|
after_window_update: bool,
|
||||||
|
},
|
||||||
ModelRelease {
|
ModelRelease {
|
||||||
model_id: usize,
|
model_id: usize,
|
||||||
model: Box<dyn AnyModel>,
|
model: Box<dyn AnyModel>,
|
||||||
|
@ -2237,14 +2398,41 @@ pub enum Effect {
|
||||||
impl Debug for Effect {
|
impl Debug for Effect {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
|
Effect::Subscription {
|
||||||
|
entity_id,
|
||||||
|
subscription_id,
|
||||||
|
..
|
||||||
|
} => f
|
||||||
|
.debug_struct("Effect::Subscribe")
|
||||||
|
.field("entity_id", entity_id)
|
||||||
|
.field("subscription_id", subscription_id)
|
||||||
|
.finish(),
|
||||||
Effect::Event { entity_id, .. } => f
|
Effect::Event { entity_id, .. } => f
|
||||||
.debug_struct("Effect::Event")
|
.debug_struct("Effect::Event")
|
||||||
.field("entity_id", entity_id)
|
.field("entity_id", entity_id)
|
||||||
.finish(),
|
.finish(),
|
||||||
|
Effect::GlobalSubscription {
|
||||||
|
type_id,
|
||||||
|
subscription_id,
|
||||||
|
..
|
||||||
|
} => f
|
||||||
|
.debug_struct("Effect::Subscribe")
|
||||||
|
.field("type_id", type_id)
|
||||||
|
.field("subscription_id", subscription_id)
|
||||||
|
.finish(),
|
||||||
Effect::GlobalEvent { payload, .. } => f
|
Effect::GlobalEvent { payload, .. } => f
|
||||||
.debug_struct("Effect::GlobalEvent")
|
.debug_struct("Effect::GlobalEvent")
|
||||||
.field("type_id", &(&*payload).type_id())
|
.field("type_id", &(&*payload).type_id())
|
||||||
.finish(),
|
.finish(),
|
||||||
|
Effect::Observation {
|
||||||
|
entity_id,
|
||||||
|
subscription_id,
|
||||||
|
..
|
||||||
|
} => f
|
||||||
|
.debug_struct("Effect::Observation")
|
||||||
|
.field("entity_id", entity_id)
|
||||||
|
.field("subscription_id", subscription_id)
|
||||||
|
.finish(),
|
||||||
Effect::ModelNotification { model_id } => f
|
Effect::ModelNotification { model_id } => f
|
||||||
.debug_struct("Effect::ModelNotification")
|
.debug_struct("Effect::ModelNotification")
|
||||||
.field("model_id", model_id)
|
.field("model_id", model_id)
|
||||||
|
@ -2254,7 +2442,7 @@ impl Debug for Effect {
|
||||||
.field("window_id", window_id)
|
.field("window_id", window_id)
|
||||||
.field("view_id", view_id)
|
.field("view_id", view_id)
|
||||||
.finish(),
|
.finish(),
|
||||||
Effect::Deferred(_) => f.debug_struct("Effect::Deferred").finish(),
|
Effect::Deferred { .. } => f.debug_struct("Effect::Deferred").finish(),
|
||||||
Effect::ModelRelease { model_id, .. } => f
|
Effect::ModelRelease { model_id, .. } => f
|
||||||
.debug_struct("Effect::ModelRelease")
|
.debug_struct("Effect::ModelRelease")
|
||||||
.field("model_id", model_id)
|
.field("model_id", model_id)
|
||||||
|
@ -2786,6 +2974,18 @@ impl<'a, T: View> ViewContext<'a, T> {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn after_window_update(
|
||||||
|
&mut self,
|
||||||
|
callback: impl 'static + FnOnce(&mut T, &mut ViewContext<T>),
|
||||||
|
) {
|
||||||
|
let handle = self.handle();
|
||||||
|
self.app.after_window_update(move |cx| {
|
||||||
|
handle.update(cx, |view, cx| {
|
||||||
|
callback(view, cx);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn propagate_action(&mut self) {
|
pub fn propagate_action(&mut self) {
|
||||||
self.app.halt_action_dispatch = false;
|
self.app.halt_action_dispatch = false;
|
||||||
}
|
}
|
||||||
|
@ -2931,6 +3131,10 @@ impl<V> UpgradeViewHandle for ViewContext<'_, V> {
|
||||||
fn upgrade_view_handle<T: View>(&self, handle: &WeakViewHandle<T>) -> Option<ViewHandle<T>> {
|
fn upgrade_view_handle<T: View>(&self, handle: &WeakViewHandle<T>) -> Option<ViewHandle<T>> {
|
||||||
self.cx.upgrade_view_handle(handle)
|
self.cx.upgrade_view_handle(handle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn upgrade_any_view_handle(&self, handle: &AnyWeakViewHandle) -> Option<AnyViewHandle> {
|
||||||
|
self.cx.upgrade_any_view_handle(handle)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View> UpdateModel for ViewContext<'_, V> {
|
impl<V: View> UpdateModel for ViewContext<'_, V> {
|
||||||
|
@ -3505,6 +3709,13 @@ impl<T> PartialEq<ViewHandle<T>> for WeakViewHandle<T> {
|
||||||
|
|
||||||
impl<T> Eq for ViewHandle<T> {}
|
impl<T> Eq for ViewHandle<T> {}
|
||||||
|
|
||||||
|
impl<T> Hash for ViewHandle<T> {
|
||||||
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
|
self.window_id.hash(state);
|
||||||
|
self.view_id.hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T> Debug for ViewHandle<T> {
|
impl<T> Debug for ViewHandle<T> {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
f.debug_struct(&format!("ViewHandle<{}>", type_name::<T>()))
|
f.debug_struct(&format!("ViewHandle<{}>", type_name::<T>()))
|
||||||
|
@ -3619,6 +3830,18 @@ impl AnyViewHandle {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn downgrade(&self) -> AnyWeakViewHandle {
|
||||||
|
AnyWeakViewHandle {
|
||||||
|
window_id: self.window_id,
|
||||||
|
view_id: self.view_id,
|
||||||
|
view_type: self.view_type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn view_type(&self) -> TypeId {
|
||||||
|
self.view_type
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Clone for AnyViewHandle {
|
impl Clone for AnyViewHandle {
|
||||||
|
@ -3845,6 +4068,28 @@ impl<T> Hash for WeakViewHandle<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct AnyWeakViewHandle {
|
||||||
|
window_id: usize,
|
||||||
|
view_id: usize,
|
||||||
|
view_type: TypeId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AnyWeakViewHandle {
|
||||||
|
pub fn upgrade(&self, cx: &impl UpgradeViewHandle) -> Option<AnyViewHandle> {
|
||||||
|
cx.upgrade_any_view_handle(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: View> From<WeakViewHandle<T>> for AnyWeakViewHandle {
|
||||||
|
fn from(handle: WeakViewHandle<T>) -> Self {
|
||||||
|
AnyWeakViewHandle {
|
||||||
|
window_id: handle.window_id,
|
||||||
|
view_id: handle.view_id,
|
||||||
|
view_type: TypeId::of::<T>(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||||
pub struct ElementStateId {
|
pub struct ElementStateId {
|
||||||
view_id: usize,
|
view_id: usize,
|
||||||
|
@ -3975,10 +4220,10 @@ impl Drop for Subscription {
|
||||||
.or_default()
|
.or_default()
|
||||||
.entry(*id)
|
.entry(*id)
|
||||||
{
|
{
|
||||||
collections::btree_map::Entry::Vacant(entry) => {
|
btree_map::Entry::Vacant(entry) => {
|
||||||
entry.insert(None);
|
entry.insert(None);
|
||||||
}
|
}
|
||||||
collections::btree_map::Entry::Occupied(entry) => {
|
btree_map::Entry::Occupied(entry) => {
|
||||||
entry.remove();
|
entry.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3991,10 +4236,10 @@ impl Drop for Subscription {
|
||||||
} => {
|
} => {
|
||||||
if let Some(subscriptions) = subscriptions.as_ref().and_then(Weak::upgrade) {
|
if let Some(subscriptions) = subscriptions.as_ref().and_then(Weak::upgrade) {
|
||||||
match subscriptions.lock().entry(*type_id).or_default().entry(*id) {
|
match subscriptions.lock().entry(*type_id).or_default().entry(*id) {
|
||||||
collections::btree_map::Entry::Vacant(entry) => {
|
btree_map::Entry::Vacant(entry) => {
|
||||||
entry.insert(None);
|
entry.insert(None);
|
||||||
}
|
}
|
||||||
collections::btree_map::Entry::Occupied(entry) => {
|
btree_map::Entry::Occupied(entry) => {
|
||||||
entry.remove();
|
entry.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4012,10 +4257,10 @@ impl Drop for Subscription {
|
||||||
.or_default()
|
.or_default()
|
||||||
.entry(*id)
|
.entry(*id)
|
||||||
{
|
{
|
||||||
collections::btree_map::Entry::Vacant(entry) => {
|
btree_map::Entry::Vacant(entry) => {
|
||||||
entry.insert(None);
|
entry.insert(None);
|
||||||
}
|
}
|
||||||
collections::btree_map::Entry::Occupied(entry) => {
|
btree_map::Entry::Occupied(entry) => {
|
||||||
entry.remove();
|
entry.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4220,7 +4465,7 @@ mod tests {
|
||||||
use smol::future::poll_once;
|
use smol::future::poll_once;
|
||||||
use std::{
|
use std::{
|
||||||
cell::Cell,
|
cell::Cell,
|
||||||
sync::atomic::{AtomicUsize, Ordering::SeqCst},
|
sync::atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[crate::test(self)]
|
#[crate::test(self)]
|
||||||
|
@ -4297,6 +4542,7 @@ mod tests {
|
||||||
|
|
||||||
let handle_1 = cx.add_model(|_| Model::default());
|
let handle_1 = cx.add_model(|_| Model::default());
|
||||||
let handle_2 = cx.add_model(|_| Model::default());
|
let handle_2 = cx.add_model(|_| Model::default());
|
||||||
|
|
||||||
handle_1.update(cx, |_, cx| {
|
handle_1.update(cx, |_, cx| {
|
||||||
cx.subscribe(&handle_2, move |model: &mut Model, emitter, event, cx| {
|
cx.subscribe(&handle_2, move |model: &mut Model, emitter, event, cx| {
|
||||||
model.events.push(*event);
|
model.events.push(*event);
|
||||||
|
@ -4316,6 +4562,37 @@ mod tests {
|
||||||
assert_eq!(handle_1.read(cx).events, vec![7, 5, 10]);
|
assert_eq!(handle_1.read(cx).events, vec![7, 5, 10]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[crate::test(self)]
|
||||||
|
fn test_model_emit_before_subscribe_in_same_update_cycle(cx: &mut MutableAppContext) {
|
||||||
|
#[derive(Default)]
|
||||||
|
struct Model;
|
||||||
|
|
||||||
|
impl Entity for Model {
|
||||||
|
type Event = ();
|
||||||
|
}
|
||||||
|
|
||||||
|
let events = Rc::new(RefCell::new(Vec::new()));
|
||||||
|
cx.add_model(|cx| {
|
||||||
|
drop(cx.subscribe(&cx.handle(), {
|
||||||
|
let events = events.clone();
|
||||||
|
move |_, _, _, _| events.borrow_mut().push("dropped before flush")
|
||||||
|
}));
|
||||||
|
cx.subscribe(&cx.handle(), {
|
||||||
|
let events = events.clone();
|
||||||
|
move |_, _, _, _| events.borrow_mut().push("before emit")
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
cx.emit(());
|
||||||
|
cx.subscribe(&cx.handle(), {
|
||||||
|
let events = events.clone();
|
||||||
|
move |_, _, _, _| events.borrow_mut().push("after emit")
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
Model
|
||||||
|
});
|
||||||
|
assert_eq!(*events.borrow(), ["before emit"]);
|
||||||
|
}
|
||||||
|
|
||||||
#[crate::test(self)]
|
#[crate::test(self)]
|
||||||
fn test_observe_and_notify_from_model(cx: &mut MutableAppContext) {
|
fn test_observe_and_notify_from_model(cx: &mut MutableAppContext) {
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
@ -4355,6 +4632,89 @@ mod tests {
|
||||||
assert_eq!(handle_1.read(cx).events, vec![7, 5, 10])
|
assert_eq!(handle_1.read(cx).events, vec![7, 5, 10])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[crate::test(self)]
|
||||||
|
fn test_model_notify_before_observe_in_same_update_cycle(cx: &mut MutableAppContext) {
|
||||||
|
#[derive(Default)]
|
||||||
|
struct Model;
|
||||||
|
|
||||||
|
impl Entity for Model {
|
||||||
|
type Event = ();
|
||||||
|
}
|
||||||
|
|
||||||
|
let events = Rc::new(RefCell::new(Vec::new()));
|
||||||
|
cx.add_model(|cx| {
|
||||||
|
drop(cx.observe(&cx.handle(), {
|
||||||
|
let events = events.clone();
|
||||||
|
move |_, _, _| events.borrow_mut().push("dropped before flush")
|
||||||
|
}));
|
||||||
|
cx.observe(&cx.handle(), {
|
||||||
|
let events = events.clone();
|
||||||
|
move |_, _, _| events.borrow_mut().push("before notify")
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
cx.notify();
|
||||||
|
cx.observe(&cx.handle(), {
|
||||||
|
let events = events.clone();
|
||||||
|
move |_, _, _| events.borrow_mut().push("after notify")
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
Model
|
||||||
|
});
|
||||||
|
assert_eq!(*events.borrow(), ["before notify"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[crate::test(self)]
|
||||||
|
fn test_defer_and_after_window_update(cx: &mut MutableAppContext) {
|
||||||
|
struct View {
|
||||||
|
render_count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Entity for View {
|
||||||
|
type Event = usize;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl super::View for View {
|
||||||
|
fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
|
||||||
|
post_inc(&mut self.render_count);
|
||||||
|
Empty::new().boxed()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ui_name() -> &'static str {
|
||||||
|
"View"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (_, view) = cx.add_window(Default::default(), |_| View { render_count: 0 });
|
||||||
|
let called_defer = Rc::new(AtomicBool::new(false));
|
||||||
|
let called_after_window_update = Rc::new(AtomicBool::new(false));
|
||||||
|
|
||||||
|
view.update(cx, |this, cx| {
|
||||||
|
assert_eq!(this.render_count, 1);
|
||||||
|
cx.defer({
|
||||||
|
let called_defer = called_defer.clone();
|
||||||
|
move |this, _| {
|
||||||
|
assert_eq!(this.render_count, 1);
|
||||||
|
called_defer.store(true, SeqCst);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cx.after_window_update({
|
||||||
|
let called_after_window_update = called_after_window_update.clone();
|
||||||
|
move |this, cx| {
|
||||||
|
assert_eq!(this.render_count, 2);
|
||||||
|
called_after_window_update.store(true, SeqCst);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
assert!(!called_defer.load(SeqCst));
|
||||||
|
assert!(!called_after_window_update.load(SeqCst));
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
|
||||||
|
assert!(called_defer.load(SeqCst));
|
||||||
|
assert!(called_after_window_update.load(SeqCst));
|
||||||
|
assert_eq!(view.read(cx).render_count, 3);
|
||||||
|
}
|
||||||
|
|
||||||
#[crate::test(self)]
|
#[crate::test(self)]
|
||||||
fn test_view_handles(cx: &mut MutableAppContext) {
|
fn test_view_handles(cx: &mut MutableAppContext) {
|
||||||
struct View {
|
struct View {
|
||||||
|
@ -4649,6 +5009,41 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[crate::test(self)]
|
||||||
|
fn test_global_events_emitted_before_subscription_in_same_update_cycle(
|
||||||
|
cx: &mut MutableAppContext,
|
||||||
|
) {
|
||||||
|
let events = Rc::new(RefCell::new(Vec::new()));
|
||||||
|
cx.update(|cx| {
|
||||||
|
{
|
||||||
|
let events = events.clone();
|
||||||
|
drop(cx.subscribe_global(move |_: &(), _| {
|
||||||
|
events.borrow_mut().push("dropped before emit");
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let events = events.clone();
|
||||||
|
cx.subscribe_global(move |_: &(), _| {
|
||||||
|
events.borrow_mut().push("before emit");
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.emit_global(());
|
||||||
|
|
||||||
|
{
|
||||||
|
let events = events.clone();
|
||||||
|
cx.subscribe_global(move |_: &(), _| {
|
||||||
|
events.borrow_mut().push("after emit");
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(*events.borrow(), ["before emit"]);
|
||||||
|
}
|
||||||
|
|
||||||
#[crate::test(self)]
|
#[crate::test(self)]
|
||||||
fn test_global_nested_events(cx: &mut MutableAppContext) {
|
fn test_global_nested_events(cx: &mut MutableAppContext) {
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
@ -4661,11 +5056,13 @@ mod tests {
|
||||||
cx.subscribe_global(move |e: &GlobalEvent, cx| {
|
cx.subscribe_global(move |e: &GlobalEvent, cx| {
|
||||||
events.borrow_mut().push(("Outer", e.clone()));
|
events.borrow_mut().push(("Outer", e.clone()));
|
||||||
|
|
||||||
|
if e.0 == 1 {
|
||||||
let events = events.clone();
|
let events = events.clone();
|
||||||
cx.subscribe_global(move |e: &GlobalEvent, _| {
|
cx.subscribe_global(move |e: &GlobalEvent, _| {
|
||||||
events.borrow_mut().push(("Inner", e.clone()));
|
events.borrow_mut().push(("Inner", e.clone()));
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
@ -4675,16 +5072,18 @@ mod tests {
|
||||||
cx.emit_global(GlobalEvent(2));
|
cx.emit_global(GlobalEvent(2));
|
||||||
cx.emit_global(GlobalEvent(3));
|
cx.emit_global(GlobalEvent(3));
|
||||||
});
|
});
|
||||||
|
cx.update(|cx| {
|
||||||
|
cx.emit_global(GlobalEvent(4));
|
||||||
|
});
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
&*events.borrow(),
|
&*events.borrow(),
|
||||||
&[
|
&[
|
||||||
("Outer", GlobalEvent(1)),
|
("Outer", GlobalEvent(1)),
|
||||||
("Outer", GlobalEvent(2)),
|
("Outer", GlobalEvent(2)),
|
||||||
("Inner", GlobalEvent(2)),
|
|
||||||
("Outer", GlobalEvent(3)),
|
("Outer", GlobalEvent(3)),
|
||||||
("Inner", GlobalEvent(3)),
|
("Outer", GlobalEvent(4)),
|
||||||
("Inner", GlobalEvent(3)),
|
("Inner", GlobalEvent(4)),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -4736,6 +5135,47 @@ mod tests {
|
||||||
observed_model.update(cx, |_, cx| cx.emit(()));
|
observed_model.update(cx, |_, cx| cx.emit(()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[crate::test(self)]
|
||||||
|
fn test_view_emit_before_subscribe_in_same_update_cycle(cx: &mut MutableAppContext) {
|
||||||
|
#[derive(Default)]
|
||||||
|
struct TestView;
|
||||||
|
|
||||||
|
impl Entity for TestView {
|
||||||
|
type Event = ();
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for TestView {
|
||||||
|
fn ui_name() -> &'static str {
|
||||||
|
"TestView"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
|
||||||
|
Empty::new().boxed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let events = Rc::new(RefCell::new(Vec::new()));
|
||||||
|
cx.add_window(Default::default(), |cx| {
|
||||||
|
drop(cx.subscribe(&cx.handle(), {
|
||||||
|
let events = events.clone();
|
||||||
|
move |_, _, _, _| events.borrow_mut().push("dropped before flush")
|
||||||
|
}));
|
||||||
|
cx.subscribe(&cx.handle(), {
|
||||||
|
let events = events.clone();
|
||||||
|
move |_, _, _, _| events.borrow_mut().push("before emit")
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
cx.emit(());
|
||||||
|
cx.subscribe(&cx.handle(), {
|
||||||
|
let events = events.clone();
|
||||||
|
move |_, _, _, _| events.borrow_mut().push("after emit")
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
TestView
|
||||||
|
});
|
||||||
|
assert_eq!(*events.borrow(), ["before emit"]);
|
||||||
|
}
|
||||||
|
|
||||||
#[crate::test(self)]
|
#[crate::test(self)]
|
||||||
fn test_observe_and_notify_from_view(cx: &mut MutableAppContext) {
|
fn test_observe_and_notify_from_view(cx: &mut MutableAppContext) {
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
@ -4783,6 +5223,47 @@ mod tests {
|
||||||
assert_eq!(view.read(cx).events, vec![11]);
|
assert_eq!(view.read(cx).events, vec![11]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[crate::test(self)]
|
||||||
|
fn test_view_notify_before_observe_in_same_update_cycle(cx: &mut MutableAppContext) {
|
||||||
|
#[derive(Default)]
|
||||||
|
struct TestView;
|
||||||
|
|
||||||
|
impl Entity for TestView {
|
||||||
|
type Event = ();
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for TestView {
|
||||||
|
fn ui_name() -> &'static str {
|
||||||
|
"TestView"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
|
||||||
|
Empty::new().boxed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let events = Rc::new(RefCell::new(Vec::new()));
|
||||||
|
cx.add_window(Default::default(), |cx| {
|
||||||
|
drop(cx.observe(&cx.handle(), {
|
||||||
|
let events = events.clone();
|
||||||
|
move |_, _, _| events.borrow_mut().push("dropped before flush")
|
||||||
|
}));
|
||||||
|
cx.observe(&cx.handle(), {
|
||||||
|
let events = events.clone();
|
||||||
|
move |_, _, _| events.borrow_mut().push("before notify")
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
cx.notify();
|
||||||
|
cx.observe(&cx.handle(), {
|
||||||
|
let events = events.clone();
|
||||||
|
move |_, _, _| events.borrow_mut().push("after notify")
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
TestView
|
||||||
|
});
|
||||||
|
assert_eq!(*events.borrow(), ["before notify"]);
|
||||||
|
}
|
||||||
|
|
||||||
#[crate::test(self)]
|
#[crate::test(self)]
|
||||||
fn test_dropping_observers(cx: &mut MutableAppContext) {
|
fn test_dropping_observers(cx: &mut MutableAppContext) {
|
||||||
struct View;
|
struct View;
|
||||||
|
|
|
@ -299,6 +299,10 @@ impl<'a> UpgradeViewHandle for LayoutContext<'a> {
|
||||||
fn upgrade_view_handle<T: View>(&self, handle: &WeakViewHandle<T>) -> Option<ViewHandle<T>> {
|
fn upgrade_view_handle<T: View>(&self, handle: &WeakViewHandle<T>) -> Option<ViewHandle<T>> {
|
||||||
self.app.upgrade_view_handle(handle)
|
self.app.upgrade_view_handle(handle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn upgrade_any_view_handle(&self, handle: &crate::AnyWeakViewHandle) -> Option<AnyViewHandle> {
|
||||||
|
self.app.upgrade_any_view_handle(handle)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> ElementStateContext for LayoutContext<'a> {
|
impl<'a> ElementStateContext for LayoutContext<'a> {
|
||||||
|
|
|
@ -142,7 +142,7 @@ pub enum Operation {
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub enum Event {
|
pub enum Event {
|
||||||
Operation(Operation),
|
Operation(Operation),
|
||||||
Edited,
|
Edited { local: bool },
|
||||||
Dirtied,
|
Dirtied,
|
||||||
Saved,
|
Saved,
|
||||||
FileHandleChanged,
|
FileHandleChanged,
|
||||||
|
@ -967,7 +967,7 @@ impl Buffer {
|
||||||
) -> Option<TransactionId> {
|
) -> Option<TransactionId> {
|
||||||
if let Some((transaction_id, start_version)) = self.text.end_transaction_at(now) {
|
if let Some((transaction_id, start_version)) = self.text.end_transaction_at(now) {
|
||||||
let was_dirty = start_version != self.saved_version;
|
let was_dirty = start_version != self.saved_version;
|
||||||
self.did_edit(&start_version, was_dirty, cx);
|
self.did_edit(&start_version, was_dirty, true, cx);
|
||||||
Some(transaction_id)
|
Some(transaction_id)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
@ -1160,6 +1160,7 @@ impl Buffer {
|
||||||
&mut self,
|
&mut self,
|
||||||
old_version: &clock::Global,
|
old_version: &clock::Global,
|
||||||
was_dirty: bool,
|
was_dirty: bool,
|
||||||
|
local: bool,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) {
|
) {
|
||||||
if self.edits_since::<usize>(old_version).next().is_none() {
|
if self.edits_since::<usize>(old_version).next().is_none() {
|
||||||
|
@ -1168,7 +1169,7 @@ impl Buffer {
|
||||||
|
|
||||||
self.reparse(cx);
|
self.reparse(cx);
|
||||||
|
|
||||||
cx.emit(Event::Edited);
|
cx.emit(Event::Edited { local });
|
||||||
if !was_dirty {
|
if !was_dirty {
|
||||||
cx.emit(Event::Dirtied);
|
cx.emit(Event::Dirtied);
|
||||||
}
|
}
|
||||||
|
@ -1205,7 +1206,7 @@ impl Buffer {
|
||||||
self.text.apply_ops(buffer_ops)?;
|
self.text.apply_ops(buffer_ops)?;
|
||||||
self.deferred_ops.insert(deferred_ops);
|
self.deferred_ops.insert(deferred_ops);
|
||||||
self.flush_deferred_ops(cx);
|
self.flush_deferred_ops(cx);
|
||||||
self.did_edit(&old_version, was_dirty, cx);
|
self.did_edit(&old_version, was_dirty, false, cx);
|
||||||
// Notify independently of whether the buffer was edited as the operations could include a
|
// Notify independently of whether the buffer was edited as the operations could include a
|
||||||
// selection update.
|
// selection update.
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
@ -1320,7 +1321,7 @@ impl Buffer {
|
||||||
|
|
||||||
if let Some((transaction_id, operation)) = self.text.undo() {
|
if let Some((transaction_id, operation)) = self.text.undo() {
|
||||||
self.send_operation(Operation::Buffer(operation), cx);
|
self.send_operation(Operation::Buffer(operation), cx);
|
||||||
self.did_edit(&old_version, was_dirty, cx);
|
self.did_edit(&old_version, was_dirty, true, cx);
|
||||||
Some(transaction_id)
|
Some(transaction_id)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
@ -1341,7 +1342,7 @@ impl Buffer {
|
||||||
self.send_operation(Operation::Buffer(operation), cx);
|
self.send_operation(Operation::Buffer(operation), cx);
|
||||||
}
|
}
|
||||||
if undone {
|
if undone {
|
||||||
self.did_edit(&old_version, was_dirty, cx)
|
self.did_edit(&old_version, was_dirty, true, cx)
|
||||||
}
|
}
|
||||||
undone
|
undone
|
||||||
}
|
}
|
||||||
|
@ -1352,7 +1353,7 @@ impl Buffer {
|
||||||
|
|
||||||
if let Some((transaction_id, operation)) = self.text.redo() {
|
if let Some((transaction_id, operation)) = self.text.redo() {
|
||||||
self.send_operation(Operation::Buffer(operation), cx);
|
self.send_operation(Operation::Buffer(operation), cx);
|
||||||
self.did_edit(&old_version, was_dirty, cx);
|
self.did_edit(&old_version, was_dirty, true, cx);
|
||||||
Some(transaction_id)
|
Some(transaction_id)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
@ -1373,7 +1374,7 @@ impl Buffer {
|
||||||
self.send_operation(Operation::Buffer(operation), cx);
|
self.send_operation(Operation::Buffer(operation), cx);
|
||||||
}
|
}
|
||||||
if redone {
|
if redone {
|
||||||
self.did_edit(&old_version, was_dirty, cx)
|
self.did_edit(&old_version, was_dirty, true, cx)
|
||||||
}
|
}
|
||||||
redone
|
redone
|
||||||
}
|
}
|
||||||
|
@ -1439,7 +1440,7 @@ impl Buffer {
|
||||||
if !ops.is_empty() {
|
if !ops.is_empty() {
|
||||||
for op in ops {
|
for op in ops {
|
||||||
self.send_operation(Operation::Buffer(op), cx);
|
self.send_operation(Operation::Buffer(op), cx);
|
||||||
self.did_edit(&old_version, was_dirty, cx);
|
self.did_edit(&old_version, was_dirty, true, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1800,12 +1801,6 @@ impl BufferSnapshot {
|
||||||
.min_by_key(|(open_range, close_range)| close_range.end - open_range.start)
|
.min_by_key(|(open_range, close_range)| close_range.end - open_range.start)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
impl BufferSnapshot
|
|
||||||
pub fn remote_selections_in_range(&self, Range<Anchor>) -> impl Iterator<Item = (ReplicaId, impl Iterator<Item = &Selection<Anchor>>)>
|
|
||||||
pub fn remote_selections_in_range(&self, Range<Anchor>) -> impl Iterator<Item = (ReplicaId, i
|
|
||||||
*/
|
|
||||||
|
|
||||||
pub fn remote_selections_in_range<'a>(
|
pub fn remote_selections_in_range<'a>(
|
||||||
&'a self,
|
&'a self,
|
||||||
range: Range<Anchor>,
|
range: Range<Anchor>,
|
||||||
|
|
|
@ -100,15 +100,16 @@ pub fn serialize_undo_map_entry(
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn serialize_selections(selections: &Arc<[Selection<Anchor>]>) -> Vec<proto::Selection> {
|
pub fn serialize_selections(selections: &Arc<[Selection<Anchor>]>) -> Vec<proto::Selection> {
|
||||||
selections
|
selections.iter().map(serialize_selection).collect()
|
||||||
.iter()
|
}
|
||||||
.map(|selection| proto::Selection {
|
|
||||||
|
pub fn serialize_selection(selection: &Selection<Anchor>) -> proto::Selection {
|
||||||
|
proto::Selection {
|
||||||
id: selection.id as u64,
|
id: selection.id as u64,
|
||||||
start: Some(serialize_anchor(&selection.start)),
|
start: Some(serialize_anchor(&selection.start)),
|
||||||
end: Some(serialize_anchor(&selection.end)),
|
end: Some(serialize_anchor(&selection.end)),
|
||||||
reversed: selection.reversed,
|
reversed: selection.reversed,
|
||||||
})
|
}
|
||||||
.collect()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn serialize_diagnostics<'a>(
|
pub fn serialize_diagnostics<'a>(
|
||||||
|
@ -274,7 +275,12 @@ pub fn deserialize_selections(selections: Vec<proto::Selection>) -> Arc<[Selecti
|
||||||
Arc::from(
|
Arc::from(
|
||||||
selections
|
selections
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|selection| {
|
.filter_map(deserialize_selection)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deserialize_selection(selection: proto::Selection) -> Option<Selection<Anchor>> {
|
||||||
Some(Selection {
|
Some(Selection {
|
||||||
id: selection.id as usize,
|
id: selection.id as usize,
|
||||||
start: deserialize_anchor(selection.start?)?,
|
start: deserialize_anchor(selection.start?)?,
|
||||||
|
@ -282,9 +288,6 @@ pub fn deserialize_selections(selections: Vec<proto::Selection>) -> Arc<[Selecti
|
||||||
reversed: selection.reversed,
|
reversed: selection.reversed,
|
||||||
goal: SelectionGoal::None,
|
goal: SelectionGoal::None,
|
||||||
})
|
})
|
||||||
})
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deserialize_diagnostics(
|
pub fn deserialize_diagnostics(
|
||||||
|
|
|
@ -122,11 +122,19 @@ fn test_edit_events(cx: &mut gpui::MutableAppContext) {
|
||||||
let buffer_1_events = buffer_1_events.borrow();
|
let buffer_1_events = buffer_1_events.borrow();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
*buffer_1_events,
|
*buffer_1_events,
|
||||||
vec![Event::Edited, Event::Dirtied, Event::Edited, Event::Edited]
|
vec![
|
||||||
|
Event::Edited { local: true },
|
||||||
|
Event::Dirtied,
|
||||||
|
Event::Edited { local: true },
|
||||||
|
Event::Edited { local: true }
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
let buffer_2_events = buffer_2_events.borrow();
|
let buffer_2_events = buffer_2_events.borrow();
|
||||||
assert_eq!(*buffer_2_events, vec![Event::Edited, Event::Dirtied]);
|
assert_eq!(
|
||||||
|
*buffer_2_events,
|
||||||
|
vec![Event::Edited { local: false }, Event::Dirtied]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
|
|
|
@ -224,7 +224,7 @@ impl OutlineView {
|
||||||
) {
|
) {
|
||||||
match event {
|
match event {
|
||||||
editor::Event::Blurred => cx.emit(Event::Dismissed),
|
editor::Event::Blurred => cx.emit(Event::Dismissed),
|
||||||
editor::Event::Edited => self.update_matches(cx),
|
editor::Event::Edited { .. } => self.update_matches(cx),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -124,6 +124,8 @@ pub enum Event {
|
||||||
DiskBasedDiagnosticsUpdated,
|
DiskBasedDiagnosticsUpdated,
|
||||||
DiskBasedDiagnosticsFinished,
|
DiskBasedDiagnosticsFinished,
|
||||||
DiagnosticsUpdated(ProjectPath),
|
DiagnosticsUpdated(ProjectPath),
|
||||||
|
RemoteIdChanged(Option<u64>),
|
||||||
|
CollaboratorLeft(PeerId),
|
||||||
}
|
}
|
||||||
|
|
||||||
enum LanguageServerEvent {
|
enum LanguageServerEvent {
|
||||||
|
@ -253,34 +255,35 @@ impl ProjectEntryId {
|
||||||
|
|
||||||
impl Project {
|
impl Project {
|
||||||
pub fn init(client: &Arc<Client>) {
|
pub fn init(client: &Arc<Client>) {
|
||||||
client.add_entity_message_handler(Self::handle_add_collaborator);
|
client.add_model_message_handler(Self::handle_add_collaborator);
|
||||||
client.add_entity_message_handler(Self::handle_buffer_reloaded);
|
client.add_model_message_handler(Self::handle_buffer_reloaded);
|
||||||
client.add_entity_message_handler(Self::handle_buffer_saved);
|
client.add_model_message_handler(Self::handle_buffer_saved);
|
||||||
client.add_entity_message_handler(Self::handle_start_language_server);
|
client.add_model_message_handler(Self::handle_start_language_server);
|
||||||
client.add_entity_message_handler(Self::handle_update_language_server);
|
client.add_model_message_handler(Self::handle_update_language_server);
|
||||||
client.add_entity_message_handler(Self::handle_remove_collaborator);
|
client.add_model_message_handler(Self::handle_remove_collaborator);
|
||||||
client.add_entity_message_handler(Self::handle_register_worktree);
|
client.add_model_message_handler(Self::handle_register_worktree);
|
||||||
client.add_entity_message_handler(Self::handle_unregister_worktree);
|
client.add_model_message_handler(Self::handle_unregister_worktree);
|
||||||
client.add_entity_message_handler(Self::handle_unshare_project);
|
client.add_model_message_handler(Self::handle_unshare_project);
|
||||||
client.add_entity_message_handler(Self::handle_update_buffer_file);
|
client.add_model_message_handler(Self::handle_update_buffer_file);
|
||||||
client.add_entity_message_handler(Self::handle_update_buffer);
|
client.add_model_message_handler(Self::handle_update_buffer);
|
||||||
client.add_entity_message_handler(Self::handle_update_diagnostic_summary);
|
client.add_model_message_handler(Self::handle_update_diagnostic_summary);
|
||||||
client.add_entity_message_handler(Self::handle_update_worktree);
|
client.add_model_message_handler(Self::handle_update_worktree);
|
||||||
client.add_entity_request_handler(Self::handle_apply_additional_edits_for_completion);
|
client.add_model_request_handler(Self::handle_apply_additional_edits_for_completion);
|
||||||
client.add_entity_request_handler(Self::handle_apply_code_action);
|
client.add_model_request_handler(Self::handle_apply_code_action);
|
||||||
client.add_entity_request_handler(Self::handle_format_buffers);
|
client.add_model_request_handler(Self::handle_format_buffers);
|
||||||
client.add_entity_request_handler(Self::handle_get_code_actions);
|
client.add_model_request_handler(Self::handle_get_code_actions);
|
||||||
client.add_entity_request_handler(Self::handle_get_completions);
|
client.add_model_request_handler(Self::handle_get_completions);
|
||||||
client.add_entity_request_handler(Self::handle_lsp_command::<GetDefinition>);
|
client.add_model_request_handler(Self::handle_lsp_command::<GetDefinition>);
|
||||||
client.add_entity_request_handler(Self::handle_lsp_command::<GetDocumentHighlights>);
|
client.add_model_request_handler(Self::handle_lsp_command::<GetDocumentHighlights>);
|
||||||
client.add_entity_request_handler(Self::handle_lsp_command::<GetReferences>);
|
client.add_model_request_handler(Self::handle_lsp_command::<GetReferences>);
|
||||||
client.add_entity_request_handler(Self::handle_lsp_command::<PrepareRename>);
|
client.add_model_request_handler(Self::handle_lsp_command::<PrepareRename>);
|
||||||
client.add_entity_request_handler(Self::handle_lsp_command::<PerformRename>);
|
client.add_model_request_handler(Self::handle_lsp_command::<PerformRename>);
|
||||||
client.add_entity_request_handler(Self::handle_search_project);
|
client.add_model_request_handler(Self::handle_search_project);
|
||||||
client.add_entity_request_handler(Self::handle_get_project_symbols);
|
client.add_model_request_handler(Self::handle_get_project_symbols);
|
||||||
client.add_entity_request_handler(Self::handle_open_buffer_for_symbol);
|
client.add_model_request_handler(Self::handle_open_buffer_for_symbol);
|
||||||
client.add_entity_request_handler(Self::handle_open_buffer);
|
client.add_model_request_handler(Self::handle_open_buffer_by_id);
|
||||||
client.add_entity_request_handler(Self::handle_save_buffer);
|
client.add_model_request_handler(Self::handle_open_buffer_by_path);
|
||||||
|
client.add_model_request_handler(Self::handle_save_buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn local(
|
pub fn local(
|
||||||
|
@ -487,7 +490,6 @@ impl Project {
|
||||||
cx.update(|cx| Project::local(client, user_store, languages, fs, cx))
|
cx.update(|cx| Project::local(client, user_store, languages, fs, cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
|
||||||
pub fn buffer_for_id(&self, remote_id: u64, cx: &AppContext) -> Option<ModelHandle<Buffer>> {
|
pub fn buffer_for_id(&self, remote_id: u64, cx: &AppContext) -> Option<ModelHandle<Buffer>> {
|
||||||
self.opened_buffers
|
self.opened_buffers
|
||||||
.get(&remote_id)
|
.get(&remote_id)
|
||||||
|
@ -566,6 +568,7 @@ impl Project {
|
||||||
self.subscriptions
|
self.subscriptions
|
||||||
.push(self.client.add_model_for_remote_entity(remote_id, cx));
|
.push(self.client.add_model_for_remote_entity(remote_id, cx));
|
||||||
}
|
}
|
||||||
|
cx.emit(Event::RemoteIdChanged(remote_id))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remote_id(&self) -> Option<u64> {
|
pub fn remote_id(&self) -> Option<u64> {
|
||||||
|
@ -930,7 +933,7 @@ impl Project {
|
||||||
let path_string = path.to_string_lossy().to_string();
|
let path_string = path.to_string_lossy().to_string();
|
||||||
cx.spawn(|this, mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
let response = rpc
|
let response = rpc
|
||||||
.request(proto::OpenBuffer {
|
.request(proto::OpenBufferByPath {
|
||||||
project_id,
|
project_id,
|
||||||
worktree_id: remote_worktree_id.to_proto(),
|
worktree_id: remote_worktree_id.to_proto(),
|
||||||
path: path_string,
|
path: path_string,
|
||||||
|
@ -979,6 +982,32 @@ impl Project {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn open_buffer_by_id(
|
||||||
|
&mut self,
|
||||||
|
id: u64,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Task<Result<ModelHandle<Buffer>>> {
|
||||||
|
if let Some(buffer) = self.buffer_for_id(id, cx) {
|
||||||
|
Task::ready(Ok(buffer))
|
||||||
|
} else if self.is_local() {
|
||||||
|
Task::ready(Err(anyhow!("buffer {} does not exist", id)))
|
||||||
|
} else if let Some(project_id) = self.remote_id() {
|
||||||
|
let request = self
|
||||||
|
.client
|
||||||
|
.request(proto::OpenBufferById { project_id, id });
|
||||||
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
let buffer = request
|
||||||
|
.await?
|
||||||
|
.buffer
|
||||||
|
.ok_or_else(|| anyhow!("invalid buffer"))?;
|
||||||
|
this.update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx))
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Task::ready(Err(anyhow!("cannot open buffer while disconnected")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn save_buffer_as(
|
pub fn save_buffer_as(
|
||||||
&mut self,
|
&mut self,
|
||||||
buffer: ModelHandle<Buffer>,
|
buffer: ModelHandle<Buffer>,
|
||||||
|
@ -1150,7 +1179,7 @@ impl Project {
|
||||||
});
|
});
|
||||||
cx.background().spawn(request).detach_and_log_err(cx);
|
cx.background().spawn(request).detach_and_log_err(cx);
|
||||||
}
|
}
|
||||||
BufferEvent::Edited => {
|
BufferEvent::Edited { .. } => {
|
||||||
let language_server = self
|
let language_server = self
|
||||||
.language_server_for_buffer(buffer.read(cx), cx)?
|
.language_server_for_buffer(buffer.read(cx), cx)?
|
||||||
.clone();
|
.clone();
|
||||||
|
@ -3340,6 +3369,7 @@ impl Project {
|
||||||
buffer.update(cx, |buffer, cx| buffer.remove_peer(replica_id, cx));
|
buffer.update(cx, |buffer, cx| buffer.remove_peer(replica_id, cx));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
cx.emit(Event::CollaboratorLeft(peer_id));
|
||||||
cx.notify();
|
cx.notify();
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
|
@ -3887,9 +3917,28 @@ impl Project {
|
||||||
hasher.finalize().as_slice().try_into().unwrap()
|
hasher.finalize().as_slice().try_into().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_open_buffer(
|
async fn handle_open_buffer_by_id(
|
||||||
this: ModelHandle<Self>,
|
this: ModelHandle<Self>,
|
||||||
envelope: TypedEnvelope<proto::OpenBuffer>,
|
envelope: TypedEnvelope<proto::OpenBufferById>,
|
||||||
|
_: Arc<Client>,
|
||||||
|
mut cx: AsyncAppContext,
|
||||||
|
) -> Result<proto::OpenBufferResponse> {
|
||||||
|
let peer_id = envelope.original_sender_id()?;
|
||||||
|
let buffer = this
|
||||||
|
.update(&mut cx, |this, cx| {
|
||||||
|
this.open_buffer_by_id(envelope.payload.id, cx)
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
Ok(proto::OpenBufferResponse {
|
||||||
|
buffer: Some(this.serialize_buffer_for_peer(&buffer, peer_id, cx)),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_open_buffer_by_path(
|
||||||
|
this: ModelHandle<Self>,
|
||||||
|
envelope: TypedEnvelope<proto::OpenBufferByPath>,
|
||||||
_: Arc<Client>,
|
_: Arc<Client>,
|
||||||
mut cx: AsyncAppContext,
|
mut cx: AsyncAppContext,
|
||||||
) -> Result<proto::OpenBufferResponse> {
|
) -> Result<proto::OpenBufferResponse> {
|
||||||
|
@ -6180,7 +6229,10 @@ mod tests {
|
||||||
assert!(buffer.is_dirty());
|
assert!(buffer.is_dirty());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
*events.borrow(),
|
*events.borrow(),
|
||||||
&[language::Event::Edited, language::Event::Dirtied]
|
&[
|
||||||
|
language::Event::Edited { local: true },
|
||||||
|
language::Event::Dirtied
|
||||||
|
]
|
||||||
);
|
);
|
||||||
events.borrow_mut().clear();
|
events.borrow_mut().clear();
|
||||||
buffer.did_save(buffer.version(), buffer.file().unwrap().mtime(), None, cx);
|
buffer.did_save(buffer.version(), buffer.file().unwrap().mtime(), None, cx);
|
||||||
|
@ -6203,9 +6255,9 @@ mod tests {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
*events.borrow(),
|
*events.borrow(),
|
||||||
&[
|
&[
|
||||||
language::Event::Edited,
|
language::Event::Edited { local: true },
|
||||||
language::Event::Dirtied,
|
language::Event::Dirtied,
|
||||||
language::Event::Edited,
|
language::Event::Edited { local: true },
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
events.borrow_mut().clear();
|
events.borrow_mut().clear();
|
||||||
|
@ -6217,7 +6269,7 @@ mod tests {
|
||||||
assert!(buffer.is_dirty());
|
assert!(buffer.is_dirty());
|
||||||
});
|
});
|
||||||
|
|
||||||
assert_eq!(*events.borrow(), &[language::Event::Edited]);
|
assert_eq!(*events.borrow(), &[language::Event::Edited { local: true }]);
|
||||||
|
|
||||||
// When a file is deleted, the buffer is considered dirty.
|
// When a file is deleted, the buffer is considered dirty.
|
||||||
let events = Rc::new(RefCell::new(Vec::new()));
|
let events = Rc::new(RefCell::new(Vec::new()));
|
||||||
|
|
|
@ -328,7 +328,7 @@ impl ProjectSymbolsView {
|
||||||
) {
|
) {
|
||||||
match event {
|
match event {
|
||||||
editor::Event::Blurred => cx.emit(Event::Dismissed),
|
editor::Event::Blurred => cx.emit(Event::Dismissed),
|
||||||
editor::Event::Edited => self.update_matches(cx),
|
editor::Event::Edited { .. } => self.update_matches(cx),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,8 +40,9 @@ message Envelope {
|
||||||
StartLanguageServer start_language_server = 33;
|
StartLanguageServer start_language_server = 33;
|
||||||
UpdateLanguageServer update_language_server = 34;
|
UpdateLanguageServer update_language_server = 34;
|
||||||
|
|
||||||
OpenBuffer open_buffer = 35;
|
OpenBufferById open_buffer_by_id = 35;
|
||||||
OpenBufferResponse open_buffer_response = 36;
|
OpenBufferByPath open_buffer_by_path = 36;
|
||||||
|
OpenBufferResponse open_buffer_response = 37;
|
||||||
UpdateBuffer update_buffer = 38;
|
UpdateBuffer update_buffer = 38;
|
||||||
UpdateBufferFile update_buffer_file = 39;
|
UpdateBufferFile update_buffer_file = 39;
|
||||||
SaveBuffer save_buffer = 40;
|
SaveBuffer save_buffer = 40;
|
||||||
|
@ -79,6 +80,11 @@ message Envelope {
|
||||||
|
|
||||||
GetUsers get_users = 70;
|
GetUsers get_users = 70;
|
||||||
GetUsersResponse get_users_response = 71;
|
GetUsersResponse get_users_response = 71;
|
||||||
|
|
||||||
|
Follow follow = 72;
|
||||||
|
FollowResponse follow_response = 73;
|
||||||
|
UpdateFollowers update_followers = 74;
|
||||||
|
Unfollow unfollow = 75;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -241,12 +247,17 @@ message OpenBufferForSymbolResponse {
|
||||||
Buffer buffer = 1;
|
Buffer buffer = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message OpenBuffer {
|
message OpenBufferByPath {
|
||||||
uint64 project_id = 1;
|
uint64 project_id = 1;
|
||||||
uint64 worktree_id = 2;
|
uint64 worktree_id = 2;
|
||||||
string path = 3;
|
string path = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message OpenBufferById {
|
||||||
|
uint64 project_id = 1;
|
||||||
|
uint64 id = 2;
|
||||||
|
}
|
||||||
|
|
||||||
message OpenBufferResponse {
|
message OpenBufferResponse {
|
||||||
Buffer buffer = 1;
|
Buffer buffer = 1;
|
||||||
}
|
}
|
||||||
|
@ -521,8 +532,77 @@ message UpdateContacts {
|
||||||
repeated Contact contacts = 1;
|
repeated Contact contacts = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message UpdateDiagnostics {
|
||||||
|
uint32 replica_id = 1;
|
||||||
|
uint32 lamport_timestamp = 2;
|
||||||
|
repeated Diagnostic diagnostics = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Follow {
|
||||||
|
uint64 project_id = 1;
|
||||||
|
uint32 leader_id = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message FollowResponse {
|
||||||
|
optional uint64 active_view_id = 1;
|
||||||
|
repeated View views = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UpdateFollowers {
|
||||||
|
uint64 project_id = 1;
|
||||||
|
repeated uint32 follower_ids = 2;
|
||||||
|
oneof variant {
|
||||||
|
UpdateActiveView update_active_view = 3;
|
||||||
|
View create_view = 4;
|
||||||
|
UpdateView update_view = 5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message Unfollow {
|
||||||
|
uint64 project_id = 1;
|
||||||
|
uint32 leader_id = 2;
|
||||||
|
}
|
||||||
|
|
||||||
// Entities
|
// Entities
|
||||||
|
|
||||||
|
message UpdateActiveView {
|
||||||
|
optional uint64 id = 1;
|
||||||
|
optional uint32 leader_id = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UpdateView {
|
||||||
|
uint64 id = 1;
|
||||||
|
optional uint32 leader_id = 2;
|
||||||
|
|
||||||
|
oneof variant {
|
||||||
|
Editor editor = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Editor {
|
||||||
|
repeated Selection selections = 1;
|
||||||
|
Anchor scroll_top_anchor = 2;
|
||||||
|
float scroll_x = 3;
|
||||||
|
float scroll_y = 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message View {
|
||||||
|
uint64 id = 1;
|
||||||
|
optional uint32 leader_id = 2;
|
||||||
|
|
||||||
|
oneof variant {
|
||||||
|
Editor editor = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Editor {
|
||||||
|
uint64 buffer_id = 1;
|
||||||
|
repeated Selection selections = 2;
|
||||||
|
Anchor scroll_top_anchor = 3;
|
||||||
|
float scroll_x = 4;
|
||||||
|
float scroll_y = 5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
message Collaborator {
|
message Collaborator {
|
||||||
uint32 peer_id = 1;
|
uint32 peer_id = 1;
|
||||||
uint32 replica_id = 2;
|
uint32 replica_id = 2;
|
||||||
|
@ -578,17 +658,6 @@ message BufferState {
|
||||||
repeated string completion_triggers = 8;
|
repeated string completion_triggers = 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
message BufferFragment {
|
|
||||||
uint32 replica_id = 1;
|
|
||||||
uint32 local_timestamp = 2;
|
|
||||||
uint32 lamport_timestamp = 3;
|
|
||||||
uint32 insertion_offset = 4;
|
|
||||||
uint32 len = 5;
|
|
||||||
bool visible = 6;
|
|
||||||
repeated VectorClockEntry deletions = 7;
|
|
||||||
repeated VectorClockEntry max_undos = 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
message SelectionSet {
|
message SelectionSet {
|
||||||
uint32 replica_id = 1;
|
uint32 replica_id = 1;
|
||||||
repeated Selection selections = 2;
|
repeated Selection selections = 2;
|
||||||
|
@ -614,12 +683,6 @@ enum Bias {
|
||||||
Right = 1;
|
Right = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message UpdateDiagnostics {
|
|
||||||
uint32 replica_id = 1;
|
|
||||||
uint32 lamport_timestamp = 2;
|
|
||||||
repeated Diagnostic diagnostics = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
message Diagnostic {
|
message Diagnostic {
|
||||||
Anchor start = 1;
|
Anchor start = 1;
|
||||||
Anchor end = 2;
|
Anchor end = 2;
|
||||||
|
|
|
@ -147,6 +147,8 @@ messages!(
|
||||||
(BufferSaved, Foreground),
|
(BufferSaved, Foreground),
|
||||||
(ChannelMessageSent, Foreground),
|
(ChannelMessageSent, Foreground),
|
||||||
(Error, Foreground),
|
(Error, Foreground),
|
||||||
|
(Follow, Foreground),
|
||||||
|
(FollowResponse, Foreground),
|
||||||
(FormatBuffers, Foreground),
|
(FormatBuffers, Foreground),
|
||||||
(FormatBuffersResponse, Foreground),
|
(FormatBuffersResponse, Foreground),
|
||||||
(GetChannelMessages, Foreground),
|
(GetChannelMessages, Foreground),
|
||||||
|
@ -175,7 +177,8 @@ messages!(
|
||||||
(UpdateLanguageServer, Foreground),
|
(UpdateLanguageServer, Foreground),
|
||||||
(LeaveChannel, Foreground),
|
(LeaveChannel, Foreground),
|
||||||
(LeaveProject, Foreground),
|
(LeaveProject, Foreground),
|
||||||
(OpenBuffer, Background),
|
(OpenBufferById, Background),
|
||||||
|
(OpenBufferByPath, Background),
|
||||||
(OpenBufferForSymbol, Background),
|
(OpenBufferForSymbol, Background),
|
||||||
(OpenBufferForSymbolResponse, Background),
|
(OpenBufferForSymbolResponse, Background),
|
||||||
(OpenBufferResponse, Background),
|
(OpenBufferResponse, Background),
|
||||||
|
@ -195,13 +198,15 @@ messages!(
|
||||||
(SendChannelMessageResponse, Foreground),
|
(SendChannelMessageResponse, Foreground),
|
||||||
(ShareProject, Foreground),
|
(ShareProject, Foreground),
|
||||||
(Test, Foreground),
|
(Test, Foreground),
|
||||||
|
(Unfollow, Foreground),
|
||||||
(UnregisterProject, Foreground),
|
(UnregisterProject, Foreground),
|
||||||
(UnregisterWorktree, Foreground),
|
(UnregisterWorktree, Foreground),
|
||||||
(UnshareProject, Foreground),
|
(UnshareProject, Foreground),
|
||||||
(UpdateBuffer, Background),
|
(UpdateBuffer, Foreground),
|
||||||
(UpdateBufferFile, Foreground),
|
(UpdateBufferFile, Foreground),
|
||||||
(UpdateContacts, Foreground),
|
(UpdateContacts, Foreground),
|
||||||
(UpdateDiagnosticSummary, Foreground),
|
(UpdateDiagnosticSummary, Foreground),
|
||||||
|
(UpdateFollowers, Foreground),
|
||||||
(UpdateWorktree, Foreground),
|
(UpdateWorktree, Foreground),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -211,6 +216,7 @@ request_messages!(
|
||||||
ApplyCompletionAdditionalEdits,
|
ApplyCompletionAdditionalEdits,
|
||||||
ApplyCompletionAdditionalEditsResponse
|
ApplyCompletionAdditionalEditsResponse
|
||||||
),
|
),
|
||||||
|
(Follow, FollowResponse),
|
||||||
(FormatBuffers, FormatBuffersResponse),
|
(FormatBuffers, FormatBuffersResponse),
|
||||||
(GetChannelMessages, GetChannelMessagesResponse),
|
(GetChannelMessages, GetChannelMessagesResponse),
|
||||||
(GetChannels, GetChannelsResponse),
|
(GetChannels, GetChannelsResponse),
|
||||||
|
@ -223,7 +229,8 @@ request_messages!(
|
||||||
(GetUsers, GetUsersResponse),
|
(GetUsers, GetUsersResponse),
|
||||||
(JoinChannel, JoinChannelResponse),
|
(JoinChannel, JoinChannelResponse),
|
||||||
(JoinProject, JoinProjectResponse),
|
(JoinProject, JoinProjectResponse),
|
||||||
(OpenBuffer, OpenBufferResponse),
|
(OpenBufferById, OpenBufferResponse),
|
||||||
|
(OpenBufferByPath, OpenBufferResponse),
|
||||||
(OpenBufferForSymbol, OpenBufferForSymbolResponse),
|
(OpenBufferForSymbol, OpenBufferForSymbolResponse),
|
||||||
(Ping, Ack),
|
(Ping, Ack),
|
||||||
(PerformRename, PerformRenameResponse),
|
(PerformRename, PerformRenameResponse),
|
||||||
|
@ -246,6 +253,7 @@ entity_messages!(
|
||||||
ApplyCompletionAdditionalEdits,
|
ApplyCompletionAdditionalEdits,
|
||||||
BufferReloaded,
|
BufferReloaded,
|
||||||
BufferSaved,
|
BufferSaved,
|
||||||
|
Follow,
|
||||||
FormatBuffers,
|
FormatBuffers,
|
||||||
GetCodeActions,
|
GetCodeActions,
|
||||||
GetCompletions,
|
GetCompletions,
|
||||||
|
@ -255,7 +263,8 @@ entity_messages!(
|
||||||
GetProjectSymbols,
|
GetProjectSymbols,
|
||||||
JoinProject,
|
JoinProject,
|
||||||
LeaveProject,
|
LeaveProject,
|
||||||
OpenBuffer,
|
OpenBufferById,
|
||||||
|
OpenBufferByPath,
|
||||||
OpenBufferForSymbol,
|
OpenBufferForSymbol,
|
||||||
PerformRename,
|
PerformRename,
|
||||||
PrepareRename,
|
PrepareRename,
|
||||||
|
@ -263,11 +272,13 @@ entity_messages!(
|
||||||
SaveBuffer,
|
SaveBuffer,
|
||||||
SearchProject,
|
SearchProject,
|
||||||
StartLanguageServer,
|
StartLanguageServer,
|
||||||
|
Unfollow,
|
||||||
UnregisterWorktree,
|
UnregisterWorktree,
|
||||||
UnshareProject,
|
UnshareProject,
|
||||||
UpdateBuffer,
|
UpdateBuffer,
|
||||||
UpdateBufferFile,
|
UpdateBufferFile,
|
||||||
UpdateDiagnosticSummary,
|
UpdateDiagnosticSummary,
|
||||||
|
UpdateFollowers,
|
||||||
UpdateLanguageServer,
|
UpdateLanguageServer,
|
||||||
RegisterWorktree,
|
RegisterWorktree,
|
||||||
UpdateWorktree,
|
UpdateWorktree,
|
||||||
|
|
|
@ -5,4 +5,4 @@ pub mod proto;
|
||||||
pub use conn::Connection;
|
pub use conn::Connection;
|
||||||
pub use peer::*;
|
pub use peer::*;
|
||||||
|
|
||||||
pub const PROTOCOL_VERSION: u32 = 11;
|
pub const PROTOCOL_VERSION: u32 = 12;
|
||||||
|
|
|
@ -360,7 +360,7 @@ impl SearchBar {
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
match event {
|
match event {
|
||||||
editor::Event::Edited => {
|
editor::Event::Edited { .. } => {
|
||||||
self.query_contains_error = false;
|
self.query_contains_error = false;
|
||||||
self.clear_matches(cx);
|
self.clear_matches(cx);
|
||||||
self.update_matches(true, cx);
|
self.update_matches(true, cx);
|
||||||
|
@ -377,8 +377,8 @@ impl SearchBar {
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
match event {
|
match event {
|
||||||
editor::Event::Edited => self.update_matches(false, cx),
|
editor::Event::Edited { .. } => self.update_matches(false, cx),
|
||||||
editor::Event::SelectionsChanged => self.update_match_index(cx),
|
editor::Event::SelectionsChanged { .. } => self.update_match_index(cx),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -250,6 +250,10 @@ impl Item for ProjectSearchView {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn project_entry_id(&self, _: &AppContext) -> Option<project::ProjectEntryId> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
fn can_save(&self, _: &gpui::AppContext) -> bool {
|
fn can_save(&self, _: &gpui::AppContext) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
@ -346,7 +350,7 @@ impl ProjectSearchView {
|
||||||
cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab))
|
cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab))
|
||||||
.detach();
|
.detach();
|
||||||
cx.subscribe(&results_editor, |this, _, event, cx| {
|
cx.subscribe(&results_editor, |this, _, event, cx| {
|
||||||
if matches!(event, editor::Event::SelectionsChanged) {
|
if matches!(event, editor::Event::SelectionsChanged { .. }) {
|
||||||
this.update_match_index(cx);
|
this.update_match_index(cx);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -92,7 +92,8 @@ impl Server {
|
||||||
.add_request_handler(Server::forward_project_request::<proto::GetDocumentHighlights>)
|
.add_request_handler(Server::forward_project_request::<proto::GetDocumentHighlights>)
|
||||||
.add_request_handler(Server::forward_project_request::<proto::GetProjectSymbols>)
|
.add_request_handler(Server::forward_project_request::<proto::GetProjectSymbols>)
|
||||||
.add_request_handler(Server::forward_project_request::<proto::OpenBufferForSymbol>)
|
.add_request_handler(Server::forward_project_request::<proto::OpenBufferForSymbol>)
|
||||||
.add_request_handler(Server::forward_project_request::<proto::OpenBuffer>)
|
.add_request_handler(Server::forward_project_request::<proto::OpenBufferById>)
|
||||||
|
.add_request_handler(Server::forward_project_request::<proto::OpenBufferByPath>)
|
||||||
.add_request_handler(Server::forward_project_request::<proto::GetCompletions>)
|
.add_request_handler(Server::forward_project_request::<proto::GetCompletions>)
|
||||||
.add_request_handler(
|
.add_request_handler(
|
||||||
Server::forward_project_request::<proto::ApplyCompletionAdditionalEdits>,
|
Server::forward_project_request::<proto::ApplyCompletionAdditionalEdits>,
|
||||||
|
@ -112,6 +113,9 @@ impl Server {
|
||||||
.add_request_handler(Server::join_channel)
|
.add_request_handler(Server::join_channel)
|
||||||
.add_message_handler(Server::leave_channel)
|
.add_message_handler(Server::leave_channel)
|
||||||
.add_request_handler(Server::send_channel_message)
|
.add_request_handler(Server::send_channel_message)
|
||||||
|
.add_request_handler(Server::follow)
|
||||||
|
.add_message_handler(Server::unfollow)
|
||||||
|
.add_message_handler(Server::update_followers)
|
||||||
.add_request_handler(Server::get_channel_messages);
|
.add_request_handler(Server::get_channel_messages);
|
||||||
|
|
||||||
Arc::new(server)
|
Arc::new(server)
|
||||||
|
@ -669,6 +673,72 @@ impl Server {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn follow(
|
||||||
|
self: Arc<Self>,
|
||||||
|
request: TypedEnvelope<proto::Follow>,
|
||||||
|
) -> tide::Result<proto::FollowResponse> {
|
||||||
|
let leader_id = ConnectionId(request.payload.leader_id);
|
||||||
|
let follower_id = request.sender_id;
|
||||||
|
if !self
|
||||||
|
.state()
|
||||||
|
.project_connection_ids(request.payload.project_id, follower_id)?
|
||||||
|
.contains(&leader_id)
|
||||||
|
{
|
||||||
|
Err(anyhow!("no such peer"))?;
|
||||||
|
}
|
||||||
|
let mut response = self
|
||||||
|
.peer
|
||||||
|
.forward_request(request.sender_id, leader_id, request.payload)
|
||||||
|
.await?;
|
||||||
|
response
|
||||||
|
.views
|
||||||
|
.retain(|view| view.leader_id != Some(follower_id.0));
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn unfollow(
|
||||||
|
self: Arc<Self>,
|
||||||
|
request: TypedEnvelope<proto::Unfollow>,
|
||||||
|
) -> tide::Result<()> {
|
||||||
|
let leader_id = ConnectionId(request.payload.leader_id);
|
||||||
|
if !self
|
||||||
|
.state()
|
||||||
|
.project_connection_ids(request.payload.project_id, request.sender_id)?
|
||||||
|
.contains(&leader_id)
|
||||||
|
{
|
||||||
|
Err(anyhow!("no such peer"))?;
|
||||||
|
}
|
||||||
|
self.peer
|
||||||
|
.forward_send(request.sender_id, leader_id, request.payload)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_followers(
|
||||||
|
self: Arc<Self>,
|
||||||
|
request: TypedEnvelope<proto::UpdateFollowers>,
|
||||||
|
) -> tide::Result<()> {
|
||||||
|
let connection_ids = self
|
||||||
|
.state()
|
||||||
|
.project_connection_ids(request.payload.project_id, request.sender_id)?;
|
||||||
|
let leader_id = request
|
||||||
|
.payload
|
||||||
|
.variant
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|variant| match variant {
|
||||||
|
proto::update_followers::Variant::CreateView(payload) => payload.leader_id,
|
||||||
|
proto::update_followers::Variant::UpdateView(payload) => payload.leader_id,
|
||||||
|
proto::update_followers::Variant::UpdateActiveView(payload) => payload.leader_id,
|
||||||
|
});
|
||||||
|
for follower_id in &request.payload.follower_ids {
|
||||||
|
let follower_id = ConnectionId(*follower_id);
|
||||||
|
if connection_ids.contains(&follower_id) && Some(follower_id.0) != leader_id {
|
||||||
|
self.peer
|
||||||
|
.forward_send(request.sender_id, follower_id, request.payload.clone())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn get_channels(
|
async fn get_channels(
|
||||||
self: Arc<Server>,
|
self: Arc<Server>,
|
||||||
request: TypedEnvelope<proto::GetChannels>,
|
request: TypedEnvelope<proto::GetChannels>,
|
||||||
|
@ -1016,7 +1086,7 @@ mod tests {
|
||||||
self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Input, Redo, Rename,
|
self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Input, Redo, Rename,
|
||||||
ToOffset, ToggleCodeActions, Undo,
|
ToOffset, ToggleCodeActions, Undo,
|
||||||
};
|
};
|
||||||
use gpui::{executor, ModelHandle, TestAppContext};
|
use gpui::{executor, geometry::vector::vec2f, ModelHandle, TestAppContext, ViewHandle};
|
||||||
use language::{
|
use language::{
|
||||||
tree_sitter_rust, Diagnostic, DiagnosticEntry, Language, LanguageConfig, LanguageRegistry,
|
tree_sitter_rust, Diagnostic, DiagnosticEntry, Language, LanguageConfig, LanguageRegistry,
|
||||||
LanguageServerConfig, OffsetRangeExt, Point, ToLspPosition,
|
LanguageServerConfig, OffsetRangeExt, Point, ToLspPosition,
|
||||||
|
@ -1028,7 +1098,7 @@ mod tests {
|
||||||
fs::{FakeFs, Fs as _},
|
fs::{FakeFs, Fs as _},
|
||||||
search::SearchQuery,
|
search::SearchQuery,
|
||||||
worktree::WorktreeHandle,
|
worktree::WorktreeHandle,
|
||||||
DiagnosticSummary, Project, ProjectPath,
|
DiagnosticSummary, Project, ProjectPath, WorktreeId,
|
||||||
};
|
};
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use rpc::PeerId;
|
use rpc::PeerId;
|
||||||
|
@ -1046,7 +1116,7 @@ mod tests {
|
||||||
},
|
},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
use workspace::{Settings, Workspace, WorkspaceParams};
|
use workspace::{Item, Settings, SplitDirection, Workspace, WorkspaceParams};
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[ctor::ctor]
|
#[ctor::ctor]
|
||||||
|
@ -3225,7 +3295,7 @@ mod tests {
|
||||||
let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(¶ms, cx));
|
let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(¶ms, cx));
|
||||||
let editor_b = workspace_b
|
let editor_b = workspace_b
|
||||||
.update(cx_b, |workspace, cx| {
|
.update(cx_b, |workspace, cx| {
|
||||||
workspace.open_path((worktree_id, "main.rs").into(), cx)
|
workspace.open_path((worktree_id, "main.rs"), cx)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
@ -3459,7 +3529,7 @@ mod tests {
|
||||||
let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(¶ms, cx));
|
let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(¶ms, cx));
|
||||||
let editor_b = workspace_b
|
let editor_b = workspace_b
|
||||||
.update(cx_b, |workspace, cx| {
|
.update(cx_b, |workspace, cx| {
|
||||||
workspace.open_path((worktree_id, "one.rs").into(), cx)
|
workspace.open_path((worktree_id, "one.rs"), cx)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
@ -4148,6 +4218,494 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test(iterations = 10)]
|
||||||
|
async fn test_following(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||||
|
cx_a.foreground().forbid_parking();
|
||||||
|
let fs = FakeFs::new(cx_a.background());
|
||||||
|
|
||||||
|
// 2 clients connect to a server.
|
||||||
|
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||||
|
let mut client_a = server.create_client(cx_a, "user_a").await;
|
||||||
|
let mut client_b = server.create_client(cx_b, "user_b").await;
|
||||||
|
cx_a.update(editor::init);
|
||||||
|
cx_b.update(editor::init);
|
||||||
|
|
||||||
|
// Client A shares a project.
|
||||||
|
fs.insert_tree(
|
||||||
|
"/a",
|
||||||
|
json!({
|
||||||
|
".zed.toml": r#"collaborators = ["user_b"]"#,
|
||||||
|
"1.txt": "one",
|
||||||
|
"2.txt": "two",
|
||||||
|
"3.txt": "three",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let (project_a, worktree_id) = client_a.build_local_project(fs.clone(), "/a", cx_a).await;
|
||||||
|
project_a
|
||||||
|
.update(cx_a, |project, cx| project.share(cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Client B joins the project.
|
||||||
|
let project_b = client_b
|
||||||
|
.build_remote_project(
|
||||||
|
project_a
|
||||||
|
.read_with(cx_a, |project, _| project.remote_id())
|
||||||
|
.unwrap(),
|
||||||
|
cx_b,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Client A opens some editors.
|
||||||
|
let workspace_a = client_a.build_workspace(&project_a, cx_a);
|
||||||
|
let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
|
||||||
|
let editor_a1 = workspace_a
|
||||||
|
.update(cx_a, |workspace, cx| {
|
||||||
|
workspace.open_path((worktree_id, "1.txt"), cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.downcast::<Editor>()
|
||||||
|
.unwrap();
|
||||||
|
let editor_a2 = workspace_a
|
||||||
|
.update(cx_a, |workspace, cx| {
|
||||||
|
workspace.open_path((worktree_id, "2.txt"), cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.downcast::<Editor>()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Client B opens an editor.
|
||||||
|
let workspace_b = client_b.build_workspace(&project_b, cx_b);
|
||||||
|
let editor_b1 = workspace_b
|
||||||
|
.update(cx_b, |workspace, cx| {
|
||||||
|
workspace.open_path((worktree_id, "1.txt"), cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.downcast::<Editor>()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let client_a_id = project_b.read_with(cx_b, |project, _| {
|
||||||
|
project.collaborators().values().next().unwrap().peer_id
|
||||||
|
});
|
||||||
|
let client_b_id = project_a.read_with(cx_a, |project, _| {
|
||||||
|
project.collaborators().values().next().unwrap().peer_id
|
||||||
|
});
|
||||||
|
|
||||||
|
// When client B starts following client A, all visible view states are replicated to client B.
|
||||||
|
editor_a1.update(cx_a, |editor, cx| editor.select_ranges([0..1], None, cx));
|
||||||
|
editor_a2.update(cx_a, |editor, cx| editor.select_ranges([2..3], None, cx));
|
||||||
|
workspace_b
|
||||||
|
.update(cx_b, |workspace, cx| {
|
||||||
|
workspace.toggle_follow(&client_a_id.into(), cx).unwrap()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
|
||||||
|
workspace
|
||||||
|
.active_item(cx)
|
||||||
|
.unwrap()
|
||||||
|
.downcast::<Editor>()
|
||||||
|
.unwrap()
|
||||||
|
});
|
||||||
|
assert!(cx_b.read(|cx| editor_b2.is_focused(cx)));
|
||||||
|
assert_eq!(
|
||||||
|
editor_b2.read_with(cx_b, |editor, cx| editor.project_path(cx)),
|
||||||
|
Some((worktree_id, "2.txt").into())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
editor_b2.read_with(cx_b, |editor, cx| editor.selected_ranges(cx)),
|
||||||
|
vec![2..3]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
editor_b1.read_with(cx_b, |editor, cx| editor.selected_ranges(cx)),
|
||||||
|
vec![0..1]
|
||||||
|
);
|
||||||
|
|
||||||
|
// When client A activates a different editor, client B does so as well.
|
||||||
|
workspace_a.update(cx_a, |workspace, cx| {
|
||||||
|
workspace.activate_item(&editor_a1, cx)
|
||||||
|
});
|
||||||
|
workspace_b
|
||||||
|
.condition(cx_b, |workspace, cx| {
|
||||||
|
workspace.active_item(cx).unwrap().id() == editor_b1.id()
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Changes to client A's editor are reflected on client B.
|
||||||
|
editor_a1.update(cx_a, |editor, cx| {
|
||||||
|
editor.select_ranges([1..1, 2..2], None, cx);
|
||||||
|
});
|
||||||
|
editor_b1
|
||||||
|
.condition(cx_b, |editor, cx| {
|
||||||
|
editor.selected_ranges(cx) == vec![1..1, 2..2]
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx));
|
||||||
|
editor_b1
|
||||||
|
.condition(cx_b, |editor, cx| editor.text(cx) == "TWO")
|
||||||
|
.await;
|
||||||
|
|
||||||
|
editor_a1.update(cx_a, |editor, cx| {
|
||||||
|
editor.select_ranges([3..3], None, cx);
|
||||||
|
editor.set_scroll_position(vec2f(0., 100.), cx);
|
||||||
|
});
|
||||||
|
editor_b1
|
||||||
|
.condition(cx_b, |editor, cx| editor.selected_ranges(cx) == vec![3..3])
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// After unfollowing, client B stops receiving updates from client A.
|
||||||
|
workspace_b.update(cx_b, |workspace, cx| {
|
||||||
|
workspace.unfollow(&workspace.active_pane().clone(), cx)
|
||||||
|
});
|
||||||
|
workspace_a.update(cx_a, |workspace, cx| {
|
||||||
|
workspace.activate_item(&editor_a2, cx)
|
||||||
|
});
|
||||||
|
cx_a.foreground().run_until_parked();
|
||||||
|
assert_eq!(
|
||||||
|
workspace_b.read_with(cx_b, |workspace, cx| workspace
|
||||||
|
.active_item(cx)
|
||||||
|
.unwrap()
|
||||||
|
.id()),
|
||||||
|
editor_b1.id()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Client A starts following client B.
|
||||||
|
workspace_a
|
||||||
|
.update(cx_a, |workspace, cx| {
|
||||||
|
workspace.toggle_follow(&client_b_id.into(), cx).unwrap()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
|
||||||
|
Some(client_b_id)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
workspace_a.read_with(cx_a, |workspace, cx| workspace
|
||||||
|
.active_item(cx)
|
||||||
|
.unwrap()
|
||||||
|
.id()),
|
||||||
|
editor_a1.id()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Following interrupts when client B disconnects.
|
||||||
|
client_b.disconnect(&cx_b.to_async()).unwrap();
|
||||||
|
cx_a.foreground().run_until_parked();
|
||||||
|
assert_eq!(
|
||||||
|
workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test(iterations = 10)]
|
||||||
|
async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||||
|
cx_a.foreground().forbid_parking();
|
||||||
|
let fs = FakeFs::new(cx_a.background());
|
||||||
|
|
||||||
|
// 2 clients connect to a server.
|
||||||
|
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||||
|
let mut client_a = server.create_client(cx_a, "user_a").await;
|
||||||
|
let mut client_b = server.create_client(cx_b, "user_b").await;
|
||||||
|
cx_a.update(editor::init);
|
||||||
|
cx_b.update(editor::init);
|
||||||
|
|
||||||
|
// Client A shares a project.
|
||||||
|
fs.insert_tree(
|
||||||
|
"/a",
|
||||||
|
json!({
|
||||||
|
".zed.toml": r#"collaborators = ["user_b"]"#,
|
||||||
|
"1.txt": "one",
|
||||||
|
"2.txt": "two",
|
||||||
|
"3.txt": "three",
|
||||||
|
"4.txt": "four",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let (project_a, worktree_id) = client_a.build_local_project(fs.clone(), "/a", cx_a).await;
|
||||||
|
project_a
|
||||||
|
.update(cx_a, |project, cx| project.share(cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Client B joins the project.
|
||||||
|
let project_b = client_b
|
||||||
|
.build_remote_project(
|
||||||
|
project_a
|
||||||
|
.read_with(cx_a, |project, _| project.remote_id())
|
||||||
|
.unwrap(),
|
||||||
|
cx_b,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Client A opens some editors.
|
||||||
|
let workspace_a = client_a.build_workspace(&project_a, cx_a);
|
||||||
|
let pane_a1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
|
||||||
|
let _editor_a1 = workspace_a
|
||||||
|
.update(cx_a, |workspace, cx| {
|
||||||
|
workspace.open_path((worktree_id, "1.txt"), cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.downcast::<Editor>()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Client B opens an editor.
|
||||||
|
let workspace_b = client_b.build_workspace(&project_b, cx_b);
|
||||||
|
let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
|
||||||
|
let _editor_b1 = workspace_b
|
||||||
|
.update(cx_b, |workspace, cx| {
|
||||||
|
workspace.open_path((worktree_id, "2.txt"), cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.downcast::<Editor>()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Clients A and B follow each other in split panes
|
||||||
|
workspace_a
|
||||||
|
.update(cx_a, |workspace, cx| {
|
||||||
|
workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
|
||||||
|
assert_ne!(*workspace.active_pane(), pane_a1);
|
||||||
|
let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap();
|
||||||
|
workspace
|
||||||
|
.toggle_follow(&workspace::ToggleFollow(leader_id), cx)
|
||||||
|
.unwrap()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
workspace_b
|
||||||
|
.update(cx_b, |workspace, cx| {
|
||||||
|
workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
|
||||||
|
assert_ne!(*workspace.active_pane(), pane_b1);
|
||||||
|
let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap();
|
||||||
|
workspace
|
||||||
|
.toggle_follow(&workspace::ToggleFollow(leader_id), cx)
|
||||||
|
.unwrap()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
workspace_a
|
||||||
|
.update(cx_a, |workspace, cx| {
|
||||||
|
workspace.activate_next_pane(cx);
|
||||||
|
assert_eq!(*workspace.active_pane(), pane_a1);
|
||||||
|
workspace.open_path((worktree_id, "3.txt"), cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
workspace_b
|
||||||
|
.update(cx_b, |workspace, cx| {
|
||||||
|
workspace.activate_next_pane(cx);
|
||||||
|
assert_eq!(*workspace.active_pane(), pane_b1);
|
||||||
|
workspace.open_path((worktree_id, "4.txt"), cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
cx_a.foreground().run_until_parked();
|
||||||
|
|
||||||
|
// Ensure leader updates don't change the active pane of followers
|
||||||
|
workspace_a.read_with(cx_a, |workspace, _| {
|
||||||
|
assert_eq!(*workspace.active_pane(), pane_a1);
|
||||||
|
});
|
||||||
|
workspace_b.read_with(cx_b, |workspace, _| {
|
||||||
|
assert_eq!(*workspace.active_pane(), pane_b1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ensure peers following each other doesn't cause an infinite loop.
|
||||||
|
assert_eq!(
|
||||||
|
workspace_a.read_with(cx_a, |workspace, cx| workspace
|
||||||
|
.active_item(cx)
|
||||||
|
.unwrap()
|
||||||
|
.project_path(cx)),
|
||||||
|
Some((worktree_id, "3.txt").into())
|
||||||
|
);
|
||||||
|
workspace_a.update(cx_a, |workspace, cx| {
|
||||||
|
assert_eq!(
|
||||||
|
workspace.active_item(cx).unwrap().project_path(cx),
|
||||||
|
Some((worktree_id, "3.txt").into())
|
||||||
|
);
|
||||||
|
workspace.activate_next_pane(cx);
|
||||||
|
assert_eq!(
|
||||||
|
workspace.active_item(cx).unwrap().project_path(cx),
|
||||||
|
Some((worktree_id, "4.txt").into())
|
||||||
|
);
|
||||||
|
});
|
||||||
|
workspace_b.update(cx_b, |workspace, cx| {
|
||||||
|
assert_eq!(
|
||||||
|
workspace.active_item(cx).unwrap().project_path(cx),
|
||||||
|
Some((worktree_id, "4.txt").into())
|
||||||
|
);
|
||||||
|
workspace.activate_next_pane(cx);
|
||||||
|
assert_eq!(
|
||||||
|
workspace.active_item(cx).unwrap().project_path(cx),
|
||||||
|
Some((worktree_id, "3.txt").into())
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test(iterations = 10)]
|
||||||
|
async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||||
|
cx_a.foreground().forbid_parking();
|
||||||
|
let fs = FakeFs::new(cx_a.background());
|
||||||
|
|
||||||
|
// 2 clients connect to a server.
|
||||||
|
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||||
|
let mut client_a = server.create_client(cx_a, "user_a").await;
|
||||||
|
let mut client_b = server.create_client(cx_b, "user_b").await;
|
||||||
|
cx_a.update(editor::init);
|
||||||
|
cx_b.update(editor::init);
|
||||||
|
|
||||||
|
// Client A shares a project.
|
||||||
|
fs.insert_tree(
|
||||||
|
"/a",
|
||||||
|
json!({
|
||||||
|
".zed.toml": r#"collaborators = ["user_b"]"#,
|
||||||
|
"1.txt": "one",
|
||||||
|
"2.txt": "two",
|
||||||
|
"3.txt": "three",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let (project_a, worktree_id) = client_a.build_local_project(fs.clone(), "/a", cx_a).await;
|
||||||
|
project_a
|
||||||
|
.update(cx_a, |project, cx| project.share(cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Client B joins the project.
|
||||||
|
let project_b = client_b
|
||||||
|
.build_remote_project(
|
||||||
|
project_a
|
||||||
|
.read_with(cx_a, |project, _| project.remote_id())
|
||||||
|
.unwrap(),
|
||||||
|
cx_b,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Client A opens some editors.
|
||||||
|
let workspace_a = client_a.build_workspace(&project_a, cx_a);
|
||||||
|
let _editor_a1 = workspace_a
|
||||||
|
.update(cx_a, |workspace, cx| {
|
||||||
|
workspace.open_path((worktree_id, "1.txt"), cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.downcast::<Editor>()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Client B starts following client A.
|
||||||
|
let workspace_b = client_b.build_workspace(&project_b, cx_b);
|
||||||
|
let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
|
||||||
|
let leader_id = project_b.read_with(cx_b, |project, _| {
|
||||||
|
project.collaborators().values().next().unwrap().peer_id
|
||||||
|
});
|
||||||
|
workspace_b
|
||||||
|
.update(cx_b, |workspace, cx| {
|
||||||
|
workspace.toggle_follow(&leader_id.into(), cx).unwrap()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||||
|
Some(leader_id)
|
||||||
|
);
|
||||||
|
let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
|
||||||
|
workspace
|
||||||
|
.active_item(cx)
|
||||||
|
.unwrap()
|
||||||
|
.downcast::<Editor>()
|
||||||
|
.unwrap()
|
||||||
|
});
|
||||||
|
|
||||||
|
// When client B moves, it automatically stops following client A.
|
||||||
|
editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx));
|
||||||
|
assert_eq!(
|
||||||
|
workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
|
||||||
|
workspace_b
|
||||||
|
.update(cx_b, |workspace, cx| {
|
||||||
|
workspace.toggle_follow(&leader_id.into(), cx).unwrap()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||||
|
Some(leader_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
// When client B edits, it automatically stops following client A.
|
||||||
|
editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx));
|
||||||
|
assert_eq!(
|
||||||
|
workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
|
||||||
|
workspace_b
|
||||||
|
.update(cx_b, |workspace, cx| {
|
||||||
|
workspace.toggle_follow(&leader_id.into(), cx).unwrap()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||||
|
Some(leader_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
// When client B scrolls, it automatically stops following client A.
|
||||||
|
editor_b2.update(cx_b, |editor, cx| {
|
||||||
|
editor.set_scroll_position(vec2f(0., 3.), cx)
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
|
||||||
|
workspace_b
|
||||||
|
.update(cx_b, |workspace, cx| {
|
||||||
|
workspace.toggle_follow(&leader_id.into(), cx).unwrap()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||||
|
Some(leader_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
// When client B activates a different pane, it continues following client A in the original pane.
|
||||||
|
workspace_b.update(cx_b, |workspace, cx| {
|
||||||
|
workspace.split_pane(pane_b.clone(), SplitDirection::Right, cx)
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||||
|
Some(leader_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
|
||||||
|
assert_eq!(
|
||||||
|
workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||||
|
Some(leader_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
// When client B activates a different item in the original pane, it automatically stops following client A.
|
||||||
|
workspace_b
|
||||||
|
.update(cx_b, |workspace, cx| {
|
||||||
|
workspace.open_path((worktree_id, "2.txt"), cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test(iterations = 100)]
|
#[gpui::test(iterations = 100)]
|
||||||
async fn test_random_collaboration(cx: &mut TestAppContext, rng: StdRng) {
|
async fn test_random_collaboration(cx: &mut TestAppContext, rng: StdRng) {
|
||||||
cx.foreground().forbid_parking();
|
cx.foreground().forbid_parking();
|
||||||
|
@ -4469,6 +5027,9 @@ mod tests {
|
||||||
|
|
||||||
Channel::init(&client);
|
Channel::init(&client);
|
||||||
Project::init(&client);
|
Project::init(&client);
|
||||||
|
cx.update(|cx| {
|
||||||
|
workspace::init(&client, cx);
|
||||||
|
});
|
||||||
|
|
||||||
let peer_id = PeerId(connection_id_rx.next().await.unwrap().0);
|
let peer_id = PeerId(connection_id_rx.next().await.unwrap().0);
|
||||||
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
|
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
|
||||||
|
@ -4477,6 +5038,7 @@ mod tests {
|
||||||
client,
|
client,
|
||||||
peer_id,
|
peer_id,
|
||||||
user_store,
|
user_store,
|
||||||
|
language_registry: Arc::new(LanguageRegistry::test()),
|
||||||
project: Default::default(),
|
project: Default::default(),
|
||||||
buffers: Default::default(),
|
buffers: Default::default(),
|
||||||
};
|
};
|
||||||
|
@ -4541,6 +5103,7 @@ mod tests {
|
||||||
client: Arc<Client>,
|
client: Arc<Client>,
|
||||||
pub peer_id: PeerId,
|
pub peer_id: PeerId,
|
||||||
pub user_store: ModelHandle<UserStore>,
|
pub user_store: ModelHandle<UserStore>,
|
||||||
|
language_registry: Arc<LanguageRegistry>,
|
||||||
project: Option<ModelHandle<Project>>,
|
project: Option<ModelHandle<Project>>,
|
||||||
buffers: HashSet<ModelHandle<language::Buffer>>,
|
buffers: HashSet<ModelHandle<language::Buffer>>,
|
||||||
}
|
}
|
||||||
|
@ -4568,6 +5131,80 @@ mod tests {
|
||||||
while authed_user.next().await.unwrap().is_none() {}
|
while authed_user.next().await.unwrap().is_none() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn build_local_project(
|
||||||
|
&mut self,
|
||||||
|
fs: Arc<FakeFs>,
|
||||||
|
root_path: impl AsRef<Path>,
|
||||||
|
cx: &mut TestAppContext,
|
||||||
|
) -> (ModelHandle<Project>, WorktreeId) {
|
||||||
|
let project = cx.update(|cx| {
|
||||||
|
Project::local(
|
||||||
|
self.client.clone(),
|
||||||
|
self.user_store.clone(),
|
||||||
|
self.language_registry.clone(),
|
||||||
|
fs,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
self.project = Some(project.clone());
|
||||||
|
let (worktree, _) = project
|
||||||
|
.update(cx, |p, cx| {
|
||||||
|
p.find_or_create_local_worktree(root_path, true, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
worktree
|
||||||
|
.read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
|
||||||
|
.await;
|
||||||
|
project
|
||||||
|
.update(cx, |project, _| project.next_remote_id())
|
||||||
|
.await;
|
||||||
|
(project, worktree.read_with(cx, |tree, _| tree.id()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn build_remote_project(
|
||||||
|
&mut self,
|
||||||
|
project_id: u64,
|
||||||
|
cx: &mut TestAppContext,
|
||||||
|
) -> ModelHandle<Project> {
|
||||||
|
let project = Project::remote(
|
||||||
|
project_id,
|
||||||
|
self.client.clone(),
|
||||||
|
self.user_store.clone(),
|
||||||
|
self.language_registry.clone(),
|
||||||
|
FakeFs::new(cx.background()),
|
||||||
|
&mut cx.to_async(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
self.project = Some(project.clone());
|
||||||
|
project
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_workspace(
|
||||||
|
&self,
|
||||||
|
project: &ModelHandle<Project>,
|
||||||
|
cx: &mut TestAppContext,
|
||||||
|
) -> ViewHandle<Workspace> {
|
||||||
|
let (window_id, _) = cx.add_window(|_| EmptyView);
|
||||||
|
cx.add_view(window_id, |cx| {
|
||||||
|
let fs = project.read(cx).fs().clone();
|
||||||
|
Workspace::new(
|
||||||
|
&WorkspaceParams {
|
||||||
|
fs,
|
||||||
|
project: project.clone(),
|
||||||
|
user_store: self.user_store.clone(),
|
||||||
|
languages: self.language_registry.clone(),
|
||||||
|
channel_list: cx.add_model(|cx| {
|
||||||
|
ChannelList::new(self.user_store.clone(), self.client.clone(), cx)
|
||||||
|
}),
|
||||||
|
client: self.client.clone(),
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn simulate_host(
|
fn simulate_host(
|
||||||
mut self,
|
mut self,
|
||||||
project: ModelHandle<Project>,
|
project: ModelHandle<Project>,
|
||||||
|
|
|
@ -18,6 +18,12 @@ pub struct Selection<T> {
|
||||||
pub goal: SelectionGoal,
|
pub goal: SelectionGoal,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for SelectionGoal {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T: Clone> Selection<T> {
|
impl<T: Clone> Selection<T> {
|
||||||
pub fn head(&self) -> T {
|
pub fn head(&self) -> T {
|
||||||
if self.reversed {
|
if self.reversed {
|
||||||
|
|
|
@ -35,6 +35,8 @@ pub struct Workspace {
|
||||||
pub tab: Tab,
|
pub tab: Tab,
|
||||||
pub active_tab: Tab,
|
pub active_tab: Tab,
|
||||||
pub pane_divider: Border,
|
pub pane_divider: Border,
|
||||||
|
pub leader_border_opacity: f32,
|
||||||
|
pub leader_border_width: f32,
|
||||||
pub left_sidebar: Sidebar,
|
pub left_sidebar: Sidebar,
|
||||||
pub right_sidebar: Sidebar,
|
pub right_sidebar: Sidebar,
|
||||||
pub status_bar: StatusBar,
|
pub status_bar: StatusBar,
|
||||||
|
|
|
@ -204,7 +204,7 @@ impl ThemeSelector {
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
match event {
|
match event {
|
||||||
editor::Event::Edited => {
|
editor::Event::Edited { .. } => {
|
||||||
self.update_matches(cx);
|
self.update_matches(cx);
|
||||||
self.select_if_matching(&cx.global::<Settings>().theme.name);
|
self.select_if_matching(&cx.global::<Settings>().theme.name);
|
||||||
self.show_selected_theme(cx);
|
self.show_selected_theme(cx);
|
||||||
|
|
|
@ -7,8 +7,8 @@ use gpui::{
|
||||||
geometry::{rect::RectF, vector::vec2f},
|
geometry::{rect::RectF, vector::vec2f},
|
||||||
keymap::Binding,
|
keymap::Binding,
|
||||||
platform::{CursorStyle, NavigationDirection},
|
platform::{CursorStyle, NavigationDirection},
|
||||||
AnyViewHandle, Entity, MutableAppContext, Quad, RenderContext, Task, View, ViewContext,
|
AnyViewHandle, AppContext, Entity, MutableAppContext, Quad, RenderContext, Task, View,
|
||||||
ViewHandle, WeakViewHandle,
|
ViewContext, ViewHandle, WeakViewHandle,
|
||||||
};
|
};
|
||||||
use project::{ProjectEntryId, ProjectPath};
|
use project::{ProjectEntryId, ProjectPath};
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -33,7 +33,7 @@ const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
|
||||||
|
|
||||||
pub fn init(cx: &mut MutableAppContext) {
|
pub fn init(cx: &mut MutableAppContext) {
|
||||||
cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
|
cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
|
||||||
pane.activate_item(action.0, cx);
|
pane.activate_item(action.0, true, cx);
|
||||||
});
|
});
|
||||||
cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
|
cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
|
||||||
pane.activate_prev_item(cx);
|
pane.activate_prev_item(cx);
|
||||||
|
@ -92,12 +92,13 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||||
|
|
||||||
pub enum Event {
|
pub enum Event {
|
||||||
Activate,
|
Activate,
|
||||||
|
ActivateItem { local: bool },
|
||||||
Remove,
|
Remove,
|
||||||
Split(SplitDirection),
|
Split(SplitDirection),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Pane {
|
pub struct Pane {
|
||||||
items: Vec<(Option<ProjectEntryId>, Box<dyn ItemHandle>)>,
|
items: Vec<Box<dyn ItemHandle>>,
|
||||||
active_item_index: usize,
|
active_item_index: usize,
|
||||||
nav_history: Rc<RefCell<NavHistory>>,
|
nav_history: Rc<RefCell<NavHistory>>,
|
||||||
toolbars: HashMap<TypeId, Box<dyn ToolbarHandle>>,
|
toolbars: HashMap<TypeId, Box<dyn ToolbarHandle>>,
|
||||||
|
@ -256,9 +257,19 @@ impl Pane {
|
||||||
let task = task.await;
|
let task = task.await;
|
||||||
if let Some(pane) = pane.upgrade(&cx) {
|
if let Some(pane) = pane.upgrade(&cx) {
|
||||||
if let Some((project_entry_id, build_item)) = task.log_err() {
|
if let Some((project_entry_id, build_item)) = task.log_err() {
|
||||||
pane.update(&mut cx, |pane, cx| {
|
pane.update(&mut cx, |pane, _| {
|
||||||
pane.nav_history.borrow_mut().set_mode(mode);
|
pane.nav_history.borrow_mut().set_mode(mode);
|
||||||
let item = pane.open_item(project_entry_id, cx, build_item);
|
});
|
||||||
|
let item = workspace.update(&mut cx, |workspace, cx| {
|
||||||
|
Self::open_item(
|
||||||
|
workspace,
|
||||||
|
pane.clone(),
|
||||||
|
project_entry_id,
|
||||||
|
cx,
|
||||||
|
build_item,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
pane.update(&mut cx, |pane, cx| {
|
||||||
pane.nav_history
|
pane.nav_history
|
||||||
.borrow_mut()
|
.borrow_mut()
|
||||||
.set_mode(NavigationMode::Normal);
|
.set_mode(NavigationMode::Normal);
|
||||||
|
@ -280,63 +291,91 @@ impl Pane {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn open_item(
|
pub(crate) fn open_item(
|
||||||
&mut self,
|
workspace: &mut Workspace,
|
||||||
|
pane: ViewHandle<Pane>,
|
||||||
project_entry_id: ProjectEntryId,
|
project_entry_id: ProjectEntryId,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Workspace>,
|
||||||
build_item: impl FnOnce(&mut MutableAppContext) -> Box<dyn ItemHandle>,
|
build_item: impl FnOnce(&mut MutableAppContext) -> Box<dyn ItemHandle>,
|
||||||
) -> Box<dyn ItemHandle> {
|
) -> Box<dyn ItemHandle> {
|
||||||
for (ix, (existing_entry_id, item)) in self.items.iter().enumerate() {
|
let existing_item = pane.update(cx, |pane, cx| {
|
||||||
if *existing_entry_id == Some(project_entry_id) {
|
for (ix, item) in pane.items.iter().enumerate() {
|
||||||
|
if item.project_entry_id(cx) == Some(project_entry_id) {
|
||||||
let item = item.boxed_clone();
|
let item = item.boxed_clone();
|
||||||
self.activate_item(ix, cx);
|
pane.activate_item(ix, true, cx);
|
||||||
return item;
|
return Some(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
None
|
||||||
|
});
|
||||||
|
if let Some(existing_item) = existing_item {
|
||||||
|
existing_item
|
||||||
|
} else {
|
||||||
let item = build_item(cx);
|
let item = build_item(cx);
|
||||||
self.add_item(Some(project_entry_id), item.boxed_clone(), cx);
|
Self::add_item(workspace, pane, item.boxed_clone(), true, cx);
|
||||||
item
|
item
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn add_item(
|
pub(crate) fn add_item(
|
||||||
&mut self,
|
workspace: &mut Workspace,
|
||||||
project_entry_id: Option<ProjectEntryId>,
|
pane: ViewHandle<Pane>,
|
||||||
mut item: Box<dyn ItemHandle>,
|
item: Box<dyn ItemHandle>,
|
||||||
cx: &mut ViewContext<Self>,
|
local: bool,
|
||||||
|
cx: &mut ViewContext<Workspace>,
|
||||||
) {
|
) {
|
||||||
item.set_nav_history(self.nav_history.clone(), cx);
|
// Prevent adding the same item to the pane more than once.
|
||||||
item.added_to_pane(cx);
|
if let Some(item_ix) = pane.read(cx).items.iter().position(|i| i.id() == item.id()) {
|
||||||
let item_idx = cmp::min(self.active_item_index + 1, self.items.len());
|
pane.update(cx, |pane, cx| pane.activate_item(item_ix, local, cx));
|
||||||
self.items.insert(item_idx, (project_entry_id, item));
|
return;
|
||||||
self.activate_item(item_idx, cx);
|
}
|
||||||
|
|
||||||
|
item.set_nav_history(pane.read(cx).nav_history.clone(), cx);
|
||||||
|
item.added_to_pane(workspace, pane.clone(), cx);
|
||||||
|
pane.update(cx, |pane, cx| {
|
||||||
|
let item_idx = cmp::min(pane.active_item_index + 1, pane.items.len());
|
||||||
|
pane.items.insert(item_idx, item);
|
||||||
|
pane.activate_item(item_idx, local, cx);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn items(&self) -> impl Iterator<Item = &Box<dyn ItemHandle>> {
|
pub fn items(&self) -> impl Iterator<Item = &Box<dyn ItemHandle>> {
|
||||||
self.items.iter().map(|(_, view)| view)
|
self.items.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn items_of_type<'a, T: View>(&'a self) -> impl 'a + Iterator<Item = ViewHandle<T>> {
|
||||||
|
self.items
|
||||||
|
.iter()
|
||||||
|
.filter_map(|item| item.to_any().downcast())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
|
pub fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
|
||||||
self.items
|
self.items.get(self.active_item_index).cloned()
|
||||||
.get(self.active_item_index)
|
|
||||||
.map(|(_, view)| view.clone())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn project_entry_id_for_item(&self, item: &dyn ItemHandle) -> Option<ProjectEntryId> {
|
pub fn project_entry_id_for_item(
|
||||||
self.items.iter().find_map(|(entry_id, existing)| {
|
&self,
|
||||||
|
item: &dyn ItemHandle,
|
||||||
|
cx: &AppContext,
|
||||||
|
) -> Option<ProjectEntryId> {
|
||||||
|
self.items.iter().find_map(|existing| {
|
||||||
if existing.id() == item.id() {
|
if existing.id() == item.id() {
|
||||||
*entry_id
|
existing.project_entry_id(cx)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn item_for_entry(&self, entry_id: ProjectEntryId) -> Option<Box<dyn ItemHandle>> {
|
pub fn item_for_entry(
|
||||||
self.items.iter().find_map(|(id, view)| {
|
&self,
|
||||||
if *id == Some(entry_id) {
|
entry_id: ProjectEntryId,
|
||||||
Some(view.boxed_clone())
|
cx: &AppContext,
|
||||||
|
) -> Option<Box<dyn ItemHandle>> {
|
||||||
|
self.items.iter().find_map(|item| {
|
||||||
|
if item.project_entry_id(cx) == Some(entry_id) {
|
||||||
|
Some(item.boxed_clone())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
@ -344,20 +383,23 @@ impl Pane {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option<usize> {
|
pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option<usize> {
|
||||||
self.items.iter().position(|(_, i)| i.id() == item.id())
|
self.items.iter().position(|i| i.id() == item.id())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn activate_item(&mut self, index: usize, cx: &mut ViewContext<Self>) {
|
pub fn activate_item(&mut self, index: usize, local: bool, cx: &mut ViewContext<Self>) {
|
||||||
if index < self.items.len() {
|
if index < self.items.len() {
|
||||||
let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
|
let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
|
||||||
if prev_active_item_ix != self.active_item_index
|
if prev_active_item_ix != self.active_item_index
|
||||||
&& prev_active_item_ix < self.items.len()
|
&& prev_active_item_ix < self.items.len()
|
||||||
{
|
{
|
||||||
self.items[prev_active_item_ix].1.deactivated(cx);
|
self.items[prev_active_item_ix].deactivated(cx);
|
||||||
|
cx.emit(Event::ActivateItem { local });
|
||||||
}
|
}
|
||||||
self.update_active_toolbar(cx);
|
self.update_active_toolbar(cx);
|
||||||
|
if local {
|
||||||
self.focus_active_item(cx);
|
self.focus_active_item(cx);
|
||||||
self.activate(cx);
|
self.activate(cx);
|
||||||
|
}
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -369,7 +411,7 @@ impl Pane {
|
||||||
} else if self.items.len() > 0 {
|
} else if self.items.len() > 0 {
|
||||||
index = self.items.len() - 1;
|
index = self.items.len() - 1;
|
||||||
}
|
}
|
||||||
self.activate_item(index, cx);
|
self.activate_item(index, true, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn activate_next_item(&mut self, cx: &mut ViewContext<Self>) {
|
pub fn activate_next_item(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
@ -379,18 +421,18 @@ impl Pane {
|
||||||
} else {
|
} else {
|
||||||
index = 0;
|
index = 0;
|
||||||
}
|
}
|
||||||
self.activate_item(index, cx);
|
self.activate_item(index, true, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn close_active_item(&mut self, cx: &mut ViewContext<Self>) {
|
pub fn close_active_item(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
if !self.items.is_empty() {
|
if !self.items.is_empty() {
|
||||||
self.close_item(self.items[self.active_item_index].1.id(), cx)
|
self.close_item(self.items[self.active_item_index].id(), cx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn close_inactive_items(&mut self, cx: &mut ViewContext<Self>) {
|
pub fn close_inactive_items(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
if !self.items.is_empty() {
|
if !self.items.is_empty() {
|
||||||
let active_item_id = self.items[self.active_item_index].1.id();
|
let active_item_id = self.items[self.active_item_index].id();
|
||||||
self.close_items(cx, |id| id != active_item_id);
|
self.close_items(cx, |id| id != active_item_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -406,7 +448,7 @@ impl Pane {
|
||||||
) {
|
) {
|
||||||
let mut item_ix = 0;
|
let mut item_ix = 0;
|
||||||
let mut new_active_item_index = self.active_item_index;
|
let mut new_active_item_index = self.active_item_index;
|
||||||
self.items.retain(|(_, item)| {
|
self.items.retain(|item| {
|
||||||
if should_close(item.id()) {
|
if should_close(item.id()) {
|
||||||
if item_ix == self.active_item_index {
|
if item_ix == self.active_item_index {
|
||||||
item.deactivated(cx);
|
item.deactivated(cx);
|
||||||
|
@ -443,7 +485,7 @@ impl Pane {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
|
pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
if let Some(active_item) = self.active_item() {
|
if let Some(active_item) = self.active_item() {
|
||||||
cx.focus(active_item);
|
cx.focus(active_item);
|
||||||
}
|
}
|
||||||
|
@ -505,7 +547,7 @@ impl Pane {
|
||||||
fn update_active_toolbar(&mut self, cx: &mut ViewContext<Self>) {
|
fn update_active_toolbar(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
let active_item = self.items.get(self.active_item_index);
|
let active_item = self.items.get(self.active_item_index);
|
||||||
for (toolbar_type_id, toolbar) in &self.toolbars {
|
for (toolbar_type_id, toolbar) in &self.toolbars {
|
||||||
let visible = toolbar.active_item_changed(active_item.map(|i| i.1.clone()), cx);
|
let visible = toolbar.active_item_changed(active_item.cloned(), cx);
|
||||||
if Some(*toolbar_type_id) == self.active_toolbar_type {
|
if Some(*toolbar_type_id) == self.active_toolbar_type {
|
||||||
self.active_toolbar_visible = visible;
|
self.active_toolbar_visible = visible;
|
||||||
}
|
}
|
||||||
|
@ -518,7 +560,7 @@ impl Pane {
|
||||||
enum Tabs {}
|
enum Tabs {}
|
||||||
let tabs = MouseEventHandler::new::<Tabs, _, _>(0, cx, |mouse_state, cx| {
|
let tabs = MouseEventHandler::new::<Tabs, _, _>(0, cx, |mouse_state, cx| {
|
||||||
let mut row = Flex::row();
|
let mut row = Flex::row();
|
||||||
for (ix, (_, item)) in self.items.iter().enumerate() {
|
for (ix, item) in self.items.iter().enumerate() {
|
||||||
let is_active = ix == self.active_item_index;
|
let is_active = ix == self.active_item_index;
|
||||||
|
|
||||||
row.add_child({
|
row.add_child({
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
|
use crate::{FollowerStatesByLeader, Pane};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use gpui::{elements::*, Axis, ViewHandle};
|
use client::PeerId;
|
||||||
|
use collections::HashMap;
|
||||||
|
use gpui::{elements::*, Axis, Border, ViewHandle};
|
||||||
|
use project::Collaborator;
|
||||||
use theme::Theme;
|
use theme::Theme;
|
||||||
|
|
||||||
use crate::Pane;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub struct PaneGroup {
|
pub struct PaneGroup {
|
||||||
root: Member,
|
root: Member,
|
||||||
|
@ -47,8 +49,19 @@ impl PaneGroup {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render<'a>(&self, theme: &Theme) -> ElementBox {
|
pub(crate) fn render<'a>(
|
||||||
self.root.render(theme)
|
&self,
|
||||||
|
theme: &Theme,
|
||||||
|
follower_states: &FollowerStatesByLeader,
|
||||||
|
collaborators: &HashMap<PeerId, Collaborator>,
|
||||||
|
) -> ElementBox {
|
||||||
|
self.root.render(theme, follower_states, collaborators)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn panes(&self) -> Vec<&ViewHandle<Pane>> {
|
||||||
|
let mut panes = Vec::new();
|
||||||
|
self.root.collect_panes(&mut panes);
|
||||||
|
panes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,10 +93,50 @@ impl Member {
|
||||||
Member::Axis(PaneAxis { axis, members })
|
Member::Axis(PaneAxis { axis, members })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render(&self, theme: &Theme) -> ElementBox {
|
pub fn render(
|
||||||
|
&self,
|
||||||
|
theme: &Theme,
|
||||||
|
follower_states: &FollowerStatesByLeader,
|
||||||
|
collaborators: &HashMap<PeerId, Collaborator>,
|
||||||
|
) -> ElementBox {
|
||||||
match self {
|
match self {
|
||||||
Member::Pane(pane) => ChildView::new(pane).boxed(),
|
Member::Pane(pane) => {
|
||||||
Member::Axis(axis) => axis.render(theme),
|
let mut border = Border::default();
|
||||||
|
let leader = follower_states
|
||||||
|
.iter()
|
||||||
|
.find_map(|(leader_id, follower_states)| {
|
||||||
|
if follower_states.contains_key(pane) {
|
||||||
|
Some(leader_id)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.and_then(|leader_id| collaborators.get(leader_id));
|
||||||
|
if let Some(leader) = leader {
|
||||||
|
let leader_color = theme
|
||||||
|
.editor
|
||||||
|
.replica_selection_style(leader.replica_id)
|
||||||
|
.cursor;
|
||||||
|
border = Border::all(theme.workspace.leader_border_width, leader_color);
|
||||||
|
border
|
||||||
|
.color
|
||||||
|
.fade_out(1. - theme.workspace.leader_border_opacity);
|
||||||
|
border.overlay = true;
|
||||||
|
}
|
||||||
|
ChildView::new(pane).contained().with_border(border).boxed()
|
||||||
|
}
|
||||||
|
Member::Axis(axis) => axis.render(theme, follower_states, collaborators),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_panes<'a>(&'a self, panes: &mut Vec<&'a ViewHandle<Pane>>) {
|
||||||
|
match self {
|
||||||
|
Member::Axis(axis) => {
|
||||||
|
for member in &axis.members {
|
||||||
|
member.collect_panes(panes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Member::Pane(pane) => panes.push(pane),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -172,11 +225,16 @@ impl PaneAxis {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render<'a>(&self, theme: &Theme) -> ElementBox {
|
fn render(
|
||||||
|
&self,
|
||||||
|
theme: &Theme,
|
||||||
|
follower_state: &FollowerStatesByLeader,
|
||||||
|
collaborators: &HashMap<PeerId, Collaborator>,
|
||||||
|
) -> ElementBox {
|
||||||
let last_member_ix = self.members.len() - 1;
|
let last_member_ix = self.members.len() - 1;
|
||||||
Flex::new(self.axis)
|
Flex::new(self.axis)
|
||||||
.with_children(self.members.iter().enumerate().map(|(ix, member)| {
|
.with_children(self.members.iter().enumerate().map(|(ix, member)| {
|
||||||
let mut member = member.render(theme);
|
let mut member = member.render(theme, follower_state, collaborators);
|
||||||
if ix < last_member_ix {
|
if ix < last_member_ix {
|
||||||
let mut border = theme.workspace.pane_divider;
|
let mut border = theme.workspace.pane_divider;
|
||||||
border.left = false;
|
border.left = false;
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -64,6 +64,7 @@ crossbeam-channel = "0.5.0"
|
||||||
ctor = "0.1.20"
|
ctor = "0.1.20"
|
||||||
dirs = "3.0"
|
dirs = "3.0"
|
||||||
easy-parallel = "3.1.0"
|
easy-parallel = "3.1.0"
|
||||||
|
env_logger = "0.8"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
http-auth-basic = "0.1.3"
|
http-auth-basic = "0.1.3"
|
||||||
ignore = "0.4"
|
ignore = "0.4"
|
||||||
|
|
|
@ -4,6 +4,8 @@ base = { family = "Zed Sans", size = 14 }
|
||||||
[workspace]
|
[workspace]
|
||||||
background = "$surface.0"
|
background = "$surface.0"
|
||||||
pane_divider = { width = 1, color = "$border.0" }
|
pane_divider = { width = 1, color = "$border.0" }
|
||||||
|
leader_border_opacity = 0.7
|
||||||
|
leader_border_width = 2.0
|
||||||
|
|
||||||
[workspace.titlebar]
|
[workspace.titlebar]
|
||||||
height = 32
|
height = 32
|
||||||
|
|
|
@ -9,7 +9,6 @@ use gpui::{App, AssetSource, Task};
|
||||||
use log::LevelFilter;
|
use log::LevelFilter;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use project::Fs;
|
use project::Fs;
|
||||||
use simplelog::SimpleLogger;
|
|
||||||
use smol::process::Command;
|
use smol::process::Command;
|
||||||
use std::{env, fs, path::PathBuf, sync::Arc};
|
use std::{env, fs, path::PathBuf, sync::Arc};
|
||||||
use theme::{ThemeRegistry, DEFAULT_THEME_NAME};
|
use theme::{ThemeRegistry, DEFAULT_THEME_NAME};
|
||||||
|
@ -69,7 +68,7 @@ fn main() {
|
||||||
project::Project::init(&client);
|
project::Project::init(&client);
|
||||||
client::Channel::init(&client);
|
client::Channel::init(&client);
|
||||||
client::init(client.clone(), cx);
|
client::init(client.clone(), cx);
|
||||||
workspace::init(cx);
|
workspace::init(&client, cx);
|
||||||
editor::init(cx);
|
editor::init(cx);
|
||||||
go_to_line::init(cx);
|
go_to_line::init(cx);
|
||||||
file_finder::init(cx);
|
file_finder::init(cx);
|
||||||
|
@ -142,11 +141,10 @@ fn main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_logger() {
|
fn init_logger() {
|
||||||
let level = LevelFilter::Info;
|
|
||||||
|
|
||||||
if stdout_is_a_pty() {
|
if stdout_is_a_pty() {
|
||||||
SimpleLogger::init(level, Default::default()).expect("could not initialize logger");
|
env_logger::init();
|
||||||
} else {
|
} else {
|
||||||
|
let level = LevelFilter::Info;
|
||||||
let log_dir_path = dirs::home_dir()
|
let log_dir_path = dirs::home_dir()
|
||||||
.expect("could not locate home directory for logging")
|
.expect("could not locate home directory for logging")
|
||||||
.join("Library/Logs/");
|
.join("Library/Logs/");
|
||||||
|
|
|
@ -252,7 +252,7 @@ mod tests {
|
||||||
async fn test_new_empty_workspace(cx: &mut TestAppContext) {
|
async fn test_new_empty_workspace(cx: &mut TestAppContext) {
|
||||||
let app_state = cx.update(test_app_state);
|
let app_state = cx.update(test_app_state);
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
workspace::init(cx);
|
workspace::init(&app_state.client, cx);
|
||||||
});
|
});
|
||||||
cx.dispatch_global_action(workspace::OpenNew(app_state.clone()));
|
cx.dispatch_global_action(workspace::OpenNew(app_state.clone()));
|
||||||
let window_id = *cx.window_ids().first().unwrap();
|
let window_id = *cx.window_ids().first().unwrap();
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue