Merge branch 'main' into breadcrumbs

This commit is contained in:
Antonio Scandurra 2022-03-29 10:05:05 +02:00
commit d7026c2228
79 changed files with 7650 additions and 3174 deletions

35
Cargo.lock generated
View file

@ -1617,6 +1617,7 @@ dependencies = [
"collections", "collections",
"ctor", "ctor",
"env_logger", "env_logger",
"futures",
"fuzzy", "fuzzy",
"gpui", "gpui",
"itertools", "itertools",
@ -1629,6 +1630,7 @@ dependencies = [
"postage", "postage",
"project", "project",
"rand 0.8.3", "rand 0.8.3",
"rpc",
"serde", "serde",
"smallvec", "smallvec",
"smol", "smol",
@ -1782,7 +1784,9 @@ dependencies = [
name = "file_finder" name = "file_finder"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"ctor",
"editor", "editor",
"env_logger",
"fuzzy", "fuzzy",
"gpui", "gpui",
"postage", "postage",
@ -2524,6 +2528,15 @@ dependencies = [
"hashbrown 0.9.1", "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]] [[package]]
name = "infer" name = "infer"
version = "0.2.3" version = "0.2.3"
@ -5550,9 +5563,9 @@ checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
[[package]] [[package]]
name = "unindent" name = "unindent"
version = "0.1.7" version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f14ee04d9415b52b3aeab06258a3f07093182b88ba0f9b8d203f211a7a7d41c7" checksum = "514672a55d7380da379785a4d70ca8386c8883ff7eaae877be4d2081cebe73d8"
[[package]] [[package]]
name = "universal-hash" name = "universal-hash"
@ -5670,6 +5683,21 @@ version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed"
[[package]]
name = "vim"
version = "0.1.0"
dependencies = [
"collections",
"editor",
"gpui",
"indoc",
"language",
"log",
"project",
"util",
"workspace",
]
[[package]] [[package]]
name = "waker-fn" name = "waker-fn"
version = "1.1.0" version = "1.1.0"
@ -5929,7 +5957,7 @@ checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
[[package]] [[package]]
name = "zed" name = "zed"
version = "0.21.0" version = "0.23.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-compression", "async-compression",
@ -6000,6 +6028,7 @@ dependencies = [
"unindent", "unindent",
"url", "url",
"util", "util",
"vim",
"workspace", "workspace",
] ]

View file

@ -64,13 +64,13 @@ impl ChatPanel {
ix, ix,
item_type, item_type,
is_hovered, is_hovered,
&cx.app_state::<Settings>().theme.chat_panel.channel_select, &cx.global::<Settings>().theme.chat_panel.channel_select,
cx, cx,
) )
} }
}) })
.with_style(move |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 { SelectStyle {
header: theme.header.container.clone(), header: theme.header.container.clone(),
menu: theme.menu.clone(), menu: theme.menu.clone(),
@ -200,7 +200,7 @@ impl ChatPanel {
} }
fn render_channel(&self, cx: &mut RenderContext<Self>) -> ElementBox { fn render_channel(&self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = &cx.app_state::<Settings>().theme; let theme = &cx.global::<Settings>().theme;
Flex::column() Flex::column()
.with_child( .with_child(
Container::new(ChildView::new(&self.channel_select).boxed()) Container::new(ChildView::new(&self.channel_select).boxed())
@ -224,7 +224,7 @@ impl ChatPanel {
fn render_message(&self, message: &ChannelMessage, cx: &AppContext) -> ElementBox { fn render_message(&self, message: &ChannelMessage, cx: &AppContext) -> ElementBox {
let now = OffsetDateTime::now_utc(); let now = OffsetDateTime::now_utc();
let settings = cx.app_state::<Settings>(); let settings = cx.global::<Settings>();
let theme = if message.is_pending() { let theme = if message.is_pending() {
&settings.theme.chat_panel.pending_message &settings.theme.chat_panel.pending_message
} else { } else {
@ -267,7 +267,7 @@ impl ChatPanel {
} }
fn render_input_box(&self, cx: &AppContext) -> ElementBox { 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()) Container::new(ChildView::new(&self.input_editor).boxed())
.with_style(theme.chat_panel.input_editor.container) .with_style(theme.chat_panel.input_editor.container)
.boxed() .boxed()
@ -304,7 +304,7 @@ impl ChatPanel {
} }
fn render_sign_in_prompt(&self, cx: &mut RenderContext<Self>) -> ElementBox { 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 rpc = self.rpc.clone();
let this = cx.handle(); let this = cx.handle();
@ -327,7 +327,12 @@ impl ChatPanel {
let rpc = rpc.clone(); let rpc = rpc.clone();
let this = this.clone(); let this = this.clone();
cx.spawn(|mut cx| async move { 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| { cx.update(|cx| {
if let Some(this) = this.upgrade(cx) { if let Some(this) = this.upgrade(cx) {
if this.is_focused(cx) { if this.is_focused(cx) {
@ -385,7 +390,7 @@ impl View for ChatPanel {
} else { } else {
self.render_sign_in_prompt(cx) self.render_sign_in_prompt(cx)
}; };
let theme = &cx.app_state::<Settings>().theme; let theme = &cx.global::<Settings>().theme;
ConstrainedBox::new( ConstrainedBox::new(
Container::new(element) Container::new(element)
.with_style(theme.chat_panel.container) .with_style(theme.chat_panel.container)

View file

@ -181,7 +181,7 @@ impl Entity for Channel {
impl Channel { impl Channel {
pub fn init(rpc: &Arc<Client>) { pub fn init(rpc: &Arc<Client>) {
rpc.add_entity_message_handler(Self::handle_message_sent); rpc.add_model_message_handler(Self::handle_message_sent);
} }
pub fn new( pub fn new(

View file

@ -13,8 +13,8 @@ use async_tungstenite::tungstenite::{
}; };
use futures::{future::LocalBoxFuture, FutureExt, StreamExt}; use futures::{future::LocalBoxFuture, FutureExt, StreamExt};
use gpui::{ use gpui::{
action, AnyModelHandle, AnyWeakModelHandle, AsyncAppContext, Entity, ModelContext, ModelHandle, action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, AsyncAppContext,
MutableAppContext, Task, Entity, ModelContext, ModelHandle, MutableAppContext, Task, View, ViewContext, ViewHandle,
}; };
use http::HttpClient; use http::HttpClient;
use lazy_static::lazy_static; use lazy_static::lazy_static;
@ -45,7 +45,7 @@ pub use user::*;
lazy_static! { lazy_static! {
static ref ZED_SERVER_URL: String = static ref ZED_SERVER_URL: String =
std::env::var("ZED_SERVER_URL").unwrap_or("https://zed.dev".to_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() .ok()
.and_then(|s| if s.is_empty() { None } else { Some(s) }); .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) { pub fn init(rpc: Arc<Client>, cx: &mut MutableAppContext) {
cx.add_global_action(move |_: &Authenticate, cx| { cx.add_global_action(move |_: &Authenticate, cx| {
let rpc = rpc.clone(); 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(); .detach();
}); });
} }
@ -136,26 +136,37 @@ impl Status {
struct ClientState { struct ClientState {
credentials: Option<Credentials>, credentials: Option<Credentials>,
status: (watch::Sender<Status>, watch::Receiver<Status>), status: (watch::Sender<Status>, watch::Receiver<Status>),
entity_id_extractors: HashMap<TypeId, Box<dyn Send + Sync + Fn(&dyn AnyTypedEnvelope) -> u64>>, entity_id_extractors: HashMap<TypeId, fn(&dyn AnyTypedEnvelope) -> u64>,
_reconnect_task: Option<Task<()>>, _reconnect_task: Option<Task<()>>,
reconnect_interval: Duration, reconnect_interval: Duration,
models_by_entity_type_and_remote_id: HashMap<(TypeId, u64), AnyWeakModelHandle>, entities_by_type_and_remote_id: HashMap<(TypeId, u64), AnyWeakEntityHandle>,
models_by_message_type: HashMap<TypeId, AnyWeakModelHandle>, models_by_message_type: HashMap<TypeId, AnyWeakModelHandle>,
model_types_by_message_type: HashMap<TypeId, TypeId>, entity_types_by_message_type: HashMap<TypeId, TypeId>,
message_handlers: HashMap< message_handlers: HashMap<
TypeId, TypeId,
Arc< Arc<
dyn Send dyn Send
+ Sync + Sync
+ Fn( + Fn(
AnyModelHandle, AnyEntityHandle,
Box<dyn AnyTypedEnvelope>, Box<dyn AnyTypedEnvelope>,
&Arc<Client>,
AsyncAppContext, AsyncAppContext,
) -> LocalBoxFuture<'static, Result<()>>, ) -> LocalBoxFuture<'static, Result<()>>,
>, >,
>, >,
} }
enum AnyWeakEntityHandle {
Model(AnyWeakModelHandle),
View(AnyWeakViewHandle),
}
enum AnyEntityHandle {
Model(AnyModelHandle),
View(AnyViewHandle),
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Credentials { pub struct Credentials {
pub user_id: u64, pub user_id: u64,
@ -171,8 +182,8 @@ impl Default for ClientState {
_reconnect_task: None, _reconnect_task: None,
reconnect_interval: Duration::from_secs(5), reconnect_interval: Duration::from_secs(5),
models_by_message_type: Default::default(), models_by_message_type: Default::default(),
models_by_entity_type_and_remote_id: Default::default(), entities_by_type_and_remote_id: Default::default(),
model_types_by_message_type: Default::default(), entity_types_by_message_type: Default::default(),
message_handlers: Default::default(), message_handlers: Default::default(),
} }
} }
@ -195,13 +206,13 @@ impl Drop for Subscription {
Subscription::Entity { client, id } => { Subscription::Entity { client, id } => {
if let Some(client) = client.upgrade() { if let Some(client) = client.upgrade() {
let mut state = client.state.write(); let mut state = client.state.write();
let _ = state.models_by_entity_type_and_remote_id.remove(id); let _ = state.entities_by_type_and_remote_id.remove(id);
} }
} }
Subscription::Message { client, id } => { Subscription::Message { client, id } => {
if let Some(client) = client.upgrade() { if let Some(client) = client.upgrade() {
let mut state = client.state.write(); let mut state = client.state.write();
let _ = state.model_types_by_message_type.remove(id); let _ = state.entity_types_by_message_type.remove(id);
let _ = state.message_handlers.remove(id); let _ = state.message_handlers.remove(id);
} }
} }
@ -239,7 +250,7 @@ impl Client {
state._reconnect_task.take(); state._reconnect_task.take();
state.message_handlers.clear(); state.message_handlers.clear();
state.models_by_message_type.clear(); state.models_by_message_type.clear();
state.models_by_entity_type_and_remote_id.clear(); state.entities_by_type_and_remote_id.clear();
state.entity_id_extractors.clear(); state.entity_id_extractors.clear();
self.peer.reset(); self.peer.reset();
} }
@ -291,7 +302,7 @@ impl Client {
state._reconnect_task = Some(cx.spawn(|cx| async move { state._reconnect_task = Some(cx.spawn(|cx| async move {
let mut rng = StdRng::from_entropy(); let mut rng = StdRng::from_entropy();
let mut delay = Duration::from_millis(100); 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); log::error!("failed to connect {}", error);
this.set_status( this.set_status(
Status::ReconnectionError { 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>( pub fn add_model_for_remote_entity<T: Entity>(
self: &Arc<Self>, self: &Arc<Self>,
remote_id: u64, remote_id: u64,
cx: &mut ModelContext<T>, cx: &mut ModelContext<T>,
) -> Subscription { ) -> Subscription {
let handle = AnyModelHandle::from(cx.handle());
let mut state = self.state.write();
let id = (TypeId::of::<T>(), remote_id); let id = (TypeId::of::<T>(), remote_id);
state self.state
.models_by_entity_type_and_remote_id .write()
.insert(id, handle.downgrade()); .entities_by_type_and_remote_id
.insert(id, AnyWeakEntityHandle::Model(cx.weak_handle().into()));
Subscription::Entity { Subscription::Entity {
client: Arc::downgrade(self), client: Arc::downgrade(self),
id, id,
@ -346,7 +372,6 @@ impl Client {
{ {
let message_type_id = TypeId::of::<M>(); let message_type_id = TypeId::of::<M>();
let client = Arc::downgrade(self);
let mut state = self.state.write(); let mut state = self.state.write();
state state
.models_by_message_type .models_by_message_type
@ -354,14 +379,15 @@ impl Client {
let prev_handler = state.message_handlers.insert( let prev_handler = state.message_handlers.insert(
message_type_id, message_type_id,
Arc::new(move |handle, envelope, cx| { Arc::new(move |handle, envelope, client, cx| {
let handle = if let AnyEntityHandle::Model(handle) = handle {
handle
} else {
unreachable!();
};
let model = handle.downcast::<E>().unwrap(); let model = handle.downcast::<E>().unwrap();
let envelope = envelope.into_any().downcast::<TypedEnvelope<M>>().unwrap(); let envelope = envelope.into_any().downcast::<TypedEnvelope<M>>().unwrap();
if let Some(client) = client.upgrade() {
handler(model, *envelope, client.clone(), cx).boxed_local() handler(model, *envelope, client.clone(), cx).boxed_local()
} else {
async move { Ok(()) }.boxed_local()
}
}), }),
); );
if prev_handler.is_some() { if prev_handler.is_some() {
@ -374,7 +400,26 @@ impl Client {
} }
} }
pub fn add_entity_message_handler<M, E, H, F>(self: &Arc<Self>, handler: H) pub fn add_view_message_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
where
M: EntityMessage,
E: View,
H: 'static
+ Send
+ Sync
+ Fn(ViewHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
F: 'static + Future<Output = Result<()>>,
{
self.add_entity_message_handler::<M, E, _, _>(move |handle, message, client, cx| {
if let AnyEntityHandle::View(handle) = handle {
handler(handle.downcast::<E>().unwrap(), message, client, cx)
} else {
unreachable!();
}
})
}
pub fn add_model_message_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
where where
M: EntityMessage, M: EntityMessage,
E: Entity, E: Entity,
@ -383,38 +428,51 @@ impl Client {
+ Sync + Sync
+ Fn(ModelHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F, + Fn(ModelHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
F: 'static + Future<Output = Result<()>>, F: 'static + Future<Output = Result<()>>,
{
self.add_entity_message_handler::<M, E, _, _>(move |handle, message, client, cx| {
if let AnyEntityHandle::Model(handle) = handle {
handler(handle.downcast::<E>().unwrap(), message, client, cx)
} else {
unreachable!();
}
})
}
fn add_entity_message_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
where
M: EntityMessage,
E: Entity,
H: 'static
+ Send
+ Sync
+ Fn(AnyEntityHandle, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
F: 'static + Future<Output = Result<()>>,
{ {
let model_type_id = TypeId::of::<E>(); let model_type_id = TypeId::of::<E>();
let message_type_id = TypeId::of::<M>(); let message_type_id = TypeId::of::<M>();
let client = Arc::downgrade(self);
let mut state = self.state.write(); let mut state = self.state.write();
state state
.model_types_by_message_type .entity_types_by_message_type
.insert(message_type_id, model_type_id); .insert(message_type_id, model_type_id);
state state
.entity_id_extractors .entity_id_extractors
.entry(message_type_id) .entry(message_type_id)
.or_insert_with(|| { .or_insert_with(|| {
Box::new(|envelope| { |envelope| {
let envelope = envelope envelope
.as_any() .as_any()
.downcast_ref::<TypedEnvelope<M>>() .downcast_ref::<TypedEnvelope<M>>()
.unwrap(); .unwrap()
envelope.payload.remote_entity_id() .payload
}) .remote_entity_id()
}
}); });
let prev_handler = state.message_handlers.insert( let prev_handler = state.message_handlers.insert(
message_type_id, message_type_id,
Arc::new(move |handle, envelope, cx| { Arc::new(move |handle, envelope, client, cx| {
let model = handle.downcast::<E>().unwrap();
let envelope = envelope.into_any().downcast::<TypedEnvelope<M>>().unwrap(); let envelope = envelope.into_any().downcast::<TypedEnvelope<M>>().unwrap();
if let Some(client) = client.upgrade() { handler(handle, *envelope, client.clone(), cx).boxed_local()
handler(model, *envelope, client.clone(), cx).boxed_local()
} else {
async move { Ok(()) }.boxed_local()
}
}), }),
); );
if prev_handler.is_some() { if prev_handler.is_some() {
@ -422,7 +480,7 @@ impl Client {
} }
} }
pub fn add_entity_request_handler<M, E, H, F>(self: &Arc<Self>, handler: H) pub fn add_model_request_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
where where
M: EntityMessage + RequestMessage, M: EntityMessage + RequestMessage,
E: Entity, E: Entity,
@ -432,10 +490,39 @@ impl Client {
+ Fn(ModelHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F, + Fn(ModelHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
F: 'static + Future<Output = Result<M::Response>>, F: 'static + Future<Output = Result<M::Response>>,
{ {
self.add_entity_message_handler(move |model, envelope, client, cx| { self.add_model_message_handler(move |entity, envelope, client, cx| {
let receipt = envelope.receipt(); Self::respond_to_request::<M, _>(
let response = handler(model, envelope, client.clone(), cx); envelope.receipt(),
async move { handler(entity, envelope, client.clone(), cx),
client,
)
})
}
pub fn add_view_request_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
where
M: EntityMessage + RequestMessage,
E: View,
H: 'static
+ Send
+ Sync
+ Fn(ViewHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
F: 'static + Future<Output = Result<M::Response>>,
{
self.add_view_message_handler(move |entity, envelope, client, cx| {
Self::respond_to_request::<M, _>(
envelope.receipt(),
handler(entity, envelope, client.clone(), cx),
client,
)
})
}
async fn respond_to_request<T: RequestMessage, F: Future<Output = Result<T::Response>>>(
receipt: Receipt<T>,
response: F,
client: Arc<Self>,
) -> Result<()> {
match response.await { match response.await {
Ok(response) => { Ok(response) => {
client.respond(receipt, response)?; client.respond(receipt, response)?;
@ -452,8 +539,6 @@ impl Client {
} }
} }
} }
})
}
pub fn has_keychain_credentials(&self, cx: &AsyncAppContext) -> bool { pub fn has_keychain_credentials(&self, cx: &AsyncAppContext) -> bool {
read_credentials_from_keychain(cx).is_some() read_credentials_from_keychain(cx).is_some()
@ -462,6 +547,7 @@ impl Client {
#[async_recursion(?Send)] #[async_recursion(?Send)]
pub async fn authenticate_and_connect( pub async fn authenticate_and_connect(
self: &Arc<Self>, self: &Arc<Self>,
try_keychain: bool,
cx: &AsyncAppContext, cx: &AsyncAppContext,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let was_disconnected = match *self.status().borrow() { let was_disconnected = match *self.status().borrow() {
@ -483,23 +569,22 @@ impl Client {
self.set_status(Status::Reauthenticating, cx) self.set_status(Status::Reauthenticating, cx)
} }
let mut used_keychain = false; let mut read_from_keychain = false;
let credentials = self.state.read().credentials.clone(); let mut credentials = self.state.read().credentials.clone();
let credentials = if let Some(credentials) = credentials { if credentials.is_none() && try_keychain {
credentials credentials = read_credentials_from_keychain(cx);
} else if let Some(credentials) = read_credentials_from_keychain(cx) { read_from_keychain = credentials.is_some();
used_keychain = true; }
credentials if credentials.is_none() {
} else { credentials = Some(match self.authenticate(&cx).await {
let credentials = match self.authenticate(&cx).await {
Ok(credentials) => credentials, Ok(credentials) => credentials,
Err(err) => { Err(err) => {
self.set_status(Status::ConnectionError, cx); self.set_status(Status::ConnectionError, cx);
return Err(err); return Err(err);
} }
}; });
credentials }
}; let credentials = credentials.unwrap();
if was_disconnected { if was_disconnected {
self.set_status(Status::Connecting, cx); self.set_status(Status::Connecting, cx);
@ -510,7 +595,7 @@ impl Client {
match self.establish_connection(&credentials, cx).await { match self.establish_connection(&credentials, cx).await {
Ok(conn) => { Ok(conn) => {
self.state.write().credentials = Some(credentials.clone()); 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(); write_credentials_to_keychain(&credentials, cx).log_err();
} }
self.set_connection(conn, cx).await; self.set_connection(conn, cx).await;
@ -518,10 +603,10 @@ impl Client {
} }
Err(EstablishConnectionError::Unauthorized) => { Err(EstablishConnectionError::Unauthorized) => {
self.state.write().credentials.take(); self.state.write().credentials.take();
if used_keychain { if read_from_keychain {
cx.platform().delete_credentials(&ZED_SERVER_URL).log_err(); cx.platform().delete_credentials(&ZED_SERVER_URL).log_err();
self.set_status(Status::SignedOut, cx); self.set_status(Status::SignedOut, cx);
self.authenticate_and_connect(cx).await self.authenticate_and_connect(false, cx).await
} else { } else {
self.set_status(Status::ConnectionError, cx); self.set_status(Status::ConnectionError, cx);
Err(EstablishConnectionError::Unauthorized)? Err(EstablishConnectionError::Unauthorized)?
@ -561,24 +646,26 @@ impl Client {
.models_by_message_type .models_by_message_type
.get(&payload_type_id) .get(&payload_type_id)
.and_then(|model| model.upgrade(&cx)) .and_then(|model| model.upgrade(&cx))
.map(AnyEntityHandle::Model)
.or_else(|| { .or_else(|| {
let model_type_id = let entity_type_id =
*state.model_types_by_message_type.get(&payload_type_id)?; *state.entity_types_by_message_type.get(&payload_type_id)?;
let entity_id = state let entity_id = state
.entity_id_extractors .entity_id_extractors
.get(&message.payload_type_id()) .get(&message.payload_type_id())
.map(|extract_entity_id| { .map(|extract_entity_id| {
(extract_entity_id)(message.as_ref()) (extract_entity_id)(message.as_ref())
})?; })?;
let model = state
.models_by_entity_type_and_remote_id let entity = state
.get(&(model_type_id, entity_id))?; .entities_by_type_and_remote_id
if let Some(model) = model.upgrade(&cx) { .get(&(entity_type_id, entity_id))?;
Some(model) if let Some(entity) = entity.upgrade(&cx) {
Some(entity)
} else { } else {
state state
.models_by_entity_type_and_remote_id .entities_by_type_and_remote_id
.remove(&(model_type_id, entity_id)); .remove(&(entity_type_id, entity_id));
None None
} }
}); });
@ -593,7 +680,7 @@ impl Client {
if let Some(handler) = state.message_handlers.get(&payload_type_id).cloned() if let Some(handler) = state.message_handlers.get(&payload_type_id).cloned()
{ {
drop(state); // Avoid deadlocks if the handler interacts with rpc::Client drop(state); // Avoid deadlocks if the handler interacts with rpc::Client
let future = handler(model, message, cx.clone()); let future = handler(model, message, &this, cx.clone());
let client_id = this.id; let client_id = this.id;
log::debug!( log::debug!(
@ -891,6 +978,15 @@ impl Client {
} }
} }
impl AnyWeakEntityHandle {
fn upgrade(&self, cx: &AsyncAppContext) -> Option<AnyEntityHandle> {
match self {
AnyWeakEntityHandle::Model(handle) => handle.upgrade(cx).map(AnyEntityHandle::Model),
AnyWeakEntityHandle::View(handle) => handle.upgrade(cx).map(AnyEntityHandle::View),
}
}
}
fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option<Credentials> { fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option<Credentials> {
if IMPERSONATE_LOGIN.is_some() { if IMPERSONATE_LOGIN.is_some() {
return None; return None;
@ -994,7 +1090,7 @@ mod tests {
let (done_tx1, mut done_rx1) = smol::channel::unbounded(); let (done_tx1, mut done_rx1) = smol::channel::unbounded();
let (done_tx2, mut done_rx2) = smol::channel::unbounded(); let (done_tx2, mut done_rx2) = smol::channel::unbounded();
client.add_entity_message_handler( client.add_model_message_handler(
move |model: ModelHandle<Model>, _: TypedEnvelope<proto::UnshareProject>, _, cx| { move |model: ModelHandle<Model>, _: TypedEnvelope<proto::UnshareProject>, _, cx| {
match model.read_with(&cx, |model, _| model.id) { match model.read_with(&cx, |model, _| model.id) {
1 => done_tx1.try_send(()).unwrap(), 1 => done_tx1.try_send(()).unwrap(),

View file

@ -91,7 +91,7 @@ impl FakeServer {
}); });
client client
.authenticate_and_connect(&cx.to_async()) .authenticate_and_connect(false, &cx.to_async())
.await .await
.unwrap(); .unwrap();
server server

View file

@ -55,7 +55,7 @@ impl ContactsPanel {
app_state: Arc<AppState>, app_state: Arc<AppState>,
cx: &mut LayoutContext, cx: &mut LayoutContext,
) -> ElementBox { ) -> ElementBox {
let theme = cx.app_state::<Settings>().theme.clone(); let theme = cx.global::<Settings>().theme.clone();
let theme = &theme.contacts_panel; let theme = &theme.contacts_panel;
let project_count = collaborator.projects.len(); let project_count = collaborator.projects.len();
let font_cache = cx.font_cache(); let font_cache = cx.font_cache();
@ -236,7 +236,7 @@ impl View for ContactsPanel {
} }
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox { 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()) Container::new(List::new(self.contacts.clone()).boxed())
.with_style(theme.container) .with_style(theme.container)
.boxed() .boxed()

View file

@ -25,7 +25,7 @@ use std::{
sync::Arc, sync::Arc,
}; };
use util::TryFutureExt; use util::TryFutureExt;
use workspace::{ItemHandle, ItemNavHistory, ItemViewHandle as _, Settings, Workspace}; use workspace::{ItemHandle as _, ItemNavHistory, Settings, Workspace};
action!(Deploy); action!(Deploy);
@ -38,12 +38,8 @@ pub fn init(cx: &mut MutableAppContext) {
type Event = editor::Event; type Event = editor::Event;
struct ProjectDiagnostics {
project: ModelHandle<Project>,
}
struct ProjectDiagnosticsEditor { struct ProjectDiagnosticsEditor {
model: ModelHandle<ProjectDiagnostics>, project: ModelHandle<Project>,
workspace: WeakViewHandle<Workspace>, workspace: WeakViewHandle<Workspace>,
editor: ViewHandle<Editor>, editor: ViewHandle<Editor>,
summary: DiagnosticSummary, summary: DiagnosticSummary,
@ -65,16 +61,6 @@ struct DiagnosticGroupState {
block_count: usize, block_count: usize,
} }
impl ProjectDiagnostics {
fn new(project: ModelHandle<Project>) -> Self {
Self { project }
}
}
impl Entity for ProjectDiagnostics {
type Event = ();
}
impl Entity for ProjectDiagnosticsEditor { impl Entity for ProjectDiagnosticsEditor {
type Event = Event; type Event = Event;
} }
@ -86,7 +72,7 @@ impl View for ProjectDiagnosticsEditor {
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox { fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
if self.path_states.is_empty() { 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( Label::new(
"No problems in workspace".to_string(), "No problems in workspace".to_string(),
theme.empty_message.clone(), theme.empty_message.clone(),
@ -109,12 +95,11 @@ impl View for ProjectDiagnosticsEditor {
impl ProjectDiagnosticsEditor { impl ProjectDiagnosticsEditor {
fn new( fn new(
model: ModelHandle<ProjectDiagnostics>, project_handle: ModelHandle<Project>,
workspace: WeakViewHandle<Workspace>, workspace: WeakViewHandle<Workspace>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Self { ) -> Self {
let project = model.read(cx).project.clone(); cx.subscribe(&project_handle, |this, _, event, cx| match event {
cx.subscribe(&project, |this, _, event, cx| match event {
project::Event::DiskBasedDiagnosticsFinished => { project::Event::DiskBasedDiagnosticsFinished => {
this.update_excerpts(cx); this.update_excerpts(cx);
this.update_title(cx); this.update_title(cx);
@ -126,20 +111,22 @@ impl ProjectDiagnosticsEditor {
}) })
.detach(); .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 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.set_vertical_scroll_margin(5, cx);
editor editor
}); });
cx.subscribe(&editor, |_, _, event, cx| cx.emit(*event)) cx.subscribe(&editor, |_, _, event, cx| cx.emit(*event))
.detach(); .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 paths_to_update = project.diagnostic_summaries(cx).map(|e| e.0).collect();
let summary = project.diagnostic_summary(cx);
let mut this = Self { let mut this = Self {
model, project: project_handle,
summary: project.diagnostic_summary(cx), summary,
workspace, workspace,
excerpts, excerpts,
editor, editor,
@ -151,18 +138,20 @@ impl ProjectDiagnosticsEditor {
} }
fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) { 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); workspace.activate_item(&existing, cx);
} else { } else {
let diagnostics = let workspace_handle = cx.weak_handle();
cx.add_model(|_| ProjectDiagnostics::new(workspace.project().clone())); let diagnostics = cx.add_view(|cx| {
workspace.open_item(diagnostics, 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>) { fn update_excerpts(&mut self, cx: &mut ViewContext<Self>) {
let paths = mem::take(&mut self.paths_to_update); 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| { cx.spawn(|this, mut cx| {
async move { async move {
for path in paths { for path in paths {
@ -289,7 +278,7 @@ impl ProjectDiagnosticsEditor {
prev_excerpt_id = excerpt_id.clone(); prev_excerpt_id = excerpt_id.clone();
first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone()); first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
group_state.excerpts.push(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 { if is_first_excerpt_for_group {
is_first_excerpt_for_group = false; is_first_excerpt_for_group = false;
@ -378,8 +367,7 @@ impl ProjectDiagnosticsEditor {
range_a range_a
.start .start
.cmp(&range_b.start, &snapshot) .cmp(&range_b.start, &snapshot)
.unwrap() .then_with(|| range_a.end.cmp(&range_b.end, &snapshot))
.then_with(|| range_a.end.cmp(&range_b.end, &snapshot).unwrap())
}); });
if path_state.diagnostic_groups.is_empty() { if path_state.diagnostic_groups.is_empty() {
@ -443,42 +431,17 @@ impl ProjectDiagnosticsEditor {
} }
fn update_title(&mut self, cx: &mut ViewContext<Self>) { 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); cx.emit(Event::TitleChanged);
} }
} }
impl workspace::Item for ProjectDiagnostics { impl workspace::Item for ProjectDiagnosticsEditor {
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())
}
fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox { fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox {
render_summary( render_summary(
&self.summary, &self.summary,
&style.label.text, &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 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 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 { fn is_dirty(&self, cx: &AppContext) -> bool {
@ -532,20 +499,21 @@ impl workspace::ItemView for ProjectDiagnosticsEditor {
matches!(event, Event::Saved | Event::Dirtied | Event::TitleChanged) matches!(event, Event::Saved | Event::Dirtied | Event::TitleChanged)
} }
fn clone_on_split( fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {
&self, self.editor.update(cx, |editor, _| {
nav_history: ItemNavHistory, editor.set_nav_history(Some(nav_history));
cx: &mut ViewContext<Self>, });
) -> Option<Self> }
fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
where where
Self: Sized, Self: Sized,
{ {
let diagnostics = Some(ProjectDiagnosticsEditor::new(
ProjectDiagnosticsEditor::new(self.model.clone(), self.workspace.clone(), cx); self.project.clone(),
diagnostics.editor.update(cx, |editor, _| { self.workspace.clone(),
editor.set_nav_history(Some(nav_history)); cx,
}); ))
Some(diagnostics)
} }
fn act_as_type( fn act_as_type(
@ -571,7 +539,7 @@ impl workspace::ItemView for ProjectDiagnosticsEditor {
fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock { fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
let (message, highlights) = highlight_diagnostic_message(&diagnostic.message); let (message, highlights) = highlight_diagnostic_message(&diagnostic.message);
Arc::new(move |cx| { Arc::new(move |cx| {
let settings = cx.app_state::<Settings>(); let settings = cx.global::<Settings>();
let theme = &settings.theme.editor; let theme = &settings.theme.editor;
let style = &theme.diagnostic_header; let style = &theme.diagnostic_header;
let font_size = (style.text_scale_factor * settings.buffer_font_size).round(); 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. // 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| { 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; view.next_notification(&cx).await;

View file

@ -49,7 +49,7 @@ impl View for DiagnosticSummary {
let in_progress = self.in_progress; let in_progress = self.in_progress;
MouseEventHandler::new::<Tag, _, _>(0, cx, |_, cx| { 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 { if in_progress {
Label::new( Label::new(
"Checking... ".to_string(), "Checking... ".to_string(),
@ -71,7 +71,7 @@ impl View for DiagnosticSummary {
impl StatusItemView for DiagnosticSummary { impl StatusItemView for DiagnosticSummary {
fn set_active_pane_item( fn set_active_pane_item(
&mut self, &mut self,
_: Option<&dyn workspace::ItemViewHandle>, _: Option<&dyn workspace::ItemHandle>,
_: &mut ViewContext<Self>, _: &mut ViewContext<Self>,
) { ) {
} }

View file

@ -27,6 +27,7 @@ gpui = { path = "../gpui" }
language = { path = "../language" } language = { path = "../language" }
lsp = { path = "../lsp" } lsp = { path = "../lsp" }
project = { path = "../project" } project = { path = "../project" }
rpc = { path = "../rpc" }
snippet = { path = "../snippet" } snippet = { path = "../snippet" }
sum_tree = { path = "../sum_tree" } sum_tree = { path = "../sum_tree" }
theme = { path = "../theme" } theme = { path = "../theme" }
@ -34,6 +35,7 @@ util = { path = "../util" }
workspace = { path = "../workspace" } workspace = { path = "../workspace" }
aho-corasick = "0.7" aho-corasick = "0.7"
anyhow = "1.0" anyhow = "1.0"
futures = "0.3"
itertools = "0.10" itertools = "0.10"
lazy_static = "1.4" lazy_static = "1.4"
log = "0.4" log = "0.4"

View file

@ -12,7 +12,7 @@ use gpui::{
Entity, ModelContext, ModelHandle, Entity, ModelContext, ModelHandle,
}; };
use language::{Point, Subscription as BufferSubscription}; 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 sum_tree::{Bias, TreeMap};
use tab_map::TabMap; use tab_map::TabMap;
use wrap_map::WrapMap; use wrap_map::WrapMap;
@ -36,6 +36,7 @@ pub struct DisplayMap {
wrap_map: ModelHandle<WrapMap>, wrap_map: ModelHandle<WrapMap>,
block_map: BlockMap, block_map: BlockMap,
text_highlights: TextHighlights, text_highlights: TextHighlights,
pub clip_at_line_ends: bool,
} }
impl Entity for DisplayMap { impl Entity for DisplayMap {
@ -67,6 +68,7 @@ impl DisplayMap {
wrap_map, wrap_map,
block_map, block_map,
text_highlights: Default::default(), text_highlights: Default::default(),
clip_at_line_ends: false,
} }
} }
@ -87,6 +89,7 @@ impl DisplayMap {
wraps_snapshot, wraps_snapshot,
blocks_snapshot, blocks_snapshot,
text_highlights: self.text_highlights.clone(), 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>( pub fn unfold<T: ToOffset>(
&mut self, &mut self,
ranges: impl IntoIterator<Item = Range<T>>, ranges: impl IntoIterator<Item = Range<T>>,
inclusive: bool,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) { ) {
let snapshot = self.buffer.read(cx).snapshot(cx); let snapshot = self.buffer.read(cx).snapshot(cx);
@ -124,7 +128,7 @@ impl DisplayMap {
.wrap_map .wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx)); .update(cx, |map, cx| map.sync(snapshot, edits, cx));
self.block_map.read(snapshot, edits); 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.tab_map.sync(snapshot, edits);
let (snapshot, edits) = self let (snapshot, edits) = self
.wrap_map .wrap_map
@ -204,6 +208,7 @@ pub struct DisplaySnapshot {
wraps_snapshot: wrap_map::WrapSnapshot, wraps_snapshot: wrap_map::WrapSnapshot,
blocks_snapshot: block_map::BlockSnapshot, blocks_snapshot: block_map::BlockSnapshot,
text_highlights: TextHighlights, text_highlights: TextHighlights,
clip_at_line_ends: bool,
} }
impl DisplaySnapshot { impl DisplaySnapshot {
@ -331,7 +336,12 @@ impl DisplaySnapshot {
} }
pub fn clip_point(&self, point: DisplayPoint, bias: Bias) -> DisplayPoint { 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>( 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); 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 { impl DisplayPoint {
pub fn new(row: u32, column: u32) -> Self { pub fn new(row: u32, column: u32) -> Self {
Self(BlockPoint(Point::new(row, column))) Self(BlockPoint(Point::new(row, column)))
@ -426,7 +446,6 @@ impl DisplayPoint {
Self::new(0, 0) Self::new(0, 0)
} }
#[cfg(test)]
pub fn is_zero(&self) -> bool { pub fn is_zero(&self) -> bool {
self.0.is_zero() self.0.is_zero()
} }
@ -478,16 +497,16 @@ impl ToDisplayPoint for Anchor {
} }
#[cfg(test)] #[cfg(test)]
mod tests { pub mod tests {
use super::*; use super::*;
use crate::movement; use crate::{movement, test::marked_display_snapshot};
use gpui::{color::Color, elements::*, test::observe, MutableAppContext}; use gpui::{color::Color, elements::*, test::observe, MutableAppContext};
use language::{Buffer, Language, LanguageConfig, RandomCharIter, SelectionGoal}; use language::{Buffer, Language, LanguageConfig, RandomCharIter, SelectionGoal};
use rand::{prelude::*, Rng}; use rand::{prelude::*, Rng};
use smol::stream::StreamExt; use smol::stream::StreamExt;
use std::{env, sync::Arc}; use std::{env, sync::Arc};
use theme::SyntaxTheme; use theme::SyntaxTheme;
use util::test::sample_text; use util::test::{marked_text_ranges, sample_text};
use Bias::*; use Bias::*;
#[gpui::test(iterations = 100)] #[gpui::test(iterations = 100)]
@ -620,7 +639,7 @@ mod tests {
if rng.gen() && fold_count > 0 { if rng.gen() && fold_count > 0 {
log::info!("unfolding ranges: {:?}", ranges); log::info!("unfolding ranges: {:?}", ranges);
map.update(cx, |map, cx| { map.update(cx, |map, cx| {
map.unfold(ranges, cx); map.unfold(ranges, true, cx);
}); });
} else { } else {
log::info!("folding ranges: {:?}", ranges); log::info!("folding ranges: {:?}", ranges);
@ -705,7 +724,7 @@ mod tests {
log::info!("Moving from point {:?}", point); 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); log::info!("Right {:?}", moved_right);
if point < max_point { if point < max_point {
assert!(moved_right > point); assert!(moved_right > point);
@ -719,7 +738,7 @@ mod tests {
assert_eq!(moved_right, point); 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); log::info!("Left {:?}", moved_left);
if point > min_point { if point > min_point {
assert!(moved_left < point); assert!(moved_left < point);
@ -777,15 +796,15 @@ mod tests {
DisplayPoint::new(1, 0) DisplayPoint::new(1, 0)
); );
assert_eq!( assert_eq!(
movement::right(&snapshot, DisplayPoint::new(0, 7)).unwrap(), movement::right(&snapshot, DisplayPoint::new(0, 7)),
DisplayPoint::new(1, 0) DisplayPoint::new(1, 0)
); );
assert_eq!( assert_eq!(
movement::left(&snapshot, DisplayPoint::new(1, 0)).unwrap(), movement::left(&snapshot, DisplayPoint::new(1, 0)),
DisplayPoint::new(0, 7) DisplayPoint::new(0, 7)
); );
assert_eq!( 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)) (DisplayPoint::new(0, 7), SelectionGoal::Column(10))
); );
assert_eq!( assert_eq!(
@ -793,8 +812,7 @@ mod tests {
&snapshot, &snapshot,
DisplayPoint::new(0, 7), DisplayPoint::new(0, 7),
SelectionGoal::Column(10) SelectionGoal::Column(10)
) ),
.unwrap(),
(DisplayPoint::new(1, 10), SelectionGoal::Column(10)) (DisplayPoint::new(1, 10), SelectionGoal::Column(10))
); );
assert_eq!( assert_eq!(
@ -802,8 +820,7 @@ mod tests {
&snapshot, &snapshot,
DisplayPoint::new(1, 10), DisplayPoint::new(1, 10),
SelectionGoal::Column(10) SelectionGoal::Column(10)
) ),
.unwrap(),
(DisplayPoint::new(2, 4), SelectionGoal::Column(10)) (DisplayPoint::new(2, 4), SelectionGoal::Column(10))
); );
@ -922,7 +939,7 @@ mod tests {
let map = cx let map = cx
.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx)); .add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx));
assert_eq!( assert_eq!(
cx.update(|cx| chunks(0..5, &map, &theme, cx)), cx.update(|cx| syntax_chunks(0..5, &map, &theme, cx)),
vec![ vec![
("fn ".to_string(), None), ("fn ".to_string(), None),
("outer".to_string(), Some(Color::blue())), ("outer".to_string(), Some(Color::blue())),
@ -933,7 +950,7 @@ mod tests {
] ]
); );
assert_eq!( assert_eq!(
cx.update(|cx| chunks(3..5, &map, &theme, cx)), cx.update(|cx| syntax_chunks(3..5, &map, &theme, cx)),
vec![ vec![
(" fn ".to_string(), Some(Color::red())), (" fn ".to_string(), Some(Color::red())),
("inner".to_string(), Some(Color::blue())), ("inner".to_string(), Some(Color::blue())),
@ -945,7 +962,7 @@ mod tests {
map.fold(vec![Point::new(0, 6)..Point::new(3, 2)], cx) map.fold(vec![Point::new(0, 6)..Point::new(3, 2)], cx)
}); });
assert_eq!( assert_eq!(
cx.update(|cx| chunks(0..2, &map, &theme, cx)), cx.update(|cx| syntax_chunks(0..2, &map, &theme, cx)),
vec![ vec![
("fn ".to_string(), None), ("fn ".to_string(), None),
("out".to_string(), Some(Color::blue())), ("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) DisplayMap::new(buffer, tab_size, font_id, font_size, Some(40.0), 1, 1, cx)
}); });
assert_eq!( 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), ("fn \n".to_string(), None),
("oute\nr".to_string(), Some(Color::blue())), ("oute\nr".to_string(), Some(Color::blue())),
@ -1019,7 +1036,7 @@ mod tests {
] ]
); );
assert_eq!( 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)] [("{}\n\n".to_string(), None)]
); );
@ -1027,7 +1044,7 @@ mod tests {
map.fold(vec![Point::new(0, 6)..Point::new(3, 2)], cx) map.fold(vec![Point::new(0, 6)..Point::new(3, 2)], cx)
}); });
assert_eq!( 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())), ("out".to_string(), Some(Color::blue())),
("\n".to_string(), None), ("\n".to_string(), None),
@ -1038,50 +1055,151 @@ mod tests {
} }
#[gpui::test] #[gpui::test]
fn test_clip_point(cx: &mut gpui::MutableAppContext) { async fn test_chunks_with_text_highlights(cx: &mut gpui::TestAppContext) {
use Bias::{Left, Right}; cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
let text = "\n'a', 'α',\t'✋',\t'❎', '🍐'\n"; let theme = SyntaxTheme::new(vec![
let display_text = "\n'a', 'α', '✋', '❎', '🍐'\n"; ("operator".to_string(), Color::red().into()),
let buffer = MultiBuffer::build_simple(text, cx); ("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 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 let font_id = font_cache
.select_font(family_id, &Default::default()) .select_font(family_id, &Default::default())
.unwrap(); .unwrap();
let font_size = 14.0; let font_size = 16.0;
let map = cx.add_model(|cx| { let map = cx
DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, 1, 1, cx) .add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx));
});
let map = map.update(cx, |map, cx| map.snapshot(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,
);
});
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!( assert_eq!(
map.clip_point(DisplayPoint::new(1, input_column as u32), bias), cx.update(|cx| chunks(0..10, &map, &theme, cx)),
DisplayPoint::new(1, output_column as u32), [
"clip_point(({}, {}))", ("const ".to_string(), None, None),
1, ("a".to_string(), None, Some(Color::blue())),
input_column, (":".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!(
unmarked_snapshot.clip_point(markers[1], Bias::Left),
markers[0]
);
}
assert("||", cx);
assert("|a|", cx);
assert("a|b|", cx);
assert("a|α|", cx);
} }
#[gpui::test] #[gpui::test]
@ -1163,27 +1281,38 @@ mod tests {
) )
} }
fn chunks<'a>( fn syntax_chunks<'a>(
rows: Range<u32>, rows: Range<u32>,
map: &ModelHandle<DisplayMap>, map: &ModelHandle<DisplayMap>,
theme: &'a SyntaxTheme, theme: &'a SyntaxTheme,
cx: &mut MutableAppContext, cx: &mut MutableAppContext,
) -> Vec<(String, Option<Color>)> { ) -> 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 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) { for chunk in snapshot.chunks(rows, true) {
let color = chunk let syntax_color = chunk
.syntax_highlight_id .syntax_highlight_id
.and_then(|id| id.style(theme)?.color); .and_then(|id| id.style(theme)?.color);
if let Some((last_chunk, last_color)) = chunks.last_mut() { let highlight_color = chunk.highlight_style.and_then(|style| style.color);
if color == *last_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); last_chunk.push_str(chunk.text);
} else { continue;
chunks.push((chunk.text.to_string(), color));
} }
} else {
chunks.push((chunk.text.to_string(), color));
} }
chunks.push((chunk.text.to_string(), syntax_color, highlight_color));
} }
chunks chunks
} }

View file

@ -499,7 +499,7 @@ impl<'a> BlockMapWriter<'a> {
let block_ix = match self let block_ix = match self
.0 .0
.blocks .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, Ok(ix) | Err(ix) => ix,
}; };

View file

@ -140,13 +140,14 @@ impl<'a> FoldMapWriter<'a> {
pub fn unfold<T: ToOffset>( pub fn unfold<T: ToOffset>(
&mut self, &mut self,
ranges: impl IntoIterator<Item = Range<T>>, ranges: impl IntoIterator<Item = Range<T>>,
inclusive: bool,
) -> (FoldSnapshot, Vec<FoldEdit>) { ) -> (FoldSnapshot, Vec<FoldEdit>) {
let mut edits = Vec::new(); let mut edits = Vec::new();
let mut fold_ixs_to_delete = Vec::new(); let mut fold_ixs_to_delete = Vec::new();
let buffer = self.0.buffer.lock().clone(); let buffer = self.0.buffer.lock().clone();
for range in ranges.into_iter() { for range in ranges.into_iter() {
// Remove intersecting folds and add their ranges to edits that are passed to sync. // 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() { while let Some(fold) = folds_cursor.item() {
let offset_range = fold.0.start.to_offset(&buffer)..fold.0.end.to_offset(&buffer); let offset_range = fold.0.start.to_offset(&buffer)..fold.0.end.to_offset(&buffer);
if offset_range.end > offset_range.start { if offset_range.end > offset_range.start {
@ -256,7 +257,7 @@ impl FoldMap {
let mut folds = self.folds.iter().peekable(); let mut folds = self.folds.iter().peekable();
while let Some(fold) = folds.next() { while let Some(fold) = folds.next() {
if let Some(next_fold) = folds.peek() { 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()); assert!(comparison.is_le());
} }
} }
@ -699,10 +700,7 @@ impl FoldSnapshot {
let ranges = &highlights.1; let ranges = &highlights.1;
let start_ix = match ranges.binary_search_by(|probe| { let start_ix = match ranges.binary_search_by(|probe| {
let cmp = probe let cmp = probe.end.cmp(&transform_start, &self.buffer_snapshot());
.end
.cmp(&transform_start, &self.buffer_snapshot())
.unwrap();
if cmp.is_gt() { if cmp.is_gt() {
Ordering::Greater Ordering::Greater
} else { } else {
@ -715,7 +713,6 @@ impl FoldSnapshot {
if range if range
.start .start
.cmp(&transform_end, &self.buffer_snapshot) .cmp(&transform_end, &self.buffer_snapshot)
.unwrap()
.is_ge() .is_ge()
{ {
break; break;
@ -820,8 +817,8 @@ where
let start = buffer.anchor_before(range.start.to_offset(buffer)); let start = buffer.anchor_before(range.start.to_offset(buffer));
let end = buffer.anchor_after(range.end.to_offset(buffer)); let end = buffer.anchor_after(range.end.to_offset(buffer));
let mut cursor = folds.filter::<_, usize>(move |summary| { let mut cursor = folds.filter::<_, usize>(move |summary| {
let start_cmp = start.cmp(&summary.max_end, buffer).unwrap(); let start_cmp = start.cmp(&summary.max_end, buffer);
let end_cmp = end.cmp(&summary.min_start, buffer).unwrap(); let end_cmp = end.cmp(&summary.min_start, buffer);
if inclusive { if inclusive {
start_cmp <= Ordering::Equal && end_cmp >= Ordering::Equal start_cmp <= Ordering::Equal && end_cmp >= Ordering::Equal
@ -962,19 +959,19 @@ impl sum_tree::Summary for FoldSummary {
type Context = MultiBufferSnapshot; type Context = MultiBufferSnapshot;
fn add_summary(&mut self, other: &Self, buffer: &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(); 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(); self.max_end = other.max_end.clone();
} }
#[cfg(debug_assertions)] #[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); assert!(start_comparison <= Ordering::Equal);
if 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 { impl<'a> sum_tree::SeekTarget<'a, FoldSummary, Fold> for Fold {
fn cmp(&self, other: &Self, buffer: &MultiBufferSnapshot) -> Ordering { 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 { fn cmp(&self, other: &Self) -> Ordering {
self.offset self.offset
.cmp(&other.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"); assert_eq!(snapshot4.text(), "123a…c123456eee");
let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]); 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![]); 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] #[gpui::test]
@ -1600,9 +1602,8 @@ mod tests {
.filter(|fold| { .filter(|fold| {
let start = buffer_snapshot.anchor_before(start); let start = buffer_snapshot.anchor_before(start);
let end = buffer_snapshot.anchor_after(end); let end = buffer_snapshot.anchor_after(end);
start.cmp(&fold.0.end, &buffer_snapshot).unwrap() == Ordering::Less start.cmp(&fold.0.end, &buffer_snapshot) == Ordering::Less
&& end.cmp(&fold.0.start, &buffer_snapshot).unwrap() && end.cmp(&fold.0.start, &buffer_snapshot) == Ordering::Greater
== Ordering::Greater
}) })
.map(|fold| fold.0) .map(|fold| fold.0)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@ -1680,7 +1681,7 @@ mod tests {
let buffer = self.buffer.lock().clone(); let buffer = self.buffer.lock().clone();
let mut folds = self.folds.items(&buffer); let mut folds = self.folds.items(&buffer);
// Ensure sorting doesn't change how folds get merged and displayed. // 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 let mut fold_ranges = folds
.iter() .iter()
.map(|fold| fold.0.start.to_offset(&buffer)..fold.0.end.to_offset(&buffer)) .map(|fold| fold.0.start.to_offset(&buffer)..fold.0.end.to_offset(&buffer))

File diff suppressed because it is too large Load diff

View file

@ -21,7 +21,7 @@ use gpui::{
MutableAppContext, PaintContext, Quad, Scene, SizeConstraint, ViewContext, WeakViewHandle, MutableAppContext, PaintContext, Quad, Scene, SizeConstraint, ViewContext, WeakViewHandle,
}; };
use json::json; use json::json;
use language::Bias; use language::{Bias, DiagnosticSeverity};
use smallvec::SmallVec; use smallvec::SmallVec;
use std::{ use std::{
cmp::{self, Ordering}, cmp::{self, Ordering},
@ -665,13 +665,15 @@ impl EditorElement {
} }
} }
let mut diagnostic_highlight = HighlightStyle { let mut diagnostic_highlight = HighlightStyle::default();
..Default::default()
};
if chunk.is_unnecessary { if chunk.is_unnecessary {
diagnostic_highlight.fade_out = Some(style.unnecessary_code_fade); diagnostic_highlight.fade_out = Some(style.unnecessary_code_fade);
} else if let Some(severity) = chunk.diagnostic_severity { }
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); let diagnostic_style = super::diagnostic_style(severity, true, style);
diagnostic_highlight.underline = Some(Underline { diagnostic_highlight.underline = Some(Underline {
color: Some(diagnostic_style.message.text.color), color: Some(diagnostic_style.message.text.color),
@ -679,6 +681,7 @@ impl EditorElement {
squiggly: true, squiggly: true,
}); });
} }
}
if let Some(highlight_style) = highlight_style.as_mut() { if let Some(highlight_style) = highlight_style.as_mut() {
highlight_style.highlight(diagnostic_highlight); highlight_style.highlight(diagnostic_highlight);
@ -906,7 +909,7 @@ impl Element for EditorElement {
.anchor_before(DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right)) .anchor_before(DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right))
}; };
let mut selections = HashMap::default(); let mut selections = Vec::new();
let mut active_rows = BTreeMap::new(); let mut active_rows = BTreeMap::new();
let mut highlighted_rows = None; let mut highlighted_rows = None;
let mut highlighted_ranges = Vec::new(); let mut highlighted_ranges = Vec::new();
@ -919,11 +922,32 @@ impl Element for EditorElement {
&display_map, &display_map,
); );
let mut remote_selections = HashMap::default();
for (replica_id, selection) in display_map
.buffer_snapshot
.remote_selections_in_range(&(start_anchor.clone()..end_anchor.clone()))
{
// The local selections match the leader's selections.
if Some(replica_id) == view.leader_replica_id {
continue;
}
remote_selections
.entry(replica_id)
.or_insert(Vec::new())
.push(crate::Selection {
id: selection.id,
goal: selection.goal,
reversed: selection.reversed,
start: selection.start.to_display_point(&display_map),
end: selection.end.to_display_point(&display_map),
});
}
selections.extend(remote_selections);
if view.show_local_selections { if view.show_local_selections {
let local_selections = view.local_selections_in_range( let local_selections =
start_anchor.clone()..end_anchor.clone(), view.local_selections_in_range(start_anchor..end_anchor, &display_map);
&display_map,
);
for selection in &local_selections { for selection in &local_selections {
let is_empty = selection.start == selection.end; let is_empty = selection.start == selection.end;
let selection_start = snapshot.prev_line_boundary(selection.start).1; let selection_start = snapshot.prev_line_boundary(selection.start).1;
@ -936,8 +960,12 @@ impl Element for EditorElement {
*contains_non_empty_selection |= !is_empty; *contains_non_empty_selection |= !is_empty;
} }
} }
selections.insert(
view.replica_id(cx), // Render the local selections in the leader's color when following.
let local_replica_id = view.leader_replica_id.unwrap_or(view.replica_id(cx));
selections.push((
local_replica_id,
local_selections local_selections
.into_iter() .into_iter()
.map(|selection| crate::Selection { .map(|selection| crate::Selection {
@ -948,23 +976,7 @@ impl Element for EditorElement {
end: selection.end.to_display_point(&display_map), end: selection.end.to_display_point(&display_map),
}) })
.collect(), .collect(),
); ));
}
for (replica_id, selection) in display_map
.buffer_snapshot
.remote_selections_in_range(&(start_anchor..end_anchor))
{
selections
.entry(replica_id)
.or_insert(Vec::new())
.push(crate::Selection {
id: selection.id,
goal: selection.goal,
reversed: selection.reversed,
start: selection.start.to_display_point(&display_map),
end: selection.end.to_display_point(&display_map),
});
} }
}); });
@ -1210,7 +1222,7 @@ pub struct LayoutState {
em_width: f32, em_width: f32,
em_advance: f32, em_advance: f32,
highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>, highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
selections: HashMap<ReplicaId, Vec<text::Selection<DisplayPoint>>>, selections: Vec<(ReplicaId, Vec<text::Selection<DisplayPoint>>)>,
context_menu: Option<(DisplayPoint, ElementBox)>, context_menu: Option<(DisplayPoint, ElementBox)>,
code_actions_indicator: Option<(u32, ElementBox)>, code_actions_indicator: Option<(u32, ElementBox)>,
} }
@ -1280,7 +1292,7 @@ impl PaintState {
} }
} }
#[derive(Copy, Clone)] #[derive(Copy, Clone, PartialEq, Eq)]
pub enum CursorShape { pub enum CursorShape {
Bar, Bar,
Block, Block,
@ -1487,7 +1499,7 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_layout_line_numbers(cx: &mut gpui::MutableAppContext) { 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 buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx);
let (window_id, editor) = cx.add_window(Default::default(), |cx| { let (window_id, editor) = cx.add_window(Default::default(), |cx| {
Editor::new(EditorMode::Full, buffer, None, None, cx) Editor::new(EditorMode::Full, buffer, None, None, cx)
@ -1509,7 +1521,7 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_layout_with_placeholder_text_and_blocks(cx: &mut gpui::MutableAppContext) { 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 buffer = MultiBuffer::build_simple("", cx);
let (window_id, editor) = cx.add_window(Default::default(), |cx| { let (window_id, editor) = cx.add_window(Default::default(), |cx| {
Editor::new(EditorMode::Full, buffer, None, None, cx) Editor::new(EditorMode::Full, buffer, None, None, cx)

View file

@ -1,162 +1,251 @@
use crate::{Autoscroll, Editor, Event, MultiBuffer, NavigationData, ToOffset, ToPoint as _}; use crate::{Anchor, Autoscroll, Editor, Event, ExcerptId, NavigationData, ToOffset, ToPoint as _};
use anyhow::Result; use anyhow::{anyhow, Result};
use futures::FutureExt;
use gpui::{ use gpui::{
elements::*, AppContext, Entity, ModelContext, ModelHandle, MutableAppContext, RenderContext, elements::*, geometry::vector::vec2f, AppContext, Entity, ModelHandle, MutableAppContext,
Subscription, Task, View, ViewContext, ViewHandle, WeakModelHandle, RenderContext, Subscription, Task, View, ViewContext, ViewHandle,
}; };
use language::{Bias, Buffer, Diagnostic, File as _}; use language::{Bias, Buffer, Diagnostic, File as _, SelectionGoal};
use project::{File, Project, ProjectPath}; use project::{File, Project, ProjectEntryId, ProjectPath};
use std::path::PathBuf; use rpc::proto::{self, update_view};
use std::rc::Rc; use std::{fmt::Write, path::PathBuf, time::Duration};
use std::{cell::RefCell, fmt::Write};
use text::{Point, Selection}; use text::{Point, Selection};
use util::ResultExt; use util::TryFutureExt;
use workspace::{ use workspace::{
ItemHandle, ItemNavHistory, ItemView, ItemViewHandle, NavHistory, PathOpener, Settings, FollowableItem, Item, ItemHandle, ItemNavHistory, ProjectItem, Settings, StatusItemView,
StatusItemView, WeakItemHandle, Workspace,
}; };
pub struct BufferOpener; pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
#[derive(Clone)] impl FollowableItem for Editor {
pub struct BufferItemHandle(pub ModelHandle<Buffer>); fn from_state_proto(
pane: ViewHandle<workspace::Pane>,
#[derive(Clone)] project: ModelHandle<Project>,
struct WeakBufferItemHandle(WeakModelHandle<Buffer>); state: &mut Option<proto::view::Variant>,
#[derive(Clone)]
pub struct MultiBufferItemHandle(pub ModelHandle<MultiBuffer>);
#[derive(Clone)]
struct WeakMultiBufferItemHandle(WeakModelHandle<MultiBuffer>);
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>)
});
Some(task)
}
}
impl ItemHandle for BufferItemHandle {
fn add_view(
&self,
window_id: usize,
workspace: &Workspace,
nav_history: Rc<RefCell<NavHistory>>,
cx: &mut MutableAppContext, cx: &mut MutableAppContext,
) -> Box<dyn ItemViewHandle> { ) -> Option<Task<Result<ViewHandle<Self>>>> {
let buffer = cx.add_model(|cx| MultiBuffer::singleton(self.0.clone(), cx)); let state = if matches!(state, Some(proto::view::Variant::Editor(_))) {
Box::new(cx.add_view(window_id, |cx| { if let Some(proto::view::Variant::Editor(state)) = state.take() {
let mut editor = Editor::for_buffer(buffer, Some(workspace.project().clone()), cx); state
editor.nav_history = Some(ItemNavHistory::new(nav_history, &cx.handle()));
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()
}
}
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 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 project_path(&self, _: &AppContext) -> Option<ProjectPath> {
None
}
fn id(&self) -> usize {
self.0.id()
}
}
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()
}
}
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()
}
}
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 { } else {
Box::new(MultiBufferItemHandle(self.buffer.clone())) unreachable!()
}
} else {
return None;
};
let buffer = project.update(cx, |project, cx| {
project.open_buffer_by_id(state.buffer_id, cx)
});
Some(cx.spawn(|mut cx| async move {
let buffer = buffer.await?;
let editor = pane
.read_with(&cx, |pane, cx| {
pane.items_of_type::<Self>().find(|editor| {
editor.read(cx).buffer.read(cx).as_singleton().as_ref() == Some(&buffer)
})
})
.unwrap_or_else(|| {
cx.add_view(pane.window_id(), |cx| {
Editor::for_buffer(buffer, Some(project), cx)
})
});
editor.update(&mut cx, |editor, cx| {
let excerpt_id;
let buffer_id;
{
let buffer = editor.buffer.read(cx).read(cx);
let singleton = buffer.as_singleton().unwrap();
excerpt_id = singleton.0.clone();
buffer_id = singleton.1;
}
let selections = state
.selections
.into_iter()
.map(|selection| {
deserialize_selection(&excerpt_id, buffer_id, selection)
.ok_or_else(|| anyhow!("invalid selection"))
})
.collect::<Result<Vec<_>>>()?;
if !selections.is_empty() {
editor.set_selections_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 set_leader_replica_id(
&mut self,
leader_replica_id: Option<u16>,
cx: &mut ViewContext<Self>,
) {
self.leader_replica_id = leader_replica_id;
if self.leader_replica_id.is_some() {
self.buffer.update(cx, |buffer, cx| {
buffer.remove_active_selections(cx);
});
} else {
self.buffer.update(cx, |buffer, cx| {
if self.focused {
buffer.set_active_selections(&self.selections, cx);
}
});
}
cx.notify();
}
fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
let buffer_id = self.buffer.read(cx).as_singleton()?.read(cx).remote_id();
Some(proto::view::Variant::Editor(proto::view::Editor {
buffer_id,
scroll_top_anchor: Some(language::proto::serialize_anchor(
&self.scroll_top_anchor.text_anchor,
)),
scroll_x: self.scroll_position.x(),
scroll_y: self.scroll_position.y(),
selections: self.selections.iter().map(serialize_selection).collect(),
}))
}
fn add_event_to_update_proto(
&self,
event: &Self::Event,
update: &mut Option<proto::update_view::Variant>,
_: &AppContext,
) -> bool {
let update =
update.get_or_insert_with(|| proto::update_view::Variant::Editor(Default::default()));
match update {
proto::update_view::Variant::Editor(update) => match event {
Event::ScrollPositionChanged { .. } => {
update.scroll_top_anchor = Some(language::proto::serialize_anchor(
&self.scroll_top_anchor.text_anchor,
));
update.scroll_x = self.scroll_position.x();
update.scroll_y = self.scroll_position.y();
true
}
Event::SelectionsChanged { .. } => {
update.selections = self
.selections
.iter()
.chain(self.pending_selection.as_ref().map(|p| &p.selection))
.map(serialize_selection)
.collect();
true
}
_ => false,
},
} }
} }
fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) { 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 should_unfollow_on_event(event: &Self::Event, _: &AppContext) -> bool {
match event {
Event::Edited => true,
Event::SelectionsChanged { local } => *local,
Event::ScrollPositionChanged { local } => *local,
_ => false,
}
}
}
fn serialize_selection(selection: &Selection<Anchor>) -> proto::Selection {
proto::Selection {
id: selection.id as u64,
start: Some(language::proto::serialize_anchor(
&selection.start.text_anchor,
)),
end: Some(language::proto::serialize_anchor(
&selection.end.text_anchor,
)),
reversed: selection.reversed,
}
}
fn deserialize_selection(
excerpt_id: &ExcerptId,
buffer_id: usize,
selection: proto::Selection,
) -> Option<Selection<Anchor>> {
Some(Selection {
id: selection.id as usize,
start: Anchor {
buffer_id: Some(buffer_id),
excerpt_id: excerpt_id.clone(),
text_anchor: language::proto::deserialize_anchor(selection.start?)?,
},
end: Anchor {
buffer_id: Some(buffer_id),
excerpt_id: excerpt_id.clone(),
text_anchor: language::proto::deserialize_anchor(selection.end?)?,
},
reversed: selection.reversed,
goal: SelectionGoal::None,
})
}
impl Item for Editor {
fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
if let Some(data) = data.downcast_ref::<NavigationData>() { if let Some(data) = data.downcast_ref::<NavigationData>() {
let buffer = self.buffer.read(cx).read(cx); let buffer = self.buffer.read(cx).read(cx);
let offset = if buffer.can_resolve(&data.anchor) { let offset = if buffer.can_resolve(&data.anchor) {
@ -164,11 +253,19 @@ impl ItemView for Editor {
} else { } else {
buffer.clip_offset(data.offset, Bias::Left) buffer.clip_offset(data.offset, Bias::Left)
}; };
let newest_selection = self.newest_selection_with_snapshot::<usize>(&buffer);
drop(buffer); drop(buffer);
if newest_selection.head() == offset {
false
} else {
let nav_history = self.nav_history.take(); let nav_history = self.nav_history.take();
self.select_ranges([offset..offset], Some(Autoscroll::Fit), cx); self.select_ranges([offset..offset], Some(Autoscroll::Fit), cx);
self.nav_history = nav_history; self.nav_history = nav_history;
true
}
} else {
false
} }
} }
@ -184,15 +281,19 @@ impl ItemView for Editor {
}) })
} }
fn clone_on_split( fn project_entry_id(&self, cx: &AppContext) -> Option<ProjectEntryId> {
&self, File::from_dyn(self.buffer().read(cx).file(cx)).and_then(|file| file.project_entry_id(cx))
nav_history: ItemNavHistory, }
cx: &mut ViewContext<Self>,
) -> Option<Self> fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
where where
Self: Sized, 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>) { fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
@ -219,9 +320,17 @@ impl ItemView for Editor {
) -> Task<Result<()>> { ) -> Task<Result<()>> {
let buffer = self.buffer().clone(); let buffer = self.buffer().clone();
let buffers = buffer.read(cx).all_buffers(); 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 { 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| { this.update(&mut cx, |editor, cx| {
editor.request_autoscroll(Autoscroll::Fit, 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 { pub struct CursorPosition {
position: Option<Point>, position: Option<Point>,
selected_count: usize, selected_count: usize,
@ -322,7 +443,7 @@ impl View for CursorPosition {
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox { fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
if let Some(position) = self.position { 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); let mut text = format!("{},{}", position.row + 1, position.column + 1);
if self.selected_count > 0 { if self.selected_count > 0 {
write!(text, " ({} selected)", self.selected_count).unwrap(); write!(text, " ({} selected)", self.selected_count).unwrap();
@ -337,7 +458,7 @@ impl View for CursorPosition {
impl StatusItemView for CursorPosition { impl StatusItemView for CursorPosition {
fn set_active_pane_item( fn set_active_pane_item(
&mut self, &mut self,
active_pane_item: Option<&dyn ItemViewHandle>, active_pane_item: Option<&dyn ItemHandle>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) { 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 { fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
if let Some(diagnostic) = &self.diagnostic { 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( Label::new(
diagnostic.message.split('\n').next().unwrap().to_string(), diagnostic.message.split('\n').next().unwrap().to_string(),
theme.diagnostic_message.clone(), theme.diagnostic_message.clone(),
@ -410,7 +531,7 @@ impl View for DiagnosticMessage {
impl StatusItemView for DiagnosticMessage { impl StatusItemView for DiagnosticMessage {
fn set_active_pane_item( fn set_active_pane_item(
&mut self, &mut self,
active_pane_item: Option<&dyn ItemViewHandle>, active_pane_item: Option<&dyn ItemHandle>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) { if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {

View file

@ -1,20 +1,19 @@
use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint}; use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
use crate::{char_kind, CharKind, ToPoint}; use crate::{char_kind, CharKind, ToPoint};
use anyhow::Result;
use language::Point; use language::Point;
use std::ops::Range; 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 { if point.column() > 0 {
*point.column_mut() -= 1; *point.column_mut() -= 1;
} else if point.row() > 0 { } else if point.row() > 0 {
*point.row_mut() -= 1; *point.row_mut() -= 1;
*point.column_mut() = map.line_len(point.row()); *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()); let max_column = map.line_len(point.row());
if point.column() < max_column { if point.column() < max_column {
*point.column_mut() += 1; *point.column_mut() += 1;
@ -22,14 +21,14 @@ pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> Result<DisplayPo
*point.row_mut() += 1; *point.row_mut() += 1;
*point.column_mut() = 0; *point.column_mut() = 0;
} }
Ok(map.clip_point(point, Bias::Right)) map.clip_point(point, Bias::Right)
} }
pub fn up( pub fn up(
map: &DisplaySnapshot, map: &DisplaySnapshot,
start: DisplayPoint, start: DisplayPoint,
goal: SelectionGoal, goal: SelectionGoal,
) -> Result<(DisplayPoint, SelectionGoal)> { ) -> (DisplayPoint, SelectionGoal) {
let mut goal_column = if let SelectionGoal::Column(column) = goal { let mut goal_column = if let SelectionGoal::Column(column) = goal {
column column
} else { } else {
@ -54,17 +53,17 @@ pub fn up(
Bias::Right Bias::Right
}; };
Ok(( (
map.clip_point(point, clip_bias), map.clip_point(point, clip_bias),
SelectionGoal::Column(goal_column), SelectionGoal::Column(goal_column),
)) )
} }
pub fn down( pub fn down(
map: &DisplaySnapshot, map: &DisplaySnapshot,
start: DisplayPoint, start: DisplayPoint,
goal: SelectionGoal, goal: SelectionGoal,
) -> Result<(DisplayPoint, SelectionGoal)> { ) -> (DisplayPoint, SelectionGoal) {
let mut goal_column = if let SelectionGoal::Column(column) = goal { let mut goal_column = if let SelectionGoal::Column(column) = goal {
column column
} else { } else {
@ -86,10 +85,10 @@ pub fn down(
Bias::Right Bias::Right
}; };
Ok(( (
map.clip_point(point, clip_bias), map.clip_point(point, clip_bias),
SelectionGoal::Column(goal_column), SelectionGoal::Column(goal_column),
)) )
} }
pub fn line_beginning( pub fn line_beginning(
@ -132,68 +131,110 @@ pub fn line_end(
} }
} }
pub fn prev_word_boundary(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
let mut line_start = 0; find_preceding_boundary(map, point, |left, right| {
if point.row() > 0 { (char_kind(left) != char_kind(right) && !right.is_whitespace()) || left == '\n'
if let Some(indent) = map.soft_wrap_indent(point.row() - 1) { })
line_start = indent;
}
}
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);
}
}
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;
}
let char_kind = char_kind(c);
if char_kind != prev_char_kind
&& char_kind != CharKind::Whitespace
&& char_kind != CharKind::Newline
{
*boundary.column_mut() = column;
}
prev_char_kind = char_kind;
column += c.len_utf8() as u32;
}
boundary
} }
pub fn next_word_boundary(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
let mut prev_char_kind = None; find_preceding_boundary(map, point, |left, right| {
for c in map.chars_at(point) { let is_word_start = char_kind(left) != char_kind(right) && !right.is_whitespace();
let char_kind = char_kind(c); let is_subword_start =
if let Some(prev_char_kind) = prev_char_kind { left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
if c == '\n' { is_word_start || is_subword_start || left == '\n'
})
}
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 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; break;
} }
if prev_char_kind != char_kind
&& prev_char_kind != CharKind::Whitespace if let Some(prev_ch) = prev_ch {
&& prev_char_kind != CharKind::Newline 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;
}
if let Some(boundary) = boundary {
return boundary;
} else if point.row() == 0 {
return DisplayPoint::zero();
} else {
*point.row_mut() -= 1;
}
}
}
/// 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; break;
} }
} }
if c == '\n' { if ch == '\n' {
*point.row_mut() += 1; *point.row_mut() += 1;
*point.column_mut() = 0; *point.column_mut() = 0;
} else { } 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) map.clip_point(point, Bias::Right)
} }
@ -225,9 +266,205 @@ pub fn surrounding_word(map: &DisplaySnapshot, position: DisplayPoint) -> Range<
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::{Buffer, DisplayMap, MultiBuffer}; use crate::{test::marked_display_snapshot, Buffer, DisplayMap, MultiBuffer};
use language::Point; 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] #[gpui::test]
fn test_move_up_and_down_with_excerpts(cx: &mut gpui::MutableAppContext) { fn test_move_up_and_down_with_excerpts(cx: &mut gpui::MutableAppContext) {
let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap(); let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
@ -249,180 +486,50 @@ mod tests {
); );
multibuffer multibuffer
}); });
let display_map = let display_map =
cx.add_model(|cx| DisplayMap::new(multibuffer, 2, font_id, 14.0, None, 2, 2, cx)); 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)); let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn"); assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn");
// Can't move up into the first excerpt's header // Can't move up into the first excerpt's header
assert_eq!( 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)), (DisplayPoint::new(2, 0), SelectionGoal::Column(0)),
); );
assert_eq!( 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)), (DisplayPoint::new(2, 0), SelectionGoal::Column(0)),
); );
// Move up and down within first excerpt // Move up and down within first excerpt
assert_eq!( 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)), (DisplayPoint::new(2, 3), SelectionGoal::Column(4)),
); );
assert_eq!( 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)), (DisplayPoint::new(3, 4), SelectionGoal::Column(4)),
); );
// Move up and down across second excerpt's header // Move up and down across second excerpt's header
assert_eq!( 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)), (DisplayPoint::new(3, 4), SelectionGoal::Column(5)),
); );
assert_eq!( 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)), (DisplayPoint::new(6, 5), SelectionGoal::Column(5)),
); );
// Can't move down off the end // Can't move down off the end
assert_eq!( 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)), (DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
); );
assert_eq!( 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)), (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),
);
}
} }

View file

@ -211,7 +211,7 @@ impl MultiBuffer {
pub fn singleton(buffer: ModelHandle<Buffer>, cx: &mut ModelContext<Self>) -> Self { pub fn singleton(buffer: ModelHandle<Buffer>, cx: &mut ModelContext<Self>) -> Self {
let mut this = Self::new(buffer.read(cx).replica_id()); let mut this = Self::new(buffer.read(cx).replica_id());
this.singleton = true; 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.snapshot.borrow_mut().singleton = true;
this this
} }
@ -522,24 +522,14 @@ impl MultiBuffer {
self.buffers.borrow()[&buffer_id] self.buffers.borrow()[&buffer_id]
.buffer .buffer
.update(cx, |buffer, cx| { .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 mut selections = selections.into_iter().peekable();
let merged_selections = Arc::from_iter(iter::from_fn(|| { let merged_selections = Arc::from_iter(iter::from_fn(|| {
let mut selection = selections.next()?; let mut selection = selections.next()?;
while let Some(next_selection) = selections.peek() { while let Some(next_selection) = selections.peek() {
if selection if selection.end.cmp(&next_selection.start, buffer).is_ge() {
.end
.cmp(&next_selection.start, buffer)
.unwrap()
.is_ge()
{
let next_selection = selections.next().unwrap(); let next_selection = selections.next().unwrap();
if next_selection if next_selection.end.cmp(&selection.end, buffer).is_ge() {
.end
.cmp(&selection.end, buffer)
.unwrap()
.is_ge()
{
selection.end = next_selection.end; selection.end = next_selection.end;
} }
} else { } else {
@ -814,11 +804,38 @@ impl MultiBuffer {
cx.notify(); 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 self.buffers
.borrow() .borrow()
.get(&buffer.id()) .values()
.map_or(Vec::new(), |state| state.excerpts.clone()) .flat_map(|state| state.excerpts.iter().cloned())
.collect()
} }
pub fn excerpt_containing( pub fn excerpt_containing(
@ -1407,7 +1424,7 @@ impl MultiBufferSnapshot {
); );
for ch in prev_chars { 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(); start -= ch.len_utf8();
} else { } else {
break; break;
@ -1415,7 +1432,7 @@ impl MultiBufferSnapshot {
} }
for ch in next_chars { 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(); end += ch.len_utf8();
} else { } else {
break; break;
@ -1909,11 +1926,7 @@ impl MultiBufferSnapshot {
.range .range
.start .start
.bias(anchor.text_anchor.bias, &excerpt.buffer); .bias(anchor.text_anchor.bias, &excerpt.buffer);
if text_anchor if text_anchor.cmp(&excerpt.range.end, &excerpt.buffer).is_gt() {
.cmp(&excerpt.range.end, &excerpt.buffer)
.unwrap()
.is_gt()
{
text_anchor = excerpt.range.end.clone(); text_anchor = excerpt.range.end.clone();
} }
Anchor { Anchor {
@ -1928,7 +1941,6 @@ impl MultiBufferSnapshot {
.bias(anchor.text_anchor.bias, &excerpt.buffer); .bias(anchor.text_anchor.bias, &excerpt.buffer);
if text_anchor if text_anchor
.cmp(&excerpt.range.start, &excerpt.buffer) .cmp(&excerpt.range.start, &excerpt.buffer)
.unwrap()
.is_lt() .is_lt()
{ {
text_anchor = excerpt.range.start.clone(); text_anchor = excerpt.range.start.clone();
@ -1948,7 +1960,7 @@ impl MultiBufferSnapshot {
result.push((anchor_ix, anchor, kept_position)); 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 result
} }
@ -2295,10 +2307,10 @@ impl MultiBufferSnapshot {
excerpt_id: excerpt.id.clone(), excerpt_id: excerpt.id.clone(),
text_anchor: selection.end.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(); 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(); end = range.end.clone();
} }
@ -2522,17 +2534,9 @@ impl Excerpt {
} }
fn clip_anchor(&self, text_anchor: text::Anchor) -> text::Anchor { fn clip_anchor(&self, text_anchor: text::Anchor) -> text::Anchor {
if text_anchor if text_anchor.cmp(&self.range.start, &self.buffer).is_lt() {
.cmp(&self.range.start, &self.buffer)
.unwrap()
.is_lt()
{
self.range.start.clone() self.range.start.clone()
} else if text_anchor } else if text_anchor.cmp(&self.range.end, &self.buffer).is_gt() {
.cmp(&self.range.end, &self.buffer)
.unwrap()
.is_gt()
{
self.range.end.clone() self.range.end.clone()
} else { } else {
text_anchor text_anchor
@ -2545,13 +2549,11 @@ impl Excerpt {
.range .range
.start .start
.cmp(&anchor.text_anchor, &self.buffer) .cmp(&anchor.text_anchor, &self.buffer)
.unwrap()
.is_le() .is_le()
&& self && self
.range .range
.end .end
.cmp(&anchor.text_anchor, &self.buffer) .cmp(&anchor.text_anchor, &self.buffer)
.unwrap()
.is_ge() .is_ge()
} }
} }
@ -3062,7 +3064,8 @@ mod tests {
); );
let snapshot = multibuffer.update(cx, |multibuffer, cx| { 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.remove_excerpts(&[buffer_2_excerpt_id], cx);
multibuffer.snapshot(cx) multibuffer.snapshot(cx)
}); });
@ -3357,7 +3360,7 @@ mod tests {
let bias = if rng.gen() { Bias::Left } else { Bias::Right }; let bias = if rng.gen() { Bias::Left } else { Bias::Right };
log::info!("Creating anchor at {} with bias {:?}", offset, bias); log::info!("Creating anchor at {} with bias {:?}", offset, bias);
anchors.push(multibuffer.anchor_at(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() => { 40..=44 if !anchors.is_empty() => {
let multibuffer = multibuffer.read(cx).read(cx); let multibuffer = multibuffer.read(cx).read(cx);

View file

@ -1,5 +1,4 @@
use super::{ExcerptId, MultiBufferSnapshot, ToOffset, ToPoint}; use super::{ExcerptId, MultiBufferSnapshot, ToOffset, ToPoint};
use anyhow::Result;
use std::{ use std::{
cmp::Ordering, cmp::Ordering,
ops::{Range, Sub}, ops::{Range, Sub},
@ -19,7 +18,7 @@ impl Anchor {
Self { Self {
buffer_id: None, buffer_id: None,
excerpt_id: ExcerptId::min(), excerpt_id: ExcerptId::min(),
text_anchor: text::Anchor::min(), text_anchor: text::Anchor::MIN,
} }
} }
@ -27,7 +26,7 @@ impl Anchor {
Self { Self {
buffer_id: None, buffer_id: None,
excerpt_id: ExcerptId::max(), excerpt_id: ExcerptId::max(),
text_anchor: text::Anchor::max(), text_anchor: text::Anchor::MAX,
} }
} }
@ -35,18 +34,18 @@ impl Anchor {
&self.excerpt_id &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); let excerpt_id_cmp = self.excerpt_id.cmp(&other.excerpt_id);
if excerpt_id_cmp.is_eq() { if excerpt_id_cmp.is_eq() {
if self.excerpt_id == ExcerptId::min() || self.excerpt_id == ExcerptId::max() { 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) { } else if let Some(excerpt) = snapshot.excerpt(&self.excerpt_id) {
self.text_anchor.cmp(&other.text_anchor, &excerpt.buffer) self.text_anchor.cmp(&other.text_anchor, &excerpt.buffer)
} else { } else {
Ok(Ordering::Equal) Ordering::Equal
} }
} else { } else {
Ok(excerpt_id_cmp) excerpt_id_cmp
} }
} }
@ -97,17 +96,17 @@ impl ToPoint for Anchor {
} }
pub trait AnchorRangeExt { 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_offset(&self, content: &MultiBufferSnapshot) -> Range<usize>;
fn to_point(&self, content: &MultiBufferSnapshot) -> Range<Point>; fn to_point(&self, content: &MultiBufferSnapshot) -> Range<Point>;
} }
impl AnchorRangeExt for Range<Anchor> { impl AnchorRangeExt for Range<Anchor> {
fn cmp(&self, other: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> Result<Ordering> { fn cmp(&self, other: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> Ordering {
Ok(match self.start.cmp(&other.start, buffer)? { match self.start.cmp(&other.start, buffer) {
Ordering::Equal => other.end.cmp(&self.end, buffer)?, Ordering::Equal => other.end.cmp(&self.end, buffer),
ord @ _ => ord, ord @ _ => ord,
}) }
} }
fn to_offset(&self, content: &MultiBufferSnapshot) -> Range<usize> { fn to_offset(&self, content: &MultiBufferSnapshot) -> Range<usize> {

View file

@ -1,3 +1,10 @@
use util::test::marked_text;
use crate::{
display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
DisplayPoint, MultiBuffer,
};
#[cfg(test)] #[cfg(test)]
#[ctor::ctor] #[ctor::ctor]
fn init_logger() { fn init_logger() {
@ -5,3 +12,30 @@ fn init_logger() {
env_logger::init(); 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)
}

View file

@ -21,3 +21,5 @@ postage = { version = "0.4.1", features = ["futures-traits"] }
gpui = { path = "../gpui", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] }
serde_json = { version = "1.0.64", features = ["preserve_order"] } serde_json = { version = "1.0.64", features = ["preserve_order"] }
workspace = { path = "../workspace", features = ["test-support"] } workspace = { path = "../workspace", features = ["test-support"] }
ctor = "0.1"
env_logger = "0.8"

View file

@ -67,7 +67,7 @@ impl View for FileFinder {
} }
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox { fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let settings = cx.app_state::<Settings>(); let settings = cx.global::<Settings>();
Align::new( Align::new(
ConstrainedBox::new( ConstrainedBox::new(
Container::new( Container::new(
@ -106,7 +106,7 @@ impl View for FileFinder {
impl FileFinder { impl FileFinder {
fn render_matches(&self, cx: &AppContext) -> ElementBox { fn render_matches(&self, cx: &AppContext) -> ElementBox {
if self.matches.is_empty() { if self.matches.is_empty() {
let settings = cx.app_state::<Settings>(); let settings = cx.global::<Settings>();
return Container::new( return Container::new(
Label::new( Label::new(
"No matches".into(), "No matches".into(),
@ -142,7 +142,7 @@ impl FileFinder {
fn render_match(&self, path_match: &PathMatch, index: usize, cx: &AppContext) -> ElementBox { fn render_match(&self, path_match: &PathMatch, index: usize, cx: &AppContext) -> ElementBox {
let selected_index = self.selected_index(); let selected_index = self.selected_index();
let settings = cx.app_state::<Settings>(); let settings = cx.global::<Settings>();
let style = if index == selected_index { let style = if index == selected_index {
&settings.theme.selector.active_item &settings.theme.selector.active_item
} else { } else {
@ -291,7 +291,7 @@ impl FileFinder {
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
match event { match event {
editor::Event::Edited => { editor::Event::BufferEdited { .. } => {
let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx)); let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx));
if query.is_empty() { if query.is_empty() {
self.latest_search_id = post_inc(&mut self.search_count); self.latest_search_id = post_inc(&mut self.search_count);
@ -407,16 +407,21 @@ mod tests {
use std::path::PathBuf; use std::path::PathBuf;
use workspace::{Workspace, WorkspaceParams}; use workspace::{Workspace, WorkspaceParams};
#[ctor::ctor]
fn init_logger() {
if std::env::var("RUST_LOG").is_ok() {
env_logger::init();
}
}
#[gpui::test] #[gpui::test]
async fn test_matching_paths(cx: &mut gpui::TestAppContext) { async fn test_matching_paths(cx: &mut gpui::TestAppContext) {
let mut path_openers = Vec::new();
cx.update(|cx| { cx.update(|cx| {
super::init(cx); super::init(cx);
editor::init(cx, &mut path_openers); editor::init(cx);
}); });
let mut params = cx.update(WorkspaceParams::test); let params = cx.update(WorkspaceParams::test);
params.path_openers = Arc::from(path_openers);
params params
.fs .fs
.as_fake() .as_fake()

View file

@ -59,7 +59,8 @@ impl GoToLine {
} }
fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) { 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>()) .and_then(|active_item| active_item.downcast::<Editor>())
{ {
workspace.toggle_modal(cx, |cx, _| { workspace.toggle_modal(cx, |cx, _| {
@ -101,7 +102,7 @@ impl GoToLine {
) { ) {
match event { match event {
editor::Event::Blurred => cx.emit(Event::Dismissed), editor::Event::Blurred => cx.emit(Event::Dismissed),
editor::Event::Edited => { editor::Event::BufferEdited { .. } => {
let line_editor = self.line_editor.read(cx).buffer().read(cx).read(cx).text(); let line_editor = self.line_editor.read(cx).buffer().read(cx).read(cx).text();
let mut components = line_editor.trim().split(&[',', ':'][..]); let mut components = line_editor.trim().split(&[',', ':'][..]);
let row = components.next().and_then(|row| row.parse::<u32>().ok()); let row = components.next().and_then(|row| row.parse::<u32>().ok());
@ -148,7 +149,7 @@ impl View for GoToLine {
} }
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox { 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!( let label = format!(
"{},{} of {} lines", "{},{} of {} lines",

File diff suppressed because it is too large Load diff

View file

@ -337,7 +337,7 @@ impl Deterministic {
if let Some((_, wakeup_time, _)) = state.pending_timers.first() { if let Some((_, wakeup_time, _)) = state.pending_timers.first() {
let wakeup_time = *wakeup_time; let wakeup_time = *wakeup_time;
if wakeup_time < new_now { if wakeup_time <= new_now {
let timer_count = state let timer_count = state
.pending_timers .pending_timers
.iter() .iter()

View file

@ -224,15 +224,19 @@ impl Keystroke {
key: key.unwrap(), key: key.unwrap(),
}) })
} }
pub fn modified(&self) -> bool {
self.ctrl || self.alt || self.shift || self.cmd
}
} }
impl Context { impl Context {
pub fn extend(&mut self, other: Context) { pub fn extend(&mut self, other: &Context) {
for v in other.set { for v in &other.set {
self.set.insert(v); self.set.insert(v.clone());
} }
for (k, v) in other.map { for (k, v) in &other.map {
self.map.insert(k, v); self.map.insert(k.clone(), v.clone());
} }
} }
} }

View file

@ -561,9 +561,10 @@ impl Renderer {
} }
for icon in icons { for icon in icons {
let origin = icon.bounds.origin() * scale_factor; // Snap sprite to pixel grid.
let target_size = icon.bounds.size() * scale_factor; let origin = (icon.bounds.origin() * scale_factor).floor();
let source_size = (target_size * 2.).ceil().to_i32(); let target_size = (icon.bounds.size() * scale_factor).ceil();
let source_size = (target_size * 2.).to_i32();
let sprite = let sprite =
self.sprite_cache self.sprite_cache

View file

@ -20,7 +20,7 @@ use std::{
pub struct Presenter { pub struct Presenter {
window_id: usize, window_id: usize,
rendered_views: HashMap<usize, ElementBox>, pub(crate) rendered_views: HashMap<usize, ElementBox>,
parents: HashMap<usize, usize>, parents: HashMap<usize, usize>,
font_cache: Arc<FontCache>, font_cache: Arc<FontCache>,
text_layout_cache: TextLayoutCache, text_layout_cache: TextLayoutCache,
@ -63,41 +63,36 @@ impl Presenter {
path 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(); cx.start_frame();
for view_id in invalidation.removed { for view_id in &invalidation.removed {
invalidation.updated.remove(&view_id); invalidation.updated.remove(&view_id);
self.rendered_views.remove(&view_id); self.rendered_views.remove(&view_id);
self.parents.remove(&view_id); self.parents.remove(&view_id);
} }
for view_id in invalidation.updated { for view_id in &invalidation.updated {
self.rendered_views.insert( self.rendered_views.insert(
view_id, *view_id,
cx.render_view(self.window_id, view_id, self.titlebar_height, false) cx.render_view(self.window_id, *view_id, self.titlebar_height, false)
.unwrap(), .unwrap(),
); );
} }
} }
pub fn refresh( pub fn refresh(&mut self, invalidation: &mut WindowInvalidation, cx: &mut MutableAppContext) {
&mut self, self.invalidate(invalidation, cx);
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);
}
}
for (view_id, view) in &mut self.rendered_views { for (view_id, view) in &mut self.rendered_views {
if !invalidation.updated.contains(view_id) {
*view = cx *view = cx
.render_view(self.window_id, *view_id, self.titlebar_height, true) .render_view(self.window_id, *view_id, self.titlebar_height, true)
.unwrap(); .unwrap();
} }
} }
}
pub fn build_scene( pub fn build_scene(
&mut self, &mut self,
@ -304,6 +299,10 @@ impl<'a> UpgradeViewHandle for LayoutContext<'a> {
fn upgrade_view_handle<T: View>(&self, handle: &WeakViewHandle<T>) -> Option<ViewHandle<T>> { fn upgrade_view_handle<T: View>(&self, handle: &WeakViewHandle<T>) -> Option<ViewHandle<T>> {
self.app.upgrade_view_handle(handle) self.app.upgrade_view_handle(handle)
} }
fn upgrade_any_view_handle(&self, handle: &crate::AnyWeakViewHandle) -> Option<AnyViewHandle> {
self.app.upgrade_any_view_handle(handle)
}
} }
impl<'a> ElementStateContext for LayoutContext<'a> { impl<'a> ElementStateContext for LayoutContext<'a> {

View file

@ -271,7 +271,6 @@ pub(crate) struct DiagnosticEndpoint {
#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug)] #[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug)]
pub enum CharKind { pub enum CharKind {
Newline,
Punctuation, Punctuation,
Whitespace, Whitespace,
Word, Word,
@ -1621,8 +1620,13 @@ impl BufferSnapshot {
let range = range.start.to_offset(self)..range.end.to_offset(self); let range = range.start.to_offset(self)..range.end.to_offset(self);
let mut cursor = tree.root_node().walk(); let mut cursor = tree.root_node().walk();
// Descend to smallest leaf that touches or exceeds the start of the range. // Descend to the first leaf that touches the start of the range,
while cursor.goto_first_child_for_byte(range.start).is_some() {} // 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. // Ascend to the smallest ancestor that strictly contains the range.
loop { 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 let Some(right_node) = right_node {
if right_node.is_named() || !left_node.is_named() { if right_node.is_named() || !left_node.is_named() {
return Some(right_node.byte_range()); 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) .min_by_key(|(open_range, close_range)| close_range.end - open_range.start)
} }
/*
impl BufferSnapshot
pub fn remote_selections_in_range(&self, Range<Anchor>) -> impl Iterator<Item = (ReplicaId, impl Iterator<Item = &Selection<Anchor>>)>
pub fn remote_selections_in_range(&self, Range<Anchor>) -> impl Iterator<Item = (ReplicaId, i
*/
pub fn remote_selections_in_range<'a>( pub fn remote_selections_in_range<'a>(
&'a self, &'a self,
range: Range<Anchor>, range: Range<Anchor>,
@ -1840,20 +1841,12 @@ impl BufferSnapshot {
}) })
.map(move |(replica_id, set)| { .map(move |(replica_id, set)| {
let start_ix = match set.selections.binary_search_by(|probe| { let start_ix = match set.selections.binary_search_by(|probe| {
probe probe.end.cmp(&range.start, self).then(Ordering::Greater)
.end
.cmp(&range.start, self)
.unwrap()
.then(Ordering::Greater)
}) { }) {
Ok(ix) | Err(ix) => ix, Ok(ix) | Err(ix) => ix,
}; };
let end_ix = match set.selections.binary_search_by(|probe| { let end_ix = match set.selections.binary_search_by(|probe| {
probe probe.start.cmp(&range.end, self).then(Ordering::Less)
.start
.cmp(&range.end, self)
.unwrap()
.then(Ordering::Less)
}) { }) {
Ok(ix) | Err(ix) => ix, Ok(ix) | Err(ix) => ix,
}; };
@ -2280,9 +2273,7 @@ pub fn contiguous_ranges(
} }
pub fn char_kind(c: char) -> CharKind { pub fn char_kind(c: char) -> CharKind {
if c == '\n' { if c.is_whitespace() {
CharKind::Newline
} else if c.is_whitespace() {
CharKind::Whitespace CharKind::Whitespace
} else if c.is_alphanumeric() || c == '_' { } else if c.is_alphanumeric() || c == '_' {
CharKind::Word CharKind::Word

View file

@ -81,8 +81,8 @@ impl DiagnosticSet {
let range = buffer.anchor_before(range.start)..buffer.anchor_at(range.end, end_bias); let range = buffer.anchor_before(range.start)..buffer.anchor_at(range.end, end_bias);
let mut cursor = self.diagnostics.filter::<_, ()>({ let mut cursor = self.diagnostics.filter::<_, ()>({
move |summary: &Summary| { move |summary: &Summary| {
let start_cmp = range.start.cmp(&summary.max_end, buffer).unwrap(); let start_cmp = range.start.cmp(&summary.max_end, buffer);
let end_cmp = range.end.cmp(&summary.min_start, buffer).unwrap(); let end_cmp = range.end.cmp(&summary.min_start, buffer);
if inclusive { if inclusive {
start_cmp <= Ordering::Equal && end_cmp >= Ordering::Equal start_cmp <= Ordering::Equal && end_cmp >= Ordering::Equal
} else { } else {
@ -123,7 +123,7 @@ impl DiagnosticSet {
let start_ix = output.len(); let start_ix = output.len();
output.extend(groups.into_values().filter_map(|mut entries| { 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 entries
.iter() .iter()
.position(|entry| entry.diagnostic.is_primary) .position(|entry| entry.diagnostic.is_primary)
@ -137,7 +137,6 @@ impl DiagnosticSet {
.range .range
.start .start
.cmp(&b.entries[b.primary_ix].range.start, buffer) .cmp(&b.entries[b.primary_ix].range.start, buffer)
.unwrap()
}); });
} }
@ -187,10 +186,10 @@ impl DiagnosticEntry<Anchor> {
impl Default for Summary { impl Default for Summary {
fn default() -> Self { fn default() -> Self {
Self { Self {
start: Anchor::min(), start: Anchor::MIN,
end: Anchor::max(), end: Anchor::MAX,
min_start: Anchor::max(), min_start: Anchor::MAX,
max_end: Anchor::min(), max_end: Anchor::MIN,
count: 0, count: 0,
} }
} }
@ -200,15 +199,10 @@ impl sum_tree::Summary for Summary {
type Context = text::BufferSnapshot; type Context = text::BufferSnapshot;
fn add_summary(&mut self, other: &Self, buffer: &Self::Context) { fn add_summary(&mut self, other: &Self, buffer: &Self::Context) {
if other if other.min_start.cmp(&self.min_start, buffer).is_lt() {
.min_start
.cmp(&self.min_start, buffer)
.unwrap()
.is_lt()
{
self.min_start = other.min_start.clone(); 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.max_end = other.max_end.clone();
} }
self.start = other.start.clone(); self.start = other.start.clone();

View file

@ -100,15 +100,16 @@ pub fn serialize_undo_map_entry(
} }
pub fn serialize_selections(selections: &Arc<[Selection<Anchor>]>) -> Vec<proto::Selection> { pub fn serialize_selections(selections: &Arc<[Selection<Anchor>]>) -> Vec<proto::Selection> {
selections selections.iter().map(serialize_selection).collect()
.iter() }
.map(|selection| proto::Selection {
pub fn serialize_selection(selection: &Selection<Anchor>) -> proto::Selection {
proto::Selection {
id: selection.id as u64, id: selection.id as u64,
start: Some(serialize_anchor(&selection.start)), start: Some(serialize_anchor(&selection.start)),
end: Some(serialize_anchor(&selection.end)), end: Some(serialize_anchor(&selection.end)),
reversed: selection.reversed, reversed: selection.reversed,
}) }
.collect()
} }
pub fn serialize_diagnostics<'a>( pub fn serialize_diagnostics<'a>(
@ -274,7 +275,12 @@ pub fn deserialize_selections(selections: Vec<proto::Selection>) -> Arc<[Selecti
Arc::from( Arc::from(
selections selections
.into_iter() .into_iter()
.filter_map(|selection| { .filter_map(deserialize_selection)
.collect::<Vec<_>>(),
)
}
pub fn deserialize_selection(selection: proto::Selection) -> Option<Selection<Anchor>> {
Some(Selection { Some(Selection {
id: selection.id as usize, id: selection.id as usize,
start: deserialize_anchor(selection.start?)?, start: deserialize_anchor(selection.start?)?,
@ -282,9 +288,6 @@ pub fn deserialize_selections(selections: Vec<proto::Selection>) -> Arc<[Selecti
reversed: selection.reversed, reversed: selection.reversed,
goal: SelectionGoal::None, goal: SelectionGoal::None,
}) })
})
.collect::<Vec<_>>(),
)
} }
pub fn deserialize_diagnostics( pub fn deserialize_diagnostics(

View file

@ -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] #[gpui::test]
fn test_edit_with_autoindent(cx: &mut MutableAppContext) { fn test_edit_with_autoindent(cx: &mut MutableAppContext) {
cx.add_model(|cx| { cx.add_model(|cx| {
@ -839,7 +877,7 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) {
for buffer in &buffers { for buffer in &buffers {
let buffer = buffer.read(cx).snapshot(); let buffer = buffer.read(cx).snapshot();
let actual_remote_selections = buffer 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<_>>())) .map(|(replica_id, selections)| (replica_id, selections.collect::<Vec<_>>()))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let expected_remote_selections = active_selections let expected_remote_selections = active_selections

View file

@ -556,7 +556,14 @@ type FakeLanguageServerHandlers = Arc<
Mutex< Mutex<
HashMap< HashMap<
&'static str, &'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,12 +592,17 @@ impl LanguageServer {
let (stdout_writer, stdout_reader) = async_pipe::pipe(); let (stdout_writer, stdout_reader) = async_pipe::pipe();
let mut fake = FakeLanguageServer::new(stdin_reader, stdout_writer, cx); let mut fake = FakeLanguageServer::new(stdin_reader, stdout_writer, cx);
fake.handle_request::<request::Initialize, _>({ fake.handle_request::<request::Initialize, _, _>({
let capabilities = capabilities.clone(); let capabilities = capabilities.clone();
move |_, _| InitializeResult { move |_, _| {
capabilities: capabilities.clone(), let capabilities = capabilities.clone();
async move {
InitializeResult {
capabilities,
..Default::default() ..Default::default()
} }
}
}
}); });
let executor = cx.background().clone(); let executor = cx.background().clone();
@ -628,7 +640,8 @@ impl FakeLanguageServer {
let response; let response;
if let Some(handler) = handlers.lock().get_mut(request.method) { if let Some(handler) = handlers.lock().get_mut(request.method) {
response = 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); log::debug!("handled lsp request. method:{}", request.method);
} else { } else {
response = serde_json::to_vec(&AnyResponse { response = serde_json::to_vec(&AnyResponse {
@ -704,19 +717,25 @@ impl FakeLanguageServer {
} }
} }
pub fn handle_request<T, F>( pub fn handle_request<T, F, Fut>(
&mut self, &mut self,
mut handler: F, mut handler: F,
) -> futures::channel::mpsc::UnboundedReceiver<()> ) -> futures::channel::mpsc::UnboundedReceiver<()>
where where
T: 'static + request::Request, 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(); let (responded_tx, responded_rx) = futures::channel::mpsc::unbounded();
self.handlers.lock().insert( self.handlers.lock().insert(
T::METHOD, T::METHOD,
Box::new(move |id, params, cx| { Box::new(move |id, params, cx| {
let result = handler(serde_json::from_slice::<T::Params>(params).unwrap(), cx); let result = handler(serde_json::from_slice::<T::Params>(params).unwrap(), cx);
let responded_tx = responded_tx.clone();
async move {
let result = result.await;
let result = serde_json::to_string(&result).unwrap(); let result = serde_json::to_string(&result).unwrap();
let result = serde_json::from_str::<&RawValue>(&result).unwrap(); let result = serde_json::from_str::<&RawValue>(&result).unwrap();
let response = AnyResponse { let response = AnyResponse {
@ -726,6 +745,8 @@ impl FakeLanguageServer {
}; };
responded_tx.unbounded_send(()).ok(); responded_tx.unbounded_send(()).ok();
serde_json::to_vec(&response).unwrap() serde_json::to_vec(&response).unwrap()
}
.boxed()
}), }),
); );
responded_rx responded_rx
@ -844,7 +865,7 @@ mod tests {
"file://b/c" "file://b/c"
); );
fake.handle_request::<request::Shutdown, _>(|_, _| ()); fake.handle_request::<request::Shutdown, _, _>(|_, _| async move {});
drop(server); drop(server);
fake.receive_notification::<notification::Exit>().await; fake.receive_notification::<notification::Exit>().await;

View file

@ -69,7 +69,7 @@ impl View for OutlineView {
} }
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox { fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let settings = cx.app_state::<Settings>(); let settings = cx.global::<Settings>();
Flex::new(Axis::Vertical) Flex::new(Axis::Vertical)
.with_child( .with_child(
@ -124,9 +124,12 @@ impl OutlineView {
.active_item(cx) .active_item(cx)
.and_then(|item| item.downcast::<Editor>()) .and_then(|item| item.downcast::<Editor>())
{ {
let buffer = editor.read(cx).buffer().read(cx).read(cx).outline(Some( let buffer = editor
cx.app_state::<Settings>().theme.editor.syntax.as_ref(), .read(cx)
)); .buffer()
.read(cx)
.read(cx)
.outline(Some(cx.global::<Settings>().theme.editor.syntax.as_ref()));
if let Some(outline) = buffer { if let Some(outline) = buffer {
workspace.toggle_modal(cx, |cx, _| { workspace.toggle_modal(cx, |cx, _| {
let view = cx.add_view(|cx| OutlineView::new(outline, editor, cx)); let view = cx.add_view(|cx| OutlineView::new(outline, editor, cx));
@ -221,7 +224,7 @@ impl OutlineView {
) { ) {
match event { match event {
editor::Event::Blurred => cx.emit(Event::Dismissed), editor::Event::Blurred => cx.emit(Event::Dismissed),
editor::Event::Edited => self.update_matches(cx), editor::Event::BufferEdited { .. } => self.update_matches(cx),
_ => {} _ => {}
} }
} }
@ -288,7 +291,7 @@ impl OutlineView {
fn render_matches(&self, cx: &AppContext) -> ElementBox { fn render_matches(&self, cx: &AppContext) -> ElementBox {
if self.matches.is_empty() { if self.matches.is_empty() {
let settings = cx.app_state::<Settings>(); let settings = cx.global::<Settings>();
return Container::new( return Container::new(
Label::new( Label::new(
"No matches".into(), "No matches".into(),
@ -330,7 +333,7 @@ impl OutlineView {
index: usize, index: usize,
cx: &AppContext, cx: &AppContext,
) -> ElementBox { ) -> ElementBox {
let settings = cx.app_state::<Settings>(); let settings = cx.global::<Settings>();
let style = if index == self.selected_match_index { let style = if index == self.selected_match_index {
&settings.theme.selector.active_item &settings.theme.selector.active_item
} else { } else {

View file

@ -11,15 +11,15 @@ use collections::{hash_map, BTreeMap, HashMap, HashSet};
use futures::{future::Shared, Future, FutureExt, StreamExt, TryFutureExt}; use futures::{future::Shared, Future, FutureExt, StreamExt, TryFutureExt};
use fuzzy::{PathMatch, PathMatchCandidate, PathMatchCandidateSet}; use fuzzy::{PathMatch, PathMatchCandidate, PathMatchCandidateSet};
use gpui::{ use gpui::{
AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task, AnyModelHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
UpgradeModelHandle, WeakModelHandle, MutableAppContext, Task, UpgradeModelHandle, WeakModelHandle,
}; };
use language::{ use language::{
proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
range_from_lsp, Anchor, Bias, Buffer, CodeAction, CodeLabel, Completion, Diagnostic, range_from_lsp, Anchor, Bias, Buffer, CodeAction, CodeLabel, Completion, Diagnostic,
DiagnosticEntry, DiagnosticSet, Event as BufferEvent, File as _, Language, LanguageRegistry, DiagnosticEntry, DiagnosticSet, Event as BufferEvent, File as _, Language, LanguageRegistry,
LocalFile, OffsetRangeExt, Operation, PointUtf16, TextBufferSnapshot, ToLspPosition, ToOffset, LocalFile, OffsetRangeExt, Operation, Patch, PointUtf16, TextBufferSnapshot, ToLspPosition,
ToPointUtf16, Transaction, ToOffset, ToPointUtf16, Transaction,
}; };
use lsp::{DiagnosticSeverity, DiagnosticTag, DocumentHighlightKind, LanguageServer}; use lsp::{DiagnosticSeverity, DiagnosticTag, DocumentHighlightKind, LanguageServer};
use lsp_command::*; use lsp_command::*;
@ -39,7 +39,7 @@ use std::{
path::{Component, Path, PathBuf}, path::{Component, Path, PathBuf},
rc::Rc, rc::Rc,
sync::{ sync::{
atomic::{AtomicBool, AtomicUsize}, atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
Arc, Arc,
}, },
time::Instant, time::Instant,
@ -49,9 +49,13 @@ use util::{post_inc, ResultExt, TryFutureExt as _};
pub use fs::*; pub use fs::*;
pub use worktree::*; pub use worktree::*;
pub trait Item: Entity {
fn entry_id(&self, cx: &AppContext) -> Option<ProjectEntryId>;
}
pub struct Project { pub struct Project {
worktrees: Vec<WorktreeHandle>, worktrees: Vec<WorktreeHandle>,
active_entry: Option<ProjectEntry>, active_entry: Option<ProjectEntryId>,
languages: Arc<LanguageRegistry>, languages: Arc<LanguageRegistry>,
language_servers: HashMap<(WorktreeId, Arc<str>), Arc<LanguageServer>>, language_servers: HashMap<(WorktreeId, Arc<str>), Arc<LanguageServer>>,
started_language_servers: HashMap<(WorktreeId, Arc<str>), Task<Option<Arc<LanguageServer>>>>, started_language_servers: HashMap<(WorktreeId, Arc<str>), Task<Option<Arc<LanguageServer>>>>,
@ -114,12 +118,14 @@ pub struct Collaborator {
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub enum Event { pub enum Event {
ActiveEntryChanged(Option<ProjectEntry>), ActiveEntryChanged(Option<ProjectEntryId>),
WorktreeRemoved(WorktreeId), WorktreeRemoved(WorktreeId),
DiskBasedDiagnosticsStarted, DiskBasedDiagnosticsStarted,
DiskBasedDiagnosticsUpdated, DiskBasedDiagnosticsUpdated,
DiskBasedDiagnosticsFinished, DiskBasedDiagnosticsFinished,
DiagnosticsUpdated(ProjectPath), DiagnosticsUpdated(ProjectPath),
RemoteIdChanged(Option<u64>),
CollaboratorLeft(PeerId),
} }
enum LanguageServerEvent { enum LanguageServerEvent {
@ -226,42 +232,58 @@ impl DiagnosticSummary {
} }
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] #[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct ProjectEntry { pub struct ProjectEntryId(usize);
pub worktree_id: WorktreeId,
pub entry_id: 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 { impl Project {
pub fn init(client: &Arc<Client>) { pub fn init(client: &Arc<Client>) {
client.add_entity_message_handler(Self::handle_add_collaborator); client.add_model_message_handler(Self::handle_add_collaborator);
client.add_entity_message_handler(Self::handle_buffer_reloaded); client.add_model_message_handler(Self::handle_buffer_reloaded);
client.add_entity_message_handler(Self::handle_buffer_saved); client.add_model_message_handler(Self::handle_buffer_saved);
client.add_entity_message_handler(Self::handle_start_language_server); client.add_model_message_handler(Self::handle_start_language_server);
client.add_entity_message_handler(Self::handle_update_language_server); client.add_model_message_handler(Self::handle_update_language_server);
client.add_entity_message_handler(Self::handle_remove_collaborator); client.add_model_message_handler(Self::handle_remove_collaborator);
client.add_entity_message_handler(Self::handle_register_worktree); client.add_model_message_handler(Self::handle_register_worktree);
client.add_entity_message_handler(Self::handle_unregister_worktree); client.add_model_message_handler(Self::handle_unregister_worktree);
client.add_entity_message_handler(Self::handle_unshare_project); client.add_model_message_handler(Self::handle_unshare_project);
client.add_entity_message_handler(Self::handle_update_buffer_file); client.add_model_message_handler(Self::handle_update_buffer_file);
client.add_entity_message_handler(Self::handle_update_buffer); client.add_model_message_handler(Self::handle_update_buffer);
client.add_entity_message_handler(Self::handle_update_diagnostic_summary); client.add_model_message_handler(Self::handle_update_diagnostic_summary);
client.add_entity_message_handler(Self::handle_update_worktree); client.add_model_message_handler(Self::handle_update_worktree);
client.add_entity_request_handler(Self::handle_apply_additional_edits_for_completion); client.add_model_request_handler(Self::handle_apply_additional_edits_for_completion);
client.add_entity_request_handler(Self::handle_apply_code_action); client.add_model_request_handler(Self::handle_apply_code_action);
client.add_entity_request_handler(Self::handle_format_buffers); client.add_model_request_handler(Self::handle_format_buffers);
client.add_entity_request_handler(Self::handle_get_code_actions); client.add_model_request_handler(Self::handle_get_code_actions);
client.add_entity_request_handler(Self::handle_get_completions); client.add_model_request_handler(Self::handle_get_completions);
client.add_entity_request_handler(Self::handle_lsp_command::<GetDefinition>); client.add_model_request_handler(Self::handle_lsp_command::<GetDefinition>);
client.add_entity_request_handler(Self::handle_lsp_command::<GetDocumentHighlights>); client.add_model_request_handler(Self::handle_lsp_command::<GetDocumentHighlights>);
client.add_entity_request_handler(Self::handle_lsp_command::<GetReferences>); client.add_model_request_handler(Self::handle_lsp_command::<GetReferences>);
client.add_entity_request_handler(Self::handle_lsp_command::<PrepareRename>); client.add_model_request_handler(Self::handle_lsp_command::<PrepareRename>);
client.add_entity_request_handler(Self::handle_lsp_command::<PerformRename>); client.add_model_request_handler(Self::handle_lsp_command::<PerformRename>);
client.add_entity_request_handler(Self::handle_search_project); client.add_model_request_handler(Self::handle_search_project);
client.add_entity_request_handler(Self::handle_get_project_symbols); client.add_model_request_handler(Self::handle_get_project_symbols);
client.add_entity_request_handler(Self::handle_open_buffer_for_symbol); client.add_model_request_handler(Self::handle_open_buffer_for_symbol);
client.add_entity_request_handler(Self::handle_open_buffer); client.add_model_request_handler(Self::handle_open_buffer_by_id);
client.add_entity_request_handler(Self::handle_save_buffer); client.add_model_request_handler(Self::handle_open_buffer_by_path);
client.add_model_request_handler(Self::handle_save_buffer);
} }
pub fn local( pub fn local(
@ -280,31 +302,11 @@ impl Project {
let mut status = rpc.status(); let mut status = rpc.status();
while let Some(status) = status.next().await { while let Some(status) = status.next().await {
if let Some(this) = this.upgrade(&cx) { if let Some(this) = this.upgrade(&cx) {
let remote_id = if status.is_connected() { if status.is_connected() {
let response = rpc.request(proto::RegisterProject {}).await?; this.update(&mut cx, |this, cx| this.register(cx)).await?;
Some(response.project_id)
} else { } else {
None this.update(&mut cx, |this, cx| this.unregister(cx));
};
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.set_remote_id(remote_id, cx));
} }
} }
Ok(()) Ok(())
@ -355,7 +357,7 @@ impl Project {
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
cx: &mut AsyncAppContext, cx: &mut AsyncAppContext,
) -> Result<ModelHandle<Self>> { ) -> Result<ModelHandle<Self>> {
client.authenticate_and_connect(&cx).await?; client.authenticate_and_connect(true, &cx).await?;
let response = client let response = client
.request(proto::JoinProject { .request(proto::JoinProject {
@ -468,7 +470,6 @@ impl Project {
cx.update(|cx| Project::local(client, user_store, languages, fs, cx)) cx.update(|cx| Project::local(client, user_store, languages, fs, cx))
} }
#[cfg(any(test, feature = "test-support"))]
pub fn buffer_for_id(&self, remote_id: u64, cx: &AppContext) -> Option<ModelHandle<Buffer>> { pub fn buffer_for_id(&self, remote_id: u64, cx: &AppContext) -> Option<ModelHandle<Buffer>> {
self.opened_buffers self.opened_buffers
.get(&remote_id) .get(&remote_id)
@ -537,16 +538,54 @@ impl Project {
&self.fs &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 { 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(); 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> { pub fn remote_id(&self) -> Option<u64> {
@ -623,6 +662,24 @@ impl Project {
.find(|worktree| worktree.read(cx).id() == id) .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<()>> { pub fn share(&self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
let rpc = self.client.clone(); let rpc = self.client.clone();
cx.spawn(|this, mut cx| async move { cx.spawn(|this, mut cx| async move {
@ -685,19 +742,35 @@ 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(); 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 { if let ProjectClientState::Local {
is_shared, is_shared,
remote_id_rx, remote_id_rx,
.. ..
} = &mut this.client_state } = &mut self.client_state
{ {
*is_shared = false; if !*is_shared {
return;
}
for open_buffer in this.opened_buffers.values_mut() { *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());
}
}
}
for open_buffer in self.opened_buffers.values_mut() {
match open_buffer { match open_buffer {
OpenBuffer::Strong(buffer) => { OpenBuffer::Strong(buffer) => {
*open_buffer = OpenBuffer::Weak(buffer.downgrade()); *open_buffer = OpenBuffer::Weak(buffer.downgrade());
@ -706,38 +779,14 @@ impl Project {
} }
} }
for worktree_handle in this.worktrees.iter_mut() { if let Some(project_id) = *remote_id_rx.borrow() {
match worktree_handle { rpc.send(proto::UnshareProject { project_id }).log_err();
WorktreeHandle::Strong(worktree) => {
if !worktree.read(cx).is_visible() {
*worktree_handle = WorktreeHandle::Weak(worktree.downgrade());
}
}
_ => {}
}
} }
remote_id_rx cx.notify();
.borrow()
.ok_or_else(|| anyhow!("no project id"))
} else { } else {
Err(anyhow!("can't share a remote project")) log::error!("attempted to unshare 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();
});
}
cx.notify()
});
Ok(())
})
} }
fn project_unshared(&mut self, cx: &mut ModelContext<Self>) { fn project_unshared(&mut self, cx: &mut ModelContext<Self>) {
@ -785,6 +834,23 @@ impl Project {
Ok(buffer) 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( pub fn open_buffer(
&mut self, &mut self,
path: impl Into<ProjectPath>, path: impl Into<ProjectPath>,
@ -876,7 +942,7 @@ impl Project {
let path_string = path.to_string_lossy().to_string(); let path_string = path.to_string_lossy().to_string();
cx.spawn(|this, mut cx| async move { cx.spawn(|this, mut cx| async move {
let response = rpc let response = rpc
.request(proto::OpenBuffer { .request(proto::OpenBufferByPath {
project_id, project_id,
worktree_id: remote_worktree_id.to_proto(), worktree_id: remote_worktree_id.to_proto(),
path: path_string, path: path_string,
@ -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( pub fn save_buffer_as(
&mut self, &mut self,
buffer: ModelHandle<Buffer>, buffer: ModelHandle<Buffer>,
@ -1096,7 +1188,7 @@ impl Project {
}); });
cx.background().spawn(request).detach_and_log_err(cx); cx.background().spawn(request).detach_and_log_err(cx);
} }
BufferEvent::Edited => { BufferEvent::Edited { .. } => {
let language_server = self let language_server = self
.language_server_for_buffer(buffer.read(cx), cx)? .language_server_for_buffer(buffer.read(cx), cx)?
.clone(); .clone();
@ -1783,38 +1875,23 @@ impl Project {
}); });
let mut sanitized_diagnostics = Vec::new(); let mut sanitized_diagnostics = Vec::new();
let mut edits_since_save = snapshot let edits_since_save = Patch::new(
snapshot
.edits_since::<PointUtf16>(buffer.read(cx).saved_version()) .edits_since::<PointUtf16>(buffer.read(cx).saved_version())
.peekable(); .collect(),
let mut last_edit_old_end = PointUtf16::zero(); );
let mut last_edit_new_end = PointUtf16::zero(); for entry in diagnostics {
'outer: for entry in diagnostics { let start;
let mut start = entry.range.start; let end;
let mut end = entry.range.end; if entry.diagnostic.is_disk_based {
// Some diagnostics are based on files on disk instead of buffers' // Some diagnostics are based on files on disk instead of buffers'
// current contents. Adjust these diagnostics' ranges to reflect // current contents. Adjust these diagnostics' ranges to reflect
// any unsaved edits. // any unsaved edits.
if entry.diagnostic.is_disk_based { start = edits_since_save.old_to_new(entry.range.start);
while let Some(edit) = edits_since_save.peek() { end = edits_since_save.old_to_new(entry.range.end);
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 { } else {
break; start = entry.range.start;
} end = entry.range.end;
}
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;
} }
let mut range = snapshot.clip_point_utf16(start, Bias::Left) 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 new_active_entry = entry.and_then(|project_path| {
let worktree = self.worktree_for_id(project_path.worktree_id, cx)?; let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
let entry = worktree.read(cx).entry_for_path(project_path.path)?; let entry = worktree.read(cx).entry_for_path(project_path.path)?;
Some(ProjectEntry { Some(entry.id)
worktree_id: project_path.worktree_id,
entry_id: entry.id,
})
}); });
if new_active_entry != self.active_entry { if new_active_entry != self.active_entry {
self.active_entry = new_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 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 // RPC message handlers
async fn handle_unshare_project( async fn handle_unshare_project(
@ -3274,6 +3363,7 @@ impl Project {
buffer.update(cx, |buffer, cx| buffer.remove_peer(replica_id, cx)); buffer.update(cx, |buffer, cx| buffer.remove_peer(replica_id, cx));
} }
} }
cx.emit(Event::CollaboratorLeft(peer_id));
cx.notify(); cx.notify();
Ok(()) Ok(())
}) })
@ -3821,9 +3911,28 @@ impl Project {
hasher.finalize().as_slice().try_into().unwrap() hasher.finalize().as_slice().try_into().unwrap()
} }
async fn handle_open_buffer( async fn handle_open_buffer_by_id(
this: ModelHandle<Self>, this: ModelHandle<Self>,
envelope: TypedEnvelope<proto::OpenBuffer>, envelope: TypedEnvelope<proto::OpenBufferById>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<proto::OpenBufferResponse> {
let peer_id = envelope.original_sender_id()?;
let buffer = this
.update(&mut cx, |this, cx| {
this.open_buffer_by_id(envelope.payload.id, cx)
})
.await?;
this.update(&mut cx, |this, cx| {
Ok(proto::OpenBufferResponse {
buffer: Some(this.serialize_buffer_for_peer(&buffer, peer_id, cx)),
})
})
}
async fn handle_open_buffer_by_path(
this: ModelHandle<Self>,
envelope: TypedEnvelope<proto::OpenBufferByPath>,
_: Arc<Client>, _: Arc<Client>,
mut cx: AsyncAppContext, mut cx: AsyncAppContext,
) -> Result<proto::OpenBufferResponse> { ) -> Result<proto::OpenBufferResponse> {
@ -4477,6 +4586,12 @@ fn relativize_path(base: &Path, path: &Path) -> PathBuf {
components.iter().map(|c| c.as_os_str()).collect() 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)] #[cfg(test)]
mod tests { mod tests {
use super::{Event, *}; use super::{Event, *};
@ -4897,7 +5012,7 @@ mod tests {
} }
#[gpui::test] #[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(); cx.foreground().forbid_parking();
let (mut lsp_config, mut fake_servers) = LanguageServerConfig::fake(); let (mut lsp_config, mut fake_servers) = LanguageServerConfig::fake();
@ -5122,11 +5237,13 @@ mod tests {
buffer.update(cx, |buffer, cx| { buffer.update(cx, |buffer, cx| {
buffer.edit(Some(Point::new(2, 0)..Point::new(2, 0)), " ", 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(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 = let change_notification_2 = fake_server
fake_server.receive_notification::<lsp::notification::DidChangeTextDocument>(); .receive_notification::<lsp::notification::DidChangeTextDocument>()
.await;
assert!( assert!(
change_notification_2.await.text_document.version change_notification_2.text_document.version
> change_notification_1.text_document.version > change_notification_1.text_document.version
); );
@ -5134,7 +5251,7 @@ mod tests {
fake_server.notify::<lsp::notification::PublishDiagnostics>( fake_server.notify::<lsp::notification::PublishDiagnostics>(
lsp::PublishDiagnosticsParams { lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path("/dir/a.rs").unwrap(), 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![ diagnostics: vec![
lsp::Diagnostic { lsp::Diagnostic {
range: lsp::Range::new(lsp::Position::new(1, 9), lsp::Position::new(1, 11)), range: lsp::Range::new(lsp::Position::new(1, 9), lsp::Position::new(1, 11)),
@ -5174,7 +5291,7 @@ mod tests {
} }
}, },
DiagnosticEntry { DiagnosticEntry {
range: Point::new(3, 9)..Point::new(3, 11), range: Point::new(3, 9)..Point::new(3, 14),
diagnostic: Diagnostic { diagnostic: Diagnostic {
severity: DiagnosticSeverity::ERROR, severity: DiagnosticSeverity::ERROR,
message: "undefined variable 'BB'".to_string(), message: "undefined variable 'BB'".to_string(),
@ -5672,7 +5789,7 @@ mod tests {
.unwrap(); .unwrap();
let mut fake_server = fake_servers.next().await.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; let params = params.text_document_position_params;
assert_eq!( assert_eq!(
params.text_document.uri.to_file_path().unwrap(), params.text_document.uri.to_file_path().unwrap(),
@ -6607,7 +6724,7 @@ mod tests {
project.prepare_rename(buffer.clone(), 7, cx) project.prepare_rename(buffer.clone(), 7, cx)
}); });
fake_server 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.text_document.uri.as_str(), "file:///dir/one.rs");
assert_eq!(params.position, lsp::Position::new(0, 7)); assert_eq!(params.position, lsp::Position::new(0, 7));
Some(lsp::PrepareRenameResponse::Range(lsp::Range::new( Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
@ -6626,7 +6743,7 @@ mod tests {
project.perform_rename(buffer.clone(), 7, "THREE".to_string(), true, cx) project.perform_rename(buffer.clone(), 7, "THREE".to_string(), true, cx)
}); });
fake_server fake_server
.handle_request::<lsp::request::Rename, _>(|params, _| { .handle_request::<lsp::request::Rename, _, _>(|params, _| async move {
assert_eq!( assert_eq!(
params.text_document_position.text_document.uri.as_str(), params.text_document_position.text_document.uri.as_str(),
"file:///dir/one.rs" "file:///dir/one.rs"

View file

@ -1,3 +1,5 @@
use crate::ProjectEntryId;
use super::{ use super::{
fs::{self, Fs}, fs::{self, Fs},
ignore::IgnoreStack, ignore::IgnoreStack,
@ -39,10 +41,7 @@ use std::{
future::Future, future::Future,
ops::{Deref, DerefMut}, ops::{Deref, DerefMut},
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::{ sync::{atomic::AtomicUsize, Arc},
atomic::{AtomicUsize, Ordering::SeqCst},
Arc,
},
time::{Duration, SystemTime}, time::{Duration, SystemTime},
}; };
use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap}; use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap};
@ -101,7 +100,7 @@ pub struct LocalSnapshot {
abs_path: Arc<Path>, abs_path: Arc<Path>,
scan_id: usize, scan_id: usize,
ignores: HashMap<Arc<Path>, (Arc<Gitignore>, 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>, next_entry_id: Arc<AtomicUsize>,
snapshot: Snapshot, snapshot: Snapshot,
} }
@ -712,7 +711,9 @@ impl LocalWorktree {
let worktree = this.as_local_mut().unwrap(); let worktree = this.as_local_mut().unwrap();
match response { match response {
Ok(_) => { Ok(_) => {
if worktree.registration == Registration::Pending {
worktree.registration = Registration::Done { project_id }; worktree.registration = Registration::Done { project_id };
}
Ok(()) Ok(())
} }
Err(error) => { Err(error) => {
@ -809,6 +810,11 @@ impl LocalWorktree {
}) })
} }
pub fn unregister(&mut self) {
self.unshare();
self.registration = Registration::None;
}
pub fn unshare(&mut self) { pub fn unshare(&mut self) {
self.share.take(); self.share.take();
} }
@ -856,13 +862,16 @@ impl Snapshot {
self.id 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<()> { pub(crate) fn apply_remote_update(&mut self, update: proto::UpdateWorktree) -> Result<()> {
let mut entries_by_path_edits = Vec::new(); let mut entries_by_path_edits = Vec::new();
let mut entries_by_id_edits = Vec::new(); let mut entries_by_id_edits = Vec::new();
for entry_id in update.removed_entries { for entry_id in update.removed_entries {
let entry_id = entry_id as usize;
let entry = self let entry = self
.entry_for_id(entry_id) .entry_for_id(ProjectEntryId::from_proto(entry_id))
.ok_or_else(|| anyhow!("unknown entry"))?; .ok_or_else(|| anyhow!("unknown entry"))?;
entries_by_path_edits.push(Edit::Remove(PathKey(entry.path.clone()))); entries_by_path_edits.push(Edit::Remove(PathKey(entry.path.clone())));
entries_by_id_edits.push(Edit::Remove(entry.id)); 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, &())?; let entry = self.entries_by_id.get(&id, &())?;
self.entry_for_path(&entry.path) self.entry_for_path(&entry.path)
} }
@ -1062,7 +1071,7 @@ impl LocalSnapshot {
other_entries.next(); other_entries.next();
} }
Ordering::Greater => { Ordering::Greater => {
removed_entries.push(other_entry.id as u64); removed_entries.push(other_entry.id.to_proto());
other_entries.next(); other_entries.next();
} }
} }
@ -1073,7 +1082,7 @@ impl LocalSnapshot {
self_entries.next(); self_entries.next();
} }
(None, Some(other_entry)) => { (None, Some(other_entry)) => {
removed_entries.push(other_entry.id as u64); removed_entries.push(other_entry.id.to_proto());
other_entries.next(); other_entries.next();
} }
(None, None) => break, (None, None) => break,
@ -1326,7 +1335,7 @@ pub struct File {
pub worktree: ModelHandle<Worktree>, pub worktree: ModelHandle<Worktree>,
pub path: Arc<Path>, pub path: Arc<Path>,
pub mtime: SystemTime, pub mtime: SystemTime,
pub(crate) entry_id: Option<usize>, pub(crate) entry_id: Option<ProjectEntryId>,
pub(crate) is_local: bool, pub(crate) is_local: bool,
} }
@ -1423,7 +1432,7 @@ impl language::File for File {
fn to_proto(&self) -> rpc::proto::File { fn to_proto(&self) -> rpc::proto::File {
rpc::proto::File { rpc::proto::File {
worktree_id: self.worktree.id() as u64, 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(), path: self.path.to_string_lossy().into(),
mtime: Some(self.mtime.into()), mtime: Some(self.mtime.into()),
} }
@ -1490,7 +1499,7 @@ impl File {
worktree, worktree,
path: Path::new(&proto.path).into(), path: Path::new(&proto.path).into(),
mtime: proto.mtime.ok_or_else(|| anyhow!("no timestamp"))?.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, is_local: false,
}) })
} }
@ -1502,11 +1511,15 @@ impl File {
pub fn worktree_id(&self, cx: &AppContext) -> WorktreeId { pub fn worktree_id(&self, cx: &AppContext) -> WorktreeId {
self.worktree.read(cx).id() self.worktree.read(cx).id()
} }
pub fn project_entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
self.entry_id
}
} }
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct Entry { pub struct Entry {
pub id: usize, pub id: ProjectEntryId,
pub kind: EntryKind, pub kind: EntryKind,
pub path: Arc<Path>, pub path: Arc<Path>,
pub inode: u64, pub inode: u64,
@ -1530,7 +1543,7 @@ impl Entry {
root_char_bag: CharBag, root_char_bag: CharBag,
) -> Self { ) -> Self {
Self { Self {
id: next_entry_id.fetch_add(1, SeqCst), id: ProjectEntryId::new(next_entry_id),
kind: if metadata.is_dir { kind: if metadata.is_dir {
EntryKind::PendingDir EntryKind::PendingDir
} else { } else {
@ -1620,7 +1633,7 @@ impl sum_tree::Summary for EntrySummary {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
struct PathEntry { struct PathEntry {
id: usize, id: ProjectEntryId,
path: Arc<Path>, path: Arc<Path>,
is_ignored: bool, is_ignored: bool,
scan_id: usize, scan_id: usize,
@ -1635,7 +1648,7 @@ impl sum_tree::Item for PathEntry {
} }
impl sum_tree::KeyedItem for PathEntry { impl sum_tree::KeyedItem for PathEntry {
type Key = usize; type Key = ProjectEntryId;
fn key(&self) -> Self::Key { fn key(&self) -> Self::Key {
self.id self.id
@ -1644,7 +1657,7 @@ impl sum_tree::KeyedItem for PathEntry {
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
struct PathEntrySummary { struct PathEntrySummary {
max_id: usize, max_id: ProjectEntryId,
} }
impl sum_tree::Summary for PathEntrySummary { 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, _: &()) { fn add_summary(&mut self, summary: &'a PathEntrySummary, _: &()) {
*self = summary.max_id; *self = summary.max_id;
} }
@ -2345,7 +2358,7 @@ impl<'a> Iterator for ChildEntriesIter<'a> {
impl<'a> From<&'a Entry> for proto::Entry { impl<'a> From<&'a Entry> for proto::Entry {
fn from(entry: &'a Entry) -> Self { fn from(entry: &'a Entry) -> Self {
Self { Self {
id: entry.id as u64, id: entry.id.to_proto(),
is_dir: entry.is_dir(), is_dir: entry.is_dir(),
path: entry.path.to_string_lossy().to_string(), path: entry.path.to_string_lossy().to_string(),
inode: entry.inode, 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)); let path: Arc<Path> = Arc::from(Path::new(&entry.path));
Ok(Entry { Ok(Entry {
id: entry.id as usize, id: ProjectEntryId::from_proto(entry.id),
kind, kind,
path: path.clone(), path: path.clone(),
inode: entry.inode, inode: entry.inode,

View file

@ -9,7 +9,7 @@ use gpui::{
AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, View, ViewContext, AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, View, ViewContext,
ViewHandle, WeakViewHandle, ViewHandle, WeakViewHandle,
}; };
use project::{Project, ProjectEntry, ProjectPath, Worktree, WorktreeId}; use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
use std::{ use std::{
collections::{hash_map, HashMap}, collections::{hash_map, HashMap},
ffi::OsStr, ffi::OsStr,
@ -24,7 +24,7 @@ pub struct ProjectPanel {
project: ModelHandle<Project>, project: ModelHandle<Project>,
list: UniformListState, list: UniformListState,
visible_entries: Vec<(WorktreeId, Vec<usize>)>, visible_entries: Vec<(WorktreeId, Vec<usize>)>,
expanded_dir_ids: HashMap<WorktreeId, Vec<usize>>, expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>,
selection: Option<Selection>, selection: Option<Selection>,
handle: WeakViewHandle<Self>, handle: WeakViewHandle<Self>,
} }
@ -32,7 +32,7 @@ pub struct ProjectPanel {
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
struct Selection { struct Selection {
worktree_id: WorktreeId, worktree_id: WorktreeId,
entry_id: usize, entry_id: ProjectEntryId,
index: usize, index: usize,
} }
@ -47,8 +47,8 @@ struct EntryDetails {
action!(ExpandSelectedEntry); action!(ExpandSelectedEntry);
action!(CollapseSelectedEntry); action!(CollapseSelectedEntry);
action!(ToggleExpanded, ProjectEntry); action!(ToggleExpanded, ProjectEntryId);
action!(Open, ProjectEntry); action!(Open, ProjectEntryId);
pub fn init(cx: &mut MutableAppContext) { pub fn init(cx: &mut MutableAppContext) {
cx.add_action(ProjectPanel::expand_selected_entry); cx.add_action(ProjectPanel::expand_selected_entry);
@ -64,10 +64,7 @@ pub fn init(cx: &mut MutableAppContext) {
} }
pub enum Event { pub enum Event {
OpenedEntry { OpenedEntry(ProjectEntryId),
worktree_id: WorktreeId,
entry_id: usize,
},
} }
impl ProjectPanel { impl ProjectPanel {
@ -78,16 +75,16 @@ impl ProjectPanel {
cx.notify(); cx.notify();
}) })
.detach(); .detach();
cx.subscribe(&project, |this, _, event, cx| match event { cx.subscribe(&project, |this, project, event, cx| match event {
project::Event::ActiveEntryChanged(Some(ProjectEntry { project::Event::ActiveEntryChanged(Some(entry_id)) => {
worktree_id, if let Some(worktree_id) = project.read(cx).worktree_id_for_entry(*entry_id, cx)
entry_id, {
})) => { this.expand_entry(worktree_id, *entry_id, cx);
this.expand_entry(*worktree_id, *entry_id, cx); this.update_visible_entries(Some((worktree_id, *entry_id)), cx);
this.update_visible_entries(Some((*worktree_id, *entry_id)), cx);
this.autoscroll(); this.autoscroll();
cx.notify(); cx.notify();
} }
}
project::Event::WorktreeRemoved(id) => { project::Event::WorktreeRemoved(id) => {
this.expanded_dir_ids.remove(id); this.expanded_dir_ids.remove(id);
this.update_visible_entries(None, cx); this.update_visible_entries(None, cx);
@ -109,16 +106,13 @@ impl ProjectPanel {
this this
}); });
cx.subscribe(&project_panel, move |workspace, _, event, cx| match event { cx.subscribe(&project_panel, move |workspace, _, event, cx| match event {
&Event::OpenedEntry { &Event::OpenedEntry(entry_id) => {
worktree_id, if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) {
entry_id,
} => {
if let Some(worktree) = project.read(cx).worktree_for_id(worktree_id, cx) {
if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) { if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) {
workspace workspace
.open_path( .open_path(
ProjectPath { ProjectPath {
worktree_id, worktree_id: worktree.read(cx).id(),
path: entry.path.clone(), path: entry.path.clone(),
}, },
cx, cx,
@ -152,10 +146,7 @@ impl ProjectPanel {
} }
} }
} else { } else {
let event = Event::OpenedEntry { let event = Event::OpenedEntry(entry.id);
worktree_id: worktree.id(),
entry_id: entry.id,
};
cx.emit(event); cx.emit(event);
} }
} }
@ -193,11 +184,8 @@ impl ProjectPanel {
} }
fn toggle_expanded(&mut self, action: &ToggleExpanded, cx: &mut ViewContext<Self>) { fn toggle_expanded(&mut self, action: &ToggleExpanded, cx: &mut ViewContext<Self>) {
let ProjectEntry { let entry_id = action.0;
worktree_id, if let Some(worktree_id) = self.project.read(cx).worktree_id_for_entry(entry_id, cx) {
entry_id,
} = action.0;
if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) { if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) {
match expanded_dir_ids.binary_search(&entry_id) { match expanded_dir_ids.binary_search(&entry_id) {
Ok(ix) => { Ok(ix) => {
@ -211,6 +199,7 @@ impl ProjectPanel {
cx.focus_self(); cx.focus_self();
} }
} }
}
fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) { fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
if let Some(selection) = self.selection { if let Some(selection) = self.selection {
@ -229,10 +218,7 @@ impl ProjectPanel {
} }
fn open_entry(&mut self, action: &Open, cx: &mut ViewContext<Self>) { fn open_entry(&mut self, action: &Open, cx: &mut ViewContext<Self>) {
cx.emit(Event::OpenedEntry { cx.emit(Event::OpenedEntry(action.0));
worktree_id: action.0.worktree_id,
entry_id: action.0.entry_id,
});
} }
fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) { fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
@ -313,7 +299,7 @@ impl ProjectPanel {
fn update_visible_entries( fn update_visible_entries(
&mut self, &mut self,
new_selected_entry: Option<(WorktreeId, usize)>, new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
let worktrees = self let worktrees = self
@ -379,7 +365,7 @@ impl ProjectPanel {
fn expand_entry( fn expand_entry(
&mut self, &mut self,
worktree_id: WorktreeId, worktree_id: WorktreeId,
entry_id: usize, entry_id: ProjectEntryId,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
let project = self.project.read(cx); let project = self.project.read(cx);
@ -411,7 +397,7 @@ impl ProjectPanel {
&self, &self,
range: Range<usize>, range: Range<usize>,
cx: &mut ViewContext<ProjectPanel>, 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; let mut ix = 0;
for (worktree_id, visible_worktree_entries) in &self.visible_entries { 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 e.worktree_id == snapshot.id() && e.entry_id == entry.id
}), }),
}; };
let entry = ProjectEntry { callback(entry.id, details, cx);
worktree_id: snapshot.id(),
entry_id: entry.id,
};
callback(entry, details, cx);
} }
} }
} }
@ -463,13 +445,13 @@ impl ProjectPanel {
} }
fn render_entry( fn render_entry(
entry: ProjectEntry, entry_id: ProjectEntryId,
details: EntryDetails, details: EntryDetails,
theme: &theme::ProjectPanel, theme: &theme::ProjectPanel,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> ElementBox { ) -> ElementBox {
let is_dir = details.is_dir; 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) { let style = match (details.is_selected, state.hovered) {
(false, false) => &theme.entry, (false, false) => &theme.entry,
(false, true) => &theme.hovered_entry, (false, true) => &theme.hovered_entry,
@ -519,9 +501,9 @@ impl ProjectPanel {
}) })
.on_click(move |cx| { .on_click(move |cx| {
if is_dir { if is_dir {
cx.dispatch_action(ToggleExpanded(entry)) cx.dispatch_action(ToggleExpanded(entry_id))
} else { } else {
cx.dispatch_action(Open(entry)) cx.dispatch_action(Open(entry_id))
} }
}) })
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
@ -535,7 +517,7 @@ impl View for ProjectPanel {
} }
fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox { 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 mut container_style = theme.container;
let padding = std::mem::take(&mut container_style.padding); let padding = std::mem::take(&mut container_style.padding);
let handle = self.handle.clone(); let handle = self.handle.clone();
@ -546,7 +528,7 @@ impl View for ProjectPanel {
.map(|(_, worktree_entries)| worktree_entries.len()) .map(|(_, worktree_entries)| worktree_entries.len())
.sum(), .sum(),
move |range, items, cx| { 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(); let this = handle.upgrade(cx).unwrap();
this.update(cx.app, |this, cx| { this.update(cx.app, |this, cx| {
this.for_each_visible_entry(range.clone(), cx, |entry, details, cx| { this.for_each_visible_entry(range.clone(), cx, |entry, details, cx| {
@ -830,13 +812,7 @@ mod tests {
let worktree = worktree.read(cx); let worktree = worktree.read(cx);
if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) { if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
let entry_id = worktree.entry_for_path(relative_path).unwrap().id; let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
panel.toggle_expanded( panel.toggle_expanded(&ToggleExpanded(entry_id), cx);
&ToggleExpanded(ProjectEntry {
worktree_id: worktree.id(),
entry_id,
}),
cx,
);
return; return;
} }
} }

View file

@ -1,6 +1,5 @@
use editor::{ use editor::{
combine_syntax_and_fuzzy_match_highlights, items::BufferItemHandle, styled_runs_for_code_label, combine_syntax_and_fuzzy_match_highlights, styled_runs_for_code_label, Autoscroll, Bias, Editor,
Autoscroll, Bias, Editor,
}; };
use fuzzy::{StringMatch, StringMatchCandidate}; use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{ use gpui::{
@ -70,7 +69,7 @@ impl View for ProjectSymbolsView {
} }
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox { fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let settings = cx.app_state::<Settings>(); let settings = cx.global::<Settings>();
Flex::new(Axis::Vertical) Flex::new(Axis::Vertical)
.with_child( .with_child(
Container::new(ChildView::new(&self.query_editor).boxed()) Container::new(ChildView::new(&self.query_editor).boxed())
@ -234,7 +233,7 @@ impl ProjectSymbolsView {
fn render_matches(&self, cx: &AppContext) -> ElementBox { fn render_matches(&self, cx: &AppContext) -> ElementBox {
if self.matches.is_empty() { if self.matches.is_empty() {
let settings = cx.app_state::<Settings>(); let settings = cx.global::<Settings>();
return Container::new( return Container::new(
Label::new( Label::new(
"No matches".into(), "No matches".into(),
@ -277,7 +276,7 @@ impl ProjectSymbolsView {
show_worktree_root_name: bool, show_worktree_root_name: bool,
cx: &AppContext, cx: &AppContext,
) -> ElementBox { ) -> ElementBox {
let settings = cx.app_state::<Settings>(); let settings = cx.global::<Settings>();
let style = if index == self.selected_match_index { let style = if index == self.selected_match_index {
&settings.theme.selector.active_item &settings.theme.selector.active_item
} else { } else {
@ -329,7 +328,7 @@ impl ProjectSymbolsView {
) { ) {
match event { match event {
editor::Event::Blurred => cx.emit(Event::Dismissed), editor::Event::Blurred => cx.emit(Event::Dismissed),
editor::Event::Edited => self.update_matches(cx), editor::Event::BufferEdited { .. } => self.update_matches(cx),
_ => {} _ => {}
} }
} }
@ -346,6 +345,7 @@ impl ProjectSymbolsView {
let buffer = workspace let buffer = workspace
.project() .project()
.update(cx, |project, cx| project.open_buffer_for_symbol(symbol, cx)); .update(cx, |project, cx| project.open_buffer_for_symbol(symbol, cx));
let symbol = symbol.clone(); let symbol = symbol.clone();
cx.spawn(|workspace, mut cx| async move { cx.spawn(|workspace, mut cx| async move {
let buffer = buffer.await?; let buffer = buffer.await?;
@ -353,10 +353,8 @@ impl ProjectSymbolsView {
let position = buffer let position = buffer
.read(cx) .read(cx)
.clip_point_utf16(symbol.range.start, Bias::Left); .clip_point_utf16(symbol.range.start, Bias::Left);
let editor = workspace
.open_item(BufferItemHandle(buffer), cx) let editor = workspace.open_project_item::<Editor>(buffer, cx);
.downcast::<Editor>()
.unwrap();
editor.update(cx, |editor, cx| { editor.update(cx, |editor, cx| {
editor.select_ranges( editor.select_ranges(
[position..position], [position..position],

View file

@ -40,8 +40,9 @@ message Envelope {
StartLanguageServer start_language_server = 33; StartLanguageServer start_language_server = 33;
UpdateLanguageServer update_language_server = 34; UpdateLanguageServer update_language_server = 34;
OpenBuffer open_buffer = 35; OpenBufferById open_buffer_by_id = 35;
OpenBufferResponse open_buffer_response = 36; OpenBufferByPath open_buffer_by_path = 36;
OpenBufferResponse open_buffer_response = 37;
UpdateBuffer update_buffer = 38; UpdateBuffer update_buffer = 38;
UpdateBufferFile update_buffer_file = 39; UpdateBufferFile update_buffer_file = 39;
SaveBuffer save_buffer = 40; SaveBuffer save_buffer = 40;
@ -79,6 +80,11 @@ message Envelope {
GetUsers get_users = 70; GetUsers get_users = 70;
GetUsersResponse get_users_response = 71; GetUsersResponse get_users_response = 71;
Follow follow = 72;
FollowResponse follow_response = 73;
UpdateFollowers update_followers = 74;
Unfollow unfollow = 75;
} }
} }
@ -241,12 +247,17 @@ message OpenBufferForSymbolResponse {
Buffer buffer = 1; Buffer buffer = 1;
} }
message OpenBuffer { message OpenBufferByPath {
uint64 project_id = 1; uint64 project_id = 1;
uint64 worktree_id = 2; uint64 worktree_id = 2;
string path = 3; string path = 3;
} }
message OpenBufferById {
uint64 project_id = 1;
uint64 id = 2;
}
message OpenBufferResponse { message OpenBufferResponse {
Buffer buffer = 1; Buffer buffer = 1;
} }
@ -521,8 +532,77 @@ message UpdateContacts {
repeated Contact contacts = 1; repeated Contact contacts = 1;
} }
message UpdateDiagnostics {
uint32 replica_id = 1;
uint32 lamport_timestamp = 2;
repeated Diagnostic diagnostics = 3;
}
message Follow {
uint64 project_id = 1;
uint32 leader_id = 2;
}
message FollowResponse {
optional uint64 active_view_id = 1;
repeated View views = 2;
}
message UpdateFollowers {
uint64 project_id = 1;
repeated uint32 follower_ids = 2;
oneof variant {
UpdateActiveView update_active_view = 3;
View create_view = 4;
UpdateView update_view = 5;
}
}
message Unfollow {
uint64 project_id = 1;
uint32 leader_id = 2;
}
// Entities // Entities
message UpdateActiveView {
optional uint64 id = 1;
optional uint32 leader_id = 2;
}
message UpdateView {
uint64 id = 1;
optional uint32 leader_id = 2;
oneof variant {
Editor editor = 3;
}
message Editor {
repeated Selection selections = 1;
Anchor scroll_top_anchor = 2;
float scroll_x = 3;
float scroll_y = 4;
}
}
message View {
uint64 id = 1;
optional uint32 leader_id = 2;
oneof variant {
Editor editor = 3;
}
message Editor {
uint64 buffer_id = 1;
repeated Selection selections = 2;
Anchor scroll_top_anchor = 3;
float scroll_x = 4;
float scroll_y = 5;
}
}
message Collaborator { message Collaborator {
uint32 peer_id = 1; uint32 peer_id = 1;
uint32 replica_id = 2; uint32 replica_id = 2;
@ -578,17 +658,6 @@ message BufferState {
repeated string completion_triggers = 8; repeated string completion_triggers = 8;
} }
message BufferFragment {
uint32 replica_id = 1;
uint32 local_timestamp = 2;
uint32 lamport_timestamp = 3;
uint32 insertion_offset = 4;
uint32 len = 5;
bool visible = 6;
repeated VectorClockEntry deletions = 7;
repeated VectorClockEntry max_undos = 8;
}
message SelectionSet { message SelectionSet {
uint32 replica_id = 1; uint32 replica_id = 1;
repeated Selection selections = 2; repeated Selection selections = 2;
@ -614,12 +683,6 @@ enum Bias {
Right = 1; Right = 1;
} }
message UpdateDiagnostics {
uint32 replica_id = 1;
uint32 lamport_timestamp = 2;
repeated Diagnostic diagnostics = 3;
}
message Diagnostic { message Diagnostic {
Anchor start = 1; Anchor start = 1;
Anchor end = 2; Anchor end = 2;

View file

@ -96,7 +96,7 @@ pub struct ConnectionState {
const KEEPALIVE_INTERVAL: Duration = Duration::from_secs(1); const KEEPALIVE_INTERVAL: Duration = Duration::from_secs(1);
const WRITE_TIMEOUT: Duration = Duration::from_secs(2); 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 { impl Peer {
pub fn new() -> Arc<Self> { pub fn new() -> Arc<Self> {

View file

@ -147,6 +147,8 @@ messages!(
(BufferSaved, Foreground), (BufferSaved, Foreground),
(ChannelMessageSent, Foreground), (ChannelMessageSent, Foreground),
(Error, Foreground), (Error, Foreground),
(Follow, Foreground),
(FollowResponse, Foreground),
(FormatBuffers, Foreground), (FormatBuffers, Foreground),
(FormatBuffersResponse, Foreground), (FormatBuffersResponse, Foreground),
(GetChannelMessages, Foreground), (GetChannelMessages, Foreground),
@ -175,7 +177,8 @@ messages!(
(UpdateLanguageServer, Foreground), (UpdateLanguageServer, Foreground),
(LeaveChannel, Foreground), (LeaveChannel, Foreground),
(LeaveProject, Foreground), (LeaveProject, Foreground),
(OpenBuffer, Background), (OpenBufferById, Background),
(OpenBufferByPath, Background),
(OpenBufferForSymbol, Background), (OpenBufferForSymbol, Background),
(OpenBufferForSymbolResponse, Background), (OpenBufferForSymbolResponse, Background),
(OpenBufferResponse, Background), (OpenBufferResponse, Background),
@ -195,13 +198,15 @@ messages!(
(SendChannelMessageResponse, Foreground), (SendChannelMessageResponse, Foreground),
(ShareProject, Foreground), (ShareProject, Foreground),
(Test, Foreground), (Test, Foreground),
(Unfollow, Foreground),
(UnregisterProject, Foreground), (UnregisterProject, Foreground),
(UnregisterWorktree, Foreground), (UnregisterWorktree, Foreground),
(UnshareProject, Foreground), (UnshareProject, Foreground),
(UpdateBuffer, Background), (UpdateBuffer, Foreground),
(UpdateBufferFile, Foreground), (UpdateBufferFile, Foreground),
(UpdateContacts, Foreground), (UpdateContacts, Foreground),
(UpdateDiagnosticSummary, Foreground), (UpdateDiagnosticSummary, Foreground),
(UpdateFollowers, Foreground),
(UpdateWorktree, Foreground), (UpdateWorktree, Foreground),
); );
@ -211,6 +216,7 @@ request_messages!(
ApplyCompletionAdditionalEdits, ApplyCompletionAdditionalEdits,
ApplyCompletionAdditionalEditsResponse ApplyCompletionAdditionalEditsResponse
), ),
(Follow, FollowResponse),
(FormatBuffers, FormatBuffersResponse), (FormatBuffers, FormatBuffersResponse),
(GetChannelMessages, GetChannelMessagesResponse), (GetChannelMessages, GetChannelMessagesResponse),
(GetChannels, GetChannelsResponse), (GetChannels, GetChannelsResponse),
@ -223,7 +229,8 @@ request_messages!(
(GetUsers, GetUsersResponse), (GetUsers, GetUsersResponse),
(JoinChannel, JoinChannelResponse), (JoinChannel, JoinChannelResponse),
(JoinProject, JoinProjectResponse), (JoinProject, JoinProjectResponse),
(OpenBuffer, OpenBufferResponse), (OpenBufferById, OpenBufferResponse),
(OpenBufferByPath, OpenBufferResponse),
(OpenBufferForSymbol, OpenBufferForSymbolResponse), (OpenBufferForSymbol, OpenBufferForSymbolResponse),
(Ping, Ack), (Ping, Ack),
(PerformRename, PerformRenameResponse), (PerformRename, PerformRenameResponse),
@ -246,6 +253,7 @@ entity_messages!(
ApplyCompletionAdditionalEdits, ApplyCompletionAdditionalEdits,
BufferReloaded, BufferReloaded,
BufferSaved, BufferSaved,
Follow,
FormatBuffers, FormatBuffers,
GetCodeActions, GetCodeActions,
GetCompletions, GetCompletions,
@ -255,7 +263,8 @@ entity_messages!(
GetProjectSymbols, GetProjectSymbols,
JoinProject, JoinProject,
LeaveProject, LeaveProject,
OpenBuffer, OpenBufferById,
OpenBufferByPath,
OpenBufferForSymbol, OpenBufferForSymbol,
PerformRename, PerformRename,
PrepareRename, PrepareRename,
@ -263,11 +272,13 @@ entity_messages!(
SaveBuffer, SaveBuffer,
SearchProject, SearchProject,
StartLanguageServer, StartLanguageServer,
Unfollow,
UnregisterWorktree, UnregisterWorktree,
UnshareProject, UnshareProject,
UpdateBuffer, UpdateBuffer,
UpdateBufferFile, UpdateBufferFile,
UpdateDiagnosticSummary, UpdateDiagnosticSummary,
UpdateFollowers,
UpdateLanguageServer, UpdateLanguageServer,
RegisterWorktree, RegisterWorktree,
UpdateWorktree, UpdateWorktree,

View file

@ -5,4 +5,4 @@ pub mod proto;
pub use conn::Connection; pub use conn::Connection;
pub use peer::*; pub use peer::*;
pub const PROTOCOL_VERSION: u32 = 11; pub const PROTOCOL_VERSION: u32 = 12;

View file

@ -8,7 +8,7 @@ use gpui::{
use language::OffsetRangeExt; use language::OffsetRangeExt;
use project::search::SearchQuery; use project::search::SearchQuery;
use std::ops::Range; use std::ops::Range;
use workspace::{ItemViewHandle, Pane, Settings, Toolbar, Workspace}; use workspace::{ItemHandle, Pane, Settings, Toolbar, Workspace};
action!(Deploy, bool); action!(Deploy, bool);
action!(Dismiss); action!(Dismiss);
@ -66,7 +66,7 @@ impl View for SearchBar {
} }
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox { 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 { let editor_container = if self.query_contains_error {
theme.search.invalid_editor theme.search.invalid_editor
} else { } else {
@ -126,7 +126,7 @@ impl View for SearchBar {
impl Toolbar for SearchBar { impl Toolbar for SearchBar {
fn active_item_changed( fn active_item_changed(
&mut self, &mut self,
item: Option<Box<dyn ItemViewHandle>>, item: Option<Box<dyn ItemHandle>>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> bool { ) -> bool {
self.active_editor_subscription.take(); self.active_editor_subscription.take();
@ -197,7 +197,7 @@ impl SearchBar {
) -> ElementBox { ) -> ElementBox {
let is_active = self.is_search_option_enabled(search_option); let is_active = self.is_search_option_enabled(search_option);
MouseEventHandler::new::<Self, _, _>(search_option as usize, cx, |state, cx| { 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) { let style = match (is_active, state.hovered) {
(false, false) => &theme.option_button, (false, false) => &theme.option_button,
(false, true) => &theme.hovered_option_button, (false, true) => &theme.hovered_option_button,
@ -222,7 +222,7 @@ impl SearchBar {
) -> ElementBox { ) -> ElementBox {
enum NavButton {} enum NavButton {}
MouseEventHandler::new::<NavButton, _, _>(direction as usize, cx, |state, cx| { 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 { let style = if state.hovered {
&theme.hovered_option_button &theme.hovered_option_button
} else { } else {
@ -336,11 +336,9 @@ impl SearchBar {
direction, direction,
&editor.buffer().read(cx).read(cx), &editor.buffer().read(cx).read(cx),
); );
editor.select_ranges( let range_to_select = ranges[new_index].clone();
[ranges[new_index].clone()], editor.unfold_ranges([range_to_select.clone()], false, cx);
Some(Autoscroll::Fit), editor.select_ranges([range_to_select], Some(Autoscroll::Fit), cx);
cx,
);
} }
}); });
} }
@ -360,7 +358,7 @@ impl SearchBar {
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
match event { match event {
editor::Event::Edited => { editor::Event::BufferEdited { .. } => {
self.query_contains_error = false; self.query_contains_error = false;
self.clear_matches(cx); self.clear_matches(cx);
self.update_matches(true, cx); self.update_matches(true, cx);
@ -377,8 +375,8 @@ impl SearchBar {
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
match event { match event {
editor::Event::Edited => self.update_matches(false, cx), editor::Event::BufferEdited { .. } => self.update_matches(false, cx),
editor::Event::SelectionsChanged => self.update_match_index(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>( editor.highlight_background::<Self>(
ranges, ranges,
theme.match_background, theme.match_background,
@ -510,8 +508,9 @@ impl SearchBar {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use editor::{DisplayPoint, Editor, MultiBuffer}; use editor::{DisplayPoint, Editor};
use gpui::{color::Color, TestAppContext}; use gpui::{color::Color, TestAppContext};
use language::Buffer;
use std::sync::Arc; use std::sync::Arc;
use unindent::Unindent as _; use unindent::Unindent as _;
@ -521,11 +520,12 @@ mod tests {
let mut theme = gpui::fonts::with_font_cache(fonts.clone(), || theme::Theme::default()); let mut theme = gpui::fonts::with_font_cache(fonts.clone(), || theme::Theme::default());
theme.search.match_background = Color::red(); theme.search.match_background = Color::red();
let settings = Settings::new("Courier", &fonts, Arc::new(theme)).unwrap(); 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| { let buffer = cx.add_model(|cx| {
MultiBuffer::build_simple( Buffer::new(
&r#" 0,
r#"
A regular expression (shortened as regex or regexp;[1] also referred to as 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 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 pattern in text. Usually such patterns are used by string-searching algorithms

View file

@ -7,7 +7,7 @@ use editor::{Anchor, Autoscroll, Editor, MultiBuffer, SelectAll};
use gpui::{ use gpui::{
action, elements::*, keymap::Binding, platform::CursorStyle, AppContext, ElementBox, Entity, action, elements::*, keymap::Binding, platform::CursorStyle, AppContext, ElementBox, Entity,
ModelContext, ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, ModelContext, ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext,
ViewHandle, WeakModelHandle, ViewHandle, WeakModelHandle, WeakViewHandle,
}; };
use project::{search::SearchQuery, Project}; use project::{search::SearchQuery, Project};
use std::{ use std::{
@ -16,7 +16,7 @@ use std::{
path::PathBuf, path::PathBuf,
}; };
use util::ResultExt as _; use util::ResultExt as _;
use workspace::{Item, ItemHandle, ItemNavHistory, ItemView, Settings, Workspace}; use workspace::{Item, ItemNavHistory, Settings, Workspace};
action!(Deploy); action!(Deploy);
action!(Search); action!(Search);
@ -26,10 +26,10 @@ action!(ToggleFocus);
const MAX_TAB_TITLE_LEN: usize = 24; const MAX_TAB_TITLE_LEN: usize = 24;
#[derive(Default)] #[derive(Default)]
struct ActiveSearches(HashMap<WeakModelHandle<Project>, WeakModelHandle<ProjectSearch>>); struct ActiveSearches(HashMap<WeakModelHandle<Project>, WeakViewHandle<ProjectSearchView>>);
pub fn init(cx: &mut MutableAppContext) { pub fn init(cx: &mut MutableAppContext) {
cx.add_app_state(ActiveSearches::default()); cx.set_global(ActiveSearches::default());
cx.add_bindings([ cx.add_bindings([
Binding::new("cmd-shift-F", ToggleFocus, Some("ProjectSearchView")), Binding::new("cmd-shift-F", ToggleFocus, Some("ProjectSearchView")),
Binding::new("cmd-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 { enum ViewEvent {
UpdateTab, UpdateTab,
} }
@ -172,7 +155,7 @@ impl View for ProjectSearchView {
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox { fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let model = &self.model.read(cx); let model = &self.model.read(cx);
let results = if model.match_ranges.is_empty() { 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() { let text = if self.query_editor.read(cx).text(cx).is_empty() {
"" ""
} else if model.pending_search.is_some() { } else if model.pending_search.is_some() {
@ -199,11 +182,11 @@ impl View for ProjectSearchView {
} }
fn on_focus(&mut self, cx: &mut ViewContext<Self>) { fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
cx.update_app_state(|state: &mut ActiveSearches, cx| { let handle = cx.weak_handle();
state.0.insert( cx.update_global(|state: &mut ActiveSearches, cx| {
self.model.read(cx).project.downgrade(), state
self.model.downgrade(), .0
) .insert(self.model.read(cx).project.downgrade(), handle)
}); });
if self.model.read(cx).match_ranges.is_empty() { 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( fn act_as_type(
&self, &self,
type_id: TypeId, type_id: TypeId,
@ -235,12 +218,8 @@ impl ItemView for ProjectSearchView {
.update(cx, |editor, cx| editor.deactivated(cx)); .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 { 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; let search_theme = &settings.theme.search;
Flex::row() Flex::row()
.with_child( .with_child(
@ -271,6 +250,10 @@ impl ItemView for ProjectSearchView {
None None
} }
fn project_entry_id(&self, _: &AppContext) -> Option<project::ProjectEntryId> {
None
}
fn can_save(&self, _: &gpui::AppContext) -> bool { fn can_save(&self, _: &gpui::AppContext) -> bool {
true true
} }
@ -305,21 +288,23 @@ impl ItemView for ProjectSearchView {
unreachable!("save_as should not have been called") unreachable!("save_as should not have been called")
} }
fn clone_on_split( fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
&self,
nav_history: ItemNavHistory,
cx: &mut ViewContext<Self>,
) -> Option<Self>
where where
Self: Sized, Self: Sized,
{ {
let model = self.model.update(cx, |model, cx| model.clone(cx)); 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 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 { fn should_update_tab_on_event(event: &ViewEvent) -> bool {
@ -328,11 +313,7 @@ impl ItemView for ProjectSearchView {
} }
impl ProjectSearchView { impl ProjectSearchView {
fn new( fn new(model: ModelHandle<ProjectSearch>, cx: &mut ViewContext<Self>) -> Self {
model: ModelHandle<ProjectSearch>,
nav_history: Option<ItemNavHistory>,
cx: &mut ViewContext<Self>,
) -> Self {
let project; let project;
let excerpts; let excerpts;
let mut query_text = String::new(); let mut query_text = String::new();
@ -362,15 +343,14 @@ impl ProjectSearchView {
}); });
let results_editor = cx.add_view(|cx| { 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_searchable(false);
editor.set_nav_history(nav_history);
editor editor
}); });
cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab)) cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab))
.detach(); .detach();
cx.subscribe(&results_editor, |this, _, event, cx| { cx.subscribe(&results_editor, |this, _, event, cx| {
if matches!(event, editor::Event::SelectionsChanged) { if matches!(event, editor::Event::SelectionsChanged { .. }) {
this.update_match_index(cx); this.update_match_index(cx);
} }
}) })
@ -394,28 +374,31 @@ impl ProjectSearchView {
// If no search exists in the workspace, create a new one. // If no search exists in the workspace, create a new one.
fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) { fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
// Clean up entries for dropped projects // 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)) state.0.retain(|project, _| project.is_upgradable(cx))
}); });
let active_search = cx let active_search = cx
.app_state::<ActiveSearches>() .global::<ActiveSearches>()
.0 .0
.get(&workspace.project().downgrade()); .get(&workspace.project().downgrade());
let existing = active_search let existing = active_search
.and_then(|active_search| { .and_then(|active_search| {
workspace workspace
.items_of_type::<ProjectSearch>(cx) .items_of_type::<ProjectSearchView>(cx)
.find(|search| search == active_search) .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 { if let Some(existing) = existing {
workspace.activate_item(&existing, cx); workspace.activate_item(&existing, cx);
} else { } else {
let model = cx.add_model(|cx| ProjectSearch::new(workspace.project().clone(), cx)); 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.search(new_query, cx);
model 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(); let range_to_select = model.match_ranges[new_index].clone();
self.results_editor.update(cx, |editor, cx| { 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); editor.select_ranges([range_to_select], Some(Autoscroll::Fit), cx);
}); });
} }
@ -552,7 +539,7 @@ impl ProjectSearchView {
if reset_selections { if reset_selections {
editor.select_ranges(match_ranges.first().cloned(), Some(Autoscroll::Fit), cx); 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); editor.highlight_background::<Self>(match_ranges, theme.match_background, cx);
}); });
if self.query_editor.is_focused(cx) { if self.query_editor.is_focused(cx) {
@ -578,7 +565,7 @@ impl ProjectSearchView {
} }
fn render_query_editor(&self, cx: &mut RenderContext<Self>) -> ElementBox { 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 { let editor_container = if self.query_contains_error {
theme.search.invalid_editor theme.search.invalid_editor
} else { } else {
@ -642,7 +629,7 @@ impl ProjectSearchView {
) -> ElementBox { ) -> ElementBox {
let is_active = self.is_option_enabled(option); let is_active = self.is_option_enabled(option);
MouseEventHandler::new::<Self, _, _>(option as usize, cx, |state, cx| { 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) { let style = match (is_active, state.hovered) {
(false, false) => &theme.option_button, (false, false) => &theme.option_button,
(false, true) => &theme.hovered_option_button, (false, true) => &theme.hovered_option_button,
@ -675,7 +662,7 @@ impl ProjectSearchView {
) -> ElementBox { ) -> ElementBox {
enum NavButton {} enum NavButton {}
MouseEventHandler::new::<NavButton, _, _>(direction as usize, cx, |state, cx| { 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 { let style = if state.hovered {
&theme.hovered_option_button &theme.hovered_option_button
} else { } else {
@ -707,7 +694,7 @@ mod tests {
let mut theme = gpui::fonts::with_font_cache(fonts.clone(), || theme::Theme::default()); let mut theme = gpui::fonts::with_font_cache(fonts.clone(), || theme::Theme::default());
theme.search.match_background = Color::red(); theme.search.match_background = Color::red();
let settings = Settings::new("Courier", &fonts, Arc::new(theme)).unwrap(); 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()); let fs = FakeFs::new(cx.background());
fs.insert_tree( fs.insert_tree(
@ -732,7 +719,7 @@ mod tests {
let search = cx.add_model(|cx| ProjectSearch::new(project, cx)); let search = cx.add_model(|cx| ProjectSearch::new(project, cx));
let search_view = cx.add_view(Default::default(), |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| { search_view.update(cx, |search_view, cx| {

View file

@ -39,9 +39,9 @@ pub(crate) fn active_match_index(
None None
} else { } else {
match ranges.binary_search_by(|probe| { 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 Ordering::Less
} else if probe.start.cmp(&cursor, &*buffer).unwrap().is_gt() { } else if probe.start.cmp(&cursor, &*buffer).is_gt() {
Ordering::Greater Ordering::Greater
} else { } else {
Ordering::Equal Ordering::Equal
@ -59,7 +59,7 @@ pub(crate) fn match_index_for_direction(
direction: Direction, direction: Direction,
buffer: &MultiBufferSnapshot, buffer: &MultiBufferSnapshot,
) -> usize { ) -> 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 direction == Direction::Prev {
if index == 0 { if index == 0 {
index = ranges.len() - 1; index = ranges.len() - 1;
@ -67,7 +67,7 @@ pub(crate) fn match_index_for_direction(
index -= 1; 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 { if direction == Direction::Next {
index = 0; index = 0;
} }

File diff suppressed because it is too large Load diff

View file

@ -12,23 +12,19 @@ pub struct Anchor {
} }
impl Anchor { impl Anchor {
pub fn min() -> Self { pub const MIN: Self = Self {
Self {
timestamp: clock::Local::MIN, timestamp: clock::Local::MIN,
offset: usize::MIN, offset: usize::MIN,
bias: Bias::Left, bias: Bias::Left,
} };
}
pub fn max() -> Self { pub const MAX: Self = Self {
Self {
timestamp: clock::Local::MAX, timestamp: clock::Local::MAX,
offset: usize::MAX, offset: usize::MAX,
bias: Bias::Right, 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 { let fragment_id_comparison = if self.timestamp == other.timestamp {
Ordering::Equal Ordering::Equal
} else { } else {
@ -37,9 +33,25 @@ impl Anchor {
.cmp(&buffer.fragment_id_for_anchor(other)) .cmp(&buffer.fragment_id_for_anchor(other))
}; };
Ok(fragment_id_comparison fragment_id_comparison
.then_with(|| self.offset.cmp(&other.offset)) .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 { pub fn bias(&self, bias: Bias, buffer: &BufferSnapshot) -> Anchor {
@ -105,8 +117,8 @@ pub trait AnchorRangeExt {
impl AnchorRangeExt for Range<Anchor> { impl AnchorRangeExt for Range<Anchor> {
fn cmp(&self, other: &Range<Anchor>, buffer: &BufferSnapshot) -> Result<Ordering> { fn cmp(&self, other: &Range<Anchor>, buffer: &BufferSnapshot) -> Result<Ordering> {
Ok(match self.start.cmp(&other.start, buffer)? { Ok(match self.start.cmp(&other.start, buffer) {
Ordering::Equal => other.end.cmp(&self.end, buffer)?, Ordering::Equal => other.end.cmp(&self.end, buffer),
ord @ _ => ord, ord @ _ => ord,
}) })
} }

View file

@ -199,6 +199,28 @@ where
self.0.push(edit); 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> { 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] #[gpui::test]
fn test_two_new_edits_touching_one_old_edit() { fn test_two_new_edits_touching_one_old_edit() {
assert_patch_composition( 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)] #[gpui::test(iterations = 100)]
fn test_random_patch_compositions(mut rng: StdRng) { fn test_random_patch_compositions(mut rng: StdRng) {
let operations = env::var("OPERATIONS") let operations = env::var("OPERATIONS")

View file

@ -1,5 +1,5 @@
use crate::Anchor; use crate::Anchor;
use crate::{rope::TextDimension, BufferSnapshot, ToOffset, ToPoint}; use crate::{rope::TextDimension, BufferSnapshot};
use std::cmp::Ordering; use std::cmp::Ordering;
#[derive(Copy, Clone, Debug, Eq, PartialEq)] #[derive(Copy, Clone, Debug, Eq, PartialEq)]
@ -18,6 +18,12 @@ pub struct Selection<T> {
pub goal: SelectionGoal, pub goal: SelectionGoal,
} }
impl Default for SelectionGoal {
fn default() -> Self {
Self::None
}
}
impl<T: Clone> Selection<T> { impl<T: Clone> Selection<T> {
pub fn head(&self) -> T { pub fn head(&self) -> T {
if self.reversed { if self.reversed {
@ -34,14 +40,27 @@ impl<T: Clone> Selection<T> {
self.start.clone() 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 { pub fn is_empty(&self) -> bool {
self.start == self.end 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 head.cmp(&self.tail()) < Ordering::Equal {
if !self.reversed { if !self.reversed {
self.end = self.start; self.end = self.start;
@ -55,6 +74,14 @@ impl<T: ToOffset + ToPoint + Copy + Ord> Selection<T> {
} }
self.end = head; 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;
} }
} }

View file

@ -340,59 +340,41 @@ fn test_anchors() {
let anchor_at_offset_2 = buffer.anchor_before(2); let anchor_at_offset_2 = buffer.anchor_before(2);
assert_eq!( assert_eq!(
anchor_at_offset_0 anchor_at_offset_0.cmp(&anchor_at_offset_0, &buffer),
.cmp(&anchor_at_offset_0, &buffer)
.unwrap(),
Ordering::Equal Ordering::Equal
); );
assert_eq!( assert_eq!(
anchor_at_offset_1 anchor_at_offset_1.cmp(&anchor_at_offset_1, &buffer),
.cmp(&anchor_at_offset_1, &buffer)
.unwrap(),
Ordering::Equal Ordering::Equal
); );
assert_eq!( assert_eq!(
anchor_at_offset_2 anchor_at_offset_2.cmp(&anchor_at_offset_2, &buffer),
.cmp(&anchor_at_offset_2, &buffer)
.unwrap(),
Ordering::Equal Ordering::Equal
); );
assert_eq!( assert_eq!(
anchor_at_offset_0 anchor_at_offset_0.cmp(&anchor_at_offset_1, &buffer),
.cmp(&anchor_at_offset_1, &buffer)
.unwrap(),
Ordering::Less Ordering::Less
); );
assert_eq!( assert_eq!(
anchor_at_offset_1 anchor_at_offset_1.cmp(&anchor_at_offset_2, &buffer),
.cmp(&anchor_at_offset_2, &buffer)
.unwrap(),
Ordering::Less Ordering::Less
); );
assert_eq!( assert_eq!(
anchor_at_offset_0 anchor_at_offset_0.cmp(&anchor_at_offset_2, &buffer),
.cmp(&anchor_at_offset_2, &buffer)
.unwrap(),
Ordering::Less Ordering::Less
); );
assert_eq!( assert_eq!(
anchor_at_offset_1 anchor_at_offset_1.cmp(&anchor_at_offset_0, &buffer),
.cmp(&anchor_at_offset_0, &buffer)
.unwrap(),
Ordering::Greater Ordering::Greater
); );
assert_eq!( assert_eq!(
anchor_at_offset_2 anchor_at_offset_2.cmp(&anchor_at_offset_1, &buffer),
.cmp(&anchor_at_offset_1, &buffer)
.unwrap(),
Ordering::Greater Ordering::Greater
); );
assert_eq!( assert_eq!(
anchor_at_offset_2 anchor_at_offset_2.cmp(&anchor_at_offset_0, &buffer),
.cmp(&anchor_at_offset_0, &buffer)
.unwrap(),
Ordering::Greater Ordering::Greater
); );
} }

View file

@ -1318,8 +1318,8 @@ impl Buffer {
let mut futures = Vec::new(); let mut futures = Vec::new();
for anchor in anchors { for anchor in anchors {
if !self.version.observed(anchor.timestamp) if !self.version.observed(anchor.timestamp)
&& *anchor != Anchor::max() && *anchor != Anchor::MAX
&& *anchor != Anchor::min() && *anchor != Anchor::MIN
{ {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
self.edit_id_resolvers self.edit_id_resolvers
@ -1638,9 +1638,9 @@ impl BufferSnapshot {
let mut position = D::default(); let mut position = D::default();
anchors.map(move |anchor| { anchors.map(move |anchor| {
if *anchor == Anchor::min() { if *anchor == Anchor::MIN {
return D::default(); return D::default();
} else if *anchor == Anchor::max() { } else if *anchor == Anchor::MAX {
return D::from_text_summary(&self.visible_text.summary()); return D::from_text_summary(&self.visible_text.summary());
} }
@ -1680,9 +1680,9 @@ impl BufferSnapshot {
where where
D: TextDimension, D: TextDimension,
{ {
if *anchor == Anchor::min() { if *anchor == Anchor::MIN {
D::default() D::default()
} else if *anchor == Anchor::max() { } else if *anchor == Anchor::MAX {
D::from_text_summary(&self.visible_text.summary()) D::from_text_summary(&self.visible_text.summary())
} else { } else {
let anchor_key = InsertionFragmentKey { let anchor_key = InsertionFragmentKey {
@ -1718,9 +1718,9 @@ impl BufferSnapshot {
} }
fn fragment_id_for_anchor(&self, anchor: &Anchor) -> &Locator { fn fragment_id_for_anchor(&self, anchor: &Anchor) -> &Locator {
if *anchor == Anchor::min() { if *anchor == Anchor::MIN {
&locator::MIN &locator::MIN
} else if *anchor == Anchor::max() { } else if *anchor == Anchor::MAX {
&locator::MAX &locator::MAX
} else { } else {
let anchor_key = InsertionFragmentKey { let anchor_key = InsertionFragmentKey {
@ -1758,9 +1758,9 @@ impl BufferSnapshot {
pub fn anchor_at<T: ToOffset>(&self, position: T, bias: Bias) -> Anchor { pub fn anchor_at<T: ToOffset>(&self, position: T, bias: Bias) -> Anchor {
let offset = position.to_offset(self); let offset = position.to_offset(self);
if bias == Bias::Left && offset == 0 { if bias == Bias::Left && offset == 0 {
Anchor::min() Anchor::MIN
} else if bias == Bias::Right && offset == self.len() { } else if bias == Bias::Right && offset == self.len() {
Anchor::max() Anchor::MAX
} else { } else {
let mut fragment_cursor = self.fragments.cursor::<usize>(); let mut fragment_cursor = self.fragments.cursor::<usize>();
fragment_cursor.seek(&offset, bias, &None); fragment_cursor.seek(&offset, bias, &None);
@ -1775,9 +1775,7 @@ impl BufferSnapshot {
} }
pub fn can_resolve(&self, anchor: &Anchor) -> bool { pub fn can_resolve(&self, anchor: &Anchor) -> bool {
*anchor == Anchor::min() *anchor == Anchor::MIN || *anchor == Anchor::MAX || self.version.observed(anchor.timestamp)
|| *anchor == Anchor::max()
|| self.version.observed(anchor.timestamp)
} }
pub fn clip_offset(&self, offset: usize, bias: Bias) -> usize { pub fn clip_offset(&self, offset: usize, bias: Bias) -> usize {
@ -1799,7 +1797,7 @@ impl BufferSnapshot {
where where
D: TextDimension + Ord, 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>( pub fn edited_ranges_for_transaction<'a, D>(

View file

@ -35,6 +35,8 @@ pub struct Workspace {
pub tab: Tab, pub tab: Tab,
pub active_tab: Tab, pub active_tab: Tab,
pub pane_divider: Border, pub pane_divider: Border,
pub leader_border_opacity: f32,
pub leader_border_width: f32,
pub left_sidebar: Sidebar, pub left_sidebar: Sidebar,
pub right_sidebar: Sidebar, pub right_sidebar: Sidebar,
pub status_bar: StatusBar, pub status_bar: StatusBar,

View file

@ -54,7 +54,7 @@ impl ThemeSelector {
cx.subscribe(&query_editor, Self::on_query_editor_event) cx.subscribe(&query_editor, Self::on_query_editor_event)
.detach(); .detach();
let original_theme = cx.app_state::<Settings>().theme.clone(); let original_theme = cx.global::<Settings>().theme.clone();
let mut this = Self { let mut this = Self {
themes: registry, themes: registry,
@ -82,7 +82,7 @@ impl ThemeSelector {
} }
fn reload(_: &mut Workspace, action: &Reload, cx: &mut ViewContext<Workspace>) { 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(); action.0.clear();
match action.0.get(&current_theme_name) { match action.0.get(&current_theme_name) {
Ok(theme) => { Ok(theme) => {
@ -204,9 +204,9 @@ impl ThemeSelector {
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
match event { match event {
editor::Event::Edited => { editor::Event::BufferEdited { .. } => {
self.update_matches(cx); 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); self.show_selected_theme(cx);
} }
editor::Event::Blurred => cx.emit(Event::Dismissed), editor::Event::Blurred => cx.emit(Event::Dismissed),
@ -216,7 +216,7 @@ impl ThemeSelector {
fn render_matches(&self, cx: &mut RenderContext<Self>) -> ElementBox { fn render_matches(&self, cx: &mut RenderContext<Self>) -> ElementBox {
if self.matches.is_empty() { if self.matches.is_empty() {
let settings = cx.app_state::<Settings>(); let settings = cx.global::<Settings>();
return Container::new( return Container::new(
Label::new( Label::new(
"No matches".into(), "No matches".into(),
@ -251,7 +251,7 @@ impl ThemeSelector {
} }
fn render_match(&self, theme_match: &StringMatch, index: usize, cx: &AppContext) -> ElementBox { 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 theme = &settings.theme;
let container = Container::new( let container = Container::new(
@ -276,7 +276,7 @@ impl ThemeSelector {
} }
fn set_theme(theme: Arc<Theme>, cx: &mut MutableAppContext) { 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; settings.theme = theme;
cx.refresh_windows(); cx.refresh_windows();
}); });
@ -299,7 +299,7 @@ impl View for ThemeSelector {
} }
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox { 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( Align::new(
ConstrainedBox::new( ConstrainedBox::new(
Container::new( Container::new(

View file

@ -1,4 +1,8 @@
use std::path::{Path, PathBuf}; use std::{
collections::HashMap,
ops::Range,
path::{Path, PathBuf},
};
use tempdir::TempDir; use tempdir::TempDir;
pub fn temp_tree(tree: serde_json::Value) -> 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 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)
}

25
crates/vim/Cargo.toml Normal file
View file

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

View file

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

30
crates/vim/src/insert.rs Normal file
View file

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

36
crates/vim/src/mode.rs Normal file
View file

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

66
crates/vim/src/normal.rs Normal file
View file

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

97
crates/vim/src/vim.rs Normal file
View file

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

253
crates/vim/src/vim_tests.rs Normal file
View file

@ -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(&params, 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
}
}

View file

@ -1,4 +1,4 @@
use crate::{ItemViewHandle, Settings, StatusItemView}; use crate::{ItemHandle, Settings, StatusItemView};
use futures::StreamExt; use futures::StreamExt;
use gpui::AppContext; use gpui::AppContext;
use gpui::{ use gpui::{
@ -116,7 +116,7 @@ impl View for LspStatus {
} }
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox { 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); let mut pending_work = self.pending_language_server_work(cx);
if let Some((lang_server_name, progress_token, progress)) = pending_work.next() { 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() { } else if !self.failed.is_empty() {
drop(pending_work); drop(pending_work);
MouseEventHandler::new::<Self, _, _>(0, cx, |_, cx| { MouseEventHandler::new::<Self, _, _>(0, cx, |_, cx| {
let theme = &cx.app_state::<Settings>().theme; let theme = &cx.global::<Settings>().theme;
Label::new( Label::new(
format!( format!(
"Failed to download {} language server{}. Click to dismiss.", "Failed to download {} language server{}. Click to dismiss.",
@ -187,5 +187,5 @@ impl View for LspStatus {
} }
impl StatusItemView 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>) {}
} }

View file

@ -1,5 +1,5 @@
use super::{ItemViewHandle, SplitDirection}; use super::{ItemHandle, SplitDirection};
use crate::{ItemHandle, ItemView, Settings, WeakItemViewHandle, Workspace}; use crate::{Item, Settings, WeakItemHandle, Workspace};
use collections::{HashMap, VecDeque}; use collections::{HashMap, VecDeque};
use gpui::{ use gpui::{
action, action,
@ -7,10 +7,10 @@ use gpui::{
geometry::{rect::RectF, vector::vec2f}, geometry::{rect::RectF, vector::vec2f},
keymap::Binding, keymap::Binding,
platform::{CursorStyle, NavigationDirection}, platform::{CursorStyle, NavigationDirection},
AnyViewHandle, Entity, MutableAppContext, Quad, RenderContext, Task, View, ViewContext, AnyViewHandle, AppContext, Entity, MutableAppContext, Quad, RenderContext, Task, View,
ViewHandle, WeakViewHandle, ViewContext, ViewHandle, WeakViewHandle,
}; };
use project::ProjectPath; use project::{ProjectEntryId, ProjectPath};
use std::{ use std::{
any::{Any, TypeId}, any::{Any, TypeId},
cell::RefCell, cell::RefCell,
@ -33,7 +33,7 @@ const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
pub fn init(cx: &mut MutableAppContext) { pub fn init(cx: &mut MutableAppContext) {
cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| { cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
pane.activate_item(action.0, cx); pane.activate_item(action.0, true, cx);
}); });
cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| { cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
pane.activate_prev_item(cx); pane.activate_prev_item(cx);
@ -92,12 +92,13 @@ pub fn init(cx: &mut MutableAppContext) {
pub enum Event { pub enum Event {
Activate, Activate,
ActivateItem { local: bool },
Remove, Remove,
Split(SplitDirection), Split(SplitDirection),
} }
pub struct Pane { pub struct Pane {
item_views: Vec<(usize, Box<dyn ItemViewHandle>)>, items: Vec<Box<dyn ItemHandle>>,
active_item_index: usize, active_item_index: usize,
nav_history: Rc<RefCell<NavHistory>>, nav_history: Rc<RefCell<NavHistory>>,
toolbars: HashMap<TypeId, Box<dyn ToolbarHandle>>, toolbars: HashMap<TypeId, Box<dyn ToolbarHandle>>,
@ -108,7 +109,7 @@ pub struct Pane {
pub trait Toolbar: View { pub trait Toolbar: View {
fn active_item_changed( fn active_item_changed(
&mut self, &mut self,
item: Option<Box<dyn ItemViewHandle>>, item: Option<Box<dyn ItemHandle>>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> bool; ) -> bool;
fn on_dismiss(&mut self, cx: &mut ViewContext<Self>); fn on_dismiss(&mut self, cx: &mut ViewContext<Self>);
@ -117,7 +118,7 @@ pub trait Toolbar: View {
trait ToolbarHandle { trait ToolbarHandle {
fn active_item_changed( fn active_item_changed(
&self, &self,
item: Option<Box<dyn ItemViewHandle>>, item: Option<Box<dyn ItemHandle>>,
cx: &mut MutableAppContext, cx: &mut MutableAppContext,
) -> bool; ) -> bool;
fn on_dismiss(&self, cx: &mut MutableAppContext); fn on_dismiss(&self, cx: &mut MutableAppContext);
@ -126,7 +127,7 @@ trait ToolbarHandle {
pub struct ItemNavHistory { pub struct ItemNavHistory {
history: Rc<RefCell<NavHistory>>, history: Rc<RefCell<NavHistory>>,
item_view: Rc<dyn WeakItemViewHandle>, item: Rc<dyn WeakItemHandle>,
} }
#[derive(Default)] #[derive(Default)]
@ -152,14 +153,14 @@ impl Default for NavigationMode {
} }
pub struct NavigationEntry { pub struct NavigationEntry {
pub item_view: Rc<dyn WeakItemViewHandle>, pub item: Rc<dyn WeakItemHandle>,
pub data: Option<Box<dyn Any>>, pub data: Option<Box<dyn Any>>,
} }
impl Pane { impl Pane {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
item_views: Vec::new(), items: Vec::new(),
active_item_index: 0, active_item_index: 0,
nav_history: Default::default(), nav_history: Default::default(),
toolbars: Default::default(), toolbars: Default::default(),
@ -211,40 +212,47 @@ impl Pane {
workspace.activate_pane(pane.clone(), cx); workspace.activate_pane(pane.clone(), cx);
let to_load = pane.update(cx, |pane, cx| { let to_load = pane.update(cx, |pane, cx| {
loop {
// Retrieve the weak item handle from the history. // Retrieve the weak item handle from the history.
let entry = pane.nav_history.borrow_mut().pop(mode)?; let entry = pane.nav_history.borrow_mut().pop(mode)?;
// If the item is still present in this pane, then activate it. // If the item is still present in this pane, then activate it.
if let Some(index) = entry if let Some(index) = entry
.item_view .item
.upgrade(cx) .upgrade(cx)
.and_then(|v| pane.index_for_item_view(v.as_ref())) .and_then(|v| pane.index_for_item(v.as_ref()))
{ {
if let Some(item_view) = pane.active_item() { if let Some(item) = pane.active_item() {
pane.nav_history.borrow_mut().set_mode(mode); pane.nav_history.borrow_mut().set_mode(mode);
item_view.deactivated(cx); item.deactivated(cx);
pane.nav_history pane.nav_history
.borrow_mut() .borrow_mut()
.set_mode(NavigationMode::Normal); .set_mode(NavigationMode::Normal);
} }
pane.active_item_index = index; let prev_active_index = mem::replace(&mut pane.active_item_index, index);
pane.focus_active_item(cx); pane.focus_active_item(cx);
let mut navigated = prev_active_index != pane.active_item_index;
if let Some(data) = entry.data { if let Some(data) = entry.data {
pane.active_item()?.navigate(data, cx); navigated |= pane.active_item()?.navigate(data, cx);
} }
if navigated {
cx.notify(); cx.notify();
None break None;
}
} }
// If the item is no longer present in this pane, then retrieve its // If the item is no longer present in this pane, then retrieve its
// project path in order to reopen it. // project path in order to reopen it.
else { else {
pane.nav_history break pane
.nav_history
.borrow_mut() .borrow_mut()
.paths_by_item .paths_by_item
.get(&entry.item_view.id()) .get(&entry.item.id())
.cloned() .cloned()
.map(|project_path| (project_path, entry)) .map(|project_path| (project_path, entry));
}
} }
}); });
@ -253,18 +261,27 @@ impl Pane {
let pane = pane.downgrade(); let pane = pane.downgrade();
let task = workspace.load_path(project_path, cx); let task = workspace.load_path(project_path, cx);
cx.spawn(|workspace, mut cx| async move { 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(pane) = pane.upgrade(&cx) {
if let Some(item) = item.log_err() { if let Some((project_entry_id, build_item)) = task.log_err() {
workspace.update(&mut cx, |workspace, cx| { pane.update(&mut cx, |pane, _| {
pane.update(cx, |p, _| p.nav_history.borrow_mut().set_mode(mode)); pane.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)
}); });
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 { if let Some(data) = entry.data {
item_view.navigate(data, cx); item.navigate(data, cx);
} }
}); });
} else { } else {
@ -281,80 +298,115 @@ impl Pane {
} }
} }
pub fn open_item<T>( pub(crate) fn open_item(
&mut self, workspace: &mut Workspace,
item_handle: T, pane: ViewHandle<Pane>,
workspace: &Workspace, project_entry_id: ProjectEntryId,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Workspace>,
) -> Box<dyn ItemViewHandle> build_item: impl FnOnce(&mut MutableAppContext) -> Box<dyn ItemHandle>,
where ) -> Box<dyn ItemHandle> {
T: 'static + ItemHandle, let existing_item = pane.update(cx, |pane, cx| {
{ for (ix, item) in pane.items.iter().enumerate() {
for (ix, (item_id, item_view)) in self.item_views.iter().enumerate() { if item.project_entry_id(cx) == Some(project_entry_id) {
if *item_id == item_handle.id() { let item = item.boxed_clone();
let item_view = item_view.boxed_clone(); pane.activate_item(ix, true, cx);
self.activate_item(ix, cx); return Some(item);
return item_view; }
}
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 = pub(crate) fn add_item(
item_handle.add_view(cx.window_id(), workspace, self.nav_history.clone(), cx); workspace: &mut Workspace,
self.add_item_view(item_view.boxed_clone(), cx); pane: ViewHandle<Pane>,
item_view item: Box<dyn ItemHandle>,
} local: bool,
cx: &mut ViewContext<Workspace>,
pub fn add_item_view(
&mut self,
mut item_view: Box<dyn ItemViewHandle>,
cx: &mut ViewContext<Self>,
) { ) {
item_view.added_to_pane(cx); // Prevent adding the same item to the pane more than once.
let item_idx = cmp::min(self.active_item_index + 1, self.item_views.len()); if let Some(item_ix) = pane.read(cx).items.iter().position(|i| i.id() == item.id()) {
self.item_views pane.update(cx, |pane, cx| pane.activate_item(item_ix, local, cx));
.insert(item_idx, (item_view.item(cx).id(), item_view)); return;
self.activate_item(item_idx, cx); }
item.set_nav_history(pane.read(cx).nav_history.clone(), cx);
item.added_to_pane(workspace, pane.clone(), cx);
pane.update(cx, |pane, cx| {
let item_idx = cmp::min(pane.active_item_index + 1, pane.items.len());
pane.items.insert(item_idx, item);
pane.activate_item(item_idx, local, cx);
cx.notify(); cx.notify();
});
} }
pub fn contains_item(&self, item: &dyn ItemHandle) -> bool { pub fn items(&self) -> impl Iterator<Item = &Box<dyn ItemHandle>> {
let item_id = item.id(); self.items.iter()
self.item_views }
pub fn items_of_type<'a, T: View>(&'a self) -> impl 'a + Iterator<Item = ViewHandle<T>> {
self.items
.iter() .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>> { pub fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
self.item_views.iter().map(|(_, view)| view) self.items.get(self.active_item_index).cloned()
} }
pub fn active_item(&self) -> Option<Box<dyn ItemViewHandle>> { pub fn project_entry_id_for_item(
self.item_views &self,
.get(self.active_item_index) item: &dyn ItemHandle,
.map(|(_, view)| view.clone()) 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> { pub fn item_for_entry(
self.item_views &self,
.iter() entry_id: ProjectEntryId,
.position(|(_, i)| i.id() == item_view.id()) 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> { 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>) { pub fn activate_item(&mut self, index: usize, local: bool, cx: &mut ViewContext<Self>) {
if index < self.item_views.len() { if index < self.items.len() {
let prev_active_item_ix = mem::replace(&mut self.active_item_index, index); let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
if prev_active_item_ix != self.active_item_index if prev_active_item_ix != self.active_item_index
&& prev_active_item_ix < self.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.update_active_toolbar(cx);
if local {
self.focus_active_item(cx); self.focus_active_item(cx);
self.activate(cx); self.activate(cx);
}
cx.notify(); cx.notify();
} }
} }
@ -363,31 +415,31 @@ impl Pane {
let mut index = self.active_item_index; let mut index = self.active_item_index;
if index > 0 { if index > 0 {
index -= 1; index -= 1;
} else if self.item_views.len() > 0 { } else if self.items.len() > 0 {
index = self.item_views.len() - 1; index = self.items.len() - 1;
} }
self.activate_item(index, cx); self.activate_item(index, true, cx);
} }
pub fn activate_next_item(&mut self, cx: &mut ViewContext<Self>) { pub fn activate_next_item(&mut self, cx: &mut ViewContext<Self>) {
let mut index = self.active_item_index; let mut index = self.active_item_index;
if index + 1 < self.item_views.len() { if index + 1 < self.items.len() {
index += 1; index += 1;
} else { } else {
index = 0; index = 0;
} }
self.activate_item(index, cx); self.activate_item(index, true, cx);
} }
pub fn close_active_item(&mut self, cx: &mut ViewContext<Self>) { pub fn close_active_item(&mut self, cx: &mut ViewContext<Self>) {
if !self.item_views.is_empty() { if !self.items.is_empty() {
self.close_item(self.item_views[self.active_item_index].1.id(), cx) self.close_item(self.items[self.active_item_index].id(), cx)
} }
} }
pub fn close_inactive_items(&mut self, cx: &mut ViewContext<Self>) { pub fn close_inactive_items(&mut self, cx: &mut ViewContext<Self>) {
if !self.item_views.is_empty() { if !self.items.is_empty() {
let active_item_id = self.item_views[self.active_item_index].1.id(); let active_item_id = self.items[self.active_item_index].id();
self.close_items(cx, |id| id != active_item_id); self.close_items(cx, |id| id != active_item_id);
} }
} }
@ -403,10 +455,10 @@ impl Pane {
) { ) {
let mut item_ix = 0; let mut item_ix = 0;
let mut new_active_item_index = self.active_item_index; let mut new_active_item_index = self.active_item_index;
self.item_views.retain(|(_, item_view)| { self.items.retain(|item| {
if should_close(item_view.id()) { if should_close(item.id()) {
if item_ix == self.active_item_index { if item_ix == self.active_item_index {
item_view.deactivated(cx); item.deactivated(cx);
} }
if item_ix < self.active_item_index { if item_ix < self.active_item_index {
@ -414,10 +466,10 @@ impl Pane {
} }
let mut nav_history = self.nav_history.borrow_mut(); let mut nav_history = self.nav_history.borrow_mut();
if let Some(path) = item_view.project_path(cx) { if let Some(path) = item.project_path(cx) {
nav_history.paths_by_item.insert(item_view.id(), path); nav_history.paths_by_item.insert(item.id(), path);
} else { } else {
nav_history.paths_by_item.remove(&item_view.id()); nav_history.paths_by_item.remove(&item.id());
} }
item_ix += 1; item_ix += 1;
@ -428,10 +480,10 @@ impl Pane {
} }
}); });
if self.item_views.is_empty() { if self.items.is_empty() {
cx.emit(Event::Remove); cx.emit(Event::Remove);
} else { } 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.focus_active_item(cx);
self.activate(cx); self.activate(cx);
} }
@ -440,7 +492,7 @@ impl Pane {
cx.notify(); cx.notify();
} }
fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) { pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
if let Some(active_item) = self.active_item() { if let Some(active_item) = self.active_item() {
cx.focus(active_item); cx.focus(active_item);
} }
@ -500,9 +552,9 @@ impl Pane {
} }
fn update_active_toolbar(&mut self, cx: &mut ViewContext<Self>) { fn update_active_toolbar(&mut self, cx: &mut ViewContext<Self>) {
let active_item = self.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 { for (toolbar_type_id, toolbar) in &self.toolbars {
let visible = toolbar.active_item_changed(active_item.map(|i| i.1.clone()), cx); let visible = toolbar.active_item_changed(active_item.cloned(), cx);
if Some(*toolbar_type_id) == self.active_toolbar_type { if Some(*toolbar_type_id) == self.active_toolbar_type {
self.active_toolbar_visible = visible; self.active_toolbar_visible = visible;
} }
@ -510,12 +562,12 @@ impl Pane {
} }
fn render_tabs(&self, cx: &mut RenderContext<Self>) -> ElementBox { 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 {} enum Tabs {}
let tabs = MouseEventHandler::new::<Tabs, _, _>(0, cx, |mouse_state, cx| { let tabs = MouseEventHandler::new::<Tabs, _, _>(0, cx, |mouse_state, cx| {
let mut row = Flex::row(); let mut row = Flex::row();
for (ix, (_, item_view)) in self.item_views.iter().enumerate() { for (ix, item) in self.items.iter().enumerate() {
let is_active = ix == self.active_item_index; let is_active = ix == self.active_item_index;
row.add_child({ row.add_child({
@ -524,7 +576,7 @@ impl Pane {
} else { } else {
theme.workspace.tab.clone() 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 { let mut style = if is_active {
theme.workspace.active_tab.clone() theme.workspace.active_tab.clone()
@ -541,9 +593,9 @@ impl Pane {
.with_child( .with_child(
Align::new({ Align::new({
let diameter = 7.0; 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) Some(style.icon_conflict)
} else if item_view.is_dirty(cx) { } else if item.is_dirty(cx) {
Some(style.icon_dirty) Some(style.icon_dirty)
} else { } else {
None None
@ -587,7 +639,7 @@ impl Pane {
.with_child( .with_child(
Align::new( Align::new(
ConstrainedBox::new(if mouse_state.hovered { ConstrainedBox::new(if mouse_state.hovered {
let item_id = item_view.id(); let item_id = item.id();
enum TabCloseButton {} enum TabCloseButton {}
let icon = Svg::new("icons/x.svg"); let icon = Svg::new("icons/x.svg");
MouseEventHandler::new::<TabCloseButton, _, _>( MouseEventHandler::new::<TabCloseButton, _, _>(
@ -691,7 +743,7 @@ impl View for Pane {
impl<T: Toolbar> ToolbarHandle for ViewHandle<T> { impl<T: Toolbar> ToolbarHandle for ViewHandle<T> {
fn active_item_changed( fn active_item_changed(
&self, &self,
item: Option<Box<dyn ItemViewHandle>>, item: Option<Box<dyn ItemHandle>>,
cx: &mut MutableAppContext, cx: &mut MutableAppContext,
) -> bool { ) -> bool {
self.update(cx, |this, cx| this.active_item_changed(item, cx)) self.update(cx, |this, cx| this.active_item_changed(item, cx))
@ -707,10 +759,10 @@ impl<T: Toolbar> ToolbarHandle for ViewHandle<T> {
} }
impl ItemNavHistory { 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 { Self {
history, 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>) { 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; self.mode = mode;
} }
pub fn push<D: 'static + Any>( pub fn push<D: 'static + Any>(&mut self, data: Option<D>, item: Rc<dyn WeakItemHandle>) {
&mut self,
data: Option<D>,
item_view: Rc<dyn WeakItemViewHandle>,
) {
match self.mode { match self.mode {
NavigationMode::Disabled => {} NavigationMode::Disabled => {}
NavigationMode::Normal => { NavigationMode::Normal => {
@ -764,7 +812,7 @@ impl NavHistory {
self.backward_stack.pop_front(); self.backward_stack.pop_front();
} }
self.backward_stack.push_back(NavigationEntry { self.backward_stack.push_back(NavigationEntry {
item_view, item,
data: data.map(|data| Box::new(data) as Box<dyn Any>), data: data.map(|data| Box::new(data) as Box<dyn Any>),
}); });
self.forward_stack.clear(); self.forward_stack.clear();
@ -774,7 +822,7 @@ impl NavHistory {
self.forward_stack.pop_front(); self.forward_stack.pop_front();
} }
self.forward_stack.push_back(NavigationEntry { self.forward_stack.push_back(NavigationEntry {
item_view, item,
data: data.map(|data| Box::new(data) as Box<dyn Any>), 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.pop_front();
} }
self.backward_stack.push_back(NavigationEntry { self.backward_stack.push_back(NavigationEntry {
item_view, item,
data: data.map(|data| Box::new(data) as Box<dyn Any>), data: data.map(|data| Box::new(data) as Box<dyn Any>),
}); });
} }

View file

@ -1,9 +1,11 @@
use crate::{FollowerStatesByLeader, Pane};
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use gpui::{elements::*, Axis, ViewHandle}; use client::PeerId;
use collections::HashMap;
use gpui::{elements::*, Axis, Border, ViewHandle};
use project::Collaborator;
use theme::Theme; use theme::Theme;
use crate::Pane;
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
pub struct PaneGroup { pub struct PaneGroup {
root: Member, root: Member,
@ -47,8 +49,19 @@ impl PaneGroup {
} }
} }
pub fn render<'a>(&self, theme: &Theme) -> ElementBox { pub(crate) fn render<'a>(
self.root.render(theme) &self,
theme: &Theme,
follower_states: &FollowerStatesByLeader,
collaborators: &HashMap<PeerId, Collaborator>,
) -> ElementBox {
self.root.render(theme, follower_states, collaborators)
}
pub(crate) fn panes(&self) -> Vec<&ViewHandle<Pane>> {
let mut panes = Vec::new();
self.root.collect_panes(&mut panes);
panes
} }
} }
@ -80,10 +93,50 @@ impl Member {
Member::Axis(PaneAxis { axis, members }) Member::Axis(PaneAxis { axis, members })
} }
pub fn render(&self, theme: &Theme) -> ElementBox { pub fn render(
&self,
theme: &Theme,
follower_states: &FollowerStatesByLeader,
collaborators: &HashMap<PeerId, Collaborator>,
) -> ElementBox {
match self { match self {
Member::Pane(pane) => ChildView::new(pane).boxed(), Member::Pane(pane) => {
Member::Axis(axis) => axis.render(theme), let mut border = Border::default();
let leader = follower_states
.iter()
.find_map(|(leader_id, follower_states)| {
if follower_states.contains_key(pane) {
Some(leader_id)
} else {
None
}
})
.and_then(|leader_id| collaborators.get(leader_id));
if let Some(leader) = leader {
let leader_color = theme
.editor
.replica_selection_style(leader.replica_id)
.cursor;
border = Border::all(theme.workspace.leader_border_width, leader_color);
border
.color
.fade_out(1. - theme.workspace.leader_border_opacity);
border.overlay = true;
}
ChildView::new(pane).contained().with_border(border).boxed()
}
Member::Axis(axis) => axis.render(theme, follower_states, collaborators),
}
}
fn collect_panes<'a>(&'a self, panes: &mut Vec<&'a ViewHandle<Pane>>) {
match self {
Member::Axis(axis) => {
for member in &axis.members {
member.collect_panes(panes);
}
}
Member::Pane(pane) => panes.push(pane),
} }
} }
} }
@ -172,11 +225,16 @@ impl PaneAxis {
} }
} }
fn render<'a>(&self, theme: &Theme) -> ElementBox { fn render(
&self,
theme: &Theme,
follower_state: &FollowerStatesByLeader,
collaborators: &HashMap<PeerId, Collaborator>,
) -> ElementBox {
let last_member_ix = self.members.len() - 1; let last_member_ix = self.members.len() - 1;
Flex::new(self.axis) Flex::new(self.axis)
.with_children(self.members.iter().enumerate().map(|(ix, member)| { .with_children(self.members.iter().enumerate().map(|(ix, member)| {
let mut member = member.render(theme); let mut member = member.render(theme, follower_state, collaborators);
if ix < last_member_ix { if ix < last_member_ix {
let mut border = theme.workspace.pane_divider; let mut border = theme.workspace.pane_divider;
border.left = false; border.left = false;

View file

@ -17,6 +17,7 @@ use util::ResultExt;
pub struct Settings { pub struct Settings {
pub buffer_font_family: FamilyId, pub buffer_font_family: FamilyId,
pub buffer_font_size: f32, pub buffer_font_size: f32,
pub vim_mode: bool,
pub tab_size: usize, pub tab_size: usize,
pub soft_wrap: SoftWrap, pub soft_wrap: SoftWrap,
pub preferred_line_length: u32, pub preferred_line_length: u32,
@ -48,6 +49,8 @@ struct SettingsFileContent {
buffer_font_family: Option<String>, buffer_font_family: Option<String>,
#[serde(default)] #[serde(default)]
buffer_font_size: Option<f32>, buffer_font_size: Option<f32>,
#[serde(default)]
vim_mode: Option<bool>,
#[serde(flatten)] #[serde(flatten)]
editor: LanguageOverride, editor: LanguageOverride,
#[serde(default)] #[serde(default)]
@ -130,6 +133,7 @@ impl Settings {
Ok(Self { Ok(Self {
buffer_font_family: font_cache.load_family(&[buffer_font_family])?, buffer_font_family: font_cache.load_family(&[buffer_font_family])?,
buffer_font_size: 15., buffer_font_size: 15.,
vim_mode: false,
tab_size: 4, tab_size: 4,
soft_wrap: SoftWrap::None, soft_wrap: SoftWrap::None,
preferred_line_length: 80, preferred_line_length: 80,
@ -174,6 +178,7 @@ impl Settings {
Settings { Settings {
buffer_font_family: cx.font_cache().load_family(&["Monaco"]).unwrap(), buffer_font_family: cx.font_cache().load_family(&["Monaco"]).unwrap(),
buffer_font_size: 14., buffer_font_size: 14.,
vim_mode: false,
tab_size: 4, tab_size: 4,
soft_wrap: SoftWrap::None, soft_wrap: SoftWrap::None,
preferred_line_length: 80, preferred_line_length: 80,
@ -200,6 +205,7 @@ impl Settings {
} }
merge(&mut self.buffer_font_size, data.buffer_font_size); 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.soft_wrap, data.editor.soft_wrap);
merge(&mut self.tab_size, data.editor.tab_size); merge(&mut self.tab_size, data.editor.tab_size);
merge( merge(

View file

@ -1,4 +1,4 @@
use crate::{ItemViewHandle, Pane, Settings}; use crate::{ItemHandle, Pane, Settings};
use gpui::{ use gpui::{
elements::*, AnyViewHandle, ElementBox, Entity, MutableAppContext, RenderContext, Subscription, elements::*, AnyViewHandle, ElementBox, Entity, MutableAppContext, RenderContext, Subscription,
View, ViewContext, ViewHandle, View, ViewContext, ViewHandle,
@ -7,7 +7,7 @@ use gpui::{
pub trait StatusItemView: View { pub trait StatusItemView: View {
fn set_active_pane_item( fn set_active_pane_item(
&mut self, &mut self,
active_pane_item: Option<&dyn crate::ItemViewHandle>, active_pane_item: Option<&dyn crate::ItemHandle>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
); );
} }
@ -16,7 +16,7 @@ trait StatusItemViewHandle {
fn to_any(&self) -> AnyViewHandle; fn to_any(&self) -> AnyViewHandle;
fn set_active_pane_item( fn set_active_pane_item(
&self, &self,
active_pane_item: Option<&dyn ItemViewHandle>, active_pane_item: Option<&dyn ItemHandle>,
cx: &mut MutableAppContext, cx: &mut MutableAppContext,
); );
} }
@ -38,7 +38,7 @@ impl View for StatusBar {
} }
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox { 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() Flex::row()
.with_children(self.left_items.iter().map(|i| { .with_children(self.left_items.iter().map(|i| {
ChildView::new(i.as_ref()) ChildView::new(i.as_ref())
@ -114,7 +114,7 @@ impl<T: StatusItemView> StatusItemViewHandle for ViewHandle<T> {
fn set_active_pane_item( fn set_active_pane_item(
&self, &self,
active_pane_item: Option<&dyn ItemViewHandle>, active_pane_item: Option<&dyn ItemHandle>,
cx: &mut MutableAppContext, cx: &mut MutableAppContext,
) { ) {
self.update(cx, |this, cx| { self.update(cx, |this, cx| {

File diff suppressed because it is too large Load diff

View file

@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
description = "The fast, collaborative code editor." description = "The fast, collaborative code editor."
edition = "2021" edition = "2021"
name = "zed" name = "zed"
version = "0.21.0" version = "0.23.0"
[lib] [lib]
name = "zed" name = "zed"
@ -55,6 +55,7 @@ text = { path = "../text" }
theme = { path = "../theme" } theme = { path = "../theme" }
theme_selector = { path = "../theme_selector" } theme_selector = { path = "../theme_selector" }
util = { path = "../util" } util = { path = "../util" }
vim = { path = "../vim" }
workspace = { path = "../workspace" } workspace = { path = "../workspace" }
anyhow = "1.0.38" anyhow = "1.0.38"
async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] } async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
@ -64,6 +65,7 @@ crossbeam-channel = "0.5.0"
ctor = "0.1.20" ctor = "0.1.20"
dirs = "3.0" dirs = "3.0"
easy-parallel = "3.1.0" easy-parallel = "3.1.0"
env_logger = "0.8"
futures = "0.3" futures = "0.3"
http-auth-basic = "0.1.3" http-auth-basic = "0.1.3"
ignore = "0.4" ignore = "0.4"

View file

@ -4,6 +4,8 @@ base = { family = "Zed Sans", size = 14 }
[workspace] [workspace]
background = "$surface.0" background = "$surface.0"
pane_divider = { width = 1, color = "$border.0" } pane_divider = { width = 1, color = "$border.0" }
leader_border_opacity = 0.7
leader_border_width = 2.0
[workspace.titlebar] [workspace.titlebar]
height = 32 height = 32

View file

@ -9,7 +9,6 @@ use gpui::{App, AssetSource, Task};
use log::LevelFilter; use log::LevelFilter;
use parking_lot::Mutex; use parking_lot::Mutex;
use project::Fs; use project::Fs;
use simplelog::SimpleLogger;
use smol::process::Command; use smol::process::Command;
use std::{env, fs, path::PathBuf, sync::Arc}; use std::{env, fs, path::PathBuf, sync::Arc};
use theme::{ThemeRegistry, DEFAULT_THEME_NAME}; use theme::{ThemeRegistry, DEFAULT_THEME_NAME};
@ -61,7 +60,6 @@ fn main() {
app.run(move |cx| { app.run(move |cx| {
let http = http::client(); let http = http::client();
let client = client::Client::new(http.clone()); 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 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 user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
let channel_list = let channel_list =
@ -70,8 +68,8 @@ fn main() {
project::Project::init(&client); project::Project::init(&client);
client::Channel::init(&client); client::Channel::init(&client);
client::init(client.clone(), cx); client::init(client.clone(), cx);
workspace::init(cx); workspace::init(&client, cx);
editor::init(cx, &mut path_openers); editor::init(cx);
go_to_line::init(cx); go_to_line::init(cx);
file_finder::init(cx); file_finder::init(cx);
chat_panel::init(cx); chat_panel::init(cx);
@ -80,11 +78,18 @@ fn main() {
project_panel::init(cx); project_panel::init(cx);
diagnostics::init(cx); diagnostics::init(cx);
search::init(cx); search::init(cx);
vim::init(cx);
cx.spawn({ cx.spawn({
let client = client.clone(); let client = client.clone();
|cx| async move { |cx| async move {
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) { if client.has_keychain_credentials(&cx) {
client.authenticate_and_connect(&cx).await?; client.authenticate_and_connect(true, &cx).await?;
}
} }
Ok::<_, anyhow::Error>(()) Ok::<_, anyhow::Error>(())
} }
@ -102,7 +107,7 @@ fn main() {
cx.spawn(|mut cx| async move { cx.spawn(|mut cx| async move {
while let Some(settings) = settings_rx.next().await { while let Some(settings) = settings_rx.next().await {
cx.update(|cx| { cx.update(|cx| {
cx.update_app_state(|s, _| *s = settings); cx.update_global(|s, _| *s = settings);
cx.refresh_windows(); cx.refresh_windows();
}); });
} }
@ -111,7 +116,7 @@ fn main() {
languages.set_language_server_download_dir(zed::ROOT_PATH.clone()); languages.set_language_server_download_dir(zed::ROOT_PATH.clone());
languages.set_theme(&settings.theme.editor.syntax); languages.set_theme(&settings.theme.editor.syntax);
cx.add_app_state(settings); cx.set_global(settings);
let app_state = Arc::new(AppState { let app_state = Arc::new(AppState {
languages: Arc::new(languages), languages: Arc::new(languages),
@ -120,7 +125,6 @@ fn main() {
client, client,
user_store, user_store,
fs, fs,
path_openers: Arc::from(path_openers),
build_window_options: &build_window_options, build_window_options: &build_window_options,
build_workspace: &build_workspace, build_workspace: &build_workspace,
}); });
@ -144,11 +148,10 @@ fn main() {
} }
fn init_logger() { fn init_logger() {
let level = LevelFilter::Info;
if stdout_is_a_pty() { if stdout_is_a_pty() {
SimpleLogger::init(level, Default::default()).expect("could not initialize logger"); env_logger::init();
} else { } else {
let level = LevelFilter::Info;
let log_dir_path = dirs::home_dir() let log_dir_path = dirs::home_dir()
.expect("could not locate home directory for logging") .expect("could not locate home directory for logging")
.join("Library/Logs/"); .join("Library/Logs/");

View file

@ -17,9 +17,8 @@ fn init_logger() {
pub fn test_app_state(cx: &mut MutableAppContext) -> Arc<AppState> { pub fn test_app_state(cx: &mut MutableAppContext) -> Arc<AppState> {
let settings = Settings::test(cx); let settings = Settings::test(cx);
let mut path_openers = Vec::new(); editor::init(cx);
editor::init(cx, &mut path_openers); cx.set_global(settings);
cx.add_app_state(settings);
let themes = ThemeRegistry::new(Assets, cx.font_cache().clone()); let themes = ThemeRegistry::new(Assets, cx.font_cache().clone());
let http = FakeHttpClient::with_404_response(); let http = FakeHttpClient::with_404_response();
let client = Client::new(http.clone()); let client = Client::new(http.clone());
@ -40,7 +39,6 @@ pub fn test_app_state(cx: &mut MutableAppContext) -> Arc<AppState> {
client, client,
user_store, user_store,
fs: FakeFs::new(cx.background().clone()), fs: FakeFs::new(cx.background().clone()),
path_openers: Arc::from(path_openers),
build_window_options: &build_window_options, build_window_options: &build_window_options,
build_workspace: &build_workspace, build_workspace: &build_workspace,
}) })

View file

@ -43,7 +43,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
cx.add_global_action(quit); cx.add_global_action(quit);
cx.add_global_action({ cx.add_global_action({
move |action: &AdjustBufferFontSize, cx| { 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 =
(settings.buffer_font_size + action.0).max(MIN_FONT_SIZE); (settings.buffer_font_size + action.0).max(MIN_FONT_SIZE);
cx.refresh_windows(); cx.refresh_windows();
@ -111,7 +111,6 @@ pub fn build_workspace(
languages: app_state.languages.clone(), languages: app_state.languages.clone(),
user_store: app_state.user_store.clone(), user_store: app_state.user_store.clone(),
channel_list: app_state.channel_list.clone(), channel_list: app_state.channel_list.clone(),
path_openers: app_state.path_openers.clone(),
}; };
let mut workspace = Workspace::new(&workspace_params, cx); let mut workspace = Workspace::new(&workspace_params, cx);
let project = workspace.project().clone(); let project = workspace.project().clone();
@ -193,7 +192,7 @@ mod tests {
use theme::{Theme, ThemeRegistry, DEFAULT_THEME_NAME}; use theme::{Theme, ThemeRegistry, DEFAULT_THEME_NAME};
use util::test::temp_tree; use util::test::temp_tree;
use workspace::{ use workspace::{
open_paths, pane, ItemView, ItemViewHandle, OpenNew, Pane, SplitDirection, WorkspaceHandle, open_paths, pane, Item, ItemHandle, OpenNew, Pane, SplitDirection, WorkspaceHandle,
}; };
#[gpui::test] #[gpui::test]
@ -253,7 +252,7 @@ mod tests {
async fn test_new_empty_workspace(cx: &mut TestAppContext) { async fn test_new_empty_workspace(cx: &mut TestAppContext) {
let app_state = cx.update(test_app_state); let app_state = cx.update(test_app_state);
cx.update(|cx| { cx.update(|cx| {
workspace::init(cx); workspace::init(&app_state.client, cx);
}); });
cx.dispatch_global_action(workspace::OpenNew(app_state.clone())); cx.dispatch_global_action(workspace::OpenNew(app_state.clone()));
let window_id = *cx.window_ids().first().unwrap(); let window_id = *cx.window_ids().first().unwrap();
@ -325,7 +324,7 @@ mod tests {
pane.active_item().unwrap().project_path(cx), pane.active_item().unwrap().project_path(cx),
Some(file1.clone()) Some(file1.clone())
); );
assert_eq!(pane.item_views().count(), 1); assert_eq!(pane.items().count(), 1);
}); });
// Open the second entry // Open the second entry
@ -339,7 +338,7 @@ mod tests {
pane.active_item().unwrap().project_path(cx), pane.active_item().unwrap().project_path(cx),
Some(file2.clone()) 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. // Open the first entry again. The existing pane item is activated.
@ -355,7 +354,7 @@ mod tests {
pane.active_item().unwrap().project_path(cx), pane.active_item().unwrap().project_path(cx),
Some(file1.clone()) 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. // Split the pane with the first entry, then open the second entry again.
@ -394,7 +393,7 @@ mod tests {
Some(file3.clone()) Some(file3.clone())
); );
let pane_entries = pane let pane_entries = pane
.item_views() .items()
.map(|i| i.project_path(cx).unwrap()) .map(|i| i.project_path(cx).unwrap())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
assert_eq!(pane_entries, &[file1, file2, file3]); assert_eq!(pane_entries, &[file1, file2, file3]);
@ -894,6 +893,52 @@ mod tests {
(file3.clone(), DisplayPoint::new(0, 0)) (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( fn active_location(
workspace: &ViewHandle<Workspace>, workspace: &ViewHandle<Workspace>,
cx: &mut TestAppContext, cx: &mut TestAppContext,