Detailed changes
@@ -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",
]
@@ -64,13 +64,13 @@ impl ChatPanel {
ix,
item_type,
is_hovered,
- &cx.app_state::<Settings>().theme.chat_panel.channel_select,
+ &cx.global::<Settings>().theme.chat_panel.channel_select,
cx,
)
}
})
.with_style(move |cx| {
- let theme = &cx.app_state::<Settings>().theme.chat_panel.channel_select;
+ let theme = &cx.global::<Settings>().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<Self>) -> ElementBox {
- let theme = &cx.app_state::<Settings>().theme;
+ let theme = &cx.global::<Settings>().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::<Settings>();
+ let settings = cx.global::<Settings>();
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::<Settings>().theme;
+ let theme = &cx.global::<Settings>().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<Self>) -> ElementBox {
- let theme = cx.app_state::<Settings>().theme.clone();
+ let theme = cx.global::<Settings>().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::<Settings>().theme;
+ let theme = &cx.global::<Settings>().theme;
ConstrainedBox::new(
Container::new(element)
.with_style(theme.chat_panel.container)
@@ -181,7 +181,7 @@ impl Entity for Channel {
impl Channel {
pub fn init(rpc: &Arc<Client>) {
- rpc.add_entity_message_handler(Self::handle_message_sent);
+ rpc.add_model_message_handler(Self::handle_message_sent);
}
pub fn new(
@@ -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<String> = std::env::var("ZED_IMPERSONATE")
+ pub static ref IMPERSONATE_LOGIN: Option<String> = 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<Client>, 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<Credentials>,
status: (watch::Sender<Status>, watch::Receiver<Status>),
- entity_id_extractors: HashMap<TypeId, Box<dyn Send + Sync + Fn(&dyn AnyTypedEnvelope) -> u64>>,
+ entity_id_extractors: HashMap<TypeId, fn(&dyn AnyTypedEnvelope) -> u64>,
_reconnect_task: Option<Task<()>>,
reconnect_interval: Duration,
- models_by_entity_type_and_remote_id: HashMap<(TypeId, u64), AnyWeakModelHandle>,
+ entities_by_type_and_remote_id: HashMap<(TypeId, u64), AnyWeakEntityHandle>,
models_by_message_type: HashMap<TypeId, AnyWeakModelHandle>,
- model_types_by_message_type: HashMap<TypeId, TypeId>,
+ entity_types_by_message_type: HashMap<TypeId, TypeId>,
message_handlers: HashMap<
TypeId,
Arc<
dyn Send
+ Sync
+ Fn(
- AnyModelHandle,
+ AnyEntityHandle,
Box<dyn AnyTypedEnvelope>,
+ &Arc<Client>,
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<T: View>(
+ self: &Arc<Self>,
+ remote_id: u64,
+ cx: &mut ViewContext<T>,
+ ) -> Subscription {
+ let id = (TypeId::of::<T>(), remote_id);
+ self.state
+ .write()
+ .entities_by_type_and_remote_id
+ .insert(id, AnyWeakEntityHandle::View(cx.weak_handle().into()));
+ Subscription::Entity {
+ client: Arc::downgrade(self),
+ id,
+ }
+ }
+
pub fn add_model_for_remote_entity<T: Entity>(
self: &Arc<Self>,
remote_id: u64,
cx: &mut ModelContext<T>,
) -> Subscription {
- let handle = AnyModelHandle::from(cx.handle());
- let mut state = self.state.write();
let id = (TypeId::of::<T>(), 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::<M>();
- 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::<E>().unwrap();
let envelope = envelope.into_any().downcast::<TypedEnvelope<M>>().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<M, E, H, F>(self: &Arc<Self>, handler: H)
+ pub fn add_view_message_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
+ where
+ M: EntityMessage,
+ E: View,
+ H: 'static
+ + Send
+ + Sync
+ + Fn(ViewHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
+ F: 'static + Future<Output = Result<()>>,
+ {
+ self.add_entity_message_handler::<M, E, _, _>(move |handle, message, client, cx| {
+ if let AnyEntityHandle::View(handle) = handle {
+ handler(handle.downcast::<E>().unwrap(), message, client, cx)
+ } else {
+ unreachable!();
+ }
+ })
+ }
+
+ pub fn add_model_message_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
where
M: EntityMessage,
E: Entity,
@@ -383,38 +428,51 @@ impl Client {
+ Sync
+ Fn(ModelHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
F: 'static + Future<Output = Result<()>>,
+ {
+ self.add_entity_message_handler::<M, E, _, _>(move |handle, message, client, cx| {
+ if let AnyEntityHandle::Model(handle) = handle {
+ handler(handle.downcast::<E>().unwrap(), message, client, cx)
+ } else {
+ unreachable!();
+ }
+ })
+ }
+
+ fn add_entity_message_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
+ where
+ M: EntityMessage,
+ E: Entity,
+ H: 'static
+ + Send
+ + Sync
+ + Fn(AnyEntityHandle, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
+ F: 'static + Future<Output = Result<()>>,
{
let model_type_id = TypeId::of::<E>();
let message_type_id = TypeId::of::<M>();
- 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::<TypedEnvelope<M>>()
- .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::<E>().unwrap();
+ Arc::new(move |handle, envelope, client, cx| {
let envelope = envelope.into_any().downcast::<TypedEnvelope<M>>().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<M, E, H, F>(self: &Arc<Self>, handler: H)
+ pub fn add_model_request_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
where
M: EntityMessage + RequestMessage,
E: Entity,
@@ -432,29 +490,56 @@ impl Client {
+ Fn(ModelHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
F: 'static + Future<Output = Result<M::Response>>,
{
- 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::<M, _>(
+ envelope.receipt(),
+ handler(entity, envelope, client.clone(), cx),
+ client,
+ )
+ })
+ }
+
+ pub fn add_view_request_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
+ where
+ M: EntityMessage + RequestMessage,
+ E: View,
+ H: 'static
+ + Send
+ + Sync
+ + Fn(ViewHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
+ F: 'static + Future<Output = Result<M::Response>>,
+ {
+ self.add_view_message_handler(move |entity, envelope, client, cx| {
+ Self::respond_to_request::<M, _>(
+ envelope.receipt(),
+ handler(entity, envelope, client.clone(), cx),
+ client,
+ )
})
}
+ async fn respond_to_request<T: RequestMessage, F: Future<Output = Result<T::Response>>>(
+ receipt: Receipt<T>,
+ response: F,
+ client: Arc<Self>,
+ ) -> Result<()> {
+ match response.await {
+ 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<Self>,
+ 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<AnyEntityHandle> {
+ match self {
+ AnyWeakEntityHandle::Model(handle) => handle.upgrade(cx).map(AnyEntityHandle::Model),
+ AnyWeakEntityHandle::View(handle) => handle.upgrade(cx).map(AnyEntityHandle::View),
+ }
+ }
+}
+
fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option<Credentials> {
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<Model>, _: TypedEnvelope<proto::UnshareProject>, _, cx| {
match model.read_with(&cx, |model, _| model.id) {
1 => done_tx1.try_send(()).unwrap(),
@@ -91,7 +91,7 @@ impl FakeServer {
});
client
- .authenticate_and_connect(&cx.to_async())
+ .authenticate_and_connect(false, &cx.to_async())
.await
.unwrap();
server
@@ -55,7 +55,7 @@ impl ContactsPanel {
app_state: Arc<AppState>,
cx: &mut LayoutContext,
) -> ElementBox {
- let theme = cx.app_state::<Settings>().theme.clone();
+ let theme = cx.global::<Settings>().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<Self>) -> ElementBox {
- let theme = &cx.app_state::<Settings>().theme.contacts_panel;
+ let theme = &cx.global::<Settings>().theme.contacts_panel;
Container::new(List::new(self.contacts.clone()).boxed())
.with_style(theme.container)
.boxed()
@@ -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<Project>,
-}
-
struct ProjectDiagnosticsEditor {
- model: ModelHandle<ProjectDiagnostics>,
+ project: ModelHandle<Project>,
workspace: WeakViewHandle<Workspace>,
editor: ViewHandle<Editor>,
summary: DiagnosticSummary,
@@ -65,16 +61,6 @@ struct DiagnosticGroupState {
block_count: usize,
}
-impl ProjectDiagnostics {
- fn new(project: ModelHandle<Project>) -> 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<Self>) -> ElementBox {
if self.path_states.is_empty() {
- let theme = &cx.app_state::<Settings>().theme.project_diagnostics;
+ let theme = &cx.global::<Settings>().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<ProjectDiagnostics>,
+ project_handle: ModelHandle<Project>,
workspace: WeakViewHandle<Workspace>,
cx: &mut ViewContext<Self>,
) -> 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<Workspace>) {
- if let Some(existing) = workspace.item_of_type::<ProjectDiagnostics>(cx) {
+ if let Some(existing) = workspace.item_of_type::<ProjectDiagnosticsEditor>(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<Self>) {
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>) {
- 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<Self>,
- workspace: &Workspace,
- nav_history: ItemNavHistory,
- cx: &mut ViewContext<Self::View>,
- ) -> 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<project::ProjectPath> {
- None
- }
-}
-
-impl workspace::ItemView for ProjectDiagnosticsEditor {
- fn item(&self, _: &AppContext) -> Box<dyn ItemHandle> {
- 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::<Settings>().theme.project_diagnostics,
+ &cx.global::<Settings>().theme.project_diagnostics,
)
}
@@ -486,9 +449,13 @@ impl workspace::ItemView for ProjectDiagnosticsEditor {
None
}
- fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) {
+ fn project_entry_id(&self, _: &AppContext) -> Option<project::ProjectEntryId> {
+ None
+ }
+
+ fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> 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<Self>,
- ) -> Option<Self>
+ fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {
+ self.editor.update(cx, |editor, _| {
+ editor.set_nav_history(Some(nav_history));
+ });
+ }
+
+ fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
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::<Settings>();
+ let settings = cx.global::<Settings>();
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;
@@ -49,7 +49,7 @@ impl View for DiagnosticSummary {
let in_progress = self.in_progress;
MouseEventHandler::new::<Tag, _, _>(0, cx, |_, cx| {
- let theme = &cx.app_state::<Settings>().theme.project_diagnostics;
+ let theme = &cx.global::<Settings>().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<Self>,
) {
}
@@ -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"
@@ -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<WrapMap>,
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<T: ToOffset>(
&mut self,
ranges: impl IntoIterator<Item = Range<T>>,
+ inclusive: bool,
cx: &mut ModelContext<Self>,
) {
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::<MyType>(),
+ 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<u32>,
map: &ModelHandle<DisplayMap>,
theme: &'a SyntaxTheme,
cx: &mut MutableAppContext,
) -> Vec<(String, Option<Color>)> {
+ chunks(rows, map, theme, cx)
+ .into_iter()
+ .map(|(text, color, _)| (text, color))
+ .collect()
+ }
+
+ fn chunks<'a>(
+ rows: Range<u32>,
+ map: &ModelHandle<DisplayMap>,
+ theme: &'a SyntaxTheme,
+ cx: &mut MutableAppContext,
+ ) -> Vec<(String, Option<Color>, Option<Color>)> {
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
- let mut chunks: Vec<(String, Option<Color>)> = Vec::new();
+ let mut chunks: Vec<(String, Option<Color>, Option<Color>)> = 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
}
@@ -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,
};
@@ -140,13 +140,14 @@ impl<'a> FoldMapWriter<'a> {
pub fn unfold<T: ToOffset>(
&mut self,
ranges: impl IntoIterator<Item = Range<T>>,
+ inclusive: bool,
) -> (FoldSnapshot, Vec<FoldEdit>) {
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::<Vec<_>>();
@@ -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))
@@ -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<Box<dyn PathOpener>>) {
- 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<Box<dyn PathOpene
Some("Editor && showing_code_actions"),
),
Binding::new("enter", ConfirmRename, Some("Editor && renaming")),
- Binding::new("tab", Tab, Some("Editor")),
+ Binding::new("tab", Tab(Direction::Next), Some("Editor")),
+ Binding::new("shift-tab", Tab(Direction::Prev), Some("Editor")),
Binding::new(
"tab",
ConfirmCompletion(None),
Some("Editor && showing_completions"),
),
- Binding::new("shift-tab", Outdent, Some("Editor")),
+ Binding::new("cmd-[", Outdent, Some("Editor")),
+ Binding::new("cmd-]", Indent, Some("Editor")),
Binding::new("ctrl-shift-K", DeleteLine, Some("Editor")),
+ Binding::new("alt-backspace", DeleteToPreviousWordStart, Some("Editor")),
+ Binding::new("alt-h", DeleteToPreviousWordStart, Some("Editor")),
Binding::new(
- "alt-backspace",
- DeleteToPreviousWordBoundary,
+ "ctrl-alt-backspace",
+ DeleteToPreviousSubwordStart,
Some("Editor"),
),
- Binding::new("alt-h", DeleteToPreviousWordBoundary, Some("Editor")),
- Binding::new("alt-delete", DeleteToNextWordBoundary, Some("Editor")),
- Binding::new("alt-d", DeleteToNextWordBoundary, Some("Editor")),
+ Binding::new("ctrl-alt-h", DeleteToPreviousSubwordStart, Some("Editor")),
+ Binding::new("alt-delete", DeleteToNextWordEnd, Some("Editor")),
+ Binding::new("alt-d", DeleteToNextWordEnd, Some("Editor")),
+ Binding::new("ctrl-alt-delete", DeleteToNextSubwordEnd, Some("Editor")),
+ Binding::new("ctrl-alt-d", DeleteToNextSubwordEnd, Some("Editor")),
Binding::new("cmd-backspace", DeleteToBeginningOfLine, Some("Editor")),
Binding::new("cmd-delete", DeleteToEndOfLine, Some("Editor")),
Binding::new("ctrl-k", CutToEndOfLine, Some("Editor")),
@@ -202,10 +216,14 @@ pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec<Box<dyn PathOpene
Binding::new("ctrl-n", MoveDown, Some("Editor")),
Binding::new("ctrl-b", MoveLeft, Some("Editor")),
Binding::new("ctrl-f", MoveRight, Some("Editor")),
- Binding::new("alt-left", MoveToPreviousWordBoundary, Some("Editor")),
- Binding::new("alt-b", MoveToPreviousWordBoundary, Some("Editor")),
- Binding::new("alt-right", MoveToNextWordBoundary, Some("Editor")),
- Binding::new("alt-f", MoveToNextWordBoundary, Some("Editor")),
+ Binding::new("alt-left", MoveToPreviousWordStart, Some("Editor")),
+ Binding::new("alt-b", MoveToPreviousWordStart, Some("Editor")),
+ Binding::new("ctrl-alt-left", MoveToPreviousSubwordStart, Some("Editor")),
+ Binding::new("ctrl-alt-b", MoveToPreviousSubwordStart, Some("Editor")),
+ Binding::new("alt-right", MoveToNextWordEnd, Some("Editor")),
+ Binding::new("alt-f", MoveToNextWordEnd, Some("Editor")),
+ Binding::new("ctrl-alt-right", MoveToNextSubwordEnd, Some("Editor")),
+ Binding::new("ctrl-alt-f", MoveToNextSubwordEnd, Some("Editor")),
Binding::new("cmd-left", MoveToBeginningOfLine, Some("Editor")),
Binding::new("ctrl-a", MoveToBeginningOfLine, Some("Editor")),
Binding::new("cmd-right", MoveToEndOfLine, Some("Editor")),
@@ -220,19 +238,31 @@ pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec<Box<dyn PathOpene
Binding::new("ctrl-shift-B", SelectLeft, Some("Editor")),
Binding::new("shift-right", SelectRight, Some("Editor")),
Binding::new("ctrl-shift-F", SelectRight, Some("Editor")),
+ Binding::new("alt-shift-left", SelectToPreviousWordStart, Some("Editor")),
+ Binding::new("alt-shift-B", SelectToPreviousWordStart, Some("Editor")),
Binding::new(
- "alt-shift-left",
- SelectToPreviousWordBoundary,
+ "ctrl-alt-shift-left",
+ SelectToPreviousSubwordStart,
Some("Editor"),
),
- Binding::new("alt-shift-B", SelectToPreviousWordBoundary, Some("Editor")),
- Binding::new("alt-shift-right", SelectToNextWordBoundary, Some("Editor")),
- Binding::new("alt-shift-F", SelectToNextWordBoundary, Some("Editor")),
+ Binding::new(
+ "ctrl-alt-shift-B",
+ SelectToPreviousSubwordStart,
+ Some("Editor"),
+ ),
+ Binding::new("alt-shift-right", SelectToNextWordEnd, Some("Editor")),
+ Binding::new("alt-shift-F", SelectToNextWordEnd, Some("Editor")),
Binding::new(
"cmd-shift-left",
SelectToBeginningOfLine(true),
Some("Editor"),
),
+ Binding::new(
+ "ctrl-alt-shift-right",
+ SelectToNextSubwordEnd,
+ Some("Editor"),
+ ),
+ Binding::new("ctrl-alt-shift-F", SelectToNextSubwordEnd, Some("Editor")),
Binding::new(
"ctrl-shift-A",
SelectToBeginningOfLine(true),
@@ -256,6 +286,8 @@ pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec<Box<dyn PathOpene
Binding::new("ctrl-w", SelectLargerSyntaxNode, Some("Editor")),
Binding::new("alt-down", SelectSmallerSyntaxNode, Some("Editor")),
Binding::new("ctrl-shift-W", SelectSmallerSyntaxNode, Some("Editor")),
+ Binding::new("cmd-u", UndoSelection, Some("Editor")),
+ Binding::new("cmd-shift-U", RedoSelection, Some("Editor")),
Binding::new("f8", GoToDiagnostic(Direction::Next), Some("Editor")),
Binding::new("shift-f8", GoToDiagnostic(Direction::Prev), Some("Editor")),
Binding::new("f2", Rename, Some("Editor")),
@@ -265,7 +297,7 @@ pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec<Box<dyn PathOpene
Binding::new("pageup", PageUp, Some("Editor")),
Binding::new("pagedown", PageDown, Some("Editor")),
Binding::new("alt-cmd-[", Fold, Some("Editor")),
- Binding::new("alt-cmd-]", Unfold, Some("Editor")),
+ Binding::new("alt-cmd-]", UnfoldLines, Some("Editor")),
Binding::new("alt-cmd-f", FoldSelectedRanges, Some("Editor")),
Binding::new("ctrl-space", ShowCompletions, Some("Editor")),
Binding::new("cmd-.", ToggleCodeActions(false), Some("Editor")),
@@ -281,10 +313,13 @@ pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec<Box<dyn PathOpene
cx.add_action(Editor::backspace);
cx.add_action(Editor::delete);
cx.add_action(Editor::tab);
+ cx.add_action(Editor::indent);
cx.add_action(Editor::outdent);
cx.add_action(Editor::delete_line);
- cx.add_action(Editor::delete_to_previous_word_boundary);
- cx.add_action(Editor::delete_to_next_word_boundary);
+ cx.add_action(Editor::delete_to_previous_word_start);
+ cx.add_action(Editor::delete_to_previous_subword_start);
+ cx.add_action(Editor::delete_to_next_word_end);
+ cx.add_action(Editor::delete_to_next_subword_end);
cx.add_action(Editor::delete_to_beginning_of_line);
cx.add_action(Editor::delete_to_end_of_line);
cx.add_action(Editor::cut_to_end_of_line);
@@ -300,8 +335,10 @@ pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec<Box<dyn PathOpene
cx.add_action(Editor::move_down);
cx.add_action(Editor::move_left);
cx.add_action(Editor::move_right);
- cx.add_action(Editor::move_to_previous_word_boundary);
- cx.add_action(Editor::move_to_next_word_boundary);
+ cx.add_action(Editor::move_to_previous_word_start);
+ cx.add_action(Editor::move_to_previous_subword_start);
+ cx.add_action(Editor::move_to_next_word_end);
+ cx.add_action(Editor::move_to_next_subword_end);
cx.add_action(Editor::move_to_beginning_of_line);
cx.add_action(Editor::move_to_end_of_line);
cx.add_action(Editor::move_to_beginning);
@@ -310,8 +347,10 @@ pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec<Box<dyn PathOpene
cx.add_action(Editor::select_down);
cx.add_action(Editor::select_left);
cx.add_action(Editor::select_right);
- cx.add_action(Editor::select_to_previous_word_boundary);
- cx.add_action(Editor::select_to_next_word_boundary);
+ cx.add_action(Editor::select_to_previous_word_start);
+ cx.add_action(Editor::select_to_previous_subword_start);
+ cx.add_action(Editor::select_to_next_word_end);
+ cx.add_action(Editor::select_to_next_subword_end);
cx.add_action(Editor::select_to_beginning_of_line);
cx.add_action(Editor::select_to_end_of_line);
cx.add_action(Editor::select_to_beginning);
@@ -326,12 +365,14 @@ pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec<Box<dyn PathOpene
cx.add_action(Editor::select_larger_syntax_node);
cx.add_action(Editor::select_smaller_syntax_node);
cx.add_action(Editor::move_to_enclosing_bracket);
+ cx.add_action(Editor::undo_selection);
+ cx.add_action(Editor::redo_selection);
cx.add_action(Editor::go_to_diagnostic);
cx.add_action(Editor::go_to_definition);
cx.add_action(Editor::page_up);
cx.add_action(Editor::page_down);
cx.add_action(Editor::fold);
- cx.add_action(Editor::unfold);
+ cx.add_action(Editor::unfold_lines);
cx.add_action(Editor::fold_selected_ranges);
cx.add_action(Editor::show_completions);
cx.add_action(Editor::toggle_code_actions);
@@ -341,14 +382,9 @@ pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec<Box<dyn PathOpene
cx.add_async_action(Editor::rename);
cx.add_async_action(Editor::confirm_rename);
cx.add_async_action(Editor::find_all_references);
-}
-trait SelectionExt {
- fn offset_range(&self, buffer: &MultiBufferSnapshot) -> Range<usize>;
- fn point_range(&self, buffer: &MultiBufferSnapshot) -> Range<Point>;
- fn display_range(&self, map: &DisplaySnapshot) -> Range<DisplayPoint>;
- fn spanned_rows(&self, include_end_if_at_line_start: bool, map: &DisplaySnapshot)
- -> Range<u32>;
+ workspace::register_project_item::<Editor>(cx);
+ workspace::register_followable_item::<Editor>(cx);
}
trait InvalidationRegion {
@@ -430,15 +466,14 @@ pub struct Editor {
columnar_selection_tail: Option<Anchor>,
add_selections_state: Option<AddSelectionsState>,
select_next_state: Option<SelectNextState>,
- selection_history:
- HashMap<TransactionId, (Arc<[Selection<Anchor>]>, Option<Arc<[Selection<Anchor>]>>)>,
+ selection_history: SelectionHistory,
autoclose_stack: InvalidationStack<BracketPairState>,
snippet_stack: InvalidationStack<SnippetState>,
select_larger_syntax_node_stack: Vec<Box<[Selection<usize>]>>,
active_diagnostics: Option<ActiveDiagnosticGroup>,
scroll_position: Vector2F,
- scroll_top_anchor: Option<Anchor>,
- autoscroll_request: Option<Autoscroll>,
+ scroll_top_anchor: Anchor,
+ autoscroll_request: Option<(Autoscroll, bool)>,
soft_wrap_mode_override: Option<settings::SoftWrap>,
get_field_editor_theme: Option<GetFieldEditorTheme>,
override_text_style: Option<Box<OverrideTextStyle>>,
@@ -463,6 +498,9 @@ pub struct Editor {
pending_rename: Option<RenameState>,
searchable: bool,
cursor_shape: CursorShape,
+ keymap_context_layers: BTreeMap<TypeId, gpui::keymap::Context>,
+ input_enabled: bool,
+ leader_replica_id: Option<u16>,
}
pub struct EditorSnapshot {
@@ -471,7 +509,7 @@ pub struct EditorSnapshot {
pub placeholder_text: Option<Arc<str>>,
is_focused: bool,
scroll_position: Vector2F,
- scroll_top_anchor: Option<Anchor>,
+ scroll_top_anchor: Anchor,
}
#[derive(Clone)]
@@ -480,11 +518,105 @@ pub struct PendingSelection {
mode: SelectMode,
}
+#[derive(Clone)]
+struct SelectionHistoryEntry {
+ selections: Arc<[Selection<Anchor>]>,
+ select_next_state: Option<SelectNextState>,
+ add_selections_state: Option<AddSelectionsState>,
+}
+
+enum SelectionHistoryMode {
+ Normal,
+ Undoing,
+ Redoing,
+}
+
+impl Default for SelectionHistoryMode {
+ fn default() -> Self {
+ Self::Normal
+ }
+}
+
+#[derive(Default)]
+struct SelectionHistory {
+ selections_by_transaction:
+ HashMap<TransactionId, (Arc<[Selection<Anchor>]>, Option<Arc<[Selection<Anchor>]>>)>,
+ mode: SelectionHistoryMode,
+ undo_stack: VecDeque<SelectionHistoryEntry>,
+ redo_stack: VecDeque<SelectionHistoryEntry>,
+}
+
+impl SelectionHistory {
+ fn insert_transaction(
+ &mut self,
+ transaction_id: TransactionId,
+ selections: Arc<[Selection<Anchor>]>,
+ ) {
+ self.selections_by_transaction
+ .insert(transaction_id, (selections, None));
+ }
+
+ fn transaction(
+ &self,
+ transaction_id: TransactionId,
+ ) -> Option<&(Arc<[Selection<Anchor>]>, Option<Arc<[Selection<Anchor>]>>)> {
+ self.selections_by_transaction.get(&transaction_id)
+ }
+
+ fn transaction_mut(
+ &mut self,
+ transaction_id: TransactionId,
+ ) -> Option<&mut (Arc<[Selection<Anchor>]>, Option<Arc<[Selection<Anchor>]>>)> {
+ 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<usize>,
}
+#[derive(Clone)]
struct SelectNextState {
query: AhoCorasick,
wordwise: bool,
@@ -802,6 +934,8 @@ pub struct NavigationData {
offset: usize,
}
+pub struct EditorCreated(pub ViewHandle<Editor>);
+
impl Editor {
pub fn single_line(
field_editor_style: Option<GetFieldEditorTheme>,
@@ -829,6 +963,15 @@ impl Editor {
}
pub fn for_buffer(
+ buffer: ModelHandle<Buffer>,
+ project: Option<ModelHandle<Project>>,
+ cx: &mut ViewContext<Self>,
+ ) -> 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<MultiBuffer>,
project: Option<ModelHandle<Project>>,
cx: &mut ViewContext<Self>,
@@ -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>) -> Self {
+ pub fn clone(&self, cx: &mut ViewContext<Self>) -> 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>,
) -> Self {
let display_map = cx.add_model(|cx| {
- let settings = cx.app_state::<Settings>();
+ let settings = cx.global::<Settings>();
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<Workspace>,
) {
- 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::<Settings>(),
+ cx.global::<Settings>(),
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<Arc<str>>,
@@ -1003,10 +1159,19 @@ impl Editor {
}
pub fn set_scroll_position(&mut self, scroll_position: Vector2F, cx: &mut ViewContext<Self>) {
+ self.set_scroll_position_internal(scroll_position, true, cx);
+ }
+
+ fn set_scroll_position_internal(
+ &mut self,
+ scroll_position: Vector2F,
+ local: bool,
+ cx: &mut ViewContext<Self>,
+ ) {
let map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
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>,
+ ) {
+ 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>) {
+ self.display_map
+ .update(cx, |map, _| map.clip_at_line_ends = clip);
+ }
+
+ pub fn set_keymap_context_layer<Tag: 'static>(&mut self, context: gpui::keymap::Context) {
+ self.keymap_context_layers
+ .insert(TypeId::of::<Tag>(), context);
+ }
+
+ pub fn remove_keymap_context_layer<Tag: 'static>(&mut self) {
+ self.keymap_context_layers.remove(&TypeId::of::<Tag>());
+ }
+
+ pub fn set_input_enabled(&mut self, input_enabled: bool) {
+ self.input_enabled = input_enabled;
+ }
+
pub fn scroll_position(&self, cx: &mut ViewContext<Self>) -> 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<Self>,
+ move_selection: impl Fn(&DisplaySnapshot, &mut Selection<DisplayPoint>),
+ ) {
+ let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+ let selections = self
+ .local_selections::<Point>(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<Self>,
+ 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<Self>,
+ 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>) {
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::<usize>(&cx);
@@ -1546,7 +1805,7 @@ impl Editor {
#[cfg(any(test, feature = "test-support"))]
pub fn selected_ranges<D: TextDimension + Ord + Sub<D, Output = D>>(
&self,
- cx: &mut MutableAppContext,
+ cx: &AppContext,
) -> Vec<Range<D>> {
self.local_selections::<D>(cx)
.iter()
@@ -1645,136 +1904,142 @@ impl Editor {
}
pub fn handle_input(&mut self, action: &Input, cx: &mut ViewContext<Self>) {
+ 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>) {
- self.start_transaction(cx);
- let mut old_selections = SmallVec::<[_; 32]>::new();
- {
- let selections = self.local_selections::<usize>(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::<usize>();
-
- let trailing_whitespace_len = buffer
- .chars_at(end)
- .take_while(|c| c.is_whitespace() && *c != '\n')
- .map(|c| c.len_utf8())
- .sum::<usize>();
-
- 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::<usize>(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::<usize>();
+
+ let trailing_whitespace_len = buffer
+ .chars_at(end)
+ .take_while(|c| c.is_whitespace() && *c != '\n')
+ .map(|c| c.len_utf8())
+ .sum::<usize>();
+
+ 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<PendingEdit> = 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<PendingEdit> = 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,
@@ -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<DisplayPoint>, Color)>,
- selections: HashMap<ReplicaId, Vec<text::Selection<DisplayPoint>>>,
+ selections: Vec<(ReplicaId, Vec<text::Selection<DisplayPoint>>)>,
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)
@@ -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<Buffer>);
-
-#[derive(Clone)]
-struct WeakBufferItemHandle(WeakModelHandle<Buffer>);
-
-#[derive(Clone)]
-pub struct MultiBufferItemHandle(pub ModelHandle<MultiBuffer>);
-
-#[derive(Clone)]
-struct WeakMultiBufferItemHandle(WeakModelHandle<MultiBuffer>);
+impl FollowableItem for Editor {
+ fn from_state_proto(
+ pane: ViewHandle<workspace::Pane>,
+ project: ModelHandle<Project>,
+ state: &mut Option<proto::view::Variant>,
+ cx: &mut MutableAppContext,
+ ) -> Option<Task<Result<ViewHandle<Self>>>> {
+ let state = if matches!(state, Some(proto::view::Variant::Editor(_))) {
+ if let Some(proto::view::Variant::Editor(state)) = state.take() {
+ state
+ } else {
+ unreachable!()
+ }
+ } else {
+ return None;
+ };
-impl PathOpener for BufferOpener {
- fn open(
- &self,
- project: &mut Project,
- project_path: ProjectPath,
- cx: &mut ModelContext<Project>,
- ) -> Option<Task<Result<Box<dyn ItemHandle>>>> {
- 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<dyn ItemHandle>)
+ 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<RefCell<NavHistory>>,
- cx: &mut MutableAppContext,
- ) -> Box<dyn ItemViewHandle> {
- 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::<Self>().find(|editor| {
+ editor.read(cx).buffer.read(cx).as_singleton().as_ref() == Some(&buffer)
+ })
+ })
+ .unwrap_or_else(|| {
+ cx.add_view(pane.window_id(), |cx| {
+ Editor::for_buffer(buffer, Some(project), cx)
+ })
+ });
+ editor.update(&mut cx, |editor, cx| {
+ let excerpt_id;
+ let buffer_id;
+ {
+ let buffer = editor.buffer.read(cx).read(cx);
+ let singleton = buffer.as_singleton().unwrap();
+ excerpt_id = singleton.0.clone();
+ buffer_id = singleton.1;
+ }
+ let selections = state
+ .selections
+ .into_iter()
+ .map(|selection| {
+ deserialize_selection(&excerpt_id, buffer_id, selection)
+ .ok_or_else(|| anyhow!("invalid selection"))
+ })
+ .collect::<Result<Vec<_>>>()?;
+ if !selections.is_empty() {
+ editor.set_selections_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<dyn ItemHandle> {
- Box::new(self.clone())
- }
-
- fn to_any(&self) -> gpui::AnyModelHandle {
- self.0.clone().into()
- }
-
- fn downgrade(&self) -> Box<dyn workspace::WeakItemHandle> {
- Box::new(WeakBufferItemHandle(self.0.downgrade()))
- }
-
- fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
- 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<u16>,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.leader_replica_id = leader_replica_id;
+ if self.leader_replica_id.is_some() {
+ self.buffer.update(cx, |buffer, cx| {
+ buffer.remove_active_selections(cx);
+ });
+ } else {
+ self.buffer.update(cx, |buffer, cx| {
+ if self.focused {
+ buffer.set_active_selections(&self.selections, cx);
+ }
+ });
+ }
+ cx.notify();
}
-}
-impl ItemHandle for MultiBufferItemHandle {
- fn add_view(
- &self,
- window_id: usize,
- workspace: &Workspace,
- nav_history: Rc<RefCell<NavHistory>>,
- cx: &mut MutableAppContext,
- ) -> Box<dyn ItemViewHandle> {
- 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<proto::view::Variant> {
+ let buffer_id = self.buffer.read(cx).as_singleton()?.read(cx).remote_id();
+ Some(proto::view::Variant::Editor(proto::view::Editor {
+ buffer_id,
+ scroll_top_anchor: Some(language::proto::serialize_anchor(
+ &self.scroll_top_anchor.text_anchor,
+ )),
+ scroll_x: self.scroll_position.x(),
+ scroll_y: self.scroll_position.y(),
+ selections: self.selections.iter().map(serialize_selection).collect(),
}))
}
- fn boxed_clone(&self) -> Box<dyn ItemHandle> {
- Box::new(self.clone())
- }
-
- fn to_any(&self) -> gpui::AnyModelHandle {
- self.0.clone().into()
- }
-
- fn downgrade(&self) -> Box<dyn WeakItemHandle> {
- Box::new(WeakMultiBufferItemHandle(self.0.downgrade()))
+ fn add_event_to_update_proto(
+ &self,
+ event: &Self::Event,
+ update: &mut Option<proto::update_view::Variant>,
+ _: &AppContext,
+ ) -> bool {
+ let update =
+ update.get_or_insert_with(|| proto::update_view::Variant::Editor(Default::default()));
+
+ match update {
+ proto::update_view::Variant::Editor(update) => match event {
+ Event::ScrollPositionChanged { .. } => {
+ update.scroll_top_anchor = Some(language::proto::serialize_anchor(
+ &self.scroll_top_anchor.text_anchor,
+ ));
+ update.scroll_x = self.scroll_position.x();
+ update.scroll_y = self.scroll_position.y();
+ true
+ }
+ Event::SelectionsChanged { .. } => {
+ update.selections = self
+ .selections
+ .iter()
+ .chain(self.pending_selection.as_ref().map(|p| &p.selection))
+ .map(serialize_selection)
+ .collect();
+ true
+ }
+ _ => false,
+ },
+ }
}
- fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
- None
+ fn apply_update_proto(
+ &mut self,
+ message: update_view::Variant,
+ cx: &mut ViewContext<Self>,
+ ) -> Result<()> {
+ match message {
+ update_view::Variant::Editor(message) => {
+ let buffer = self.buffer.read(cx);
+ let buffer = buffer.read(cx);
+ let (excerpt_id, buffer_id, _) = buffer.as_singleton().unwrap();
+ let excerpt_id = excerpt_id.clone();
+ drop(buffer);
+
+ let selections = message
+ .selections
+ .into_iter()
+ .filter_map(|selection| {
+ deserialize_selection(&excerpt_id, buffer_id, selection)
+ })
+ .collect::<Vec<_>>();
+
+ if !selections.is_empty() {
+ self.set_selections_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<Box<dyn ItemHandle>> {
- self.0
- .upgrade(cx)
- .map(|buffer| Box::new(BufferItemHandle(buffer)) as Box<dyn ItemHandle>)
- }
-
- fn id(&self) -> usize {
- self.0.id()
+fn serialize_selection(selection: &Selection<Anchor>) -> proto::Selection {
+ proto::Selection {
+ id: selection.id as u64,
+ start: Some(language::proto::serialize_anchor(
+ &selection.start.text_anchor,
+ )),
+ end: Some(language::proto::serialize_anchor(
+ &selection.end.text_anchor,
+ )),
+ reversed: selection.reversed,
}
}
-impl WeakItemHandle for WeakMultiBufferItemHandle {
- fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
- self.0
- .upgrade(cx)
- .map(|buffer| Box::new(MultiBufferItemHandle(buffer)) as Box<dyn ItemHandle>)
- }
-
- fn id(&self) -> usize {
- self.0.id()
- }
+fn deserialize_selection(
+ excerpt_id: &ExcerptId,
+ buffer_id: usize,
+ selection: proto::Selection,
+) -> Option<Selection<Anchor>> {
+ Some(Selection {
+ id: selection.id as usize,
+ start: Anchor {
+ buffer_id: Some(buffer_id),
+ excerpt_id: excerpt_id.clone(),
+ text_anchor: language::proto::deserialize_anchor(selection.start?)?,
+ },
+ end: Anchor {
+ buffer_id: Some(buffer_id),
+ excerpt_id: excerpt_id.clone(),
+ text_anchor: language::proto::deserialize_anchor(selection.end?)?,
+ },
+ reversed: selection.reversed,
+ goal: SelectionGoal::None,
+ })
}
-impl ItemView for Editor {
- fn item(&self, cx: &AppContext) -> Box<dyn ItemHandle> {
- 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<dyn std::any::Any>, cx: &mut ViewContext<Self>) {
+impl Item for Editor {
+ fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
if let Some(data) = data.downcast_ref::<NavigationData>() {
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::<usize>(&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<Self>,
- ) -> Option<Self>
+ fn project_entry_id(&self, cx: &AppContext) -> Option<ProjectEntryId> {
+ File::from_dyn(self.buffer().read(cx).file(cx)).and_then(|file| file.project_entry_id(cx))
+ }
+
+ fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
where
Self: Sized,
{
- Some(self.clone(nav_history, cx))
+ Some(self.clone(cx))
+ }
+
+ fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext<Self>) {
+ self.nav_history = Some(history);
}
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
@@ -219,9 +320,17 @@ impl ItemView for Editor {
) -> Task<Result<()>> {
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<Project>,
+ buffer: ModelHandle<Buffer>,
+ cx: &mut ViewContext<Self>,
+ ) -> Self {
+ Self::for_buffer(buffer, Some(project), cx)
+ }
+}
+
pub struct CursorPosition {
position: Option<Point>,
selected_count: usize,
@@ -322,7 +443,7 @@ impl View for CursorPosition {
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
if let Some(position) = self.position {
- let theme = &cx.app_state::<Settings>().theme.workspace.status_bar;
+ let theme = &cx.global::<Settings>().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<Self>,
) {
if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
@@ -395,7 +516,7 @@ impl View for DiagnosticMessage {
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
if let Some(diagnostic) = &self.diagnostic {
- let theme = &cx.app_state::<Settings>().theme.workspace.status_bar;
+ let theme = &cx.global::<Settings>().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<Self>,
) {
if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
@@ -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<DisplayPoint> {
+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<DisplayPoint> {
+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<DisplayPo
*point.row_mut() += 1;
*point.column_mut() = 0;
}
- Ok(map.clip_point(point, Bias::Right))
+ map.clip_point(point, Bias::Right)
}
pub fn up(
map: &DisplaySnapshot,
start: DisplayPoint,
goal: SelectionGoal,
-) -> 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),
- );
- }
}
@@ -211,7 +211,7 @@ impl MultiBuffer {
pub fn singleton(buffer: ModelHandle<Buffer>, cx: &mut ModelContext<Self>) -> 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<Buffer>) -> Vec<ExcerptId> {
+ pub fn excerpts_for_buffer(
+ &self,
+ buffer: &ModelHandle<Buffer>,
+ cx: &AppContext,
+ ) -> Vec<(ExcerptId, Range<text::Anchor>)> {
+ let mut excerpts = Vec::new();
+ let snapshot = self.read(cx);
+ let buffers = self.buffers.borrow();
+ let mut cursor = snapshot.excerpts.cursor::<Option<&ExcerptId>>();
+ 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<ExcerptId> {
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);
@@ -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<Ordering> {
+ 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<Anchor>, buffer: &MultiBufferSnapshot) -> Result<Ordering>;
+ fn cmp(&self, b: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> Ordering;
fn to_offset(&self, content: &MultiBufferSnapshot) -> Range<usize>;
fn to_point(&self, content: &MultiBufferSnapshot) -> Range<Point>;
}
impl AnchorRangeExt for Range<Anchor> {
- fn cmp(&self, other: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> Result<Ordering> {
- Ok(match self.start.cmp(&other.start, buffer)? {
- Ordering::Equal => other.end.cmp(&self.end, buffer)?,
+ fn cmp(&self, other: &Range<Anchor>, 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<usize> {
@@ -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<DisplayPoint>) {
+ 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)
+}
@@ -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"
@@ -67,7 +67,7 @@ impl View for FileFinder {
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
- let settings = cx.app_state::<Settings>();
+ let settings = cx.global::<Settings>();
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::<Settings>();
+ let settings = cx.global::<Settings>();
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::<Settings>();
+ let settings = cx.global::<Settings>();
let style = if index == selected_index {
&settings.theme.selector.active_item
} else {
@@ -291,7 +291,7 @@ impl FileFinder {
cx: &mut ViewContext<Self>,
) {
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()
@@ -59,7 +59,8 @@ impl GoToLine {
}
fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
- if let Some(editor) = workspace.active_item(cx)
+ if let Some(editor) = workspace
+ .active_item(cx)
.and_then(|active_item| active_item.downcast::<Editor>())
{
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::<u32>().ok());
@@ -148,7 +149,7 @@ impl View for GoToLine {
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
- let theme = &cx.app_state::<Settings>().theme.selector;
+ let theme = &cx.global::<Settings>().theme.selector;
let label = format!(
"{},{} of {} lines",
@@ -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<T: View>(&self, handle: &WeakViewHandle<T>) -> Option<ViewHandle<T>>;
+
+ fn upgrade_any_view_handle(&self, handle: &AnyWeakViewHandle) -> Option<AnyViewHandle>;
}
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<usize>,
- keystroke: &Keystroke,
- ) -> Result<bool> {
- let mut state = self.cx.borrow_mut();
- state.dispatch_keystroke(window_id, responder_chain, keystroke)
+ keystroke: Keystroke,
+ input: Option<String>,
+ 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<T, F>(&mut self, build_model: F) -> ModelHandle<T>
@@ -494,7 +522,7 @@ impl TestAppContext {
pub fn update<T, F: FnOnce(&mut MutableAppContext) -> 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<T, F>(&mut self, window_id: usize, build_view: F) -> ViewHandle<T>
+ where
+ T: View,
+ F: FnOnce(&mut ViewContext<T>) -> T,
+ {
+ self.update(|cx| cx.add_view(window_id, build_view))
+ }
+
pub fn platform(&self) -> Arc<dyn Platform> {
self.0.borrow().platform()
}
@@ -639,6 +675,10 @@ impl UpgradeViewHandle for AsyncAppContext {
fn upgrade_view_handle<T: View>(&self, handle: &WeakViewHandle<T>) -> Option<ViewHandle<T>> {
self.0.borrow_mut().upgrade_view_handle(handle)
}
+
+ fn upgrade_any_view_handle(&self, handle: &AnyWeakViewHandle) -> Option<AnyViewHandle> {
+ self.0.borrow_mut().upgrade_any_view_handle(handle)
+ }
}
impl ReadModelWith for AsyncAppContext {
@@ -742,6 +782,7 @@ type GlobalActionCallback = dyn FnMut(&dyn AnyAction, &mut MutableAppContext);
type SubscriptionCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext) -> bool>;
type GlobalSubscriptionCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext)>;
type ObservationCallback = Box<dyn FnMut(&mut MutableAppContext) -> bool>;
+type GlobalObservationCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext)>;
type ReleaseObservationCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext)>;
pub struct MutableAppContext {
@@ -761,12 +802,15 @@ pub struct MutableAppContext {
global_subscriptions:
Arc<Mutex<HashMap<TypeId, BTreeMap<usize, Option<GlobalSubscriptionCallback>>>>>,
observations: Arc<Mutex<HashMap<usize, BTreeMap<usize, Option<ObservationCallback>>>>>,
+ global_observations:
+ Arc<Mutex<HashMap<TypeId, BTreeMap<usize, Option<GlobalObservationCallback>>>>>,
release_observations: Arc<Mutex<HashMap<usize, BTreeMap<usize, ReleaseObservationCallback>>>>,
presenters_and_platform_windows:
HashMap<usize, (Rc<RefCell<Presenter>>, Box<dyn platform::Window>)>,
foreground: Rc<executor::Foreground>,
pending_effects: VecDeque<Effect>,
pending_notifications: HashSet<usize>,
+ pending_global_notifications: HashSet<TypeId>,
pending_flushes: usize,
flushing_effects: bool,
next_cursor_style_handle_id: Arc<AtomicUsize>,
@@ -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::<E>();
- 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<E>,
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<E>,
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<G, F>(&mut self, mut observe: F) -> Subscription
+ where
+ G: Any,
+ F: 'static + FnMut(&G, &mut MutableAppContext),
+ {
+ let type_id = TypeId::of::<G>();
+ 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<dyn FnOnce(&mut MutableAppContext)>) {
- 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<A: Action>(
&mut self,
window_id: usize,
@@ -1322,17 +1405,15 @@ impl MutableAppContext {
window_id: usize,
responder_chain: Vec<usize>,
keystroke: &Keystroke,
- ) -> Result<bool> {
+ ) -> 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<T: 'static>(&mut self, state: T) {
- self.cx
- .app_states
- .insert(TypeId::of::<T>(), Box::new(state));
+ pub fn default_global<T: 'static + Default>(&mut self) -> &T {
+ let type_id = TypeId::of::<T>();
+ 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<T: 'static, F, U>(&mut self, update: F) -> U
+ pub fn set_global<T: 'static>(&mut self, state: T) {
+ self.update(|this| {
+ let type_id = TypeId::of::<T>();
+ this.cx.globals.insert(type_id, Box::new(state));
+ this.notify_global(type_id);
+ });
+ }
+
+ pub fn update_default_global<T, F, U>(&mut self, update: F) -> U
where
+ T: 'static + Default,
F: FnOnce(&mut T, &mut MutableAppContext) -> U,
{
- let type_id = TypeId::of::<T>();
- 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::<T>();
+ 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<T, F, U>(&mut self, update: F) -> U
+ where
+ T: 'static,
+ F: FnOnce(&mut T, &mut MutableAppContext) -> U,
+ {
+ self.update(|this| {
+ let type_id = TypeId::of::<T>();
+ 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<T, F>(&mut self, build_model: F) -> ModelHandle<T>
@@ -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<dyn Any>) {
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<dyn Any>) {
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<T: View>(&self, handle: &WeakViewHandle<T>) -> Option<ViewHandle<T>> {
self.cx.upgrade_view_handle(handle)
}
+
+ fn upgrade_any_view_handle(&self, handle: &AnyWeakViewHandle) -> Option<AnyViewHandle> {
+ self.cx.upgrade_any_view_handle(handle)
+ }
}
impl ReadView for MutableAppContext {
@@ -2039,7 +2307,7 @@ pub struct AppContext {
models: HashMap<usize, Box<dyn AnyModel>>,
views: HashMap<(usize, usize), Box<dyn AnyView>>,
windows: HashMap<usize, Window>,
- app_states: HashMap<TypeId, Box<dyn Any>>,
+ globals: HashMap<TypeId, Box<dyn Any>>,
element_states: HashMap<ElementStateId, Box<dyn Any>>,
background: Arc<executor::Background>,
ref_counts: Arc<Mutex<RefCounts>>,
@@ -2072,8 +2340,12 @@ impl AppContext {
&self.platform
}
- pub fn app_state<T: 'static>(&self) -> &T {
- self.app_states
+ pub fn has_global<T: 'static>(&self) -> bool {
+ self.globals.contains_key(&TypeId::of::<T>())
+ }
+
+ pub fn global<T: 'static>(&self) -> &T {
+ self.globals
.get(&TypeId::of::<T>())
.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<AnyViewHandle> {
+ if self.ref_counts.lock().is_entity_alive(handle.view_id) {
+ Some(AnyViewHandle::new(
+ handle.window_id,
+ handle.view_id,
+ handle.view_type,
+ self.ref_counts.clone(),
+ ))
+ } else {
+ None
+ }
+ }
}
impl ReadView for AppContext {
@@ -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<dyn Any>,
},
+ GlobalSubscription {
+ type_id: TypeId,
+ subscription_id: usize,
+ callback: GlobalSubscriptionCallback,
+ },
GlobalEvent {
payload: Box<dyn Any>,
},
+ 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<dyn FnOnce(&mut MutableAppContext)>),
+ Deferred {
+ callback: Box<dyn FnOnce(&mut MutableAppContext)>,
+ after_window_update: bool,
+ },
+ GlobalNotification {
+ type_id: TypeId,
+ },
ModelRelease {
model_id: usize,
model: Box<dyn AnyModel>,
@@ -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<T>)) {
+ 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<T>)) {
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<T>),
+ ) {
+ let handle = self.handle();
+ self.app.after_window_update(move |cx| {
+ handle.update(cx, |view, cx| {
+ callback(view, cx);
+ })
+ })
}
pub fn propagate_action(&mut self) {
@@ -2892,6 +3250,10 @@ impl<V> UpgradeViewHandle for ViewContext<'_, V> {
fn upgrade_view_handle<T: View>(&self, handle: &WeakViewHandle<T>) -> Option<ViewHandle<T>> {
self.cx.upgrade_view_handle(handle)
}
+
+ fn upgrade_any_view_handle(&self, handle: &AnyWeakViewHandle) -> Option<AnyViewHandle> {
+ self.cx.upgrade_any_view_handle(handle)
+ }
}
impl<V: View> UpdateModel for ViewContext<'_, V> {
@@ -3338,9 +3700,9 @@ impl<T: View> ViewHandle<T> {
F: 'static + FnOnce(&mut T, &mut ViewContext<T>),
{
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 {
@@ -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()
@@ -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());
}
}
}
@@ -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
@@ -20,7 +20,7 @@ use std::{
pub struct Presenter {
window_id: usize,
- rendered_views: HashMap<usize, ElementBox>,
+ pub(crate) rendered_views: HashMap<usize, ElementBox>,
parents: HashMap<usize, usize>,
font_cache: Arc<FontCache>,
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<WindowInvalidation>,
- 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<T: View>(&self, handle: &WeakViewHandle<T>) -> Option<ViewHandle<T>> {
self.app.upgrade_view_handle(handle)
}
+
+ fn upgrade_any_view_handle(&self, handle: &crate::AnyWeakViewHandle) -> Option<AnyViewHandle> {
+ self.app.upgrade_any_view_handle(handle)
+ }
}
impl<'a> ElementStateContext for LayoutContext<'a> {
@@ -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<Anchor>) -> impl Iterator<Item = (ReplicaId, impl Iterator<Item = &Selection<Anchor>>)>
- pub fn remote_selections_in_range(&self, Range<Anchor>) -> impl Iterator<Item = (ReplicaId, i
- */
-
pub fn remote_selections_in_range<'a>(
&'a self,
range: Range<Anchor>,
@@ -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
@@ -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<Anchor> {
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();
@@ -100,15 +100,16 @@ pub fn serialize_undo_map_entry(
}
pub fn serialize_selections(selections: &Arc<[Selection<Anchor>]>) -> Vec<proto::Selection> {
- 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<Anchor>) -> 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<proto::Selection>) -> 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::<Vec<_>>(),
)
}
+pub fn deserialize_selection(selection: proto::Selection) -> Option<Selection<Anchor>> {
+ 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<proto::Diagnostic>,
) -> Arc<[DiagnosticEntry<Anchor>]> {
@@ -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<usize> {
+ let start = text.find(part).unwrap();
+ start..start
+ }
+
+ fn range_of(text: &str, part: &str) -> Range<usize> {
+ 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::<Vec<_>>()))
.collect::<Vec<_>>();
let expected_remote_selections = active_selections
@@ -556,7 +556,14 @@ type FakeLanguageServerHandlers = Arc<
Mutex<
HashMap<
&'static str,
- Box<dyn Send + FnMut(usize, &[u8], gpui::AsyncAppContext) -> Vec<u8>>,
+ Box<
+ dyn Send
+ + FnMut(
+ usize,
+ &[u8],
+ gpui::AsyncAppContext,
+ ) -> futures::future::BoxFuture<'static, Vec<u8>>,
+ >,
>,
>,
>;
@@ -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::<request::Initialize, _>({
+ fake.handle_request::<request::Initialize, _, _>({
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<T, F>(
+ pub fn handle_request<T, F, Fut>(
&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<Output = T::Result>,
{
+ 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::<T::Params>(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::<request::Shutdown, _>(|_, _| ());
+ fake.handle_request::<request::Shutdown, _, _>(|_, _| async move {});
drop(server);
fake.receive_notification::<notification::Exit>().await;
@@ -69,7 +69,7 @@ impl View for OutlineView {
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
- let settings = cx.app_state::<Settings>();
+ let settings = cx.global::<Settings>();
Flex::new(Axis::Vertical)
.with_child(
@@ -124,9 +124,12 @@ impl OutlineView {
.active_item(cx)
.and_then(|item| item.downcast::<Editor>())
{
- let buffer = editor.read(cx).buffer().read(cx).read(cx).outline(Some(
- cx.app_state::<Settings>().theme.editor.syntax.as_ref(),
- ));
+ let buffer = editor
+ .read(cx)
+ .buffer()
+ .read(cx)
+ .read(cx)
+ .outline(Some(cx.global::<Settings>().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::<Settings>();
+ let settings = cx.global::<Settings>();
return Container::new(
Label::new(
"No matches".into(),
@@ -330,7 +333,7 @@ impl OutlineView {
index: usize,
cx: &AppContext,
) -> ElementBox {
- let settings = cx.app_state::<Settings>();
+ let settings = cx.global::<Settings>();
let style = if index == self.selected_match_index {
&settings.theme.selector.active_item
} else {
@@ -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<ProjectEntryId>;
+}
+
pub struct Project {
worktrees: Vec<WorktreeHandle>,
- active_entry: Option<ProjectEntry>,
+ active_entry: Option<ProjectEntryId>,
languages: Arc<LanguageRegistry>,
language_servers: HashMap<(WorktreeId, Arc<str>), Arc<LanguageServer>>,
started_language_servers: HashMap<(WorktreeId, Arc<str>), Task<Option<Arc<LanguageServer>>>>,
@@ -114,12 +118,14 @@ pub struct Collaborator {
#[derive(Clone, Debug, PartialEq)]
pub enum Event {
- ActiveEntryChanged(Option<ProjectEntry>),
+ ActiveEntryChanged(Option<ProjectEntryId>),
WorktreeRemoved(WorktreeId),
DiskBasedDiagnosticsStarted,
DiskBasedDiagnosticsUpdated,
DiskBasedDiagnosticsFinished,
DiagnosticsUpdated(ProjectPath),
+ RemoteIdChanged(Option<u64>),
+ 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>) {
- 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::<GetDefinition>);
- client.add_entity_request_handler(Self::handle_lsp_command::<GetDocumentHighlights>);
- client.add_entity_request_handler(Self::handle_lsp_command::<GetReferences>);
- client.add_entity_request_handler(Self::handle_lsp_command::<PrepareRename>);
- client.add_entity_request_handler(Self::handle_lsp_command::<PerformRename>);
- 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::<GetDefinition>);
+ client.add_model_request_handler(Self::handle_lsp_command::<GetDocumentHighlights>);
+ client.add_model_request_handler(Self::handle_lsp_command::<GetReferences>);
+ client.add_model_request_handler(Self::handle_lsp_command::<PrepareRename>);
+ client.add_model_request_handler(Self::handle_lsp_command::<PerformRename>);
+ 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::<Vec<_>>() {
- 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<dyn Fs>,
cx: &mut AsyncAppContext,
) -> Result<ModelHandle<Self>> {
- 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<ModelHandle<Buffer>> {
self.opened_buffers
.get(&remote_id)
@@ -537,16 +538,54 @@ impl Project {
&self.fs
}
- fn set_remote_id(&mut self, remote_id: Option<u64>, cx: &mut ModelContext<Self>) {
+ fn unregister(&mut self, cx: &mut ModelContext<Self>) {
+ 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<Self>) -> Task<Result<()>> {
+ 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<u64> {
@@ -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<ModelHandle<Worktree>> {
+ 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<WorktreeId> {
+ self.worktree_for_entry(entry_id, cx)
+ .map(|worktree| worktree.read(cx).id())
+ }
+
pub fn share(&self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
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<Self>) -> Task<Result<()>> {
+ pub fn unshare(&mut self, cx: &mut ModelContext<Self>) {
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::<Vec<_>>() {
- 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<Self>) {
@@ -785,6 +834,23 @@ impl Project {
Ok(buffer)
}
+ pub fn open_path(
+ &mut self,
+ path: impl Into<ProjectPath>,
+ cx: &mut ModelContext<Self>,
+ ) -> Task<Result<(ProjectEntryId, AnyModelHandle)>> {
+ 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<ProjectPath>,
@@ -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<Self>,
+ ) -> Task<Result<ModelHandle<Buffer>>> {
+ if let Some(buffer) = self.buffer_for_id(id, cx) {
+ Task::ready(Ok(buffer))
+ } else if self.is_local() {
+ Task::ready(Err(anyhow!("buffer {} does not exist", id)))
+ } else if let Some(project_id) = self.remote_id() {
+ let request = self
+ .client
+ .request(proto::OpenBufferById { project_id, id });
+ cx.spawn(|this, mut cx| async move {
+ let buffer = request
+ .await?
+ .buffer
+ .ok_or_else(|| anyhow!("invalid buffer"))?;
+ this.update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx))
+ .await
+ })
+ } else {
+ Task::ready(Err(anyhow!("cannot open buffer while disconnected")))
+ }
+ }
+
pub fn save_buffer_as(
&mut self,
buffer: ModelHandle<Buffer>,
@@ -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::<PointUtf16>(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::<PointUtf16>(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<ProjectEntry> {
+ pub fn active_entry(&self) -> Option<ProjectEntryId> {
self.active_entry
}
+ pub fn entry_for_path(&self, path: &ProjectPath, cx: &AppContext) -> Option<ProjectEntryId> {
+ 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<ProjectPath> {
+ 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<Self>,
+ envelope: TypedEnvelope<proto::OpenBufferById>,
+ _: Arc<Client>,
+ mut cx: AsyncAppContext,
+ ) -> Result<proto::OpenBufferResponse> {
+ let peer_id = envelope.original_sender_id()?;
+ let buffer = this
+ .update(&mut cx, |this, cx| {
+ this.open_buffer_by_id(envelope.payload.id, cx)
+ })
+ .await?;
+ this.update(&mut cx, |this, cx| {
+ Ok(proto::OpenBufferResponse {
+ buffer: Some(this.serialize_buffer_for_peer(&buffer, peer_id, cx)),
+ })
+ })
+ }
+
+ async fn handle_open_buffer_by_path(
this: ModelHandle<Self>,
- envelope: TypedEnvelope<proto::OpenBuffer>,
+ envelope: TypedEnvelope<proto::OpenBufferByPath>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<proto::OpenBufferResponse> {
@@ -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<ProjectEntryId> {
+ 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::<lsp::notification::DidChangeTextDocument>();
+ let change_notification_2 = fake_server
+ .receive_notification::<lsp::notification::DidChangeTextDocument>()
+ .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::notification::PublishDiagnostics>(
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::<lsp::request::GotoDefinition, _>(move |params, _| {
+ fake_server.handle_request::<lsp::request::GotoDefinition, _, _>(|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::<lsp::request::PrepareRenameRequest, _>(|params, _| {
+ .handle_request::<lsp::request::PrepareRenameRequest, _, _>(|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::<lsp::request::Rename, _>(|params, _| {
+ .handle_request::<lsp::request::Rename, _, _>(|params, _| async move {
assert_eq!(
params.text_document_position.text_document.uri.as_str(),
"file:///dir/one.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<Path>,
scan_id: usize,
ignores: HashMap<Arc<Path>, (Arc<Gitignore>, usize)>,
- removed_entry_ids: HashMap<u64, usize>,
+ removed_entry_ids: HashMap<u64, ProjectEntryId>,
next_entry_id: Arc<AtomicUsize>,
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<Worktree>,
pub path: Arc<Path>,
pub mtime: SystemTime,
- pub(crate) entry_id: Option<usize>,
+ pub(crate) entry_id: Option<ProjectEntryId>,
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<ProjectEntryId> {
+ self.entry_id
+ }
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Entry {
- pub id: usize,
+ pub id: ProjectEntryId,
pub kind: EntryKind,
pub path: Arc<Path>,
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<Path>,
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<Path> = 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,
@@ -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<Project>,
list: UniformListState,
visible_entries: Vec<(WorktreeId, Vec<usize>)>,
- expanded_dir_ids: HashMap<WorktreeId, Vec<usize>>,
+ expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>,
selection: Option<Selection>,
handle: WeakViewHandle<Self>,
}
@@ -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<Self>) {
- 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<Self>) {
- 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<Self>) {
@@ -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<Self>,
) {
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<Self>,
) {
let project = self.project.read(cx);
@@ -411,7 +397,7 @@ impl ProjectPanel {
&self,
range: Range<usize>,
cx: &mut ViewContext<ProjectPanel>,
- mut callback: impl FnMut(ProjectEntry, EntryDetails, &mut ViewContext<ProjectPanel>),
+ mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut ViewContext<ProjectPanel>),
) {
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<Self>,
) -> ElementBox {
let is_dir = details.is_dir;
- MouseEventHandler::new::<Self, _, _>(entry.entry_id, cx, |state, _| {
+ MouseEventHandler::new::<Self, _, _>(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::<Settings>().theme.project_panel;
+ let theme = &cx.global::<Settings>().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::<Settings>().theme.clone();
+ let theme = cx.global::<Settings>().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;
}
}
@@ -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<Self>) -> ElementBox {
- let settings = cx.app_state::<Settings>();
+ let settings = cx.global::<Settings>();
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::<Settings>();
+ let settings = cx.global::<Settings>();
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::<Settings>();
+ let settings = cx.global::<Settings>();
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::<Editor>()
- .unwrap();
+
+ let editor = workspace.open_project_item::<Editor>(buffer, cx);
editor.update(cx, |editor, cx| {
editor.select_ranges(
[position..position],
@@ -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;
@@ -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<Self> {
@@ -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,
@@ -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;
@@ -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<Self>) -> ElementBox {
- let theme = cx.app_state::<Settings>().theme.clone();
+ let theme = cx.global::<Settings>().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<Box<dyn ItemViewHandle>>,
+ item: Option<Box<dyn ItemHandle>>,
cx: &mut ViewContext<Self>,
) -> 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::<Self, _, _>(search_option as usize, cx, |state, cx| {
- let theme = &cx.app_state::<Settings>().theme.search;
+ let theme = &cx.global::<Settings>().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::<NavButton, _, _>(direction as usize, cx, |state, cx| {
- let theme = &cx.app_state::<Settings>().theme.search;
+ let theme = &cx.global::<Settings>().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<Self>,
) {
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<Self>,
) {
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::<Settings>().theme.search;
+ let theme = &cx.global::<Settings>().theme.search;
editor.highlight_background::<Self>(
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
@@ -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<Project>, WeakModelHandle<ProjectSearch>>);
+struct ActiveSearches(HashMap<WeakModelHandle<Project>, WeakViewHandle<ProjectSearchView>>);
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<Self>,
- _: &Workspace,
- nav_history: ItemNavHistory,
- cx: &mut gpui::ViewContext<Self::View>,
- ) -> Self::View {
- ProjectSearchView::new(model, Some(nav_history), cx)
- }
-
- fn project_path(&self) -> Option<project::ProjectPath> {
- None
- }
-}
-
enum ViewEvent {
UpdateTab,
}
@@ -172,7 +155,7 @@ impl View for ProjectSearchView {
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let model = &self.model.read(cx);
let results = if model.match_ranges.is_empty() {
- let theme = &cx.app_state::<Settings>().theme;
+ let theme = &cx.global::<Settings>().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<Self>) {
- 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<dyn ItemHandle> {
- Box::new(self.model.clone())
- }
-
fn tab_content(&self, tab_theme: &theme::Tab, cx: &gpui::AppContext) -> ElementBox {
- let settings = cx.app_state::<Settings>();
+ let settings = cx.global::<Settings>();
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<project::ProjectEntryId> {
+ 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<Self>,
- ) -> Option<Self>
+ fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
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<dyn Any>, cx: &mut ViewContext<Self>) {
+ fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {
+ self.results_editor.update(cx, |editor, _| {
+ editor.set_nav_history(Some(nav_history));
+ });
+ }
+
+ fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> 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<ProjectSearch>,
- nav_history: Option<ItemNavHistory>,
- cx: &mut ViewContext<Self>,
- ) -> Self {
+ fn new(model: ModelHandle<ProjectSearch>, cx: &mut ViewContext<Self>) -> 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<Workspace>) {
// 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::<ActiveSearches>()
+ .global::<ActiveSearches>()
.0
.get(&workspace.project().downgrade());
let existing = active_search
.and_then(|active_search| {
workspace
- .items_of_type::<ProjectSearch>(cx)
+ .items_of_type::<ProjectSearchView>(cx)
.find(|search| search == active_search)
})
- .or_else(|| workspace.item_of_type::<ProjectSearch>(cx));
+ .or_else(|| workspace.item_of_type::<ProjectSearchView>(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::<Settings>().theme.search;
+ let theme = &cx.global::<Settings>().theme.search;
editor.highlight_background::<Self>(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<Self>) -> ElementBox {
- let theme = cx.app_state::<Settings>().theme.clone();
+ let theme = cx.global::<Settings>().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::<Self, _, _>(option as usize, cx, |state, cx| {
- let theme = &cx.app_state::<Settings>().theme.search;
+ let theme = &cx.global::<Settings>().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::<NavButton, _, _>(direction as usize, cx, |state, cx| {
- let theme = &cx.app_state::<Settings>().theme.search;
+ let theme = &cx.global::<Settings>().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| {
@@ -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;
}
@@ -92,7 +92,8 @@ impl Server {
.add_request_handler(Server::forward_project_request::<proto::GetDocumentHighlights>)
.add_request_handler(Server::forward_project_request::<proto::GetProjectSymbols>)
.add_request_handler(Server::forward_project_request::<proto::OpenBufferForSymbol>)
- .add_request_handler(Server::forward_project_request::<proto::OpenBuffer>)
+ .add_request_handler(Server::forward_project_request::<proto::OpenBufferById>)
+ .add_request_handler(Server::forward_project_request::<proto::OpenBufferByPath>)
.add_request_handler(Server::forward_project_request::<proto::GetCompletions>)
.add_request_handler(
Server::forward_project_request::<proto::ApplyCompletionAdditionalEdits>,
@@ -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<Self>,
+ request: TypedEnvelope<proto::Follow>,
+ ) -> tide::Result<proto::FollowResponse> {
+ let leader_id = ConnectionId(request.payload.leader_id);
+ let follower_id = request.sender_id;
+ if !self
+ .state()
+ .project_connection_ids(request.payload.project_id, follower_id)?
+ .contains(&leader_id)
+ {
+ Err(anyhow!("no such peer"))?;
+ }
+ let mut response = self
+ .peer
+ .forward_request(request.sender_id, leader_id, request.payload)
+ .await?;
+ response
+ .views
+ .retain(|view| view.leader_id != Some(follower_id.0));
+ Ok(response)
+ }
+
+ async fn unfollow(
+ self: Arc<Self>,
+ request: TypedEnvelope<proto::Unfollow>,
+ ) -> tide::Result<()> {
+ let leader_id = ConnectionId(request.payload.leader_id);
+ if !self
+ .state()
+ .project_connection_ids(request.payload.project_id, request.sender_id)?
+ .contains(&leader_id)
+ {
+ Err(anyhow!("no such peer"))?;
+ }
+ self.peer
+ .forward_send(request.sender_id, leader_id, request.payload)?;
+ Ok(())
+ }
+
+ async fn update_followers(
+ self: Arc<Self>,
+ request: TypedEnvelope<proto::UpdateFollowers>,
+ ) -> tide::Result<()> {
+ let connection_ids = self
+ .state()
+ .project_connection_ids(request.payload.project_id, request.sender_id)?;
+ let leader_id = request
+ .payload
+ .variant
+ .as_ref()
+ .and_then(|variant| match variant {
+ proto::update_followers::Variant::CreateView(payload) => payload.leader_id,
+ proto::update_followers::Variant::UpdateView(payload) => payload.leader_id,
+ proto::update_followers::Variant::UpdateActiveView(payload) => payload.leader_id,
+ });
+ for follower_id in &request.payload.follower_ids {
+ let follower_id = ConnectionId(*follower_id);
+ if connection_ids.contains(&follower_id) && Some(follower_id.0) != leader_id {
+ self.peer
+ .forward_send(request.sender_id, follower_id, request.payload.clone())?;
+ }
+ }
+ Ok(())
+ }
+
async fn get_channels(
self: Arc<Server>,
request: TypedEnvelope<proto::GetChannels>,
@@ -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::<lsp::request::Completion, _>(|params, _| {
+ .handle_request::<lsp::request::Completion, _, _>(|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::<lsp::request::ResolveCompletionItem, _>(
- |params, _| {
+ fake_language_server.handle_request::<lsp::request::ResolveCompletionItem, _, _>(
+ |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::<lsp::request::Formatting, _>(|_, _| {
+ fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| 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::<lsp::request::GotoDefinition, _>(|_, _| {
- 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::<lsp::request::GotoDefinition, _, _>(
+ |_, _| 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::<lsp::request::GotoDefinition, _>(|_, _| {
- 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::<lsp::request::GotoDefinition, _, _>(
+ |_, _| 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::<lsp::request::References, _>(|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::<lsp::request::References, _, _>(
+ |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::<lsp::request::DocumentHighlightRequest, _>(
- |params, _| {
+ fake_language_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>(
+ |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::<lsp::request::WorkspaceSymbol, _>(|_, _| {
- #[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::<lsp::request::WorkspaceSymbol, _, _>(
+ |_, _| 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::<lsp::request::GotoDefinition, _>(|_, _| {
- 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::<lsp::request::GotoDefinition, _, _>(
+ |_, _| 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::<lsp::request::CodeActionRequest, _>(|params, _| {
+ .handle_request::<lsp::request::CodeActionRequest, _, _>(|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::<lsp::request::CodeActionRequest, _>(|params, _| {
+ .handle_request::<lsp::request::CodeActionRequest, _, _>(|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::request::CodeActionResolveRequest, _>(|_, _| {
- 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::<lsp::request::CodeActionResolveRequest, _, _>(
+ |_, _| 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::<lsp::request::PrepareRenameRequest, _>(|params, _| {
+ .handle_request::<lsp::request::PrepareRenameRequest, _, _>(|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::<lsp::request::Rename, _>(|params, _| {
+ .handle_request::<lsp::request::Rename, _, _>(|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::<Editor>()
+ .unwrap();
+ let editor_a2 = workspace_a
+ .update(cx_a, |workspace, cx| {
+ workspace.open_path((worktree_id, "2.txt"), cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+
+ // Client B opens an editor.
+ let workspace_b = client_b.build_workspace(&project_b, cx_b);
+ let editor_b1 = workspace_b
+ .update(cx_b, |workspace, cx| {
+ workspace.open_path((worktree_id, "1.txt"), cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+
+ let client_a_id = project_b.read_with(cx_b, |project, _| {
+ project.collaborators().values().next().unwrap().peer_id
+ });
+ let client_b_id = project_a.read_with(cx_a, |project, _| {
+ project.collaborators().values().next().unwrap().peer_id
+ });
+
+ // When client B starts following client A, all visible view states are replicated to client B.
+ editor_a1.update(cx_a, |editor, cx| editor.select_ranges([0..1], None, cx));
+ editor_a2.update(cx_a, |editor, cx| editor.select_ranges([2..3], None, cx));
+ workspace_b
+ .update(cx_b, |workspace, cx| {
+ workspace.toggle_follow(&client_a_id.into(), cx).unwrap()
+ })
+ .await
+ .unwrap();
+ let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
+ workspace
+ .active_item(cx)
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap()
+ });
+ assert!(cx_b.read(|cx| editor_b2.is_focused(cx)));
+ assert_eq!(
+ editor_b2.read_with(cx_b, |editor, cx| editor.project_path(cx)),
+ Some((worktree_id, "2.txt").into())
+ );
+ assert_eq!(
+ editor_b2.read_with(cx_b, |editor, cx| editor.selected_ranges(cx)),
+ vec![2..3]
+ );
+ assert_eq!(
+ editor_b1.read_with(cx_b, |editor, cx| editor.selected_ranges(cx)),
+ vec![0..1]
+ );
+
+ // When client A activates a different editor, client B does so as well.
+ workspace_a.update(cx_a, |workspace, cx| {
+ workspace.activate_item(&editor_a1, cx)
+ });
+ workspace_b
+ .condition(cx_b, |workspace, cx| {
+ workspace.active_item(cx).unwrap().id() == editor_b1.id()
+ })
+ .await;
+
+ // Changes to client A's editor are reflected on client B.
+ editor_a1.update(cx_a, |editor, cx| {
+ editor.select_ranges([1..1, 2..2], None, cx);
+ });
+ editor_b1
+ .condition(cx_b, |editor, cx| {
+ editor.selected_ranges(cx) == vec![1..1, 2..2]
+ })
+ .await;
+
+ editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx));
+ editor_b1
+ .condition(cx_b, |editor, cx| editor.text(cx) == "TWO")
+ .await;
+
+ editor_a1.update(cx_a, |editor, cx| {
+ editor.select_ranges([3..3], None, cx);
+ editor.set_scroll_position(vec2f(0., 100.), cx);
+ });
+ editor_b1
+ .condition(cx_b, |editor, cx| editor.selected_ranges(cx) == vec![3..3])
+ .await;
+
+ // After unfollowing, client B stops receiving updates from client A.
+ workspace_b.update(cx_b, |workspace, cx| {
+ workspace.unfollow(&workspace.active_pane().clone(), cx)
+ });
+ workspace_a.update(cx_a, |workspace, cx| {
+ workspace.activate_item(&editor_a2, cx)
+ });
+ cx_a.foreground().run_until_parked();
+ assert_eq!(
+ workspace_b.read_with(cx_b, |workspace, cx| workspace
+ .active_item(cx)
+ .unwrap()
+ .id()),
+ editor_b1.id()
+ );
+
+ // Client A starts following client B.
+ workspace_a
+ .update(cx_a, |workspace, cx| {
+ workspace.toggle_follow(&client_b_id.into(), cx).unwrap()
+ })
+ .await
+ .unwrap();
+ assert_eq!(
+ workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
+ Some(client_b_id)
+ );
+ assert_eq!(
+ workspace_a.read_with(cx_a, |workspace, cx| workspace
+ .active_item(cx)
+ .unwrap()
+ .id()),
+ editor_a1.id()
+ );
+
+ // Following interrupts when client B disconnects.
+ client_b.disconnect(&cx_b.to_async()).unwrap();
+ cx_a.foreground().run_until_parked();
+ assert_eq!(
+ workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
+ None
+ );
+ }
+
+ #[gpui::test(iterations = 10)]
+ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
+ cx_a.foreground().forbid_parking();
+ let fs = FakeFs::new(cx_a.background());
+
+ // 2 clients connect to a server.
+ let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
+ let mut client_a = server.create_client(cx_a, "user_a").await;
+ let mut client_b = server.create_client(cx_b, "user_b").await;
+ cx_a.update(editor::init);
+ cx_b.update(editor::init);
+
+ // Client A shares a project.
+ fs.insert_tree(
+ "/a",
+ json!({
+ ".zed.toml": r#"collaborators = ["user_b"]"#,
+ "1.txt": "one",
+ "2.txt": "two",
+ "3.txt": "three",
+ "4.txt": "four",
+ }),
+ )
+ .await;
+ let (project_a, worktree_id) = client_a.build_local_project(fs.clone(), "/a", cx_a).await;
+ project_a
+ .update(cx_a, |project, cx| project.share(cx))
+ .await
+ .unwrap();
+
+ // Client B joins the project.
+ let project_b = client_b
+ .build_remote_project(
+ project_a
+ .read_with(cx_a, |project, _| project.remote_id())
+ .unwrap(),
+ cx_b,
+ )
+ .await;
+
+ // Client A opens some editors.
+ let workspace_a = client_a.build_workspace(&project_a, cx_a);
+ let pane_a1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
+ let _editor_a1 = workspace_a
+ .update(cx_a, |workspace, cx| {
+ workspace.open_path((worktree_id, "1.txt"), cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+
+ // Client B opens an editor.
+ let workspace_b = client_b.build_workspace(&project_b, cx_b);
+ let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
+ let _editor_b1 = workspace_b
+ .update(cx_b, |workspace, cx| {
+ workspace.open_path((worktree_id, "2.txt"), cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+
+ // Clients A and B follow each other in split panes
+ workspace_a
+ .update(cx_a, |workspace, cx| {
+ workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
+ assert_ne!(*workspace.active_pane(), pane_a1);
+ let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap();
+ workspace
+ .toggle_follow(&workspace::ToggleFollow(leader_id), cx)
+ .unwrap()
+ })
+ .await
+ .unwrap();
+ workspace_b
+ .update(cx_b, |workspace, cx| {
+ workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
+ assert_ne!(*workspace.active_pane(), pane_b1);
+ let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap();
+ workspace
+ .toggle_follow(&workspace::ToggleFollow(leader_id), cx)
+ .unwrap()
+ })
+ .await
+ .unwrap();
+
+ workspace_a
+ .update(cx_a, |workspace, cx| {
+ workspace.activate_next_pane(cx);
+ assert_eq!(*workspace.active_pane(), pane_a1);
+ workspace.open_path((worktree_id, "3.txt"), cx)
+ })
+ .await
+ .unwrap();
+ workspace_b
+ .update(cx_b, |workspace, cx| {
+ workspace.activate_next_pane(cx);
+ assert_eq!(*workspace.active_pane(), pane_b1);
+ workspace.open_path((worktree_id, "4.txt"), cx)
+ })
+ .await
+ .unwrap();
+ cx_a.foreground().run_until_parked();
+
+ // Ensure leader updates don't change the active pane of followers
+ workspace_a.read_with(cx_a, |workspace, _| {
+ assert_eq!(*workspace.active_pane(), pane_a1);
+ });
+ workspace_b.read_with(cx_b, |workspace, _| {
+ assert_eq!(*workspace.active_pane(), pane_b1);
+ });
+
+ // Ensure peers following each other doesn't cause an infinite loop.
+ assert_eq!(
+ workspace_a.read_with(cx_a, |workspace, cx| workspace
+ .active_item(cx)
+ .unwrap()
+ .project_path(cx)),
+ Some((worktree_id, "3.txt").into())
+ );
+ workspace_a.update(cx_a, |workspace, cx| {
+ assert_eq!(
+ workspace.active_item(cx).unwrap().project_path(cx),
+ Some((worktree_id, "3.txt").into())
+ );
+ workspace.activate_next_pane(cx);
+ assert_eq!(
+ workspace.active_item(cx).unwrap().project_path(cx),
+ Some((worktree_id, "4.txt").into())
+ );
+ });
+ workspace_b.update(cx_b, |workspace, cx| {
+ assert_eq!(
+ workspace.active_item(cx).unwrap().project_path(cx),
+ Some((worktree_id, "4.txt").into())
+ );
+ workspace.activate_next_pane(cx);
+ assert_eq!(
+ workspace.active_item(cx).unwrap().project_path(cx),
+ Some((worktree_id, "3.txt").into())
+ );
+ });
+ }
+
+ #[gpui::test(iterations = 10)]
+ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
+ cx_a.foreground().forbid_parking();
+ let fs = FakeFs::new(cx_a.background());
+
+ // 2 clients connect to a server.
+ let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
+ let mut client_a = server.create_client(cx_a, "user_a").await;
+ let mut client_b = server.create_client(cx_b, "user_b").await;
+ cx_a.update(editor::init);
+ cx_b.update(editor::init);
+
+ // Client A shares a project.
+ fs.insert_tree(
+ "/a",
+ json!({
+ ".zed.toml": r#"collaborators = ["user_b"]"#,
+ "1.txt": "one",
+ "2.txt": "two",
+ "3.txt": "three",
+ }),
+ )
+ .await;
+ let (project_a, worktree_id) = client_a.build_local_project(fs.clone(), "/a", cx_a).await;
+ project_a
+ .update(cx_a, |project, cx| project.share(cx))
+ .await
+ .unwrap();
+
+ // Client B joins the project.
+ let project_b = client_b
+ .build_remote_project(
+ project_a
+ .read_with(cx_a, |project, _| project.remote_id())
+ .unwrap(),
+ cx_b,
+ )
+ .await;
+
+ // Client A opens some editors.
+ let workspace_a = client_a.build_workspace(&project_a, cx_a);
+ let _editor_a1 = workspace_a
+ .update(cx_a, |workspace, cx| {
+ workspace.open_path((worktree_id, "1.txt"), cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+
+ // Client B starts following client A.
+ let workspace_b = client_b.build_workspace(&project_b, cx_b);
+ let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
+ let leader_id = project_b.read_with(cx_b, |project, _| {
+ project.collaborators().values().next().unwrap().peer_id
+ });
+ workspace_b
+ .update(cx_b, |workspace, cx| {
+ workspace.toggle_follow(&leader_id.into(), cx).unwrap()
+ })
+ .await
+ .unwrap();
+ assert_eq!(
+ workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+ Some(leader_id)
+ );
+ let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
+ workspace
+ .active_item(cx)
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap()
+ });
+
+ // When client B moves, it automatically stops following client A.
+ editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx));
+ assert_eq!(
+ workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+ None
+ );
+
+ workspace_b
+ .update(cx_b, |workspace, cx| {
+ workspace.toggle_follow(&leader_id.into(), cx).unwrap()
+ })
+ .await
+ .unwrap();
+ assert_eq!(
+ workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+ Some(leader_id)
+ );
+
+ // When client B edits, it automatically stops following client A.
+ editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx));
+ assert_eq!(
+ workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+ None
+ );
+
+ workspace_b
+ .update(cx_b, |workspace, cx| {
+ workspace.toggle_follow(&leader_id.into(), cx).unwrap()
+ })
+ .await
+ .unwrap();
+ assert_eq!(
+ workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+ Some(leader_id)
+ );
+
+ // When client B scrolls, it automatically stops following client A.
+ editor_b2.update(cx_b, |editor, cx| {
+ editor.set_scroll_position(vec2f(0., 3.), cx)
+ });
+ assert_eq!(
+ workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+ None
+ );
+
+ workspace_b
+ .update(cx_b, |workspace, cx| {
+ workspace.toggle_follow(&leader_id.into(), cx).unwrap()
+ })
+ .await
+ .unwrap();
+ assert_eq!(
+ workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+ Some(leader_id)
+ );
+
+ // When client B activates a different pane, it continues following client A in the original pane.
+ workspace_b.update(cx_b, |workspace, cx| {
+ workspace.split_pane(pane_b.clone(), SplitDirection::Right, cx)
+ });
+ assert_eq!(
+ workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+ Some(leader_id)
+ );
+
+ workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
+ assert_eq!(
+ workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+ Some(leader_id)
+ );
+
+ // When client B activates a different item in the original pane, it automatically stops following client A.
+ workspace_b
+ .update(cx_b, |workspace, cx| {
+ workspace.open_path((worktree_id, "2.txt"), cx)
+ })
+ .await
+ .unwrap();
+ assert_eq!(
+ workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+ None
+ );
+ }
+
#[gpui::test(iterations = 100)]
async fn test_random_collaboration(cx: &mut TestAppContext, rng: StdRng) {
cx.foreground().forbid_parking();
@@ -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<Ordering> {
+ 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<Anchor> {
fn cmp(&self, other: &Range<Anchor>, buffer: &BufferSnapshot) -> Result<Ordering> {
- 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,
})
}
@@ -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<T: Clone> IntoIterator for Patch<T> {
@@ -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")
@@ -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<T> {
pub goal: SelectionGoal,
}
+impl Default for SelectionGoal {
+ fn default() -> Self {
+ Self::None
+ }
+}
+
impl<T: Clone> Selection<T> {
pub fn head(&self) -> T {
if self.reversed {
@@ -34,14 +40,27 @@ impl<T: Clone> Selection<T> {
self.start.clone()
}
}
+
+ pub fn map<F, S>(&self, f: F) -> Selection<S>
+ where
+ F: Fn(T) -> S,
+ {
+ Selection::<S> {
+ id: self.id,
+ start: f(self.start.clone()),
+ end: f(self.end.clone()),
+ reversed: self.reversed,
+ goal: self.goal,
+ }
+ }
}
-impl<T: ToOffset + ToPoint + Copy + Ord> Selection<T> {
+impl<T: Copy + Ord> Selection<T> {
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<T: ToOffset + ToPoint + Copy + Ord> Selection<T> {
}
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;
}
}
@@ -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
);
}
@@ -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<T: ToOffset>(&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::<usize>();
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>(
@@ -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,
@@ -54,7 +54,7 @@ impl ThemeSelector {
cx.subscribe(&query_editor, Self::on_query_editor_event)
.detach();
- let original_theme = cx.app_state::<Settings>().theme.clone();
+ let original_theme = cx.global::<Settings>().theme.clone();
let mut this = Self {
themes: registry,
@@ -82,7 +82,7 @@ impl ThemeSelector {
}
fn reload(_: &mut Workspace, action: &Reload, cx: &mut ViewContext<Workspace>) {
- let current_theme_name = cx.app_state::<Settings>().theme.name.clone();
+ let current_theme_name = cx.global::<Settings>().theme.name.clone();
action.0.clear();
match action.0.get(¤t_theme_name) {
Ok(theme) => {
@@ -204,9 +204,9 @@ impl ThemeSelector {
cx: &mut ViewContext<Self>,
) {
match event {
- editor::Event::Edited => {
+ editor::Event::BufferEdited { .. } => {
self.update_matches(cx);
- self.select_if_matching(&cx.app_state::<Settings>().theme.name);
+ self.select_if_matching(&cx.global::<Settings>().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<Self>) -> ElementBox {
if self.matches.is_empty() {
- let settings = cx.app_state::<Settings>();
+ let settings = cx.global::<Settings>();
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::<Settings>();
+ let settings = cx.global::<Settings>();
let theme = &settings.theme;
let container = Container::new(
@@ -276,7 +276,7 @@ impl ThemeSelector {
}
fn set_theme(theme: Arc<Theme>, cx: &mut MutableAppContext) {
- cx.update_app_state::<Settings, _, _>(|settings, cx| {
+ cx.update_global::<Settings, _, _>(|settings, cx| {
settings.theme = theme;
cx.refresh_windows();
});
@@ -299,7 +299,7 @@ impl View for ThemeSelector {
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
- let theme = cx.app_state::<Settings>().theme.clone();
+ let theme = cx.global::<Settings>().theme.clone();
Align::new(
ConstrainedBox::new(
Container::new(
@@ -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<char>,
+) -> (String, HashMap<char, Vec<usize>>) {
+ let mut extracted_markers: HashMap<char, Vec<usize>> = 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<usize>) {
+ 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<Range<usize>>) {
+ 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)
+}
@@ -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"] }
@@ -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;
+ }
+ }
+ });
+}
@@ -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<Workspace>) {
+ 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);
+ })
+}
@@ -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
+ }
+}
@@ -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<Workspace>) {
+ 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<Workspace>) {
+ 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<Workspace>) {
+ 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<Workspace>) {
+ 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)
+ });
+ });
+ });
+}
@@ -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, _>(|settings, cx| {
+ VimState::update_global(cx, |state, cx| state.set_enabled(settings.vim_mode, cx))
+ })
+ .detach();
+}
+
+#[derive(Default)]
+pub struct VimState {
+ editors: HashMap<usize, WeakViewHandle<Editor>>,
+ active_editor: Option<WeakViewHandle<Editor>>,
+
+ enabled: bool,
+ mode: Mode,
+}
+
+impl VimState {
+ fn update_global<F, S>(cx: &mut MutableAppContext, update: F) -> S
+ where
+ F: FnOnce(&mut Self, &mut MutableAppContext) -> S,
+ {
+ cx.update_default_global(update)
+ }
+
+ fn update_active_editor<S>(
+ &self,
+ cx: &mut MutableAppContext,
+ update: impl FnOnce(&mut Editor, &mut ViewContext<Editor>) -> S,
+ ) -> Option<S> {
+ 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::<Self>(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::<Self>();
+ }
+ });
+ }
+ }
+ }
+}
@@ -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<Editor>,
+}
+
+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::<Editor>(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<DisplayPoint> {
+ self.editor.update(self.cx, |editor, cx| {
+ let snapshot = editor.snapshot(cx);
+ editor
+ .newest_selection::<Point>(cx)
+ .map(|point| point.to_display_point(&snapshot.display_snapshot))
+ })
+ }
+
+ fn mode(&mut self) -> Mode {
+ self.cx.update(|cx| cx.global::<VimState>().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
+ }
+}
@@ -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<Self>) -> ElementBox {
- let theme = &cx.app_state::<Settings>().theme;
+ let theme = &cx.global::<Settings>().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::<Self, _, _>(0, cx, |_, cx| {
- let theme = &cx.app_state::<Settings>().theme;
+ let theme = &cx.global::<Settings>().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<Self>) {}
+ fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext<Self>) {}
}
@@ -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<dyn ItemViewHandle>)>,
+ items: Vec<Box<dyn ItemHandle>>,
active_item_index: usize,
nav_history: Rc<RefCell<NavHistory>>,
toolbars: HashMap<TypeId, Box<dyn ToolbarHandle>>,
@@ -108,7 +109,7 @@ pub struct Pane {
pub trait Toolbar: View {
fn active_item_changed(
&mut self,
- item: Option<Box<dyn ItemViewHandle>>,
+ item: Option<Box<dyn ItemHandle>>,
cx: &mut ViewContext<Self>,
) -> bool;
fn on_dismiss(&mut self, cx: &mut ViewContext<Self>);
@@ -117,7 +118,7 @@ pub trait Toolbar: View {
trait ToolbarHandle {
fn active_item_changed(
&self,
- item: Option<Box<dyn ItemViewHandle>>,
+ item: Option<Box<dyn ItemHandle>>,
cx: &mut MutableAppContext,
) -> bool;
fn on_dismiss(&self, cx: &mut MutableAppContext);
@@ -126,7 +127,7 @@ trait ToolbarHandle {
pub struct ItemNavHistory {
history: Rc<RefCell<NavHistory>>,
- item_view: Rc<dyn WeakItemViewHandle>,
+ item: Rc<dyn WeakItemHandle>,
}
#[derive(Default)]
@@ -152,14 +153,14 @@ impl Default for NavigationMode {
}
pub struct NavigationEntry {
- pub item_view: Rc<dyn WeakItemViewHandle>,
+ pub item: Rc<dyn WeakItemHandle>,
pub data: Option<Box<dyn Any>>,
}
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<T>(
- &mut self,
- item_handle: T,
- workspace: &Workspace,
- cx: &mut ViewContext<Self>,
- ) -> Box<dyn ItemViewHandle>
- 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<Pane>,
+ project_entry_id: ProjectEntryId,
+ cx: &mut ViewContext<Workspace>,
+ build_item: impl FnOnce(&mut MutableAppContext) -> Box<dyn ItemHandle>,
+ ) -> Box<dyn ItemHandle> {
+ 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<dyn ItemViewHandle>,
- cx: &mut ViewContext<Self>,
+ pub(crate) fn add_item(
+ workspace: &mut Workspace,
+ pane: ViewHandle<Pane>,
+ item: Box<dyn ItemHandle>,
+ local: bool,
+ cx: &mut ViewContext<Workspace>,
) {
- 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<Item = &Box<dyn ItemHandle>> {
+ 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<Item = ViewHandle<T>> {
+ 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<Item = &Box<dyn ItemViewHandle>> {
- self.item_views.iter().map(|(_, view)| view)
+ pub fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
+ self.items.get(self.active_item_index).cloned()
}
- pub fn active_item(&self) -> Option<Box<dyn ItemViewHandle>> {
- 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<ProjectEntryId> {
+ 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<usize> {
- self.item_views
- .iter()
- .position(|(_, i)| i.id() == item_view.id())
+ pub fn item_for_entry(
+ &self,
+ entry_id: ProjectEntryId,
+ cx: &AppContext,
+ ) -> Option<Box<dyn ItemHandle>> {
+ self.items.iter().find_map(|item| {
+ if item.project_entry_id(cx) == Some(entry_id) {
+ Some(item.boxed_clone())
+ } else {
+ None
+ }
+ })
}
pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option<usize> {
- 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<Self>) {
- if index < self.item_views.len() {
+ pub fn activate_item(&mut self, index: usize, local: bool, cx: &mut ViewContext<Self>) {
+ 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<Self>) {
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<Self>) {
- 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<Self>) {
- 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<Self>) {
+ pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
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<Self>) {
- 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<Self>) -> ElementBox {
- let theme = cx.app_state::<Settings>().theme.clone();
+ let theme = cx.global::<Settings>().theme.clone();
enum Tabs {}
let tabs = MouseEventHandler::new::<Tabs, _, _>(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::<TabCloseButton, _, _>(
@@ -691,7 +743,7 @@ impl View for Pane {
impl<T: Toolbar> ToolbarHandle for ViewHandle<T> {
fn active_item_changed(
&self,
- item: Option<Box<dyn ItemViewHandle>>,
+ item: Option<Box<dyn ItemHandle>>,
cx: &mut MutableAppContext,
) -> bool {
self.update(cx, |this, cx| this.active_item_changed(item, cx))
@@ -707,10 +759,10 @@ impl<T: Toolbar> ToolbarHandle for ViewHandle<T> {
}
impl ItemNavHistory {
- pub fn new<T: ItemView>(history: Rc<RefCell<NavHistory>>, item_view: &ViewHandle<T>) -> Self {
+ pub fn new<T: Item>(history: Rc<RefCell<NavHistory>>, item: &ViewHandle<T>) -> Self {
Self {
history,
- item_view: Rc::new(item_view.downgrade()),
+ item: Rc::new(item.downgrade()),
}
}
@@ -719,7 +771,7 @@ impl ItemNavHistory {
}
pub fn push<D: 'static + Any>(&self, data: Option<D>) {
- 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<D: 'static + Any>(
- &mut self,
- data: Option<D>,
- item_view: Rc<dyn WeakItemViewHandle>,
- ) {
+ pub fn push<D: 'static + Any>(&mut self, data: Option<D>, item: Rc<dyn WeakItemHandle>) {
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<dyn Any>),
});
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<dyn Any>),
});
}
@@ -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<dyn Any>),
});
}
@@ -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<PeerId, Collaborator>,
+ ) -> ElementBox {
+ self.root.render(theme, follower_states, collaborators)
+ }
+
+ pub(crate) fn panes(&self) -> Vec<&ViewHandle<Pane>> {
+ let mut panes = Vec::new();
+ self.root.collect_panes(&mut panes);
+ panes
}
}
@@ -80,10 +93,50 @@ impl Member {
Member::Axis(PaneAxis { axis, members })
}
- pub fn render(&self, theme: &Theme) -> ElementBox {
+ pub fn render(
+ &self,
+ theme: &Theme,
+ follower_states: &FollowerStatesByLeader,
+ collaborators: &HashMap<PeerId, Collaborator>,
+ ) -> 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<Pane>>) {
+ match self {
+ Member::Axis(axis) => {
+ for member in &axis.members {
+ member.collect_panes(panes);
+ }
+ }
+ Member::Pane(pane) => panes.push(pane),
}
}
}
@@ -172,11 +225,16 @@ impl PaneAxis {
}
}
- fn render<'a>(&self, theme: &Theme) -> ElementBox {
+ fn render(
+ &self,
+ theme: &Theme,
+ follower_state: &FollowerStatesByLeader,
+ collaborators: &HashMap<PeerId, Collaborator>,
+ ) -> ElementBox {
let last_member_ix = self.members.len() - 1;
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;
@@ -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<String>,
#[serde(default)]
buffer_font_size: Option<f32>,
+ #[serde(default)]
+ vim_mode: Option<bool>,
#[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(
@@ -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<Self>,
);
}
@@ -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<Self>) -> ElementBox {
- let theme = &cx.app_state::<Settings>().theme.workspace.status_bar;
+ let theme = &cx.global::<Settings>().theme.workspace.status_bar;
Flex::row()
.with_children(self.left_items.iter().map(|i| {
ChildView::new(i.as_ref())
@@ -114,7 +114,7 @@ impl<T: StatusItemView> StatusItemViewHandle for ViewHandle<T> {
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| {
@@ -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<Project>, AnyModelHandle, &mut MutableAppContext) -> Box<dyn ItemHandle>,
+>;
+
+type FollowableItemBuilder = fn(
+ ViewHandle<Pane>,
+ ModelHandle<Project>,
+ &mut Option<proto::view::Variant>,
+ &mut MutableAppContext,
+) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>;
+type FollowableItemBuilders = HashMap<
+ TypeId,
+ (
+ FollowableItemBuilder,
+ fn(AnyViewHandle) -> Box<dyn FollowableItemHandle>,
+ ),
+>;
action!(Open, Arc<AppState>);
action!(OpenNew, Arc<AppState>);
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<Client>, 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<Workspace>| {
+ let pane = workspace.active_pane().clone();
+ workspace.unfollow(&pane, cx);
+ },
+ );
cx.add_action(
|workspace: &mut Workspace, _: &Save, cx: &mut ViewContext<Workspace>| {
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<I: ProjectItem>(cx: &mut MutableAppContext) {
+ cx.update_default_global(|builders: &mut ProjectItemBuilders, _| {
+ builders.insert(TypeId::of::<I::Item>(), |window_id, project, model, cx| {
+ let item = model.downcast::<I::Item>().unwrap();
+ Box::new(cx.add_view(window_id, |cx| I::for_project_item(project, item, cx)))
+ });
+ });
+}
+
+pub fn register_followable_item<I: FollowableItem>(cx: &mut MutableAppContext) {
+ cx.update_default_global(|builders: &mut FollowableItemBuilders, _| {
+ builders.insert(
+ TypeId::of::<I>(),
+ (
+ |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::<I>().unwrap()),
+ ),
+ );
+ });
}
pub struct AppState {
@@ -105,7 +181,6 @@ pub struct AppState {
pub user_store: ModelHandle<client::UserStore>,
pub fs: Arc<dyn fs::Fs>,
pub channel_list: ModelHandle<client::ChannelList>,
- pub path_openers: Arc<[Box<dyn PathOpener>]>,
pub build_window_options: &'static dyn Fn() -> WindowOptions<'static>,
pub build_workspace: &'static dyn Fn(
ModelHandle<Project>,
@@ -126,35 +201,16 @@ pub struct JoinProjectParams {
pub app_state: Arc<AppState>,
}
-pub trait PathOpener {
- fn open(
- &self,
- project: &mut Project,
- path: ProjectPath,
- cx: &mut ModelContext<Project>,
- ) -> Option<Task<Result<Box<dyn ItemHandle>>>>;
-}
-
-pub trait Item: Entity + Sized {
- type View: ItemView;
-
- fn build_view(
- handle: ModelHandle<Self>,
- workspace: &Workspace,
- nav_history: ItemNavHistory,
- cx: &mut ViewContext<Self::View>,
- ) -> Self::View;
-
- fn project_path(&self) -> Option<ProjectPath>;
-}
-
-pub trait ItemView: View {
+pub trait Item: View {
fn deactivated(&mut self, _: &mut ViewContext<Self>) {}
- fn navigate(&mut self, _: Box<dyn Any>, _: &mut ViewContext<Self>) {}
- fn item(&self, cx: &AppContext) -> Box<dyn ItemHandle>;
+ fn navigate(&mut self, _: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
+ false
+ }
fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox;
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
- fn clone_on_split(&self, _: ItemNavHistory, _: &mut ViewContext<Self>) -> Option<Self>
+ fn project_entry_id(&self, cx: &AppContext) -> Option<ProjectEntryId>;
+ fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext<Self>);
+ fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
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<Project>,
+ item: ModelHandle<Self::Item>,
+ cx: &mut ViewContext<Self>,
+ ) -> Self;
+}
+
+pub trait FollowableItem: Item {
+ fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant>;
+ fn from_state_proto(
+ pane: ViewHandle<Pane>,
+ project: ModelHandle<Project>,
+ state: &mut Option<proto::view::Variant>,
+ cx: &mut MutableAppContext,
+ ) -> Option<Task<Result<ViewHandle<Self>>>>;
+ fn add_event_to_update_proto(
&self,
- window_id: usize,
- workspace: &Workspace,
- nav_history: Rc<RefCell<NavHistory>>,
+ event: &Self::Event,
+ update: &mut Option<proto::update_view::Variant>,
+ cx: &AppContext,
+ ) -> bool;
+ fn apply_update_proto(
+ &mut self,
+ message: proto::update_view::Variant,
+ cx: &mut ViewContext<Self>,
+ ) -> Result<()>;
+
+ fn set_leader_replica_id(&mut self, leader_replica_id: Option<u16>, cx: &mut ViewContext<Self>);
+ 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<u16>, cx: &mut MutableAppContext);
+ fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant>;
+ fn add_event_to_update_proto(
+ &self,
+ event: &dyn Any,
+ update: &mut Option<proto::update_view::Variant>,
+ cx: &AppContext,
+ ) -> bool;
+ fn apply_update_proto(
+ &self,
+ message: proto::update_view::Variant,
cx: &mut MutableAppContext,
- ) -> Box<dyn ItemViewHandle>;
- fn boxed_clone(&self) -> Box<dyn ItemHandle>;
- fn downgrade(&self) -> Box<dyn WeakItemHandle>;
- fn to_any(&self) -> AnyModelHandle;
- fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
+ ) -> 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<Box<dyn ItemHandle>>;
+impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
+ fn set_leader_replica_id(&self, leader_replica_id: Option<u16>, 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<proto::view::Variant> {
+ self.read(cx).to_state_proto(cx)
+ }
+
+ fn add_event_to_update_proto(
+ &self,
+ event: &dyn Any,
+ update: &mut Option<proto::update_view::Variant>,
+ 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<dyn ItemHandle>;
+pub trait ItemHandle: 'static + fmt::Debug {
fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox;
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
- fn boxed_clone(&self) -> Box<dyn ItemViewHandle>;
- fn clone_on_split(
+ fn project_entry_id(&self, cx: &AppContext) -> Option<ProjectEntryId>;
+ fn boxed_clone(&self) -> Box<dyn ItemHandle>;
+ fn set_nav_history(&self, nav_history: Rc<RefCell<NavHistory>>, cx: &mut MutableAppContext);
+ fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemHandle>>;
+ fn added_to_pane(
&self,
- nav_history: Rc<RefCell<NavHistory>>,
- cx: &mut MutableAppContext,
- ) -> Option<Box<dyn ItemViewHandle>>;
- fn added_to_pane(&mut self, cx: &mut ViewContext<Pane>);
+ workspace: &mut Workspace,
+ pane: ViewHandle<Pane>,
+ cx: &mut ViewContext<Workspace>,
+ );
fn deactivated(&self, cx: &mut MutableAppContext);
- fn navigate(&self, data: Box<dyn Any>, cx: &mut MutableAppContext);
+ fn navigate(&self, data: Box<dyn Any>, 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<Result<()>>;
fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyViewHandle>;
+ fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>>;
}
-pub trait WeakItemViewHandle {
+pub trait WeakItemHandle {
fn id(&self) -> usize;
- fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemViewHandle>>;
-}
-
-impl<T: Item> ItemHandle for ModelHandle<T> {
- fn id(&self) -> usize {
- self.id()
- }
-
- fn add_view(
- &self,
- window_id: usize,
- workspace: &Workspace,
- nav_history: Rc<RefCell<NavHistory>>,
- cx: &mut MutableAppContext,
- ) -> Box<dyn ItemViewHandle> {
- 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<dyn ItemHandle> {
- Box::new(self.clone())
- }
-
- fn downgrade(&self) -> Box<dyn WeakItemHandle> {
- Box::new(self.downgrade())
- }
-
- fn to_any(&self) -> AnyModelHandle {
- self.clone().into()
- }
-
- fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
- self.read(cx).project_path()
- }
-}
-
-impl ItemHandle for Box<dyn ItemHandle> {
- fn id(&self) -> usize {
- ItemHandle::id(self.as_ref())
- }
-
- fn add_view(
- &self,
- window_id: usize,
- workspace: &Workspace,
- nav_history: Rc<RefCell<NavHistory>>,
- cx: &mut MutableAppContext,
- ) -> Box<dyn ItemViewHandle> {
- ItemHandle::add_view(self.as_ref(), window_id, workspace, nav_history, cx)
- }
-
- fn boxed_clone(&self) -> Box<dyn ItemHandle> {
- self.as_ref().boxed_clone()
- }
-
- fn downgrade(&self) -> Box<dyn WeakItemHandle> {
- self.as_ref().downgrade()
- }
-
- fn to_any(&self) -> AnyModelHandle {
- self.as_ref().to_any()
- }
-
- fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
- self.as_ref().project_path(cx)
- }
-}
-
-impl<T: Item> WeakItemHandle for WeakModelHandle<T> {
- fn id(&self) -> usize {
- WeakModelHandle::id(self)
- }
-
- fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
- WeakModelHandle::<T>::upgrade(self, cx).map(|i| Box::new(i) as Box<dyn ItemHandle>)
- }
-}
-
-impl Hash for Box<dyn WeakItemHandle> {
- fn hash<H: Hasher>(&self, state: &mut H) {
- self.id().hash(state);
- }
-}
-
-impl PartialEq for Box<dyn WeakItemHandle> {
- fn eq(&self, other: &Self) -> bool {
- self.id() == other.id()
- }
+ fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>>;
}
-impl Eq for Box<dyn WeakItemHandle> {}
-
-impl dyn ItemViewHandle {
+impl dyn ItemHandle {
pub fn downcast<T: View>(&self) -> Option<ViewHandle<T>> {
self.to_any().downcast()
}
@@ -358,11 +398,7 @@ impl dyn ItemViewHandle {
}
}
-impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
- fn item(&self, cx: &AppContext) -> Box<dyn ItemHandle> {
- self.read(cx).item(cx)
- }
-
+impl<T: Item> ItemHandle for ViewHandle<T> {
fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox {
self.read(cx).tab_content(style, cx)
}
@@ -371,37 +407,102 @@ impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
self.read(cx).project_path(cx)
}
- fn boxed_clone(&self) -> Box<dyn ItemViewHandle> {
+ fn project_entry_id(&self, cx: &AppContext) -> Option<ProjectEntryId> {
+ self.read(cx).project_entry_id(cx)
+ }
+
+ fn boxed_clone(&self) -> Box<dyn ItemHandle> {
Box::new(self.clone())
}
- fn clone_on_split(
- &self,
- nav_history: Rc<RefCell<NavHistory>>,
- cx: &mut MutableAppContext,
- ) -> Option<Box<dyn ItemViewHandle>> {
+ fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemHandle>> {
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<dyn ItemViewHandle>)
+ .map(|handle| Box::new(handle) as Box<dyn ItemHandle>)
}
- fn added_to_pane(&mut self, cx: &mut ViewContext<Pane>) {
- cx.subscribe(self, |pane, item, event, cx| {
+ fn set_nav_history(&self, nav_history: Rc<RefCell<NavHistory>>, 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<Pane>,
+ cx: &mut ViewContext<Workspace>,
+ ) {
+ 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<T: ItemView> ItemViewHandle for ViewHandle<T> {
self.update(cx, |this, cx| this.deactivated(cx));
}
- fn navigate(&self, data: Box<dyn Any>, cx: &mut MutableAppContext) {
- self.update(cx, |this, cx| this.navigate(data, cx));
+ fn navigate(&self, data: Box<dyn Any>, cx: &mut MutableAppContext) -> bool {
+ self.update(cx, |this, cx| this.navigate(data, cx))
}
fn save(&self, project: ModelHandle<Project>, cx: &mut MutableAppContext) -> Task<Result<()>> {
@@ -455,17 +556,21 @@ impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyViewHandle> {
self.read(cx).act_as_type(type_id, self, cx)
}
-}
-impl Into<AnyViewHandle> for Box<dyn ItemViewHandle> {
- fn into(self) -> AnyViewHandle {
- self.to_any()
+ fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>> {
+ if cx.has_global::<FollowableItemBuilders>() {
+ let builders = cx.global::<FollowableItemBuilders>();
+ let item = self.to_any();
+ Some(builders.get(&item.view_type())?.1(item))
+ } else {
+ None
+ }
}
}
-impl Clone for Box<dyn ItemViewHandle> {
- fn clone(&self) -> Box<dyn ItemViewHandle> {
- self.boxed_clone()
+impl Into<AnyViewHandle> for Box<dyn ItemHandle> {
+ fn into(self) -> AnyViewHandle {
+ self.to_any()
}
}
@@ -475,14 +580,13 @@ impl Clone for Box<dyn ItemHandle> {
}
}
-impl<T: ItemView> WeakItemViewHandle for WeakViewHandle<T> {
+impl<T: Item> WeakItemHandle for WeakViewHandle<T> {
fn id(&self) -> usize {
self.id()
}
- fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemViewHandle>> {
- self.upgrade(cx)
- .map(|v| Box::new(v) as Box<dyn ItemViewHandle>)
+ fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
+ self.upgrade(cx).map(|v| Box::new(v) as Box<dyn ItemHandle>)
}
}
@@ -494,14 +598,13 @@ pub struct WorkspaceParams {
pub languages: Arc<LanguageRegistry>,
pub user_store: ModelHandle<UserStore>,
pub channel_list: ModelHandle<ChannelList>,
- pub path_openers: Arc<[Box<dyn PathOpener>]>,
}
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<Self>,
client: Arc<Client>,
user_store: ModelHandle<client::UserStore>,
+ remote_entity_subscription: Option<Subscription>,
fs: Arc<dyn Fs>,
modal: Option<AnyViewHandle>,
center: PaneGroup,
@@ -562,11 +664,31 @@ pub struct Workspace {
active_pane: ViewHandle<Pane>,
status_bar: ViewHandle<StatusBar>,
project: ModelHandle<Project>,
- path_openers: Arc<[Box<dyn PathOpener>]>,
- items: BTreeMap<Reverse<usize>, Box<dyn WeakItemHandle>>,
+ leader_state: LeaderState,
+ follower_states_by_leader: FollowerStatesByLeader,
+ last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
_observe_current_user: Task<()>,
}
+#[derive(Default)]
+struct LeaderState {
+ followers: HashSet<PeerId>,
+}
+
+type FollowerStatesByLeader = HashMap<PeerId, HashMap<ViewHandle<Pane>, FollowerState>>;
+
+#[derive(Default)]
+struct FollowerState {
+ active_view_id: Option<u64>,
+ items_by_leader_view_id: HashMap<u64, FollowerItem>,
+}
+
+#[derive(Debug)]
+enum FollowerItem {
+ Loading(Vec<proto::update_view::Variant>),
+ Loaded(Box<dyn FollowableItemHandle>),
+}
+
impl Workspace {
pub fn new(params: &WorkspaceParams, cx: &mut ViewContext<Self>) -> 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<Self> {
@@ -690,7 +833,7 @@ impl Workspace {
&mut self,
abs_paths: &[PathBuf],
cx: &mut ViewContext<Self>,
- ) -> Task<Vec<Option<Result<Box<dyn ItemViewHandle>, Arc<anyhow::Error>>>>> {
+ ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>>>> {
let entries = abs_paths
.iter()
.cloned()
@@ -782,68 +925,27 @@ impl Workspace {
}
}
- pub fn open_path(
- &mut self,
- path: ProjectPath,
- cx: &mut ViewContext<Self>,
- ) -> Task<Result<Box<dyn ItemViewHandle>, Arc<anyhow::Error>>> {
- 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<Self>,
- ) -> Task<Result<Box<dyn ItemHandle>>> {
- 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<Box<dyn ItemHandle>> {
- 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<Item = &Box<dyn ItemHandle>> {
+ self.panes.iter().flat_map(|pane| pane.read(cx).items())
}
- pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<ModelHandle<T>> {
- self.items
- .values()
- .find_map(|i| i.upgrade(cx).and_then(|i| i.to_any().downcast()))
+ pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<ViewHandle<T>> {
+ 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<Item = ModelHandle<T>> {
- self.items
- .values()
- .filter_map(|i| i.upgrade(cx).and_then(|i| i.to_any().downcast()))
+ ) -> impl 'a + Iterator<Item = ViewHandle<T>> {
+ self.panes
+ .iter()
+ .flat_map(|pane| pane.read(cx).items_of_type())
}
- pub fn active_item(&self, cx: &AppContext) -> Option<Box<dyn ItemViewHandle>> {
+ pub fn active_item(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
self.active_pane().read(cx).active_item()
}
@@ -962,49 +1064,84 @@ impl Workspace {
pane
}
- pub fn open_item<T>(
+ pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
+ 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<ProjectPath>,
cx: &mut ViewContext<Self>,
- ) -> Box<dyn ItemViewHandle>
- where
- T: 'static + ItemHandle,
- {
- self.open_item_in_pane(item_handle, &self.active_pane().clone(), cx)
+ ) -> Task<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>> {
+ 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<T>(
+ pub(crate) fn load_path(
&mut self,
- item_handle: T,
- pane: &ViewHandle<Pane>,
+ path: ProjectPath,
cx: &mut ViewContext<Self>,
- ) -> Box<dyn ItemViewHandle>
- 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<dyn ItemHandle>,
+ )>,
+ > {
+ 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::<ProjectItemBuilders>()
+ .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<T>(
&mut self,
- item: &dyn ItemHandle,
+ project_item: ModelHandle<T::Item>,
cx: &mut ViewContext<Self>,
- ) -> 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<T>
+ 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<Self>) -> 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<Self>) {
- 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<Self>) {
+ 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<Pane>, cx: &mut ViewContext<Self>) {
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(
@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
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"
@@ -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
@@ -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/");
@@ -17,9 +17,8 @@ fn init_logger() {
pub fn test_app_state(cx: &mut MutableAppContext) -> Arc<AppState> {
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<AppState> {
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,
})
@@ -43,7 +43,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
cx.add_global_action(quit);
cx.add_global_action({
move |action: &AdjustBufferFontSize, cx| {
- cx.update_app_state::<Settings, _, _>(|settings, cx| {
+ cx.update_global::<Settings, _, _>(|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::<Vec<_>>();
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<Workspace>,
cx: &mut TestAppContext,