diff --git a/Cargo.lock b/Cargo.lock index 28c433c43f85e7df2de479d96eaf99b3d1a453d0..9c3692396461c298a0c73160fb177ee8f8c45850 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1617,6 +1617,7 @@ dependencies = [ "collections", "ctor", "env_logger", + "futures", "fuzzy", "gpui", "itertools", @@ -1629,6 +1630,7 @@ dependencies = [ "postage", "project", "rand 0.8.3", + "rpc", "serde", "smallvec", "smol", @@ -1782,7 +1784,9 @@ dependencies = [ name = "file_finder" version = "0.1.0" dependencies = [ + "ctor", "editor", + "env_logger", "fuzzy", "gpui", "postage", @@ -2524,6 +2528,15 @@ dependencies = [ "hashbrown 0.9.1", ] +[[package]] +name = "indoc" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7906a9fababaeacb774f72410e497a1d18de916322e33797bb2cd29baa23c9e" +dependencies = [ + "unindent", +] + [[package]] name = "infer" version = "0.2.3" @@ -5550,9 +5563,9 @@ checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" [[package]] name = "unindent" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f14ee04d9415b52b3aeab06258a3f07093182b88ba0f9b8d203f211a7a7d41c7" +checksum = "514672a55d7380da379785a4d70ca8386c8883ff7eaae877be4d2081cebe73d8" [[package]] name = "universal-hash" @@ -5670,6 +5683,21 @@ version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" +[[package]] +name = "vim" +version = "0.1.0" +dependencies = [ + "collections", + "editor", + "gpui", + "indoc", + "language", + "log", + "project", + "util", + "workspace", +] + [[package]] name = "waker-fn" version = "1.1.0" @@ -5929,7 +5957,7 @@ checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" [[package]] name = "zed" -version = "0.21.0" +version = "0.23.0" dependencies = [ "anyhow", "async-compression", @@ -6000,6 +6028,7 @@ dependencies = [ "unindent", "url", "util", + "vim", "workspace", ] diff --git a/crates/chat_panel/src/chat_panel.rs b/crates/chat_panel/src/chat_panel.rs index ceeddc599a0d3d90b0328e2d6d94148b98e42002..452f041c7b040c58879caccc19859c379a152042 100644 --- a/crates/chat_panel/src/chat_panel.rs +++ b/crates/chat_panel/src/chat_panel.rs @@ -64,13 +64,13 @@ impl ChatPanel { ix, item_type, is_hovered, - &cx.app_state::().theme.chat_panel.channel_select, + &cx.global::().theme.chat_panel.channel_select, cx, ) } }) .with_style(move |cx| { - let theme = &cx.app_state::().theme.chat_panel.channel_select; + let theme = &cx.global::().theme.chat_panel.channel_select; SelectStyle { header: theme.header.container.clone(), menu: theme.menu.clone(), @@ -200,7 +200,7 @@ impl ChatPanel { } fn render_channel(&self, cx: &mut RenderContext) -> ElementBox { - let theme = &cx.app_state::().theme; + let theme = &cx.global::().theme; Flex::column() .with_child( Container::new(ChildView::new(&self.channel_select).boxed()) @@ -224,7 +224,7 @@ impl ChatPanel { fn render_message(&self, message: &ChannelMessage, cx: &AppContext) -> ElementBox { let now = OffsetDateTime::now_utc(); - let settings = cx.app_state::(); + let settings = cx.global::(); let theme = if message.is_pending() { &settings.theme.chat_panel.pending_message } else { @@ -267,7 +267,7 @@ impl ChatPanel { } fn render_input_box(&self, cx: &AppContext) -> ElementBox { - let theme = &cx.app_state::().theme; + let theme = &cx.global::().theme; Container::new(ChildView::new(&self.input_editor).boxed()) .with_style(theme.chat_panel.input_editor.container) .boxed() @@ -304,7 +304,7 @@ impl ChatPanel { } fn render_sign_in_prompt(&self, cx: &mut RenderContext) -> ElementBox { - let theme = cx.app_state::().theme.clone(); + let theme = cx.global::().theme.clone(); let rpc = self.rpc.clone(); let this = cx.handle(); @@ -327,7 +327,12 @@ impl ChatPanel { let rpc = rpc.clone(); let this = this.clone(); cx.spawn(|mut cx| async move { - if rpc.authenticate_and_connect(&cx).log_err().await.is_some() { + if rpc + .authenticate_and_connect(true, &cx) + .log_err() + .await + .is_some() + { cx.update(|cx| { if let Some(this) = this.upgrade(cx) { if this.is_focused(cx) { @@ -385,7 +390,7 @@ impl View for ChatPanel { } else { self.render_sign_in_prompt(cx) }; - let theme = &cx.app_state::().theme; + let theme = &cx.global::().theme; ConstrainedBox::new( Container::new(element) .with_style(theme.chat_panel.container) diff --git a/crates/client/src/channel.rs b/crates/client/src/channel.rs index 18a0e156db6b881cf0c6a0a8e779c573d1c34f40..ac235dc19e0eb7d4f30d2804b277dcb672fe9a1a 100644 --- a/crates/client/src/channel.rs +++ b/crates/client/src/channel.rs @@ -181,7 +181,7 @@ impl Entity for Channel { impl Channel { pub fn init(rpc: &Arc) { - rpc.add_entity_message_handler(Self::handle_message_sent); + rpc.add_model_message_handler(Self::handle_message_sent); } pub fn new( diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 59110f73c643cfac93730a0ef5e1900d896397eb..1bae6cd49e5eb9f5e24b86f6165b7a647c6a30e8 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -13,8 +13,8 @@ use async_tungstenite::tungstenite::{ }; use futures::{future::LocalBoxFuture, FutureExt, StreamExt}; use gpui::{ - action, AnyModelHandle, AnyWeakModelHandle, AsyncAppContext, Entity, ModelContext, ModelHandle, - MutableAppContext, Task, + action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, AsyncAppContext, + Entity, ModelContext, ModelHandle, MutableAppContext, Task, View, ViewContext, ViewHandle, }; use http::HttpClient; use lazy_static::lazy_static; @@ -45,7 +45,7 @@ pub use user::*; lazy_static! { static ref ZED_SERVER_URL: String = std::env::var("ZED_SERVER_URL").unwrap_or("https://zed.dev".to_string()); - static ref IMPERSONATE_LOGIN: Option = std::env::var("ZED_IMPERSONATE") + pub static ref IMPERSONATE_LOGIN: Option = std::env::var("ZED_IMPERSONATE") .ok() .and_then(|s| if s.is_empty() { None } else { Some(s) }); } @@ -55,7 +55,7 @@ action!(Authenticate); pub fn init(rpc: Arc, cx: &mut MutableAppContext) { cx.add_global_action(move |_: &Authenticate, cx| { let rpc = rpc.clone(); - cx.spawn(|cx| async move { rpc.authenticate_and_connect(&cx).log_err().await }) + cx.spawn(|cx| async move { rpc.authenticate_and_connect(true, &cx).log_err().await }) .detach(); }); } @@ -136,26 +136,37 @@ impl Status { struct ClientState { credentials: Option, status: (watch::Sender, watch::Receiver), - entity_id_extractors: HashMap u64>>, + entity_id_extractors: HashMap u64>, _reconnect_task: Option>, 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, - model_types_by_message_type: HashMap, + entity_types_by_message_type: HashMap, message_handlers: HashMap< TypeId, Arc< dyn Send + Sync + Fn( - AnyModelHandle, + AnyEntityHandle, Box, + &Arc, AsyncAppContext, ) -> LocalBoxFuture<'static, Result<()>>, >, >, } +enum AnyWeakEntityHandle { + Model(AnyWeakModelHandle), + View(AnyWeakViewHandle), +} + +enum AnyEntityHandle { + Model(AnyModelHandle), + View(AnyViewHandle), +} + #[derive(Clone, Debug)] pub struct Credentials { pub user_id: u64, @@ -171,8 +182,8 @@ impl Default for ClientState { _reconnect_task: None, reconnect_interval: Duration::from_secs(5), models_by_message_type: Default::default(), - models_by_entity_type_and_remote_id: Default::default(), - model_types_by_message_type: Default::default(), + entities_by_type_and_remote_id: Default::default(), + entity_types_by_message_type: Default::default(), message_handlers: Default::default(), } } @@ -195,13 +206,13 @@ impl Drop for Subscription { Subscription::Entity { client, id } => { if let Some(client) = client.upgrade() { 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 } => { if let Some(client) = client.upgrade() { 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); } } @@ -239,7 +250,7 @@ impl Client { state._reconnect_task.take(); state.message_handlers.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(); self.peer.reset(); } @@ -291,7 +302,7 @@ impl Client { state._reconnect_task = Some(cx.spawn(|cx| async move { let mut rng = StdRng::from_entropy(); let mut delay = Duration::from_millis(100); - while let Err(error) = this.authenticate_and_connect(&cx).await { + while let Err(error) = this.authenticate_and_connect(true, &cx).await { log::error!("failed to connect {}", error); this.set_status( Status::ReconnectionError { @@ -313,17 +324,32 @@ impl Client { } } + pub fn add_view_for_remote_entity( + self: &Arc, + remote_id: u64, + cx: &mut ViewContext, + ) -> Subscription { + let id = (TypeId::of::(), 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( self: &Arc, remote_id: u64, cx: &mut ModelContext, ) -> Subscription { - let handle = AnyModelHandle::from(cx.handle()); - let mut state = self.state.write(); let id = (TypeId::of::(), remote_id); - state - .models_by_entity_type_and_remote_id - .insert(id, handle.downgrade()); + self.state + .write() + .entities_by_type_and_remote_id + .insert(id, AnyWeakEntityHandle::Model(cx.weak_handle().into())); Subscription::Entity { client: Arc::downgrade(self), id, @@ -346,7 +372,6 @@ impl Client { { let message_type_id = TypeId::of::(); - let client = Arc::downgrade(self); let mut state = self.state.write(); state .models_by_message_type @@ -354,14 +379,15 @@ impl Client { let prev_handler = state.message_handlers.insert( 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::().unwrap(); let envelope = envelope.into_any().downcast::>().unwrap(); - if let Some(client) = client.upgrade() { - handler(model, *envelope, client.clone(), cx).boxed_local() - } else { - async move { Ok(()) }.boxed_local() - } + handler(model, *envelope, client.clone(), cx).boxed_local() }), ); if prev_handler.is_some() { @@ -374,7 +400,26 @@ impl Client { } } - pub fn add_entity_message_handler(self: &Arc, handler: H) + pub fn add_view_message_handler(self: &Arc, handler: H) + where + M: EntityMessage, + E: View, + H: 'static + + Send + + Sync + + Fn(ViewHandle, TypedEnvelope, Arc, AsyncAppContext) -> F, + F: 'static + Future>, + { + self.add_entity_message_handler::(move |handle, message, client, cx| { + if let AnyEntityHandle::View(handle) = handle { + handler(handle.downcast::().unwrap(), message, client, cx) + } else { + unreachable!(); + } + }) + } + + pub fn add_model_message_handler(self: &Arc, handler: H) where M: EntityMessage, E: Entity, @@ -383,38 +428,51 @@ impl Client { + Sync + Fn(ModelHandle, TypedEnvelope, Arc, AsyncAppContext) -> F, F: 'static + Future>, + { + self.add_entity_message_handler::(move |handle, message, client, cx| { + if let AnyEntityHandle::Model(handle) = handle { + handler(handle.downcast::().unwrap(), message, client, cx) + } else { + unreachable!(); + } + }) + } + + fn add_entity_message_handler(self: &Arc, handler: H) + where + M: EntityMessage, + E: Entity, + H: 'static + + Send + + Sync + + Fn(AnyEntityHandle, TypedEnvelope, Arc, AsyncAppContext) -> F, + F: 'static + Future>, { let model_type_id = TypeId::of::(); let message_type_id = TypeId::of::(); - let client = Arc::downgrade(self); let mut state = self.state.write(); state - .model_types_by_message_type + .entity_types_by_message_type .insert(message_type_id, model_type_id); state .entity_id_extractors .entry(message_type_id) .or_insert_with(|| { - Box::new(|envelope| { - let envelope = envelope + |envelope| { + envelope .as_any() .downcast_ref::>() - .unwrap(); - envelope.payload.remote_entity_id() - }) + .unwrap() + .payload + .remote_entity_id() + } }); - let prev_handler = state.message_handlers.insert( message_type_id, - Arc::new(move |handle, envelope, cx| { - let model = handle.downcast::().unwrap(); + Arc::new(move |handle, envelope, client, cx| { let envelope = envelope.into_any().downcast::>().unwrap(); - if let Some(client) = client.upgrade() { - handler(model, *envelope, client.clone(), cx).boxed_local() - } else { - async move { Ok(()) }.boxed_local() - } + handler(handle, *envelope, client.clone(), cx).boxed_local() }), ); if prev_handler.is_some() { @@ -422,7 +480,7 @@ impl Client { } } - pub fn add_entity_request_handler(self: &Arc, handler: H) + pub fn add_model_request_handler(self: &Arc, handler: H) where M: EntityMessage + RequestMessage, E: Entity, @@ -432,29 +490,56 @@ impl Client { + Fn(ModelHandle, TypedEnvelope, Arc, AsyncAppContext) -> F, F: 'static + Future>, { - self.add_entity_message_handler(move |model, envelope, client, cx| { - let receipt = envelope.receipt(); - let response = handler(model, envelope, client.clone(), cx); - async move { - match response.await { - Ok(response) => { - client.respond(receipt, response)?; - Ok(()) - } - Err(error) => { - client.respond_with_error( - receipt, - proto::Error { - message: error.to_string(), - }, - )?; - Err(error) - } - } - } + self.add_model_message_handler(move |entity, envelope, client, cx| { + Self::respond_to_request::( + envelope.receipt(), + handler(entity, envelope, client.clone(), cx), + client, + ) + }) + } + + pub fn add_view_request_handler(self: &Arc, handler: H) + where + M: EntityMessage + RequestMessage, + E: View, + H: 'static + + Send + + Sync + + Fn(ViewHandle, TypedEnvelope, Arc, AsyncAppContext) -> F, + F: 'static + Future>, + { + self.add_view_message_handler(move |entity, envelope, client, cx| { + Self::respond_to_request::( + envelope.receipt(), + handler(entity, envelope, client.clone(), cx), + client, + ) }) } + async fn respond_to_request>>( + receipt: Receipt, + response: F, + client: Arc, + ) -> Result<()> { + match response.await { + Ok(response) => { + client.respond(receipt, response)?; + Ok(()) + } + Err(error) => { + client.respond_with_error( + receipt, + proto::Error { + message: error.to_string(), + }, + )?; + Err(error) + } + } + } + pub fn has_keychain_credentials(&self, cx: &AsyncAppContext) -> bool { read_credentials_from_keychain(cx).is_some() } @@ -462,6 +547,7 @@ impl Client { #[async_recursion(?Send)] pub async fn authenticate_and_connect( self: &Arc, + try_keychain: bool, cx: &AsyncAppContext, ) -> anyhow::Result<()> { let was_disconnected = match *self.status().borrow() { @@ -483,23 +569,22 @@ impl Client { self.set_status(Status::Reauthenticating, cx) } - let mut used_keychain = false; - let credentials = self.state.read().credentials.clone(); - let credentials = if let Some(credentials) = credentials { - credentials - } else if let Some(credentials) = read_credentials_from_keychain(cx) { - used_keychain = true; - credentials - } else { - let credentials = match self.authenticate(&cx).await { + let mut read_from_keychain = false; + let mut credentials = self.state.read().credentials.clone(); + if credentials.is_none() && try_keychain { + credentials = read_credentials_from_keychain(cx); + read_from_keychain = credentials.is_some(); + } + if credentials.is_none() { + credentials = Some(match self.authenticate(&cx).await { Ok(credentials) => credentials, Err(err) => { self.set_status(Status::ConnectionError, cx); return Err(err); } - }; - credentials - }; + }); + } + let credentials = credentials.unwrap(); if was_disconnected { self.set_status(Status::Connecting, cx); @@ -510,7 +595,7 @@ impl Client { match self.establish_connection(&credentials, cx).await { Ok(conn) => { self.state.write().credentials = Some(credentials.clone()); - if !used_keychain && IMPERSONATE_LOGIN.is_none() { + if !read_from_keychain && IMPERSONATE_LOGIN.is_none() { write_credentials_to_keychain(&credentials, cx).log_err(); } self.set_connection(conn, cx).await; @@ -518,10 +603,10 @@ impl Client { } Err(EstablishConnectionError::Unauthorized) => { self.state.write().credentials.take(); - if used_keychain { + if read_from_keychain { cx.platform().delete_credentials(&ZED_SERVER_URL).log_err(); self.set_status(Status::SignedOut, cx); - self.authenticate_and_connect(cx).await + self.authenticate_and_connect(false, cx).await } else { self.set_status(Status::ConnectionError, cx); Err(EstablishConnectionError::Unauthorized)? @@ -561,24 +646,26 @@ impl Client { .models_by_message_type .get(&payload_type_id) .and_then(|model| model.upgrade(&cx)) + .map(AnyEntityHandle::Model) .or_else(|| { - let model_type_id = - *state.model_types_by_message_type.get(&payload_type_id)?; + let entity_type_id = + *state.entity_types_by_message_type.get(&payload_type_id)?; let entity_id = state .entity_id_extractors .get(&message.payload_type_id()) .map(|extract_entity_id| { (extract_entity_id)(message.as_ref()) })?; - let model = state - .models_by_entity_type_and_remote_id - .get(&(model_type_id, entity_id))?; - if let Some(model) = model.upgrade(&cx) { - Some(model) + + let entity = state + .entities_by_type_and_remote_id + .get(&(entity_type_id, entity_id))?; + if let Some(entity) = entity.upgrade(&cx) { + Some(entity) } else { state - .models_by_entity_type_and_remote_id - .remove(&(model_type_id, entity_id)); + .entities_by_type_and_remote_id + .remove(&(entity_type_id, entity_id)); None } }); @@ -593,7 +680,7 @@ impl Client { if let Some(handler) = state.message_handlers.get(&payload_type_id).cloned() { 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; log::debug!( @@ -891,6 +978,15 @@ impl Client { } } +impl AnyWeakEntityHandle { + fn upgrade(&self, cx: &AsyncAppContext) -> Option { + 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 { if IMPERSONATE_LOGIN.is_some() { return None; @@ -994,7 +1090,7 @@ mod tests { let (done_tx1, mut done_rx1) = 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, _: TypedEnvelope, _, cx| { match model.read_with(&cx, |model, _| model.id) { 1 => done_tx1.try_send(()).unwrap(), diff --git a/crates/client/src/test.rs b/crates/client/src/test.rs index f630d9c0ee0a08fc76753b39817d3665afeae4fc..35a8e85922b153ce3d997ac8901dd70992adfe9d 100644 --- a/crates/client/src/test.rs +++ b/crates/client/src/test.rs @@ -91,7 +91,7 @@ impl FakeServer { }); client - .authenticate_and_connect(&cx.to_async()) + .authenticate_and_connect(false, &cx.to_async()) .await .unwrap(); server diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index b5740a5ed75bde0fdfd7233133a4afd025c2f80e..b8b5b3a361bfa548372b81d982094ab2e0cf19ad 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -55,7 +55,7 @@ impl ContactsPanel { app_state: Arc, cx: &mut LayoutContext, ) -> ElementBox { - let theme = cx.app_state::().theme.clone(); + let theme = cx.global::().theme.clone(); let theme = &theme.contacts_panel; let project_count = collaborator.projects.len(); let font_cache = cx.font_cache(); @@ -236,7 +236,7 @@ impl View for ContactsPanel { } fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - let theme = &cx.app_state::().theme.contacts_panel; + let theme = &cx.global::().theme.contacts_panel; Container::new(List::new(self.contacts.clone()).boxed()) .with_style(theme.container) .boxed() diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 1a432f52e20658eacd3141aa141d243324b89299..56de434cf49e73c82ac19eefd7be36bdd3f5c71e 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -25,7 +25,7 @@ use std::{ sync::Arc, }; use util::TryFutureExt; -use workspace::{ItemHandle, ItemNavHistory, ItemViewHandle as _, Settings, Workspace}; +use workspace::{ItemHandle as _, ItemNavHistory, Settings, Workspace}; action!(Deploy); @@ -38,12 +38,8 @@ pub fn init(cx: &mut MutableAppContext) { type Event = editor::Event; -struct ProjectDiagnostics { - project: ModelHandle, -} - struct ProjectDiagnosticsEditor { - model: ModelHandle, + project: ModelHandle, workspace: WeakViewHandle, editor: ViewHandle, summary: DiagnosticSummary, @@ -65,16 +61,6 @@ struct DiagnosticGroupState { block_count: usize, } -impl ProjectDiagnostics { - fn new(project: ModelHandle) -> Self { - Self { project } - } -} - -impl Entity for ProjectDiagnostics { - type Event = (); -} - impl Entity for ProjectDiagnosticsEditor { type Event = Event; } @@ -86,7 +72,7 @@ impl View for ProjectDiagnosticsEditor { fn render(&mut self, cx: &mut RenderContext) -> ElementBox { if self.path_states.is_empty() { - let theme = &cx.app_state::().theme.project_diagnostics; + let theme = &cx.global::().theme.project_diagnostics; Label::new( "No problems in workspace".to_string(), theme.empty_message.clone(), @@ -109,12 +95,11 @@ impl View for ProjectDiagnosticsEditor { impl ProjectDiagnosticsEditor { fn new( - model: ModelHandle, + project_handle: ModelHandle, workspace: WeakViewHandle, cx: &mut ViewContext, ) -> Self { - let project = model.read(cx).project.clone(); - cx.subscribe(&project, |this, _, event, cx| match event { + cx.subscribe(&project_handle, |this, _, event, cx| match event { project::Event::DiskBasedDiagnosticsFinished => { this.update_excerpts(cx); this.update_title(cx); @@ -126,20 +111,22 @@ impl ProjectDiagnosticsEditor { }) .detach(); - let excerpts = cx.add_model(|cx| MultiBuffer::new(project.read(cx).replica_id())); + let excerpts = cx.add_model(|cx| MultiBuffer::new(project_handle.read(cx).replica_id())); let editor = cx.add_view(|cx| { - let mut editor = Editor::for_buffer(excerpts.clone(), Some(project.clone()), cx); + let mut editor = + Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), cx); editor.set_vertical_scroll_margin(5, cx); editor }); cx.subscribe(&editor, |_, _, event, cx| cx.emit(*event)) .detach(); - let project = project.read(cx); + let project = project_handle.read(cx); let paths_to_update = project.diagnostic_summaries(cx).map(|e| e.0).collect(); + let summary = project.diagnostic_summary(cx); let mut this = Self { - model, - summary: project.diagnostic_summary(cx), + project: project_handle, + summary, workspace, excerpts, editor, @@ -151,18 +138,20 @@ impl ProjectDiagnosticsEditor { } fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { - if let Some(existing) = workspace.item_of_type::(cx) { + if let Some(existing) = workspace.item_of_type::(cx) { workspace.activate_item(&existing, cx); } else { - let diagnostics = - cx.add_model(|_| ProjectDiagnostics::new(workspace.project().clone())); - workspace.open_item(diagnostics, cx); + let workspace_handle = cx.weak_handle(); + let diagnostics = cx.add_view(|cx| { + ProjectDiagnosticsEditor::new(workspace.project().clone(), workspace_handle, cx) + }); + workspace.add_item(Box::new(diagnostics), cx); } } fn update_excerpts(&mut self, cx: &mut ViewContext) { let paths = mem::take(&mut self.paths_to_update); - let project = self.model.read(cx).project.clone(); + let project = self.project.clone(); cx.spawn(|this, mut cx| { async move { for path in paths { @@ -289,7 +278,7 @@ impl ProjectDiagnosticsEditor { prev_excerpt_id = excerpt_id.clone(); first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone()); group_state.excerpts.push(excerpt_id.clone()); - let header_position = (excerpt_id.clone(), language::Anchor::min()); + let header_position = (excerpt_id.clone(), language::Anchor::MIN); if is_first_excerpt_for_group { is_first_excerpt_for_group = false; @@ -378,8 +367,7 @@ impl ProjectDiagnosticsEditor { range_a .start .cmp(&range_b.start, &snapshot) - .unwrap() - .then_with(|| range_a.end.cmp(&range_b.end, &snapshot).unwrap()) + .then_with(|| range_a.end.cmp(&range_b.end, &snapshot)) }); if path_state.diagnostic_groups.is_empty() { @@ -443,42 +431,17 @@ impl ProjectDiagnosticsEditor { } fn update_title(&mut self, cx: &mut ViewContext) { - self.summary = self.model.read(cx).project.read(cx).diagnostic_summary(cx); + self.summary = self.project.read(cx).diagnostic_summary(cx); cx.emit(Event::TitleChanged); } } -impl workspace::Item for ProjectDiagnostics { - type View = ProjectDiagnosticsEditor; - - fn build_view( - handle: ModelHandle, - workspace: &Workspace, - nav_history: ItemNavHistory, - cx: &mut ViewContext, - ) -> Self::View { - let diagnostics = ProjectDiagnosticsEditor::new(handle, workspace.weak_handle(), cx); - diagnostics - .editor - .update(cx, |editor, _| editor.set_nav_history(Some(nav_history))); - diagnostics - } - - fn project_path(&self) -> Option { - None - } -} - -impl workspace::ItemView for ProjectDiagnosticsEditor { - fn item(&self, _: &AppContext) -> Box { - Box::new(self.model.clone()) - } - +impl workspace::Item for ProjectDiagnosticsEditor { fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox { render_summary( &self.summary, &style.label.text, - &cx.app_state::().theme.project_diagnostics, + &cx.global::().theme.project_diagnostics, ) } @@ -486,9 +449,13 @@ impl workspace::ItemView for ProjectDiagnosticsEditor { None } - fn navigate(&mut self, data: Box, cx: &mut ViewContext) { + fn project_entry_id(&self, _: &AppContext) -> Option { + None + } + + fn navigate(&mut self, data: Box, cx: &mut ViewContext) -> bool { self.editor - .update(cx, |editor, cx| editor.navigate(data, cx)); + .update(cx, |editor, cx| editor.navigate(data, cx)) } fn is_dirty(&self, cx: &AppContext) -> bool { @@ -532,20 +499,21 @@ impl workspace::ItemView for ProjectDiagnosticsEditor { matches!(event, Event::Saved | Event::Dirtied | Event::TitleChanged) } - fn clone_on_split( - &self, - nav_history: ItemNavHistory, - cx: &mut ViewContext, - ) -> Option + fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext) { + self.editor.update(cx, |editor, _| { + editor.set_nav_history(Some(nav_history)); + }); + } + + fn clone_on_split(&self, cx: &mut ViewContext) -> Option where Self: Sized, { - let diagnostics = - ProjectDiagnosticsEditor::new(self.model.clone(), self.workspace.clone(), cx); - diagnostics.editor.update(cx, |editor, _| { - editor.set_nav_history(Some(nav_history)); - }); - Some(diagnostics) + Some(ProjectDiagnosticsEditor::new( + self.project.clone(), + self.workspace.clone(), + cx, + )) } fn act_as_type( @@ -571,7 +539,7 @@ impl workspace::ItemView for ProjectDiagnosticsEditor { fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock { let (message, highlights) = highlight_diagnostic_message(&diagnostic.message); Arc::new(move |cx| { - let settings = cx.app_state::(); + let settings = cx.global::(); let theme = &settings.theme.editor; let style = &theme.diagnostic_header; let font_size = (style.text_scale_factor * settings.buffer_font_size).round(); @@ -829,9 +797,8 @@ mod tests { }); // Open the project diagnostics view while there are already diagnostics. - let model = cx.add_model(|_| ProjectDiagnostics::new(project.clone())); let view = cx.add_view(0, |cx| { - ProjectDiagnosticsEditor::new(model, workspace.downgrade(), cx) + ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx) }); view.next_notification(&cx).await; diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs index 13d90f01190ea0eeddf14585193f22097988b326..690c5100ecf9ec3a8edf89414a2ef545f868201a 100644 --- a/crates/diagnostics/src/items.rs +++ b/crates/diagnostics/src/items.rs @@ -49,7 +49,7 @@ impl View for DiagnosticSummary { let in_progress = self.in_progress; MouseEventHandler::new::(0, cx, |_, cx| { - let theme = &cx.app_state::().theme.project_diagnostics; + let theme = &cx.global::().theme.project_diagnostics; if in_progress { Label::new( "Checking... ".to_string(), @@ -71,7 +71,7 @@ impl View for DiagnosticSummary { impl StatusItemView for DiagnosticSummary { fn set_active_pane_item( &mut self, - _: Option<&dyn workspace::ItemViewHandle>, + _: Option<&dyn workspace::ItemHandle>, _: &mut ViewContext, ) { } diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index 609e92af0fdd8531c00a9068521c617c0888a3e6..77e169b91b2e03648402687722b29ea045c7822b 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -27,6 +27,7 @@ gpui = { path = "../gpui" } language = { path = "../language" } lsp = { path = "../lsp" } project = { path = "../project" } +rpc = { path = "../rpc" } snippet = { path = "../snippet" } sum_tree = { path = "../sum_tree" } theme = { path = "../theme" } @@ -34,6 +35,7 @@ util = { path = "../util" } workspace = { path = "../workspace" } aho-corasick = "0.7" anyhow = "1.0" +futures = "0.3" itertools = "0.10" lazy_static = "1.4" log = "0.4" diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index dbadbb386eb2b5fed57f81d1a5bfc83d264ab04d..2bea851ec2fa92605aa32b9f2a2c584a649e7305 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -12,7 +12,7 @@ use gpui::{ Entity, ModelContext, ModelHandle, }; use language::{Point, Subscription as BufferSubscription}; -use std::{any::TypeId, ops::Range, sync::Arc}; +use std::{any::TypeId, fmt::Debug, ops::Range, sync::Arc}; use sum_tree::{Bias, TreeMap}; use tab_map::TabMap; use wrap_map::WrapMap; @@ -36,6 +36,7 @@ pub struct DisplayMap { wrap_map: ModelHandle, block_map: BlockMap, text_highlights: TextHighlights, + pub clip_at_line_ends: bool, } impl Entity for DisplayMap { @@ -67,6 +68,7 @@ impl DisplayMap { wrap_map, block_map, text_highlights: Default::default(), + clip_at_line_ends: false, } } @@ -87,6 +89,7 @@ impl DisplayMap { wraps_snapshot, blocks_snapshot, text_highlights: self.text_highlights.clone(), + clip_at_line_ends: self.clip_at_line_ends, } } @@ -114,6 +117,7 @@ impl DisplayMap { pub fn unfold( &mut self, ranges: impl IntoIterator>, + inclusive: bool, cx: &mut ModelContext, ) { let snapshot = self.buffer.read(cx).snapshot(cx); @@ -124,7 +128,7 @@ impl DisplayMap { .wrap_map .update(cx, |map, cx| map.sync(snapshot, edits, cx)); self.block_map.read(snapshot, edits); - let (snapshot, edits) = fold_map.unfold(ranges); + let (snapshot, edits) = fold_map.unfold(ranges, inclusive); let (snapshot, edits) = self.tab_map.sync(snapshot, edits); let (snapshot, edits) = self .wrap_map @@ -204,6 +208,7 @@ pub struct DisplaySnapshot { wraps_snapshot: wrap_map::WrapSnapshot, blocks_snapshot: block_map::BlockSnapshot, text_highlights: TextHighlights, + clip_at_line_ends: bool, } impl DisplaySnapshot { @@ -331,7 +336,12 @@ impl DisplaySnapshot { } pub fn clip_point(&self, point: DisplayPoint, bias: Bias) -> DisplayPoint { - DisplayPoint(self.blocks_snapshot.clip_point(point.0, bias)) + let mut clipped = self.blocks_snapshot.clip_point(point.0, bias); + if self.clip_at_line_ends && clipped.column == self.line_len(clipped.row) { + clipped.column = clipped.column.saturating_sub(1); + clipped = self.blocks_snapshot.clip_point(clipped, Bias::Left); + } + DisplayPoint(clipped) } pub fn folds_in_range<'a, T>( @@ -414,9 +424,19 @@ impl DisplaySnapshot { } } -#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] +#[derive(Copy, Clone, Default, Eq, Ord, PartialOrd, PartialEq)] pub struct DisplayPoint(BlockPoint); +impl Debug for DisplayPoint { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!( + "DisplayPoint({}, {})", + self.row(), + self.column() + )) + } +} + impl DisplayPoint { pub fn new(row: u32, column: u32) -> Self { Self(BlockPoint(Point::new(row, column))) @@ -426,7 +446,6 @@ impl DisplayPoint { Self::new(0, 0) } - #[cfg(test)] pub fn is_zero(&self) -> bool { self.0.is_zero() } @@ -478,16 +497,16 @@ impl ToDisplayPoint for Anchor { } #[cfg(test)] -mod tests { +pub mod tests { use super::*; - use crate::movement; + use crate::{movement, test::marked_display_snapshot}; use gpui::{color::Color, elements::*, test::observe, MutableAppContext}; use language::{Buffer, Language, LanguageConfig, RandomCharIter, SelectionGoal}; use rand::{prelude::*, Rng}; use smol::stream::StreamExt; use std::{env, sync::Arc}; use theme::SyntaxTheme; - use util::test::sample_text; + use util::test::{marked_text_ranges, sample_text}; use Bias::*; #[gpui::test(iterations = 100)] @@ -620,7 +639,7 @@ mod tests { if rng.gen() && fold_count > 0 { log::info!("unfolding ranges: {:?}", ranges); map.update(cx, |map, cx| { - map.unfold(ranges, cx); + map.unfold(ranges, true, cx); }); } else { log::info!("folding ranges: {:?}", ranges); @@ -705,7 +724,7 @@ mod tests { log::info!("Moving from point {:?}", point); - let moved_right = movement::right(&snapshot, point).unwrap(); + let moved_right = movement::right(&snapshot, point); log::info!("Right {:?}", moved_right); if point < max_point { assert!(moved_right > point); @@ -719,7 +738,7 @@ mod tests { assert_eq!(moved_right, point); } - let moved_left = movement::left(&snapshot, point).unwrap(); + let moved_left = movement::left(&snapshot, point); log::info!("Left {:?}", moved_left); if point > min_point { assert!(moved_left < point); @@ -777,15 +796,15 @@ mod tests { DisplayPoint::new(1, 0) ); assert_eq!( - movement::right(&snapshot, DisplayPoint::new(0, 7)).unwrap(), + movement::right(&snapshot, DisplayPoint::new(0, 7)), DisplayPoint::new(1, 0) ); assert_eq!( - movement::left(&snapshot, DisplayPoint::new(1, 0)).unwrap(), + movement::left(&snapshot, DisplayPoint::new(1, 0)), DisplayPoint::new(0, 7) ); assert_eq!( - movement::up(&snapshot, DisplayPoint::new(1, 10), SelectionGoal::None).unwrap(), + movement::up(&snapshot, DisplayPoint::new(1, 10), SelectionGoal::None), (DisplayPoint::new(0, 7), SelectionGoal::Column(10)) ); assert_eq!( @@ -793,8 +812,7 @@ mod tests { &snapshot, DisplayPoint::new(0, 7), SelectionGoal::Column(10) - ) - .unwrap(), + ), (DisplayPoint::new(1, 10), SelectionGoal::Column(10)) ); assert_eq!( @@ -802,8 +820,7 @@ mod tests { &snapshot, DisplayPoint::new(1, 10), SelectionGoal::Column(10) - ) - .unwrap(), + ), (DisplayPoint::new(2, 4), SelectionGoal::Column(10)) ); @@ -922,7 +939,7 @@ mod tests { let map = cx .add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx)); assert_eq!( - cx.update(|cx| chunks(0..5, &map, &theme, cx)), + cx.update(|cx| syntax_chunks(0..5, &map, &theme, cx)), vec![ ("fn ".to_string(), None), ("outer".to_string(), Some(Color::blue())), @@ -933,7 +950,7 @@ mod tests { ] ); assert_eq!( - cx.update(|cx| chunks(3..5, &map, &theme, cx)), + cx.update(|cx| syntax_chunks(3..5, &map, &theme, cx)), vec![ (" fn ".to_string(), Some(Color::red())), ("inner".to_string(), Some(Color::blue())), @@ -945,7 +962,7 @@ mod tests { map.fold(vec![Point::new(0, 6)..Point::new(3, 2)], cx) }); assert_eq!( - cx.update(|cx| chunks(0..2, &map, &theme, cx)), + cx.update(|cx| syntax_chunks(0..2, &map, &theme, cx)), vec![ ("fn ".to_string(), None), ("out".to_string(), Some(Color::blue())), @@ -1011,7 +1028,7 @@ mod tests { DisplayMap::new(buffer, tab_size, font_id, font_size, Some(40.0), 1, 1, cx) }); assert_eq!( - cx.update(|cx| chunks(0..5, &map, &theme, cx)), + cx.update(|cx| syntax_chunks(0..5, &map, &theme, cx)), [ ("fn \n".to_string(), None), ("oute\nr".to_string(), Some(Color::blue())), @@ -1019,7 +1036,7 @@ mod tests { ] ); assert_eq!( - cx.update(|cx| chunks(3..5, &map, &theme, cx)), + cx.update(|cx| syntax_chunks(3..5, &map, &theme, cx)), [("{}\n\n".to_string(), None)] ); @@ -1027,7 +1044,7 @@ mod tests { map.fold(vec![Point::new(0, 6)..Point::new(3, 2)], cx) }); assert_eq!( - cx.update(|cx| chunks(1..4, &map, &theme, cx)), + cx.update(|cx| syntax_chunks(1..4, &map, &theme, cx)), [ ("out".to_string(), Some(Color::blue())), ("…\n".to_string(), None), @@ -1038,50 +1055,151 @@ mod tests { } #[gpui::test] - fn test_clip_point(cx: &mut gpui::MutableAppContext) { - use Bias::{Left, Right}; + async fn test_chunks_with_text_highlights(cx: &mut gpui::TestAppContext) { + cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX); - let text = "\n'a', 'α',\t'✋',\t'❎', '🍐'\n"; - let display_text = "\n'a', 'α', '✋', '❎', '🍐'\n"; - let buffer = MultiBuffer::build_simple(text, cx); + let theme = SyntaxTheme::new(vec![ + ("operator".to_string(), Color::red().into()), + ("string".to_string(), Color::green().into()), + ]); + let language = Arc::new( + Language::new( + LanguageConfig { + name: "Test".into(), + path_suffixes: vec![".test".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ) + .with_highlights_query( + r#" + ":" @operator + (string_literal) @string + "#, + ) + .unwrap(), + ); + language.set_theme(&theme); + + let (text, highlighted_ranges) = marked_text_ranges(r#"const[] [a]: B = "c [d]""#); + + let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); + buffer.condition(&cx, |buf, _| !buf.is_parsing()).await; + + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx)); - let tab_size = 4; let font_cache = cx.font_cache(); - let family_id = font_cache.load_family(&["Helvetica"]).unwrap(); + let tab_size = 4; + let family_id = font_cache.load_family(&["Courier"]).unwrap(); let font_id = font_cache .select_font(family_id, &Default::default()) .unwrap(); - let font_size = 14.0; - let map = cx.add_model(|cx| { - DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, 1, 1, cx) + let font_size = 16.0; + let map = cx + .add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx)); + + enum MyType {} + + let style = HighlightStyle { + color: Some(Color::blue()), + ..Default::default() + }; + + map.update(cx, |map, _cx| { + map.highlight_text( + TypeId::of::(), + highlighted_ranges + .into_iter() + .map(|range| { + buffer_snapshot.anchor_before(range.start) + ..buffer_snapshot.anchor_before(range.end) + }) + .collect(), + style, + ); }); - let map = map.update(cx, |map, cx| map.snapshot(cx)); - assert_eq!(map.text(), display_text); - for (input_column, bias, output_column) in vec![ - ("'a', '".len(), Left, "'a', '".len()), - ("'a', '".len() + 1, Left, "'a', '".len()), - ("'a', '".len() + 1, Right, "'a', 'α".len()), - ("'a', 'α', ".len(), Left, "'a', 'α',".len()), - ("'a', 'α', ".len(), Right, "'a', 'α', ".len()), - ("'a', 'α', '".len() + 1, Left, "'a', 'α', '".len()), - ("'a', 'α', '".len() + 1, Right, "'a', 'α', '✋".len()), - ("'a', 'α', '✋',".len(), Right, "'a', 'α', '✋',".len()), - ("'a', 'α', '✋', ".len(), Left, "'a', 'α', '✋',".len()), - ( - "'a', 'α', '✋', ".len(), - Right, - "'a', 'α', '✋', ".len(), - ), - ] { + assert_eq!( + cx.update(|cx| chunks(0..10, &map, &theme, cx)), + [ + ("const ".to_string(), None, None), + ("a".to_string(), None, Some(Color::blue())), + (":".to_string(), Some(Color::red()), None), + (" B = ".to_string(), None, None), + ("\"c ".to_string(), Some(Color::green()), None), + ("d".to_string(), Some(Color::green()), Some(Color::blue())), + ("\"".to_string(), Some(Color::green()), None), + ] + ); + } + + #[gpui::test] + fn test_clip_point(cx: &mut gpui::MutableAppContext) { + fn assert(text: &str, shift_right: bool, bias: Bias, cx: &mut gpui::MutableAppContext) { + let (unmarked_snapshot, mut markers) = marked_display_snapshot(text, cx); + + match bias { + Bias::Left => { + if shift_right { + *markers[1].column_mut() += 1; + } + + assert_eq!(unmarked_snapshot.clip_point(markers[1], bias), markers[0]) + } + Bias::Right => { + if shift_right { + *markers[0].column_mut() += 1; + } + + assert_eq!( + unmarked_snapshot.clip_point(dbg!(markers[0]), bias), + markers[1] + ) + } + }; + } + + use Bias::{Left, Right}; + assert("||α", false, Left, cx); + assert("||α", true, Left, cx); + assert("||α", false, Right, cx); + assert("|α|", true, Right, cx); + assert("||✋", false, Left, cx); + assert("||✋", true, Left, cx); + assert("||✋", false, Right, cx); + assert("|✋|", true, Right, cx); + assert("||🍐", false, Left, cx); + assert("||🍐", true, Left, cx); + assert("||🍐", false, Right, cx); + assert("|🍐|", true, Right, cx); + assert("||\t", false, Left, cx); + assert("||\t", true, Left, cx); + assert("||\t", false, Right, cx); + assert("|\t|", true, Right, cx); + assert(" ||\t", false, Left, cx); + assert(" ||\t", true, Left, cx); + assert(" ||\t", false, Right, cx); + assert(" |\t|", true, Right, cx); + assert(" ||\t", false, Left, cx); + assert(" ||\t", false, Right, cx); + } + + #[gpui::test] + fn test_clip_at_line_ends(cx: &mut gpui::MutableAppContext) { + fn assert(text: &str, cx: &mut gpui::MutableAppContext) { + let (mut unmarked_snapshot, markers) = marked_display_snapshot(text, cx); + unmarked_snapshot.clip_at_line_ends = true; assert_eq!( - map.clip_point(DisplayPoint::new(1, input_column as u32), bias), - DisplayPoint::new(1, output_column as u32), - "clip_point(({}, {}))", - 1, - input_column, + unmarked_snapshot.clip_point(markers[1], Bias::Left), + markers[0] ); } + + assert("||", cx); + assert("|a|", cx); + assert("a|b|", cx); + assert("a|α|", cx); } #[gpui::test] @@ -1163,27 +1281,38 @@ mod tests { ) } - fn chunks<'a>( + fn syntax_chunks<'a>( rows: Range, map: &ModelHandle, theme: &'a SyntaxTheme, cx: &mut MutableAppContext, ) -> Vec<(String, Option)> { + chunks(rows, map, theme, cx) + .into_iter() + .map(|(text, color, _)| (text, color)) + .collect() + } + + fn chunks<'a>( + rows: Range, + map: &ModelHandle, + theme: &'a SyntaxTheme, + cx: &mut MutableAppContext, + ) -> Vec<(String, Option, Option)> { let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); - let mut chunks: Vec<(String, Option)> = Vec::new(); + let mut chunks: Vec<(String, Option, Option)> = Vec::new(); for chunk in snapshot.chunks(rows, true) { - let color = chunk + let syntax_color = chunk .syntax_highlight_id .and_then(|id| id.style(theme)?.color); - if let Some((last_chunk, last_color)) = chunks.last_mut() { - if color == *last_color { + let highlight_color = chunk.highlight_style.and_then(|style| style.color); + if let Some((last_chunk, last_syntax_color, last_highlight_color)) = chunks.last_mut() { + if syntax_color == *last_syntax_color && highlight_color == *last_highlight_color { last_chunk.push_str(chunk.text); - } else { - chunks.push((chunk.text.to_string(), color)); + continue; } - } else { - chunks.push((chunk.text.to_string(), color)); } + chunks.push((chunk.text.to_string(), syntax_color, highlight_color)); } chunks } diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 489381daef3d7156090847e36d48e4c33b94f6b2..9d1b7a75886b234bbcf54d92a31df9898b568469 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -499,7 +499,7 @@ impl<'a> BlockMapWriter<'a> { let block_ix = match self .0 .blocks - .binary_search_by(|probe| probe.position.cmp(&position, &buffer).unwrap()) + .binary_search_by(|probe| probe.position.cmp(&position, &buffer)) { Ok(ix) | Err(ix) => ix, }; diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index ac4477263d4cb015d86274ab881b4e673f747f73..2c09244a7df38492b10ad4837f5443e3e8f7c457 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -140,13 +140,14 @@ impl<'a> FoldMapWriter<'a> { pub fn unfold( &mut self, ranges: impl IntoIterator>, + inclusive: bool, ) -> (FoldSnapshot, Vec) { let mut edits = Vec::new(); let mut fold_ixs_to_delete = Vec::new(); let buffer = self.0.buffer.lock().clone(); for range in ranges.into_iter() { // Remove intersecting folds and add their ranges to edits that are passed to sync. - let mut folds_cursor = intersecting_folds(&buffer, &self.0.folds, range, true); + let mut folds_cursor = intersecting_folds(&buffer, &self.0.folds, range, inclusive); while let Some(fold) = folds_cursor.item() { let offset_range = fold.0.start.to_offset(&buffer)..fold.0.end.to_offset(&buffer); if offset_range.end > offset_range.start { @@ -256,7 +257,7 @@ impl FoldMap { let mut folds = self.folds.iter().peekable(); while let Some(fold) = folds.next() { if let Some(next_fold) = folds.peek() { - let comparison = fold.0.cmp(&next_fold.0, &self.buffer.lock()).unwrap(); + let comparison = fold.0.cmp(&next_fold.0, &self.buffer.lock()); assert!(comparison.is_le()); } } @@ -699,10 +700,7 @@ impl FoldSnapshot { let ranges = &highlights.1; let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe - .end - .cmp(&transform_start, &self.buffer_snapshot()) - .unwrap(); + let cmp = probe.end.cmp(&transform_start, &self.buffer_snapshot()); if cmp.is_gt() { Ordering::Greater } else { @@ -715,7 +713,6 @@ impl FoldSnapshot { if range .start .cmp(&transform_end, &self.buffer_snapshot) - .unwrap() .is_ge() { break; @@ -820,8 +817,8 @@ where let start = buffer.anchor_before(range.start.to_offset(buffer)); let end = buffer.anchor_after(range.end.to_offset(buffer)); let mut cursor = folds.filter::<_, usize>(move |summary| { - let start_cmp = start.cmp(&summary.max_end, buffer).unwrap(); - let end_cmp = end.cmp(&summary.min_start, buffer).unwrap(); + let start_cmp = start.cmp(&summary.max_end, buffer); + let end_cmp = end.cmp(&summary.min_start, buffer); if inclusive { start_cmp <= Ordering::Equal && end_cmp >= Ordering::Equal @@ -962,19 +959,19 @@ impl sum_tree::Summary for FoldSummary { type Context = MultiBufferSnapshot; fn add_summary(&mut self, other: &Self, buffer: &MultiBufferSnapshot) { - if other.min_start.cmp(&self.min_start, buffer).unwrap() == Ordering::Less { + if other.min_start.cmp(&self.min_start, buffer) == Ordering::Less { self.min_start = other.min_start.clone(); } - if other.max_end.cmp(&self.max_end, buffer).unwrap() == Ordering::Greater { + if other.max_end.cmp(&self.max_end, buffer) == Ordering::Greater { self.max_end = other.max_end.clone(); } #[cfg(debug_assertions)] { - let start_comparison = self.start.cmp(&other.start, buffer).unwrap(); + let start_comparison = self.start.cmp(&other.start, buffer); assert!(start_comparison <= Ordering::Equal); if start_comparison == Ordering::Equal { - assert!(self.end.cmp(&other.end, buffer).unwrap() >= Ordering::Equal); + assert!(self.end.cmp(&other.end, buffer) >= Ordering::Equal); } } @@ -993,7 +990,7 @@ impl<'a> sum_tree::Dimension<'a, FoldSummary> for Fold { impl<'a> sum_tree::SeekTarget<'a, FoldSummary, Fold> for Fold { fn cmp(&self, other: &Self, buffer: &MultiBufferSnapshot) -> Ordering { - self.0.cmp(&other.0, buffer).unwrap() + self.0.cmp(&other.0, buffer) } } @@ -1156,7 +1153,7 @@ impl Ord for HighlightEndpoint { fn cmp(&self, other: &Self) -> Ordering { self.offset .cmp(&other.offset) - .then_with(|| self.is_start.cmp(&other.is_start)) + .then_with(|| other.is_start.cmp(&self.is_start)) } } @@ -1282,9 +1279,14 @@ mod tests { assert_eq!(snapshot4.text(), "123a…c123456eee"); let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]); - writer.unfold(Some(Point::new(0, 4)..Point::new(0, 5))); + writer.unfold(Some(Point::new(0, 4)..Point::new(0, 4)), false); let (snapshot5, _) = map.read(buffer_snapshot.clone(), vec![]); - assert_eq!(snapshot5.text(), "123aaaaa\nbbbbbb\nccc123456eee"); + assert_eq!(snapshot5.text(), "123a…c123456eee"); + + let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]); + writer.unfold(Some(Point::new(0, 4)..Point::new(0, 4)), true); + let (snapshot6, _) = map.read(buffer_snapshot.clone(), vec![]); + assert_eq!(snapshot6.text(), "123aaaaa\nbbbbbb\nccc123456eee"); } #[gpui::test] @@ -1600,9 +1602,8 @@ mod tests { .filter(|fold| { let start = buffer_snapshot.anchor_before(start); let end = buffer_snapshot.anchor_after(end); - start.cmp(&fold.0.end, &buffer_snapshot).unwrap() == Ordering::Less - && end.cmp(&fold.0.start, &buffer_snapshot).unwrap() - == Ordering::Greater + start.cmp(&fold.0.end, &buffer_snapshot) == Ordering::Less + && end.cmp(&fold.0.start, &buffer_snapshot) == Ordering::Greater }) .map(|fold| fold.0) .collect::>(); @@ -1680,7 +1681,7 @@ mod tests { let buffer = self.buffer.lock().clone(); let mut folds = self.folds.items(&buffer); // Ensure sorting doesn't change how folds get merged and displayed. - folds.sort_by(|a, b| a.0.cmp(&b.0, &buffer).unwrap()); + folds.sort_by(|a, b| a.0.cmp(&b.0, &buffer)); let mut fold_ranges = folds .iter() .map(|fold| fold.0.start.to_offset(&buffer)..fold.0.end.to_offset(&buffer)) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index a80af9ea5f21dfe30e9842ef794580c717bb2dcc..f861b6ac60b1f4fe2c1b8ca83df26fed4abf9d6f 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -10,7 +10,7 @@ mod test; use aho_corasick::AhoCorasick; use anyhow::Result; use clock::ReplicaId; -use collections::{BTreeMap, Bound, HashMap, HashSet}; +use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque}; pub use display_map::DisplayPoint; use display_map::*; pub use element::*; @@ -28,7 +28,6 @@ use gpui::{ ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; -use items::{BufferItemHandle, MultiBufferItemHandle}; use itertools::Itertools as _; pub use language::{char_kind, CharKind}; use language::{ @@ -58,22 +57,26 @@ pub use sum_tree::Bias; use text::rope::TextDimension; use theme::DiagnosticStyle; use util::{post_inc, ResultExt, TryFutureExt}; -use workspace::{settings, ItemNavHistory, PathOpener, Settings, Workspace}; +use workspace::{settings, ItemNavHistory, Settings, Workspace}; const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); const MAX_LINE_LEN: usize = 1024; const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10; +const MAX_SELECTION_HISTORY_LEN: usize = 1024; action!(Cancel); action!(Backspace); action!(Delete); action!(Input, String); action!(Newline); -action!(Tab); +action!(Tab, Direction); +action!(Indent); action!(Outdent); action!(DeleteLine); -action!(DeleteToPreviousWordBoundary); -action!(DeleteToNextWordBoundary); +action!(DeleteToPreviousWordStart); +action!(DeleteToPreviousSubwordStart); +action!(DeleteToNextWordEnd); +action!(DeleteToNextSubwordEnd); action!(DeleteToBeginningOfLine); action!(DeleteToEndOfLine); action!(CutToEndOfLine); @@ -89,8 +92,10 @@ action!(MoveUp); action!(MoveDown); action!(MoveLeft); action!(MoveRight); -action!(MoveToPreviousWordBoundary); -action!(MoveToNextWordBoundary); +action!(MoveToPreviousWordStart); +action!(MoveToPreviousSubwordStart); +action!(MoveToNextWordEnd); +action!(MoveToNextSubwordEnd); action!(MoveToBeginningOfLine); action!(MoveToEndOfLine); action!(MoveToBeginning); @@ -99,8 +104,10 @@ action!(SelectUp); action!(SelectDown); action!(SelectLeft); action!(SelectRight); -action!(SelectToPreviousWordBoundary); -action!(SelectToNextWordBoundary); +action!(SelectToPreviousWordStart); +action!(SelectToPreviousSubwordStart); +action!(SelectToNextWordEnd); +action!(SelectToNextSubwordEnd); action!(SelectToBeginningOfLine, bool); action!(SelectToEndOfLine, bool); action!(SelectToBeginning); @@ -115,6 +122,8 @@ action!(ToggleComments); action!(SelectLargerSyntaxNode); action!(SelectSmallerSyntaxNode); action!(MoveToEnclosingBracket); +action!(UndoSelection); +action!(RedoSelection); action!(GoToDiagnostic, Direction); action!(GoToDefinition); action!(FindAllReferences); @@ -123,7 +132,7 @@ action!(ConfirmRename); action!(PageUp); action!(PageDown); action!(Fold); -action!(Unfold); +action!(UnfoldLines); action!(FoldSelectedRanges); action!(Scroll, Vector2F); action!(Select, SelectPhase); @@ -142,8 +151,7 @@ pub enum Direction { Next, } -pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec>) { - path_openers.push(Box::new(items::BufferOpener)); +pub fn init(cx: &mut MutableAppContext) { cx.add_bindings(vec![ Binding::new("escape", Cancel, Some("Editor")), Binding::new("backspace", Backspace, Some("Editor")), @@ -167,22 +175,28 @@ pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec Range; - fn point_range(&self, buffer: &MultiBufferSnapshot) -> Range; - fn display_range(&self, map: &DisplaySnapshot) -> Range; - fn spanned_rows(&self, include_end_if_at_line_start: bool, map: &DisplaySnapshot) - -> Range; + workspace::register_project_item::(cx); + workspace::register_followable_item::(cx); } trait InvalidationRegion { @@ -430,15 +466,14 @@ pub struct Editor { columnar_selection_tail: Option, add_selections_state: Option, select_next_state: Option, - selection_history: - HashMap]>, Option]>>)>, + selection_history: SelectionHistory, autoclose_stack: InvalidationStack, snippet_stack: InvalidationStack, select_larger_syntax_node_stack: Vec]>>, active_diagnostics: Option, scroll_position: Vector2F, - scroll_top_anchor: Option, - autoscroll_request: Option, + scroll_top_anchor: Anchor, + autoscroll_request: Option<(Autoscroll, bool)>, soft_wrap_mode_override: Option, get_field_editor_theme: Option, override_text_style: Option>, @@ -463,6 +498,9 @@ pub struct Editor { pending_rename: Option, searchable: bool, cursor_shape: CursorShape, + keymap_context_layers: BTreeMap, + input_enabled: bool, + leader_replica_id: Option, } pub struct EditorSnapshot { @@ -471,7 +509,7 @@ pub struct EditorSnapshot { pub placeholder_text: Option>, is_focused: bool, scroll_position: Vector2F, - scroll_top_anchor: Option, + scroll_top_anchor: Anchor, } #[derive(Clone)] @@ -480,11 +518,105 @@ pub struct PendingSelection { mode: SelectMode, } +#[derive(Clone)] +struct SelectionHistoryEntry { + selections: Arc<[Selection]>, + select_next_state: Option, + add_selections_state: Option, +} + +enum SelectionHistoryMode { + Normal, + Undoing, + Redoing, +} + +impl Default for SelectionHistoryMode { + fn default() -> Self { + Self::Normal + } +} + +#[derive(Default)] +struct SelectionHistory { + selections_by_transaction: + HashMap]>, Option]>>)>, + mode: SelectionHistoryMode, + undo_stack: VecDeque, + redo_stack: VecDeque, +} + +impl SelectionHistory { + fn insert_transaction( + &mut self, + transaction_id: TransactionId, + selections: Arc<[Selection]>, + ) { + self.selections_by_transaction + .insert(transaction_id, (selections, None)); + } + + fn transaction( + &self, + transaction_id: TransactionId, + ) -> Option<&(Arc<[Selection]>, Option]>>)> { + self.selections_by_transaction.get(&transaction_id) + } + + fn transaction_mut( + &mut self, + transaction_id: TransactionId, + ) -> Option<&mut (Arc<[Selection]>, Option]>>)> { + self.selections_by_transaction.get_mut(&transaction_id) + } + + fn push(&mut self, entry: SelectionHistoryEntry) { + if !entry.selections.is_empty() { + match self.mode { + SelectionHistoryMode::Normal => { + self.push_undo(entry); + self.redo_stack.clear(); + } + SelectionHistoryMode::Undoing => self.push_redo(entry), + SelectionHistoryMode::Redoing => self.push_undo(entry), + } + } + } + + fn push_undo(&mut self, entry: SelectionHistoryEntry) { + if self + .undo_stack + .back() + .map_or(true, |e| e.selections != entry.selections) + { + self.undo_stack.push_back(entry); + if self.undo_stack.len() > MAX_SELECTION_HISTORY_LEN { + self.undo_stack.pop_front(); + } + } + } + + fn push_redo(&mut self, entry: SelectionHistoryEntry) { + if self + .redo_stack + .back() + .map_or(true, |e| e.selections != entry.selections) + { + self.redo_stack.push_back(entry); + if self.redo_stack.len() > MAX_SELECTION_HISTORY_LEN { + self.redo_stack.pop_front(); + } + } + } +} + +#[derive(Clone)] struct AddSelectionsState { above: bool, stack: Vec, } +#[derive(Clone)] struct SelectNextState { query: AhoCorasick, wordwise: bool, @@ -802,6 +934,8 @@ pub struct NavigationData { offset: usize, } +pub struct EditorCreated(pub ViewHandle); + impl Editor { pub fn single_line( field_editor_style: Option, @@ -829,6 +963,15 @@ impl Editor { } pub fn for_buffer( + buffer: ModelHandle, + project: Option>, + cx: &mut ViewContext, + ) -> Self { + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + Self::new(EditorMode::Full, buffer, project, None, cx) + } + + pub fn for_multibuffer( buffer: ModelHandle, project: Option>, cx: &mut ViewContext, @@ -836,7 +979,7 @@ impl Editor { Self::new(EditorMode::Full, buffer, project, None, cx) } - pub fn clone(&self, nav_history: ItemNavHistory, cx: &mut ViewContext) -> Self { + pub fn clone(&self, cx: &mut ViewContext) -> Self { let mut clone = Self::new( self.mode, self.buffer.clone(), @@ -846,7 +989,6 @@ impl Editor { ); clone.scroll_position = self.scroll_position; clone.scroll_top_anchor = self.scroll_top_anchor.clone(); - clone.nav_history = Some(nav_history); clone.searchable = self.searchable; clone } @@ -859,7 +1001,7 @@ impl Editor { cx: &mut ViewContext, ) -> Self { let display_map = cx.add_model(|cx| { - let settings = cx.app_state::(); + let settings = cx.global::(); let style = build_style(&*settings, get_field_editor_theme, None, cx); DisplayMap::new( buffer.clone(), @@ -905,7 +1047,7 @@ impl Editor { get_field_editor_theme, project, scroll_position: Vector2F::zero(), - scroll_top_anchor: None, + scroll_top_anchor: Anchor::min(), autoscroll_request: None, focused: false, show_local_cursors: false, @@ -928,8 +1070,15 @@ impl Editor { searchable: true, override_text_style: None, cursor_shape: Default::default(), + keymap_context_layers: Default::default(), + input_enabled: true, + leader_replica_id: None, }; this.end_selection(cx); + + let editor_created_event = EditorCreated(cx.handle()); + cx.emit_global(editor_created_event); + this } @@ -938,14 +1087,17 @@ impl Editor { _: &workspace::OpenNew, cx: &mut ViewContext, ) { - let project = workspace.project(); + let project = workspace.project().clone(); if project.read(cx).is_remote() { cx.propagate_action(); } else if let Some(buffer) = project .update(cx, |project, cx| project.create_buffer(cx)) .log_err() { - workspace.open_item(BufferItemHandle(buffer), cx); + workspace.add_item( + Box::new(cx.add_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx))), + cx, + ); } } @@ -981,13 +1133,17 @@ impl Editor { fn style(&self, cx: &AppContext) -> EditorStyle { build_style( - cx.app_state::(), + cx.global::(), self.get_field_editor_theme, self.override_text_style.as_deref(), cx, ) } + pub fn mode(&self) -> EditorMode { + self.mode + } + pub fn set_placeholder_text( &mut self, placeholder_text: impl Into>, @@ -1003,10 +1159,19 @@ impl Editor { } pub fn set_scroll_position(&mut self, scroll_position: Vector2F, cx: &mut ViewContext) { + self.set_scroll_position_internal(scroll_position, true, cx); + } + + fn set_scroll_position_internal( + &mut self, + scroll_position: Vector2F, + local: bool, + cx: &mut ViewContext, + ) { let map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); if scroll_position.y() == 0. { - self.scroll_top_anchor = None; + self.scroll_top_anchor = Anchor::min(); self.scroll_position = scroll_position; } else { let scroll_top_buffer_offset = @@ -1018,9 +1183,22 @@ impl Editor { scroll_position.x(), 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.scroll_top_anchor = anchor; + self.scroll_position = position; + cx.emit(Event::ScrollPositionChanged { local: false }); cx.notify(); } @@ -1029,6 +1207,24 @@ impl Editor { cx.notify(); } + pub fn set_clip_at_line_ends(&mut self, clip: bool, cx: &mut ViewContext) { + self.display_map + .update(cx, |map, _| map.clip_at_line_ends = clip); + } + + pub fn set_keymap_context_layer(&mut self, context: gpui::keymap::Context) { + self.keymap_context_layers + .insert(TypeId::of::(), context); + } + + pub fn remove_keymap_context_layer(&mut self) { + self.keymap_context_layers.remove(&TypeId::of::()); + } + + pub fn set_input_enabled(&mut self, input_enabled: bool) { + self.input_enabled = input_enabled; + } + pub fn scroll_position(&self, cx: &mut ViewContext) -> Vector2F { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); compute_scroll_position(&display_map, self.scroll_position, &self.scroll_top_anchor) @@ -1063,7 +1259,7 @@ impl Editor { 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 } else { return false; @@ -1115,15 +1311,15 @@ impl Editor { if target_top < start_row { 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 { 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 => { 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); } } @@ -1189,6 +1385,66 @@ impl Editor { } } + pub fn move_selections( + &mut self, + cx: &mut ViewContext, + move_selection: impl Fn(&DisplaySnapshot, &mut Selection), + ) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let selections = self + .local_selections::(cx) + .into_iter() + .map(|selection| { + let mut selection = Selection { + id: selection.id, + start: selection.start.to_display_point(&display_map), + end: selection.end.to_display_point(&display_map), + reversed: selection.reversed, + goal: selection.goal, + }; + move_selection(&display_map, &mut selection); + Selection { + id: selection.id, + start: selection.start.to_point(&display_map), + end: selection.end.to_point(&display_map), + reversed: selection.reversed, + goal: selection.goal, + } + }) + .collect(); + self.update_selections(selections, Some(Autoscroll::Fit), cx); + } + + pub fn move_selection_heads( + &mut self, + cx: &mut ViewContext, + update_head: impl Fn( + &DisplaySnapshot, + DisplayPoint, + SelectionGoal, + ) -> (DisplayPoint, SelectionGoal), + ) { + self.move_selections(cx, |map, selection| { + let (new_head, new_goal) = update_head(map, selection.head(), selection.goal); + selection.set_head(new_head, new_goal); + }); + } + + pub fn move_cursors( + &mut self, + cx: &mut ViewContext, + update_cursor_position: impl Fn( + &DisplaySnapshot, + DisplayPoint, + SelectionGoal, + ) -> (DisplayPoint, SelectionGoal), + ) { + self.move_selections(cx, |map, selection| { + let (cursor, new_goal) = update_cursor_position(map, selection.head(), selection.goal); + selection.collapse_to(cursor, new_goal) + }); + } + fn select(&mut self, Select(phase): &Select, cx: &mut ViewContext) { self.hide_context_menu(cx); @@ -1245,7 +1501,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( @@ -1299,8 +1555,6 @@ impl Editor { } } - self.push_to_nav_history(newest_selection.head(), Some(end.to_point(&buffer)), cx); - let selection = Selection { id: post_inc(&mut self.next_selection_id), start, @@ -1325,7 +1579,12 @@ impl Editor { } else { selections = Arc::from([]); } - self.set_selections(selections, Some(PendingSelection { selection, mode }), cx); + self.set_selections( + selections, + Some(PendingSelection { selection, mode }), + true, + cx, + ); cx.notify(); } @@ -1439,7 +1698,7 @@ impl Editor { pending.selection.end = buffer.anchor_before(head); pending.selection.reversed = false; } - self.set_selections(self.selections.clone(), Some(pending), cx); + self.set_selections(self.selections.clone(), Some(pending), true, cx); } else { log::error!("update_selection dispatched with no pending selection"); return; @@ -1526,7 +1785,7 @@ impl Editor { if selections.is_empty() { selections = Arc::from([pending.selection]); } - self.set_selections(selections, None, cx); + self.set_selections(selections, None, true, cx); self.request_autoscroll(Autoscroll::Fit, cx); } else { let mut oldest_selection = self.oldest_selection::(&cx); @@ -1546,7 +1805,7 @@ impl Editor { #[cfg(any(test, feature = "test-support"))] pub fn selected_ranges>( &self, - cx: &mut MutableAppContext, + cx: &AppContext, ) -> Vec> { self.local_selections::(cx) .iter() @@ -1645,136 +1904,142 @@ impl Editor { } pub fn handle_input(&mut self, action: &Input, cx: &mut ViewContext) { + if !self.input_enabled { + cx.propagate_action(); + return; + } + let text = action.0.as_ref(); if !self.skip_autoclose_end(text, cx) { - self.start_transaction(cx); - if !self.surround_with_bracket_pair(text, cx) { - self.insert(text, cx); - self.autoclose_bracket_pairs(cx); - } - self.end_transaction(cx); + self.transact(cx, |this, cx| { + if !this.surround_with_bracket_pair(text, cx) { + this.insert(text, cx); + this.autoclose_bracket_pairs(cx); + } + }); self.trigger_completion_on_input(text, cx); } } pub fn newline(&mut self, _: &Newline, cx: &mut ViewContext) { - self.start_transaction(cx); - let mut old_selections = SmallVec::<[_; 32]>::new(); - { - let selections = self.local_selections::(cx); - let buffer = self.buffer.read(cx).snapshot(cx); - for selection in selections.iter() { - let start_point = selection.start.to_point(&buffer); - let indent = buffer - .indent_column_for_line(start_point.row) - .min(start_point.column); - let start = selection.start; - let end = selection.end; - - let mut insert_extra_newline = false; - if let Some(language) = buffer.language() { - let leading_whitespace_len = buffer - .reversed_chars_at(start) - .take_while(|c| c.is_whitespace() && *c != '\n') - .map(|c| c.len_utf8()) - .sum::(); - - let trailing_whitespace_len = buffer - .chars_at(end) - .take_while(|c| c.is_whitespace() && *c != '\n') - .map(|c| c.len_utf8()) - .sum::(); - - insert_extra_newline = language.brackets().iter().any(|pair| { - let pair_start = pair.start.trim_end(); - let pair_end = pair.end.trim_start(); - - pair.newline - && buffer.contains_str_at(end + trailing_whitespace_len, pair_end) - && buffer.contains_str_at( - (start - leading_whitespace_len).saturating_sub(pair_start.len()), - pair_start, - ) - }); - } + self.transact(cx, |this, cx| { + let mut old_selections = SmallVec::<[_; 32]>::new(); + { + let selections = this.local_selections::(cx); + let buffer = this.buffer.read(cx).snapshot(cx); + for selection in selections.iter() { + let start_point = selection.start.to_point(&buffer); + let indent = buffer + .indent_column_for_line(start_point.row) + .min(start_point.column); + let start = selection.start; + let end = selection.end; + + let mut insert_extra_newline = false; + if let Some(language) = buffer.language() { + let leading_whitespace_len = buffer + .reversed_chars_at(start) + .take_while(|c| c.is_whitespace() && *c != '\n') + .map(|c| c.len_utf8()) + .sum::(); + + let trailing_whitespace_len = buffer + .chars_at(end) + .take_while(|c| c.is_whitespace() && *c != '\n') + .map(|c| c.len_utf8()) + .sum::(); + + insert_extra_newline = language.brackets().iter().any(|pair| { + let pair_start = pair.start.trim_end(); + let pair_end = pair.end.trim_start(); + + pair.newline + && buffer.contains_str_at(end + trailing_whitespace_len, pair_end) + && buffer.contains_str_at( + (start - leading_whitespace_len) + .saturating_sub(pair_start.len()), + pair_start, + ) + }); + } - old_selections.push(( - selection.id, - buffer.anchor_after(end), - start..end, - indent, - insert_extra_newline, - )); + old_selections.push(( + selection.id, + buffer.anchor_after(end), + start..end, + indent, + insert_extra_newline, + )); + } } - } - self.buffer.update(cx, |buffer, cx| { - let mut delta = 0_isize; - let mut pending_edit: Option = None; - for (_, _, range, indent, insert_extra_newline) in &old_selections { - if pending_edit.as_ref().map_or(false, |pending| { - pending.indent != *indent - || pending.insert_extra_newline != *insert_extra_newline - }) { - let pending = pending_edit.take().unwrap(); - let mut new_text = String::with_capacity(1 + pending.indent as usize); - new_text.push('\n'); - new_text.extend(iter::repeat(' ').take(pending.indent as usize)); - if pending.insert_extra_newline { - new_text = new_text.repeat(2); + this.buffer.update(cx, |buffer, cx| { + let mut delta = 0_isize; + let mut pending_edit: Option = None; + for (_, _, range, indent, insert_extra_newline) in &old_selections { + if pending_edit.as_ref().map_or(false, |pending| { + pending.indent != *indent + || pending.insert_extra_newline != *insert_extra_newline + }) { + let pending = pending_edit.take().unwrap(); + let mut new_text = String::with_capacity(1 + pending.indent as usize); + new_text.push('\n'); + new_text.extend(iter::repeat(' ').take(pending.indent as usize)); + if pending.insert_extra_newline { + new_text = new_text.repeat(2); + } + buffer.edit_with_autoindent(pending.ranges, new_text, cx); + delta += pending.delta; } - buffer.edit_with_autoindent(pending.ranges, new_text, cx); - delta += pending.delta; - } - let start = (range.start as isize + delta) as usize; - let end = (range.end as isize + delta) as usize; - let mut text_len = *indent as usize + 1; - if *insert_extra_newline { - text_len *= 2; + let start = (range.start as isize + delta) as usize; + let end = (range.end as isize + delta) as usize; + let mut text_len = *indent as usize + 1; + if *insert_extra_newline { + text_len *= 2; + } + + let pending = pending_edit.get_or_insert_with(Default::default); + pending.delta += text_len as isize - (end - start) as isize; + pending.indent = *indent; + pending.insert_extra_newline = *insert_extra_newline; + pending.ranges.push(start..end); } - let pending = pending_edit.get_or_insert_with(Default::default); - pending.delta += text_len as isize - (end - start) as isize; - pending.indent = *indent; - pending.insert_extra_newline = *insert_extra_newline; - pending.ranges.push(start..end); - } + let pending = pending_edit.unwrap(); + let mut new_text = String::with_capacity(1 + pending.indent as usize); + new_text.push('\n'); + new_text.extend(iter::repeat(' ').take(pending.indent as usize)); + if pending.insert_extra_newline { + new_text = new_text.repeat(2); + } + buffer.edit_with_autoindent(pending.ranges, new_text, cx); - let pending = pending_edit.unwrap(); - let mut new_text = String::with_capacity(1 + pending.indent as usize); - new_text.push('\n'); - new_text.extend(iter::repeat(' ').take(pending.indent as usize)); - if pending.insert_extra_newline { - new_text = new_text.repeat(2); - } - buffer.edit_with_autoindent(pending.ranges, new_text, cx); + let buffer = buffer.read(cx); + this.selections = this + .selections + .iter() + .cloned() + .zip(old_selections) + .map( + |(mut new_selection, (_, end_anchor, _, _, insert_extra_newline))| { + let mut cursor = end_anchor.to_point(&buffer); + if insert_extra_newline { + cursor.row -= 1; + cursor.column = buffer.line_len(cursor.row); + } + let anchor = buffer.anchor_after(cursor); + new_selection.start = anchor.clone(); + new_selection.end = anchor; + new_selection + }, + ) + .collect(); + }); - let buffer = buffer.read(cx); - self.selections = self - .selections - .iter() - .cloned() - .zip(old_selections) - .map( - |(mut new_selection, (_, end_anchor, _, _, insert_extra_newline))| { - let mut cursor = end_anchor.to_point(&buffer); - if insert_extra_newline { - cursor.row -= 1; - cursor.column = buffer.line_len(cursor.row); - } - let anchor = buffer.anchor_after(cursor); - new_selection.start = anchor.clone(); - new_selection.end = anchor; - new_selection - }, - ) - .collect(); + this.request_autoscroll(Autoscroll::Fit, cx); }); - self.request_autoscroll(Autoscroll::Fit, cx); - self.end_transaction(cx); - #[derive(Default)] struct PendingEdit { indent: u32, @@ -1785,40 +2050,39 @@ impl Editor { } pub fn insert(&mut self, text: &str, cx: &mut ViewContext) { - self.start_transaction(cx); + self.transact(cx, |this, cx| { + let old_selections = this.local_selections::(cx); + let selection_anchors = this.buffer.update(cx, |buffer, cx| { + let anchors = { + let snapshot = buffer.read(cx); + old_selections + .iter() + .map(|s| (s.id, s.goal, snapshot.anchor_after(s.end))) + .collect::>() + }; + let edit_ranges = old_selections.iter().map(|s| s.start..s.end); + buffer.edit_with_autoindent(edit_ranges, text, cx); + anchors + }); - let old_selections = self.local_selections::(cx); - let selection_anchors = self.buffer.update(cx, |buffer, cx| { - let anchors = { - let snapshot = buffer.read(cx); - old_selections - .iter() - .map(|s| (s.id, s.goal, snapshot.anchor_after(s.end))) - .collect::>() + let selections = { + let snapshot = this.buffer.read(cx).read(cx); + selection_anchors + .into_iter() + .map(|(id, goal, position)| { + let position = position.to_offset(&snapshot); + Selection { + id, + start: position, + end: position, + goal, + reversed: false, + } + }) + .collect() }; - let edit_ranges = old_selections.iter().map(|s| s.start..s.end); - buffer.edit_with_autoindent(edit_ranges, text, cx); - anchors + this.update_selections(selections, Some(Autoscroll::Fit), cx); }); - - let selections = { - let snapshot = self.buffer.read(cx).read(cx); - selection_anchors - .into_iter() - .map(|(id, goal, position)| { - let position = position.to_offset(&snapshot); - Selection { - id, - start: position, - end: position, - goal, - reversed: false, - } - }) - .collect() - }; - self.update_selections(selections, Some(Autoscroll::Fit), cx); - self.end_transaction(cx); } fn trigger_completion_on_input(&mut self, text: &str, cx: &mut ViewContext) { @@ -1873,7 +2137,7 @@ impl Editor { } drop(snapshot); - self.set_selections(selections.into(), None, cx); + self.set_selections(selections.into(), None, true, cx); true } } else { @@ -2187,21 +2451,21 @@ impl Editor { } let text = &text[common_prefix_len..]; - self.start_transaction(cx); - if let Some(mut snippet) = snippet { - snippet.text = text.to_string(); - for tabstop in snippet.tabstops.iter_mut().flatten() { - tabstop.start -= common_prefix_len as isize; - tabstop.end -= common_prefix_len as isize; - } + self.transact(cx, |this, cx| { + if let Some(mut snippet) = snippet { + snippet.text = text.to_string(); + for tabstop in snippet.tabstops.iter_mut().flatten() { + tabstop.start -= common_prefix_len as isize; + tabstop.end -= common_prefix_len as isize; + } - self.insert_snippet(&ranges, snippet, cx).log_err(); - } else { - self.buffer.update(cx, |buffer, cx| { - buffer.edit_with_autoindent(ranges, text, cx); - }); - } - self.end_transaction(cx); + this.insert_snippet(&ranges, snippet, cx).log_err(); + } else { + this.buffer.update(cx, |buffer, cx| { + buffer.edit_with_autoindent(ranges, text, cx); + }); + } + }); let project = self.project.clone()?; let apply_edits = project.update(cx, |project, cx| { @@ -2300,7 +2564,7 @@ impl Editor { ) -> Result<()> { let replica_id = this.read_with(&cx, |this, cx| this.replica_id(cx)); - // If the code action's edits are all contained within this editor, then + // If the project transaction's edits are all contained within this editor, then // avoid opening a new editor to display them. let mut entries = transaction.0.iter(); if let Some((buffer, transaction)) = entries.next() { @@ -2349,13 +2613,14 @@ impl Editor { }); workspace.update(&mut cx, |workspace, cx| { - let editor = workspace.open_item(MultiBufferItemHandle(excerpt_buffer), cx); - if let Some(editor) = editor.act_as::(cx) { - editor.update(cx, |editor, cx| { - let color = editor.style(cx).highlighted_line_background; - editor.highlight_background::(ranges_to_highlight, color, cx); - }); - } + let project = workspace.project().clone(); + let editor = + cx.add_view(|cx| Editor::for_multibuffer(excerpt_buffer, Some(project), cx)); + workspace.add_item(Box::new(editor.clone()), cx); + editor.update(cx, |editor, cx| { + let color = editor.style(cx).highlighted_line_background; + editor.highlight_background::(ranges_to_highlight, color, cx); + }); }); Ok(()) @@ -2421,7 +2686,6 @@ impl Editor { } let buffer_id = cursor_position.buffer_id; - let excerpt_id = cursor_position.excerpt_id.clone(); let style = this.style(cx); let read_background = style.document_highlight_read_background; let write_background = style.document_highlight_write_background; @@ -2433,22 +2697,39 @@ impl Editor { return; } + let cursor_buffer_snapshot = cursor_buffer.read(cx); let mut write_ranges = Vec::new(); let mut read_ranges = Vec::new(); for highlight in highlights { - let range = Anchor { - buffer_id, - excerpt_id: excerpt_id.clone(), - text_anchor: highlight.range.start, - }..Anchor { - buffer_id, - excerpt_id: excerpt_id.clone(), - text_anchor: highlight.range.end, - }; - if highlight.kind == lsp::DocumentHighlightKind::WRITE { - write_ranges.push(range); - } else { - read_ranges.push(range); + for (excerpt_id, excerpt_range) in + buffer.excerpts_for_buffer(&cursor_buffer, cx) + { + let start = highlight + .range + .start + .max(&excerpt_range.start, cursor_buffer_snapshot); + let end = highlight + .range + .end + .min(&excerpt_range.end, cursor_buffer_snapshot); + if start.cmp(&end, cursor_buffer_snapshot).is_ge() { + continue; + } + + let range = Anchor { + buffer_id, + excerpt_id: excerpt_id.clone(), + text_anchor: start, + }..Anchor { + buffer_id, + excerpt_id, + text_anchor: end, + }; + if highlight.kind == lsp::DocumentHighlightKind::WRITE { + write_ranges.push(range); + } else { + read_ranges.push(range); + } } } @@ -2558,8 +2839,7 @@ impl Editor { }) }) .collect::>(); - tabstop_ranges - .sort_unstable_by(|a, b| a.start.cmp(&b.start, snapshot).unwrap()); + tabstop_ranges.sort_unstable_by(|a, b| a.start.cmp(&b.start, snapshot)); tabstop_ranges }) .collect::>() @@ -2634,14 +2914,13 @@ impl Editor { } pub fn clear(&mut self, cx: &mut ViewContext) { - self.start_transaction(cx); - self.select_all(&SelectAll, cx); - self.insert("", cx); - self.end_transaction(cx); + self.transact(cx, |this, cx| { + this.select_all(&SelectAll, cx); + this.insert("", cx); + }); } pub fn backspace(&mut self, _: &Backspace, cx: &mut ViewContext) { - self.start_transaction(cx); let mut selections = self.local_selections::(cx); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); for selection in &mut selections { @@ -2649,7 +2928,6 @@ impl Editor { let old_head = selection.head(); let mut new_head = movement::left(&display_map, old_head.to_display_point(&display_map)) - .unwrap() .to_point(&display_map); if let Some((buffer, line_buffer_range)) = display_map .buffer_snapshot @@ -2665,60 +2943,82 @@ impl Editor { } } - selection.set_head(new_head); - selection.goal = SelectionGoal::None; + selection.set_head(new_head, SelectionGoal::None); } } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - self.insert("", cx); - self.end_transaction(cx); + + self.transact(cx, |this, cx| { + this.update_selections(selections, Some(Autoscroll::Fit), cx); + this.insert("", cx); + }); } pub fn delete(&mut self, _: &Delete, cx: &mut ViewContext) { - self.start_transaction(cx); - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - if selection.is_empty() { - let head = selection.head().to_display_point(&display_map); - let cursor = movement::right(&display_map, head) - .unwrap() - .to_point(&display_map); - selection.set_head(cursor); - selection.goal = SelectionGoal::None; - } - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - self.insert(&"", cx); - self.end_transaction(cx); + self.transact(cx, |this, cx| { + this.move_selections(cx, |map, selection| { + if selection.is_empty() { + let cursor = movement::right(map, selection.head()); + selection.set_head(cursor, SelectionGoal::None); + } + }); + this.insert(&"", cx); + }); } - pub fn tab(&mut self, _: &Tab, cx: &mut ViewContext) { - if self.move_to_next_snippet_tabstop(cx) { - return; + pub fn tab(&mut self, &Tab(direction): &Tab, cx: &mut ViewContext) { + match direction { + Direction::Prev => { + if !self.snippet_stack.is_empty() { + self.move_to_prev_snippet_tabstop(cx); + return; + } + + self.outdent(&Outdent, cx); + } + Direction::Next => { + if self.move_to_next_snippet_tabstop(cx) { + return; + } + + let tab_size = cx.global::().tab_size; + let mut selections = self.local_selections::(cx); + if selections.iter().all(|s| s.is_empty()) { + self.transact(cx, |this, cx| { + this.buffer.update(cx, |buffer, cx| { + for selection in &mut selections { + let char_column = buffer + .read(cx) + .text_for_range( + Point::new(selection.start.row, 0)..selection.start, + ) + .flat_map(str::chars) + .count(); + let chars_to_next_tab_stop = tab_size - (char_column % tab_size); + buffer.edit( + [selection.start..selection.start], + " ".repeat(chars_to_next_tab_stop), + cx, + ); + selection.start.column += chars_to_next_tab_stop as u32; + selection.end = selection.start; + } + }); + this.update_selections(selections, Some(Autoscroll::Fit), cx); + }); + } else { + self.indent(&Indent, cx); + } + } } + } - self.start_transaction(cx); - let tab_size = cx.app_state::().tab_size; + pub fn indent(&mut self, _: &Indent, cx: &mut ViewContext) { + let tab_size = cx.global::().tab_size; let mut selections = self.local_selections::(cx); - let mut last_indent = None; - self.buffer.update(cx, |buffer, cx| { - for selection in &mut selections { - if selection.is_empty() { - let char_column = buffer - .read(cx) - .text_for_range(Point::new(selection.start.row, 0)..selection.start) - .flat_map(str::chars) - .count(); - let chars_to_next_tab_stop = tab_size - (char_column % tab_size); - buffer.edit( - [selection.start..selection.start], - " ".repeat(chars_to_next_tab_stop), - cx, - ); - selection.start.column += chars_to_next_tab_stop as u32; - selection.end = selection.start; - } else { + self.transact(cx, |this, cx| { + let mut last_indent = None; + this.buffer.update(cx, |buffer, cx| { + for selection in &mut selections { let mut start_row = selection.start.row; let mut end_row = selection.end.row + 1; @@ -2762,21 +3062,14 @@ impl Editor { last_indent = Some((row, columns_to_next_tab_stop as u32)); } } - } - }); + }); - self.update_selections(selections, Some(Autoscroll::Fit), cx); - self.end_transaction(cx); + this.update_selections(selections, Some(Autoscroll::Fit), cx); + }); } pub fn outdent(&mut self, _: &Outdent, cx: &mut ViewContext) { - if !self.snippet_stack.is_empty() { - self.move_to_prev_snippet_tabstop(cx); - return; - } - - self.start_transaction(cx); - let tab_size = cx.app_state::().tab_size; + let tab_size = cx.global::().tab_size; let selections = self.local_selections::(cx); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut deletion_ranges = Vec::new(); @@ -2807,21 +3100,20 @@ impl Editor { } } } - self.buffer.update(cx, |buffer, cx| { - buffer.edit(deletion_ranges, "", cx); - }); - self.update_selections( - self.local_selections::(cx), - Some(Autoscroll::Fit), - cx, - ); - self.end_transaction(cx); + self.transact(cx, |this, cx| { + this.buffer.update(cx, |buffer, cx| { + buffer.edit(deletion_ranges, "", cx); + }); + this.update_selections( + this.local_selections::(cx), + Some(Autoscroll::Fit), + cx, + ); + }); } pub fn delete_line(&mut self, _: &DeleteLine, cx: &mut ViewContext) { - self.start_transaction(cx); - let selections = self.local_selections::(cx); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let buffer = self.buffer.read(cx).snapshot(cx); @@ -2871,30 +3163,29 @@ impl Editor { edit_ranges.push(edit_start..edit_end); } - let buffer = self.buffer.update(cx, |buffer, cx| { - buffer.edit(edit_ranges, "", cx); - buffer.snapshot(cx) + self.transact(cx, |this, cx| { + let buffer = this.buffer.update(cx, |buffer, cx| { + buffer.edit(edit_ranges, "", cx); + buffer.snapshot(cx) + }); + let new_selections = new_cursors + .into_iter() + .map(|(id, cursor)| { + let cursor = cursor.to_point(&buffer); + Selection { + id, + start: cursor, + end: cursor, + reversed: false, + goal: SelectionGoal::None, + } + }) + .collect(); + this.update_selections(new_selections, Some(Autoscroll::Fit), cx); }); - let new_selections = new_cursors - .into_iter() - .map(|(id, cursor)| { - let cursor = cursor.to_point(&buffer); - Selection { - id, - start: cursor, - end: cursor, - reversed: false, - goal: SelectionGoal::None, - } - }) - .collect(); - self.update_selections(new_selections, Some(Autoscroll::Fit), cx); - self.end_transaction(cx); } pub fn duplicate_line(&mut self, _: &DuplicateLine, cx: &mut ViewContext) { - self.start_transaction(cx); - let selections = self.local_selections::(cx); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let buffer = &display_map.buffer_snapshot; @@ -2925,14 +3216,15 @@ impl Editor { edits.push((start, text, rows.len() as u32)); } - self.buffer.update(cx, |buffer, cx| { - for (point, text, _) in edits.into_iter().rev() { - buffer.edit(Some(point..point), text, cx); - } - }); + self.transact(cx, |this, cx| { + this.buffer.update(cx, |buffer, cx| { + for (point, text, _) in edits.into_iter().rev() { + buffer.edit(Some(point..point), text, cx); + } + }); - self.request_autoscroll(Autoscroll::Fit, cx); - self.end_transaction(cx); + this.request_autoscroll(Autoscroll::Fit, cx); + }); } pub fn move_line_up(&mut self, _: &MoveLineUp, cx: &mut ViewContext) { @@ -3033,16 +3325,16 @@ impl Editor { new_selections.extend(contiguous_row_selections.drain(..)); } - self.start_transaction(cx); - self.unfold_ranges(unfold_ranges, cx); - self.buffer.update(cx, |buffer, cx| { - for (range, text) in edits { - buffer.edit([range], text, cx); - } + self.transact(cx, |this, cx| { + this.unfold_ranges(unfold_ranges, true, cx); + this.buffer.update(cx, |buffer, cx| { + for (range, text) in edits { + buffer.edit([range], text, cx); + } + }); + this.fold_ranges(refold_ranges, cx); + this.update_selections(new_selections, Some(Autoscroll::Fit), cx); }); - self.fold_ranges(refold_ranges, cx); - self.update_selections(new_selections, Some(Autoscroll::Fit), cx); - self.end_transaction(cx); } pub fn move_line_down(&mut self, _: &MoveLineDown, cx: &mut ViewContext) { @@ -3136,20 +3428,19 @@ impl Editor { new_selections.extend(contiguous_row_selections.drain(..)); } - self.start_transaction(cx); - self.unfold_ranges(unfold_ranges, cx); - self.buffer.update(cx, |buffer, cx| { - for (range, text) in edits { - buffer.edit([range], text, cx); - } + self.transact(cx, |this, cx| { + this.unfold_ranges(unfold_ranges, true, cx); + this.buffer.update(cx, |buffer, cx| { + for (range, text) in edits { + buffer.edit([range], text, cx); + } + }); + this.fold_ranges(refold_ranges, cx); + this.update_selections(new_selections, Some(Autoscroll::Fit), cx); }); - self.fold_ranges(refold_ranges, cx); - self.update_selections(new_selections, Some(Autoscroll::Fit), cx); - self.end_transaction(cx); } pub fn cut(&mut self, _: &Cut, cx: &mut ViewContext) { - self.start_transaction(cx); let mut text = String::new(); let mut selections = self.local_selections::(cx); let mut clipboard_selections = Vec::with_capacity(selections.len()); @@ -3174,12 +3465,12 @@ impl Editor { }); } } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - self.insert("", cx); - self.end_transaction(cx); - cx.as_mut() - .write_to_clipboard(ClipboardItem::new(text).with_metadata(clipboard_selections)); + self.transact(cx, |this, cx| { + this.update_selections(selections, Some(Autoscroll::Fit), cx); + this.insert("", cx); + cx.write_to_clipboard(ClipboardItem::new(text).with_metadata(clipboard_selections)); + }); } pub fn copy(&mut self, _: &Copy, cx: &mut ViewContext) { @@ -3209,80 +3500,85 @@ impl Editor { } } - cx.as_mut() - .write_to_clipboard(ClipboardItem::new(text).with_metadata(clipboard_selections)); + cx.write_to_clipboard(ClipboardItem::new(text).with_metadata(clipboard_selections)); } pub fn paste(&mut self, _: &Paste, cx: &mut ViewContext) { - if let Some(item) = cx.as_mut().read_from_clipboard() { - let clipboard_text = item.text(); - if let Some(mut clipboard_selections) = item.metadata::>() { - let mut selections = self.local_selections::(cx); - let all_selections_were_entire_line = - clipboard_selections.iter().all(|s| s.is_entire_line); - if clipboard_selections.len() != selections.len() { - clipboard_selections.clear(); - } - - let mut delta = 0_isize; - let mut start_offset = 0; - for (i, selection) in selections.iter_mut().enumerate() { - let to_insert; - let entire_line; - if let Some(clipboard_selection) = clipboard_selections.get(i) { - let end_offset = start_offset + clipboard_selection.len; - to_insert = &clipboard_text[start_offset..end_offset]; - entire_line = clipboard_selection.is_entire_line; - start_offset = end_offset - } else { - to_insert = clipboard_text.as_str(); - entire_line = all_selections_were_entire_line; + self.transact(cx, |this, cx| { + if let Some(item) = cx.as_mut().read_from_clipboard() { + let clipboard_text = item.text(); + if let Some(mut clipboard_selections) = item.metadata::>() { + let mut selections = this.local_selections::(cx); + let all_selections_were_entire_line = + clipboard_selections.iter().all(|s| s.is_entire_line); + if clipboard_selections.len() != selections.len() { + clipboard_selections.clear(); } - selection.start = (selection.start as isize + delta) as usize; - selection.end = (selection.end as isize + delta) as usize; - - self.buffer.update(cx, |buffer, cx| { - // If the corresponding selection was empty when this slice of the - // clipboard text was written, then the entire line containing the - // selection was copied. If this selection is also currently empty, - // then paste the line before the current line of the buffer. - let range = if selection.is_empty() && entire_line { - let column = selection.start.to_point(&buffer.read(cx)).column as usize; - let line_start = selection.start - column; - line_start..line_start + let mut delta = 0_isize; + let mut start_offset = 0; + for (i, selection) in selections.iter_mut().enumerate() { + let to_insert; + let entire_line; + if let Some(clipboard_selection) = clipboard_selections.get(i) { + let end_offset = start_offset + clipboard_selection.len; + to_insert = &clipboard_text[start_offset..end_offset]; + entire_line = clipboard_selection.is_entire_line; + start_offset = end_offset } else { - selection.start..selection.end - }; + to_insert = clipboard_text.as_str(); + entire_line = all_selections_were_entire_line; + } - delta += to_insert.len() as isize - range.len() as isize; - buffer.edit([range], to_insert, cx); - selection.start += to_insert.len(); - selection.end = selection.start; - }); + selection.start = (selection.start as isize + delta) as usize; + selection.end = (selection.end as isize + delta) as usize; + + this.buffer.update(cx, |buffer, cx| { + // If the corresponding selection was empty when this slice of the + // clipboard text was written, then the entire line containing the + // selection was copied. If this selection is also currently empty, + // then paste the line before the current line of the buffer. + let range = if selection.is_empty() && entire_line { + let column = + selection.start.to_point(&buffer.read(cx)).column as usize; + let line_start = selection.start - column; + line_start..line_start + } else { + selection.start..selection.end + }; + + delta += to_insert.len() as isize - range.len() as isize; + buffer.edit([range], to_insert, cx); + selection.start += to_insert.len(); + selection.end = selection.start; + }); + } + this.update_selections(selections, Some(Autoscroll::Fit), cx); + } else { + this.insert(clipboard_text, cx); } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - } else { - self.insert(clipboard_text, cx); } - } + }); } pub fn undo(&mut self, _: &Undo, cx: &mut ViewContext) { 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() { - self.set_selections(selections, None, cx); + if let Some((selections, _)) = self.selection_history.transaction(tx_id).cloned() { + self.set_selections(selections, None, true, cx); } self.request_autoscroll(Autoscroll::Fit, cx); + cx.emit(Event::Edited); } } pub fn redo(&mut self, _: &Redo, cx: &mut ViewContext) { 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() { - self.set_selections(selections, None, cx); + if let Some((_, Some(selections))) = self.selection_history.transaction(tx_id).cloned() + { + self.set_selections(selections, None, true, cx); } self.request_autoscroll(Autoscroll::Fit, cx); + cx.emit(Event::Edited); } } @@ -3292,75 +3588,37 @@ impl Editor { } pub fn move_left(&mut self, _: &MoveLeft, cx: &mut ViewContext) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let start = selection.start.to_display_point(&display_map); - let end = selection.end.to_display_point(&display_map); - - if start != end { - selection.end = selection.start.clone(); + self.move_selections(cx, |map, selection| { + let cursor = if selection.is_empty() { + movement::left(map, selection.start) } else { - let cursor = movement::left(&display_map, start) - .unwrap() - .to_point(&display_map); - selection.start = cursor.clone(); - selection.end = cursor; - } - selection.reversed = false; - selection.goal = SelectionGoal::None; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); + selection.start + }; + selection.collapse_to(cursor, SelectionGoal::None); + }); } pub fn select_left(&mut self, _: &SelectLeft, cx: &mut ViewContext) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let head = selection.head().to_display_point(&display_map); - let cursor = movement::left(&display_map, head) - .unwrap() - .to_point(&display_map); - selection.set_head(cursor); - selection.goal = SelectionGoal::None; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); + self.move_selection_heads(cx, |map, head, _| { + (movement::left(map, head), SelectionGoal::None) + }); } pub fn move_right(&mut self, _: &MoveRight, cx: &mut ViewContext) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let start = selection.start.to_display_point(&display_map); - let end = selection.end.to_display_point(&display_map); - - if start != end { - selection.start = selection.end.clone(); + self.move_selections(cx, |map, selection| { + let cursor = if selection.is_empty() { + movement::right(map, selection.end) } else { - let cursor = movement::right(&display_map, end) - .unwrap() - .to_point(&display_map); - selection.start = cursor; - selection.end = cursor; - } - selection.reversed = false; - selection.goal = SelectionGoal::None; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); + selection.end + }; + selection.collapse_to(cursor, SelectionGoal::None) + }); } pub fn select_right(&mut self, _: &SelectRight, cx: &mut ViewContext) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let head = selection.head().to_display_point(&display_map); - let cursor = movement::right(&display_map, head) - .unwrap() - .to_point(&display_map); - selection.set_head(cursor); - selection.goal = SelectionGoal::None; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); + self.move_selection_heads(cx, |map, head, _| { + (movement::right(map, head), SelectionGoal::None) + }); } pub fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext) { @@ -3379,36 +3637,17 @@ impl Editor { return; } - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let start = selection.start.to_display_point(&display_map); - let end = selection.end.to_display_point(&display_map); - if start != end { + self.move_selections(cx, |map, selection| { + if !selection.is_empty() { selection.goal = SelectionGoal::None; } - - let (start, goal) = movement::up(&display_map, start, selection.goal).unwrap(); - let cursor = start.to_point(&display_map); - selection.start = cursor; - selection.end = cursor; - selection.goal = goal; - selection.reversed = false; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); + let (cursor, goal) = movement::up(&map, selection.start, selection.goal); + selection.collapse_to(cursor, goal); + }); } pub fn select_up(&mut self, _: &SelectUp, cx: &mut ViewContext) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let head = selection.head().to_display_point(&display_map); - let (head, goal) = movement::up(&display_map, head, selection.goal).unwrap(); - let cursor = head.to_point(&display_map); - selection.set_head(cursor); - selection.goal = goal; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); + self.move_selection_heads(cx, movement::up) } pub fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext) { @@ -3425,148 +3664,161 @@ impl Editor { return; } - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let start = selection.start.to_display_point(&display_map); - let end = selection.end.to_display_point(&display_map); - if start != end { + self.move_selections(cx, |map, selection| { + if !selection.is_empty() { selection.goal = SelectionGoal::None; } - - let (start, goal) = movement::down(&display_map, end, selection.goal).unwrap(); - let cursor = start.to_point(&display_map); - selection.start = cursor; - selection.end = cursor; - selection.goal = goal; - selection.reversed = false; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); + let (cursor, goal) = movement::down(&map, selection.end, selection.goal); + selection.collapse_to(cursor, goal); + }); } pub fn select_down(&mut self, _: &SelectDown, cx: &mut ViewContext) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let head = selection.head().to_display_point(&display_map); - let (head, goal) = movement::down(&display_map, head, selection.goal).unwrap(); - let cursor = head.to_point(&display_map); - selection.set_head(cursor); - selection.goal = goal; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); + self.move_selection_heads(cx, movement::down) } - pub fn move_to_previous_word_boundary( + pub fn move_to_previous_word_start( &mut self, - _: &MoveToPreviousWordBoundary, + _: &MoveToPreviousWordStart, cx: &mut ViewContext, ) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let head = selection.head().to_display_point(&display_map); - let cursor = movement::prev_word_boundary(&display_map, head).to_point(&display_map); - selection.start = cursor.clone(); - selection.end = cursor; - selection.reversed = false; - selection.goal = SelectionGoal::None; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); + self.move_cursors(cx, |map, head, _| { + ( + movement::previous_word_start(map, head), + SelectionGoal::None, + ) + }); } - pub fn select_to_previous_word_boundary( + pub fn move_to_previous_subword_start( &mut self, - _: &SelectToPreviousWordBoundary, + _: &MoveToPreviousSubwordStart, cx: &mut ViewContext, ) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let head = selection.head().to_display_point(&display_map); - let cursor = movement::prev_word_boundary(&display_map, head).to_point(&display_map); - selection.set_head(cursor); - selection.goal = SelectionGoal::None; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); + self.move_cursors(cx, |map, head, _| { + ( + movement::previous_subword_start(map, head), + SelectionGoal::None, + ) + }); } - pub fn delete_to_previous_word_boundary( + pub fn select_to_previous_word_start( &mut self, - _: &DeleteToPreviousWordBoundary, + _: &SelectToPreviousWordStart, cx: &mut ViewContext, ) { - self.start_transaction(cx); - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - if selection.is_empty() { - let head = selection.head().to_display_point(&display_map); - let cursor = - movement::prev_word_boundary(&display_map, head).to_point(&display_map); - selection.set_head(cursor); - selection.goal = SelectionGoal::None; - } - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - self.insert("", cx); - self.end_transaction(cx); + self.move_selection_heads(cx, |map, head, _| { + ( + movement::previous_word_start(map, head), + SelectionGoal::None, + ) + }); } - pub fn move_to_next_word_boundary( + pub fn select_to_previous_subword_start( &mut self, - _: &MoveToNextWordBoundary, + _: &SelectToPreviousSubwordStart, cx: &mut ViewContext, ) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let head = selection.head().to_display_point(&display_map); - let cursor = movement::next_word_boundary(&display_map, head).to_point(&display_map); - selection.start = cursor; - selection.end = cursor; - selection.reversed = false; - selection.goal = SelectionGoal::None; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); + self.move_selection_heads(cx, |map, head, _| { + ( + movement::previous_subword_start(map, head), + SelectionGoal::None, + ) + }); } - pub fn select_to_next_word_boundary( + pub fn delete_to_previous_word_start( &mut self, - _: &SelectToNextWordBoundary, + _: &DeleteToPreviousWordStart, cx: &mut ViewContext, ) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let head = selection.head().to_display_point(&display_map); - let cursor = movement::next_word_boundary(&display_map, head).to_point(&display_map); - selection.set_head(cursor); - selection.goal = SelectionGoal::None; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); + self.transact(cx, |this, cx| { + this.move_selections(cx, |map, selection| { + if selection.is_empty() { + let cursor = movement::previous_word_start(map, selection.head()); + selection.set_head(cursor, SelectionGoal::None); + } + }); + this.insert("", cx); + }); } - pub fn delete_to_next_word_boundary( + pub fn delete_to_previous_subword_start( &mut self, - _: &DeleteToNextWordBoundary, + _: &DeleteToPreviousSubwordStart, cx: &mut ViewContext, ) { - self.start_transaction(cx); - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - if selection.is_empty() { - let head = selection.head().to_display_point(&display_map); - let cursor = - movement::next_word_boundary(&display_map, head).to_point(&display_map); - selection.set_head(cursor); - selection.goal = SelectionGoal::None; - } - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - self.insert("", cx); - self.end_transaction(cx); + self.transact(cx, |this, cx| { + this.move_selections(cx, |map, selection| { + if selection.is_empty() { + let cursor = movement::previous_subword_start(map, selection.head()); + selection.set_head(cursor, SelectionGoal::None); + } + }); + this.insert("", cx); + }); + } + + pub fn move_to_next_word_end(&mut self, _: &MoveToNextWordEnd, cx: &mut ViewContext) { + self.move_cursors(cx, |map, head, _| { + (movement::next_word_end(map, head), SelectionGoal::None) + }); + } + + pub fn move_to_next_subword_end( + &mut self, + _: &MoveToNextSubwordEnd, + cx: &mut ViewContext, + ) { + self.move_cursors(cx, |map, head, _| { + (movement::next_subword_end(map, head), SelectionGoal::None) + }); + } + + pub fn select_to_next_word_end(&mut self, _: &SelectToNextWordEnd, cx: &mut ViewContext) { + self.move_selection_heads(cx, |map, head, _| { + (movement::next_word_end(map, head), SelectionGoal::None) + }); + } + + pub fn select_to_next_subword_end( + &mut self, + _: &SelectToNextSubwordEnd, + cx: &mut ViewContext, + ) { + self.move_selection_heads(cx, |map, head, _| { + (movement::next_subword_end(map, head), SelectionGoal::None) + }); + } + + pub fn delete_to_next_word_end(&mut self, _: &DeleteToNextWordEnd, cx: &mut ViewContext) { + self.transact(cx, |this, cx| { + this.move_selections(cx, |map, selection| { + if selection.is_empty() { + let cursor = movement::next_word_end(map, selection.head()); + selection.set_head(cursor, SelectionGoal::None); + } + }); + this.insert("", cx); + }); + } + + pub fn delete_to_next_subword_end( + &mut self, + _: &DeleteToNextSubwordEnd, + cx: &mut ViewContext, + ) { + self.transact(cx, |this, cx| { + this.move_selections(cx, |map, selection| { + if selection.is_empty() { + let cursor = movement::next_subword_end(map, selection.head()); + selection.set_head(cursor, SelectionGoal::None); + } + }); + this.insert("", cx); + }); } pub fn move_to_beginning_of_line( @@ -3574,18 +3826,12 @@ impl Editor { _: &MoveToBeginningOfLine, cx: &mut ViewContext, ) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let head = selection.head().to_display_point(&display_map); - let new_head = movement::line_beginning(&display_map, head, true); - let cursor = new_head.to_point(&display_map); - selection.start = cursor; - selection.end = cursor; - selection.reversed = false; - selection.goal = SelectionGoal::None; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); + self.move_cursors(cx, |map, head, _| { + ( + movement::line_beginning(map, head, true), + SelectionGoal::None, + ) + }); } pub fn select_to_beginning_of_line( @@ -3593,15 +3839,12 @@ impl Editor { SelectToBeginningOfLine(stop_at_soft_boundaries): &SelectToBeginningOfLine, cx: &mut ViewContext, ) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let head = selection.head().to_display_point(&display_map); - let new_head = movement::line_beginning(&display_map, head, *stop_at_soft_boundaries); - selection.set_head(new_head.to_point(&display_map)); - selection.goal = SelectionGoal::None; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); + self.move_selection_heads(cx, |map, head, _| { + ( + movement::line_beginning(map, head, *stop_at_soft_boundaries), + SelectionGoal::None, + ) + }); } pub fn delete_to_beginning_of_line( @@ -3609,27 +3852,16 @@ impl Editor { _: &DeleteToBeginningOfLine, cx: &mut ViewContext, ) { - self.start_transaction(cx); - self.select_to_beginning_of_line(&SelectToBeginningOfLine(false), cx); - self.backspace(&Backspace, cx); - self.end_transaction(cx); + self.transact(cx, |this, cx| { + this.select_to_beginning_of_line(&SelectToBeginningOfLine(false), cx); + this.backspace(&Backspace, cx); + }); } - pub fn move_to_end_of_line(&mut self, _: &MoveToEndOfLine, cx: &mut ViewContext) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - { - for selection in &mut selections { - let head = selection.head().to_display_point(&display_map); - let new_head = movement::line_end(&display_map, head, true); - let anchor = new_head.to_point(&display_map); - selection.start = anchor.clone(); - selection.end = anchor; - selection.reversed = false; - selection.goal = SelectionGoal::None; - } - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); + pub fn move_to_end_of_line(&mut self, _: &MoveToEndOfLine, cx: &mut ViewContext) { + self.move_cursors(cx, |map, head, _| { + (movement::line_end(map, head, true), SelectionGoal::None) + }); } pub fn select_to_end_of_line( @@ -3637,29 +3869,26 @@ impl Editor { SelectToEndOfLine(stop_at_soft_boundaries): &SelectToEndOfLine, cx: &mut ViewContext, ) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let head = selection.head().to_display_point(&display_map); - let new_head = movement::line_end(&display_map, head, *stop_at_soft_boundaries); - selection.set_head(new_head.to_point(&display_map)); - selection.goal = SelectionGoal::None; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); + self.move_selection_heads(cx, |map, head, _| { + ( + movement::line_end(map, head, *stop_at_soft_boundaries), + SelectionGoal::None, + ) + }); } pub fn delete_to_end_of_line(&mut self, _: &DeleteToEndOfLine, cx: &mut ViewContext) { - self.start_transaction(cx); - self.select_to_end_of_line(&SelectToEndOfLine(false), cx); - self.delete(&Delete, cx); - self.end_transaction(cx); + self.transact(cx, |this, cx| { + this.select_to_end_of_line(&SelectToEndOfLine(false), cx); + this.delete(&Delete, cx); + }); } pub fn cut_to_end_of_line(&mut self, _: &CutToEndOfLine, cx: &mut ViewContext) { - self.start_transaction(cx); - self.select_to_end_of_line(&SelectToEndOfLine(false), cx); - self.cut(&Cut, cx); - self.end_transaction(cx); + self.transact(cx, |this, cx| { + this.select_to_end_of_line(&SelectToEndOfLine(false), cx); + this.cut(&Cut, cx); + }); } pub fn move_to_beginning(&mut self, _: &MoveToBeginning, cx: &mut ViewContext) { @@ -3680,7 +3909,7 @@ impl Editor { pub fn select_to_beginning(&mut self, _: &SelectToBeginning, cx: &mut ViewContext) { let mut selection = self.local_selections::(cx).last().unwrap().clone(); - selection.set_head(Point::zero()); + selection.set_head(Point::zero(), SelectionGoal::None); self.update_selections(vec![selection], Some(Autoscroll::Fit), cx); } @@ -3737,7 +3966,7 @@ impl Editor { pub fn select_to_end(&mut self, _: &SelectToEnd, cx: &mut ViewContext) { let mut selection = self.local_selections::(cx).first().unwrap().clone(); - selection.set_head(self.buffer.read(cx).read(cx).len()); + selection.set_head(self.buffer.read(cx).read(cx).len(), SelectionGoal::None); self.update_selections(vec![selection], Some(Autoscroll::Fit), cx); } @@ -3796,7 +4025,7 @@ impl Editor { to_unfold.push(selection.start..selection.end); } } - self.unfold_ranges(to_unfold, cx); + self.unfold_ranges(to_unfold, true, cx); self.update_selections(new_selections, Some(Autoscroll::Fit), cx); } @@ -3809,6 +4038,7 @@ impl Editor { } fn add_selection(&mut self, above: bool, cx: &mut ViewContext) { + self.push_to_selection_history(); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut selections = self.local_selections::(cx); let mut state = self.add_selections_state.take().unwrap_or_else(|| { @@ -3895,13 +4125,14 @@ impl Editor { state.stack.pop(); } - self.update_selections(new_selections, Some(Autoscroll::Fit), cx); + self.update_selections(new_selections, Some(Autoscroll::Newest), cx); if state.stack.len() > 1 { self.add_selections_state = Some(state); } } pub fn select_next(&mut self, action: &SelectNext, cx: &mut ViewContext) { + self.push_to_selection_history(); let replace_newest = action.0; let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let buffer = &display_map.buffer_snapshot; @@ -3955,6 +4186,7 @@ impl Editor { reversed: false, goal: SelectionGoal::None, }); + self.unfold_ranges([next_selected_range], false, cx); self.update_selections(selections, Some(Autoscroll::Newest), cx); } else { select_next_state.done = true; @@ -3982,6 +4214,7 @@ impl Editor { wordwise: true, done: false, }; + self.unfold_ranges([selection.start..selection.end], false, cx); self.update_selections(selections, Some(Autoscroll::Newest), cx); self.select_next_state = Some(select_state); } else { @@ -4010,90 +4243,94 @@ impl Editor { let comment_prefix = full_comment_prefix.trim_end_matches(' '); let comment_prefix_whitespace = &full_comment_prefix[comment_prefix.len()..]; - self.start_transaction(cx); - let mut selections = self.local_selections::(cx); - let mut all_selection_lines_are_comments = true; - let mut edit_ranges = Vec::new(); - let mut last_toggled_row = None; - self.buffer.update(cx, |buffer, cx| { - for selection in &mut selections { - edit_ranges.clear(); - let snapshot = buffer.snapshot(cx); + self.transact(cx, |this, cx| { + let mut selections = this.local_selections::(cx); + let mut all_selection_lines_are_comments = true; + let mut edit_ranges = Vec::new(); + let mut last_toggled_row = None; + this.buffer.update(cx, |buffer, cx| { + for selection in &mut selections { + edit_ranges.clear(); + let snapshot = buffer.snapshot(cx); - let end_row = - if selection.end.row > selection.start.row && selection.end.column == 0 { - selection.end.row - } else { - selection.end.row + 1 - }; + let end_row = + if selection.end.row > selection.start.row && selection.end.column == 0 { + selection.end.row + } else { + selection.end.row + 1 + }; - for row in selection.start.row..end_row { - // If multiple selections contain a given row, avoid processing that - // row more than once. - if last_toggled_row == Some(row) { - continue; - } else { - last_toggled_row = Some(row); - } + for row in selection.start.row..end_row { + // If multiple selections contain a given row, avoid processing that + // row more than once. + if last_toggled_row == Some(row) { + continue; + } else { + last_toggled_row = Some(row); + } - if snapshot.is_line_blank(row) { - continue; - } + if snapshot.is_line_blank(row) { + continue; + } - let start = Point::new(row, snapshot.indent_column_for_line(row)); - let mut line_bytes = snapshot - .bytes_in_range(start..snapshot.max_point()) - .flatten() - .copied(); - - // If this line currently begins with the line comment prefix, then record - // the range containing the prefix. - if all_selection_lines_are_comments - && line_bytes - .by_ref() - .take(comment_prefix.len()) - .eq(comment_prefix.bytes()) - { - // Include any whitespace that matches the comment prefix. - let matching_whitespace_len = line_bytes - .zip(comment_prefix_whitespace.bytes()) - .take_while(|(a, b)| a == b) - .count() as u32; - let end = Point::new( - row, - start.column + comment_prefix.len() as u32 + matching_whitespace_len, - ); - edit_ranges.push(start..end); - } - // If this line does not begin with the line comment prefix, then record - // the position where the prefix should be inserted. - else { - all_selection_lines_are_comments = false; - edit_ranges.push(start..start); + let start = Point::new(row, snapshot.indent_column_for_line(row)); + let mut line_bytes = snapshot + .bytes_in_range(start..snapshot.max_point()) + .flatten() + .copied(); + + // If this line currently begins with the line comment prefix, then record + // the range containing the prefix. + if all_selection_lines_are_comments + && line_bytes + .by_ref() + .take(comment_prefix.len()) + .eq(comment_prefix.bytes()) + { + // Include any whitespace that matches the comment prefix. + let matching_whitespace_len = line_bytes + .zip(comment_prefix_whitespace.bytes()) + .take_while(|(a, b)| a == b) + .count() + as u32; + let end = Point::new( + row, + start.column + + comment_prefix.len() as u32 + + matching_whitespace_len, + ); + edit_ranges.push(start..end); + } + // If this line does not begin with the line comment prefix, then record + // the position where the prefix should be inserted. + else { + all_selection_lines_are_comments = false; + edit_ranges.push(start..start); + } } - } - if !edit_ranges.is_empty() { - if all_selection_lines_are_comments { - buffer.edit(edit_ranges.iter().cloned(), "", cx); - } else { - let min_column = edit_ranges.iter().map(|r| r.start.column).min().unwrap(); - let edit_ranges = edit_ranges.iter().map(|range| { - let position = Point::new(range.start.row, min_column); - position..position - }); - buffer.edit(edit_ranges, &full_comment_prefix, cx); + if !edit_ranges.is_empty() { + if all_selection_lines_are_comments { + buffer.edit(edit_ranges.iter().cloned(), "", cx); + } else { + let min_column = + edit_ranges.iter().map(|r| r.start.column).min().unwrap(); + let edit_ranges = edit_ranges.iter().map(|range| { + let position = Point::new(range.start.row, min_column); + position..position + }); + buffer.edit(edit_ranges, &full_comment_prefix, cx); + } } } - } - }); + }); - self.update_selections( - self.local_selections::(cx), - Some(Autoscroll::Fit), - cx, - ); - self.end_transaction(cx); + this.update_selections( + this.local_selections::(cx), + Some(Autoscroll::Fit), + cx, + ); + }); } pub fn select_larger_syntax_node( @@ -4180,6 +4417,30 @@ impl Editor { self.update_selections(selections, Some(Autoscroll::Fit), cx); } + pub fn undo_selection(&mut self, _: &UndoSelection, cx: &mut ViewContext) { + self.end_selection(cx); + self.selection_history.mode = SelectionHistoryMode::Undoing; + if let Some(entry) = self.selection_history.undo_stack.pop_back() { + self.set_selections(entry.selections, None, true, cx); + self.select_next_state = entry.select_next_state; + self.add_selections_state = entry.add_selections_state; + self.request_autoscroll(Autoscroll::Newest, cx); + } + self.selection_history.mode = SelectionHistoryMode::Normal; + } + + pub fn redo_selection(&mut self, _: &RedoSelection, cx: &mut ViewContext) { + self.end_selection(cx); + self.selection_history.mode = SelectionHistoryMode::Redoing; + if let Some(entry) = self.selection_history.redo_stack.pop_back() { + self.set_selections(entry.selections, None, true, cx); + self.select_next_state = entry.select_next_state; + self.add_selections_state = entry.add_selections_state; + self.request_autoscroll(Autoscroll::Newest, cx); + } + self.selection_history.mode = SelectionHistoryMode::Normal; + } + pub fn go_to_diagnostic( &mut self, &GoToDiagnostic(direction): &GoToDiagnostic, @@ -4211,7 +4472,7 @@ impl Editor { }; let group = diagnostics.find_map(|entry| { if entry.diagnostic.is_primary - && !entry.diagnostic.is_unnecessary + && entry.diagnostic.severity <= DiagnosticSeverity::WARNING && !entry.range.is_empty() && Some(entry.range.end) != active_primary_range.as_ref().map(|r| *r.end()) { @@ -4280,20 +4541,16 @@ impl Editor { return; }; - let definitions = workspace - .project() - .update(cx, |project, cx| project.definition(&buffer, head, cx)); + let project = workspace.project().clone(); + let definitions = project.update(cx, |project, cx| project.definition(&buffer, head, cx)); cx.spawn(|workspace, mut cx| async move { let definitions = definitions.await?; workspace.update(&mut cx, |workspace, cx| { let nav_history = workspace.active_pane().read(cx).nav_history().clone(); for definition in definitions { let range = definition.range.to_offset(definition.buffer.read(cx)); - let target_editor_handle = workspace - .open_item(BufferItemHandle(definition.buffer), cx) - .downcast::() - .unwrap(); + let target_editor_handle = workspace.open_project_item(definition.buffer, cx); target_editor_handle.update(cx, |target_editor, cx| { // When selecting a definition in a different buffer, disable the nav history // to avoid creating a history entry at the previous cursor location. @@ -4324,9 +4581,8 @@ impl Editor { let (buffer, head) = editor.buffer.read(cx).text_anchor_for_position(head, cx)?; let replica_id = editor.replica_id(cx); - let references = workspace - .project() - .update(cx, |project, cx| project.references(&buffer, head, cx)); + let project = workspace.project().clone(); + let references = project.update(cx, |project, cx| project.references(&buffer, head, cx)); Some(cx.spawn(|workspace, mut cx| async move { let mut locations = references.await?; if locations.is_empty() { @@ -4370,13 +4626,13 @@ impl Editor { }); workspace.update(&mut cx, |workspace, cx| { - let editor = workspace.open_item(MultiBufferItemHandle(excerpt_buffer), cx); - if let Some(editor) = editor.act_as::(cx) { - editor.update(cx, |editor, cx| { - let color = editor.style(cx).highlighted_line_background; - editor.highlight_background::(ranges_to_highlight, color, cx); - }); - } + let editor = + cx.add_view(|cx| Editor::for_multibuffer(excerpt_buffer, Some(project), cx)); + editor.update(cx, |editor, cx| { + let color = editor.style(cx).highlighted_line_background; + editor.highlight_background::(ranges_to_highlight, color, cx); + }); + workspace.add_item(Box::new(editor), cx); }); Ok(()) @@ -4458,6 +4714,7 @@ impl Editor { .flat_map(|(_, ranges)| ranges), ) .collect(); + this.highlight_text::( ranges, HighlightStyle { @@ -4736,13 +4993,13 @@ impl Editor { let start_ix = match self .selections - .binary_search_by(|probe| probe.end.cmp(&range.start, &buffer).unwrap()) + .binary_search_by(|probe| probe.end.cmp(&range.start, &buffer)) { Ok(ix) | Err(ix) => ix, }; let end_ix = match self .selections - .binary_search_by(|probe| probe.start.cmp(&range.end, &buffer).unwrap()) + .binary_search_by(|probe| probe.start.cmp(&range.end, &buffer)) { Ok(ix) => ix + 1, Err(ix) => ix, @@ -4949,10 +5206,52 @@ impl Editor { } })), None, + true, cx, ); } + pub fn set_selections_from_remote( + &mut self, + mut selections: Vec>, + cx: &mut ViewContext, + ) { + let buffer = self.buffer.read(cx); + let buffer = buffer.read(cx); + selections.sort_by(|a, b| { + a.start + .cmp(&b.start, &*buffer) + .then_with(|| b.end.cmp(&a.end, &*buffer)) + }); + + // Merge overlapping selections + let mut i = 1; + while i < selections.len() { + if selections[i - 1] + .end + .cmp(&selections[i].start, &*buffer) + .is_ge() + { + let removed = selections.remove(i); + if removed + .start + .cmp(&selections[i - 1].start, &*buffer) + .is_lt() + { + selections[i - 1].start = removed.start; + } + if removed.end.cmp(&selections[i - 1].end, &*buffer).is_gt() { + selections[i - 1].end = removed.end; + } + } else { + i += 1; + } + } + + drop(buffer); + self.set_selections(selections.into(), None, false, cx); + } + /// Compute new ranges for any selections that were located in excerpts that have /// since been removed. /// @@ -4961,26 +5260,41 @@ impl Editor { /// the id of the new excerpt where the head of the selection has been moved. pub fn refresh_selections(&mut self, cx: &mut ViewContext) -> HashMap { let snapshot = self.buffer.read(cx).read(cx); + let mut selections_with_lost_position = HashMap::default(); + + let mut pending_selection = self.pending_selection.take(); + if let Some(pending) = pending_selection.as_mut() { + let anchors = + snapshot.refresh_anchors([&pending.selection.start, &pending.selection.end]); + let (_, start, kept_start) = anchors[0].clone(); + let (_, end, kept_end) = anchors[1].clone(); + let kept_head = if pending.selection.reversed { + kept_start + } else { + kept_end + }; + if !kept_head { + selections_with_lost_position.insert( + pending.selection.id, + pending.selection.head().excerpt_id.clone(), + ); + } + + pending.selection.start = start; + pending.selection.end = end; + } + let anchors_with_status = snapshot.refresh_anchors( self.selections .iter() .flat_map(|selection| [&selection.start, &selection.end]), ); - let offsets = - snapshot.summaries_for_anchors::(anchors_with_status.iter().map(|a| &a.1)); - assert_eq!(anchors_with_status.len(), 2 * self.selections.len()); - assert_eq!(offsets.len(), anchors_with_status.len()); - - let offsets = offsets.chunks(2); - let statuses = anchors_with_status + self.selections = anchors_with_status .chunks(2) - .map(|a| (a[0].0 / 2, a[0].2, a[1].2)); - - let mut selections_with_lost_position = HashMap::default(); - let new_selections = offsets - .zip(statuses) - .map(|(offsets, (selection_ix, kept_start, kept_end))| { - let selection = &self.selections[selection_ix]; + .map(|selection_anchors| { + let (anchor_ix, start, kept_start) = selection_anchors[0].clone(); + let (_, end, kept_end) = selection_anchors[1].clone(); + let selection = &self.selections[anchor_ix / 2]; let kept_head = if selection.reversed { kept_start } else { @@ -4993,15 +5307,21 @@ impl Editor { Selection { id: selection.id, - start: offsets[0], - end: offsets[1], + start, + end, reversed: selection.reversed, goal: selection.goal, } }) .collect(); drop(snapshot); - self.update_selections(new_selections, Some(Autoscroll::Fit), cx); + + let new_selections = self.local_selections::(cx); + if !new_selections.is_empty() { + self.update_selections(new_selections, Some(Autoscroll::Fit), cx); + } + self.pending_selection = pending_selection; + selections_with_lost_position } @@ -5009,6 +5329,7 @@ impl Editor { &mut self, selections: Arc<[Selection]>, pending_selection: Option, + local: bool, cx: &mut ViewContext, ) { assert!( @@ -5018,9 +5339,10 @@ impl Editor { let old_cursor_position = self.newest_anchor_selection().head(); + self.push_to_selection_history(); self.selections = selections; self.pending_selection = pending_selection; - if self.focused { + if self.focused && self.leader_replica_id.is_none() { self.buffer.update(cx, |buffer, cx| { buffer.set_active_selections(&self.selections, cx) }); @@ -5045,48 +5367,70 @@ impl Editor { cx, ); - let completion_menu = match self.context_menu.as_mut() { - Some(ContextMenu::Completions(menu)) => Some(menu), - _ => { - self.context_menu.take(); - None + if local { + let completion_menu = match self.context_menu.as_mut() { + Some(ContextMenu::Completions(menu)) => Some(menu), + _ => { + self.context_menu.take(); + None + } + }; + + if let Some(completion_menu) = completion_menu { + let cursor_position = new_cursor_position.to_offset(&buffer); + let (word_range, kind) = + buffer.surrounding_word(completion_menu.initial_position.clone()); + if kind == Some(CharKind::Word) + && word_range.to_inclusive().contains(&cursor_position) + { + let query = Self::completion_query(&buffer, cursor_position); + cx.background() + .block(completion_menu.filter(query.as_deref(), cx.background().clone())); + self.show_completions(&ShowCompletions, cx); + } else { + self.hide_context_menu(cx); + } } - }; - if let Some(completion_menu) = completion_menu { - let cursor_position = new_cursor_position.to_offset(&buffer); - let (word_range, kind) = - buffer.surrounding_word(completion_menu.initial_position.clone()); - if kind == Some(CharKind::Word) && word_range.to_inclusive().contains(&cursor_position) + if old_cursor_position.to_display_point(&display_map).row() + != new_cursor_position.to_display_point(&display_map).row() { - let query = Self::completion_query(&buffer, cursor_position); - cx.background() - .block(completion_menu.filter(query.as_deref(), cx.background().clone())); - self.show_completions(&ShowCompletions, cx); - } else { - self.hide_context_menu(cx); + self.available_code_actions.take(); } + self.refresh_code_actions(cx); + self.refresh_document_highlights(cx); } - if old_cursor_position.to_display_point(&display_map).row() - != new_cursor_position.to_display_point(&display_map).row() - { - self.available_code_actions.take(); - } - self.refresh_code_actions(cx); - self.refresh_document_highlights(cx); - self.pause_cursor_blinking(cx); - cx.emit(Event::SelectionsChanged); + cx.emit(Event::SelectionsChanged { local }); + } + + fn push_to_selection_history(&mut self) { + self.selection_history.push(SelectionHistoryEntry { + selections: self.selections.clone(), + select_next_state: self.select_next_state.clone(), + add_selections_state: self.add_selections_state.clone(), + }); } pub fn request_autoscroll(&mut self, autoscroll: Autoscroll, cx: &mut ViewContext) { - self.autoscroll_request = Some(autoscroll); + self.autoscroll_request = Some((autoscroll, true)); cx.notify(); } - fn start_transaction(&mut self, cx: &mut ViewContext) { + fn request_autoscroll_remotely(&mut self, autoscroll: Autoscroll, cx: &mut ViewContext) { + self.autoscroll_request = Some((autoscroll, false)); + cx.notify(); + } + + pub fn transact( + &mut self, + cx: &mut ViewContext, + update: impl FnOnce(&mut Self, &mut ViewContext), + ) { self.start_transaction_at(Instant::now(), cx); + update(self, cx); + self.end_transaction_at(Instant::now(), cx); } fn start_transaction_at(&mut self, now: Instant, cx: &mut ViewContext) { @@ -5096,24 +5440,22 @@ impl Editor { .update(cx, |buffer, cx| buffer.start_transaction_at(now, cx)) { self.selection_history - .insert(tx_id, (self.selections.clone(), None)); + .insert_transaction(tx_id, self.selections.clone()); } } - fn end_transaction(&mut self, cx: &mut ViewContext) { - self.end_transaction_at(Instant::now(), cx); - } - fn end_transaction_at(&mut self, now: Instant, cx: &mut ViewContext) { if let Some(tx_id) = self .buffer .update(cx, |buffer, cx| buffer.end_transaction_at(now, cx)) { - if let Some((_, end_selections)) = self.selection_history.get_mut(&tx_id) { + if let Some((_, end_selections)) = self.selection_history.transaction_mut(tx_id) { *end_selections = Some(self.selections.clone()); } else { log::error!("unexpectedly ended a transaction that wasn't started by this editor"); } + + cx.emit(Event::Edited); } } @@ -5150,7 +5492,7 @@ impl Editor { self.fold_ranges(fold_ranges, cx); } - pub fn unfold(&mut self, _: &Unfold, cx: &mut ViewContext) { + pub fn unfold_lines(&mut self, _: &UnfoldLines, cx: &mut ViewContext) { let selections = self.local_selections::(cx); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let buffer = &display_map.buffer_snapshot; @@ -5165,7 +5507,7 @@ impl Editor { start..end }) .collect::>(); - self.unfold_ranges(ranges, cx); + self.unfold_ranges(ranges, true, cx); } fn is_line_foldable(&self, display_map: &DisplaySnapshot, display_row: u32) -> bool { @@ -5216,7 +5558,7 @@ impl Editor { self.fold_ranges(ranges, cx); } - fn fold_ranges( + pub fn fold_ranges( &mut self, ranges: impl IntoIterator>, cx: &mut ViewContext, @@ -5229,10 +5571,16 @@ impl Editor { } } - fn unfold_ranges(&mut self, ranges: Vec>, cx: &mut ViewContext) { - if !ranges.is_empty() { + pub fn unfold_ranges( + &mut self, + ranges: impl IntoIterator>, + inclusive: bool, + cx: &mut ViewContext, + ) { + let mut ranges = ranges.into_iter().peekable(); + if ranges.peek().is_some() { self.display_map - .update(cx, |map, cx| map.unfold(ranges, cx)); + .update(cx, |map, cx| map.unfold(ranges, inclusive, cx)); self.request_autoscroll(Autoscroll::Fit, cx); cx.notify(); } @@ -5283,11 +5631,13 @@ impl Editor { } pub fn set_text(&mut self, text: impl Into, cx: &mut ViewContext) { - self.buffer - .read(cx) - .as_singleton() - .expect("you can only call set_text on editors for singleton buffers") - .update(cx, |buffer, cx| buffer.set_text(text, cx)); + self.transact(cx, |this, cx| { + this.buffer + .read(cx) + .as_singleton() + .expect("you can only call set_text on editors for singleton buffers") + .update(cx, |buffer, cx| buffer.set_text(text, cx)); + }); } pub fn display_text(&self, cx: &mut MutableAppContext) -> String { @@ -5298,7 +5648,7 @@ impl Editor { pub fn soft_wrap_mode(&self, cx: &AppContext) -> SoftWrap { let language = self.language(cx); - let settings = cx.app_state::(); + let settings = cx.global::(); let mode = self .soft_wrap_mode_override .unwrap_or_else(|| settings.soft_wrap(language)); @@ -5375,7 +5725,7 @@ impl Editor { let buffer = &display_snapshot.buffer_snapshot; for (color, ranges) in self.background_highlights.values() { let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe.end.cmp(&search_range.start, &buffer).unwrap(); + let cmp = probe.end.cmp(&search_range.start, &buffer); if cmp.is_gt() { Ordering::Greater } else { @@ -5385,7 +5735,7 @@ impl Editor { Ok(i) | Err(i) => i, }; for range in &ranges[start_ix..] { - if range.start.cmp(&search_range.end, &buffer).unwrap().is_ge() { + if range.start.cmp(&search_range.end, &buffer).is_ge() { break; } let start = range @@ -5476,7 +5826,7 @@ impl Editor { } pub fn show_local_cursors(&self) -> bool { - self.show_local_cursors + self.show_local_cursors && self.focused } fn on_buffer_changed(&mut self, _: ModelHandle, cx: &mut ViewContext) { @@ -5493,7 +5843,7 @@ impl Editor { language::Event::Edited => { self.refresh_active_diagnostics(cx); self.refresh_code_actions(cx); - cx.emit(Event::Edited); + cx.emit(Event::BufferEdited); } language::Event::Dirtied => cx.emit(Event::Dirtied), language::Event::Saved => cx.emit(Event::Saved), @@ -5563,17 +5913,10 @@ impl Editor { // and activating a new item causes the pane to call a method on us reentrantly, // which panics if we're on the stack. cx.defer(move |workspace, cx| { - for (ix, (buffer, ranges)) in new_selections_by_buffer.into_iter().enumerate() { - let buffer = BufferItemHandle(buffer); - if ix == 0 && !workspace.activate_pane_for_item(&buffer, cx) { - workspace.activate_next_pane(cx); - } - - let editor = workspace - .open_item(buffer, cx) - .downcast::() - .unwrap(); + workspace.activate_next_pane(cx); + for (buffer, ranges) in new_selections_by_buffer.into_iter() { + let editor = workspace.open_project_item::(buffer, cx); editor.update(cx, |editor, cx| { editor.select_ranges(ranges, Some(Autoscroll::Newest), cx); }); @@ -5613,10 +5956,10 @@ impl Deref for EditorSnapshot { fn compute_scroll_position( snapshot: &DisplaySnapshot, mut scroll_position: Vector2F, - scroll_top_anchor: &Option, + scroll_top_anchor: &Anchor, ) -> Vector2F { - if let Some(anchor) = scroll_top_anchor { - let scroll_top = anchor.to_display_point(snapshot).row() as f32; + if *scroll_top_anchor != Anchor::min() { + let scroll_top = scroll_top_anchor.to_display_point(snapshot).row() as f32; scroll_position.set_y(scroll_top + scroll_position.y()); } else { scroll_position.set_y(0.); @@ -5624,20 +5967,30 @@ fn compute_scroll_position( scroll_position } -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum Event { Activate, + BufferEdited, Edited, Blurred, Dirtied, Saved, TitleChanged, - SelectionsChanged, + SelectionsChanged { local: bool }, + ScrollPositionChanged { local: bool }, Closed, } +pub struct EditorFocused(pub ViewHandle); +pub struct EditorBlurred(pub ViewHandle); +pub struct EditorReleased(pub WeakViewHandle); + impl Entity for Editor { type Event = Event; + + fn release(&mut self, cx: &mut MutableAppContext) { + cx.emit_global(EditorReleased(self.handle.clone())); + } } impl View for Editor { @@ -5654,6 +6007,8 @@ impl View for Editor { } fn on_focus(&mut self, cx: &mut ViewContext) { + let focused_event = EditorFocused(cx.handle()); + cx.emit_global(focused_event); if let Some(rename) = self.pending_rename.as_ref() { cx.focus(&rename.editor); } else { @@ -5661,12 +6016,16 @@ impl View for Editor { self.blink_cursors(self.blink_epoch, cx); self.buffer.update(cx, |buffer, 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); + } }); } } fn on_blur(&mut self, cx: &mut ViewContext) { + let blurred_event = EditorBlurred(cx.handle()); + cx.emit_global(blurred_event); self.focused = false; self.buffer .update(cx, |buffer, cx| buffer.remove_active_selections(cx)); @@ -5676,26 +6035,31 @@ impl View for Editor { } fn keymap_context(&self, _: &AppContext) -> gpui::keymap::Context { - let mut cx = Self::default_keymap_context(); + let mut context = Self::default_keymap_context(); let mode = match self.mode { EditorMode::SingleLine => "single_line", EditorMode::AutoHeight { .. } => "auto_height", EditorMode::Full => "full", }; - cx.map.insert("mode".into(), mode.into()); + context.map.insert("mode".into(), mode.into()); if self.pending_rename.is_some() { - cx.set.insert("renaming".into()); + context.set.insert("renaming".into()); } match self.context_menu.as_ref() { Some(ContextMenu::Completions(_)) => { - cx.set.insert("showing_completions".into()); + context.set.insert("showing_completions".into()); } Some(ContextMenu::CodeActions(_)) => { - cx.set.insert("showing_code_actions".into()); + context.set.insert("showing_code_actions".into()); } None => {} } - cx + + for layer in self.keymap_context_layers.values() { + context.extend(layer); + } + + context } } @@ -5758,6 +6122,14 @@ fn build_style( style } +trait SelectionExt { + fn offset_range(&self, buffer: &MultiBufferSnapshot) -> Range; + fn point_range(&self, buffer: &MultiBufferSnapshot) -> Range; + fn display_range(&self, map: &DisplaySnapshot) -> Range; + fn spanned_rows(&self, include_end_if_at_line_start: bool, map: &DisplaySnapshot) + -> Range; +} + impl SelectionExt for Selection { fn point_range(&self, buffer: &MultiBufferSnapshot) -> Range { let start = self.start.to_point(buffer); @@ -5887,7 +6259,7 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend } Arc::new(move |cx: &BlockContext| { - let settings = cx.app_state::(); + let settings = cx.global::(); let theme = &settings.theme.editor; let style = diagnostic_style(diagnostic.severity, is_valid, theme); let font_size = (style.text_scale_factor * settings.buffer_font_size).round(); @@ -6066,7 +6438,12 @@ pub fn styled_runs_for_code_label<'a>( #[cfg(test)] mod tests { + use super::*; + use gpui::{ + geometry::rect::RectF, + platform::{WindowBounds, WindowOptions}, + }; use language::{LanguageConfig, LanguageServerConfig}; use lsp::FakeLanguageServer; use project::FakeFs; @@ -6074,7 +6451,116 @@ mod tests { use std::{cell::RefCell, rc::Rc, time::Instant}; use text::Point; use unindent::Unindent; - use util::test::sample_text; + use util::test::{marked_text_by, marked_text_ranges, sample_text}; + use workspace::{FollowableItem, ItemHandle}; + + #[gpui::test] + fn test_edit_events(cx: &mut MutableAppContext) { + populate_settings(cx); + let buffer = cx.add_model(|cx| language::Buffer::new(0, "123456", cx)); + + let events = Rc::new(RefCell::new(Vec::new())); + let (_, editor1) = cx.add_window(Default::default(), { + let events = events.clone(); + |cx| { + cx.subscribe(&cx.handle(), move |_, _, event, _| { + if matches!(event, Event::Edited | Event::BufferEdited | Event::Dirtied) { + events.borrow_mut().push(("editor1", *event)); + } + }) + .detach(); + Editor::for_buffer(buffer.clone(), None, cx) + } + }); + let (_, editor2) = cx.add_window(Default::default(), { + let events = events.clone(); + |cx| { + cx.subscribe(&cx.handle(), move |_, _, event, _| { + if matches!(event, Event::Edited | Event::BufferEdited | Event::Dirtied) { + events.borrow_mut().push(("editor2", *event)); + } + }) + .detach(); + Editor::for_buffer(buffer.clone(), None, cx) + } + }); + assert_eq!(mem::take(&mut *events.borrow_mut()), []); + + // Mutating editor 1 will emit an `Edited` event only for that editor. + editor1.update(cx, |editor, cx| editor.insert("X", cx)); + assert_eq!( + mem::take(&mut *events.borrow_mut()), + [ + ("editor1", Event::Edited), + ("editor1", Event::BufferEdited), + ("editor2", Event::BufferEdited), + ("editor1", Event::Dirtied), + ("editor2", Event::Dirtied) + ] + ); + + // Mutating editor 2 will emit an `Edited` event only for that editor. + editor2.update(cx, |editor, cx| editor.delete(&Delete, cx)); + assert_eq!( + mem::take(&mut *events.borrow_mut()), + [ + ("editor2", Event::Edited), + ("editor1", Event::BufferEdited), + ("editor2", Event::BufferEdited), + ] + ); + + // Undoing on editor 1 will emit an `Edited` event only for that editor. + editor1.update(cx, |editor, cx| editor.undo(&Undo, cx)); + assert_eq!( + mem::take(&mut *events.borrow_mut()), + [ + ("editor1", Event::Edited), + ("editor1", Event::BufferEdited), + ("editor2", Event::BufferEdited), + ] + ); + + // Redoing on editor 1 will emit an `Edited` event only for that editor. + editor1.update(cx, |editor, cx| editor.redo(&Redo, cx)); + assert_eq!( + mem::take(&mut *events.borrow_mut()), + [ + ("editor1", Event::Edited), + ("editor1", Event::BufferEdited), + ("editor2", Event::BufferEdited), + ] + ); + + // Undoing on editor 2 will emit an `Edited` event only for that editor. + editor2.update(cx, |editor, cx| editor.undo(&Undo, cx)); + assert_eq!( + mem::take(&mut *events.borrow_mut()), + [ + ("editor2", Event::Edited), + ("editor1", Event::BufferEdited), + ("editor2", Event::BufferEdited), + ] + ); + + // Redoing on editor 2 will emit an `Edited` event only for that editor. + editor2.update(cx, |editor, cx| editor.redo(&Redo, cx)); + assert_eq!( + mem::take(&mut *events.borrow_mut()), + [ + ("editor2", Event::Edited), + ("editor1", Event::BufferEdited), + ("editor2", Event::BufferEdited), + ] + ); + + // No event is emitted when the mutation is a no-op. + editor2.update(cx, |editor, cx| { + editor.select_ranges([0..0], None, cx); + editor.backspace(&Backspace, cx); + }); + assert_eq!(mem::take(&mut *events.borrow_mut()), []); + } #[gpui::test] fn test_undo_redo_with_selection_restoration(cx: &mut MutableAppContext) { @@ -6148,13 +6634,12 @@ mod tests { #[gpui::test] fn test_selection_with_mouse(cx: &mut gpui::MutableAppContext) { populate_settings(cx); - let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); - let (_, editor) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); + let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx); + let (_, editor) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); editor.update(cx, |view, cx| { view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx); }); - assert_eq!( editor.update(cx, |view, cx| view.selected_display_ranges(cx)), [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)] @@ -6246,7 +6731,7 @@ mod tests { #[gpui::test] fn test_navigation_history(cx: &mut gpui::MutableAppContext) { populate_settings(cx); - use workspace::ItemView; + use workspace::Item; let nav_history = Rc::new(RefCell::new(workspace::NavHistory::default())); let buffer = MultiBuffer::build_simple(&sample_text(30, 5, 'a'), cx); @@ -6265,11 +6750,12 @@ mod tests { editor.select_display_ranges(&[DisplayPoint::new(13, 0)..DisplayPoint::new(13, 3)], cx); let nav_entry = nav_history.borrow_mut().pop_backward().unwrap(); editor.navigate(nav_entry.data.unwrap(), cx); - assert_eq!(nav_entry.item_view.id(), cx.view_id()); + assert_eq!(nav_entry.item.id(), cx.view_id()); assert_eq!( editor.selected_display_ranges(cx), &[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)] ); + assert!(nav_history.borrow_mut().pop_backward().is_none()); // Move the cursor a small distance via the mouse. // Nothing is added to the navigation history. @@ -6291,11 +6777,12 @@ mod tests { ); let nav_entry = nav_history.borrow_mut().pop_backward().unwrap(); editor.navigate(nav_entry.data.unwrap(), cx); - assert_eq!(nav_entry.item_view.id(), cx.view_id()); + assert_eq!(nav_entry.item.id(), cx.view_id()); assert_eq!( editor.selected_display_ranges(cx), &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)] ); + assert!(nav_history.borrow_mut().pop_backward().is_none()); editor }); @@ -6400,7 +6887,7 @@ mod tests { .unindent(), ); - view.unfold(&Unfold, cx); + view.unfold_lines(&UnfoldLines, cx); assert_eq!( view.display_text(cx), " @@ -6421,7 +6908,7 @@ mod tests { .unindent(), ); - view.unfold(&Unfold, cx); + view.unfold_lines(&UnfoldLines, cx); assert_eq!(view.display_text(cx), buffer.read(cx).read(cx).text()); }); } @@ -6800,127 +7287,94 @@ mod tests { ], cx, ); - }); - view.update(cx, |view, cx| { - view.move_to_previous_word_boundary(&MoveToPreviousWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 9)..DisplayPoint::new(0, 9), - DisplayPoint::new(2, 3)..DisplayPoint::new(2, 3), - ] + view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); + assert_selection_ranges( + "use std::<>str::{foo, bar}\n\n {[]baz.qux()}", + vec![('<', '>'), ('[', ']')], + view, + cx, ); - }); - view.update(cx, |view, cx| { - view.move_to_previous_word_boundary(&MoveToPreviousWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 7)..DisplayPoint::new(0, 7), - DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2), - ] + view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); + assert_selection_ranges( + "use std<>::str::{foo, bar}\n\n []{baz.qux()}", + vec![('<', '>'), ('[', ']')], + view, + cx, ); - }); - view.update(cx, |view, cx| { - view.move_to_previous_word_boundary(&MoveToPreviousWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 4)..DisplayPoint::new(0, 4), - DisplayPoint::new(2, 0)..DisplayPoint::new(2, 0), - ] + view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); + assert_selection_ranges( + "use <>std::str::{foo, bar}\n\n[] {baz.qux()}", + vec![('<', '>'), ('[', ']')], + view, + cx, ); - }); - view.update(cx, |view, cx| { - view.move_to_previous_word_boundary(&MoveToPreviousWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), - ] + view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); + assert_selection_ranges( + "<>use std::str::{foo, bar}\n[]\n {baz.qux()}", + vec![('<', '>'), ('[', ']')], + view, + cx, ); - }); - - view.update(cx, |view, cx| { - view.move_to_previous_word_boundary(&MoveToPreviousWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), - DisplayPoint::new(0, 23)..DisplayPoint::new(0, 23), - ] + + view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); + assert_selection_ranges( + "<>use std::str::{foo, bar[]}\n\n {baz.qux()}", + vec![('<', '>'), ('[', ']')], + view, + cx, ); - }); - view.update(cx, |view, cx| { - view.move_to_next_word_boundary(&MoveToNextWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), - DisplayPoint::new(0, 24)..DisplayPoint::new(0, 24), - ] + view.move_to_next_word_end(&MoveToNextWordEnd, cx); + assert_selection_ranges( + "use<> std::str::{foo, bar}[]\n\n {baz.qux()}", + vec![('<', '>'), ('[', ']')], + view, + cx, ); - }); - view.update(cx, |view, cx| { - view.move_to_next_word_boundary(&MoveToNextWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 7)..DisplayPoint::new(0, 7), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), - ] + view.move_to_next_word_end(&MoveToNextWordEnd, cx); + assert_selection_ranges( + "use std<>::str::{foo, bar}\n[]\n {baz.qux()}", + vec![('<', '>'), ('[', ']')], + view, + cx, ); - }); - view.update(cx, |view, cx| { - view.move_to_next_word_boundary(&MoveToNextWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 9)..DisplayPoint::new(0, 9), - DisplayPoint::new(2, 3)..DisplayPoint::new(2, 3), - ] + view.move_to_next_word_end(&MoveToNextWordEnd, cx); + assert_selection_ranges( + "use std::<>str::{foo, bar}\n\n {[]baz.qux()}", + vec![('<', '>'), ('[', ']')], + view, + cx, ); - }); - view.update(cx, |view, cx| { view.move_right(&MoveRight, cx); - view.select_to_previous_word_boundary(&SelectToPreviousWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 10)..DisplayPoint::new(0, 9), - DisplayPoint::new(2, 4)..DisplayPoint::new(2, 3), - ] + view.select_to_previous_word_start(&SelectToPreviousWordStart, cx); + assert_selection_ranges( + "use std::>s'), ('[', ']')], + view, + cx, ); - }); - view.update(cx, |view, cx| { - view.select_to_previous_word_boundary(&SelectToPreviousWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 10)..DisplayPoint::new(0, 7), - DisplayPoint::new(2, 4)..DisplayPoint::new(2, 2), - ] + view.select_to_previous_word_start(&SelectToPreviousWordStart, cx); + assert_selection_ranges( + "use std>::s'), ('[', ']')], + view, + cx, ); - }); - view.update(cx, |view, cx| { - view.select_to_next_word_boundary(&SelectToNextWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 10)..DisplayPoint::new(0, 9), - DisplayPoint::new(2, 4)..DisplayPoint::new(2, 3), - ] + view.select_to_next_word_end(&SelectToNextWordEnd, cx); + assert_selection_ranges( + "use std::>s'), ('[', ']')], + view, + cx, ); }); } @@ -6940,37 +7394,37 @@ mod tests { view.select_display_ranges(&[DisplayPoint::new(1, 7)..DisplayPoint::new(1, 7)], cx); - view.move_to_next_word_boundary(&MoveToNextWordBoundary, cx); + view.move_to_next_word_end(&MoveToNextWordEnd, cx); assert_eq!( view.selected_display_ranges(cx), &[DisplayPoint::new(1, 9)..DisplayPoint::new(1, 9)] ); - view.move_to_next_word_boundary(&MoveToNextWordBoundary, cx); + view.move_to_next_word_end(&MoveToNextWordEnd, cx); assert_eq!( view.selected_display_ranges(cx), &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)] ); - view.move_to_next_word_boundary(&MoveToNextWordBoundary, cx); + view.move_to_next_word_end(&MoveToNextWordEnd, cx); assert_eq!( view.selected_display_ranges(cx), &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)] ); - view.move_to_next_word_boundary(&MoveToNextWordBoundary, cx); + view.move_to_next_word_end(&MoveToNextWordEnd, cx); assert_eq!( view.selected_display_ranges(cx), &[DisplayPoint::new(2, 8)..DisplayPoint::new(2, 8)] ); - view.move_to_previous_word_boundary(&MoveToPreviousWordBoundary, cx); + view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); assert_eq!( view.selected_display_ranges(cx), &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)] ); - view.move_to_previous_word_boundary(&MoveToPreviousWordBoundary, cx); + view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); assert_eq!( view.selected_display_ranges(cx), &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)] @@ -6994,7 +7448,7 @@ mod tests { ], cx, ); - view.delete_to_previous_word_boundary(&DeleteToPreviousWordBoundary, cx); + view.delete_to_previous_word_start(&DeleteToPreviousWordStart, cx); }); assert_eq!(buffer.read(cx).read(cx).text(), "e two te four"); @@ -7009,7 +7463,7 @@ mod tests { ], cx, ); - view.delete_to_next_word_boundary(&DeleteToNextWordBoundary, cx); + view.delete_to_next_word_end(&DeleteToNextWordEnd, cx); }); assert_eq!(buffer.read(cx).read(cx).text(), "e t te our"); @@ -7165,7 +7619,7 @@ mod tests { ); // indent from mid-tabstop to full tabstop - view.tab(&Tab, cx); + view.tab(&Tab(Direction::Next), cx); assert_eq!(view.text(cx), " one two\nthree\n four"); assert_eq!( view.selected_display_ranges(cx), @@ -7176,7 +7630,7 @@ mod tests { ); // outdent from 1 tabstop to 0 tabstops - view.outdent(&Outdent, cx); + view.tab(&Tab(Direction::Prev), cx); assert_eq!(view.text(cx), "one two\nthree\n four"); assert_eq!( view.selected_display_ranges(cx), @@ -7190,13 +7644,13 @@ mod tests { view.select_display_ranges(&[DisplayPoint::new(1, 1)..DisplayPoint::new(2, 0)], cx); // indent and outdent affect only the preceding line - view.tab(&Tab, cx); + view.tab(&Tab(Direction::Next), cx); assert_eq!(view.text(cx), "one two\n three\n four"); assert_eq!( view.selected_display_ranges(cx), &[DisplayPoint::new(1, 5)..DisplayPoint::new(2, 0)] ); - view.outdent(&Outdent, cx); + view.tab(&Tab(Direction::Prev), cx); assert_eq!(view.text(cx), "one two\nthree\n four"); assert_eq!( view.selected_display_ranges(cx), @@ -7205,7 +7659,7 @@ mod tests { // Ensure that indenting/outdenting works when the cursor is at column 0. view.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx); - view.tab(&Tab, cx); + view.tab(&Tab(Direction::Next), cx); assert_eq!(view.text(cx), "one two\n three\n four"); assert_eq!( view.selected_display_ranges(cx), @@ -7213,7 +7667,7 @@ mod tests { ); view.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx); - view.outdent(&Outdent, cx); + view.tab(&Tab(Direction::Prev), cx); assert_eq!(view.text(cx), "one two\nthree\n four"); assert_eq!( view.selected_display_ranges(cx), @@ -7792,6 +8246,21 @@ mod tests { view.selected_display_ranges(cx), vec![DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)] ); + + view.undo_selection(&UndoSelection, cx); + assert_eq!( + view.selected_display_ranges(cx), + vec![ + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3) + ] + ); + + view.redo_selection(&RedoSelection, cx); + assert_eq!( + view.selected_display_ranges(cx), + vec![DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)] + ); }); view.update(cx, |view, cx| { @@ -7924,6 +8393,36 @@ mod tests { }); } + #[gpui::test] + fn test_select_next(cx: &mut gpui::MutableAppContext) { + populate_settings(cx); + + let (text, ranges) = marked_text_ranges("[abc]\n[abc] [abc]\ndefabc\n[abc]"); + let buffer = MultiBuffer::build_simple(&text, cx); + let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); + + view.update(cx, |view, cx| { + view.select_ranges([ranges[1].start + 1..ranges[1].start + 1], None, cx); + view.select_next(&SelectNext(false), cx); + assert_eq!(view.selected_ranges(cx), &ranges[1..2]); + + view.select_next(&SelectNext(false), cx); + assert_eq!(view.selected_ranges(cx), &ranges[1..3]); + + view.undo_selection(&UndoSelection, cx); + assert_eq!(view.selected_ranges(cx), &ranges[1..2]); + + view.redo_selection(&RedoSelection, cx); + assert_eq!(view.selected_ranges(cx), &ranges[1..3]); + + view.select_next(&SelectNext(false), cx); + assert_eq!(view.selected_ranges(cx), &ranges[1..4]); + + view.select_next(&SelectNext(false), cx); + assert_eq!(view.selected_ranges(cx), &ranges[0..4]); + }); + } + #[gpui::test] async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { cx.update(populate_settings); @@ -8376,6 +8875,94 @@ mod tests { }); } + #[gpui::test] + async fn test_format_during_save(cx: &mut gpui::TestAppContext) { + cx.foreground().forbid_parking(); + cx.update(populate_settings); + + let (mut language_server_config, mut fake_servers) = LanguageServerConfig::fake(); + language_server_config.set_fake_capabilities(lsp::ServerCapabilities { + document_formatting_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }); + let language = Arc::new(Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + language_server: Some(language_server_config), + ..Default::default() + }, + Some(tree_sitter_rust::language()), + )); + + let fs = FakeFs::new(cx.background().clone()); + fs.insert_file("/file.rs", Default::default()).await; + + let project = Project::test(fs, cx); + project.update(cx, |project, _| project.languages().add(language)); + + let worktree_id = project + .update(cx, |project, cx| { + project.find_or_create_local_worktree("/file.rs", true, cx) + }) + .await + .unwrap() + .0 + .read_with(cx, |tree, _| tree.id()); + let buffer = project + .update(cx, |project, cx| project.open_buffer((worktree_id, ""), cx)) + .await + .unwrap(); + let mut fake_server = fake_servers.next().await.unwrap(); + + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + let (_, editor) = cx.add_window(|cx| build_editor(buffer, cx)); + editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); + assert!(cx.read(|cx| editor.is_dirty(cx))); + + let save = cx.update(|cx| editor.save(project.clone(), cx)); + fake_server + .handle_request::(move |params, _| async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/file.rs").unwrap() + ); + Some(vec![lsp::TextEdit::new( + lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)), + ", ".to_string(), + )]) + }) + .next() + .await; + save.await.unwrap(); + assert_eq!( + editor.read_with(cx, |editor, cx| editor.text(cx)), + "one, two\nthree\n" + ); + assert!(!cx.read(|cx| editor.is_dirty(cx))); + + editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); + assert!(cx.read(|cx| editor.is_dirty(cx))); + + // Ensure we can still save even if formatting hangs. + fake_server.handle_request::(move |params, _| async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/file.rs").unwrap() + ); + futures::future::pending::<()>().await; + unreachable!() + }); + let save = cx.update(|cx| editor.save(project.clone(), cx)); + cx.foreground().advance_clock(items::FORMAT_TIMEOUT); + save.await.unwrap(); + assert_eq!( + editor.read_with(cx, |editor, cx| editor.text(cx)), + "one\ntwo\nthree\n" + ); + assert!(!cx.read(|cx| editor.is_dirty(cx))); + } + #[gpui::test] async fn test_completion(cx: &mut gpui::TestAppContext) { cx.update(populate_settings); @@ -8557,31 +9144,34 @@ mod tests { position: Point, completions: Vec<(Range, &'static str)>, ) { - fake.handle_request::(move |params, _| { - assert_eq!( - params.text_document_position.text_document.uri, - lsp::Url::from_file_path(path).unwrap() - ); - assert_eq!( - params.text_document_position.position, - lsp::Position::new(position.row, position.column) - ); - Some(lsp::CompletionResponse::Array( - completions - .iter() - .map(|(range, new_text)| lsp::CompletionItem { - label: new_text.to_string(), - text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { - range: lsp::Range::new( - lsp::Position::new(range.start.row, range.start.column), - lsp::Position::new(range.start.row, range.start.column), - ), - new_text: new_text.to_string(), - })), - ..Default::default() - }) - .collect(), - )) + fake.handle_request::(move |params, _| { + let completions = completions.clone(); + async move { + assert_eq!( + params.text_document_position.text_document.uri, + lsp::Url::from_file_path(path).unwrap() + ); + assert_eq!( + params.text_document_position.position, + lsp::Position::new(position.row, position.column) + ); + Some(lsp::CompletionResponse::Array( + completions + .iter() + .map(|(range, new_text)| lsp::CompletionItem { + label: new_text.to_string(), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: lsp::Range::new( + lsp::Position::new(range.start.row, range.start.column), + lsp::Position::new(range.start.row, range.start.column), + ), + new_text: new_text.to_string(), + })), + ..Default::default() + }) + .collect(), + )) + } }) .next() .await; @@ -8591,18 +9181,21 @@ mod tests { fake: &mut FakeLanguageServer, edit: Option<(Range, &'static str)>, ) { - fake.handle_request::(move |_, _| { - lsp::CompletionItem { - additional_text_edits: edit.clone().map(|(range, new_text)| { - vec![lsp::TextEdit::new( - lsp::Range::new( - lsp::Position::new(range.start.row, range.start.column), - lsp::Position::new(range.end.row, range.end.column), - ), - new_text.to_string(), - )] - }), - ..Default::default() + fake.handle_request::(move |_, _| { + let edit = edit.clone(); + async move { + lsp::CompletionItem { + additional_text_edits: edit.map(|(range, new_text)| { + vec![lsp::TextEdit::new( + lsp::Range::new( + lsp::Position::new(range.start.row, range.start.column), + lsp::Position::new(range.end.row, range.end.column), + ), + new_text.to_string(), + )] + }), + ..Default::default() + } } }) .next() @@ -8814,13 +9407,15 @@ mod tests { ); let (_, editor) = cx.add_window(Default::default(), |cx| { let mut editor = build_editor(multibuffer.clone(), cx); - editor.select_ranges( + let snapshot = editor.snapshot(cx); + editor.select_ranges([Point::new(1, 3)..Point::new(1, 3)], None, cx); + editor.begin_selection(Point::new(2, 1).to_display_point(&snapshot), true, 1, cx); + assert_eq!( + editor.selected_ranges(cx), [ Point::new(1, 3)..Point::new(1, 3), Point::new(2, 1)..Point::new(2, 1), - ], - None, - cx, + ] ); editor }); @@ -8860,6 +9455,61 @@ mod tests { Point::new(0, 3)..Point::new(0, 3) ] ); + assert!(editor.pending_selection.is_some()); + }); + } + + #[gpui::test] + fn test_refresh_selections_while_selecting_with_mouse(cx: &mut gpui::MutableAppContext) { + populate_settings(cx); + let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx)); + let mut excerpt1_id = None; + let multibuffer = cx.add_model(|cx| { + let mut multibuffer = MultiBuffer::new(0); + excerpt1_id = multibuffer + .push_excerpts( + buffer.clone(), + [ + Point::new(0, 0)..Point::new(1, 4), + Point::new(1, 0)..Point::new(2, 4), + ], + cx, + ) + .into_iter() + .next(); + multibuffer + }); + assert_eq!( + multibuffer.read(cx).read(cx).text(), + "aaaa\nbbbb\nbbbb\ncccc" + ); + let (_, editor) = cx.add_window(Default::default(), |cx| { + let mut editor = build_editor(multibuffer.clone(), cx); + let snapshot = editor.snapshot(cx); + editor.begin_selection(Point::new(1, 3).to_display_point(&snapshot), false, 1, cx); + assert_eq!( + editor.selected_ranges(cx), + [Point::new(1, 3)..Point::new(1, 3)] + ); + editor + }); + + multibuffer.update(cx, |multibuffer, cx| { + multibuffer.remove_excerpts([&excerpt1_id.unwrap()], cx); + }); + editor.update(cx, |editor, cx| { + assert_eq!( + editor.selected_ranges(cx), + [Point::new(0, 0)..Point::new(0, 0)] + ); + + // Ensure we don't panic when selections are refreshed and that the pending selection is finalized. + editor.refresh_selections(cx); + assert_eq!( + editor.selected_ranges(cx), + [Point::new(0, 3)..Point::new(0, 3)] + ); + assert!(editor.pending_selection.is_some()); }); } @@ -9009,6 +9659,98 @@ 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]); + + // Creating a pending selection that precedes another selection + leader.update(cx, |leader, cx| { + leader.select_ranges([1..1], None, cx); + leader.begin_selection(DisplayPoint::new(0, 0), true, 1, 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![0..0, 1..1]); + + // Extend the pending selection so that it surrounds another selection + leader.update(cx, |leader, cx| { + leader.extend_selection(DisplayPoint::new(0, 2), 1, 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![0..2]); + } + #[test] fn test_combine_syntax_and_fuzzy_match_highlights() { let string = "abcdefghijklmnop"; @@ -9089,7 +9831,36 @@ mod tests { fn populate_settings(cx: &mut gpui::MutableAppContext) { let settings = Settings::test(cx); - cx.add_app_state(settings); + cx.set_global(settings); + } + + fn assert_selection_ranges( + marked_text: &str, + selection_marker_pairs: Vec<(char, char)>, + view: &mut Editor, + cx: &mut ViewContext, + ) { + let snapshot = view.snapshot(cx).display_snapshot; + let mut marker_chars = Vec::new(); + for (start, end) in selection_marker_pairs.iter() { + marker_chars.push(*start); + marker_chars.push(*end); + } + let (_, markers) = marked_text_by(marked_text, marker_chars); + let asserted_ranges: Vec> = selection_marker_pairs + .iter() + .map(|(start, end)| { + let start = markers.get(start).unwrap()[0].to_display_point(&snapshot); + let end = markers.get(end).unwrap()[0].to_display_point(&snapshot); + start..end + }) + .collect(); + assert_eq!( + view.selected_display_ranges(cx), + &asserted_ranges[..], + "Assert selections are {}", + marked_text + ); } } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 1aed9791c41a2c5443bb2924cc12dddd35fb991c..18f780dacc1ad68d28814a92241163f3fcf27748 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -21,7 +21,7 @@ use gpui::{ MutableAppContext, PaintContext, Quad, Scene, SizeConstraint, ViewContext, WeakViewHandle, }; use json::json; -use language::Bias; +use language::{Bias, DiagnosticSeverity}; use smallvec::SmallVec; use std::{ cmp::{self, Ordering}, @@ -665,19 +665,22 @@ impl EditorElement { } } - let mut diagnostic_highlight = HighlightStyle { - ..Default::default() - }; + let mut diagnostic_highlight = HighlightStyle::default(); if chunk.is_unnecessary { diagnostic_highlight.fade_out = Some(style.unnecessary_code_fade); - } else if let Some(severity) = chunk.diagnostic_severity { - let diagnostic_style = super::diagnostic_style(severity, true, style); - diagnostic_highlight.underline = Some(Underline { - color: Some(diagnostic_style.message.text.color), - thickness: 1.0.into(), - squiggly: true, - }); + } + + if let Some(severity) = chunk.diagnostic_severity { + // Omit underlines for HINT/INFO diagnostics on 'unnecessary' code. + if severity <= DiagnosticSeverity::WARNING || !chunk.is_unnecessary { + let diagnostic_style = super::diagnostic_style(severity, true, style); + diagnostic_highlight.underline = Some(Underline { + color: Some(diagnostic_style.message.text.color), + thickness: 1.0.into(), + squiggly: true, + }); + } } if let Some(highlight_style) = highlight_style.as_mut() { @@ -906,7 +909,7 @@ impl Element for EditorElement { .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 highlighted_rows = None; let mut highlighted_ranges = Vec::new(); @@ -919,11 +922,32 @@ impl Element for EditorElement { &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 { - let local_selections = view.local_selections_in_range( - start_anchor.clone()..end_anchor.clone(), - &display_map, - ); + let local_selections = + view.local_selections_in_range(start_anchor..end_anchor, &display_map); for selection in &local_selections { let is_empty = selection.start == selection.end; let selection_start = snapshot.prev_line_boundary(selection.start).1; @@ -936,8 +960,12 @@ impl Element for EditorElement { *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 .into_iter() .map(|selection| crate::Selection { @@ -948,23 +976,7 @@ impl Element for EditorElement { end: selection.end.to_display_point(&display_map), }) .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), - }); + )); } }); @@ -1210,7 +1222,7 @@ pub struct LayoutState { em_width: f32, em_advance: f32, highlighted_ranges: Vec<(Range, Color)>, - selections: HashMap>>, + selections: Vec<(ReplicaId, Vec>)>, context_menu: Option<(DisplayPoint, ElementBox)>, code_actions_indicator: Option<(u32, ElementBox)>, } @@ -1280,7 +1292,7 @@ impl PaintState { } } -#[derive(Copy, Clone)] +#[derive(Copy, Clone, PartialEq, Eq)] pub enum CursorShape { Bar, Block, @@ -1487,7 +1499,7 @@ mod tests { #[gpui::test] fn test_layout_line_numbers(cx: &mut gpui::MutableAppContext) { - cx.add_app_state(Settings::test(cx)); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx); let (window_id, editor) = cx.add_window(Default::default(), |cx| { Editor::new(EditorMode::Full, buffer, None, None, cx) @@ -1509,7 +1521,7 @@ mod tests { #[gpui::test] fn test_layout_with_placeholder_text_and_blocks(cx: &mut gpui::MutableAppContext) { - cx.add_app_state(Settings::test(cx)); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("", cx); let (window_id, editor) = cx.add_window(Default::default(), |cx| { Editor::new(EditorMode::Full, buffer, None, None, cx) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index c9af9f0854cf7beba9254dc02cb76e7053079bfc..79b25f8f60fa2ebe9e9832aac99b68c91b775f6b 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1,162 +1,251 @@ -use crate::{Autoscroll, Editor, Event, MultiBuffer, NavigationData, ToOffset, ToPoint as _}; -use anyhow::Result; +use crate::{Anchor, Autoscroll, Editor, Event, ExcerptId, NavigationData, ToOffset, ToPoint as _}; +use anyhow::{anyhow, Result}; +use futures::FutureExt; use gpui::{ - elements::*, AppContext, Entity, ModelContext, ModelHandle, MutableAppContext, RenderContext, - Subscription, Task, View, ViewContext, ViewHandle, WeakModelHandle, + elements::*, geometry::vector::vec2f, AppContext, Entity, ModelHandle, MutableAppContext, + RenderContext, Subscription, Task, View, ViewContext, ViewHandle, }; -use language::{Bias, Buffer, Diagnostic, File as _}; -use project::{File, Project, ProjectPath}; -use std::path::PathBuf; -use std::rc::Rc; -use std::{cell::RefCell, fmt::Write}; +use language::{Bias, Buffer, Diagnostic, File as _, SelectionGoal}; +use project::{File, Project, ProjectEntryId, ProjectPath}; +use rpc::proto::{self, update_view}; +use std::{fmt::Write, path::PathBuf, time::Duration}; use text::{Point, Selection}; -use util::ResultExt; +use util::TryFutureExt; use workspace::{ - ItemHandle, ItemNavHistory, ItemView, ItemViewHandle, NavHistory, PathOpener, Settings, - StatusItemView, WeakItemHandle, Workspace, + FollowableItem, Item, ItemHandle, ItemNavHistory, ProjectItem, Settings, StatusItemView, }; -pub struct BufferOpener; +pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2); -#[derive(Clone)] -pub struct BufferItemHandle(pub ModelHandle); - -#[derive(Clone)] -struct WeakBufferItemHandle(WeakModelHandle); - -#[derive(Clone)] -pub struct MultiBufferItemHandle(pub ModelHandle); - -#[derive(Clone)] -struct WeakMultiBufferItemHandle(WeakModelHandle); +impl FollowableItem for Editor { + fn from_state_proto( + pane: ViewHandle, + project: ModelHandle, + state: &mut Option, + cx: &mut MutableAppContext, + ) -> Option>>> { + 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; + }; -impl PathOpener for BufferOpener { - fn open( - &self, - project: &mut Project, - project_path: ProjectPath, - cx: &mut ModelContext, - ) -> Option>>> { - let buffer = project.open_buffer(project_path, cx); - let task = cx.spawn(|_, _| async move { - let buffer = buffer.await?; - Ok(Box::new(BufferItemHandle(buffer)) as Box) + let buffer = project.update(cx, |project, cx| { + project.open_buffer_by_id(state.buffer_id, cx) }); - Some(task) - } -} - -impl ItemHandle for BufferItemHandle { - fn add_view( - &self, - window_id: usize, - workspace: &Workspace, - nav_history: Rc>, - cx: &mut MutableAppContext, - ) -> Box { - let buffer = cx.add_model(|cx| MultiBuffer::singleton(self.0.clone(), cx)); - Box::new(cx.add_view(window_id, |cx| { - let mut editor = Editor::for_buffer(buffer, Some(workspace.project().clone()), cx); - editor.nav_history = Some(ItemNavHistory::new(nav_history, &cx.handle())); - editor + Some(cx.spawn(|mut cx| async move { + let buffer = buffer.await?; + let editor = pane + .read_with(&cx, |pane, cx| { + pane.items_of_type::().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::>>()?; + if !selections.is_empty() { + editor.set_selections_from_remote(selections.into(), 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 boxed_clone(&self) -> Box { - Box::new(self.clone()) - } - - fn to_any(&self) -> gpui::AnyModelHandle { - self.0.clone().into() - } - - fn downgrade(&self) -> Box { - Box::new(WeakBufferItemHandle(self.0.downgrade())) - } - - fn project_path(&self, cx: &AppContext) -> Option { - File::from_dyn(self.0.read(cx).file()).map(|f| ProjectPath { - worktree_id: f.worktree_id(cx), - path: f.path().clone(), - }) - } - - fn id(&self) -> usize { - self.0.id() + fn set_leader_replica_id( + &mut self, + leader_replica_id: Option, + cx: &mut ViewContext, + ) { + 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(); } -} -impl ItemHandle for MultiBufferItemHandle { - fn add_view( - &self, - window_id: usize, - workspace: &Workspace, - nav_history: Rc>, - cx: &mut MutableAppContext, - ) -> Box { - Box::new(cx.add_view(window_id, |cx| { - let mut editor = - Editor::for_buffer(self.0.clone(), Some(workspace.project().clone()), cx); - editor.nav_history = Some(ItemNavHistory::new(nav_history, &cx.handle())); - editor + fn to_state_proto(&self, cx: &AppContext) -> Option { + 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 boxed_clone(&self) -> Box { - Box::new(self.clone()) - } - - fn to_any(&self) -> gpui::AnyModelHandle { - self.0.clone().into() - } - - fn downgrade(&self) -> Box { - Box::new(WeakMultiBufferItemHandle(self.0.downgrade())) + fn add_event_to_update_proto( + &self, + event: &Self::Event, + update: &mut Option, + _: &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 project_path(&self, _: &AppContext) -> Option { - None + fn apply_update_proto( + &mut self, + message: update_view::Variant, + cx: &mut ViewContext, + ) -> 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::>(); + + if !selections.is_empty() { + self.set_selections_from_remote(selections, 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 id(&self) -> usize { - self.0.id() + fn should_unfollow_on_event(event: &Self::Event, _: &AppContext) -> bool { + match event { + Event::Edited => true, + Event::SelectionsChanged { local } => *local, + Event::ScrollPositionChanged { local } => *local, + _ => false, + } } } -impl WeakItemHandle for WeakBufferItemHandle { - fn upgrade(&self, cx: &AppContext) -> Option> { - self.0 - .upgrade(cx) - .map(|buffer| Box::new(BufferItemHandle(buffer)) as Box) - } - - fn id(&self) -> usize { - self.0.id() +fn serialize_selection(selection: &Selection) -> 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, } } -impl WeakItemHandle for WeakMultiBufferItemHandle { - fn upgrade(&self, cx: &AppContext) -> Option> { - self.0 - .upgrade(cx) - .map(|buffer| Box::new(MultiBufferItemHandle(buffer)) as Box) - } - - fn id(&self) -> usize { - self.0.id() - } +fn deserialize_selection( + excerpt_id: &ExcerptId, + buffer_id: usize, + selection: proto::Selection, +) -> Option> { + 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 ItemView for Editor { - fn item(&self, cx: &AppContext) -> Box { - if let Some(buffer) = self.buffer.read(cx).as_singleton() { - Box::new(BufferItemHandle(buffer)) - } else { - Box::new(MultiBufferItemHandle(self.buffer.clone())) - } - } - - fn navigate(&mut self, data: Box, cx: &mut ViewContext) { +impl Item for Editor { + fn navigate(&mut self, data: Box, cx: &mut ViewContext) -> bool { if let Some(data) = data.downcast_ref::() { let buffer = self.buffer.read(cx).read(cx); let offset = if buffer.can_resolve(&data.anchor) { @@ -164,11 +253,19 @@ impl ItemView for Editor { } else { buffer.clip_offset(data.offset, Bias::Left) }; - + let newest_selection = self.newest_selection_with_snapshot::(&buffer); drop(buffer); - let nav_history = self.nav_history.take(); - self.select_ranges([offset..offset], Some(Autoscroll::Fit), cx); - self.nav_history = nav_history; + + if newest_selection.head() == offset { + false + } else { + let nav_history = self.nav_history.take(); + self.select_ranges([offset..offset], Some(Autoscroll::Fit), cx); + self.nav_history = nav_history; + true + } + } else { + false } } @@ -184,15 +281,19 @@ impl ItemView for Editor { }) } - fn clone_on_split( - &self, - nav_history: ItemNavHistory, - cx: &mut ViewContext, - ) -> Option + fn project_entry_id(&self, cx: &AppContext) -> Option { + 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) -> Option where Self: Sized, { - Some(self.clone(nav_history, cx)) + Some(self.clone(cx)) + } + + fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext) { + self.nav_history = Some(history); } fn deactivated(&mut self, cx: &mut ViewContext) { @@ -219,9 +320,17 @@ impl ItemView for Editor { ) -> Task> { let buffer = self.buffer().clone(); let buffers = buffer.read(cx).all_buffers(); - let transaction = project.update(cx, |project, cx| project.format(buffers, true, cx)); + let mut timeout = cx.background().timer(FORMAT_TIMEOUT).fuse(); + let format = project.update(cx, |project, cx| project.format(buffers, true, cx)); cx.spawn(|this, mut cx| async move { - let transaction = transaction.await.log_err(); + let transaction = futures::select_biased! { + _ = timeout => { + log::warn!("timed out waiting for formatting"); + None + } + transaction = format.log_err().fuse() => transaction, + }; + this.update(&mut cx, |editor, cx| { editor.request_autoscroll(Autoscroll::Fit, cx) }); @@ -275,6 +384,18 @@ impl ItemView for Editor { } } +impl ProjectItem for Editor { + type Item = Buffer; + + fn for_project_item( + project: ModelHandle, + buffer: ModelHandle, + cx: &mut ViewContext, + ) -> Self { + Self::for_buffer(buffer, Some(project), cx) + } +} + pub struct CursorPosition { position: Option, selected_count: usize, @@ -322,7 +443,7 @@ impl View for CursorPosition { fn render(&mut self, cx: &mut RenderContext) -> ElementBox { if let Some(position) = self.position { - let theme = &cx.app_state::().theme.workspace.status_bar; + let theme = &cx.global::().theme.workspace.status_bar; let mut text = format!("{},{}", position.row + 1, position.column + 1); if self.selected_count > 0 { write!(text, " ({} selected)", self.selected_count).unwrap(); @@ -337,7 +458,7 @@ impl View for CursorPosition { impl StatusItemView for CursorPosition { fn set_active_pane_item( &mut self, - active_pane_item: Option<&dyn ItemViewHandle>, + active_pane_item: Option<&dyn ItemHandle>, cx: &mut ViewContext, ) { if let Some(editor) = active_pane_item.and_then(|item| item.downcast::()) { @@ -395,7 +516,7 @@ impl View for DiagnosticMessage { fn render(&mut self, cx: &mut RenderContext) -> ElementBox { if let Some(diagnostic) = &self.diagnostic { - let theme = &cx.app_state::().theme.workspace.status_bar; + let theme = &cx.global::().theme.workspace.status_bar; Label::new( diagnostic.message.split('\n').next().unwrap().to_string(), theme.diagnostic_message.clone(), @@ -410,7 +531,7 @@ impl View for DiagnosticMessage { impl StatusItemView for DiagnosticMessage { fn set_active_pane_item( &mut self, - active_pane_item: Option<&dyn ItemViewHandle>, + active_pane_item: Option<&dyn ItemHandle>, cx: &mut ViewContext, ) { if let Some(editor) = active_pane_item.and_then(|item| item.downcast::()) { diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 5951e2c20db8b9f675f0d6a25afb7e5590e8dc7b..cf2d772b16627bce0ca40409649856157e19405f 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -1,20 +1,19 @@ use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint}; use crate::{char_kind, CharKind, ToPoint}; -use anyhow::Result; use language::Point; use std::ops::Range; -pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> Result { +pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { if point.column() > 0 { *point.column_mut() -= 1; } else if point.row() > 0 { *point.row_mut() -= 1; *point.column_mut() = map.line_len(point.row()); } - Ok(map.clip_point(point, Bias::Left)) + map.clip_point(point, Bias::Left) } -pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> Result { +pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { let max_column = map.line_len(point.row()); if point.column() < max_column { *point.column_mut() += 1; @@ -22,14 +21,14 @@ pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> Result Result<(DisplayPoint, SelectionGoal)> { +) -> (DisplayPoint, SelectionGoal) { let mut goal_column = if let SelectionGoal::Column(column) = goal { column } else { @@ -54,17 +53,17 @@ pub fn up( Bias::Right }; - Ok(( + ( map.clip_point(point, clip_bias), SelectionGoal::Column(goal_column), - )) + ) } pub fn down( map: &DisplaySnapshot, start: DisplayPoint, goal: SelectionGoal, -) -> Result<(DisplayPoint, SelectionGoal)> { +) -> (DisplayPoint, SelectionGoal) { let mut goal_column = if let SelectionGoal::Column(column) = goal { column } else { @@ -86,10 +85,10 @@ pub fn down( Bias::Right }; - Ok(( + ( map.clip_point(point, clip_bias), SelectionGoal::Column(goal_column), - )) + ) } pub fn line_beginning( @@ -132,68 +131,110 @@ pub fn line_end( } } -pub fn prev_word_boundary(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { - let mut line_start = 0; - if point.row() > 0 { - if let Some(indent) = map.soft_wrap_indent(point.row() - 1) { - line_start = indent; - } - } +pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { + find_preceding_boundary(map, point, |left, right| { + (char_kind(left) != char_kind(right) && !right.is_whitespace()) || left == '\n' + }) +} - if point.column() == line_start { - if point.row() == 0 { - return DisplayPoint::new(0, 0); - } else { - let row = point.row() - 1; - point = map.clip_point(DisplayPoint::new(row, map.line_len(row)), Bias::Left); - } - } +pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { + find_preceding_boundary(map, point, |left, right| { + let is_word_start = char_kind(left) != char_kind(right) && !right.is_whitespace(); + let is_subword_start = + left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase(); + is_word_start || is_subword_start || left == '\n' + }) +} - let mut boundary = DisplayPoint::new(point.row(), 0); - let mut column = 0; - let mut prev_char_kind = CharKind::Newline; - for c in map.chars_at(DisplayPoint::new(point.row(), 0)) { - if column >= point.column() { - break; +pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { + find_boundary(map, point, |left, right| { + (char_kind(left) != char_kind(right) && !left.is_whitespace()) || right == '\n' + }) +} + +pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { + find_boundary(map, point, |left, right| { + let is_word_end = (char_kind(left) != char_kind(right)) && !left.is_whitespace(); + let is_subword_end = + left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase(); + is_word_end || is_subword_end || right == '\n' + }) +} + +/// Scans for a boundary from the start of each line preceding the given end point until a boundary +/// is found, indicated by the given predicate returning true. The predicate is called with the +/// character to the left and right of the candidate boundary location, and will be called with `\n` +/// characters indicating the start or end of a line. If the predicate returns true multiple times +/// on a line, the *rightmost* boundary is returned. +pub fn find_preceding_boundary( + map: &DisplaySnapshot, + end: DisplayPoint, + mut is_boundary: impl FnMut(char, char) -> bool, +) -> DisplayPoint { + let mut point = end; + loop { + *point.column_mut() = 0; + if point.row() > 0 { + if let Some(indent) = map.soft_wrap_indent(point.row() - 1) { + *point.column_mut() = indent; + } } - let char_kind = char_kind(c); - if char_kind != prev_char_kind - && char_kind != CharKind::Whitespace - && char_kind != CharKind::Newline - { - *boundary.column_mut() = column; + let mut boundary = None; + let mut prev_ch = if point.is_zero() { None } else { Some('\n') }; + for ch in map.chars_at(point) { + if point >= end { + break; + } + + if let Some(prev_ch) = prev_ch { + if is_boundary(prev_ch, ch) { + boundary = Some(point); + } + } + + if ch == '\n' { + break; + } + + prev_ch = Some(ch); + *point.column_mut() += ch.len_utf8() as u32; } - prev_char_kind = char_kind; - column += c.len_utf8() as u32; + if let Some(boundary) = boundary { + return boundary; + } else if point.row() == 0 { + return DisplayPoint::zero(); + } else { + *point.row_mut() -= 1; + } } - boundary } -pub fn next_word_boundary(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { - let mut prev_char_kind = None; - for c in map.chars_at(point) { - let char_kind = char_kind(c); - if let Some(prev_char_kind) = prev_char_kind { - if c == '\n' { - break; - } - if prev_char_kind != char_kind - && prev_char_kind != CharKind::Whitespace - && prev_char_kind != CharKind::Newline - { +/// Scans for a boundary following the given start point until a boundary is found, indicated by the +/// given predicate returning true. The predicate is called with the character to the left and right +/// of the candidate boundary location, and will be called with `\n` characters indicating the start +/// or end of a line. +pub fn find_boundary( + map: &DisplaySnapshot, + mut point: DisplayPoint, + mut is_boundary: impl FnMut(char, char) -> bool, +) -> DisplayPoint { + let mut prev_ch = None; + for ch in map.chars_at(point) { + if let Some(prev_ch) = prev_ch { + if is_boundary(prev_ch, ch) { break; } } - if c == '\n' { + if ch == '\n' { *point.row_mut() += 1; *point.column_mut() = 0; } else { - *point.column_mut() += c.len_utf8() as u32; + *point.column_mut() += ch.len_utf8() as u32; } - prev_char_kind = Some(char_kind); + prev_ch = Some(ch); } map.clip_point(point, Bias::Right) } @@ -225,9 +266,205 @@ pub fn surrounding_word(map: &DisplaySnapshot, position: DisplayPoint) -> Range< #[cfg(test)] mod tests { use super::*; - use crate::{Buffer, DisplayMap, MultiBuffer}; + use crate::{test::marked_display_snapshot, Buffer, DisplayMap, MultiBuffer}; use language::Point; + #[gpui::test] + fn test_previous_word_start(cx: &mut gpui::MutableAppContext) { + fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) { + let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); + assert_eq!( + previous_word_start(&snapshot, display_points[1]), + display_points[0] + ); + } + + assert("\n| |lorem", cx); + assert("|\n| lorem", cx); + assert(" |lorem|", cx); + assert("| |lorem", cx); + assert(" |lor|em", cx); + assert("\nlorem\n| |ipsum", cx); + assert("\n\n|\n|", cx); + assert(" |lorem |ipsum", cx); + assert("lorem|-|ipsum", cx); + assert("lorem|-#$@|ipsum", cx); + assert("|lorem_|ipsum", cx); + assert(" |defγ|", cx); + assert(" |bcΔ|", cx); + assert(" ab|——|cd", cx); + } + + #[gpui::test] + fn test_previous_subword_start(cx: &mut gpui::MutableAppContext) { + fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) { + let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); + assert_eq!( + previous_subword_start(&snapshot, display_points[1]), + display_points[0] + ); + } + + // Subword boundaries are respected + assert("lorem_|ip|sum", cx); + assert("lorem_|ipsum|", cx); + assert("|lorem_|ipsum", cx); + assert("lorem_|ipsum_|dolor", cx); + assert("lorem|Ip|sum", cx); + assert("lorem|Ipsum|", cx); + + // Word boundaries are still respected + assert("\n| |lorem", cx); + assert(" |lorem|", cx); + assert(" |lor|em", cx); + assert("\nlorem\n| |ipsum", cx); + assert("\n\n|\n|", cx); + assert(" |lorem |ipsum", cx); + assert("lorem|-|ipsum", cx); + assert("lorem|-#$@|ipsum", cx); + assert(" |defγ|", cx); + assert(" bc|Δ|", cx); + assert(" |bcδ|", cx); + assert(" ab|——|cd", cx); + } + + #[gpui::test] + fn test_find_preceding_boundary(cx: &mut gpui::MutableAppContext) { + fn assert( + marked_text: &str, + cx: &mut gpui::MutableAppContext, + is_boundary: impl FnMut(char, char) -> bool, + ) { + let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); + assert_eq!( + find_preceding_boundary(&snapshot, display_points[1], is_boundary), + display_points[0] + ); + } + + assert("abc|def\ngh\nij|k", cx, |left, right| { + left == 'c' && right == 'd' + }); + assert("abcdef\n|gh\nij|k", cx, |left, right| { + left == '\n' && right == 'g' + }); + let mut line_count = 0; + assert("abcdef\n|gh\nij|k", cx, |left, _| { + if left == '\n' { + line_count += 1; + line_count == 2 + } else { + false + } + }); + } + + #[gpui::test] + fn test_next_word_end(cx: &mut gpui::MutableAppContext) { + fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) { + let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); + assert_eq!( + next_word_end(&snapshot, display_points[0]), + display_points[1] + ); + } + + assert("\n| lorem|", cx); + assert(" |lorem|", cx); + assert(" lor|em|", cx); + assert(" lorem| |\nipsum\n", cx); + assert("\n|\n|\n\n", cx); + assert("lorem| ipsum| ", cx); + assert("lorem|-|ipsum", cx); + assert("lorem|#$@-|ipsum", cx); + assert("lorem|_ipsum|", cx); + assert(" |bcΔ|", cx); + assert(" ab|——|cd", cx); + } + + #[gpui::test] + fn test_next_subword_end(cx: &mut gpui::MutableAppContext) { + fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) { + let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); + assert_eq!( + next_subword_end(&snapshot, display_points[0]), + display_points[1] + ); + } + + // Subword boundaries are respected + assert("lo|rem|_ipsum", cx); + assert("|lorem|_ipsum", cx); + assert("lorem|_ipsum|", cx); + assert("lorem|_ipsum|_dolor", cx); + assert("lo|rem|Ipsum", cx); + assert("lorem|Ipsum|Dolor", cx); + + // Word boundaries are still respected + assert("\n| lorem|", cx); + assert(" |lorem|", cx); + assert(" lor|em|", cx); + assert(" lorem| |\nipsum\n", cx); + assert("\n|\n|\n\n", cx); + assert("lorem| ipsum| ", cx); + assert("lorem|-|ipsum", cx); + assert("lorem|#$@-|ipsum", cx); + assert("lorem|_ipsum|", cx); + assert(" |bc|Δ", cx); + assert(" ab|——|cd", cx); + } + + #[gpui::test] + fn test_find_boundary(cx: &mut gpui::MutableAppContext) { + fn assert( + marked_text: &str, + cx: &mut gpui::MutableAppContext, + is_boundary: impl FnMut(char, char) -> bool, + ) { + let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); + assert_eq!( + find_boundary(&snapshot, display_points[0], is_boundary), + display_points[1] + ); + } + + assert("abc|def\ngh\nij|k", cx, |left, right| { + left == 'j' && right == 'k' + }); + assert("ab|cdef\ngh\n|ijk", cx, |left, right| { + left == '\n' && right == 'i' + }); + let mut line_count = 0; + assert("abc|def\ngh\n|ijk", cx, |left, _| { + if left == '\n' { + line_count += 1; + line_count == 2 + } else { + false + } + }); + } + + #[gpui::test] + fn test_surrounding_word(cx: &mut gpui::MutableAppContext) { + fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) { + let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); + assert_eq!( + surrounding_word(&snapshot, display_points[1]), + display_points[0]..display_points[2] + ); + } + + assert("||lorem| ipsum", cx); + assert("|lo|rem| ipsum", cx); + assert("|lorem|| ipsum", cx); + assert("lorem| | |ipsum", cx); + assert("lorem\n|||\nipsum", cx); + assert("lorem\n||ipsum|", cx); + assert("lorem,|| |ipsum", cx); + assert("|lorem||, ipsum", cx); + } + #[gpui::test] fn test_move_up_and_down_with_excerpts(cx: &mut gpui::MutableAppContext) { let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap(); @@ -249,180 +486,50 @@ mod tests { ); multibuffer }); - let display_map = cx.add_model(|cx| DisplayMap::new(multibuffer, 2, font_id, 14.0, None, 2, 2, cx)); - let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx)); + assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn"); // Can't move up into the first excerpt's header assert_eq!( - up(&snapshot, DisplayPoint::new(2, 2), SelectionGoal::Column(2)).unwrap(), + up(&snapshot, DisplayPoint::new(2, 2), SelectionGoal::Column(2)), (DisplayPoint::new(2, 0), SelectionGoal::Column(0)), ); assert_eq!( - up(&snapshot, DisplayPoint::new(2, 0), SelectionGoal::None).unwrap(), + up(&snapshot, DisplayPoint::new(2, 0), SelectionGoal::None), (DisplayPoint::new(2, 0), SelectionGoal::Column(0)), ); // Move up and down within first excerpt assert_eq!( - up(&snapshot, DisplayPoint::new(3, 4), SelectionGoal::Column(4)).unwrap(), + up(&snapshot, DisplayPoint::new(3, 4), SelectionGoal::Column(4)), (DisplayPoint::new(2, 3), SelectionGoal::Column(4)), ); assert_eq!( - down(&snapshot, DisplayPoint::new(2, 3), SelectionGoal::Column(4)).unwrap(), + down(&snapshot, DisplayPoint::new(2, 3), SelectionGoal::Column(4)), (DisplayPoint::new(3, 4), SelectionGoal::Column(4)), ); // Move up and down across second excerpt's header assert_eq!( - up(&snapshot, DisplayPoint::new(6, 5), SelectionGoal::Column(5)).unwrap(), + up(&snapshot, DisplayPoint::new(6, 5), SelectionGoal::Column(5)), (DisplayPoint::new(3, 4), SelectionGoal::Column(5)), ); assert_eq!( - down(&snapshot, DisplayPoint::new(3, 4), SelectionGoal::Column(5)).unwrap(), + down(&snapshot, DisplayPoint::new(3, 4), SelectionGoal::Column(5)), (DisplayPoint::new(6, 5), SelectionGoal::Column(5)), ); // Can't move down off the end assert_eq!( - down(&snapshot, DisplayPoint::new(7, 0), SelectionGoal::Column(0)).unwrap(), + down(&snapshot, DisplayPoint::new(7, 0), SelectionGoal::Column(0)), (DisplayPoint::new(7, 2), SelectionGoal::Column(2)), ); assert_eq!( - down(&snapshot, DisplayPoint::new(7, 2), SelectionGoal::Column(2)).unwrap(), + down(&snapshot, DisplayPoint::new(7, 2), SelectionGoal::Column(2)), (DisplayPoint::new(7, 2), SelectionGoal::Column(2)), ); } - - #[gpui::test] - fn test_prev_next_word_boundary_multibyte(cx: &mut gpui::MutableAppContext) { - let tab_size = 4; - let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap(); - let font_id = cx - .font_cache() - .select_font(family_id, &Default::default()) - .unwrap(); - let font_size = 14.0; - - let buffer = MultiBuffer::build_simple("a bcΔ defγ hi—jk", cx); - let display_map = cx - .add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx)); - let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx)); - assert_eq!( - prev_word_boundary(&snapshot, DisplayPoint::new(0, 12)), - DisplayPoint::new(0, 7) - ); - assert_eq!( - prev_word_boundary(&snapshot, DisplayPoint::new(0, 7)), - DisplayPoint::new(0, 2) - ); - assert_eq!( - prev_word_boundary(&snapshot, DisplayPoint::new(0, 6)), - DisplayPoint::new(0, 2) - ); - assert_eq!( - prev_word_boundary(&snapshot, DisplayPoint::new(0, 2)), - DisplayPoint::new(0, 0) - ); - assert_eq!( - prev_word_boundary(&snapshot, DisplayPoint::new(0, 1)), - DisplayPoint::new(0, 0) - ); - - assert_eq!( - next_word_boundary(&snapshot, DisplayPoint::new(0, 0)), - DisplayPoint::new(0, 1) - ); - assert_eq!( - next_word_boundary(&snapshot, DisplayPoint::new(0, 1)), - DisplayPoint::new(0, 6) - ); - assert_eq!( - next_word_boundary(&snapshot, DisplayPoint::new(0, 2)), - DisplayPoint::new(0, 6) - ); - assert_eq!( - next_word_boundary(&snapshot, DisplayPoint::new(0, 6)), - DisplayPoint::new(0, 12) - ); - assert_eq!( - next_word_boundary(&snapshot, DisplayPoint::new(0, 7)), - DisplayPoint::new(0, 12) - ); - } - - #[gpui::test] - fn test_surrounding_word(cx: &mut gpui::MutableAppContext) { - let tab_size = 4; - let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap(); - let font_id = cx - .font_cache() - .select_font(family_id, &Default::default()) - .unwrap(); - let font_size = 14.0; - let buffer = MultiBuffer::build_simple("lorem ipsum dolor\n sit", cx); - let display_map = cx - .add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx)); - let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx)); - - assert_eq!( - surrounding_word(&snapshot, DisplayPoint::new(0, 0)), - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5), - ); - assert_eq!( - surrounding_word(&snapshot, DisplayPoint::new(0, 2)), - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5), - ); - assert_eq!( - surrounding_word(&snapshot, DisplayPoint::new(0, 5)), - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5), - ); - assert_eq!( - surrounding_word(&snapshot, DisplayPoint::new(0, 6)), - DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11), - ); - assert_eq!( - surrounding_word(&snapshot, DisplayPoint::new(0, 7)), - DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11), - ); - assert_eq!( - surrounding_word(&snapshot, DisplayPoint::new(0, 11)), - DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11), - ); - assert_eq!( - surrounding_word(&snapshot, DisplayPoint::new(0, 13)), - DisplayPoint::new(0, 11)..DisplayPoint::new(0, 14), - ); - assert_eq!( - surrounding_word(&snapshot, DisplayPoint::new(0, 14)), - DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19), - ); - assert_eq!( - surrounding_word(&snapshot, DisplayPoint::new(0, 17)), - DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19), - ); - assert_eq!( - surrounding_word(&snapshot, DisplayPoint::new(0, 19)), - DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19), - ); - assert_eq!( - surrounding_word(&snapshot, DisplayPoint::new(1, 0)), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 4), - ); - assert_eq!( - surrounding_word(&snapshot, DisplayPoint::new(1, 1)), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 4), - ); - assert_eq!( - surrounding_word(&snapshot, DisplayPoint::new(1, 6)), - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 7), - ); - assert_eq!( - surrounding_word(&snapshot, DisplayPoint::new(1, 7)), - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 7), - ); - } } diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index e145488d650cb49d44d6b3de3af9e6304f02609e..af98f3d5896d955872193a29730696aa9a80ed17 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -211,7 +211,7 @@ impl MultiBuffer { pub fn singleton(buffer: ModelHandle, cx: &mut ModelContext) -> Self { let mut this = Self::new(buffer.read(cx).replica_id()); this.singleton = true; - this.push_excerpts(buffer, [text::Anchor::min()..text::Anchor::max()], cx); + this.push_excerpts(buffer, [text::Anchor::MIN..text::Anchor::MAX], cx); this.snapshot.borrow_mut().singleton = true; this } @@ -522,24 +522,14 @@ impl MultiBuffer { self.buffers.borrow()[&buffer_id] .buffer .update(cx, |buffer, cx| { - selections.sort_unstable_by(|a, b| a.start.cmp(&b.start, buffer).unwrap()); + selections.sort_unstable_by(|a, b| a.start.cmp(&b.start, buffer)); let mut selections = selections.into_iter().peekable(); let merged_selections = Arc::from_iter(iter::from_fn(|| { let mut selection = selections.next()?; while let Some(next_selection) = selections.peek() { - if selection - .end - .cmp(&next_selection.start, buffer) - .unwrap() - .is_ge() - { + if selection.end.cmp(&next_selection.start, buffer).is_ge() { let next_selection = selections.next().unwrap(); - if next_selection - .end - .cmp(&selection.end, buffer) - .unwrap() - .is_ge() - { + if next_selection.end.cmp(&selection.end, buffer).is_ge() { selection.end = next_selection.end; } } else { @@ -814,11 +804,38 @@ impl MultiBuffer { cx.notify(); } - pub fn excerpt_ids_for_buffer(&self, buffer: &ModelHandle) -> Vec { + pub fn excerpts_for_buffer( + &self, + buffer: &ModelHandle, + cx: &AppContext, + ) -> Vec<(ExcerptId, Range)> { + let mut excerpts = Vec::new(); + let snapshot = self.read(cx); + let buffers = self.buffers.borrow(); + let mut cursor = snapshot.excerpts.cursor::>(); + for excerpt_id in buffers + .get(&buffer.id()) + .map(|state| &state.excerpts) + .into_iter() + .flatten() + { + cursor.seek_forward(&Some(excerpt_id), Bias::Left, &()); + if let Some(excerpt) = cursor.item() { + if excerpt.id == *excerpt_id { + excerpts.push((excerpt.id.clone(), excerpt.range.clone())); + } + } + } + + excerpts + } + + pub fn excerpt_ids(&self) -> Vec { self.buffers .borrow() - .get(&buffer.id()) - .map_or(Vec::new(), |state| state.excerpts.clone()) + .values() + .flat_map(|state| state.excerpts.iter().cloned()) + .collect() } pub fn excerpt_containing( @@ -1407,7 +1424,7 @@ impl MultiBufferSnapshot { ); for ch in prev_chars { - if Some(char_kind(ch)) == word_kind { + if Some(char_kind(ch)) == word_kind && ch != '\n' { start -= ch.len_utf8(); } else { break; @@ -1415,7 +1432,7 @@ impl MultiBufferSnapshot { } for ch in next_chars { - if Some(char_kind(ch)) == word_kind { + if Some(char_kind(ch)) == word_kind && ch != '\n' { end += ch.len_utf8(); } else { break; @@ -1909,11 +1926,7 @@ impl MultiBufferSnapshot { .range .start .bias(anchor.text_anchor.bias, &excerpt.buffer); - if text_anchor - .cmp(&excerpt.range.end, &excerpt.buffer) - .unwrap() - .is_gt() - { + if text_anchor.cmp(&excerpt.range.end, &excerpt.buffer).is_gt() { text_anchor = excerpt.range.end.clone(); } Anchor { @@ -1928,7 +1941,6 @@ impl MultiBufferSnapshot { .bias(anchor.text_anchor.bias, &excerpt.buffer); if text_anchor .cmp(&excerpt.range.start, &excerpt.buffer) - .unwrap() .is_lt() { text_anchor = excerpt.range.start.clone(); @@ -1948,7 +1960,7 @@ impl MultiBufferSnapshot { result.push((anchor_ix, anchor, kept_position)); } } - result.sort_unstable_by(|a, b| a.1.cmp(&b.1, self).unwrap()); + result.sort_unstable_by(|a, b| a.1.cmp(&b.1, self)); result } @@ -2295,10 +2307,10 @@ impl MultiBufferSnapshot { excerpt_id: excerpt.id.clone(), text_anchor: selection.end.clone(), }; - if range.start.cmp(&start, self).unwrap().is_gt() { + if range.start.cmp(&start, self).is_gt() { start = range.start.clone(); } - if range.end.cmp(&end, self).unwrap().is_lt() { + if range.end.cmp(&end, self).is_lt() { end = range.end.clone(); } @@ -2522,17 +2534,9 @@ impl Excerpt { } fn clip_anchor(&self, text_anchor: text::Anchor) -> text::Anchor { - if text_anchor - .cmp(&self.range.start, &self.buffer) - .unwrap() - .is_lt() - { + if text_anchor.cmp(&self.range.start, &self.buffer).is_lt() { self.range.start.clone() - } else if text_anchor - .cmp(&self.range.end, &self.buffer) - .unwrap() - .is_gt() - { + } else if text_anchor.cmp(&self.range.end, &self.buffer).is_gt() { self.range.end.clone() } else { text_anchor @@ -2545,13 +2549,11 @@ impl Excerpt { .range .start .cmp(&anchor.text_anchor, &self.buffer) - .unwrap() .is_le() && self .range .end .cmp(&anchor.text_anchor, &self.buffer) - .unwrap() .is_ge() } } @@ -3062,7 +3064,8 @@ mod tests { ); let snapshot = multibuffer.update(cx, |multibuffer, cx| { - let buffer_2_excerpt_id = multibuffer.excerpt_ids_for_buffer(&buffer_2)[0].clone(); + let (buffer_2_excerpt_id, _) = + multibuffer.excerpts_for_buffer(&buffer_2, cx)[0].clone(); multibuffer.remove_excerpts(&[buffer_2_excerpt_id], cx); multibuffer.snapshot(cx) }); @@ -3357,7 +3360,7 @@ mod tests { let bias = if rng.gen() { Bias::Left } else { Bias::Right }; log::info!("Creating anchor at {} with bias {:?}", offset, bias); anchors.push(multibuffer.anchor_at(offset, bias)); - anchors.sort_by(|a, b| a.cmp(&b, &multibuffer).unwrap()); + anchors.sort_by(|a, b| a.cmp(&b, &multibuffer)); } 40..=44 if !anchors.is_empty() => { let multibuffer = multibuffer.read(cx).read(cx); diff --git a/crates/editor/src/multi_buffer/anchor.rs b/crates/editor/src/multi_buffer/anchor.rs index 33147ce285f371c97870c1281d25a3015a85fb7b..df080f074cdd5d1295b6a0e4729939819ec71bb0 100644 --- a/crates/editor/src/multi_buffer/anchor.rs +++ b/crates/editor/src/multi_buffer/anchor.rs @@ -1,5 +1,4 @@ use super::{ExcerptId, MultiBufferSnapshot, ToOffset, ToPoint}; -use anyhow::Result; use std::{ cmp::Ordering, ops::{Range, Sub}, @@ -19,7 +18,7 @@ impl Anchor { Self { buffer_id: None, excerpt_id: ExcerptId::min(), - text_anchor: text::Anchor::min(), + text_anchor: text::Anchor::MIN, } } @@ -27,7 +26,7 @@ impl Anchor { Self { buffer_id: None, excerpt_id: ExcerptId::max(), - text_anchor: text::Anchor::max(), + text_anchor: text::Anchor::MAX, } } @@ -35,18 +34,18 @@ impl Anchor { &self.excerpt_id } - pub fn cmp<'a>(&self, other: &Anchor, snapshot: &MultiBufferSnapshot) -> Result { + pub fn cmp<'a>(&self, other: &Anchor, snapshot: &MultiBufferSnapshot) -> Ordering { let excerpt_id_cmp = self.excerpt_id.cmp(&other.excerpt_id); if excerpt_id_cmp.is_eq() { if self.excerpt_id == ExcerptId::min() || self.excerpt_id == ExcerptId::max() { - Ok(Ordering::Equal) + Ordering::Equal } else if let Some(excerpt) = snapshot.excerpt(&self.excerpt_id) { self.text_anchor.cmp(&other.text_anchor, &excerpt.buffer) } else { - Ok(Ordering::Equal) + Ordering::Equal } } else { - Ok(excerpt_id_cmp) + excerpt_id_cmp } } @@ -97,17 +96,17 @@ impl ToPoint for Anchor { } pub trait AnchorRangeExt { - fn cmp(&self, b: &Range, buffer: &MultiBufferSnapshot) -> Result; + fn cmp(&self, b: &Range, buffer: &MultiBufferSnapshot) -> Ordering; fn to_offset(&self, content: &MultiBufferSnapshot) -> Range; fn to_point(&self, content: &MultiBufferSnapshot) -> Range; } impl AnchorRangeExt for Range { - fn cmp(&self, other: &Range, buffer: &MultiBufferSnapshot) -> Result { - Ok(match self.start.cmp(&other.start, buffer)? { - Ordering::Equal => other.end.cmp(&self.end, buffer)?, + fn cmp(&self, other: &Range, buffer: &MultiBufferSnapshot) -> Ordering { + match self.start.cmp(&other.start, buffer) { + Ordering::Equal => other.end.cmp(&self.end, buffer), ord @ _ => ord, - }) + } } fn to_offset(&self, content: &MultiBufferSnapshot) -> Range { diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index 67622db83f94539ff40ec728843799a51628e8aa..e80547c9dddcd0827d2fa527b20a84d0c6c9df55 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -1,3 +1,10 @@ +use util::test::marked_text; + +use crate::{ + display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint}, + DisplayPoint, MultiBuffer, +}; + #[cfg(test)] #[ctor::ctor] fn init_logger() { @@ -5,3 +12,30 @@ fn init_logger() { env_logger::init(); } } + +// Returns a snapshot from text containing '|' character markers with the markers removed, and DisplayPoints for each one. +pub fn marked_display_snapshot( + text: &str, + cx: &mut gpui::MutableAppContext, +) -> (DisplaySnapshot, Vec) { + let (unmarked_text, markers) = marked_text(text); + + let tab_size = 4; + let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap(); + let font_id = cx + .font_cache() + .select_font(family_id, &Default::default()) + .unwrap(); + let font_size = 14.0; + + let buffer = MultiBuffer::build_simple(&unmarked_text, cx); + let display_map = + cx.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx)); + let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx)); + let markers = markers + .into_iter() + .map(|offset| offset.to_display_point(&snapshot)) + .collect(); + + (snapshot, markers) +} diff --git a/crates/file_finder/Cargo.toml b/crates/file_finder/Cargo.toml index c5300dbcd9f47279aac127315e2d03f354aaae04..b946ea48fbbf58a7196f56949ececfe23fa1fd75 100644 --- a/crates/file_finder/Cargo.toml +++ b/crates/file_finder/Cargo.toml @@ -21,3 +21,5 @@ postage = { version = "0.4.1", features = ["futures-traits"] } gpui = { path = "../gpui", features = ["test-support"] } serde_json = { version = "1.0.64", features = ["preserve_order"] } workspace = { path = "../workspace", features = ["test-support"] } +ctor = "0.1" +env_logger = "0.8" diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 4340cd5a1b7bb30bf05e40c5e0bf6ab1de88a6b5..9f0137ef62a25a15eec50ce9d5f84c7c717c3a8b 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -67,7 +67,7 @@ impl View for FileFinder { } fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - let settings = cx.app_state::(); + let settings = cx.global::(); Align::new( ConstrainedBox::new( Container::new( @@ -106,7 +106,7 @@ impl View for FileFinder { impl FileFinder { fn render_matches(&self, cx: &AppContext) -> ElementBox { if self.matches.is_empty() { - let settings = cx.app_state::(); + let settings = cx.global::(); return Container::new( Label::new( "No matches".into(), @@ -142,7 +142,7 @@ impl FileFinder { fn render_match(&self, path_match: &PathMatch, index: usize, cx: &AppContext) -> ElementBox { let selected_index = self.selected_index(); - let settings = cx.app_state::(); + let settings = cx.global::(); let style = if index == selected_index { &settings.theme.selector.active_item } else { @@ -291,7 +291,7 @@ impl FileFinder { cx: &mut ViewContext, ) { match event { - editor::Event::Edited => { + editor::Event::BufferEdited { .. } => { let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx)); if query.is_empty() { self.latest_search_id = post_inc(&mut self.search_count); @@ -407,16 +407,21 @@ mod tests { use std::path::PathBuf; use workspace::{Workspace, WorkspaceParams}; + #[ctor::ctor] + fn init_logger() { + if std::env::var("RUST_LOG").is_ok() { + env_logger::init(); + } + } + #[gpui::test] async fn test_matching_paths(cx: &mut gpui::TestAppContext) { - let mut path_openers = Vec::new(); cx.update(|cx| { super::init(cx); - editor::init(cx, &mut path_openers); + editor::init(cx); }); - let mut params = cx.update(WorkspaceParams::test); - params.path_openers = Arc::from(path_openers); + let params = cx.update(WorkspaceParams::test); params .fs .as_fake() diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index 50bae39ae9af73b5b8a82596129deec652dfd776..109d33097d69cf5f4d251451aa634887774145de 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/crates/go_to_line/src/go_to_line.rs @@ -59,7 +59,8 @@ impl GoToLine { } fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext) { - if let Some(editor) = workspace.active_item(cx) + if let Some(editor) = workspace + .active_item(cx) .and_then(|active_item| active_item.downcast::()) { workspace.toggle_modal(cx, |cx, _| { @@ -101,7 +102,7 @@ impl GoToLine { ) { match event { editor::Event::Blurred => cx.emit(Event::Dismissed), - editor::Event::Edited => { + editor::Event::BufferEdited { .. } => { let line_editor = self.line_editor.read(cx).buffer().read(cx).read(cx).text(); let mut components = line_editor.trim().split(&[',', ':'][..]); let row = components.next().and_then(|row| row.parse::().ok()); @@ -148,7 +149,7 @@ impl View for GoToLine { } fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - let theme = &cx.app_state::().theme.selector; + let theme = &cx.global::().theme.selector; let label = format!( "{},{} of {} lines", diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 89cf9afba2c9c2c831fc0e280992ded440d4a529..7f0c3ffcdeeb1c9696d94dd2279b090eb29bfcdf 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -8,6 +8,7 @@ use crate::{ AssetCache, AssetSource, ClipboardItem, FontCache, PathPromptOptions, TextLayoutCache, }; use anyhow::{anyhow, Result}; +use collections::btree_map; use keymap::MatchResult; use lazy_static::lazy_static; use parking_lot::Mutex; @@ -93,6 +94,8 @@ pub trait UpgradeModelHandle { pub trait UpgradeViewHandle { fn upgrade_view_handle(&self, handle: &WeakViewHandle) -> Option>; + + fn upgrade_any_view_handle(&self, handle: &AnyWeakViewHandle) -> Option; } pub trait ReadView { @@ -182,6 +185,12 @@ macro_rules! action { Box::new(self.clone()) } } + + impl From<$arg> for $name { + fn from(arg: $arg) -> Self { + Self(arg) + } + } }; ($name:ident) => { @@ -433,13 +442,32 @@ impl TestAppContext { } pub fn dispatch_keystroke( - &self, + &mut self, window_id: usize, - responder_chain: Vec, - keystroke: &Keystroke, - ) -> Result { - let mut state = self.cx.borrow_mut(); - state.dispatch_keystroke(window_id, responder_chain, keystroke) + keystroke: Keystroke, + input: Option, + is_held: bool, + ) { + self.cx.borrow_mut().update(|cx| { + let presenter = cx + .presenters_and_platform_windows + .get(&window_id) + .unwrap() + .0 + .clone(); + let responder_chain = presenter.borrow().dispatch_path(cx.as_ref()); + + if !cx.dispatch_keystroke(window_id, responder_chain, &keystroke) { + presenter.borrow_mut().dispatch_event( + Event::KeyDown { + keystroke, + input, + is_held, + }, + cx, + ); + } + }); } pub fn add_model(&mut self, build_model: F) -> ModelHandle @@ -494,7 +522,7 @@ impl TestAppContext { pub fn update T>(&mut self, callback: F) -> T { let mut state = self.cx.borrow_mut(); - // Don't increment pending flushes in order to effects to be flushed before the callback + // Don't increment pending flushes in order for effects to be flushed before the callback // completes, which is helpful in tests. let result = callback(&mut *state); // Flush effects after the callback just in case there are any. This can happen in edge @@ -595,6 +623,14 @@ impl AsyncAppContext { self.update(|cx| cx.add_model(build_model)) } + pub fn add_view(&mut self, window_id: usize, build_view: F) -> ViewHandle + where + T: View, + F: FnOnce(&mut ViewContext) -> T, + { + self.update(|cx| cx.add_view(window_id, build_view)) + } + pub fn platform(&self) -> Arc { self.0.borrow().platform() } @@ -639,6 +675,10 @@ impl UpgradeViewHandle for AsyncAppContext { fn upgrade_view_handle(&self, handle: &WeakViewHandle) -> Option> { self.0.borrow_mut().upgrade_view_handle(handle) } + + fn upgrade_any_view_handle(&self, handle: &AnyWeakViewHandle) -> Option { + self.0.borrow_mut().upgrade_any_view_handle(handle) + } } impl ReadModelWith for AsyncAppContext { @@ -742,6 +782,7 @@ type GlobalActionCallback = dyn FnMut(&dyn AnyAction, &mut MutableAppContext); type SubscriptionCallback = Box bool>; type GlobalSubscriptionCallback = Box; type ObservationCallback = Box bool>; +type GlobalObservationCallback = Box; type ReleaseObservationCallback = Box; pub struct MutableAppContext { @@ -761,12 +802,15 @@ pub struct MutableAppContext { global_subscriptions: Arc>>>>, observations: Arc>>>>, + global_observations: + Arc>>>>, release_observations: Arc>>>, presenters_and_platform_windows: HashMap>, Box)>, foreground: Rc, pending_effects: VecDeque, pending_notifications: HashSet, + pending_global_notifications: HashSet, pending_flushes: usize, flushing_effects: bool, next_cursor_style_handle_id: Arc, @@ -791,7 +835,7 @@ impl MutableAppContext { models: Default::default(), views: Default::default(), windows: Default::default(), - app_states: Default::default(), + globals: Default::default(), element_states: Default::default(), ref_counts: Arc::new(Mutex::new(ref_counts)), background, @@ -810,10 +854,12 @@ impl MutableAppContext { global_subscriptions: Default::default(), observations: Default::default(), release_observations: Default::default(), + global_observations: Default::default(), presenters_and_platform_windows: HashMap::new(), foreground, pending_effects: VecDeque::new(), pending_notifications: HashSet::new(), + pending_global_notifications: HashSet::new(), pending_flushes: 0, flushing_effects: false, next_cursor_style_handle_id: Default::default(), @@ -1090,21 +1136,18 @@ impl MutableAppContext { E: Any, 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::(); - self.global_subscriptions - .lock() - .entry(type_id) - .or_default() - .insert( - id, - Some(Box::new(move |payload, cx| { - let payload = payload.downcast_ref().expect("downcast is type safe"); - callback(payload, cx) - })), - ); + self.pending_effects.push_back(Effect::GlobalSubscription { + type_id, + subscription_id, + callback: Box::new(move |payload, cx| { + let payload = payload.downcast_ref().expect("downcast is type safe"); + callback(payload, cx) + }), + }); Subscription::GlobalSubscription { - id, + id: subscription_id, type_id, subscriptions: Some(Arc::downgrade(&self.global_subscriptions)), } @@ -1130,25 +1173,22 @@ impl MutableAppContext { H: Handle, 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(); - self.subscriptions - .lock() - .entry(handle.id()) - .or_default() - .insert( - id, - Some(Box::new(move |payload, cx| { - if let Some(emitter) = H::upgrade_from(&emitter, cx.as_ref()) { - let payload = payload.downcast_ref().expect("downcast is type safe"); - callback(emitter, payload, cx) - } else { - false - } - })), - ); + self.pending_effects.push_back(Effect::Subscription { + entity_id: handle.id(), + subscription_id, + callback: Box::new(move |payload, cx| { + if let Some(emitter) = H::upgrade_from(&emitter, cx.as_ref()) { + let payload = payload.downcast_ref().expect("downcast is type safe"); + callback(emitter, payload, cx) + } else { + false + } + }), + }); Subscription::Subscription { - id, + id: subscription_id, entity_id: handle.id(), subscriptions: Some(Arc::downgrade(&self.subscriptions)), } @@ -1161,26 +1201,52 @@ impl MutableAppContext { H: Handle, 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(); - self.observations + let entity_id = handle.id(); + self.pending_effects.push_back(Effect::Observation { + entity_id, + subscription_id, + callback: Box::new(move |cx| { + if let Some(observed) = H::upgrade_from(&observed, cx) { + callback(observed, cx) + } else { + false + } + }), + }); + Subscription::Observation { + id: subscription_id, + entity_id, + observations: Some(Arc::downgrade(&self.observations)), + } + } + + pub fn observe_global(&mut self, mut observe: F) -> Subscription + where + G: Any, + F: 'static + FnMut(&G, &mut MutableAppContext), + { + let type_id = TypeId::of::(); + let id = post_inc(&mut self.next_subscription_id); + + self.global_observations .lock() - .entry(handle.id()) + .entry(type_id) .or_default() .insert( id, - Some(Box::new(move |cx| { - if let Some(observed) = H::upgrade_from(&observed, cx) { - callback(observed, cx) - } else { - false - } - })), + Some( + Box::new(move |global: &dyn Any, cx: &mut MutableAppContext| { + observe(global.downcast_ref().unwrap(), cx) + }) as GlobalObservationCallback, + ), ); - Subscription::Observation { + + Subscription::GlobalObservation { id, - entity_id: handle.id(), - observations: Some(Arc::downgrade(&self.observations)), + type_id, + observations: Some(Arc::downgrade(&self.global_observations)), } } @@ -1210,8 +1276,18 @@ impl MutableAppContext { } } - fn defer(&mut self, callback: Box) { - self.pending_effects.push_back(Effect::Deferred(callback)) + pub fn defer(&mut self, callback: impl 'static + FnOnce(&mut MutableAppContext)) { + self.pending_effects.push_back(Effect::Deferred { + callback: Box::new(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) { @@ -1228,6 +1304,13 @@ impl MutableAppContext { } } + pub(crate) fn notify_global(&mut self, type_id: TypeId) { + if self.pending_global_notifications.insert(type_id) { + self.pending_effects + .push_back(Effect::GlobalNotification { type_id }); + } + } + pub fn dispatch_action( &mut self, window_id: usize, @@ -1322,17 +1405,15 @@ impl MutableAppContext { window_id: usize, responder_chain: Vec, keystroke: &Keystroke, - ) -> Result { + ) -> bool { let mut context_chain = Vec::new(); for view_id in &responder_chain { - if let Some(view) = self.cx.views.get(&(window_id, *view_id)) { - context_chain.push(view.keymap_context(self.as_ref())); - } else { - return Err(anyhow!( - "View {} in responder chain does not exist", - view_id - )); - } + let view = self + .cx + .views + .get(&(window_id, *view_id)) + .expect("view in responder chain does not exist"); + context_chain.push(view.keymap_context(self.as_ref())); } let mut pending = false; @@ -1347,34 +1428,74 @@ impl MutableAppContext { if self.dispatch_action_any(window_id, &responder_chain[0..=i], action.as_ref()) { self.keystroke_matcher.clear_pending(); - return Ok(true); + return true; } } } } - Ok(pending) + pending } - pub fn add_app_state(&mut self, state: T) { - self.cx - .app_states - .insert(TypeId::of::(), Box::new(state)); + pub fn default_global(&mut self) -> &T { + let type_id = TypeId::of::(); + self.update(|this| { + if !this.globals.contains_key(&type_id) { + this.notify_global(type_id); + } + + this.cx + .globals + .entry(type_id) + .or_insert_with(|| Box::new(T::default())); + }); + self.globals.get(&type_id).unwrap().downcast_ref().unwrap() } - pub fn update_app_state(&mut self, update: F) -> U + pub fn set_global(&mut self, state: T) { + self.update(|this| { + let type_id = TypeId::of::(); + this.cx.globals.insert(type_id, Box::new(state)); + this.notify_global(type_id); + }); + } + + pub fn update_default_global(&mut self, update: F) -> U where + T: 'static + Default, F: FnOnce(&mut T, &mut MutableAppContext) -> U, { - let type_id = TypeId::of::(); - let mut state = self - .cx - .app_states - .remove(&type_id) - .expect("no app state has been added for this type"); - let result = update(state.downcast_mut().unwrap(), self); - self.cx.app_states.insert(type_id, state); - result + self.update(|this| { + let type_id = TypeId::of::(); + let mut state = this + .cx + .globals + .remove(&type_id) + .unwrap_or_else(|| Box::new(T::default())); + let result = update(state.downcast_mut().unwrap(), this); + this.cx.globals.insert(type_id, state); + this.notify_global(type_id); + result + }) + } + + pub fn update_global(&mut self, update: F) -> U + where + T: 'static, + F: FnOnce(&mut T, &mut MutableAppContext) -> U, + { + self.update(|this| { + let type_id = TypeId::of::(); + let mut state = this + .cx + .globals + .remove(&type_id) + .expect("no global has been added for this type"); + let result = update(state.downcast_mut().unwrap(), this); + this.cx.globals.insert(type_id, state); + this.notify_global(type_id); + result + }) } pub fn add_model(&mut self, build_model: F) -> ModelHandle @@ -1413,11 +1534,10 @@ impl MutableAppContext { invalidation: None, }, ); - this.open_platform_window(window_id, window_options); root_view.update(this, |view, cx| { view.on_focus(cx); - cx.notify(); }); + this.open_platform_window(window_id, window_options); (window_id, root_view) }) @@ -1444,14 +1564,11 @@ impl MutableAppContext { window.on_event(Box::new(move |event| { app.update(|cx| { if let Event::KeyDown { keystroke, .. } = &event { - if cx - .dispatch_keystroke( - window_id, - presenter.borrow().dispatch_path(cx.as_ref()), - keystroke, - ) - .unwrap() - { + if cx.dispatch_keystroke( + window_id, + presenter.borrow().dispatch_path(cx.as_ref()), + keystroke, + ) { return; } } @@ -1475,6 +1592,11 @@ impl MutableAppContext { })); } + let scene = + presenter + .borrow_mut() + .build_scene(window.size(), window.scale_factor(), false, self); + window.present_scene(scene); self.presenters_and_platform_windows .insert(window_id, (presenter.clone(), window)); } @@ -1599,6 +1721,7 @@ impl MutableAppContext { fn flush_effects(&mut self) { 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 { self.flushing_effects = true; @@ -1607,15 +1730,46 @@ impl MutableAppContext { loop { if let Some(effect) = self.pending_effects.pop_front() { 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::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::Observation { + entity_id, + subscription_id, + callback, + } => self.handle_observation_effect(entity_id, subscription_id, callback), Effect::ModelNotification { model_id } => { self.notify_model_observers(model_id) } Effect::ViewNotification { window_id, view_id } => { self.notify_view_observers(window_id, view_id) } - Effect::Deferred(callback) => callback(self), + Effect::GlobalNotification { type_id } => { + self.notify_global_observers(type_id) + } + Effect::Deferred { + callback, + after_window_update, + } => { + if after_window_update { + after_window_update_callbacks.push(callback); + } else { + callback(self) + } + } Effect::ModelRelease { model_id, model } => { self.notify_release_observers(model_id, model.as_any()) } @@ -1647,12 +1801,19 @@ impl MutableAppContext { } if self.pending_effects.is_empty() { - self.flushing_effects = false; - self.pending_notifications.clear(); - break; - } else { - refreshing = false; + for callback in after_window_update_callbacks.drain(..) { + callback(self); + } + + if self.pending_effects.is_empty() { + self.flushing_effects = false; + self.pending_notifications.clear(); + self.pending_global_notifications.clear(); + break; + } } + + refreshing = false; } } } @@ -1666,13 +1827,13 @@ impl MutableAppContext { } } - for (window_id, invalidation) in invalidations { + for (window_id, mut invalidation) in invalidations { if let Some((presenter, mut window)) = self.presenters_and_platform_windows.remove(&window_id) { { let mut presenter = presenter.borrow_mut(); - presenter.invalidate(invalidation, self); + presenter.invalidate(&mut invalidation, self); let scene = presenter.build_scene(window.size(), window.scale_factor(), false, self); window.present_scene(scene); @@ -1695,7 +1856,7 @@ impl MutableAppContext { fn perform_window_refresh(&mut self) { let mut presenters = mem::take(&mut self.presenters_and_platform_windows); for (window_id, (presenter, window)) in &mut presenters { - let invalidation = self + let mut invalidation = self .cx .windows .get_mut(&window_id) @@ -1703,7 +1864,10 @@ impl MutableAppContext { .invalidation .take(); let mut presenter = presenter.borrow_mut(); - presenter.refresh(invalidation, self); + presenter.refresh( + invalidation.as_mut().unwrap_or(&mut Default::default()), + self, + ); let scene = presenter.build_scene(window.size(), window.scale_factor(), true, self); window.present_scene(scene); } @@ -1720,6 +1884,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) { let callbacks = self.subscriptions.lock().remove(&entity_id); if let Some(callbacks) = callbacks { @@ -1734,10 +1922,10 @@ impl MutableAppContext { .or_default() .entry(id) { - collections::btree_map::Entry::Vacant(entry) => { + btree_map::Entry::Vacant(entry) => { entry.insert(Some(callback)); } - collections::btree_map::Entry::Occupied(entry) => { + btree_map::Entry::Occupied(entry) => { entry.remove(); } } @@ -1747,6 +1935,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) { let type_id = (&*payload).type_id(); let callbacks = self.global_subscriptions.lock().remove(&type_id); @@ -1761,10 +1973,10 @@ impl MutableAppContext { .or_default() .entry(id) { - collections::btree_map::Entry::Vacant(entry) => { + btree_map::Entry::Vacant(entry) => { entry.insert(Some(callback)); } - collections::btree_map::Entry::Occupied(entry) => { + btree_map::Entry::Occupied(entry) => { entry.remove(); } } @@ -1773,6 +1985,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) { let callbacks = self.observations.lock().remove(&observed_id); if let Some(callbacks) = callbacks { @@ -1788,10 +2024,10 @@ impl MutableAppContext { .or_default() .entry(id) { - collections::btree_map::Entry::Vacant(entry) => { + btree_map::Entry::Vacant(entry) => { entry.insert(Some(callback)); } - collections::btree_map::Entry::Occupied(entry) => { + btree_map::Entry::Occupied(entry) => { entry.remove(); } } @@ -1829,10 +2065,10 @@ impl MutableAppContext { .or_default() .entry(id) { - collections::btree_map::Entry::Vacant(entry) => { + btree_map::Entry::Vacant(entry) => { entry.insert(Some(callback)); } - collections::btree_map::Entry::Occupied(entry) => { + btree_map::Entry::Occupied(entry) => { entry.remove(); } } @@ -1843,6 +2079,34 @@ impl MutableAppContext { } } + fn notify_global_observers(&mut self, observed_type_id: TypeId) { + let callbacks = self.global_observations.lock().remove(&observed_type_id); + if let Some(callbacks) = callbacks { + if let Some(global) = self.cx.globals.remove(&observed_type_id) { + for (id, callback) in callbacks { + if let Some(mut callback) = callback { + callback(global.as_ref(), self); + match self + .global_observations + .lock() + .entry(observed_type_id) + .or_default() + .entry(id) + { + collections::btree_map::Entry::Vacant(entry) => { + entry.insert(Some(callback)); + } + collections::btree_map::Entry::Occupied(entry) => { + entry.remove(); + } + } + } + } + self.cx.globals.insert(observed_type_id, global); + } + } + } + fn notify_release_observers(&mut self, entity_id: usize, entity: &dyn Any) { let callbacks = self.release_observations.lock().remove(&entity_id); if let Some(callbacks) = callbacks { @@ -1978,6 +2242,10 @@ impl UpgradeViewHandle for MutableAppContext { fn upgrade_view_handle(&self, handle: &WeakViewHandle) -> Option> { self.cx.upgrade_view_handle(handle) } + + fn upgrade_any_view_handle(&self, handle: &AnyWeakViewHandle) -> Option { + self.cx.upgrade_any_view_handle(handle) + } } impl ReadView for MutableAppContext { @@ -2039,7 +2307,7 @@ pub struct AppContext { models: HashMap>, views: HashMap<(usize, usize), Box>, windows: HashMap, - app_states: HashMap>, + globals: HashMap>, element_states: HashMap>, background: Arc, ref_counts: Arc>, @@ -2072,8 +2340,12 @@ impl AppContext { &self.platform } - pub fn app_state(&self) -> &T { - self.app_states + pub fn has_global(&self) -> bool { + self.globals.contains_key(&TypeId::of::()) + } + + pub fn global(&self) -> &T { + self.globals .get(&TypeId::of::()) .expect("no app state has been added for this type") .downcast_ref() @@ -2135,6 +2407,19 @@ impl UpgradeViewHandle for AppContext { None } } + + fn upgrade_any_view_handle(&self, handle: &AnyWeakViewHandle) -> Option { + 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 { @@ -2162,13 +2447,28 @@ pub struct WindowInvalidation { } pub enum Effect { + Subscription { + entity_id: usize, + subscription_id: usize, + callback: SubscriptionCallback, + }, Event { entity_id: usize, payload: Box, }, + GlobalSubscription { + type_id: TypeId, + subscription_id: usize, + callback: GlobalSubscriptionCallback, + }, GlobalEvent { payload: Box, }, + Observation { + entity_id: usize, + subscription_id: usize, + callback: ObservationCallback, + }, ModelNotification { model_id: usize, }, @@ -2176,7 +2476,13 @@ pub enum Effect { window_id: usize, view_id: usize, }, - Deferred(Box), + Deferred { + callback: Box, + after_window_update: bool, + }, + GlobalNotification { + type_id: TypeId, + }, ModelRelease { model_id: usize, model: Box, @@ -2198,14 +2504,41 @@ pub enum Effect { impl Debug for Effect { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 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 .debug_struct("Effect::Event") .field("entity_id", entity_id) .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 .debug_struct("Effect::GlobalEvent") .field("type_id", &(&*payload).type_id()) .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 .debug_struct("Effect::ModelNotification") .field("model_id", model_id) @@ -2215,7 +2548,11 @@ impl Debug for Effect { .field("window_id", window_id) .field("view_id", view_id) .finish(), - Effect::Deferred(_) => f.debug_struct("Effect::Deferred").finish(), + Effect::GlobalNotification { type_id } => f + .debug_struct("Effect::GlobalNotification") + .field("type_id", type_id) + .finish(), + Effect::Deferred { .. } => f.debug_struct("Effect::Deferred").finish(), Effect::ModelRelease { model_id, .. } => f .debug_struct("Effect::ModelRelease") .field("model_id", model_id) @@ -2394,6 +2731,15 @@ impl<'a, T: Entity> ModelContext<'a, T> { self.app.add_model(build_model) } + pub fn defer(&mut self, callback: impl 'static + FnOnce(&mut T, &mut ModelContext)) { + let handle = self.handle(); + self.app.defer(move |cx| { + handle.update(cx, |model, cx| { + callback(model, cx); + }) + }) + } + pub fn emit(&mut self, payload: T::Event) { self.app.pending_effects.push_back(Effect::Event { entity_id: self.model_id, @@ -2740,11 +3086,23 @@ impl<'a, T: View> ViewContext<'a, T> { pub fn defer(&mut self, callback: impl 'static + FnOnce(&mut T, &mut ViewContext)) { let handle = self.handle(); - self.app.defer(Box::new(move |cx| { + self.app.defer(move |cx| { handle.update(cx, |view, cx| { callback(view, cx); }) - })) + }) + } + + pub fn after_window_update( + &mut self, + callback: impl 'static + FnOnce(&mut T, &mut ViewContext), + ) { + 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) { @@ -2892,6 +3250,10 @@ impl UpgradeViewHandle for ViewContext<'_, V> { fn upgrade_view_handle(&self, handle: &WeakViewHandle) -> Option> { self.cx.upgrade_view_handle(handle) } + + fn upgrade_any_view_handle(&self, handle: &AnyWeakViewHandle) -> Option { + self.cx.upgrade_any_view_handle(handle) + } } impl UpdateModel for ViewContext<'_, V> { @@ -3338,9 +3700,9 @@ impl ViewHandle { F: 'static + FnOnce(&mut T, &mut ViewContext), { let this = self.clone(); - cx.as_mut().defer(Box::new(move |cx| { + cx.as_mut().defer(move |cx| { this.update(cx, |view, cx| update(view, cx)); - })); + }); } pub fn is_focused(&self, cx: &AppContext) -> bool { @@ -3452,8 +3814,27 @@ impl PartialEq for ViewHandle { } } +impl PartialEq> for ViewHandle { + fn eq(&self, other: &WeakViewHandle) -> bool { + self.window_id == other.window_id && self.view_id == other.view_id + } +} + +impl PartialEq> for WeakViewHandle { + fn eq(&self, other: &ViewHandle) -> bool { + self.window_id == other.window_id && self.view_id == other.view_id + } +} + impl Eq for ViewHandle {} +impl Hash for ViewHandle { + fn hash(&self, state: &mut H) { + self.window_id.hash(state); + self.view_id.hash(state); + } +} + impl Debug for ViewHandle { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct(&format!("ViewHandle<{}>", type_name::())) @@ -3568,6 +3949,18 @@ impl AnyViewHandle { 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 { @@ -3690,6 +4083,10 @@ impl AnyModelHandle { pub fn is(&self) -> bool { self.model_type == TypeId::of::() } + + pub fn model_type(&self) -> TypeId { + self.model_type + } } impl From> for AnyModelHandle { @@ -3790,6 +4187,28 @@ impl Hash for WeakViewHandle { } } +pub struct AnyWeakViewHandle { + window_id: usize, + view_id: usize, + view_type: TypeId, +} + +impl AnyWeakViewHandle { + pub fn upgrade(&self, cx: &impl UpgradeViewHandle) -> Option { + cx.upgrade_any_view_handle(self) + } +} + +impl From> for AnyWeakViewHandle { + fn from(handle: WeakViewHandle) -> Self { + AnyWeakViewHandle { + window_id: handle.window_id, + view_id: handle.view_id, + view_type: TypeId::of::(), + } + } +} + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct ElementStateId { view_id: usize, @@ -3878,6 +4297,13 @@ pub enum Subscription { observations: Option>>>>>, }, + GlobalObservation { + id: usize, + type_id: TypeId, + observations: Option< + Weak>>>>, + >, + }, ReleaseObservation { id: usize, entity_id: usize, @@ -3898,6 +4324,9 @@ impl Subscription { Subscription::Observation { observations, .. } => { observations.take(); } + Subscription::GlobalObservation { observations, .. } => { + observations.take(); + } Subscription::ReleaseObservation { observations, .. } => { observations.take(); } @@ -3920,10 +4349,10 @@ impl Drop for Subscription { .or_default() .entry(*id) { - collections::btree_map::Entry::Vacant(entry) => { + btree_map::Entry::Vacant(entry) => { entry.insert(None); } - collections::btree_map::Entry::Occupied(entry) => { + btree_map::Entry::Occupied(entry) => { entry.remove(); } } @@ -3936,10 +4365,10 @@ impl Drop for Subscription { } => { if let Some(subscriptions) = subscriptions.as_ref().and_then(Weak::upgrade) { match subscriptions.lock().entry(*type_id).or_default().entry(*id) { - collections::btree_map::Entry::Vacant(entry) => { + btree_map::Entry::Vacant(entry) => { entry.insert(None); } - collections::btree_map::Entry::Occupied(entry) => { + btree_map::Entry::Occupied(entry) => { entry.remove(); } } @@ -3957,6 +4386,22 @@ impl Drop for Subscription { .or_default() .entry(*id) { + btree_map::Entry::Vacant(entry) => { + entry.insert(None); + } + btree_map::Entry::Occupied(entry) => { + entry.remove(); + } + } + } + } + Subscription::GlobalObservation { + id, + type_id, + observations, + } => { + if let Some(observations) = observations.as_ref().and_then(Weak::upgrade) { + match observations.lock().entry(*type_id).or_default().entry(*id) { collections::btree_map::Entry::Vacant(entry) => { entry.insert(None); } @@ -4165,7 +4610,7 @@ mod tests { use smol::future::poll_once; use std::{ cell::Cell, - sync::atomic::{AtomicUsize, Ordering::SeqCst}, + sync::atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst}, }; #[crate::test(self)] @@ -4242,6 +4687,7 @@ mod tests { let handle_1 = cx.add_model(|_| Model::default()); let handle_2 = cx.add_model(|_| Model::default()); + handle_1.update(cx, |_, cx| { cx.subscribe(&handle_2, move |model: &mut Model, emitter, event, cx| { model.events.push(*event); @@ -4261,6 +4707,37 @@ mod tests { 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)] fn test_observe_and_notify_from_model(cx: &mut MutableAppContext) { #[derive(Default)] @@ -4300,6 +4777,89 @@ mod tests { 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) -> 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)] fn test_view_handles(cx: &mut MutableAppContext) { struct View { @@ -4594,6 +5154,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)] fn test_global_nested_events(cx: &mut MutableAppContext) { #[derive(Clone, Debug, Eq, PartialEq)] @@ -4606,11 +5201,13 @@ mod tests { cx.subscribe_global(move |e: &GlobalEvent, cx| { events.borrow_mut().push(("Outer", e.clone())); - let events = events.clone(); - cx.subscribe_global(move |e: &GlobalEvent, _| { - events.borrow_mut().push(("Inner", e.clone())); - }) - .detach(); + if e.0 == 1 { + let events = events.clone(); + cx.subscribe_global(move |e: &GlobalEvent, _| { + events.borrow_mut().push(("Inner", e.clone())); + }) + .detach(); + } }) .detach(); } @@ -4620,20 +5217,77 @@ mod tests { cx.emit_global(GlobalEvent(2)); cx.emit_global(GlobalEvent(3)); }); + cx.update(|cx| { + cx.emit_global(GlobalEvent(4)); + }); assert_eq!( &*events.borrow(), &[ ("Outer", GlobalEvent(1)), ("Outer", GlobalEvent(2)), - ("Inner", GlobalEvent(2)), ("Outer", GlobalEvent(3)), - ("Inner", GlobalEvent(3)), - ("Inner", GlobalEvent(3)), + ("Outer", GlobalEvent(4)), + ("Inner", GlobalEvent(4)), ] ); } + #[crate::test(self)] + fn test_global(cx: &mut MutableAppContext) { + type Global = usize; + + let observation_count = Rc::new(RefCell::new(0)); + let subscription = cx.observe_global::({ + let observation_count = observation_count.clone(); + move |_, _| { + *observation_count.borrow_mut() += 1; + } + }); + + assert!(!cx.has_global::()); + assert_eq!(cx.default_global::(), &0); + assert_eq!(*observation_count.borrow(), 1); + assert!(cx.has_global::()); + assert_eq!( + cx.update_global::(|global, _| { + *global = 1; + "Update Result" + }), + "Update Result" + ); + assert_eq!(*observation_count.borrow(), 2); + assert_eq!(cx.global::(), &1); + + drop(subscription); + cx.update_global::(|global, _| { + *global = 2; + }); + assert_eq!(*observation_count.borrow(), 2); + + type OtherGlobal = f32; + + let observation_count = Rc::new(RefCell::new(0)); + cx.observe_global::({ + let observation_count = observation_count.clone(); + move |_, _| { + *observation_count.borrow_mut() += 1; + } + }) + .detach(); + + assert_eq!( + cx.update_default_global::(|global, _| { + assert_eq!(global, &0.0); + *global = 2.0; + "Default update result" + }), + "Default update result" + ); + assert_eq!(cx.global::(), &2.0); + assert_eq!(*observation_count.borrow(), 1); + } + #[crate::test(self)] fn test_dropping_subscribers(cx: &mut MutableAppContext) { struct View; @@ -4681,6 +5335,47 @@ mod tests { 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) -> 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)] fn test_observe_and_notify_from_view(cx: &mut MutableAppContext) { #[derive(Default)] @@ -4728,6 +5423,47 @@ mod tests { 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) -> 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)] fn test_dropping_observers(cx: &mut MutableAppContext) { struct View; @@ -4901,6 +5637,22 @@ mod tests { }); assert_eq!(*observation_count.borrow(), 1); + + // Global Observation + let observation_count = Rc::new(RefCell::new(0)); + let subscription = Rc::new(RefCell::new(None)); + *subscription.borrow_mut() = Some(cx.observe_global::<(), _>({ + let observation_count = observation_count.clone(); + let subscription = subscription.clone(); + move |_, _| { + subscription.borrow_mut().take(); + *observation_count.borrow_mut() += 1; + } + })); + + cx.default_global::<()>(); + cx.set_global(()); + assert_eq!(*observation_count.borrow(), 1); } #[crate::test(self)] @@ -5191,8 +5943,7 @@ mod tests { window_id, vec![view_1.id(), view_2.id(), view_3.id()], &Keystroke::parse("a").unwrap(), - ) - .unwrap(); + ); assert_eq!(&*actions.borrow(), &["2 a"]); @@ -5201,8 +5952,7 @@ mod tests { window_id, vec![view_1.id(), view_2.id(), view_3.id()], &Keystroke::parse("b").unwrap(), - ) - .unwrap(); + ); assert_eq!(&*actions.borrow(), &["3 b", "2 b", "1 b", "global b"]); } @@ -5363,4 +6113,65 @@ mod tests { cx.update(|_| drop(view)); condition.await; } + + #[crate::test(self)] + fn test_refresh_windows(cx: &mut MutableAppContext) { + struct View(usize); + + impl super::Entity for View { + type Event = (); + } + + impl super::View for View { + fn ui_name() -> &'static str { + "test view" + } + + fn render(&mut self, _: &mut RenderContext) -> ElementBox { + Empty::new().named(format!("render count: {}", post_inc(&mut self.0))) + } + } + + let (window_id, root_view) = cx.add_window(Default::default(), |_| View(0)); + let presenter = cx.presenters_and_platform_windows[&window_id].0.clone(); + + assert_eq!( + presenter.borrow().rendered_views[&root_view.id()].name(), + Some("render count: 0") + ); + + let view = cx.add_view(window_id, |cx| { + cx.refresh_windows(); + View(0) + }); + + assert_eq!( + presenter.borrow().rendered_views[&root_view.id()].name(), + Some("render count: 1") + ); + assert_eq!( + presenter.borrow().rendered_views[&view.id()].name(), + Some("render count: 0") + ); + + cx.update(|cx| cx.refresh_windows()); + assert_eq!( + presenter.borrow().rendered_views[&root_view.id()].name(), + Some("render count: 2") + ); + assert_eq!( + presenter.borrow().rendered_views[&view.id()].name(), + Some("render count: 1") + ); + + cx.update(|cx| { + cx.refresh_windows(); + drop(view); + }); + assert_eq!( + presenter.borrow().rendered_views[&root_view.id()].name(), + Some("render count: 3") + ); + assert_eq!(presenter.borrow().rendered_views.len(), 1); + } } diff --git a/crates/gpui/src/executor.rs b/crates/gpui/src/executor.rs index 2089b954fb83376237d692993f731e9ab8f2fb31..2c3a8df8a1468a127589bef5ea8f12ec427b8e28 100644 --- a/crates/gpui/src/executor.rs +++ b/crates/gpui/src/executor.rs @@ -337,7 +337,7 @@ impl Deterministic { if let Some((_, wakeup_time, _)) = state.pending_timers.first() { let wakeup_time = *wakeup_time; - if wakeup_time < new_now { + if wakeup_time <= new_now { let timer_count = state .pending_timers .iter() diff --git a/crates/gpui/src/keymap.rs b/crates/gpui/src/keymap.rs index 05fbd5b74b4d4e4c2a0b0595a75d4859b7fa0106..37223d77d1e74cbf1694e643b7c7dfcb6177713c 100644 --- a/crates/gpui/src/keymap.rs +++ b/crates/gpui/src/keymap.rs @@ -224,15 +224,19 @@ impl Keystroke { key: key.unwrap(), }) } + + pub fn modified(&self) -> bool { + self.ctrl || self.alt || self.shift || self.cmd + } } impl Context { - pub fn extend(&mut self, other: Context) { - for v in other.set { - self.set.insert(v); + pub fn extend(&mut self, other: &Context) { + for v in &other.set { + self.set.insert(v.clone()); } - for (k, v) in other.map { - self.map.insert(k, v); + for (k, v) in &other.map { + self.map.insert(k.clone(), v.clone()); } } } diff --git a/crates/gpui/src/platform/mac/renderer.rs b/crates/gpui/src/platform/mac/renderer.rs index 07d425af3ebd4235a178ca479a21052d0d71b043..873586b61e35b6e1c5eebd0728213feb0bc1ebc0 100644 --- a/crates/gpui/src/platform/mac/renderer.rs +++ b/crates/gpui/src/platform/mac/renderer.rs @@ -561,9 +561,10 @@ impl Renderer { } for icon in icons { - let origin = icon.bounds.origin() * scale_factor; - let target_size = icon.bounds.size() * scale_factor; - let source_size = (target_size * 2.).ceil().to_i32(); + // Snap sprite to pixel grid. + let origin = (icon.bounds.origin() * scale_factor).floor(); + let target_size = (icon.bounds.size() * scale_factor).ceil(); + let source_size = (target_size * 2.).to_i32(); let sprite = self.sprite_cache diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index 5ee5b36f876e8f1dfbbc018c0bf30ef5612ad5e4..b4e419107a32dc14ddd80c9bbf5ba2365ec29aa8 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -20,7 +20,7 @@ use std::{ pub struct Presenter { window_id: usize, - rendered_views: HashMap, + pub(crate) rendered_views: HashMap, parents: HashMap, font_cache: Arc, text_layout_cache: TextLayoutCache, @@ -63,39 +63,34 @@ impl Presenter { path } - pub fn invalidate(&mut self, mut invalidation: WindowInvalidation, cx: &mut MutableAppContext) { + pub fn invalidate( + &mut self, + invalidation: &mut WindowInvalidation, + cx: &mut MutableAppContext, + ) { cx.start_frame(); - for view_id in invalidation.removed { + for view_id in &invalidation.removed { invalidation.updated.remove(&view_id); self.rendered_views.remove(&view_id); self.parents.remove(&view_id); } - for view_id in invalidation.updated { + for view_id in &invalidation.updated { self.rendered_views.insert( - view_id, - cx.render_view(self.window_id, view_id, self.titlebar_height, false) + *view_id, + cx.render_view(self.window_id, *view_id, self.titlebar_height, false) .unwrap(), ); } } - pub fn refresh( - &mut self, - invalidation: Option, - cx: &mut MutableAppContext, - ) { - cx.start_frame(); - if let Some(invalidation) = invalidation { - for view_id in invalidation.removed { - self.rendered_views.remove(&view_id); - self.parents.remove(&view_id); - } - } - + pub fn refresh(&mut self, invalidation: &mut WindowInvalidation, cx: &mut MutableAppContext) { + self.invalidate(invalidation, cx); for (view_id, view) in &mut self.rendered_views { - *view = cx - .render_view(self.window_id, *view_id, self.titlebar_height, true) - .unwrap(); + if !invalidation.updated.contains(view_id) { + *view = cx + .render_view(self.window_id, *view_id, self.titlebar_height, true) + .unwrap(); + } } } @@ -304,6 +299,10 @@ impl<'a> UpgradeViewHandle for LayoutContext<'a> { fn upgrade_view_handle(&self, handle: &WeakViewHandle) -> Option> { self.app.upgrade_view_handle(handle) } + + fn upgrade_any_view_handle(&self, handle: &crate::AnyWeakViewHandle) -> Option { + self.app.upgrade_any_view_handle(handle) + } } impl<'a> ElementStateContext for LayoutContext<'a> { diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 37dc026abf6b9026253ba44c21bcb1678f8d0a8f..36ef7d4a0155ea71ed0cac778efd63a482d548f0 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -271,7 +271,6 @@ pub(crate) struct DiagnosticEndpoint { #[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug)] pub enum CharKind { - Newline, Punctuation, Whitespace, Word, @@ -1621,8 +1620,13 @@ impl BufferSnapshot { let range = range.start.to_offset(self)..range.end.to_offset(self); let mut cursor = tree.root_node().walk(); - // Descend to smallest leaf that touches or exceeds the start of the range. - while cursor.goto_first_child_for_byte(range.start).is_some() {} + // Descend to the first leaf that touches the start of the range, + // and if the range is non-empty, extends beyond the start. + while cursor.goto_first_child_for_byte(range.start).is_some() { + if !range.is_empty() && cursor.node().end_byte() == range.start { + cursor.goto_next_sibling(); + } + } // Ascend to the smallest ancestor that strictly contains the range. loop { @@ -1656,6 +1660,9 @@ impl BufferSnapshot { } } + // If there is a candidate node on both sides of the (empty) range, then + // decide between the two by favoring a named node over an anonymous token. + // If both nodes are the same in that regard, favor the right one. if let Some(right_node) = right_node { if right_node.is_named() || !left_node.is_named() { return Some(right_node.byte_range()); @@ -1822,12 +1829,6 @@ impl BufferSnapshot { .min_by_key(|(open_range, close_range)| close_range.end - open_range.start) } - /* - impl BufferSnapshot - pub fn remote_selections_in_range(&self, Range) -> impl Iterator>)> - pub fn remote_selections_in_range(&self, Range) -> impl Iterator( &'a self, range: Range, @@ -1840,20 +1841,12 @@ impl BufferSnapshot { }) .map(move |(replica_id, set)| { let start_ix = match set.selections.binary_search_by(|probe| { - probe - .end - .cmp(&range.start, self) - .unwrap() - .then(Ordering::Greater) + probe.end.cmp(&range.start, self).then(Ordering::Greater) }) { Ok(ix) | Err(ix) => ix, }; let end_ix = match set.selections.binary_search_by(|probe| { - probe - .start - .cmp(&range.end, self) - .unwrap() - .then(Ordering::Less) + probe.start.cmp(&range.end, self).then(Ordering::Less) }) { Ok(ix) | Err(ix) => ix, }; @@ -2280,9 +2273,7 @@ pub fn contiguous_ranges( } pub fn char_kind(c: char) -> CharKind { - if c == '\n' { - CharKind::Newline - } else if c.is_whitespace() { + if c.is_whitespace() { CharKind::Whitespace } else if c.is_alphanumeric() || c == '_' { CharKind::Word diff --git a/crates/language/src/diagnostic_set.rs b/crates/language/src/diagnostic_set.rs index e25551ee3a6b81317aa5c00d90cb70d89cc3954e..490789a8c80c3abe320d54bc12b88ec3529832cc 100644 --- a/crates/language/src/diagnostic_set.rs +++ b/crates/language/src/diagnostic_set.rs @@ -81,8 +81,8 @@ impl DiagnosticSet { let range = buffer.anchor_before(range.start)..buffer.anchor_at(range.end, end_bias); let mut cursor = self.diagnostics.filter::<_, ()>({ move |summary: &Summary| { - let start_cmp = range.start.cmp(&summary.max_end, buffer).unwrap(); - let end_cmp = range.end.cmp(&summary.min_start, buffer).unwrap(); + let start_cmp = range.start.cmp(&summary.max_end, buffer); + let end_cmp = range.end.cmp(&summary.min_start, buffer); if inclusive { start_cmp <= Ordering::Equal && end_cmp >= Ordering::Equal } else { @@ -123,7 +123,7 @@ impl DiagnosticSet { let start_ix = output.len(); output.extend(groups.into_values().filter_map(|mut entries| { - entries.sort_unstable_by(|a, b| a.range.start.cmp(&b.range.start, buffer).unwrap()); + entries.sort_unstable_by(|a, b| a.range.start.cmp(&b.range.start, buffer)); entries .iter() .position(|entry| entry.diagnostic.is_primary) @@ -137,7 +137,6 @@ impl DiagnosticSet { .range .start .cmp(&b.entries[b.primary_ix].range.start, buffer) - .unwrap() }); } @@ -187,10 +186,10 @@ impl DiagnosticEntry { impl Default for Summary { fn default() -> Self { Self { - start: Anchor::min(), - end: Anchor::max(), - min_start: Anchor::max(), - max_end: Anchor::min(), + start: Anchor::MIN, + end: Anchor::MAX, + min_start: Anchor::MAX, + max_end: Anchor::MIN, count: 0, } } @@ -200,15 +199,10 @@ impl sum_tree::Summary for Summary { type Context = text::BufferSnapshot; fn add_summary(&mut self, other: &Self, buffer: &Self::Context) { - if other - .min_start - .cmp(&self.min_start, buffer) - .unwrap() - .is_lt() - { + if other.min_start.cmp(&self.min_start, buffer).is_lt() { self.min_start = other.min_start.clone(); } - if other.max_end.cmp(&self.max_end, buffer).unwrap().is_gt() { + if other.max_end.cmp(&self.max_end, buffer).is_gt() { self.max_end = other.max_end.clone(); } self.start = other.start.clone(); diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index 4a22d6ce5af061ae248e00788d0d4aa5af62a67e..deedf3e88b596209457b5b50087e4655cb968a10 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -100,15 +100,16 @@ pub fn serialize_undo_map_entry( } pub fn serialize_selections(selections: &Arc<[Selection]>) -> Vec { - selections - .iter() - .map(|selection| proto::Selection { - id: selection.id as u64, - start: Some(serialize_anchor(&selection.start)), - end: Some(serialize_anchor(&selection.end)), - reversed: selection.reversed, - }) - .collect() + selections.iter().map(serialize_selection).collect() +} + +pub fn serialize_selection(selection: &Selection) -> proto::Selection { + proto::Selection { + id: selection.id as u64, + start: Some(serialize_anchor(&selection.start)), + end: Some(serialize_anchor(&selection.end)), + reversed: selection.reversed, + } } pub fn serialize_diagnostics<'a>( @@ -274,19 +275,21 @@ pub fn deserialize_selections(selections: Vec) -> Arc<[Selecti Arc::from( selections .into_iter() - .filter_map(|selection| { - Some(Selection { - id: selection.id as usize, - start: deserialize_anchor(selection.start?)?, - end: deserialize_anchor(selection.end?)?, - reversed: selection.reversed, - goal: SelectionGoal::None, - }) - }) + .filter_map(deserialize_selection) .collect::>(), ) } +pub fn deserialize_selection(selection: proto::Selection) -> Option> { + Some(Selection { + id: selection.id as usize, + start: deserialize_anchor(selection.start?)?, + end: deserialize_anchor(selection.end?)?, + reversed: selection.reversed, + goal: SelectionGoal::None, + }) +} + pub fn deserialize_diagnostics( diagnostics: Vec, ) -> Arc<[DiagnosticEntry]> { diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index 1cad180f64edc40d73c53798314da3242c2f9c7b..1c15f1e598de36ea618be4128d612cc930194a3f 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -508,6 +508,44 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) { ); } +#[gpui::test] +fn test_range_for_syntax_ancestor(cx: &mut MutableAppContext) { + cx.add_model(|cx| { + let text = "fn a() { b(|c| {}) }"; + let buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); + let snapshot = buffer.snapshot(); + + assert_eq!( + snapshot.range_for_syntax_ancestor(empty_range_at(text, "|")), + Some(range_of(text, "|")) + ); + assert_eq!( + snapshot.range_for_syntax_ancestor(range_of(text, "|")), + Some(range_of(text, "|c|")) + ); + assert_eq!( + snapshot.range_for_syntax_ancestor(range_of(text, "|c|")), + Some(range_of(text, "|c| {}")) + ); + assert_eq!( + snapshot.range_for_syntax_ancestor(range_of(text, "|c| {}")), + Some(range_of(text, "(|c| {})")) + ); + + buffer + }); + + fn empty_range_at(text: &str, part: &str) -> Range { + let start = text.find(part).unwrap(); + start..start + } + + fn range_of(text: &str, part: &str) -> Range { + let start = text.find(part).unwrap(); + start..start + part.len() + } +} + #[gpui::test] fn test_edit_with_autoindent(cx: &mut MutableAppContext) { cx.add_model(|cx| { @@ -839,7 +877,7 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) { for buffer in &buffers { let buffer = buffer.read(cx).snapshot(); let actual_remote_selections = buffer - .remote_selections_in_range(Anchor::min()..Anchor::max()) + .remote_selections_in_range(Anchor::MIN..Anchor::MAX) .map(|(replica_id, selections)| (replica_id, selections.collect::>())) .collect::>(); let expected_remote_selections = active_selections diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index a8ae9e4cd7071632fb30fb9086c574c4940f53be..fad49d2424017eea8f66308d2e061b02c9a41ffe 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -556,7 +556,14 @@ type FakeLanguageServerHandlers = Arc< Mutex< HashMap< &'static str, - Box Vec>, + Box< + dyn Send + + FnMut( + usize, + &[u8], + gpui::AsyncAppContext, + ) -> futures::future::BoxFuture<'static, Vec>, + >, >, >, >; @@ -585,11 +592,16 @@ impl LanguageServer { let (stdout_writer, stdout_reader) = async_pipe::pipe(); let mut fake = FakeLanguageServer::new(stdin_reader, stdout_writer, cx); - fake.handle_request::({ + fake.handle_request::({ let capabilities = capabilities.clone(); - move |_, _| InitializeResult { - capabilities: capabilities.clone(), - ..Default::default() + move |_, _| { + let capabilities = capabilities.clone(); + async move { + InitializeResult { + capabilities, + ..Default::default() + } + } } }); @@ -628,7 +640,8 @@ impl FakeLanguageServer { let response; if let Some(handler) = handlers.lock().get_mut(request.method) { response = - handler(request.id, request.params.get().as_bytes(), cx.clone()); + handler(request.id, request.params.get().as_bytes(), cx.clone()) + .await; log::debug!("handled lsp request. method:{}", request.method); } else { response = serde_json::to_vec(&AnyResponse { @@ -704,28 +717,36 @@ impl FakeLanguageServer { } } - pub fn handle_request( + pub fn handle_request( &mut self, mut handler: F, ) -> futures::channel::mpsc::UnboundedReceiver<()> where T: 'static + request::Request, - F: 'static + Send + FnMut(T::Params, gpui::AsyncAppContext) -> T::Result, + F: 'static + Send + FnMut(T::Params, gpui::AsyncAppContext) -> Fut, + Fut: 'static + Send + Future, { + use futures::FutureExt as _; + let (responded_tx, responded_rx) = futures::channel::mpsc::unbounded(); self.handlers.lock().insert( T::METHOD, Box::new(move |id, params, cx| { let result = handler(serde_json::from_slice::(params).unwrap(), cx); - let result = serde_json::to_string(&result).unwrap(); - let result = serde_json::from_str::<&RawValue>(&result).unwrap(); - let response = AnyResponse { - id, - error: None, - result: Some(result), - }; - responded_tx.unbounded_send(()).ok(); - serde_json::to_vec(&response).unwrap() + let responded_tx = responded_tx.clone(); + async move { + let result = result.await; + let result = serde_json::to_string(&result).unwrap(); + let result = serde_json::from_str::<&RawValue>(&result).unwrap(); + let response = AnyResponse { + id, + error: None, + result: Some(result), + }; + responded_tx.unbounded_send(()).ok(); + serde_json::to_vec(&response).unwrap() + } + .boxed() }), ); responded_rx @@ -844,7 +865,7 @@ mod tests { "file://b/c" ); - fake.handle_request::(|_, _| ()); + fake.handle_request::(|_, _| async move {}); drop(server); fake.receive_notification::().await; diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index 2b4bafa479de3e8b70ac1ded33a7a1f9d4454060..a626ff89c86e4715345db38229717325c1c7b92c 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -69,7 +69,7 @@ impl View for OutlineView { } fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - let settings = cx.app_state::(); + let settings = cx.global::(); Flex::new(Axis::Vertical) .with_child( @@ -124,9 +124,12 @@ impl OutlineView { .active_item(cx) .and_then(|item| item.downcast::()) { - let buffer = editor.read(cx).buffer().read(cx).read(cx).outline(Some( - cx.app_state::().theme.editor.syntax.as_ref(), - )); + let buffer = editor + .read(cx) + .buffer() + .read(cx) + .read(cx) + .outline(Some(cx.global::().theme.editor.syntax.as_ref())); if let Some(outline) = buffer { workspace.toggle_modal(cx, |cx, _| { let view = cx.add_view(|cx| OutlineView::new(outline, editor, cx)); @@ -221,7 +224,7 @@ impl OutlineView { ) { match event { editor::Event::Blurred => cx.emit(Event::Dismissed), - editor::Event::Edited => self.update_matches(cx), + editor::Event::BufferEdited { .. } => self.update_matches(cx), _ => {} } } @@ -288,7 +291,7 @@ impl OutlineView { fn render_matches(&self, cx: &AppContext) -> ElementBox { if self.matches.is_empty() { - let settings = cx.app_state::(); + let settings = cx.global::(); return Container::new( Label::new( "No matches".into(), @@ -330,7 +333,7 @@ impl OutlineView { index: usize, cx: &AppContext, ) -> ElementBox { - let settings = cx.app_state::(); + let settings = cx.global::(); let style = if index == self.selected_match_index { &settings.theme.selector.active_item } else { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index f5af7fc70ea0c0c89ae707ea3b8cde487c686038..404d867069ec383c24dac33f550ac63306468122 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -11,15 +11,15 @@ use collections::{hash_map, BTreeMap, HashMap, HashSet}; use futures::{future::Shared, Future, FutureExt, StreamExt, TryFutureExt}; use fuzzy::{PathMatch, PathMatchCandidate, PathMatchCandidateSet}; use gpui::{ - AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task, - UpgradeModelHandle, WeakModelHandle, + AnyModelHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, + MutableAppContext, Task, UpgradeModelHandle, WeakModelHandle, }; use language::{ proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, range_from_lsp, Anchor, Bias, Buffer, CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Event as BufferEvent, File as _, Language, LanguageRegistry, - LocalFile, OffsetRangeExt, Operation, PointUtf16, TextBufferSnapshot, ToLspPosition, ToOffset, - ToPointUtf16, Transaction, + LocalFile, OffsetRangeExt, Operation, Patch, PointUtf16, TextBufferSnapshot, ToLspPosition, + ToOffset, ToPointUtf16, Transaction, }; use lsp::{DiagnosticSeverity, DiagnosticTag, DocumentHighlightKind, LanguageServer}; use lsp_command::*; @@ -39,7 +39,7 @@ use std::{ path::{Component, Path, PathBuf}, rc::Rc, sync::{ - atomic::{AtomicBool, AtomicUsize}, + atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst}, Arc, }, time::Instant, @@ -49,9 +49,13 @@ use util::{post_inc, ResultExt, TryFutureExt as _}; pub use fs::*; pub use worktree::*; +pub trait Item: Entity { + fn entry_id(&self, cx: &AppContext) -> Option; +} + pub struct Project { worktrees: Vec, - active_entry: Option, + active_entry: Option, languages: Arc, language_servers: HashMap<(WorktreeId, Arc), Arc>, started_language_servers: HashMap<(WorktreeId, Arc), Task>>>, @@ -114,12 +118,14 @@ pub struct Collaborator { #[derive(Clone, Debug, PartialEq)] pub enum Event { - ActiveEntryChanged(Option), + ActiveEntryChanged(Option), WorktreeRemoved(WorktreeId), DiskBasedDiagnosticsStarted, DiskBasedDiagnosticsUpdated, DiskBasedDiagnosticsFinished, DiagnosticsUpdated(ProjectPath), + RemoteIdChanged(Option), + CollaboratorLeft(PeerId), } enum LanguageServerEvent { @@ -226,42 +232,58 @@ impl DiagnosticSummary { } } -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub struct ProjectEntry { - pub worktree_id: WorktreeId, - pub entry_id: usize, +#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub struct ProjectEntryId(usize); + +impl ProjectEntryId { + pub fn new(counter: &AtomicUsize) -> Self { + Self(counter.fetch_add(1, SeqCst)) + } + + pub fn from_proto(id: u64) -> Self { + Self(id as usize) + } + + pub fn to_proto(&self) -> u64 { + self.0 as u64 + } + + pub fn to_usize(&self) -> usize { + self.0 + } } impl Project { pub fn init(client: &Arc) { - client.add_entity_message_handler(Self::handle_add_collaborator); - client.add_entity_message_handler(Self::handle_buffer_reloaded); - client.add_entity_message_handler(Self::handle_buffer_saved); - client.add_entity_message_handler(Self::handle_start_language_server); - client.add_entity_message_handler(Self::handle_update_language_server); - client.add_entity_message_handler(Self::handle_remove_collaborator); - client.add_entity_message_handler(Self::handle_register_worktree); - client.add_entity_message_handler(Self::handle_unregister_worktree); - client.add_entity_message_handler(Self::handle_unshare_project); - client.add_entity_message_handler(Self::handle_update_buffer_file); - client.add_entity_message_handler(Self::handle_update_buffer); - client.add_entity_message_handler(Self::handle_update_diagnostic_summary); - client.add_entity_message_handler(Self::handle_update_worktree); - client.add_entity_request_handler(Self::handle_apply_additional_edits_for_completion); - client.add_entity_request_handler(Self::handle_apply_code_action); - client.add_entity_request_handler(Self::handle_format_buffers); - client.add_entity_request_handler(Self::handle_get_code_actions); - client.add_entity_request_handler(Self::handle_get_completions); - client.add_entity_request_handler(Self::handle_lsp_command::); - client.add_entity_request_handler(Self::handle_lsp_command::); - client.add_entity_request_handler(Self::handle_lsp_command::); - client.add_entity_request_handler(Self::handle_lsp_command::); - client.add_entity_request_handler(Self::handle_lsp_command::); - client.add_entity_request_handler(Self::handle_search_project); - client.add_entity_request_handler(Self::handle_get_project_symbols); - client.add_entity_request_handler(Self::handle_open_buffer_for_symbol); - client.add_entity_request_handler(Self::handle_open_buffer); - client.add_entity_request_handler(Self::handle_save_buffer); + client.add_model_message_handler(Self::handle_add_collaborator); + client.add_model_message_handler(Self::handle_buffer_reloaded); + client.add_model_message_handler(Self::handle_buffer_saved); + client.add_model_message_handler(Self::handle_start_language_server); + client.add_model_message_handler(Self::handle_update_language_server); + client.add_model_message_handler(Self::handle_remove_collaborator); + client.add_model_message_handler(Self::handle_register_worktree); + client.add_model_message_handler(Self::handle_unregister_worktree); + client.add_model_message_handler(Self::handle_unshare_project); + client.add_model_message_handler(Self::handle_update_buffer_file); + client.add_model_message_handler(Self::handle_update_buffer); + client.add_model_message_handler(Self::handle_update_diagnostic_summary); + client.add_model_message_handler(Self::handle_update_worktree); + client.add_model_request_handler(Self::handle_apply_additional_edits_for_completion); + client.add_model_request_handler(Self::handle_apply_code_action); + client.add_model_request_handler(Self::handle_format_buffers); + client.add_model_request_handler(Self::handle_get_code_actions); + client.add_model_request_handler(Self::handle_get_completions); + client.add_model_request_handler(Self::handle_lsp_command::); + client.add_model_request_handler(Self::handle_lsp_command::); + client.add_model_request_handler(Self::handle_lsp_command::); + client.add_model_request_handler(Self::handle_lsp_command::); + client.add_model_request_handler(Self::handle_lsp_command::); + client.add_model_request_handler(Self::handle_search_project); + client.add_model_request_handler(Self::handle_get_project_symbols); + client.add_model_request_handler(Self::handle_open_buffer_for_symbol); + client.add_model_request_handler(Self::handle_open_buffer_by_id); + client.add_model_request_handler(Self::handle_open_buffer_by_path); + client.add_model_request_handler(Self::handle_save_buffer); } pub fn local( @@ -280,31 +302,11 @@ impl Project { let mut status = rpc.status(); while let Some(status) = status.next().await { if let Some(this) = this.upgrade(&cx) { - let remote_id = if status.is_connected() { - let response = rpc.request(proto::RegisterProject {}).await?; - Some(response.project_id) + if status.is_connected() { + this.update(&mut cx, |this, cx| this.register(cx)).await?; } else { - None - }; - - if let Some(project_id) = remote_id { - let mut registrations = Vec::new(); - this.update(&mut cx, |this, cx| { - for worktree in this.worktrees(cx).collect::>() { - registrations.push(worktree.update( - cx, - |worktree, cx| { - let worktree = worktree.as_local_mut().unwrap(); - worktree.register(project_id, cx) - }, - )); - } - }); - for registration in registrations { - registration.await?; - } + this.update(&mut cx, |this, cx| this.unregister(cx)); } - this.update(&mut cx, |this, cx| this.set_remote_id(remote_id, cx)); } } Ok(()) @@ -355,7 +357,7 @@ impl Project { fs: Arc, cx: &mut AsyncAppContext, ) -> Result> { - client.authenticate_and_connect(&cx).await?; + client.authenticate_and_connect(true, &cx).await?; let response = client .request(proto::JoinProject { @@ -468,7 +470,6 @@ impl Project { 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> { self.opened_buffers .get(&remote_id) @@ -537,16 +538,54 @@ impl Project { &self.fs } - fn set_remote_id(&mut self, remote_id: Option, cx: &mut ModelContext) { + fn unregister(&mut self, cx: &mut ModelContext) { + self.unshare(cx); + for worktree in &self.worktrees { + if let Some(worktree) = worktree.upgrade(cx) { + worktree.update(cx, |worktree, _| { + worktree.as_local_mut().unwrap().unregister(); + }); + } + } + if let ProjectClientState::Local { remote_id_tx, .. } = &mut self.client_state { - *remote_id_tx.borrow_mut() = remote_id; + *remote_id_tx.borrow_mut() = None; } self.subscriptions.clear(); - if let Some(remote_id) = remote_id { - self.subscriptions - .push(self.client.add_model_for_remote_entity(remote_id, cx)); - } + } + + fn register(&mut self, cx: &mut ModelContext) -> Task> { + self.unregister(cx); + + let response = self.client.request(proto::RegisterProject {}); + cx.spawn(|this, mut cx| async move { + let remote_id = response.await?.project_id; + + let mut registrations = Vec::new(); + this.update(&mut cx, |this, cx| { + if let ProjectClientState::Local { remote_id_tx, .. } = &mut this.client_state { + *remote_id_tx.borrow_mut() = Some(remote_id); + } + + cx.emit(Event::RemoteIdChanged(Some(remote_id))); + + this.subscriptions + .push(this.client.add_model_for_remote_entity(remote_id, cx)); + + for worktree in &this.worktrees { + if let Some(worktree) = worktree.upgrade(cx) { + registrations.push(worktree.update(cx, |worktree, cx| { + let worktree = worktree.as_local_mut().unwrap(); + worktree.register(remote_id, cx) + })); + } + } + }); + + futures::future::try_join_all(registrations).await?; + Ok(()) + }) } pub fn remote_id(&self) -> Option { @@ -623,6 +662,24 @@ impl Project { .find(|worktree| worktree.read(cx).id() == id) } + pub fn worktree_for_entry( + &self, + entry_id: ProjectEntryId, + cx: &AppContext, + ) -> Option> { + self.worktrees(cx) + .find(|worktree| worktree.read(cx).contains_entry(entry_id)) + } + + pub fn worktree_id_for_entry( + &self, + entry_id: ProjectEntryId, + cx: &AppContext, + ) -> Option { + self.worktree_for_entry(entry_id, cx) + .map(|worktree| worktree.read(cx).id()) + } + pub fn share(&self, cx: &mut ModelContext) -> Task> { let rpc = self.client.clone(); cx.spawn(|this, mut cx| async move { @@ -685,59 +742,51 @@ impl Project { }) } - pub fn unshare(&self, cx: &mut ModelContext) -> Task> { + pub fn unshare(&mut self, cx: &mut ModelContext) { let rpc = self.client.clone(); - cx.spawn(|this, mut cx| async move { - let project_id = this.update(&mut cx, |this, cx| { - if let ProjectClientState::Local { - is_shared, - remote_id_rx, - .. - } = &mut this.client_state - { - *is_shared = false; - for open_buffer in this.opened_buffers.values_mut() { - match open_buffer { - OpenBuffer::Strong(buffer) => { - *open_buffer = OpenBuffer::Weak(buffer.downgrade()); - } - _ => {} - } - } + if let ProjectClientState::Local { + is_shared, + remote_id_rx, + .. + } = &mut self.client_state + { + if !*is_shared { + return; + } - for worktree_handle in this.worktrees.iter_mut() { - match worktree_handle { - WorktreeHandle::Strong(worktree) => { - if !worktree.read(cx).is_visible() { - *worktree_handle = WorktreeHandle::Weak(worktree.downgrade()); - } - } - _ => {} - } + *is_shared = false; + self.collaborators.clear(); + self.shared_buffers.clear(); + for worktree_handle in self.worktrees.iter_mut() { + if let WorktreeHandle::Strong(worktree) = worktree_handle { + let is_visible = worktree.update(cx, |worktree, _| { + worktree.as_local_mut().unwrap().unshare(); + worktree.is_visible() + }); + if !is_visible { + *worktree_handle = WorktreeHandle::Weak(worktree.downgrade()); } - - remote_id_rx - .borrow() - .ok_or_else(|| anyhow!("no project id")) - } else { - Err(anyhow!("can't share a remote project")) } - })?; + } - rpc.send(proto::UnshareProject { project_id })?; - this.update(&mut cx, |this, cx| { - this.collaborators.clear(); - this.shared_buffers.clear(); - for worktree in this.worktrees(cx).collect::>() { - worktree.update(cx, |worktree, _| { - worktree.as_local_mut().unwrap().unshare(); - }); + for open_buffer in self.opened_buffers.values_mut() { + match open_buffer { + OpenBuffer::Strong(buffer) => { + *open_buffer = OpenBuffer::Weak(buffer.downgrade()); + } + _ => {} } - cx.notify() - }); - Ok(()) - }) + } + + if let Some(project_id) = *remote_id_rx.borrow() { + rpc.send(proto::UnshareProject { project_id }).log_err(); + } + + cx.notify(); + } else { + log::error!("attempted to unshare a remote project"); + } } fn project_unshared(&mut self, cx: &mut ModelContext) { @@ -785,6 +834,23 @@ impl Project { Ok(buffer) } + pub fn open_path( + &mut self, + path: impl Into, + cx: &mut ModelContext, + ) -> Task> { + let task = self.open_buffer(path, cx); + cx.spawn_weak(|_, cx| async move { + let buffer = task.await?; + let project_entry_id = buffer + .read_with(&cx, |buffer, cx| { + File::from_dyn(buffer.file()).and_then(|file| file.project_entry_id(cx)) + }) + .ok_or_else(|| anyhow!("no project entry"))?; + Ok((project_entry_id, buffer.into())) + }) + } + pub fn open_buffer( &mut self, path: impl Into, @@ -876,7 +942,7 @@ impl Project { let path_string = path.to_string_lossy().to_string(); cx.spawn(|this, mut cx| async move { let response = rpc - .request(proto::OpenBuffer { + .request(proto::OpenBufferByPath { project_id, worktree_id: remote_worktree_id.to_proto(), path: path_string, @@ -925,6 +991,32 @@ impl Project { }) } + pub fn open_buffer_by_id( + &mut self, + id: u64, + cx: &mut ModelContext, + ) -> Task>> { + 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( &mut self, buffer: ModelHandle, @@ -1096,7 +1188,7 @@ impl Project { }); cx.background().spawn(request).detach_and_log_err(cx); } - BufferEvent::Edited => { + BufferEvent::Edited { .. } => { let language_server = self .language_server_for_buffer(buffer.read(cx), cx)? .clone(); @@ -1783,38 +1875,23 @@ impl Project { }); let mut sanitized_diagnostics = Vec::new(); - let mut edits_since_save = snapshot - .edits_since::(buffer.read(cx).saved_version()) - .peekable(); - let mut last_edit_old_end = PointUtf16::zero(); - let mut last_edit_new_end = PointUtf16::zero(); - 'outer: for entry in diagnostics { - let mut start = entry.range.start; - let mut end = entry.range.end; - - // Some diagnostics are based on files on disk instead of buffers' - // current contents. Adjust these diagnostics' ranges to reflect - // any unsaved edits. + let edits_since_save = Patch::new( + snapshot + .edits_since::(buffer.read(cx).saved_version()) + .collect(), + ); + for entry in diagnostics { + let start; + let end; if entry.diagnostic.is_disk_based { - while let Some(edit) = edits_since_save.peek() { - if edit.old.end <= start { - last_edit_old_end = edit.old.end; - last_edit_new_end = edit.new.end; - edits_since_save.next(); - } else if edit.old.start <= end && edit.old.end >= start { - continue 'outer; - } else { - break; - } - } - - let start_overshoot = start - last_edit_old_end; - start = last_edit_new_end; - start += start_overshoot; - - let end_overshoot = end - last_edit_old_end; - end = last_edit_new_end; - end += end_overshoot; + // Some diagnostics are based on files on disk instead of buffers' + // current contents. Adjust these diagnostics' ranges to reflect + // any unsaved edits. + start = edits_since_save.old_to_new(entry.range.start); + end = edits_since_save.old_to_new(entry.range.end); + } else { + start = entry.range.start; + end = entry.range.end; } let mut range = snapshot.clip_point_utf16(start, Bias::Left) @@ -3163,10 +3240,7 @@ impl Project { let new_active_entry = entry.and_then(|project_path| { let worktree = self.worktree_for_id(project_path.worktree_id, cx)?; let entry = worktree.read(cx).entry_for_path(project_path.path)?; - Some(ProjectEntry { - worktree_id: project_path.worktree_id, - entry_id: entry.id, - }) + Some(entry.id) }); if new_active_entry != self.active_entry { self.active_entry = new_active_entry; @@ -3217,10 +3291,25 @@ impl Project { } } - pub fn active_entry(&self) -> Option { + pub fn active_entry(&self) -> Option { self.active_entry } + pub fn entry_for_path(&self, path: &ProjectPath, cx: &AppContext) -> Option { + self.worktree_for_id(path.worktree_id, cx)? + .read(cx) + .entry_for_path(&path.path) + .map(|entry| entry.id) + } + + pub fn path_for_entry(&self, entry_id: ProjectEntryId, cx: &AppContext) -> Option { + let worktree = self.worktree_for_entry(entry_id, cx)?; + let worktree = worktree.read(cx); + let worktree_id = worktree.id(); + let path = worktree.entry_for_id(entry_id)?.path.clone(); + Some(ProjectPath { worktree_id, path }) + } + // RPC message handlers async fn handle_unshare_project( @@ -3274,6 +3363,7 @@ impl Project { buffer.update(cx, |buffer, cx| buffer.remove_peer(replica_id, cx)); } } + cx.emit(Event::CollaboratorLeft(peer_id)); cx.notify(); Ok(()) }) @@ -3821,9 +3911,28 @@ impl Project { hasher.finalize().as_slice().try_into().unwrap() } - async fn handle_open_buffer( + async fn handle_open_buffer_by_id( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + 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, - envelope: TypedEnvelope, + envelope: TypedEnvelope, _: Arc, mut cx: AsyncAppContext, ) -> Result { @@ -4477,6 +4586,12 @@ fn relativize_path(base: &Path, path: &Path) -> PathBuf { components.iter().map(|c| c.as_os_str()).collect() } +impl Item for Buffer { + fn entry_id(&self, cx: &AppContext) -> Option { + File::from_dyn(self.file()).and_then(|file| file.project_entry_id(cx)) + } +} + #[cfg(test)] mod tests { use super::{Event, *}; @@ -4897,7 +5012,7 @@ mod tests { } #[gpui::test] - async fn test_transforming_disk_based_diagnostics(cx: &mut gpui::TestAppContext) { + async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) { cx.foreground().forbid_parking(); let (mut lsp_config, mut fake_servers) = LanguageServerConfig::fake(); @@ -5122,11 +5237,13 @@ mod tests { buffer.update(cx, |buffer, cx| { buffer.edit(Some(Point::new(2, 0)..Point::new(2, 0)), " ", cx); buffer.edit(Some(Point::new(2, 8)..Point::new(2, 10)), "(x: usize)", cx); + buffer.edit(Some(Point::new(3, 10)..Point::new(3, 10)), "xxx", cx); }); - let change_notification_2 = - fake_server.receive_notification::(); + let change_notification_2 = fake_server + .receive_notification::() + .await; assert!( - change_notification_2.await.text_document.version + change_notification_2.text_document.version > change_notification_1.text_document.version ); @@ -5134,7 +5251,7 @@ mod tests { fake_server.notify::( lsp::PublishDiagnosticsParams { uri: lsp::Url::from_file_path("/dir/a.rs").unwrap(), - version: Some(open_notification.text_document.version), + version: Some(change_notification_2.text_document.version), diagnostics: vec![ lsp::Diagnostic { range: lsp::Range::new(lsp::Position::new(1, 9), lsp::Position::new(1, 11)), @@ -5174,7 +5291,7 @@ mod tests { } }, DiagnosticEntry { - range: Point::new(3, 9)..Point::new(3, 11), + range: Point::new(3, 9)..Point::new(3, 14), diagnostic: Diagnostic { severity: DiagnosticSeverity::ERROR, message: "undefined variable 'BB'".to_string(), @@ -5672,7 +5789,7 @@ mod tests { .unwrap(); let mut fake_server = fake_servers.next().await.unwrap(); - fake_server.handle_request::(move |params, _| { + fake_server.handle_request::(|params, _| async move { let params = params.text_document_position_params; assert_eq!( params.text_document.uri.to_file_path().unwrap(), @@ -6607,7 +6724,7 @@ mod tests { project.prepare_rename(buffer.clone(), 7, cx) }); fake_server - .handle_request::(|params, _| { + .handle_request::(|params, _| async move { assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs"); assert_eq!(params.position, lsp::Position::new(0, 7)); Some(lsp::PrepareRenameResponse::Range(lsp::Range::new( @@ -6626,7 +6743,7 @@ mod tests { project.perform_rename(buffer.clone(), 7, "THREE".to_string(), true, cx) }); fake_server - .handle_request::(|params, _| { + .handle_request::(|params, _| async move { assert_eq!( params.text_document_position.text_document.uri.as_str(), "file:///dir/one.rs" diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 1ef8dd34a0da1d0e11d7e50e7c2b45a248ed08cf..beacc5a8630346aa7e3e99fe029484e3b48774f2 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -1,3 +1,5 @@ +use crate::ProjectEntryId; + use super::{ fs::{self, Fs}, ignore::IgnoreStack, @@ -39,10 +41,7 @@ use std::{ future::Future, ops::{Deref, DerefMut}, path::{Path, PathBuf}, - sync::{ - atomic::{AtomicUsize, Ordering::SeqCst}, - Arc, - }, + sync::{atomic::AtomicUsize, Arc}, time::{Duration, SystemTime}, }; use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap}; @@ -101,7 +100,7 @@ pub struct LocalSnapshot { abs_path: Arc, scan_id: usize, ignores: HashMap, (Arc, usize)>, - removed_entry_ids: HashMap, + removed_entry_ids: HashMap, next_entry_id: Arc, snapshot: Snapshot, } @@ -712,7 +711,9 @@ impl LocalWorktree { let worktree = this.as_local_mut().unwrap(); match response { Ok(_) => { - worktree.registration = Registration::Done { project_id }; + if worktree.registration == Registration::Pending { + worktree.registration = Registration::Done { project_id }; + } Ok(()) } Err(error) => { @@ -809,6 +810,11 @@ impl LocalWorktree { }) } + pub fn unregister(&mut self) { + self.unshare(); + self.registration = Registration::None; + } + pub fn unshare(&mut self) { self.share.take(); } @@ -856,13 +862,16 @@ impl Snapshot { self.id } + pub fn contains_entry(&self, entry_id: ProjectEntryId) -> bool { + self.entries_by_id.get(&entry_id, &()).is_some() + } + pub(crate) fn apply_remote_update(&mut self, update: proto::UpdateWorktree) -> Result<()> { let mut entries_by_path_edits = Vec::new(); let mut entries_by_id_edits = Vec::new(); for entry_id in update.removed_entries { - let entry_id = entry_id as usize; let entry = self - .entry_for_id(entry_id) + .entry_for_id(ProjectEntryId::from_proto(entry_id)) .ok_or_else(|| anyhow!("unknown entry"))?; entries_by_path_edits.push(Edit::Remove(PathKey(entry.path.clone()))); entries_by_id_edits.push(Edit::Remove(entry.id)); @@ -985,7 +994,7 @@ impl Snapshot { }) } - pub fn entry_for_id(&self, id: usize) -> Option<&Entry> { + pub fn entry_for_id(&self, id: ProjectEntryId) -> Option<&Entry> { let entry = self.entries_by_id.get(&id, &())?; self.entry_for_path(&entry.path) } @@ -1062,7 +1071,7 @@ impl LocalSnapshot { other_entries.next(); } Ordering::Greater => { - removed_entries.push(other_entry.id as u64); + removed_entries.push(other_entry.id.to_proto()); other_entries.next(); } } @@ -1073,7 +1082,7 @@ impl LocalSnapshot { self_entries.next(); } (None, Some(other_entry)) => { - removed_entries.push(other_entry.id as u64); + removed_entries.push(other_entry.id.to_proto()); other_entries.next(); } (None, None) => break, @@ -1326,7 +1335,7 @@ pub struct File { pub worktree: ModelHandle, pub path: Arc, pub mtime: SystemTime, - pub(crate) entry_id: Option, + pub(crate) entry_id: Option, pub(crate) is_local: bool, } @@ -1423,7 +1432,7 @@ impl language::File for File { fn to_proto(&self) -> rpc::proto::File { rpc::proto::File { worktree_id: self.worktree.id() as u64, - entry_id: self.entry_id.map(|entry_id| entry_id as u64), + entry_id: self.entry_id.map(|entry_id| entry_id.to_proto()), path: self.path.to_string_lossy().into(), mtime: Some(self.mtime.into()), } @@ -1490,7 +1499,7 @@ impl File { worktree, path: Path::new(&proto.path).into(), mtime: proto.mtime.ok_or_else(|| anyhow!("no timestamp"))?.into(), - entry_id: proto.entry_id.map(|entry_id| entry_id as usize), + entry_id: proto.entry_id.map(ProjectEntryId::from_proto), is_local: false, }) } @@ -1502,11 +1511,15 @@ impl File { pub fn worktree_id(&self, cx: &AppContext) -> WorktreeId { self.worktree.read(cx).id() } + + pub fn project_entry_id(&self, _: &AppContext) -> Option { + self.entry_id + } } #[derive(Clone, Debug, PartialEq, Eq)] pub struct Entry { - pub id: usize, + pub id: ProjectEntryId, pub kind: EntryKind, pub path: Arc, pub inode: u64, @@ -1530,7 +1543,7 @@ impl Entry { root_char_bag: CharBag, ) -> Self { Self { - id: next_entry_id.fetch_add(1, SeqCst), + id: ProjectEntryId::new(next_entry_id), kind: if metadata.is_dir { EntryKind::PendingDir } else { @@ -1620,7 +1633,7 @@ impl sum_tree::Summary for EntrySummary { #[derive(Clone, Debug)] struct PathEntry { - id: usize, + id: ProjectEntryId, path: Arc, is_ignored: bool, scan_id: usize, @@ -1635,7 +1648,7 @@ impl sum_tree::Item for PathEntry { } impl sum_tree::KeyedItem for PathEntry { - type Key = usize; + type Key = ProjectEntryId; fn key(&self) -> Self::Key { self.id @@ -1644,7 +1657,7 @@ impl sum_tree::KeyedItem for PathEntry { #[derive(Clone, Debug, Default)] struct PathEntrySummary { - max_id: usize, + max_id: ProjectEntryId, } impl sum_tree::Summary for PathEntrySummary { @@ -1655,7 +1668,7 @@ impl sum_tree::Summary for PathEntrySummary { } } -impl<'a> sum_tree::Dimension<'a, PathEntrySummary> for usize { +impl<'a> sum_tree::Dimension<'a, PathEntrySummary> for ProjectEntryId { fn add_summary(&mut self, summary: &'a PathEntrySummary, _: &()) { *self = summary.max_id; } @@ -2345,7 +2358,7 @@ impl<'a> Iterator for ChildEntriesIter<'a> { impl<'a> From<&'a Entry> for proto::Entry { fn from(entry: &'a Entry) -> Self { Self { - id: entry.id as u64, + id: entry.id.to_proto(), is_dir: entry.is_dir(), path: entry.path.to_string_lossy().to_string(), inode: entry.inode, @@ -2370,7 +2383,7 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry { }; let path: Arc = Arc::from(Path::new(&entry.path)); Ok(Entry { - id: entry.id as usize, + id: ProjectEntryId::from_proto(entry.id), kind, path: path.clone(), inode: entry.inode, diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 7f03005b80cc2d8eddab64f0c59a509cf2d6f12b..0dd6b08bacc68bf888e9348636ec0b66669a509c 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -9,7 +9,7 @@ use gpui::{ AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, View, ViewContext, ViewHandle, WeakViewHandle, }; -use project::{Project, ProjectEntry, ProjectPath, Worktree, WorktreeId}; +use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId}; use std::{ collections::{hash_map, HashMap}, ffi::OsStr, @@ -24,7 +24,7 @@ pub struct ProjectPanel { project: ModelHandle, list: UniformListState, visible_entries: Vec<(WorktreeId, Vec)>, - expanded_dir_ids: HashMap>, + expanded_dir_ids: HashMap>, selection: Option, handle: WeakViewHandle, } @@ -32,7 +32,7 @@ pub struct ProjectPanel { #[derive(Copy, Clone)] struct Selection { worktree_id: WorktreeId, - entry_id: usize, + entry_id: ProjectEntryId, index: usize, } @@ -47,8 +47,8 @@ struct EntryDetails { action!(ExpandSelectedEntry); action!(CollapseSelectedEntry); -action!(ToggleExpanded, ProjectEntry); -action!(Open, ProjectEntry); +action!(ToggleExpanded, ProjectEntryId); +action!(Open, ProjectEntryId); pub fn init(cx: &mut MutableAppContext) { cx.add_action(ProjectPanel::expand_selected_entry); @@ -64,10 +64,7 @@ pub fn init(cx: &mut MutableAppContext) { } pub enum Event { - OpenedEntry { - worktree_id: WorktreeId, - entry_id: usize, - }, + OpenedEntry(ProjectEntryId), } impl ProjectPanel { @@ -78,15 +75,15 @@ impl ProjectPanel { cx.notify(); }) .detach(); - cx.subscribe(&project, |this, _, event, cx| match event { - project::Event::ActiveEntryChanged(Some(ProjectEntry { - worktree_id, - entry_id, - })) => { - this.expand_entry(*worktree_id, *entry_id, cx); - this.update_visible_entries(Some((*worktree_id, *entry_id)), cx); - this.autoscroll(); - cx.notify(); + cx.subscribe(&project, |this, project, event, cx| match event { + project::Event::ActiveEntryChanged(Some(entry_id)) => { + if let Some(worktree_id) = project.read(cx).worktree_id_for_entry(*entry_id, cx) + { + this.expand_entry(worktree_id, *entry_id, cx); + this.update_visible_entries(Some((worktree_id, *entry_id)), cx); + this.autoscroll(); + cx.notify(); + } } project::Event::WorktreeRemoved(id) => { this.expanded_dir_ids.remove(id); @@ -109,16 +106,13 @@ impl ProjectPanel { this }); cx.subscribe(&project_panel, move |workspace, _, event, cx| match event { - &Event::OpenedEntry { - worktree_id, - entry_id, - } => { - if let Some(worktree) = project.read(cx).worktree_for_id(worktree_id, cx) { + &Event::OpenedEntry(entry_id) => { + if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) { if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) { workspace .open_path( ProjectPath { - worktree_id, + worktree_id: worktree.read(cx).id(), path: entry.path.clone(), }, cx, @@ -152,10 +146,7 @@ impl ProjectPanel { } } } else { - let event = Event::OpenedEntry { - worktree_id: worktree.id(), - entry_id: entry.id, - }; + let event = Event::OpenedEntry(entry.id); cx.emit(event); } } @@ -193,22 +184,20 @@ impl ProjectPanel { } fn toggle_expanded(&mut self, action: &ToggleExpanded, cx: &mut ViewContext) { - let ProjectEntry { - worktree_id, - entry_id, - } = action.0; - - if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) { - match expanded_dir_ids.binary_search(&entry_id) { - Ok(ix) => { - expanded_dir_ids.remove(ix); - } - Err(ix) => { - expanded_dir_ids.insert(ix, entry_id); + let entry_id = action.0; + if let Some(worktree_id) = self.project.read(cx).worktree_id_for_entry(entry_id, cx) { + if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) { + match expanded_dir_ids.binary_search(&entry_id) { + Ok(ix) => { + expanded_dir_ids.remove(ix); + } + Err(ix) => { + expanded_dir_ids.insert(ix, entry_id); + } } + self.update_visible_entries(Some((worktree_id, entry_id)), cx); + cx.focus_self(); } - self.update_visible_entries(Some((worktree_id, entry_id)), cx); - cx.focus_self(); } } @@ -229,10 +218,7 @@ impl ProjectPanel { } fn open_entry(&mut self, action: &Open, cx: &mut ViewContext) { - cx.emit(Event::OpenedEntry { - worktree_id: action.0.worktree_id, - entry_id: action.0.entry_id, - }); + cx.emit(Event::OpenedEntry(action.0)); } fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { @@ -313,7 +299,7 @@ impl ProjectPanel { fn update_visible_entries( &mut self, - new_selected_entry: Option<(WorktreeId, usize)>, + new_selected_entry: Option<(WorktreeId, ProjectEntryId)>, cx: &mut ViewContext, ) { let worktrees = self @@ -379,7 +365,7 @@ impl ProjectPanel { fn expand_entry( &mut self, worktree_id: WorktreeId, - entry_id: usize, + entry_id: ProjectEntryId, cx: &mut ViewContext, ) { let project = self.project.read(cx); @@ -411,7 +397,7 @@ impl ProjectPanel { &self, range: Range, cx: &mut ViewContext, - mut callback: impl FnMut(ProjectEntry, EntryDetails, &mut ViewContext), + mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut ViewContext), ) { let mut ix = 0; for (worktree_id, visible_worktree_entries) in &self.visible_entries { @@ -450,11 +436,7 @@ impl ProjectPanel { e.worktree_id == snapshot.id() && e.entry_id == entry.id }), }; - let entry = ProjectEntry { - worktree_id: snapshot.id(), - entry_id: entry.id, - }; - callback(entry, details, cx); + callback(entry.id, details, cx); } } } @@ -463,13 +445,13 @@ impl ProjectPanel { } fn render_entry( - entry: ProjectEntry, + entry_id: ProjectEntryId, details: EntryDetails, theme: &theme::ProjectPanel, cx: &mut ViewContext, ) -> ElementBox { let is_dir = details.is_dir; - MouseEventHandler::new::(entry.entry_id, cx, |state, _| { + MouseEventHandler::new::(entry_id.to_usize(), cx, |state, _| { let style = match (details.is_selected, state.hovered) { (false, false) => &theme.entry, (false, true) => &theme.hovered_entry, @@ -519,9 +501,9 @@ impl ProjectPanel { }) .on_click(move |cx| { if is_dir { - cx.dispatch_action(ToggleExpanded(entry)) + cx.dispatch_action(ToggleExpanded(entry_id)) } else { - cx.dispatch_action(Open(entry)) + cx.dispatch_action(Open(entry_id)) } }) .with_cursor_style(CursorStyle::PointingHand) @@ -535,7 +517,7 @@ impl View for ProjectPanel { } fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox { - let theme = &cx.app_state::().theme.project_panel; + let theme = &cx.global::().theme.project_panel; let mut container_style = theme.container; let padding = std::mem::take(&mut container_style.padding); let handle = self.handle.clone(); @@ -546,7 +528,7 @@ impl View for ProjectPanel { .map(|(_, worktree_entries)| worktree_entries.len()) .sum(), move |range, items, cx| { - let theme = cx.app_state::().theme.clone(); + let theme = cx.global::().theme.clone(); let this = handle.upgrade(cx).unwrap(); this.update(cx.app, |this, cx| { this.for_each_visible_entry(range.clone(), cx, |entry, details, cx| { @@ -830,13 +812,7 @@ mod tests { let worktree = worktree.read(cx); if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) { let entry_id = worktree.entry_for_path(relative_path).unwrap().id; - panel.toggle_expanded( - &ToggleExpanded(ProjectEntry { - worktree_id: worktree.id(), - entry_id, - }), - cx, - ); + panel.toggle_expanded(&ToggleExpanded(entry_id), cx); return; } } diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index bfd204671f6994a07b5ab1717afc684b9659c9c8..34d5306d99e78eaf2c056dd1da968e2853ff2c85 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -1,6 +1,5 @@ use editor::{ - combine_syntax_and_fuzzy_match_highlights, items::BufferItemHandle, styled_runs_for_code_label, - Autoscroll, Bias, Editor, + combine_syntax_and_fuzzy_match_highlights, styled_runs_for_code_label, Autoscroll, Bias, Editor, }; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ @@ -70,7 +69,7 @@ impl View for ProjectSymbolsView { } fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - let settings = cx.app_state::(); + let settings = cx.global::(); Flex::new(Axis::Vertical) .with_child( Container::new(ChildView::new(&self.query_editor).boxed()) @@ -234,7 +233,7 @@ impl ProjectSymbolsView { fn render_matches(&self, cx: &AppContext) -> ElementBox { if self.matches.is_empty() { - let settings = cx.app_state::(); + let settings = cx.global::(); return Container::new( Label::new( "No matches".into(), @@ -277,7 +276,7 @@ impl ProjectSymbolsView { show_worktree_root_name: bool, cx: &AppContext, ) -> ElementBox { - let settings = cx.app_state::(); + let settings = cx.global::(); let style = if index == self.selected_match_index { &settings.theme.selector.active_item } else { @@ -329,7 +328,7 @@ impl ProjectSymbolsView { ) { match event { editor::Event::Blurred => cx.emit(Event::Dismissed), - editor::Event::Edited => self.update_matches(cx), + editor::Event::BufferEdited { .. } => self.update_matches(cx), _ => {} } } @@ -346,6 +345,7 @@ impl ProjectSymbolsView { let buffer = workspace .project() .update(cx, |project, cx| project.open_buffer_for_symbol(symbol, cx)); + let symbol = symbol.clone(); cx.spawn(|workspace, mut cx| async move { let buffer = buffer.await?; @@ -353,10 +353,8 @@ impl ProjectSymbolsView { let position = buffer .read(cx) .clip_point_utf16(symbol.range.start, Bias::Left); - let editor = workspace - .open_item(BufferItemHandle(buffer), cx) - .downcast::() - .unwrap(); + + let editor = workspace.open_project_item::(buffer, cx); editor.update(cx, |editor, cx| { editor.select_ranges( [position..position], diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index d0cb621ab92457adf9942a5cc1287f17d2572f99..9d25e66190b14bc7d4624885ec7da3dc57acb61b 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -40,8 +40,9 @@ message Envelope { StartLanguageServer start_language_server = 33; UpdateLanguageServer update_language_server = 34; - OpenBuffer open_buffer = 35; - OpenBufferResponse open_buffer_response = 36; + OpenBufferById open_buffer_by_id = 35; + OpenBufferByPath open_buffer_by_path = 36; + OpenBufferResponse open_buffer_response = 37; UpdateBuffer update_buffer = 38; UpdateBufferFile update_buffer_file = 39; SaveBuffer save_buffer = 40; @@ -79,6 +80,11 @@ message Envelope { GetUsers get_users = 70; 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; } -message OpenBuffer { +message OpenBufferByPath { uint64 project_id = 1; uint64 worktree_id = 2; string path = 3; } +message OpenBufferById { + uint64 project_id = 1; + uint64 id = 2; +} + message OpenBufferResponse { Buffer buffer = 1; } @@ -521,8 +532,77 @@ message UpdateContacts { 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 +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 { uint32 peer_id = 1; uint32 replica_id = 2; @@ -578,17 +658,6 @@ message BufferState { 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 { uint32 replica_id = 1; repeated Selection selections = 2; @@ -614,12 +683,6 @@ enum Bias { Right = 1; } -message UpdateDiagnostics { - uint32 replica_id = 1; - uint32 lamport_timestamp = 2; - repeated Diagnostic diagnostics = 3; -} - message Diagnostic { Anchor start = 1; Anchor end = 2; diff --git a/crates/rpc/src/peer.rs b/crates/rpc/src/peer.rs index 4156c0883d9c897ab2dc64a2d9df8c958cfa5a96..726453bea86968c0215e2d70c31884b02f066275 100644 --- a/crates/rpc/src/peer.rs +++ b/crates/rpc/src/peer.rs @@ -96,7 +96,7 @@ pub struct ConnectionState { const KEEPALIVE_INTERVAL: Duration = Duration::from_secs(1); const WRITE_TIMEOUT: Duration = Duration::from_secs(2); -const RECEIVE_TIMEOUT: Duration = Duration::from_secs(30); +pub const RECEIVE_TIMEOUT: Duration = Duration::from_secs(5); impl Peer { pub fn new() -> Arc { diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 54b26b830ca584f74110c74e5b18d3371d092c18..59d6773451fd2feebc28b17120e0b50a58de1127 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -147,6 +147,8 @@ messages!( (BufferSaved, Foreground), (ChannelMessageSent, Foreground), (Error, Foreground), + (Follow, Foreground), + (FollowResponse, Foreground), (FormatBuffers, Foreground), (FormatBuffersResponse, Foreground), (GetChannelMessages, Foreground), @@ -175,7 +177,8 @@ messages!( (UpdateLanguageServer, Foreground), (LeaveChannel, Foreground), (LeaveProject, Foreground), - (OpenBuffer, Background), + (OpenBufferById, Background), + (OpenBufferByPath, Background), (OpenBufferForSymbol, Background), (OpenBufferForSymbolResponse, Background), (OpenBufferResponse, Background), @@ -195,13 +198,15 @@ messages!( (SendChannelMessageResponse, Foreground), (ShareProject, Foreground), (Test, Foreground), + (Unfollow, Foreground), (UnregisterProject, Foreground), (UnregisterWorktree, Foreground), (UnshareProject, Foreground), - (UpdateBuffer, Background), + (UpdateBuffer, Foreground), (UpdateBufferFile, Foreground), (UpdateContacts, Foreground), (UpdateDiagnosticSummary, Foreground), + (UpdateFollowers, Foreground), (UpdateWorktree, Foreground), ); @@ -211,6 +216,7 @@ request_messages!( ApplyCompletionAdditionalEdits, ApplyCompletionAdditionalEditsResponse ), + (Follow, FollowResponse), (FormatBuffers, FormatBuffersResponse), (GetChannelMessages, GetChannelMessagesResponse), (GetChannels, GetChannelsResponse), @@ -223,7 +229,8 @@ request_messages!( (GetUsers, GetUsersResponse), (JoinChannel, JoinChannelResponse), (JoinProject, JoinProjectResponse), - (OpenBuffer, OpenBufferResponse), + (OpenBufferById, OpenBufferResponse), + (OpenBufferByPath, OpenBufferResponse), (OpenBufferForSymbol, OpenBufferForSymbolResponse), (Ping, Ack), (PerformRename, PerformRenameResponse), @@ -246,6 +253,7 @@ entity_messages!( ApplyCompletionAdditionalEdits, BufferReloaded, BufferSaved, + Follow, FormatBuffers, GetCodeActions, GetCompletions, @@ -255,7 +263,8 @@ entity_messages!( GetProjectSymbols, JoinProject, LeaveProject, - OpenBuffer, + OpenBufferById, + OpenBufferByPath, OpenBufferForSymbol, PerformRename, PrepareRename, @@ -263,11 +272,13 @@ entity_messages!( SaveBuffer, SearchProject, StartLanguageServer, + Unfollow, UnregisterWorktree, UnshareProject, UpdateBuffer, UpdateBufferFile, UpdateDiagnosticSummary, + UpdateFollowers, UpdateLanguageServer, RegisterWorktree, UpdateWorktree, diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index e937f6daaf2d9b754ee2b3b6457145a9206ecfd7..cfe780d5118c764a829cf047d288bc1e7a0b590e 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -5,4 +5,4 @@ pub mod proto; pub use conn::Connection; pub use peer::*; -pub const PROTOCOL_VERSION: u32 = 11; +pub const PROTOCOL_VERSION: u32 = 12; diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 24b438c7e465deef2ff6963fa1faa56af790a6dd..da9ee0664ba5423f1a8e8fffd2e53d5c1300fb52 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -8,7 +8,7 @@ use gpui::{ use language::OffsetRangeExt; use project::search::SearchQuery; use std::ops::Range; -use workspace::{ItemViewHandle, Pane, Settings, Toolbar, Workspace}; +use workspace::{ItemHandle, Pane, Settings, Toolbar, Workspace}; action!(Deploy, bool); action!(Dismiss); @@ -66,7 +66,7 @@ impl View for SearchBar { } fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - let theme = cx.app_state::().theme.clone(); + let theme = cx.global::().theme.clone(); let editor_container = if self.query_contains_error { theme.search.invalid_editor } else { @@ -126,7 +126,7 @@ impl View for SearchBar { impl Toolbar for SearchBar { fn active_item_changed( &mut self, - item: Option>, + item: Option>, cx: &mut ViewContext, ) -> bool { self.active_editor_subscription.take(); @@ -197,7 +197,7 @@ impl SearchBar { ) -> ElementBox { let is_active = self.is_search_option_enabled(search_option); MouseEventHandler::new::(search_option as usize, cx, |state, cx| { - let theme = &cx.app_state::().theme.search; + let theme = &cx.global::().theme.search; let style = match (is_active, state.hovered) { (false, false) => &theme.option_button, (false, true) => &theme.hovered_option_button, @@ -222,7 +222,7 @@ impl SearchBar { ) -> ElementBox { enum NavButton {} MouseEventHandler::new::(direction as usize, cx, |state, cx| { - let theme = &cx.app_state::().theme.search; + let theme = &cx.global::().theme.search; let style = if state.hovered { &theme.hovered_option_button } else { @@ -336,11 +336,9 @@ impl SearchBar { direction, &editor.buffer().read(cx).read(cx), ); - editor.select_ranges( - [ranges[new_index].clone()], - Some(Autoscroll::Fit), - cx, - ); + let range_to_select = ranges[new_index].clone(); + editor.unfold_ranges([range_to_select.clone()], false, cx); + editor.select_ranges([range_to_select], Some(Autoscroll::Fit), cx); } }); } @@ -360,7 +358,7 @@ impl SearchBar { cx: &mut ViewContext, ) { match event { - editor::Event::Edited => { + editor::Event::BufferEdited { .. } => { self.query_contains_error = false; self.clear_matches(cx); self.update_matches(true, cx); @@ -377,8 +375,8 @@ impl SearchBar { cx: &mut ViewContext, ) { match event { - editor::Event::Edited => self.update_matches(false, cx), - editor::Event::SelectionsChanged => self.update_match_index(cx), + editor::Event::BufferEdited { .. } => self.update_matches(false, cx), + editor::Event::SelectionsChanged { .. } => self.update_match_index(cx), _ => {} } } @@ -475,7 +473,7 @@ impl SearchBar { } } - let theme = &cx.app_state::().theme.search; + let theme = &cx.global::().theme.search; editor.highlight_background::( ranges, theme.match_background, @@ -510,8 +508,9 @@ impl SearchBar { #[cfg(test)] mod tests { use super::*; - use editor::{DisplayPoint, Editor, MultiBuffer}; + use editor::{DisplayPoint, Editor}; use gpui::{color::Color, TestAppContext}; + use language::Buffer; use std::sync::Arc; use unindent::Unindent as _; @@ -521,11 +520,12 @@ mod tests { let mut theme = gpui::fonts::with_font_cache(fonts.clone(), || theme::Theme::default()); theme.search.match_background = Color::red(); let settings = Settings::new("Courier", &fonts, Arc::new(theme)).unwrap(); - cx.update(|cx| cx.add_app_state(settings)); + cx.update(|cx| cx.set_global(settings)); - let buffer = cx.update(|cx| { - MultiBuffer::build_simple( - &r#" + let buffer = cx.add_model(|cx| { + Buffer::new( + 0, + r#" A regular expression (shortened as regex or regexp;[1] also referred to as rational expression[2][3]) is a sequence of characters that specifies a search pattern in text. Usually such patterns are used by string-searching algorithms diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index b09c88a4a0ff8b020dbf839684cbbe4c1f3cf88b..d78fcb12b72697df3ebeee81d1e4e5de993581ac 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -7,7 +7,7 @@ use editor::{Anchor, Autoscroll, Editor, MultiBuffer, SelectAll}; use gpui::{ action, elements::*, keymap::Binding, platform::CursorStyle, AppContext, ElementBox, Entity, ModelContext, ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, - ViewHandle, WeakModelHandle, + ViewHandle, WeakModelHandle, WeakViewHandle, }; use project::{search::SearchQuery, Project}; use std::{ @@ -16,7 +16,7 @@ use std::{ path::PathBuf, }; use util::ResultExt as _; -use workspace::{Item, ItemHandle, ItemNavHistory, ItemView, Settings, Workspace}; +use workspace::{Item, ItemNavHistory, Settings, Workspace}; action!(Deploy); action!(Search); @@ -26,10 +26,10 @@ action!(ToggleFocus); const MAX_TAB_TITLE_LEN: usize = 24; #[derive(Default)] -struct ActiveSearches(HashMap, WeakModelHandle>); +struct ActiveSearches(HashMap, WeakViewHandle>); pub fn init(cx: &mut MutableAppContext) { - cx.add_app_state(ActiveSearches::default()); + cx.set_global(ActiveSearches::default()); cx.add_bindings([ Binding::new("cmd-shift-F", ToggleFocus, Some("ProjectSearchView")), Binding::new("cmd-f", ToggleFocus, Some("ProjectSearchView")), @@ -139,23 +139,6 @@ impl ProjectSearch { } } -impl Item for ProjectSearch { - type View = ProjectSearchView; - - fn build_view( - model: ModelHandle, - _: &Workspace, - nav_history: ItemNavHistory, - cx: &mut gpui::ViewContext, - ) -> Self::View { - ProjectSearchView::new(model, Some(nav_history), cx) - } - - fn project_path(&self) -> Option { - None - } -} - enum ViewEvent { UpdateTab, } @@ -172,7 +155,7 @@ impl View for ProjectSearchView { fn render(&mut self, cx: &mut RenderContext) -> ElementBox { let model = &self.model.read(cx); let results = if model.match_ranges.is_empty() { - let theme = &cx.app_state::().theme; + let theme = &cx.global::().theme; let text = if self.query_editor.read(cx).text(cx).is_empty() { "" } else if model.pending_search.is_some() { @@ -199,11 +182,11 @@ impl View for ProjectSearchView { } fn on_focus(&mut self, cx: &mut ViewContext) { - cx.update_app_state(|state: &mut ActiveSearches, cx| { - state.0.insert( - self.model.read(cx).project.downgrade(), - self.model.downgrade(), - ) + let handle = cx.weak_handle(); + cx.update_global(|state: &mut ActiveSearches, cx| { + state + .0 + .insert(self.model.read(cx).project.downgrade(), handle) }); if self.model.read(cx).match_ranges.is_empty() { @@ -214,7 +197,7 @@ impl View for ProjectSearchView { } } -impl ItemView for ProjectSearchView { +impl Item for ProjectSearchView { fn act_as_type( &self, type_id: TypeId, @@ -235,12 +218,8 @@ impl ItemView for ProjectSearchView { .update(cx, |editor, cx| editor.deactivated(cx)); } - fn item(&self, _: &gpui::AppContext) -> Box { - Box::new(self.model.clone()) - } - fn tab_content(&self, tab_theme: &theme::Tab, cx: &gpui::AppContext) -> ElementBox { - let settings = cx.app_state::(); + let settings = cx.global::(); let search_theme = &settings.theme.search; Flex::row() .with_child( @@ -271,6 +250,10 @@ impl ItemView for ProjectSearchView { None } + fn project_entry_id(&self, _: &AppContext) -> Option { + None + } + fn can_save(&self, _: &gpui::AppContext) -> bool { true } @@ -305,21 +288,23 @@ impl ItemView for ProjectSearchView { unreachable!("save_as should not have been called") } - fn clone_on_split( - &self, - nav_history: ItemNavHistory, - cx: &mut ViewContext, - ) -> Option + fn clone_on_split(&self, cx: &mut ViewContext) -> Option where Self: Sized, { let model = self.model.update(cx, |model, cx| model.clone(cx)); - Some(Self::new(model, Some(nav_history), cx)) + Some(Self::new(model, cx)) } - fn navigate(&mut self, data: Box, cx: &mut ViewContext) { + fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext) { + self.results_editor.update(cx, |editor, _| { + editor.set_nav_history(Some(nav_history)); + }); + } + + fn navigate(&mut self, data: Box, cx: &mut ViewContext) -> bool { self.results_editor - .update(cx, |editor, cx| editor.navigate(data, cx)); + .update(cx, |editor, cx| editor.navigate(data, cx)) } fn should_update_tab_on_event(event: &ViewEvent) -> bool { @@ -328,11 +313,7 @@ impl ItemView for ProjectSearchView { } impl ProjectSearchView { - fn new( - model: ModelHandle, - nav_history: Option, - cx: &mut ViewContext, - ) -> Self { + fn new(model: ModelHandle, cx: &mut ViewContext) -> Self { let project; let excerpts; let mut query_text = String::new(); @@ -362,15 +343,14 @@ impl ProjectSearchView { }); let results_editor = cx.add_view(|cx| { - let mut editor = Editor::for_buffer(excerpts, Some(project), cx); + let mut editor = Editor::for_multibuffer(excerpts, Some(project), cx); editor.set_searchable(false); - editor.set_nav_history(nav_history); editor }); cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab)) .detach(); cx.subscribe(&results_editor, |this, _, event, cx| { - if matches!(event, editor::Event::SelectionsChanged) { + if matches!(event, editor::Event::SelectionsChanged { .. }) { this.update_match_index(cx); } }) @@ -394,28 +374,31 @@ impl ProjectSearchView { // If no search exists in the workspace, create a new one. fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { // Clean up entries for dropped projects - cx.update_app_state(|state: &mut ActiveSearches, cx| { + cx.update_global(|state: &mut ActiveSearches, cx| { state.0.retain(|project, _| project.is_upgradable(cx)) }); let active_search = cx - .app_state::() + .global::() .0 .get(&workspace.project().downgrade()); let existing = active_search .and_then(|active_search| { workspace - .items_of_type::(cx) + .items_of_type::(cx) .find(|search| search == active_search) }) - .or_else(|| workspace.item_of_type::(cx)); + .or_else(|| workspace.item_of_type::(cx)); if let Some(existing) = existing { workspace.activate_item(&existing, cx); } else { let model = cx.add_model(|cx| ProjectSearch::new(workspace.project().clone(), cx)); - workspace.open_item(model, cx); + workspace.add_item( + Box::new(cx.add_view(|cx| ProjectSearchView::new(model, cx))), + cx, + ); } } @@ -450,7 +433,10 @@ impl ProjectSearchView { model.search(new_query, cx); model }); - workspace.open_item(model, cx); + workspace.add_item( + Box::new(cx.add_view(|cx| ProjectSearchView::new(model, cx))), + cx, + ); } } } @@ -503,6 +489,7 @@ impl ProjectSearchView { ); let range_to_select = model.match_ranges[new_index].clone(); self.results_editor.update(cx, |editor, cx| { + editor.unfold_ranges([range_to_select.clone()], false, cx); editor.select_ranges([range_to_select], Some(Autoscroll::Fit), cx); }); } @@ -552,7 +539,7 @@ impl ProjectSearchView { if reset_selections { editor.select_ranges(match_ranges.first().cloned(), Some(Autoscroll::Fit), cx); } - let theme = &cx.app_state::().theme.search; + let theme = &cx.global::().theme.search; editor.highlight_background::(match_ranges, theme.match_background, cx); }); if self.query_editor.is_focused(cx) { @@ -578,7 +565,7 @@ impl ProjectSearchView { } fn render_query_editor(&self, cx: &mut RenderContext) -> ElementBox { - let theme = cx.app_state::().theme.clone(); + let theme = cx.global::().theme.clone(); let editor_container = if self.query_contains_error { theme.search.invalid_editor } else { @@ -642,7 +629,7 @@ impl ProjectSearchView { ) -> ElementBox { let is_active = self.is_option_enabled(option); MouseEventHandler::new::(option as usize, cx, |state, cx| { - let theme = &cx.app_state::().theme.search; + let theme = &cx.global::().theme.search; let style = match (is_active, state.hovered) { (false, false) => &theme.option_button, (false, true) => &theme.hovered_option_button, @@ -675,7 +662,7 @@ impl ProjectSearchView { ) -> ElementBox { enum NavButton {} MouseEventHandler::new::(direction as usize, cx, |state, cx| { - let theme = &cx.app_state::().theme.search; + let theme = &cx.global::().theme.search; let style = if state.hovered { &theme.hovered_option_button } else { @@ -707,7 +694,7 @@ mod tests { let mut theme = gpui::fonts::with_font_cache(fonts.clone(), || theme::Theme::default()); theme.search.match_background = Color::red(); let settings = Settings::new("Courier", &fonts, Arc::new(theme)).unwrap(); - cx.update(|cx| cx.add_app_state(settings)); + cx.update(|cx| cx.set_global(settings)); let fs = FakeFs::new(cx.background()); fs.insert_tree( @@ -732,7 +719,7 @@ mod tests { let search = cx.add_model(|cx| ProjectSearch::new(project, cx)); let search_view = cx.add_view(Default::default(), |cx| { - ProjectSearchView::new(search.clone(), None, cx) + ProjectSearchView::new(search.clone(), cx) }); search_view.update(cx, |search_view, cx| { diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index a1e335a0354ffdaaf903ba30c3d32f5d24c90093..9fb4cda8e9a8545b1861ccd9b23327563b06a937 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -39,9 +39,9 @@ pub(crate) fn active_match_index( None } else { match ranges.binary_search_by(|probe| { - if probe.end.cmp(&cursor, &*buffer).unwrap().is_lt() { + if probe.end.cmp(&cursor, &*buffer).is_lt() { Ordering::Less - } else if probe.start.cmp(&cursor, &*buffer).unwrap().is_gt() { + } else if probe.start.cmp(&cursor, &*buffer).is_gt() { Ordering::Greater } else { Ordering::Equal @@ -59,7 +59,7 @@ pub(crate) fn match_index_for_direction( direction: Direction, buffer: &MultiBufferSnapshot, ) -> usize { - if ranges[index].start.cmp(&cursor, &buffer).unwrap().is_gt() { + if ranges[index].start.cmp(&cursor, &buffer).is_gt() { if direction == Direction::Prev { if index == 0 { index = ranges.len() - 1; @@ -67,7 +67,7 @@ pub(crate) fn match_index_for_direction( index -= 1; } } - } else if ranges[index].end.cmp(&cursor, &buffer).unwrap().is_lt() { + } else if ranges[index].end.cmp(&cursor, &buffer).is_lt() { if direction == Direction::Next { index = 0; } diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index ed45c2d5d6b80d46c9d0a7455ff3621506bf14c1..374aaf6a7ec6820022c1802757cdabf3b51aa946 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -92,7 +92,8 @@ impl Server { .add_request_handler(Server::forward_project_request::) .add_request_handler(Server::forward_project_request::) .add_request_handler(Server::forward_project_request::) - .add_request_handler(Server::forward_project_request::) + .add_request_handler(Server::forward_project_request::) + .add_request_handler(Server::forward_project_request::) .add_request_handler(Server::forward_project_request::) .add_request_handler( Server::forward_project_request::, @@ -112,6 +113,9 @@ impl Server { .add_request_handler(Server::join_channel) .add_message_handler(Server::leave_channel) .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); Arc::new(server) @@ -669,6 +673,72 @@ impl Server { Ok(()) } + async fn follow( + self: Arc, + request: TypedEnvelope, + ) -> tide::Result { + 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, + request: TypedEnvelope, + ) -> 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, + request: TypedEnvelope, + ) -> 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( self: Arc, request: TypedEnvelope, @@ -1013,10 +1083,10 @@ mod tests { }; use collections::BTreeMap; use editor::{ - self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Input, MultiBuffer, - Redo, Rename, ToOffset, ToggleCodeActions, Undo, + self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Input, Redo, Rename, + ToOffset, ToggleCodeActions, Undo, }; - use gpui::{executor, ModelHandle, TestAppContext}; + use gpui::{executor, geometry::vector::vec2f, ModelHandle, TestAppContext, ViewHandle}; use language::{ tree_sitter_rust, Diagnostic, DiagnosticEntry, Language, LanguageConfig, LanguageRegistry, LanguageServerConfig, OffsetRangeExt, Point, ToLspPosition, @@ -1028,7 +1098,7 @@ mod tests { fs::{FakeFs, Fs as _}, search::SearchQuery, worktree::WorktreeHandle, - DiagnosticSummary, Project, ProjectPath, + DiagnosticSummary, Project, ProjectPath, WorktreeId, }; use rand::prelude::*; use rpc::PeerId; @@ -1046,7 +1116,7 @@ mod tests { }, time::Duration, }; - use workspace::{Settings, Workspace, WorkspaceParams}; + use workspace::{Item, Settings, SplitDirection, Workspace, WorkspaceParams}; #[cfg(test)] #[ctor::ctor] @@ -1140,10 +1210,7 @@ mod tests { .update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx)) .await .unwrap(); - let buffer_b = cx_b.add_model(|cx| MultiBuffer::singleton(buffer_b, cx)); - buffer_b.read_with(cx_b, |buf, cx| { - assert_eq!(buf.read(cx).text(), "b-contents") - }); + buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "b-contents")); project_a.read_with(cx_a, |project, cx| { assert!(project.has_open_buffer((worktree_id, "b.txt"), cx)) }); @@ -1243,10 +1310,105 @@ mod tests { .unwrap(); // Unshare the project as client A + project_a.update(cx_a, |project, cx| project.unshare(cx)); + project_b + .condition(cx_b, |project, _| project.is_read_only()) + .await; + assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared())); + cx_b.update(|_| { + drop(project_b); + }); + + // Share the project again and ensure guests can still join. project_a - .update(cx_a, |project, cx| project.unshare(cx)) + .update(cx_a, |project, cx| project.share(cx)) .await .unwrap(); + assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared())); + + let project_b2 = Project::remote( + project_id, + client_b.clone(), + client_b.user_store.clone(), + lang_registry.clone(), + fs.clone(), + &mut cx_b.to_async(), + ) + .await + .unwrap(); + project_b2 + .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) + .await + .unwrap(); + } + + #[gpui::test(iterations = 10)] + async fn test_host_disconnect(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { + let lang_registry = Arc::new(LanguageRegistry::test()); + let fs = FakeFs::new(cx_a.background()); + cx_a.foreground().forbid_parking(); + + // Connect to a server as 2 clients. + let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + + // Share a project as client A + fs.insert_tree( + "/a", + json!({ + ".zed.toml": r#"collaborators = ["user_b"]"#, + "a.txt": "a-contents", + "b.txt": "b-contents", + }), + ) + .await; + let project_a = cx_a.update(|cx| { + Project::local( + client_a.clone(), + client_a.user_store.clone(), + lang_registry.clone(), + fs.clone(), + cx, + ) + }); + let (worktree_a, _) = project_a + .update(cx_a, |p, cx| { + p.find_or_create_local_worktree("/a", true, cx) + }) + .await + .unwrap(); + worktree_a + .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) + .await; + let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await; + let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id()); + project_a.update(cx_a, |p, cx| p.share(cx)).await.unwrap(); + assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared())); + + // Join that project as client B + let project_b = Project::remote( + project_id, + client_b.clone(), + client_b.user_store.clone(), + lang_registry.clone(), + fs.clone(), + &mut cx_b.to_async(), + ) + .await + .unwrap(); + project_b + .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) + .await + .unwrap(); + + // Drop client A's connection. Collaborators should disappear and the project should not be shown as shared. + server.disconnect_client(client_a.current_user_id(cx_a)); + cx_a.foreground().advance_clock(rpc::RECEIVE_TIMEOUT); + project_a + .condition(cx_a, |project, _| project.collaborators().is_empty()) + .await; + project_a.read_with(cx_a, |project, _| assert!(!project.is_shared())); project_b .condition(cx_b, |project, _| project.is_read_only()) .await; @@ -1255,6 +1417,9 @@ mod tests { drop(project_b); }); + // Await reconnection + let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await; + // Share the project again and ensure guests can still join. project_a .update(cx_a, |project, cx| project.share(cx)) @@ -2176,11 +2341,7 @@ mod tests { .unwrap(); let (window_b, _) = cx_b.add_window(|_| EmptyView); let editor_b = cx_b.add_view(window_b, |cx| { - Editor::for_buffer( - cx.add_model(|cx| MultiBuffer::singleton(buffer_b.clone(), cx)), - Some(project_b.clone()), - cx, - ) + Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx) }); let mut fake_language_server = fake_language_servers.next().await.unwrap(); @@ -2199,7 +2360,7 @@ mod tests { // Return some completions from the host's language server. cx_a.foreground().start_waiting(); fake_language_server - .handle_request::(|params, _| { + .handle_request::(|params, _| async move { assert_eq!( params.text_document_position.text_document.uri, lsp::Url::from_file_path("/a/main.rs").unwrap(), @@ -2263,8 +2424,8 @@ mod tests { // Return a resolved completion from the host's language server. // The resolved completion has an additional text edit. - fake_language_server.handle_request::( - |params, _| { + fake_language_server.handle_request::( + |params, _| async move { assert_eq!(params.label, "first_method(…)"); lsp::CompletionItem { label: "first_method(…)".into(), @@ -2374,7 +2535,7 @@ mod tests { .unwrap(); let mut fake_language_server = fake_language_servers.next().await.unwrap(); - fake_language_server.handle_request::(|_, _| { + fake_language_server.handle_request::(|_, _| async move { Some(vec![ lsp::TextEdit { range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 4)), @@ -2483,12 +2644,14 @@ mod tests { // Request the definition of a symbol as the guest. let mut fake_language_server = fake_language_servers.next().await.unwrap(); - fake_language_server.handle_request::(|_, _| { - Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location::new( - lsp::Url::from_file_path("/root-2/b.rs").unwrap(), - lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)), - ))) - }); + fake_language_server.handle_request::( + |_, _| async move { + Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location::new( + lsp::Url::from_file_path("/root-2/b.rs").unwrap(), + lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)), + ))) + }, + ); let definitions_1 = project_b .update(cx_b, |p, cx| p.definition(&buffer_b, 23, cx)) @@ -2510,12 +2673,14 @@ mod tests { // Try getting more definitions for the same buffer, ensuring the buffer gets reused from // the previous call to `definition`. - fake_language_server.handle_request::(|_, _| { - Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location::new( - lsp::Url::from_file_path("/root-2/b.rs").unwrap(), - lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)), - ))) - }); + fake_language_server.handle_request::( + |_, _| async move { + Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location::new( + lsp::Url::from_file_path("/root-2/b.rs").unwrap(), + lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)), + ))) + }, + ); let definitions_2 = project_b .update(cx_b, |p, cx| p.definition(&buffer_b, 33, cx)) @@ -2622,26 +2787,37 @@ mod tests { // Request references to a symbol as the guest. let mut fake_language_server = fake_language_servers.next().await.unwrap(); - fake_language_server.handle_request::(|params, _| { - assert_eq!( - params.text_document_position.text_document.uri.as_str(), - "file:///root-1/one.rs" - ); - Some(vec![ - lsp::Location { - uri: lsp::Url::from_file_path("/root-1/two.rs").unwrap(), - range: lsp::Range::new(lsp::Position::new(0, 24), lsp::Position::new(0, 27)), - }, - lsp::Location { - uri: lsp::Url::from_file_path("/root-1/two.rs").unwrap(), - range: lsp::Range::new(lsp::Position::new(0, 35), lsp::Position::new(0, 38)), - }, - lsp::Location { - uri: lsp::Url::from_file_path("/root-2/three.rs").unwrap(), - range: lsp::Range::new(lsp::Position::new(0, 37), lsp::Position::new(0, 40)), - }, - ]) - }); + fake_language_server.handle_request::( + |params, _| async move { + assert_eq!( + params.text_document_position.text_document.uri.as_str(), + "file:///root-1/one.rs" + ); + Some(vec![ + lsp::Location { + uri: lsp::Url::from_file_path("/root-1/two.rs").unwrap(), + range: lsp::Range::new( + lsp::Position::new(0, 24), + lsp::Position::new(0, 27), + ), + }, + lsp::Location { + uri: lsp::Url::from_file_path("/root-1/two.rs").unwrap(), + range: lsp::Range::new( + lsp::Position::new(0, 35), + lsp::Position::new(0, 38), + ), + }, + lsp::Location { + uri: lsp::Url::from_file_path("/root-2/three.rs").unwrap(), + range: lsp::Range::new( + lsp::Position::new(0, 37), + lsp::Position::new(0, 40), + ), + }, + ]) + }, + ); let references = project_b .update(cx_b, |p, cx| p.references(&buffer_b, 7, cx)) @@ -2851,8 +3027,8 @@ mod tests { // Request document highlights as the guest. let mut fake_language_server = fake_language_servers.next().await.unwrap(); - fake_language_server.handle_request::( - |params, _| { + fake_language_server.handle_request::( + |params, _| async move { assert_eq!( params .text_document_position_params @@ -2997,20 +3173,22 @@ mod tests { .unwrap(); let mut fake_language_server = fake_language_servers.next().await.unwrap(); - fake_language_server.handle_request::(|_, _| { - #[allow(deprecated)] - Some(vec![lsp::SymbolInformation { - name: "TWO".into(), - location: lsp::Location { - uri: lsp::Url::from_file_path("/code/crate-2/two.rs").unwrap(), - range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)), - }, - kind: lsp::SymbolKind::CONSTANT, - tags: None, - container_name: None, - deprecated: None, - }]) - }); + fake_language_server.handle_request::( + |_, _| async move { + #[allow(deprecated)] + Some(vec![lsp::SymbolInformation { + name: "TWO".into(), + location: lsp::Location { + uri: lsp::Url::from_file_path("/code/crate-2/two.rs").unwrap(), + range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)), + }, + kind: lsp::SymbolKind::CONSTANT, + tags: None, + container_name: None, + deprecated: None, + }]) + }, + ); // Request the definition of a symbol as the guest. let symbols = project_b @@ -3128,12 +3306,14 @@ mod tests { .unwrap(); let mut fake_language_server = fake_language_servers.next().await.unwrap(); - fake_language_server.handle_request::(|_, _| { - Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location::new( - lsp::Url::from_file_path("/root/b.rs").unwrap(), - lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)), - ))) - }); + fake_language_server.handle_request::( + |_, _| async move { + Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location::new( + lsp::Url::from_file_path("/root/b.rs").unwrap(), + lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)), + ))) + }, + ); let definitions; let buffer_b2; @@ -3159,8 +3339,7 @@ mod tests { cx_a.foreground().forbid_parking(); let mut lang_registry = Arc::new(LanguageRegistry::test()); let fs = FakeFs::new(cx_a.background()); - let mut path_openers_b = Vec::new(); - cx_b.update(|cx| editor::init(cx, &mut path_openers_b)); + cx_b.update(|cx| editor::init(cx)); // Set up a fake language server. let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake(); @@ -3229,12 +3408,11 @@ mod tests { params.client = client_b.client.clone(); params.user_store = client_b.user_store.clone(); params.project = project_b; - params.path_openers = path_openers_b.into(); let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(¶ms, cx)); let editor_b = workspace_b .update(cx_b, |workspace, cx| { - workspace.open_path((worktree_id, "main.rs").into(), cx) + workspace.open_path((worktree_id, "main.rs"), cx) }) .await .unwrap() @@ -3243,7 +3421,7 @@ mod tests { let mut fake_language_server = fake_language_servers.next().await.unwrap(); fake_language_server - .handle_request::(|params, _| { + .handle_request::(|params, _| async move { assert_eq!( params.text_document.uri, lsp::Url::from_file_path("/a/main.rs").unwrap(), @@ -3262,7 +3440,7 @@ mod tests { }); fake_language_server - .handle_request::(|params, _| { + .handle_request::(|params, _| async move { assert_eq!( params.text_document.uri, lsp::Url::from_file_path("/a/main.rs").unwrap(), @@ -3333,41 +3511,43 @@ mod tests { Editor::confirm_code_action(workspace, &ConfirmCodeAction(Some(0)), cx) }) .unwrap(); - fake_language_server.handle_request::(|_, _| { - lsp::CodeAction { - title: "Inline into all callers".to_string(), - edit: Some(lsp::WorkspaceEdit { - changes: Some( - [ - ( - lsp::Url::from_file_path("/a/main.rs").unwrap(), - vec![lsp::TextEdit::new( - lsp::Range::new( - lsp::Position::new(1, 22), - lsp::Position::new(1, 34), - ), - "4".to_string(), - )], - ), - ( - lsp::Url::from_file_path("/a/other.rs").unwrap(), - vec![lsp::TextEdit::new( - lsp::Range::new( - lsp::Position::new(0, 0), - lsp::Position::new(0, 27), - ), - "".to_string(), - )], - ), - ] - .into_iter() - .collect(), - ), + fake_language_server.handle_request::( + |_, _| async move { + lsp::CodeAction { + title: "Inline into all callers".to_string(), + edit: Some(lsp::WorkspaceEdit { + changes: Some( + [ + ( + lsp::Url::from_file_path("/a/main.rs").unwrap(), + vec![lsp::TextEdit::new( + lsp::Range::new( + lsp::Position::new(1, 22), + lsp::Position::new(1, 34), + ), + "4".to_string(), + )], + ), + ( + lsp::Url::from_file_path("/a/other.rs").unwrap(), + vec![lsp::TextEdit::new( + lsp::Range::new( + lsp::Position::new(0, 0), + lsp::Position::new(0, 27), + ), + "".to_string(), + )], + ), + ] + .into_iter() + .collect(), + ), + ..Default::default() + }), ..Default::default() - }), - ..Default::default() - } - }); + } + }, + ); // After the action is confirmed, an editor containing both modified files is opened. confirm_action.await.unwrap(); @@ -3395,8 +3575,7 @@ mod tests { cx_a.foreground().forbid_parking(); let mut lang_registry = Arc::new(LanguageRegistry::test()); let fs = FakeFs::new(cx_a.background()); - let mut path_openers_b = Vec::new(); - cx_b.update(|cx| editor::init(cx, &mut path_openers_b)); + cx_b.update(|cx| editor::init(cx)); // Set up a fake language server. let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake(); @@ -3465,12 +3644,11 @@ mod tests { params.client = client_b.client.clone(); params.user_store = client_b.user_store.clone(); params.project = project_b; - params.path_openers = path_openers_b.into(); let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(¶ms, cx)); let editor_b = workspace_b .update(cx_b, |workspace, cx| { - workspace.open_path((worktree_id, "one.rs").into(), cx) + workspace.open_path((worktree_id, "one.rs"), cx) }) .await .unwrap() @@ -3485,7 +3663,7 @@ mod tests { }); fake_language_server - .handle_request::(|params, _| { + .handle_request::(|params, _| async move { assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs"); assert_eq!(params.position, lsp::Position::new(0, 7)); Some(lsp::PrepareRenameResponse::Range(lsp::Range::new( @@ -3515,7 +3693,7 @@ mod tests { Editor::confirm_rename(workspace, &ConfirmRename, cx).unwrap() }); fake_language_server - .handle_request::(|params, _| { + .handle_request::(|params, _| async move { assert_eq!( params.text_document_position.text_document.uri.as_str(), "file:///dir/one.rs" @@ -4159,6 +4337,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::() + .unwrap(); + let editor_a2 = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "2.txt"), cx) + }) + .await + .unwrap() + .downcast::() + .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::() + .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::() + .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::() + .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::() + .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::() + .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::() + .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)] async fn test_random_collaboration(cx: &mut TestAppContext, rng: StdRng) { cx.foreground().forbid_parking(); @@ -4418,7 +5084,7 @@ mod tests { async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> TestClient { cx.update(|cx| { let settings = Settings::test(cx); - cx.add_app_state(settings); + cx.set_global(settings); }); let http = FakeHttpClient::with_404_response(); @@ -4474,12 +5140,15 @@ mod tests { }); client - .authenticate_and_connect(&cx.to_async()) + .authenticate_and_connect(false, &cx.to_async()) .await .unwrap(); Channel::init(&client); Project::init(&client); + cx.update(|cx| { + workspace::init(&client, cx); + }); let peer_id = PeerId(connection_id_rx.next().await.unwrap().0); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx)); @@ -4488,6 +5157,7 @@ mod tests { client, peer_id, user_store, + language_registry: Arc::new(LanguageRegistry::test()), project: Default::default(), buffers: Default::default(), }; @@ -4552,6 +5222,7 @@ mod tests { client: Arc, pub peer_id: PeerId, pub user_store: ModelHandle, + language_registry: Arc, project: Option>, buffers: HashSet>, } @@ -4579,6 +5250,80 @@ mod tests { while authed_user.next().await.unwrap().is_none() {} } + async fn build_local_project( + &mut self, + fs: Arc, + root_path: impl AsRef, + cx: &mut TestAppContext, + ) -> (ModelHandle, 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 { + 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, + cx: &mut TestAppContext, + ) -> ViewHandle { + 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( mut self, project: ModelHandle, @@ -4596,30 +5341,34 @@ mod tests { let files = files.clone(); let project = project.downgrade(); move |fake_server| { - fake_server.handle_request::(|_, _| { - Some(lsp::CompletionResponse::Array(vec![lsp::CompletionItem { - text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { - range: lsp::Range::new( - lsp::Position::new(0, 0), - lsp::Position::new(0, 0), - ), - new_text: "the-new-text".to_string(), - })), - ..Default::default() - }])) - }); - - fake_server.handle_request::(|_, _| { - Some(vec![lsp::CodeActionOrCommand::CodeAction( - lsp::CodeAction { - title: "the-code-action".to_string(), + fake_server.handle_request::( + |_, _| async move { + Some(lsp::CompletionResponse::Array(vec![lsp::CompletionItem { + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: lsp::Range::new( + lsp::Position::new(0, 0), + lsp::Position::new(0, 0), + ), + new_text: "the-new-text".to_string(), + })), ..Default::default() - }, - )]) - }); + }])) + }, + ); - fake_server.handle_request::( - |params, _| { + fake_server.handle_request::( + |_, _| async move { + Some(vec![lsp::CodeActionOrCommand::CodeAction( + lsp::CodeAction { + title: "the-code-action".to_string(), + ..Default::default() + }, + )]) + }, + ); + + fake_server.handle_request::( + |params, _| async move { Some(lsp::PrepareRenameResponse::Range(lsp::Range::new( params.position, params.position, @@ -4627,34 +5376,38 @@ mod tests { }, ); - fake_server.handle_request::({ + fake_server.handle_request::({ let files = files.clone(); let rng = rng.clone(); move |_, _| { - let files = files.lock(); - let mut rng = rng.lock(); - let count = rng.gen_range::(1..3); - let files = (0..count) - .map(|_| files.choose(&mut *rng).unwrap()) - .collect::>(); - log::info!("LSP: Returning definitions in files {:?}", &files); - Some(lsp::GotoDefinitionResponse::Array( - files - .into_iter() - .map(|file| lsp::Location { - uri: lsp::Url::from_file_path(file).unwrap(), - range: Default::default(), - }) - .collect(), - )) + let files = files.clone(); + let rng = rng.clone(); + async move { + let files = files.lock(); + let mut rng = rng.lock(); + let count = rng.gen_range::(1..3); + let files = (0..count) + .map(|_| files.choose(&mut *rng).unwrap()) + .collect::>(); + log::info!("LSP: Returning definitions in files {:?}", &files); + Some(lsp::GotoDefinitionResponse::Array( + files + .into_iter() + .map(|file| lsp::Location { + uri: lsp::Url::from_file_path(file).unwrap(), + range: Default::default(), + }) + .collect(), + )) + } } }); - fake_server.handle_request::({ + fake_server.handle_request::({ let rng = rng.clone(); let project = project.clone(); move |params, mut cx| { - if let Some(project) = project.upgrade(&cx) { + let highlights = if let Some(project) = project.upgrade(&cx) { project.update(&mut cx, |project, cx| { let path = params .text_document_position_params @@ -4691,7 +5444,8 @@ mod tests { }) } else { None - } + }; + async move { highlights } } }); } diff --git a/crates/text/src/anchor.rs b/crates/text/src/anchor.rs index 28da998d6770f6881ac65fcb439726ae972d139c..e642aa45d3ff06ffd326dcf633df1985ee5d7ef0 100644 --- a/crates/text/src/anchor.rs +++ b/crates/text/src/anchor.rs @@ -12,23 +12,19 @@ pub struct Anchor { } impl Anchor { - pub fn min() -> Self { - Self { - timestamp: clock::Local::MIN, - offset: usize::MIN, - bias: Bias::Left, - } - } + pub const MIN: Self = Self { + timestamp: clock::Local::MIN, + offset: usize::MIN, + bias: Bias::Left, + }; - pub fn max() -> Self { - Self { - timestamp: clock::Local::MAX, - offset: usize::MAX, - bias: Bias::Right, - } - } + pub const MAX: Self = Self { + timestamp: clock::Local::MAX, + offset: usize::MAX, + bias: Bias::Right, + }; - pub fn cmp(&self, other: &Anchor, buffer: &BufferSnapshot) -> Result { + pub fn cmp(&self, other: &Anchor, buffer: &BufferSnapshot) -> Ordering { let fragment_id_comparison = if self.timestamp == other.timestamp { Ordering::Equal } else { @@ -37,9 +33,25 @@ impl Anchor { .cmp(&buffer.fragment_id_for_anchor(other)) }; - Ok(fragment_id_comparison + fragment_id_comparison .then_with(|| self.offset.cmp(&other.offset)) - .then_with(|| self.bias.cmp(&other.bias))) + .then_with(|| self.bias.cmp(&other.bias)) + } + + pub fn min(&self, other: &Self, buffer: &BufferSnapshot) -> Self { + if self.cmp(other, buffer).is_le() { + self.clone() + } else { + other.clone() + } + } + + pub fn max(&self, other: &Self, buffer: &BufferSnapshot) -> Self { + if self.cmp(other, buffer).is_ge() { + self.clone() + } else { + other.clone() + } } pub fn bias(&self, bias: Bias, buffer: &BufferSnapshot) -> Anchor { @@ -105,8 +117,8 @@ pub trait AnchorRangeExt { impl AnchorRangeExt for Range { fn cmp(&self, other: &Range, buffer: &BufferSnapshot) -> Result { - Ok(match self.start.cmp(&other.start, buffer)? { - Ordering::Equal => other.end.cmp(&self.end, buffer)?, + Ok(match self.start.cmp(&other.start, buffer) { + Ordering::Equal => other.end.cmp(&self.end, buffer), ord @ _ => ord, }) } diff --git a/crates/text/src/patch.rs b/crates/text/src/patch.rs index 8e68b2545ce2e54833bdac4022c0f25c32593fb9..63e69f45a7d231c74dd7a13f82b57ac60f19eb8d 100644 --- a/crates/text/src/patch.rs +++ b/crates/text/src/patch.rs @@ -199,6 +199,28 @@ where self.0.push(edit); } } + + pub fn old_to_new(&self, old: T) -> T { + let ix = match self.0.binary_search_by(|probe| probe.old.start.cmp(&old)) { + Ok(ix) => ix, + Err(ix) => { + if ix == 0 { + return old; + } else { + ix - 1 + } + } + }; + if let Some(edit) = self.0.get(ix) { + if old >= edit.old.end { + edit.new.end + (old - edit.old.end) + } else { + edit.new.start + } + } else { + old + } + } } impl IntoIterator for Patch { @@ -399,26 +421,6 @@ mod tests { ); } - // #[test] - // fn test_compose_edits() { - // assert_eq!( - // compose_edits( - // &Edit { - // old: 3..3, - // new: 3..6, - // }, - // &Edit { - // old: 2..7, - // new: 2..4, - // }, - // ), - // Edit { - // old: 2..4, - // new: 2..4 - // } - // ); - // } - #[gpui::test] fn test_two_new_edits_touching_one_old_edit() { assert_patch_composition( @@ -455,6 +457,30 @@ mod tests { ); } + #[gpui::test] + fn test_old_to_new() { + let patch = Patch(vec![ + Edit { + old: 2..4, + new: 2..4, + }, + Edit { + old: 7..8, + new: 7..11, + }, + ]); + assert_eq!(patch.old_to_new(0), 0); + assert_eq!(patch.old_to_new(1), 1); + assert_eq!(patch.old_to_new(2), 2); + assert_eq!(patch.old_to_new(3), 2); + assert_eq!(patch.old_to_new(4), 4); + assert_eq!(patch.old_to_new(5), 5); + assert_eq!(patch.old_to_new(6), 6); + assert_eq!(patch.old_to_new(7), 7); + assert_eq!(patch.old_to_new(8), 11); + assert_eq!(patch.old_to_new(9), 12); + } + #[gpui::test(iterations = 100)] fn test_random_patch_compositions(mut rng: StdRng) { let operations = env::var("OPERATIONS") diff --git a/crates/text/src/selection.rs b/crates/text/src/selection.rs index 4e7d6f52367094fa5bbf07cd8f55250259bd7601..23b0d2b3b042cbbee44edc52e3e97d8137c6bdf3 100644 --- a/crates/text/src/selection.rs +++ b/crates/text/src/selection.rs @@ -1,5 +1,5 @@ use crate::Anchor; -use crate::{rope::TextDimension, BufferSnapshot, ToOffset, ToPoint}; +use crate::{rope::TextDimension, BufferSnapshot}; use std::cmp::Ordering; #[derive(Copy, Clone, Debug, Eq, PartialEq)] @@ -18,6 +18,12 @@ pub struct Selection { pub goal: SelectionGoal, } +impl Default for SelectionGoal { + fn default() -> Self { + Self::None + } +} + impl Selection { pub fn head(&self) -> T { if self.reversed { @@ -34,14 +40,27 @@ impl Selection { self.start.clone() } } + + pub fn map(&self, f: F) -> Selection + where + F: Fn(T) -> S, + { + Selection:: { + id: self.id, + start: f(self.start.clone()), + end: f(self.end.clone()), + reversed: self.reversed, + goal: self.goal, + } + } } -impl Selection { +impl Selection { pub fn is_empty(&self) -> bool { self.start == self.end } - pub fn set_head(&mut self, head: T) { + pub fn set_head(&mut self, head: T, new_goal: SelectionGoal) { if head.cmp(&self.tail()) < Ordering::Equal { if !self.reversed { self.end = self.start; @@ -55,6 +74,14 @@ impl Selection { } self.end = head; } + self.goal = new_goal; + } + + pub fn collapse_to(&mut self, point: T, new_goal: SelectionGoal) { + self.start = point; + self.end = point; + self.goal = new_goal; + self.reversed = false; } } diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs index 05cf0af6ec52e9006816ec056939c1d2be713ba4..7961dccd569c8380c3bb32e57e9057481e4371fd 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -340,59 +340,41 @@ fn test_anchors() { let anchor_at_offset_2 = buffer.anchor_before(2); assert_eq!( - anchor_at_offset_0 - .cmp(&anchor_at_offset_0, &buffer) - .unwrap(), + anchor_at_offset_0.cmp(&anchor_at_offset_0, &buffer), Ordering::Equal ); assert_eq!( - anchor_at_offset_1 - .cmp(&anchor_at_offset_1, &buffer) - .unwrap(), + anchor_at_offset_1.cmp(&anchor_at_offset_1, &buffer), Ordering::Equal ); assert_eq!( - anchor_at_offset_2 - .cmp(&anchor_at_offset_2, &buffer) - .unwrap(), + anchor_at_offset_2.cmp(&anchor_at_offset_2, &buffer), Ordering::Equal ); assert_eq!( - anchor_at_offset_0 - .cmp(&anchor_at_offset_1, &buffer) - .unwrap(), + anchor_at_offset_0.cmp(&anchor_at_offset_1, &buffer), Ordering::Less ); assert_eq!( - anchor_at_offset_1 - .cmp(&anchor_at_offset_2, &buffer) - .unwrap(), + anchor_at_offset_1.cmp(&anchor_at_offset_2, &buffer), Ordering::Less ); assert_eq!( - anchor_at_offset_0 - .cmp(&anchor_at_offset_2, &buffer) - .unwrap(), + anchor_at_offset_0.cmp(&anchor_at_offset_2, &buffer), Ordering::Less ); assert_eq!( - anchor_at_offset_1 - .cmp(&anchor_at_offset_0, &buffer) - .unwrap(), + anchor_at_offset_1.cmp(&anchor_at_offset_0, &buffer), Ordering::Greater ); assert_eq!( - anchor_at_offset_2 - .cmp(&anchor_at_offset_1, &buffer) - .unwrap(), + anchor_at_offset_2.cmp(&anchor_at_offset_1, &buffer), Ordering::Greater ); assert_eq!( - anchor_at_offset_2 - .cmp(&anchor_at_offset_0, &buffer) - .unwrap(), + anchor_at_offset_2.cmp(&anchor_at_offset_0, &buffer), Ordering::Greater ); } diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 338a5c0ad1425049a811c43fc81b02281ccbe8d2..b811d08c046c58f8c5d6c020c27c45a430258474 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -1318,8 +1318,8 @@ impl Buffer { let mut futures = Vec::new(); for anchor in anchors { if !self.version.observed(anchor.timestamp) - && *anchor != Anchor::max() - && *anchor != Anchor::min() + && *anchor != Anchor::MAX + && *anchor != Anchor::MIN { let (tx, rx) = oneshot::channel(); self.edit_id_resolvers @@ -1638,9 +1638,9 @@ impl BufferSnapshot { let mut position = D::default(); anchors.map(move |anchor| { - if *anchor == Anchor::min() { + if *anchor == Anchor::MIN { return D::default(); - } else if *anchor == Anchor::max() { + } else if *anchor == Anchor::MAX { return D::from_text_summary(&self.visible_text.summary()); } @@ -1680,9 +1680,9 @@ impl BufferSnapshot { where D: TextDimension, { - if *anchor == Anchor::min() { + if *anchor == Anchor::MIN { D::default() - } else if *anchor == Anchor::max() { + } else if *anchor == Anchor::MAX { D::from_text_summary(&self.visible_text.summary()) } else { let anchor_key = InsertionFragmentKey { @@ -1718,9 +1718,9 @@ impl BufferSnapshot { } fn fragment_id_for_anchor(&self, anchor: &Anchor) -> &Locator { - if *anchor == Anchor::min() { + if *anchor == Anchor::MIN { &locator::MIN - } else if *anchor == Anchor::max() { + } else if *anchor == Anchor::MAX { &locator::MAX } else { let anchor_key = InsertionFragmentKey { @@ -1758,9 +1758,9 @@ impl BufferSnapshot { pub fn anchor_at(&self, position: T, bias: Bias) -> Anchor { let offset = position.to_offset(self); if bias == Bias::Left && offset == 0 { - Anchor::min() + Anchor::MIN } else if bias == Bias::Right && offset == self.len() { - Anchor::max() + Anchor::MAX } else { let mut fragment_cursor = self.fragments.cursor::(); fragment_cursor.seek(&offset, bias, &None); @@ -1775,9 +1775,7 @@ impl BufferSnapshot { } pub fn can_resolve(&self, anchor: &Anchor) -> bool { - *anchor == Anchor::min() - || *anchor == Anchor::max() - || self.version.observed(anchor.timestamp) + *anchor == Anchor::MIN || *anchor == Anchor::MAX || self.version.observed(anchor.timestamp) } pub fn clip_offset(&self, offset: usize, bias: Bias) -> usize { @@ -1799,7 +1797,7 @@ impl BufferSnapshot { where D: TextDimension + Ord, { - self.edits_since_in_range(since, Anchor::min()..Anchor::max()) + self.edits_since_in_range(since, Anchor::MIN..Anchor::MAX) } pub fn edited_ranges_for_transaction<'a, D>( diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 5e8e799b4c58fd9d073ff078ad19fbb6db0af53d..d10c282e3526056acd397794b184491bd17a43b6 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -35,6 +35,8 @@ pub struct Workspace { pub tab: Tab, pub active_tab: Tab, pub pane_divider: Border, + pub leader_border_opacity: f32, + pub leader_border_width: f32, pub left_sidebar: Sidebar, pub right_sidebar: Sidebar, pub status_bar: StatusBar, diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index c3199bc20bf24153de96ef98f24da8b97d6874ed..725319be419b8e196433590bf673f8bc89b936ac 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/crates/theme_selector/src/theme_selector.rs @@ -54,7 +54,7 @@ impl ThemeSelector { cx.subscribe(&query_editor, Self::on_query_editor_event) .detach(); - let original_theme = cx.app_state::().theme.clone(); + let original_theme = cx.global::().theme.clone(); let mut this = Self { themes: registry, @@ -82,7 +82,7 @@ impl ThemeSelector { } fn reload(_: &mut Workspace, action: &Reload, cx: &mut ViewContext) { - let current_theme_name = cx.app_state::().theme.name.clone(); + let current_theme_name = cx.global::().theme.name.clone(); action.0.clear(); match action.0.get(¤t_theme_name) { Ok(theme) => { @@ -204,9 +204,9 @@ impl ThemeSelector { cx: &mut ViewContext, ) { match event { - editor::Event::Edited => { + editor::Event::BufferEdited { .. } => { self.update_matches(cx); - self.select_if_matching(&cx.app_state::().theme.name); + self.select_if_matching(&cx.global::().theme.name); self.show_selected_theme(cx); } editor::Event::Blurred => cx.emit(Event::Dismissed), @@ -216,7 +216,7 @@ impl ThemeSelector { fn render_matches(&self, cx: &mut RenderContext) -> ElementBox { if self.matches.is_empty() { - let settings = cx.app_state::(); + let settings = cx.global::(); return Container::new( Label::new( "No matches".into(), @@ -251,7 +251,7 @@ impl ThemeSelector { } fn render_match(&self, theme_match: &StringMatch, index: usize, cx: &AppContext) -> ElementBox { - let settings = cx.app_state::(); + let settings = cx.global::(); let theme = &settings.theme; let container = Container::new( @@ -276,7 +276,7 @@ impl ThemeSelector { } fn set_theme(theme: Arc, cx: &mut MutableAppContext) { - cx.update_app_state::(|settings, cx| { + cx.update_global::(|settings, cx| { settings.theme = theme; cx.refresh_windows(); }); @@ -299,7 +299,7 @@ impl View for ThemeSelector { } fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - let theme = cx.app_state::().theme.clone(); + let theme = cx.global::().theme.clone(); Align::new( ConstrainedBox::new( Container::new( diff --git a/crates/util/src/test.rs b/crates/util/src/test.rs index 71b847df692af9e65bf24223c1bdeae35a923d25..252383b3477704bd56fe8d98427b38e424a903b4 100644 --- a/crates/util/src/test.rs +++ b/crates/util/src/test.rs @@ -1,4 +1,8 @@ -use std::path::{Path, PathBuf}; +use std::{ + collections::HashMap, + ops::Range, + path::{Path, PathBuf}, +}; use tempdir::TempDir; pub fn temp_tree(tree: serde_json::Value) -> TempDir { @@ -48,3 +52,44 @@ pub fn sample_text(rows: usize, cols: usize, start_char: char) -> String { } text } + +pub fn marked_text_by( + marked_text: &str, + markers: Vec, +) -> (String, HashMap>) { + let mut extracted_markers: HashMap> = Default::default(); + let mut unmarked_text = String::new(); + + for char in marked_text.chars() { + if markers.contains(&char) { + let char_offsets = extracted_markers.entry(char).or_insert(Vec::new()); + char_offsets.push(unmarked_text.len()); + } else { + unmarked_text.push(char); + } + } + + (unmarked_text, extracted_markers) +} + +pub fn marked_text(marked_text: &str) -> (String, Vec) { + let (unmarked_text, mut markers) = marked_text_by(marked_text, vec!['|']); + (unmarked_text, markers.remove(&'|').unwrap_or_else(Vec::new)) +} + +pub fn marked_text_ranges(marked_text: &str) -> (String, Vec>) { + let (unmarked_text, mut markers) = marked_text_by(marked_text, vec!['[', ']']); + let opens = markers.remove(&'[').unwrap_or_default(); + let closes = markers.remove(&']').unwrap_or_default(); + assert_eq!(opens.len(), closes.len(), "marked ranges are unbalanced"); + + let ranges = opens + .into_iter() + .zip(closes) + .map(|(open, close)| { + assert!(close >= open, "marked ranges must be disjoint"); + open..close + }) + .collect(); + (unmarked_text, ranges) +} diff --git a/crates/vim/Cargo.toml b/crates/vim/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..28ee7de8729f5fc5a8ba4bba2032ffa9d38ee99e --- /dev/null +++ b/crates/vim/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "vim" +version = "0.1.0" +edition = "2021" + +[lib] +path = "src/vim.rs" +doctest = false + +[dependencies] +collections = { path = "../collections" } +editor = { path = "../editor" } +gpui = { path = "../gpui" } +language = { path = "../language" } +workspace = { path = "../workspace" } +log = "0.4" + +[dev-dependencies] +indoc = "1.0.4" +editor = { path = "../editor", features = ["test-support"] } +gpui = { path = "../gpui", features = ["test-support"] } +project = { path = "../project", features = ["test-support"] } +language = { path = "../language", features = ["test-support"] } +util = { path = "../util", features = ["test-support"] } +workspace = { path = "../workspace", features = ["test-support"] } \ No newline at end of file diff --git a/crates/vim/src/editor_events.rs b/crates/vim/src/editor_events.rs new file mode 100644 index 0000000000000000000000000000000000000000..7e49b473f9de17b5969eabfa4b8e014bf4fb7779 --- /dev/null +++ b/crates/vim/src/editor_events.rs @@ -0,0 +1,53 @@ +use editor::{EditorBlurred, EditorCreated, EditorFocused, EditorMode, EditorReleased}; +use gpui::MutableAppContext; + +use crate::{mode::Mode, SwitchMode, VimState}; + +pub fn init(cx: &mut MutableAppContext) { + cx.subscribe_global(editor_created).detach(); + cx.subscribe_global(editor_focused).detach(); + cx.subscribe_global(editor_blurred).detach(); + cx.subscribe_global(editor_released).detach(); +} + +fn editor_created(EditorCreated(editor): &EditorCreated, cx: &mut MutableAppContext) { + cx.update_default_global(|vim_state: &mut VimState, cx| { + vim_state.editors.insert(editor.id(), editor.downgrade()); + vim_state.sync_editor_options(cx); + }) +} + +fn editor_focused(EditorFocused(editor): &EditorFocused, cx: &mut MutableAppContext) { + let mode = if matches!(editor.read(cx).mode(), EditorMode::SingleLine) { + Mode::Insert + } else { + Mode::Normal + }; + + VimState::update_global(cx, |state, cx| { + state.active_editor = Some(editor.downgrade()); + state.switch_mode(&SwitchMode(mode), cx); + }); +} + +fn editor_blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut MutableAppContext) { + VimState::update_global(cx, |state, cx| { + if let Some(previous_editor) = state.active_editor.clone() { + if previous_editor == editor.clone() { + state.active_editor = None; + } + } + state.sync_editor_options(cx); + }) +} + +fn editor_released(EditorReleased(editor): &EditorReleased, cx: &mut MutableAppContext) { + cx.update_default_global(|vim_state: &mut VimState, _| { + vim_state.editors.remove(&editor.id()); + if let Some(previous_editor) = vim_state.active_editor.clone() { + if previous_editor == editor.clone() { + vim_state.active_editor = None; + } + } + }); +} diff --git a/crates/vim/src/insert.rs b/crates/vim/src/insert.rs new file mode 100644 index 0000000000000000000000000000000000000000..c027ff2c1f3de1e2d63f824e4c58df5362c6e108 --- /dev/null +++ b/crates/vim/src/insert.rs @@ -0,0 +1,30 @@ +use editor::Bias; +use gpui::{action, keymap::Binding, MutableAppContext, ViewContext}; +use language::SelectionGoal; +use workspace::Workspace; + +use crate::{mode::Mode, SwitchMode, VimState}; + +action!(NormalBefore); + +pub fn init(cx: &mut MutableAppContext) { + let context = Some("Editor && vim_mode == insert"); + cx.add_bindings(vec![ + Binding::new("escape", NormalBefore, context), + Binding::new("ctrl-c", NormalBefore, context), + ]); + + cx.add_action(normal_before); +} + +fn normal_before(_: &mut Workspace, _: &NormalBefore, cx: &mut ViewContext) { + VimState::update_global(cx, |state, cx| { + state.update_active_editor(cx, |editor, cx| { + editor.move_cursors(cx, |map, mut cursor, _| { + *cursor.column_mut() = cursor.column().saturating_sub(1); + (map.clip_point(cursor, Bias::Left), SelectionGoal::None) + }); + }); + state.switch_mode(&SwitchMode(Mode::Normal), cx); + }) +} diff --git a/crates/vim/src/mode.rs b/crates/vim/src/mode.rs new file mode 100644 index 0000000000000000000000000000000000000000..2438a9fa3d4bc3cc180c70a7418abafa1ddb2a03 --- /dev/null +++ b/crates/vim/src/mode.rs @@ -0,0 +1,36 @@ +use editor::CursorShape; +use gpui::keymap::Context; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Mode { + Normal, + Insert, +} + +impl Mode { + pub fn cursor_shape(&self) -> CursorShape { + match self { + Mode::Normal => CursorShape::Block, + Mode::Insert => CursorShape::Bar, + } + } + + pub fn keymap_context_layer(&self) -> Context { + let mut context = Context::default(); + context.map.insert( + "vim_mode".to_string(), + match self { + Self::Normal => "normal", + Self::Insert => "insert", + } + .to_string(), + ); + context + } +} + +impl Default for Mode { + fn default() -> Self { + Self::Normal + } +} diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs new file mode 100644 index 0000000000000000000000000000000000000000..232a76a030ae42f60ee96218a19e9b506f313392 --- /dev/null +++ b/crates/vim/src/normal.rs @@ -0,0 +1,66 @@ +use editor::{movement, Bias}; +use gpui::{action, keymap::Binding, MutableAppContext, ViewContext}; +use language::SelectionGoal; +use workspace::Workspace; + +use crate::{Mode, SwitchMode, VimState}; + +action!(InsertBefore); +action!(MoveLeft); +action!(MoveDown); +action!(MoveUp); +action!(MoveRight); + +pub fn init(cx: &mut MutableAppContext) { + let context = Some("Editor && vim_mode == normal"); + cx.add_bindings(vec![ + Binding::new("i", SwitchMode(Mode::Insert), context), + Binding::new("h", MoveLeft, context), + Binding::new("j", MoveDown, context), + Binding::new("k", MoveUp, context), + Binding::new("l", MoveRight, context), + ]); + + cx.add_action(move_left); + cx.add_action(move_down); + cx.add_action(move_up); + cx.add_action(move_right); +} + +fn move_left(_: &mut Workspace, _: &MoveLeft, cx: &mut ViewContext) { + VimState::update_global(cx, |state, cx| { + state.update_active_editor(cx, |editor, cx| { + editor.move_cursors(cx, |map, mut cursor, _| { + *cursor.column_mut() = cursor.column().saturating_sub(1); + (map.clip_point(cursor, Bias::Left), SelectionGoal::None) + }); + }); + }) +} + +fn move_down(_: &mut Workspace, _: &MoveDown, cx: &mut ViewContext) { + VimState::update_global(cx, |state, cx| { + state.update_active_editor(cx, |editor, cx| { + editor.move_cursors(cx, movement::down); + }); + }); +} + +fn move_up(_: &mut Workspace, _: &MoveUp, cx: &mut ViewContext) { + VimState::update_global(cx, |state, cx| { + state.update_active_editor(cx, |editor, cx| { + editor.move_cursors(cx, movement::up); + }); + }); +} + +fn move_right(_: &mut Workspace, _: &MoveRight, cx: &mut ViewContext) { + VimState::update_global(cx, |state, cx| { + state.update_active_editor(cx, |editor, cx| { + editor.move_cursors(cx, |map, mut cursor, _| { + *cursor.column_mut() += 1; + (map.clip_point(cursor, Bias::Right), SelectionGoal::None) + }); + }); + }); +} diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs new file mode 100644 index 0000000000000000000000000000000000000000..26f7e24cf29bc7cbb919fc1c65597b7d826beb1a --- /dev/null +++ b/crates/vim/src/vim.rs @@ -0,0 +1,97 @@ +mod editor_events; +mod insert; +mod mode; +mod normal; +#[cfg(test)] +mod vim_tests; + +use collections::HashMap; +use editor::{CursorShape, Editor}; +use gpui::{action, MutableAppContext, ViewContext, WeakViewHandle}; + +use mode::Mode; +use workspace::{self, Settings, Workspace}; + +action!(SwitchMode, Mode); + +pub fn init(cx: &mut MutableAppContext) { + editor_events::init(cx); + insert::init(cx); + normal::init(cx); + + cx.add_action(|_: &mut Workspace, action: &SwitchMode, cx| { + VimState::update_global(cx, |state, cx| state.switch_mode(action, cx)) + }); + + cx.observe_global::(|settings, cx| { + VimState::update_global(cx, |state, cx| state.set_enabled(settings.vim_mode, cx)) + }) + .detach(); +} + +#[derive(Default)] +pub struct VimState { + editors: HashMap>, + active_editor: Option>, + + enabled: bool, + mode: Mode, +} + +impl VimState { + fn update_global(cx: &mut MutableAppContext, update: F) -> S + where + F: FnOnce(&mut Self, &mut MutableAppContext) -> S, + { + cx.update_default_global(update) + } + + fn update_active_editor( + &self, + cx: &mut MutableAppContext, + update: impl FnOnce(&mut Editor, &mut ViewContext) -> S, + ) -> Option { + self.active_editor + .clone() + .and_then(|ae| ae.upgrade(cx)) + .map(|ae| ae.update(cx, update)) + } + + fn switch_mode(&mut self, SwitchMode(mode): &SwitchMode, cx: &mut MutableAppContext) { + self.mode = *mode; + self.sync_editor_options(cx); + } + + fn set_enabled(&mut self, enabled: bool, cx: &mut MutableAppContext) { + if self.enabled != enabled { + self.enabled = enabled; + if enabled { + self.mode = Mode::Normal; + } + self.sync_editor_options(cx); + } + } + + fn sync_editor_options(&self, cx: &mut MutableAppContext) { + let mode = self.mode; + let cursor_shape = mode.cursor_shape(); + for editor in self.editors.values() { + if let Some(editor) = editor.upgrade(cx) { + editor.update(cx, |editor, cx| { + if self.enabled { + editor.set_cursor_shape(cursor_shape, cx); + editor.set_clip_at_line_ends(cursor_shape == CursorShape::Block, cx); + editor.set_input_enabled(mode == Mode::Insert); + let context_layer = mode.keymap_context_layer(); + editor.set_keymap_context_layer::(context_layer); + } else { + editor.set_cursor_shape(CursorShape::Bar, cx); + editor.set_clip_at_line_ends(false, cx); + editor.set_input_enabled(true); + editor.remove_keymap_context_layer::(); + } + }); + } + } + } +} diff --git a/crates/vim/src/vim_tests.rs b/crates/vim/src/vim_tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..051ff21ce76575e480192024ed4b953ba1ca840e --- /dev/null +++ b/crates/vim/src/vim_tests.rs @@ -0,0 +1,253 @@ +use indoc::indoc; +use std::ops::Deref; + +use editor::{display_map::ToDisplayPoint, DisplayPoint}; +use gpui::{json::json, keymap::Keystroke, ViewHandle}; +use language::{Point, Selection}; +use util::test::marked_text; +use workspace::{WorkspaceHandle, WorkspaceParams}; + +use crate::*; + +#[gpui::test] +async fn test_insert_mode(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestAppContext::new(cx, true, "").await; + cx.simulate_keystroke("i"); + assert_eq!(cx.mode(), Mode::Insert); + cx.simulate_keystrokes(&["T", "e", "s", "t"]); + cx.assert_newest_selection_head("Test|"); + cx.simulate_keystroke("escape"); + assert_eq!(cx.mode(), Mode::Normal); + cx.assert_newest_selection_head("Tes|t"); +} + +#[gpui::test] +async fn test_normal_hjkl(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestAppContext::new(cx, true, "Test\nTestTest\nTest").await; + cx.simulate_keystroke("l"); + cx.assert_newest_selection_head(indoc! {" + T|est + TestTest + Test"}); + cx.simulate_keystroke("h"); + cx.assert_newest_selection_head(indoc! {" + |Test + TestTest + Test"}); + cx.simulate_keystroke("j"); + cx.assert_newest_selection_head(indoc! {" + Test + |TestTest + Test"}); + cx.simulate_keystroke("k"); + cx.assert_newest_selection_head(indoc! {" + |Test + TestTest + Test"}); + cx.simulate_keystroke("j"); + cx.assert_newest_selection_head(indoc! {" + Test + |TestTest + Test"}); + + // When moving left, cursor does not wrap to the previous line + cx.simulate_keystroke("h"); + cx.assert_newest_selection_head(indoc! {" + Test + |TestTest + Test"}); + + // When moving right, cursor does not reach the line end or wrap to the next line + for _ in 0..9 { + cx.simulate_keystroke("l"); + } + cx.assert_newest_selection_head(indoc! {" + Test + TestTes|t + Test"}); + + // Goal column respects the inability to reach the end of the line + cx.simulate_keystroke("k"); + cx.assert_newest_selection_head(indoc! {" + Tes|t + TestTest + Test"}); + cx.simulate_keystroke("j"); + cx.assert_newest_selection_head(indoc! {" + Test + TestTes|t + Test"}); +} + +#[gpui::test] +async fn test_toggle_through_settings(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestAppContext::new(cx, true, "").await; + + cx.simulate_keystroke("i"); + assert_eq!(cx.mode(), Mode::Insert); + + // Editor acts as though vim is disabled + cx.disable_vim(); + cx.simulate_keystrokes(&["h", "j", "k", "l"]); + cx.assert_newest_selection_head("hjkl|"); + + // Enabling dynamically sets vim mode again and restores normal mode + cx.enable_vim(); + assert_eq!(cx.mode(), Mode::Normal); + cx.simulate_keystrokes(&["h", "h", "h", "l"]); + assert_eq!(cx.editor_text(), "hjkl".to_owned()); + cx.assert_newest_selection_head("hj|kl"); + cx.simulate_keystrokes(&["i", "T", "e", "s", "t"]); + cx.assert_newest_selection_head("hjTest|kl"); + + // Disabling and enabling resets to normal mode + assert_eq!(cx.mode(), Mode::Insert); + cx.disable_vim(); + assert_eq!(cx.mode(), Mode::Insert); + cx.enable_vim(); + assert_eq!(cx.mode(), Mode::Normal); +} + +#[gpui::test] +async fn test_initially_disabled(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestAppContext::new(cx, false, "").await; + cx.simulate_keystrokes(&["h", "j", "k", "l"]); + cx.assert_newest_selection_head("hjkl|"); +} + +struct VimTestAppContext<'a> { + cx: &'a mut gpui::TestAppContext, + window_id: usize, + editor: ViewHandle, +} + +impl<'a> VimTestAppContext<'a> { + async fn new( + cx: &'a mut gpui::TestAppContext, + enabled: bool, + initial_editor_text: &str, + ) -> VimTestAppContext<'a> { + cx.update(|cx| { + editor::init(cx); + crate::init(cx); + }); + let params = cx.update(WorkspaceParams::test); + + cx.update(|cx| { + cx.update_global(|settings: &mut Settings, _| { + settings.vim_mode = enabled; + }); + }); + + params + .fs + .as_fake() + .insert_tree( + "/root", + json!({ "dir": { "test.txt": initial_editor_text } }), + ) + .await; + + let (window_id, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx)); + params + .project + .update(cx, |project, cx| { + project.find_or_create_local_worktree("/root", true, cx) + }) + .await + .unwrap(); + cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx)) + .await; + + let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone()); + let item = workspace + .update(cx, |workspace, cx| workspace.open_path(file, cx)) + .await + .expect("Could not open test file"); + + let editor = cx.update(|cx| { + item.act_as::(cx) + .expect("Opened test file wasn't an editor") + }); + editor.update(cx, |_, cx| cx.focus_self()); + + Self { + cx, + window_id, + editor, + } + } + + fn enable_vim(&mut self) { + self.cx.update(|cx| { + cx.update_global(|settings: &mut Settings, _| { + settings.vim_mode = true; + }); + }) + } + + fn disable_vim(&mut self) { + self.cx.update(|cx| { + cx.update_global(|settings: &mut Settings, _| { + settings.vim_mode = false; + }); + }) + } + + fn newest_selection(&mut self) -> Selection { + self.editor.update(self.cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + editor + .newest_selection::(cx) + .map(|point| point.to_display_point(&snapshot.display_snapshot)) + }) + } + + fn mode(&mut self) -> Mode { + self.cx.update(|cx| cx.global::().mode) + } + + fn editor_text(&mut self) -> String { + self.editor + .update(self.cx, |editor, cx| editor.snapshot(cx).text()) + } + + fn simulate_keystroke(&mut self, keystroke_text: &str) { + let keystroke = Keystroke::parse(keystroke_text).unwrap(); + let input = if keystroke.modified() { + None + } else { + Some(keystroke.key.clone()) + }; + self.cx + .dispatch_keystroke(self.window_id, keystroke, input, false); + } + + fn simulate_keystrokes(&mut self, keystroke_texts: &[&str]) { + for keystroke_text in keystroke_texts.into_iter() { + self.simulate_keystroke(keystroke_text); + } + } + + fn assert_newest_selection_head(&mut self, text: &str) { + let (unmarked_text, markers) = marked_text(&text); + assert_eq!( + self.editor_text(), + unmarked_text, + "Unmarked text doesn't match editor text" + ); + let newest_selection = self.newest_selection(); + let expected_head = self.editor.update(self.cx, |editor, cx| { + markers[0].to_display_point(&editor.snapshot(cx)) + }); + assert_eq!(newest_selection.head(), expected_head) + } +} + +impl<'a> Deref for VimTestAppContext<'a> { + type Target = gpui::TestAppContext; + + fn deref(&self) -> &Self::Target { + self.cx + } +} diff --git a/crates/workspace/src/lsp_status.rs b/crates/workspace/src/lsp_status.rs index db160ab06fed41d4974d7fd83721f58acb415a19..a12f81857f1d1b2ed4d44aa01c6283da67f2c8c2 100644 --- a/crates/workspace/src/lsp_status.rs +++ b/crates/workspace/src/lsp_status.rs @@ -1,4 +1,4 @@ -use crate::{ItemViewHandle, Settings, StatusItemView}; +use crate::{ItemHandle, Settings, StatusItemView}; use futures::StreamExt; use gpui::AppContext; use gpui::{ @@ -116,7 +116,7 @@ impl View for LspStatus { } fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - let theme = &cx.app_state::().theme; + let theme = &cx.global::().theme; let mut pending_work = self.pending_language_server_work(cx); if let Some((lang_server_name, progress_token, progress)) = pending_work.next() { @@ -166,7 +166,7 @@ impl View for LspStatus { } else if !self.failed.is_empty() { drop(pending_work); MouseEventHandler::new::(0, cx, |_, cx| { - let theme = &cx.app_state::().theme; + let theme = &cx.global::().theme; Label::new( format!( "Failed to download {} language server{}. Click to dismiss.", @@ -187,5 +187,5 @@ impl View for LspStatus { } impl StatusItemView for LspStatus { - fn set_active_pane_item(&mut self, _: Option<&dyn ItemViewHandle>, _: &mut ViewContext) {} + fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext) {} } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index c54a1b050b341cb75f1768269579f25c43fa64cb..df30d48dbec6010aa85e1d4e9b86461d39fc01a6 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1,5 +1,5 @@ -use super::{ItemViewHandle, SplitDirection}; -use crate::{ItemHandle, ItemView, Settings, WeakItemViewHandle, Workspace}; +use super::{ItemHandle, SplitDirection}; +use crate::{Item, Settings, WeakItemHandle, Workspace}; use collections::{HashMap, VecDeque}; use gpui::{ action, @@ -7,10 +7,10 @@ use gpui::{ geometry::{rect::RectF, vector::vec2f}, keymap::Binding, platform::{CursorStyle, NavigationDirection}, - AnyViewHandle, Entity, MutableAppContext, Quad, RenderContext, Task, View, ViewContext, - ViewHandle, WeakViewHandle, + AnyViewHandle, AppContext, Entity, MutableAppContext, Quad, RenderContext, Task, View, + ViewContext, ViewHandle, WeakViewHandle, }; -use project::ProjectPath; +use project::{ProjectEntryId, ProjectPath}; use std::{ any::{Any, TypeId}, cell::RefCell, @@ -33,7 +33,7 @@ const MAX_NAVIGATION_HISTORY_LEN: usize = 1024; pub fn init(cx: &mut MutableAppContext) { 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| { pane.activate_prev_item(cx); @@ -92,12 +92,13 @@ pub fn init(cx: &mut MutableAppContext) { pub enum Event { Activate, + ActivateItem { local: bool }, Remove, Split(SplitDirection), } pub struct Pane { - item_views: Vec<(usize, Box)>, + items: Vec>, active_item_index: usize, nav_history: Rc>, toolbars: HashMap>, @@ -108,7 +109,7 @@ pub struct Pane { pub trait Toolbar: View { fn active_item_changed( &mut self, - item: Option>, + item: Option>, cx: &mut ViewContext, ) -> bool; fn on_dismiss(&mut self, cx: &mut ViewContext); @@ -117,7 +118,7 @@ pub trait Toolbar: View { trait ToolbarHandle { fn active_item_changed( &self, - item: Option>, + item: Option>, cx: &mut MutableAppContext, ) -> bool; fn on_dismiss(&self, cx: &mut MutableAppContext); @@ -126,7 +127,7 @@ trait ToolbarHandle { pub struct ItemNavHistory { history: Rc>, - item_view: Rc, + item: Rc, } #[derive(Default)] @@ -152,14 +153,14 @@ impl Default for NavigationMode { } pub struct NavigationEntry { - pub item_view: Rc, + pub item: Rc, pub data: Option>, } impl Pane { pub fn new() -> Self { Self { - item_views: Vec::new(), + items: Vec::new(), active_item_index: 0, nav_history: Default::default(), toolbars: Default::default(), @@ -211,40 +212,47 @@ impl Pane { workspace.activate_pane(pane.clone(), cx); let to_load = pane.update(cx, |pane, cx| { - // Retrieve the weak item handle from the history. - let entry = pane.nav_history.borrow_mut().pop(mode)?; - - // If the item is still present in this pane, then activate it. - if let Some(index) = entry - .item_view - .upgrade(cx) - .and_then(|v| pane.index_for_item_view(v.as_ref())) - { - if let Some(item_view) = pane.active_item() { - pane.nav_history.borrow_mut().set_mode(mode); - item_view.deactivated(cx); - pane.nav_history - .borrow_mut() - .set_mode(NavigationMode::Normal); - } + loop { + // Retrieve the weak item handle from the history. + let entry = pane.nav_history.borrow_mut().pop(mode)?; + + // If the item is still present in this pane, then activate it. + if let Some(index) = entry + .item + .upgrade(cx) + .and_then(|v| pane.index_for_item(v.as_ref())) + { + if let Some(item) = pane.active_item() { + pane.nav_history.borrow_mut().set_mode(mode); + item.deactivated(cx); + pane.nav_history + .borrow_mut() + .set_mode(NavigationMode::Normal); + } - pane.active_item_index = index; - pane.focus_active_item(cx); - if let Some(data) = entry.data { - pane.active_item()?.navigate(data, cx); + let prev_active_index = mem::replace(&mut pane.active_item_index, index); + pane.focus_active_item(cx); + let mut navigated = prev_active_index != pane.active_item_index; + if let Some(data) = entry.data { + navigated |= pane.active_item()?.navigate(data, cx); + } + + if navigated { + cx.notify(); + break None; + } + } + // If the item is no longer present in this pane, then retrieve its + // project path in order to reopen it. + else { + break pane + .nav_history + .borrow_mut() + .paths_by_item + .get(&entry.item.id()) + .cloned() + .map(|project_path| (project_path, entry)); } - cx.notify(); - None - } - // If the item is no longer present in this pane, then retrieve its - // project path in order to reopen it. - else { - pane.nav_history - .borrow_mut() - .paths_by_item - .get(&entry.item_view.id()) - .cloned() - .map(|project_path| (project_path, entry)) } }); @@ -253,18 +261,27 @@ impl Pane { let pane = pane.downgrade(); let task = workspace.load_path(project_path, cx); cx.spawn(|workspace, mut cx| async move { - let item = task.await; + let task = task.await; if let Some(pane) = pane.upgrade(&cx) { - if let Some(item) = item.log_err() { - workspace.update(&mut cx, |workspace, cx| { - pane.update(cx, |p, _| p.nav_history.borrow_mut().set_mode(mode)); - let item_view = workspace.open_item_in_pane(item, &pane, cx); - pane.update(cx, |p, _| { - p.nav_history.borrow_mut().set_mode(NavigationMode::Normal) - }); - + if let Some((project_entry_id, build_item)) = task.log_err() { + pane.update(&mut cx, |pane, _| { + pane.nav_history.borrow_mut().set_mode(mode); + }); + 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 + .borrow_mut() + .set_mode(NavigationMode::Normal); if let Some(data) = entry.data { - item_view.navigate(data, cx); + item.navigate(data, cx); } }); } else { @@ -281,80 +298,115 @@ impl Pane { } } - pub fn open_item( - &mut self, - item_handle: T, - workspace: &Workspace, - cx: &mut ViewContext, - ) -> Box - where - T: 'static + ItemHandle, - { - for (ix, (item_id, item_view)) in self.item_views.iter().enumerate() { - if *item_id == item_handle.id() { - let item_view = item_view.boxed_clone(); - self.activate_item(ix, cx); - return item_view; + pub(crate) fn open_item( + workspace: &mut Workspace, + pane: ViewHandle, + project_entry_id: ProjectEntryId, + cx: &mut ViewContext, + build_item: impl FnOnce(&mut MutableAppContext) -> Box, + ) -> Box { + let existing_item = pane.update(cx, |pane, cx| { + for (ix, item) in pane.items.iter().enumerate() { + if item.project_entry_id(cx) == Some(project_entry_id) { + let item = item.boxed_clone(); + pane.activate_item(ix, true, cx); + return Some(item); + } } + None + }); + if let Some(existing_item) = existing_item { + existing_item + } else { + let item = build_item(cx); + Self::add_item(workspace, pane, item.boxed_clone(), true, cx); + item } - - let item_view = - item_handle.add_view(cx.window_id(), workspace, self.nav_history.clone(), cx); - self.add_item_view(item_view.boxed_clone(), cx); - item_view } - pub fn add_item_view( - &mut self, - mut item_view: Box, - cx: &mut ViewContext, + pub(crate) fn add_item( + workspace: &mut Workspace, + pane: ViewHandle, + item: Box, + local: bool, + cx: &mut ViewContext, ) { - item_view.added_to_pane(cx); - let item_idx = cmp::min(self.active_item_index + 1, self.item_views.len()); - self.item_views - .insert(item_idx, (item_view.item(cx).id(), item_view)); - self.activate_item(item_idx, cx); - cx.notify(); + // Prevent adding the same item to the pane more than once. + if let Some(item_ix) = pane.read(cx).items.iter().position(|i| i.id() == item.id()) { + pane.update(cx, |pane, cx| pane.activate_item(item_ix, local, cx)); + return; + } + + 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(); + }); + } + + pub fn items(&self) -> impl Iterator> { + self.items.iter() } - pub fn contains_item(&self, item: &dyn ItemHandle) -> bool { - let item_id = item.id(); - self.item_views + pub fn items_of_type<'a, T: View>(&'a self) -> impl 'a + Iterator> { + self.items .iter() - .any(|(existing_item_id, _)| *existing_item_id == item_id) + .filter_map(|item| item.to_any().downcast()) } - pub fn item_views(&self) -> impl Iterator> { - self.item_views.iter().map(|(_, view)| view) + pub fn active_item(&self) -> Option> { + self.items.get(self.active_item_index).cloned() } - pub fn active_item(&self) -> Option> { - self.item_views - .get(self.active_item_index) - .map(|(_, view)| view.clone()) + pub fn project_entry_id_for_item( + &self, + item: &dyn ItemHandle, + cx: &AppContext, + ) -> Option { + self.items.iter().find_map(|existing| { + if existing.id() == item.id() { + existing.project_entry_id(cx) + } else { + None + } + }) } - pub fn index_for_item_view(&self, item_view: &dyn ItemViewHandle) -> Option { - self.item_views - .iter() - .position(|(_, i)| i.id() == item_view.id()) + pub fn item_for_entry( + &self, + entry_id: ProjectEntryId, + cx: &AppContext, + ) -> Option> { + self.items.iter().find_map(|item| { + if item.project_entry_id(cx) == Some(entry_id) { + Some(item.boxed_clone()) + } else { + None + } + }) } pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option { - self.item_views.iter().position(|(id, _)| *id == item.id()) + self.items.iter().position(|i| i.id() == item.id()) } - pub fn activate_item(&mut self, index: usize, cx: &mut ViewContext) { - if index < self.item_views.len() { + pub fn activate_item(&mut self, index: usize, local: bool, cx: &mut ViewContext) { + if index < self.items.len() { let prev_active_item_ix = mem::replace(&mut self.active_item_index, index); if prev_active_item_ix != self.active_item_index - && prev_active_item_ix < self.item_views.len() + && prev_active_item_ix < self.items.len() { - self.item_views[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.focus_active_item(cx); - self.activate(cx); + if local { + self.focus_active_item(cx); + self.activate(cx); + } cx.notify(); } } @@ -363,31 +415,31 @@ impl Pane { let mut index = self.active_item_index; if index > 0 { index -= 1; - } else if self.item_views.len() > 0 { - index = self.item_views.len() - 1; + } else if self.items.len() > 0 { + 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) { let mut index = self.active_item_index; - if index + 1 < self.item_views.len() { + if index + 1 < self.items.len() { index += 1; } else { index = 0; } - self.activate_item(index, cx); + self.activate_item(index, true, cx); } pub fn close_active_item(&mut self, cx: &mut ViewContext) { - if !self.item_views.is_empty() { - self.close_item(self.item_views[self.active_item_index].1.id(), cx) + if !self.items.is_empty() { + self.close_item(self.items[self.active_item_index].id(), cx) } } pub fn close_inactive_items(&mut self, cx: &mut ViewContext) { - if !self.item_views.is_empty() { - let active_item_id = self.item_views[self.active_item_index].1.id(); + if !self.items.is_empty() { + let active_item_id = self.items[self.active_item_index].id(); self.close_items(cx, |id| id != active_item_id); } } @@ -403,10 +455,10 @@ impl Pane { ) { let mut item_ix = 0; let mut new_active_item_index = self.active_item_index; - self.item_views.retain(|(_, item_view)| { - if should_close(item_view.id()) { + self.items.retain(|item| { + if should_close(item.id()) { if item_ix == self.active_item_index { - item_view.deactivated(cx); + item.deactivated(cx); } if item_ix < self.active_item_index { @@ -414,10 +466,10 @@ impl Pane { } let mut nav_history = self.nav_history.borrow_mut(); - if let Some(path) = item_view.project_path(cx) { - nav_history.paths_by_item.insert(item_view.id(), path); + if let Some(path) = item.project_path(cx) { + nav_history.paths_by_item.insert(item.id(), path); } else { - nav_history.paths_by_item.remove(&item_view.id()); + nav_history.paths_by_item.remove(&item.id()); } item_ix += 1; @@ -428,10 +480,10 @@ impl Pane { } }); - if self.item_views.is_empty() { + if self.items.is_empty() { cx.emit(Event::Remove); } else { - self.active_item_index = cmp::min(new_active_item_index, self.item_views.len() - 1); + self.active_item_index = cmp::min(new_active_item_index, self.items.len() - 1); self.focus_active_item(cx); self.activate(cx); } @@ -440,7 +492,7 @@ impl Pane { cx.notify(); } - fn focus_active_item(&mut self, cx: &mut ViewContext) { + pub fn focus_active_item(&mut self, cx: &mut ViewContext) { if let Some(active_item) = self.active_item() { cx.focus(active_item); } @@ -500,9 +552,9 @@ impl Pane { } fn update_active_toolbar(&mut self, cx: &mut ViewContext) { - let active_item = self.item_views.get(self.active_item_index); + let active_item = self.items.get(self.active_item_index); 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 { self.active_toolbar_visible = visible; } @@ -510,12 +562,12 @@ impl Pane { } fn render_tabs(&self, cx: &mut RenderContext) -> ElementBox { - let theme = cx.app_state::().theme.clone(); + let theme = cx.global::().theme.clone(); enum Tabs {} let tabs = MouseEventHandler::new::(0, cx, |mouse_state, cx| { let mut row = Flex::row(); - for (ix, (_, item_view)) in self.item_views.iter().enumerate() { + for (ix, item) in self.items.iter().enumerate() { let is_active = ix == self.active_item_index; row.add_child({ @@ -524,7 +576,7 @@ impl Pane { } else { theme.workspace.tab.clone() }; - let title = item_view.tab_content(&tab_style, cx); + let title = item.tab_content(&tab_style, cx); let mut style = if is_active { theme.workspace.active_tab.clone() @@ -541,9 +593,9 @@ impl Pane { .with_child( Align::new({ let diameter = 7.0; - let icon_color = if item_view.has_conflict(cx) { + let icon_color = if item.has_conflict(cx) { Some(style.icon_conflict) - } else if item_view.is_dirty(cx) { + } else if item.is_dirty(cx) { Some(style.icon_dirty) } else { None @@ -587,7 +639,7 @@ impl Pane { .with_child( Align::new( ConstrainedBox::new(if mouse_state.hovered { - let item_id = item_view.id(); + let item_id = item.id(); enum TabCloseButton {} let icon = Svg::new("icons/x.svg"); MouseEventHandler::new::( @@ -691,7 +743,7 @@ impl View for Pane { impl ToolbarHandle for ViewHandle { fn active_item_changed( &self, - item: Option>, + item: Option>, cx: &mut MutableAppContext, ) -> bool { self.update(cx, |this, cx| this.active_item_changed(item, cx)) @@ -707,10 +759,10 @@ impl ToolbarHandle for ViewHandle { } impl ItemNavHistory { - pub fn new(history: Rc>, item_view: &ViewHandle) -> Self { + pub fn new(history: Rc>, item: &ViewHandle) -> Self { Self { history, - item_view: Rc::new(item_view.downgrade()), + item: Rc::new(item.downgrade()), } } @@ -719,7 +771,7 @@ impl ItemNavHistory { } pub fn push(&self, data: Option) { - self.history.borrow_mut().push(data, self.item_view.clone()); + self.history.borrow_mut().push(data, self.item.clone()); } } @@ -752,11 +804,7 @@ impl NavHistory { self.mode = mode; } - pub fn push( - &mut self, - data: Option, - item_view: Rc, - ) { + pub fn push(&mut self, data: Option, item: Rc) { match self.mode { NavigationMode::Disabled => {} NavigationMode::Normal => { @@ -764,7 +812,7 @@ impl NavHistory { self.backward_stack.pop_front(); } self.backward_stack.push_back(NavigationEntry { - item_view, + item, data: data.map(|data| Box::new(data) as Box), }); self.forward_stack.clear(); @@ -774,7 +822,7 @@ impl NavHistory { self.forward_stack.pop_front(); } self.forward_stack.push_back(NavigationEntry { - item_view, + item, data: data.map(|data| Box::new(data) as Box), }); } @@ -783,7 +831,7 @@ impl NavHistory { self.backward_stack.pop_front(); } self.backward_stack.push_back(NavigationEntry { - item_view, + item, data: data.map(|data| Box::new(data) as Box), }); } diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 2b56a023fc2a7dbc423b177503c7117e6c02b044..afffec507452ee00bcf4d185abb92ef09a92a9d7 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -1,9 +1,11 @@ +use crate::{FollowerStatesByLeader, Pane}; 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 crate::Pane; - #[derive(Clone, Debug, Eq, PartialEq)] pub struct PaneGroup { root: Member, @@ -47,8 +49,19 @@ impl PaneGroup { } } - pub fn render<'a>(&self, theme: &Theme) -> ElementBox { - self.root.render(theme) + pub(crate) fn render<'a>( + &self, + theme: &Theme, + follower_states: &FollowerStatesByLeader, + collaborators: &HashMap, + ) -> ElementBox { + self.root.render(theme, follower_states, collaborators) + } + + pub(crate) fn panes(&self) -> Vec<&ViewHandle> { + let mut panes = Vec::new(); + self.root.collect_panes(&mut panes); + panes } } @@ -80,10 +93,50 @@ impl Member { Member::Axis(PaneAxis { axis, members }) } - pub fn render(&self, theme: &Theme) -> ElementBox { + pub fn render( + &self, + theme: &Theme, + follower_states: &FollowerStatesByLeader, + collaborators: &HashMap, + ) -> ElementBox { match self { - Member::Pane(pane) => ChildView::new(pane).boxed(), - Member::Axis(axis) => axis.render(theme), + Member::Pane(pane) => { + 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>) { + 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, + ) -> ElementBox { let last_member_ix = self.members.len() - 1; Flex::new(self.axis) .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 { let mut border = theme.workspace.pane_divider; border.left = false; diff --git a/crates/workspace/src/settings.rs b/crates/workspace/src/settings.rs index e97f7f8920f8f126a41485b612820d3b14e57e88..5ccf8056e674b98953a340645677606bae80cfaf 100644 --- a/crates/workspace/src/settings.rs +++ b/crates/workspace/src/settings.rs @@ -17,6 +17,7 @@ use util::ResultExt; pub struct Settings { pub buffer_font_family: FamilyId, pub buffer_font_size: f32, + pub vim_mode: bool, pub tab_size: usize, pub soft_wrap: SoftWrap, pub preferred_line_length: u32, @@ -48,6 +49,8 @@ struct SettingsFileContent { buffer_font_family: Option, #[serde(default)] buffer_font_size: Option, + #[serde(default)] + vim_mode: Option, #[serde(flatten)] editor: LanguageOverride, #[serde(default)] @@ -130,6 +133,7 @@ impl Settings { Ok(Self { buffer_font_family: font_cache.load_family(&[buffer_font_family])?, buffer_font_size: 15., + vim_mode: false, tab_size: 4, soft_wrap: SoftWrap::None, preferred_line_length: 80, @@ -174,6 +178,7 @@ impl Settings { Settings { buffer_font_family: cx.font_cache().load_family(&["Monaco"]).unwrap(), buffer_font_size: 14., + vim_mode: false, tab_size: 4, soft_wrap: SoftWrap::None, preferred_line_length: 80, @@ -200,6 +205,7 @@ impl Settings { } merge(&mut self.buffer_font_size, data.buffer_font_size); + merge(&mut self.vim_mode, data.vim_mode); merge(&mut self.soft_wrap, data.editor.soft_wrap); merge(&mut self.tab_size, data.editor.tab_size); merge( diff --git a/crates/workspace/src/status_bar.rs b/crates/workspace/src/status_bar.rs index c387fbe07064ccc136e839f4d8c07a57b1962d84..4d00591787c9cf88e20ab9a60c705a73250421ee 100644 --- a/crates/workspace/src/status_bar.rs +++ b/crates/workspace/src/status_bar.rs @@ -1,4 +1,4 @@ -use crate::{ItemViewHandle, Pane, Settings}; +use crate::{ItemHandle, Pane, Settings}; use gpui::{ elements::*, AnyViewHandle, ElementBox, Entity, MutableAppContext, RenderContext, Subscription, View, ViewContext, ViewHandle, @@ -7,7 +7,7 @@ use gpui::{ pub trait StatusItemView: View { fn set_active_pane_item( &mut self, - active_pane_item: Option<&dyn crate::ItemViewHandle>, + active_pane_item: Option<&dyn crate::ItemHandle>, cx: &mut ViewContext, ); } @@ -16,7 +16,7 @@ trait StatusItemViewHandle { fn to_any(&self) -> AnyViewHandle; fn set_active_pane_item( &self, - active_pane_item: Option<&dyn ItemViewHandle>, + active_pane_item: Option<&dyn ItemHandle>, cx: &mut MutableAppContext, ); } @@ -38,7 +38,7 @@ impl View for StatusBar { } fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - let theme = &cx.app_state::().theme.workspace.status_bar; + let theme = &cx.global::().theme.workspace.status_bar; Flex::row() .with_children(self.left_items.iter().map(|i| { ChildView::new(i.as_ref()) @@ -114,7 +114,7 @@ impl StatusItemViewHandle for ViewHandle { fn set_active_pane_item( &self, - active_pane_item: Option<&dyn ItemViewHandle>, + active_pane_item: Option<&dyn ItemHandle>, cx: &mut MutableAppContext, ) { self.update(cx, |this, cx| { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 6bff12a577bab115c96bf8554cbf5e2a3ca59d44..17b0c4b51859b9f42acb2a513be6e3c2b94114ae 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -6,10 +6,12 @@ pub mod settings; pub mod sidebar; mod status_bar; -use anyhow::{anyhow, Result}; -use client::{Authenticate, ChannelList, Client, User, UserStore}; +use anyhow::{anyhow, Context, Result}; +use client::{ + proto, Authenticate, ChannelList, Client, PeerId, Subscription, TypedEnvelope, User, UserStore, +}; use clock::ReplicaId; -use collections::BTreeMap; +use collections::{hash_map, HashMap, HashSet}; use gpui::{ action, color::Color, @@ -18,16 +20,16 @@ use gpui::{ json::{self, to_string_pretty, ToJson}, keymap::Binding, platform::{CursorStyle, WindowOptions}, - AnyModelHandle, AnyViewHandle, AppContext, ClipboardItem, Entity, ImageData, ModelContext, - ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View, - ViewContext, ViewHandle, WeakModelHandle, WeakViewHandle, + AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Border, ClipboardItem, Entity, + ImageData, ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, + View, ViewContext, ViewHandle, WeakViewHandle, }; use language::LanguageRegistry; use log::error; pub use pane::*; pub use pane_group::*; use postage::prelude::Stream; -use project::{fs, Fs, Project, ProjectPath, Worktree}; +use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, Worktree}; pub use settings::Settings; use sidebar::{Side, Sidebar, SidebarItemId, ToggleSidebarItem, ToggleSidebarItemFocus}; use status_bar::StatusBar; @@ -35,24 +37,51 @@ pub use status_bar::StatusItemView; use std::{ any::{Any, TypeId}, cell::RefCell, - cmp::Reverse, + fmt, future::Future, - hash::{Hash, Hasher}, path::{Path, PathBuf}, rc::Rc, - sync::Arc, + sync::{ + atomic::{AtomicBool, Ordering::SeqCst}, + Arc, + }, }; use theme::{Theme, ThemeRegistry}; +use util::ResultExt; + +type ProjectItemBuilders = HashMap< + TypeId, + fn(usize, ModelHandle, AnyModelHandle, &mut MutableAppContext) -> Box, +>; + +type FollowableItemBuilder = fn( + ViewHandle, + ModelHandle, + &mut Option, + &mut MutableAppContext, +) -> Option>>>; +type FollowableItemBuilders = HashMap< + TypeId, + ( + FollowableItemBuilder, + fn(AnyViewHandle) -> Box, + ), +>; action!(Open, Arc); action!(OpenNew, Arc); action!(OpenPaths, OpenParams); action!(ToggleShare); +action!(ToggleFollow, PeerId); +action!(FollowNextCollaborator); +action!(Unfollow); action!(JoinProject, JoinProjectParams); action!(Save); action!(DebugElements); +action!(ActivatePreviousPane); +action!(ActivateNextPane); -pub fn init(cx: &mut MutableAppContext) { +pub fn init(client: &Arc, cx: &mut MutableAppContext) { pane::init(cx); menu::init(cx); @@ -68,6 +97,14 @@ pub fn init(cx: &mut MutableAppContext) { }); cx.add_action(Workspace::toggle_share); + cx.add_async_action(Workspace::toggle_follow); + cx.add_async_action(Workspace::follow_next_collaborator); + cx.add_action( + |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext| { + let pane = workspace.active_pane().clone(); + workspace.unfollow(&pane, cx); + }, + ); cx.add_action( |workspace: &mut Workspace, _: &Save, cx: &mut ViewContext| { workspace.save_active_item(cx).detach_and_log_err(cx); @@ -76,9 +113,18 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(Workspace::debug_elements); cx.add_action(Workspace::toggle_sidebar_item); cx.add_action(Workspace::toggle_sidebar_item_focus); + cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| { + workspace.activate_previous_pane(cx) + }); + cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| { + workspace.activate_next_pane(cx) + }); cx.add_bindings(vec![ + Binding::new("ctrl-alt-cmd-f", FollowNextCollaborator, None), Binding::new("cmd-s", Save, None), Binding::new("cmd-alt-i", DebugElements, None), + Binding::new("cmd-k cmd-left", ActivatePreviousPane, None), + Binding::new("cmd-k cmd-right", ActivateNextPane, None), Binding::new( "cmd-shift-!", ToggleSidebarItem(SidebarItemId { @@ -96,6 +142,36 @@ pub fn init(cx: &mut MutableAppContext) { None, ), ]); + + client.add_view_request_handler(Workspace::handle_follow); + client.add_view_message_handler(Workspace::handle_unfollow); + client.add_view_message_handler(Workspace::handle_update_followers); +} + +pub fn register_project_item(cx: &mut MutableAppContext) { + cx.update_default_global(|builders: &mut ProjectItemBuilders, _| { + builders.insert(TypeId::of::(), |window_id, project, model, cx| { + let item = model.downcast::().unwrap(); + Box::new(cx.add_view(window_id, |cx| I::for_project_item(project, item, cx))) + }); + }); +} + +pub fn register_followable_item(cx: &mut MutableAppContext) { + cx.update_default_global(|builders: &mut FollowableItemBuilders, _| { + builders.insert( + TypeId::of::(), + ( + |pane, project, state, cx| { + I::from_state_proto(pane, project, state, cx).map(|task| { + cx.foreground() + .spawn(async move { Ok(Box::new(task.await?) as Box<_>) }) + }) + }, + |this| Box::new(this.downcast::().unwrap()), + ), + ); + }); } pub struct AppState { @@ -105,7 +181,6 @@ pub struct AppState { pub user_store: ModelHandle, pub fs: Arc, pub channel_list: ModelHandle, - pub path_openers: Arc<[Box]>, pub build_window_options: &'static dyn Fn() -> WindowOptions<'static>, pub build_workspace: &'static dyn Fn( ModelHandle, @@ -126,35 +201,16 @@ pub struct JoinProjectParams { pub app_state: Arc, } -pub trait PathOpener { - fn open( - &self, - project: &mut Project, - path: ProjectPath, - cx: &mut ModelContext, - ) -> Option>>>; -} - -pub trait Item: Entity + Sized { - type View: ItemView; - - fn build_view( - handle: ModelHandle, - workspace: &Workspace, - nav_history: ItemNavHistory, - cx: &mut ViewContext, - ) -> Self::View; - - fn project_path(&self) -> Option; -} - -pub trait ItemView: View { +pub trait Item: View { fn deactivated(&mut self, _: &mut ViewContext) {} - fn navigate(&mut self, _: Box, _: &mut ViewContext) {} - fn item(&self, cx: &AppContext) -> Box; + fn navigate(&mut self, _: Box, _: &mut ViewContext) -> bool { + false + } fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox; fn project_path(&self, cx: &AppContext) -> Option; - fn clone_on_split(&self, _: ItemNavHistory, _: &mut ViewContext) -> Option + fn project_entry_id(&self, cx: &AppContext) -> Option; + fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext); + fn clone_on_split(&self, _: &mut ViewContext) -> Option where Self: Sized, { @@ -202,39 +258,113 @@ pub trait ItemView: View { } } -pub trait ItemHandle: Send + Sync { - fn id(&self) -> usize; - fn add_view( +pub trait ProjectItem: Item { + type Item: project::Item; + + fn for_project_item( + project: ModelHandle, + item: ModelHandle, + cx: &mut ViewContext, + ) -> Self; +} + +pub trait FollowableItem: Item { + fn to_state_proto(&self, cx: &AppContext) -> Option; + fn from_state_proto( + pane: ViewHandle, + project: ModelHandle, + state: &mut Option, + cx: &mut MutableAppContext, + ) -> Option>>>; + fn add_event_to_update_proto( &self, - window_id: usize, - workspace: &Workspace, - nav_history: Rc>, + event: &Self::Event, + update: &mut Option, + cx: &AppContext, + ) -> bool; + fn apply_update_proto( + &mut self, + message: proto::update_view::Variant, + cx: &mut ViewContext, + ) -> Result<()>; + + fn set_leader_replica_id(&mut self, leader_replica_id: Option, cx: &mut ViewContext); + fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool; +} + +pub trait FollowableItemHandle: ItemHandle { + fn set_leader_replica_id(&self, leader_replica_id: Option, cx: &mut MutableAppContext); + fn to_state_proto(&self, cx: &AppContext) -> Option; + fn add_event_to_update_proto( + &self, + event: &dyn Any, + update: &mut Option, + cx: &AppContext, + ) -> bool; + fn apply_update_proto( + &self, + message: proto::update_view::Variant, cx: &mut MutableAppContext, - ) -> Box; - fn boxed_clone(&self) -> Box; - fn downgrade(&self) -> Box; - fn to_any(&self) -> AnyModelHandle; - fn project_path(&self, cx: &AppContext) -> Option; + ) -> Result<()>; + fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool; } -pub trait WeakItemHandle { - fn id(&self) -> usize; - fn upgrade(&self, cx: &AppContext) -> Option>; +impl FollowableItemHandle for ViewHandle { + fn set_leader_replica_id(&self, leader_replica_id: Option, cx: &mut MutableAppContext) { + self.update(cx, |this, cx| { + this.set_leader_replica_id(leader_replica_id, cx) + }) + } + + fn to_state_proto(&self, cx: &AppContext) -> Option { + self.read(cx).to_state_proto(cx) + } + + fn add_event_to_update_proto( + &self, + event: &dyn Any, + update: &mut Option, + cx: &AppContext, + ) -> bool { + if let Some(event) = event.downcast_ref() { + self.read(cx).add_event_to_update_proto(event, update, cx) + } else { + false + } + } + + fn apply_update_proto( + &self, + message: proto::update_view::Variant, + cx: &mut MutableAppContext, + ) -> Result<()> { + self.update(cx, |this, cx| this.apply_update_proto(message, cx)) + } + + fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool { + if let Some(event) = event.downcast_ref() { + T::should_unfollow_on_event(event, cx) + } else { + false + } + } } -pub trait ItemViewHandle: 'static { - fn item(&self, cx: &AppContext) -> Box; +pub trait ItemHandle: 'static + fmt::Debug { fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox; fn project_path(&self, cx: &AppContext) -> Option; - fn boxed_clone(&self) -> Box; - fn clone_on_split( + fn project_entry_id(&self, cx: &AppContext) -> Option; + fn boxed_clone(&self) -> Box; + fn set_nav_history(&self, nav_history: Rc>, cx: &mut MutableAppContext); + fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option>; + fn added_to_pane( &self, - nav_history: Rc>, - cx: &mut MutableAppContext, - ) -> Option>; - fn added_to_pane(&mut self, cx: &mut ViewContext); + workspace: &mut Workspace, + pane: ViewHandle, + cx: &mut ViewContext, + ); fn deactivated(&self, cx: &mut MutableAppContext); - fn navigate(&self, data: Box, cx: &mut MutableAppContext); + fn navigate(&self, data: Box, cx: &mut MutableAppContext) -> bool; fn id(&self) -> usize; fn to_any(&self) -> AnyViewHandle; fn is_dirty(&self, cx: &AppContext) -> bool; @@ -249,105 +379,15 @@ pub trait ItemViewHandle: 'static { cx: &mut MutableAppContext, ) -> Task>; fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option; + fn to_followable_item_handle(&self, cx: &AppContext) -> Option>; } -pub trait WeakItemViewHandle { +pub trait WeakItemHandle { fn id(&self) -> usize; - fn upgrade(&self, cx: &AppContext) -> Option>; -} - -impl ItemHandle for ModelHandle { - fn id(&self) -> usize { - self.id() - } - - fn add_view( - &self, - window_id: usize, - workspace: &Workspace, - nav_history: Rc>, - cx: &mut MutableAppContext, - ) -> Box { - Box::new(cx.add_view(window_id, |cx| { - let nav_history = ItemNavHistory::new(nav_history, &cx.handle()); - T::build_view(self.clone(), workspace, nav_history, cx) - })) - } - - fn boxed_clone(&self) -> Box { - Box::new(self.clone()) - } - - fn downgrade(&self) -> Box { - Box::new(self.downgrade()) - } - - fn to_any(&self) -> AnyModelHandle { - self.clone().into() - } - - fn project_path(&self, cx: &AppContext) -> Option { - self.read(cx).project_path() - } -} - -impl ItemHandle for Box { - fn id(&self) -> usize { - ItemHandle::id(self.as_ref()) - } - - fn add_view( - &self, - window_id: usize, - workspace: &Workspace, - nav_history: Rc>, - cx: &mut MutableAppContext, - ) -> Box { - ItemHandle::add_view(self.as_ref(), window_id, workspace, nav_history, cx) - } - - fn boxed_clone(&self) -> Box { - self.as_ref().boxed_clone() - } - - fn downgrade(&self) -> Box { - self.as_ref().downgrade() - } - - fn to_any(&self) -> AnyModelHandle { - self.as_ref().to_any() - } - - fn project_path(&self, cx: &AppContext) -> Option { - self.as_ref().project_path(cx) - } -} - -impl WeakItemHandle for WeakModelHandle { - fn id(&self) -> usize { - WeakModelHandle::id(self) - } - - fn upgrade(&self, cx: &AppContext) -> Option> { - WeakModelHandle::::upgrade(self, cx).map(|i| Box::new(i) as Box) - } -} - -impl Hash for Box { - fn hash(&self, state: &mut H) { - self.id().hash(state); - } -} - -impl PartialEq for Box { - fn eq(&self, other: &Self) -> bool { - self.id() == other.id() - } + fn upgrade(&self, cx: &AppContext) -> Option>; } -impl Eq for Box {} - -impl dyn ItemViewHandle { +impl dyn ItemHandle { pub fn downcast(&self) -> Option> { self.to_any().downcast() } @@ -358,11 +398,7 @@ impl dyn ItemViewHandle { } } -impl ItemViewHandle for ViewHandle { - fn item(&self, cx: &AppContext) -> Box { - self.read(cx).item(cx) - } - +impl ItemHandle for ViewHandle { fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox { self.read(cx).tab_content(style, cx) } @@ -371,37 +407,102 @@ impl ItemViewHandle for ViewHandle { self.read(cx).project_path(cx) } - fn boxed_clone(&self) -> Box { + fn project_entry_id(&self, cx: &AppContext) -> Option { + self.read(cx).project_entry_id(cx) + } + + fn boxed_clone(&self) -> Box { Box::new(self.clone()) } - fn clone_on_split( - &self, - nav_history: Rc>, - cx: &mut MutableAppContext, - ) -> Option> { + fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option> { self.update(cx, |item, cx| { - cx.add_option_view(|cx| { - item.clone_on_split(ItemNavHistory::new(nav_history, &cx.handle()), cx) - }) + cx.add_option_view(|cx| item.clone_on_split(cx)) }) - .map(|handle| Box::new(handle) as Box) + .map(|handle| Box::new(handle) as Box) } - fn added_to_pane(&mut self, cx: &mut ViewContext) { - cx.subscribe(self, |pane, item, event, cx| { + fn set_nav_history(&self, nav_history: Rc>, cx: &mut MutableAppContext) { + self.update(cx, |item, cx| { + item.set_nav_history(ItemNavHistory::new(nav_history, &cx.handle()), cx); + }) + } + + fn added_to_pane( + &self, + workspace: &mut Workspace, + pane: ViewHandle, + cx: &mut ViewContext, + ) { + if let Some(followed_item) = self.to_followable_item_handle(cx) { + if let Some(message) = followed_item.to_state_proto(cx) { + workspace.update_followers( + proto::update_followers::Variant::CreateView(proto::View { + id: followed_item.id() as u64, + variant: Some(message), + leader_id: workspace.leader_for_pane(&pane).map(|id| id.0), + }), + cx, + ); + } + } + + let pending_update = Rc::new(RefCell::new(None)); + let pending_update_scheduled = Rc::new(AtomicBool::new(false)); + let pane = pane.downgrade(); + cx.subscribe(self, move |workspace, item, event, cx| { + let pane = if let Some(pane) = pane.upgrade(cx) { + pane + } else { + log::error!("unexpected item event after pane was dropped"); + return; + }; + + if let Some(item) = item.to_followable_item_handle(cx) { + let leader_id = workspace.leader_for_pane(&pane); + + if leader_id.is_some() && item.should_unfollow_on_event(event, cx) { + workspace.unfollow(&pane, cx); + } + + if item.add_event_to_update_proto(event, &mut *pending_update.borrow_mut(), cx) + && !pending_update_scheduled.load(SeqCst) + { + pending_update_scheduled.store(true, SeqCst); + cx.after_window_update({ + let pending_update = pending_update.clone(); + let pending_update_scheduled = pending_update_scheduled.clone(); + move |this, cx| { + pending_update_scheduled.store(false, SeqCst); + this.update_followers( + proto::update_followers::Variant::UpdateView(proto::UpdateView { + id: item.id() as u64, + variant: pending_update.borrow_mut().take(), + leader_id: leader_id.map(|id| id.0), + }), + cx, + ); + } + }); + } + } + if T::should_close_item_on_event(event) { - pane.close_item(item.id(), cx); + pane.update(cx, |pane, cx| pane.close_item(item.id(), cx)); return; } + if T::should_activate_item_on_event(event) { - if let Some(ix) = pane.index_for_item_view(&item) { - pane.activate_item(ix, cx); - pane.activate(cx); - } + pane.update(cx, |pane, cx| { + if let Some(ix) = pane.index_for_item(&item) { + pane.activate_item(ix, true, cx); + pane.activate(cx); + } + }); } + if T::should_update_tab_on_event(event) { - cx.notify() + pane.update(cx, |_, cx| cx.notify()); } }) .detach(); @@ -411,8 +512,8 @@ impl ItemViewHandle for ViewHandle { self.update(cx, |this, cx| this.deactivated(cx)); } - fn navigate(&self, data: Box, cx: &mut MutableAppContext) { - self.update(cx, |this, cx| this.navigate(data, cx)); + fn navigate(&self, data: Box, cx: &mut MutableAppContext) -> bool { + self.update(cx, |this, cx| this.navigate(data, cx)) } fn save(&self, project: ModelHandle, cx: &mut MutableAppContext) -> Task> { @@ -455,17 +556,21 @@ impl ItemViewHandle for ViewHandle { fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option { self.read(cx).act_as_type(type_id, self, cx) } -} -impl Into for Box { - fn into(self) -> AnyViewHandle { - self.to_any() + fn to_followable_item_handle(&self, cx: &AppContext) -> Option> { + if cx.has_global::() { + let builders = cx.global::(); + let item = self.to_any(); + Some(builders.get(&item.view_type())?.1(item)) + } else { + None + } } } -impl Clone for Box { - fn clone(&self) -> Box { - self.boxed_clone() +impl Into for Box { + fn into(self) -> AnyViewHandle { + self.to_any() } } @@ -475,14 +580,13 @@ impl Clone for Box { } } -impl WeakItemViewHandle for WeakViewHandle { +impl WeakItemHandle for WeakViewHandle { fn id(&self) -> usize { self.id() } - fn upgrade(&self, cx: &AppContext) -> Option> { - self.upgrade(cx) - .map(|v| Box::new(v) as Box) + fn upgrade(&self, cx: &AppContext) -> Option> { + self.upgrade(cx).map(|v| Box::new(v) as Box) } } @@ -494,14 +598,13 @@ pub struct WorkspaceParams { pub languages: Arc, pub user_store: ModelHandle, pub channel_list: ModelHandle, - pub path_openers: Arc<[Box]>, } impl WorkspaceParams { #[cfg(any(test, feature = "test-support"))] pub fn test(cx: &mut MutableAppContext) -> Self { let settings = Settings::test(cx); - cx.add_app_state(settings); + cx.set_global(settings); let fs = project::FakeFs::new(cx.background().clone()); let languages = Arc::new(LanguageRegistry::test()); @@ -525,7 +628,6 @@ impl WorkspaceParams { fs, languages, user_store, - path_openers: Arc::from([]), } } @@ -544,7 +646,6 @@ impl WorkspaceParams { languages: app_state.languages.clone(), user_store: app_state.user_store.clone(), channel_list: app_state.channel_list.clone(), - path_openers: app_state.path_openers.clone(), } } } @@ -553,6 +654,7 @@ pub struct Workspace { weak_self: WeakViewHandle, client: Arc, user_store: ModelHandle, + remote_entity_subscription: Option, fs: Arc, modal: Option, center: PaneGroup, @@ -562,11 +664,31 @@ pub struct Workspace { active_pane: ViewHandle, status_bar: ViewHandle, project: ModelHandle, - path_openers: Arc<[Box]>, - items: BTreeMap, Box>, + leader_state: LeaderState, + follower_states_by_leader: FollowerStatesByLeader, + last_leaders_by_pane: HashMap, PeerId>, _observe_current_user: Task<()>, } +#[derive(Default)] +struct LeaderState { + followers: HashSet, +} + +type FollowerStatesByLeader = HashMap, FollowerState>>; + +#[derive(Default)] +struct FollowerState { + active_view_id: Option, + items_by_leader_view_id: HashMap, +} + +#[derive(Debug)] +enum FollowerItem { + Loading(Vec), + Loaded(Box), +} + impl Workspace { pub fn new(params: &WorkspaceParams, cx: &mut ViewContext) -> Self { cx.observe(¶ms.project, |_, project, cx| { @@ -577,6 +699,23 @@ impl Workspace { }) .detach(); + cx.subscribe(¶ms.project, move |this, project, event, cx| { + match event { + project::Event::RemoteIdChanged(remote_id) => { + this.project_remote_id_changed(*remote_id, cx); + } + project::Event::CollaboratorLeft(peer_id) => { + this.collaborator_left(*peer_id, cx); + } + _ => {} + } + if project.read(cx).is_read_only() { + cx.blur(); + } + cx.notify() + }) + .detach(); + let pane = cx.add_view(|_| Pane::new()); let pane_id = pane.id(); cx.observe(&pane, move |me, _, cx| { @@ -613,7 +752,7 @@ impl Workspace { cx.emit_global(WorkspaceCreated(weak_self.clone())); - Workspace { + let mut this = Workspace { modal: None, weak_self, center: PaneGroup::new(pane.clone()), @@ -621,15 +760,19 @@ impl Workspace { active_pane: pane.clone(), status_bar, client: params.client.clone(), + remote_entity_subscription: None, user_store: params.user_store.clone(), fs: params.fs.clone(), left_sidebar: Sidebar::new(Side::Left), right_sidebar: Sidebar::new(Side::Right), project: params.project.clone(), - path_openers: params.path_openers.clone(), - items: Default::default(), + leader_state: Default::default(), + follower_states_by_leader: Default::default(), + last_leaders_by_pane: Default::default(), _observe_current_user, - } + }; + this.project_remote_id_changed(this.project.read(cx).remote_id(), cx); + this } pub fn weak_handle(&self) -> WeakViewHandle { @@ -690,7 +833,7 @@ impl Workspace { &mut self, abs_paths: &[PathBuf], cx: &mut ViewContext, - ) -> Task, Arc>>>> { + ) -> Task, Arc>>>> { let entries = abs_paths .iter() .cloned() @@ -782,68 +925,27 @@ impl Workspace { } } - pub fn open_path( - &mut self, - path: ProjectPath, - cx: &mut ViewContext, - ) -> Task, Arc>> { - let load_task = self.load_path(path, cx); - let pane = self.active_pane().clone().downgrade(); - cx.spawn(|this, mut cx| async move { - let item = load_task.await?; - this.update(&mut cx, |this, cx| { - let pane = pane - .upgrade(cx) - .ok_or_else(|| anyhow!("could not upgrade pane reference"))?; - Ok(this.open_item_in_pane(item, &pane, cx)) - }) - }) - } - - pub fn load_path( - &mut self, - path: ProjectPath, - cx: &mut ViewContext, - ) -> Task>> { - if let Some(existing_item) = self.item_for_path(&path, cx) { - return Task::ready(Ok(existing_item)); - } - - let project_path = path.clone(); - let path_openers = self.path_openers.clone(); - self.project.update(cx, |project, cx| { - for opener in path_openers.iter() { - if let Some(task) = opener.open(project, project_path.clone(), cx) { - return task; - } - } - Task::ready(Err(anyhow!("no opener found for path {:?}", project_path))) - }) - } - - fn item_for_path(&self, path: &ProjectPath, cx: &AppContext) -> Option> { - self.items - .values() - .filter_map(|i| i.upgrade(cx)) - .find(|i| i.project_path(cx).as_ref() == Some(path)) + pub fn items<'a>( + &'a self, + cx: &'a AppContext, + ) -> impl 'a + Iterator> { + self.panes.iter().flat_map(|pane| pane.read(cx).items()) } - pub fn item_of_type(&self, cx: &AppContext) -> Option> { - self.items - .values() - .find_map(|i| i.upgrade(cx).and_then(|i| i.to_any().downcast())) + pub fn item_of_type(&self, cx: &AppContext) -> Option> { + self.items_of_type(cx).max_by_key(|item| item.id()) } pub fn items_of_type<'a, T: Item>( &'a self, cx: &'a AppContext, - ) -> impl 'a + Iterator> { - self.items - .values() - .filter_map(|i| i.upgrade(cx).and_then(|i| i.to_any().downcast())) + ) -> impl 'a + Iterator> { + self.panes + .iter() + .flat_map(|pane| pane.read(cx).items_of_type()) } - pub fn active_item(&self, cx: &AppContext) -> Option> { + pub fn active_item(&self, cx: &AppContext) -> Option> { self.active_pane().read(cx).active_item() } @@ -962,49 +1064,84 @@ impl Workspace { pane } - pub fn open_item( + pub fn add_item(&mut self, item: Box, cx: &mut ViewContext) { + let pane = self.active_pane().clone(); + Pane::add_item(self, pane, item, true, cx); + } + + pub fn open_path( &mut self, - item_handle: T, + path: impl Into, cx: &mut ViewContext, - ) -> Box - where - T: 'static + ItemHandle, - { - self.open_item_in_pane(item_handle, &self.active_pane().clone(), cx) + ) -> Task, Arc>> { + let pane = self.active_pane().downgrade(); + let task = self.load_path(path.into(), cx); + cx.spawn(|this, mut cx| async move { + let (project_entry_id, build_item) = task.await?; + let pane = pane + .upgrade(&cx) + .ok_or_else(|| anyhow!("pane was closed"))?; + this.update(&mut cx, |this, cx| { + Ok(Pane::open_item( + this, + pane, + project_entry_id, + cx, + build_item, + )) + }) + }) } - pub fn open_item_in_pane( + pub(crate) fn load_path( &mut self, - item_handle: T, - pane: &ViewHandle, + path: ProjectPath, cx: &mut ViewContext, - ) -> Box - where - T: 'static + ItemHandle, - { - self.items - .insert(Reverse(item_handle.id()), item_handle.downgrade()); - pane.update(cx, |pane, cx| pane.open_item(item_handle, self, cx)) + ) -> Task< + Result<( + ProjectEntryId, + impl 'static + FnOnce(&mut MutableAppContext) -> Box, + )>, + > { + let project = self.project().clone(); + let project_item = project.update(cx, |project, cx| project.open_path(path, cx)); + let window_id = cx.window_id(); + cx.as_mut().spawn(|mut cx| async move { + let (project_entry_id, project_item) = project_item.await?; + let build_item = cx.update(|cx| { + cx.default_global::() + .get(&project_item.model_type()) + .ok_or_else(|| anyhow!("no item builder for project item")) + .cloned() + })?; + let build_item = + move |cx: &mut MutableAppContext| build_item(window_id, project, project_item, cx); + Ok((project_entry_id, build_item)) + }) } - pub fn activate_pane_for_item( + pub fn open_project_item( &mut self, - item: &dyn ItemHandle, + project_item: ModelHandle, cx: &mut ViewContext, - ) -> bool { - let pane = self.panes.iter().find_map(|pane| { - if pane.read(cx).contains_item(item) { - Some(pane.clone()) - } else { - None - } - }); - if let Some(pane) = pane { - self.activate_pane(pane.clone(), cx); - true - } else { - false + ) -> ViewHandle + where + T: ProjectItem, + { + use project::Item as _; + + let entry_id = project_item.read(cx).entry_id(cx); + if let Some(item) = entry_id + .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx)) + .and_then(|item| item.downcast()) + { + self.activate_item(&item, cx); + return item; } + + let item = cx.add_view(|cx| T::for_project_item(self.project().clone(), project_item, cx)); + self.add_item(Box::new(item.clone()), cx); + item } pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut ViewContext) -> bool { @@ -1017,7 +1154,7 @@ impl Workspace { }); if let Some((pane, ix)) = result { self.activate_pane(pane.clone(), cx); - pane.update(cx, |pane, cx| pane.activate_item(ix, cx)); + pane.update(cx, |pane, cx| pane.activate_item(ix, true, cx)); true } else { false @@ -1025,24 +1162,48 @@ impl Workspace { } pub fn activate_next_pane(&mut self, cx: &mut ViewContext) { - let ix = self - .panes - .iter() - .position(|pane| pane == &self.active_pane) - .unwrap(); - let next_ix = (ix + 1) % self.panes.len(); - self.activate_pane(self.panes[next_ix].clone(), cx); + let next_pane = { + let panes = self.center.panes(); + let ix = panes + .iter() + .position(|pane| **pane == self.active_pane) + .unwrap(); + let next_ix = (ix + 1) % panes.len(); + panes[next_ix].clone() + }; + self.activate_pane(next_pane, cx); + } + + pub fn activate_previous_pane(&mut self, cx: &mut ViewContext) { + let prev_pane = { + let panes = self.center.panes(); + let ix = panes + .iter() + .position(|pane| **pane == self.active_pane) + .unwrap(); + let prev_ix = if ix == 0 { panes.len() - 1 } else { ix - 1 }; + panes[prev_ix].clone() + }; + self.activate_pane(prev_pane, cx); } fn activate_pane(&mut self, pane: ViewHandle, cx: &mut ViewContext) { if self.active_pane != pane { - self.active_pane = pane; + self.active_pane = pane.clone(); self.status_bar.update(cx, |status_bar, cx| { status_bar.set_active_pane(&self.active_pane, cx); }); cx.focus(&self.active_pane); cx.notify(); } + + self.update_followers( + proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView { + id: self.active_item(cx).map(|item| item.id() as u64), + leader_id: self.leader_for_pane(&pane).map(|id| id.0), + }), + cx, + ); } fn handle_pane_event( @@ -1062,6 +1223,11 @@ impl Workspace { pane::Event::Activate => { self.activate_pane(pane, cx); } + pane::Event::ActivateItem { local } => { + if *local { + self.unfollow(&pane, cx); + } + } } } else { error!("pane {} not found", pane_id); @@ -1077,11 +1243,8 @@ impl Workspace { let new_pane = self.add_pane(cx); self.activate_pane(new_pane.clone(), cx); if let Some(item) = pane.read(cx).active_item() { - let nav_history = new_pane.read(cx).nav_history().clone(); - if let Some(clone) = item.clone_on_split(nav_history, cx.as_mut()) { - let item = clone.item(cx).downgrade(); - self.items.insert(Reverse(item.id()), item); - new_pane.update(cx, |new_pane, cx| new_pane.add_item_view(clone, cx)); + if let Some(clone) = item.clone_on_split(cx.as_mut()) { + Pane::add_item(self, new_pane.clone(), clone, true, cx); } } self.center.split(&pane, &new_pane, direction).unwrap(); @@ -1093,6 +1256,8 @@ impl Workspace { if self.center.remove(&pane).unwrap() { self.panes.retain(|p| p != &pane); self.activate_pane(self.panes.last().unwrap().clone(), cx); + self.unfollow(&pane, cx); + self.last_leaders_by_pane.remove(&pane.downgrade()); cx.notify(); } } @@ -1113,7 +1278,7 @@ impl Workspace { self.project.update(cx, |project, cx| { if project.is_local() { if project.is_shared() { - project.unshare(cx).detach(); + project.unshare(cx); } else { project.share(cx).detach(); } @@ -1121,8 +1286,141 @@ impl Workspace { }); } + fn project_remote_id_changed(&mut self, remote_id: Option, cx: &mut ViewContext) { + if let Some(remote_id) = remote_id { + self.remote_entity_subscription = + Some(self.client.add_view_for_remote_entity(remote_id, cx)); + } else { + self.remote_entity_subscription.take(); + } + } + + fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext) { + self.leader_state.followers.remove(&peer_id); + if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) { + for state in states_by_pane.into_values() { + for item in state.items_by_leader_view_id.into_values() { + if let FollowerItem::Loaded(item) = item { + item.set_leader_replica_id(None, cx); + } + } + } + } + cx.notify(); + } + + pub fn toggle_follow( + &mut self, + ToggleFollow(leader_id): &ToggleFollow, + cx: &mut ViewContext, + ) -> Option>> { + let leader_id = *leader_id; + let pane = self.active_pane().clone(); + + if let Some(prev_leader_id) = self.unfollow(&pane, cx) { + if leader_id == prev_leader_id { + return None; + } + } + + self.last_leaders_by_pane + .insert(pane.downgrade(), leader_id); + self.follower_states_by_leader + .entry(leader_id) + .or_default() + .insert(pane.clone(), Default::default()); + cx.notify(); + + let project_id = self.project.read(cx).remote_id()?; + let request = self.client.request(proto::Follow { + project_id, + leader_id: leader_id.0, + }); + Some(cx.spawn_weak(|this, mut cx| async move { + let response = request.await?; + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, _| { + let state = this + .follower_states_by_leader + .get_mut(&leader_id) + .and_then(|states_by_pane| states_by_pane.get_mut(&pane)) + .ok_or_else(|| anyhow!("following interrupted"))?; + state.active_view_id = response.active_view_id; + Ok::<_, anyhow::Error>(()) + })?; + Self::add_views_from_leader(this, leader_id, vec![pane], response.views, &mut cx) + .await?; + } + Ok(()) + })) + } + + pub fn follow_next_collaborator( + &mut self, + _: &FollowNextCollaborator, + cx: &mut ViewContext, + ) -> Option>> { + let collaborators = self.project.read(cx).collaborators(); + let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) { + let mut collaborators = collaborators.keys().copied(); + while let Some(peer_id) = collaborators.next() { + if peer_id == leader_id { + break; + } + } + collaborators.next() + } else if let Some(last_leader_id) = + self.last_leaders_by_pane.get(&self.active_pane.downgrade()) + { + if collaborators.contains_key(last_leader_id) { + Some(*last_leader_id) + } else { + None + } + } else { + None + }; + + next_leader_id + .or_else(|| collaborators.keys().copied().next()) + .and_then(|leader_id| self.toggle_follow(&ToggleFollow(leader_id), cx)) + } + + pub fn unfollow( + &mut self, + pane: &ViewHandle, + cx: &mut ViewContext, + ) -> Option { + for (leader_id, states_by_pane) in &mut self.follower_states_by_leader { + let leader_id = *leader_id; + if let Some(state) = states_by_pane.remove(&pane) { + for (_, item) in state.items_by_leader_view_id { + if let FollowerItem::Loaded(item) = item { + item.set_leader_replica_id(None, cx); + } + } + + if states_by_pane.is_empty() { + self.follower_states_by_leader.remove(&leader_id); + if let Some(project_id) = self.project.read(cx).remote_id() { + self.client + .send(proto::Unfollow { + project_id, + leader_id: leader_id.0, + }) + .log_err(); + } + } + + cx.notify(); + return Some(leader_id); + } + } + None + } + fn render_connection_status(&self, cx: &mut RenderContext) -> Option { - let theme = &cx.app_state::().theme; + let theme = &cx.global::().theme; match &*self.client.status().borrow() { client::Status::ConnectionError | client::Status::ConnectionLost @@ -1210,7 +1508,9 @@ impl Workspace { Some(self.render_avatar( collaborator.user.avatar.clone()?, collaborator.replica_id, + Some(collaborator.peer_id), theme, + cx, )) }) .collect() @@ -1224,7 +1524,7 @@ impl Workspace { cx: &mut RenderContext, ) -> ElementBox { if let Some(avatar) = user.and_then(|user| user.avatar.clone()) { - self.render_avatar(avatar, replica_id, theme) + self.render_avatar(avatar, replica_id, None, theme, cx) } else { MouseEventHandler::new::(0, cx, |state, _| { let style = if state.hovered { @@ -1248,52 +1548,65 @@ impl Workspace { &self, avatar: Arc, replica_id: ReplicaId, + peer_id: Option, theme: &Theme, + cx: &mut RenderContext, ) -> ElementBox { - ConstrainedBox::new( - Stack::new() - .with_child( - ConstrainedBox::new( - Image::new(avatar) - .with_style(theme.workspace.titlebar.avatar) - .boxed(), - ) + let replica_color = theme.editor.replica_selection_style(replica_id).cursor; + let is_followed = peer_id.map_or(false, |peer_id| { + self.follower_states_by_leader.contains_key(&peer_id) + }); + let mut avatar_style = theme.workspace.titlebar.avatar; + if is_followed { + avatar_style.border = Border::all(1.0, replica_color); + } + let content = Stack::new() + .with_child( + Image::new(avatar) + .with_style(avatar_style) + .constrained() .with_width(theme.workspace.titlebar.avatar_width) .aligned() .boxed(), - ) - .with_child( - AvatarRibbon::new(theme.editor.replica_selection_style(replica_id).cursor) - .constrained() - .with_width(theme.workspace.titlebar.avatar_ribbon.width) - .with_height(theme.workspace.titlebar.avatar_ribbon.height) - .aligned() - .bottom() - .boxed(), - ) - .boxed(), - ) - .with_width(theme.workspace.right_sidebar.width) - .boxed() + ) + .with_child( + AvatarRibbon::new(replica_color) + .constrained() + .with_width(theme.workspace.titlebar.avatar_ribbon.width) + .with_height(theme.workspace.titlebar.avatar_ribbon.height) + .aligned() + .bottom() + .boxed(), + ) + .constrained() + .with_width(theme.workspace.right_sidebar.width) + .boxed(); + + if let Some(peer_id) = peer_id { + MouseEventHandler::new::(replica_id.into(), cx, move |_, _| content) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(move |cx| cx.dispatch_action(ToggleFollow(peer_id))) + .boxed() + } else { + content + } } fn render_share_icon(&self, theme: &Theme, cx: &mut RenderContext) -> Option { if self.project().read(cx).is_local() && self.client.user_id().is_some() { - enum Share {} - let color = if self.project().read(cx).is_shared() { theme.workspace.titlebar.share_icon_active_color } else { theme.workspace.titlebar.share_icon_color }; Some( - MouseEventHandler::new::(0, cx, |_, _| { + MouseEventHandler::new::(0, cx, |_, _| { Align::new( - ConstrainedBox::new( - Svg::new("icons/broadcast-24.svg").with_color(color).boxed(), - ) - .with_width(24.) - .boxed(), + Svg::new("icons/broadcast-24.svg") + .with_color(color) + .constrained() + .with_width(24.) + .boxed(), ) .boxed() }) @@ -1308,7 +1621,7 @@ impl Workspace { fn render_disconnected_overlay(&self, cx: &AppContext) -> Option { if self.project.read(cx).is_read_only() { - let theme = &cx.app_state::().theme; + let theme = &cx.global::().theme; Some( EventHandler::new( Label::new( @@ -1327,6 +1640,279 @@ impl Workspace { None } } + + // RPC handlers + + async fn handle_follow( + this: ViewHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + this.update(&mut cx, |this, cx| { + this.leader_state + .followers + .insert(envelope.original_sender_id()?); + + let active_view_id = this + .active_item(cx) + .and_then(|i| i.to_followable_item_handle(cx)) + .map(|i| i.id() as u64); + Ok(proto::FollowResponse { + active_view_id, + views: this + .panes() + .iter() + .flat_map(|pane| { + let leader_id = this.leader_for_pane(pane).map(|id| id.0); + pane.read(cx).items().filter_map({ + let cx = &cx; + move |item| { + let id = item.id() as u64; + let item = item.to_followable_item_handle(cx)?; + let variant = item.to_state_proto(cx)?; + Some(proto::View { + id, + leader_id, + variant: Some(variant), + }) + } + }) + }) + .collect(), + }) + }) + } + + async fn handle_unfollow( + this: ViewHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |this, _| { + this.leader_state + .followers + .remove(&envelope.original_sender_id()?); + Ok(()) + }) + } + + async fn handle_update_followers( + this: ViewHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + let leader_id = envelope.original_sender_id()?; + match envelope + .payload + .variant + .ok_or_else(|| anyhow!("invalid update"))? + { + proto::update_followers::Variant::UpdateActiveView(update_active_view) => { + this.update(&mut cx, |this, cx| { + this.update_leader_state(leader_id, cx, |state, _| { + state.active_view_id = update_active_view.id; + }); + Ok::<_, anyhow::Error>(()) + }) + } + proto::update_followers::Variant::UpdateView(update_view) => { + this.update(&mut cx, |this, cx| { + let variant = update_view + .variant + .ok_or_else(|| anyhow!("missing update view variant"))?; + this.update_leader_state(leader_id, cx, |state, cx| { + let variant = variant.clone(); + match state + .items_by_leader_view_id + .entry(update_view.id) + .or_insert(FollowerItem::Loading(Vec::new())) + { + FollowerItem::Loaded(item) => { + item.apply_update_proto(variant, cx).log_err(); + } + FollowerItem::Loading(updates) => updates.push(variant), + } + }); + Ok(()) + }) + } + proto::update_followers::Variant::CreateView(view) => { + let panes = this.read_with(&cx, |this, _| { + this.follower_states_by_leader + .get(&leader_id) + .into_iter() + .flat_map(|states_by_pane| states_by_pane.keys()) + .cloned() + .collect() + }); + Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], &mut cx) + .await?; + Ok(()) + } + } + .log_err(); + + Ok(()) + } + + async fn add_views_from_leader( + this: ViewHandle, + leader_id: PeerId, + panes: Vec>, + views: Vec, + cx: &mut AsyncAppContext, + ) -> Result<()> { + let project = this.read_with(cx, |this, _| this.project.clone()); + let replica_id = project + .read_with(cx, |project, _| { + project + .collaborators() + .get(&leader_id) + .map(|c| c.replica_id) + }) + .ok_or_else(|| anyhow!("no such collaborator {}", leader_id))?; + + let item_builders = cx.update(|cx| { + cx.default_global::() + .values() + .map(|b| b.0) + .collect::>() + .clone() + }); + + let mut item_tasks_by_pane = HashMap::default(); + for pane in panes { + let mut item_tasks = Vec::new(); + let mut leader_view_ids = Vec::new(); + for view in &views { + let mut variant = view.variant.clone(); + if variant.is_none() { + Err(anyhow!("missing variant"))?; + } + for build_item in &item_builders { + let task = + cx.update(|cx| build_item(pane.clone(), project.clone(), &mut variant, cx)); + if let Some(task) = task { + item_tasks.push(task); + leader_view_ids.push(view.id); + break; + } else { + assert!(variant.is_some()); + } + } + } + + item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids)); + } + + for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane { + let items = futures::future::try_join_all(item_tasks).await?; + this.update(cx, |this, cx| { + let state = this + .follower_states_by_leader + .get_mut(&leader_id)? + .get_mut(&pane)?; + + for (id, item) in leader_view_ids.into_iter().zip(items) { + item.set_leader_replica_id(Some(replica_id), cx); + match state.items_by_leader_view_id.entry(id) { + hash_map::Entry::Occupied(e) => { + let e = e.into_mut(); + if let FollowerItem::Loading(updates) = e { + for update in updates.drain(..) { + item.apply_update_proto(update, cx) + .context("failed to apply view update") + .log_err(); + } + } + *e = FollowerItem::Loaded(item); + } + hash_map::Entry::Vacant(e) => { + e.insert(FollowerItem::Loaded(item)); + } + } + } + + Some(()) + }); + } + this.update(cx, |this, cx| this.leader_updated(leader_id, cx)); + + Ok(()) + } + + fn update_followers( + &self, + update: proto::update_followers::Variant, + cx: &AppContext, + ) -> Option<()> { + let project_id = self.project.read(cx).remote_id()?; + if !self.leader_state.followers.is_empty() { + self.client + .send(proto::UpdateFollowers { + project_id, + follower_ids: self.leader_state.followers.iter().map(|f| f.0).collect(), + variant: Some(update), + }) + .log_err(); + } + None + } + + pub fn leader_for_pane(&self, pane: &ViewHandle) -> Option { + self.follower_states_by_leader + .iter() + .find_map(|(leader_id, state)| { + if state.contains_key(pane) { + Some(*leader_id) + } else { + None + } + }) + } + + fn update_leader_state( + &mut self, + leader_id: PeerId, + cx: &mut ViewContext, + mut update_fn: impl FnMut(&mut FollowerState, &mut ViewContext), + ) { + for (_, state) in self + .follower_states_by_leader + .get_mut(&leader_id) + .into_iter() + .flatten() + { + update_fn(state, cx); + } + self.leader_updated(leader_id, cx); + } + + fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext) -> Option<()> { + let mut items_to_add = Vec::new(); + for (pane, state) in self.follower_states_by_leader.get(&leader_id)? { + if let Some(active_item) = state + .active_view_id + .and_then(|id| state.items_by_leader_view_id.get(&id)) + { + if let FollowerItem::Loaded(item) = active_item { + items_to_add.push((pane.clone(), item.boxed_clone())); + } + } + } + + for (pane, item) in items_to_add { + Pane::add_item(self, pane.clone(), item.boxed_clone(), false, cx); + if pane == self.active_pane { + pane.update(cx, |pane, cx| pane.focus_active_item(cx)); + } + cx.notify(); + } + None + } } impl Entity for Workspace { @@ -1339,7 +1925,7 @@ impl View for Workspace { } fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - let theme = cx.app_state::().theme.clone(); + let theme = cx.global::().theme.clone(); Stack::new() .with_child( Flex::column() @@ -1357,8 +1943,16 @@ impl View for Workspace { content.add_child( Flex::column() .with_child( - Flexible::new(1., true, self.center.render(&theme)) - .boxed(), + Flexible::new( + 1., + true, + self.center.render( + &theme, + &self.follower_states_by_leader, + self.project.read(cx).collaborators(), + ), + ) + .boxed(), ) .with_child(ChildView::new(&self.status_bar).boxed()) .flexible(1., true) diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index b0e5a63137dcbd6b8a0cdde080b99772f87e9447..fc9946b778bc8c74254b49b6fd872e402b515f36 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] description = "The fast, collaborative code editor." edition = "2021" name = "zed" -version = "0.21.0" +version = "0.23.0" [lib] name = "zed" @@ -55,6 +55,7 @@ text = { path = "../text" } theme = { path = "../theme" } theme_selector = { path = "../theme_selector" } util = { path = "../util" } +vim = { path = "../vim" } workspace = { path = "../workspace" } anyhow = "1.0.38" async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] } @@ -64,6 +65,7 @@ crossbeam-channel = "0.5.0" ctor = "0.1.20" dirs = "3.0" easy-parallel = "3.1.0" +env_logger = "0.8" futures = "0.3" http-auth-basic = "0.1.3" ignore = "0.4" diff --git a/crates/zed/assets/fonts/zed-mono/zed-mono-extended.ttf b/crates/zed/assets/fonts/zed-mono/zed-mono-extended.ttf index 1cab6121aad689ef98ac7f79b408108d15b87e68..05b8c7085e51d4321bf1e73f763980629c8ddfd0 100644 Binary files a/crates/zed/assets/fonts/zed-mono/zed-mono-extended.ttf and b/crates/zed/assets/fonts/zed-mono/zed-mono-extended.ttf differ diff --git a/crates/zed/assets/fonts/zed-mono/zed-mono-extendedbold.ttf b/crates/zed/assets/fonts/zed-mono/zed-mono-extendedbold.ttf index 5c027864e01d3346cc84847db83e022d1fcf320c..d5dde1bb14d5809c4c3312f53903adc7bece6b52 100644 Binary files a/crates/zed/assets/fonts/zed-mono/zed-mono-extendedbold.ttf and b/crates/zed/assets/fonts/zed-mono/zed-mono-extendedbold.ttf differ diff --git a/crates/zed/assets/fonts/zed-mono/zed-mono-extendedbolditalic.ttf b/crates/zed/assets/fonts/zed-mono/zed-mono-extendedbolditalic.ttf index 47a3a9bc27d1f4be40eb1795f523a3f184027e8c..bcd8c7e618e0b684e236a728c85860308b5ad386 100644 Binary files a/crates/zed/assets/fonts/zed-mono/zed-mono-extendedbolditalic.ttf and b/crates/zed/assets/fonts/zed-mono/zed-mono-extendedbolditalic.ttf differ diff --git a/crates/zed/assets/fonts/zed-mono/zed-mono-extendeditalic.ttf b/crates/zed/assets/fonts/zed-mono/zed-mono-extendeditalic.ttf index 85ff2c3c4ae8e54881c262de18a0f457685481f5..023c5a87cea6c1ba98238367e162d1de1003397c 100644 Binary files a/crates/zed/assets/fonts/zed-mono/zed-mono-extendeditalic.ttf and b/crates/zed/assets/fonts/zed-mono/zed-mono-extendeditalic.ttf differ diff --git a/crates/zed/assets/fonts/zed-sans/zed-sans-extended.ttf b/crates/zed/assets/fonts/zed-sans/zed-sans-extended.ttf index d5cc91e27aaee686ea84ed9fcb99634c16c156b5..07a968568039987f2ba5b64de12a5ce1016a0054 100644 Binary files a/crates/zed/assets/fonts/zed-sans/zed-sans-extended.ttf and b/crates/zed/assets/fonts/zed-sans/zed-sans-extended.ttf differ diff --git a/crates/zed/assets/fonts/zed-sans/zed-sans-extendedbold.ttf b/crates/zed/assets/fonts/zed-sans/zed-sans-extendedbold.ttf index 0e1264b1ceedf72417968c21d2dd0c7b2f1176ea..696c3cdd5b8ecde7cf574ec8491c15094b3bb4fa 100644 Binary files a/crates/zed/assets/fonts/zed-sans/zed-sans-extendedbold.ttf and b/crates/zed/assets/fonts/zed-sans/zed-sans-extendedbold.ttf differ diff --git a/crates/zed/assets/fonts/zed-sans/zed-sans-extendedbolditalic.ttf b/crates/zed/assets/fonts/zed-sans/zed-sans-extendedbolditalic.ttf index 568d9930b72772311e9631c331b675c69b6ef33f..74cb8f7a3c6dd616839a19381b3d2f69bebfc557 100644 Binary files a/crates/zed/assets/fonts/zed-sans/zed-sans-extendedbolditalic.ttf and b/crates/zed/assets/fonts/zed-sans/zed-sans-extendedbolditalic.ttf differ diff --git a/crates/zed/assets/fonts/zed-sans/zed-sans-extendeditalic.ttf b/crates/zed/assets/fonts/zed-sans/zed-sans-extendeditalic.ttf index d5f10a2ee69c96921fb15796000f394963ba9c7b..9460e5a7dd690fbe8ceb05d85f1c2ee9a3a2d9d5 100644 Binary files a/crates/zed/assets/fonts/zed-sans/zed-sans-extendeditalic.ttf and b/crates/zed/assets/fonts/zed-sans/zed-sans-extendeditalic.ttf differ diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index 76547967bfb8cc34cec0fae1307250e57c08b6f4..7bd0c59045fa6f99dee87429d5eef38b327f0137 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -4,6 +4,8 @@ base = { family = "Zed Sans", size = 14 } [workspace] background = "$surface.0" pane_divider = { width = 1, color = "$border.0" } +leader_border_opacity = 0.7 +leader_border_width = 2.0 [workspace.titlebar] height = 32 diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 85960002469249d94503954f4775d4e4e7a23851..63721346c399ca60ea4f701f330ad78fb61d63e0 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -9,7 +9,6 @@ use gpui::{App, AssetSource, Task}; use log::LevelFilter; use parking_lot::Mutex; use project::Fs; -use simplelog::SimpleLogger; use smol::process::Command; use std::{env, fs, path::PathBuf, sync::Arc}; use theme::{ThemeRegistry, DEFAULT_THEME_NAME}; @@ -61,7 +60,6 @@ fn main() { app.run(move |cx| { let http = http::client(); let client = client::Client::new(http.clone()); - let mut path_openers = Vec::new(); let mut languages = language::build_language_registry(login_shell_env_loaded); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx)); let channel_list = @@ -70,8 +68,8 @@ fn main() { project::Project::init(&client); client::Channel::init(&client); client::init(client.clone(), cx); - workspace::init(cx); - editor::init(cx, &mut path_openers); + workspace::init(&client, cx); + editor::init(cx); go_to_line::init(cx); file_finder::init(cx); chat_panel::init(cx); @@ -80,11 +78,18 @@ fn main() { project_panel::init(cx); diagnostics::init(cx); search::init(cx); + vim::init(cx); cx.spawn({ let client = client.clone(); |cx| async move { - if client.has_keychain_credentials(&cx) { - client.authenticate_and_connect(&cx).await?; + if stdout_is_a_pty() { + if client::IMPERSONATE_LOGIN.is_some() { + client.authenticate_and_connect(false, &cx).await?; + } + } else { + if client.has_keychain_credentials(&cx) { + client.authenticate_and_connect(true, &cx).await?; + } } Ok::<_, anyhow::Error>(()) } @@ -102,7 +107,7 @@ fn main() { cx.spawn(|mut cx| async move { while let Some(settings) = settings_rx.next().await { cx.update(|cx| { - cx.update_app_state(|s, _| *s = settings); + cx.update_global(|s, _| *s = settings); cx.refresh_windows(); }); } @@ -111,7 +116,7 @@ fn main() { languages.set_language_server_download_dir(zed::ROOT_PATH.clone()); languages.set_theme(&settings.theme.editor.syntax); - cx.add_app_state(settings); + cx.set_global(settings); let app_state = Arc::new(AppState { languages: Arc::new(languages), @@ -120,7 +125,6 @@ fn main() { client, user_store, fs, - path_openers: Arc::from(path_openers), build_window_options: &build_window_options, build_workspace: &build_workspace, }); @@ -144,11 +148,10 @@ fn main() { } fn init_logger() { - let level = LevelFilter::Info; - if stdout_is_a_pty() { - SimpleLogger::init(level, Default::default()).expect("could not initialize logger"); + env_logger::init(); } else { + let level = LevelFilter::Info; let log_dir_path = dirs::home_dir() .expect("could not locate home directory for logging") .join("Library/Logs/"); diff --git a/crates/zed/src/test.rs b/crates/zed/src/test.rs index 35610854f30fd272a8763f6ecc8d016381e12baf..5b3bb41c1523bf910a523766f6af2c9a631d264d 100644 --- a/crates/zed/src/test.rs +++ b/crates/zed/src/test.rs @@ -17,9 +17,8 @@ fn init_logger() { pub fn test_app_state(cx: &mut MutableAppContext) -> Arc { let settings = Settings::test(cx); - let mut path_openers = Vec::new(); - editor::init(cx, &mut path_openers); - cx.add_app_state(settings); + editor::init(cx); + cx.set_global(settings); let themes = ThemeRegistry::new(Assets, cx.font_cache().clone()); let http = FakeHttpClient::with_404_response(); let client = Client::new(http.clone()); @@ -40,7 +39,6 @@ pub fn test_app_state(cx: &mut MutableAppContext) -> Arc { client, user_store, fs: FakeFs::new(cx.background().clone()), - path_openers: Arc::from(path_openers), build_window_options: &build_window_options, build_workspace: &build_workspace, }) diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 20da4e4800b6547b3b6a26096a3a865b7aca08b3..1302d54067810653140367b40966e869edcf8372 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -43,7 +43,7 @@ pub fn init(app_state: &Arc, cx: &mut gpui::MutableAppContext) { cx.add_global_action(quit); cx.add_global_action({ move |action: &AdjustBufferFontSize, cx| { - cx.update_app_state::(|settings, cx| { + cx.update_global::(|settings, cx| { settings.buffer_font_size = (settings.buffer_font_size + action.0).max(MIN_FONT_SIZE); cx.refresh_windows(); @@ -111,7 +111,6 @@ pub fn build_workspace( languages: app_state.languages.clone(), user_store: app_state.user_store.clone(), channel_list: app_state.channel_list.clone(), - path_openers: app_state.path_openers.clone(), }; let mut workspace = Workspace::new(&workspace_params, cx); let project = workspace.project().clone(); @@ -193,7 +192,7 @@ mod tests { use theme::{Theme, ThemeRegistry, DEFAULT_THEME_NAME}; use util::test::temp_tree; use workspace::{ - open_paths, pane, ItemView, ItemViewHandle, OpenNew, Pane, SplitDirection, WorkspaceHandle, + open_paths, pane, Item, ItemHandle, OpenNew, Pane, SplitDirection, WorkspaceHandle, }; #[gpui::test] @@ -253,7 +252,7 @@ mod tests { async fn test_new_empty_workspace(cx: &mut TestAppContext) { let app_state = cx.update(test_app_state); cx.update(|cx| { - workspace::init(cx); + workspace::init(&app_state.client, cx); }); cx.dispatch_global_action(workspace::OpenNew(app_state.clone())); let window_id = *cx.window_ids().first().unwrap(); @@ -325,7 +324,7 @@ mod tests { pane.active_item().unwrap().project_path(cx), Some(file1.clone()) ); - assert_eq!(pane.item_views().count(), 1); + assert_eq!(pane.items().count(), 1); }); // Open the second entry @@ -339,7 +338,7 @@ mod tests { pane.active_item().unwrap().project_path(cx), Some(file2.clone()) ); - assert_eq!(pane.item_views().count(), 2); + assert_eq!(pane.items().count(), 2); }); // Open the first entry again. The existing pane item is activated. @@ -355,7 +354,7 @@ mod tests { pane.active_item().unwrap().project_path(cx), Some(file1.clone()) ); - assert_eq!(pane.item_views().count(), 2); + assert_eq!(pane.items().count(), 2); }); // Split the pane with the first entry, then open the second entry again. @@ -394,7 +393,7 @@ mod tests { Some(file3.clone()) ); let pane_entries = pane - .item_views() + .items() .map(|i| i.project_path(cx).unwrap()) .collect::>(); assert_eq!(pane_entries, &[file1, file2, file3]); @@ -894,6 +893,52 @@ mod tests { (file3.clone(), DisplayPoint::new(0, 0)) ); + // Modify file to remove nav history location, and ensure duplicates are skipped + editor1.update(cx, |editor, cx| { + editor.select_display_ranges(&[DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)], cx) + }); + + for _ in 0..5 { + editor1.update(cx, |editor, cx| { + editor + .select_display_ranges(&[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)], cx); + }); + editor1.update(cx, |editor, cx| { + editor.select_display_ranges( + &[DisplayPoint::new(13, 0)..DisplayPoint::new(13, 0)], + cx, + ) + }); + } + + editor1.update(cx, |editor, cx| { + editor.transact(cx, |editor, cx| { + editor.select_display_ranges( + &[DisplayPoint::new(2, 0)..DisplayPoint::new(14, 0)], + cx, + ); + editor.insert("", cx); + }) + }); + + editor1.update(cx, |editor, cx| { + editor.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx) + }); + workspace + .update(cx, |w, cx| Pane::go_back(w, None, cx)) + .await; + assert_eq!( + active_location(&workspace, cx), + (file1.clone(), DisplayPoint::new(2, 0)) + ); + workspace + .update(cx, |w, cx| Pane::go_back(w, None, cx)) + .await; + assert_eq!( + active_location(&workspace, cx), + (file1.clone(), DisplayPoint::new(3, 0)) + ); + fn active_location( workspace: &ViewHandle, cx: &mut TestAppContext,