Merge branch 'main' into breadcrumbs
This commit is contained in:
commit
d7026c2228
79 changed files with 7650 additions and 3174 deletions
35
Cargo.lock
generated
35
Cargo.lock
generated
|
@ -1617,6 +1617,7 @@ dependencies = [
|
|||
"collections",
|
||||
"ctor",
|
||||
"env_logger",
|
||||
"futures",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"itertools",
|
||||
|
@ -1629,6 +1630,7 @@ dependencies = [
|
|||
"postage",
|
||||
"project",
|
||||
"rand 0.8.3",
|
||||
"rpc",
|
||||
"serde",
|
||||
"smallvec",
|
||||
"smol",
|
||||
|
@ -1782,7 +1784,9 @@ dependencies = [
|
|||
name = "file_finder"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ctor",
|
||||
"editor",
|
||||
"env_logger",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"postage",
|
||||
|
@ -2524,6 +2528,15 @@ dependencies = [
|
|||
"hashbrown 0.9.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indoc"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7906a9fababaeacb774f72410e497a1d18de916322e33797bb2cd29baa23c9e"
|
||||
dependencies = [
|
||||
"unindent",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "infer"
|
||||
version = "0.2.3"
|
||||
|
@ -5550,9 +5563,9 @@ checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
|
|||
|
||||
[[package]]
|
||||
name = "unindent"
|
||||
version = "0.1.7"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f14ee04d9415b52b3aeab06258a3f07093182b88ba0f9b8d203f211a7a7d41c7"
|
||||
checksum = "514672a55d7380da379785a4d70ca8386c8883ff7eaae877be4d2081cebe73d8"
|
||||
|
||||
[[package]]
|
||||
name = "universal-hash"
|
||||
|
@ -5670,6 +5683,21 @@ version = "0.9.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed"
|
||||
|
||||
[[package]]
|
||||
name = "vim"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"collections",
|
||||
"editor",
|
||||
"gpui",
|
||||
"indoc",
|
||||
"language",
|
||||
"log",
|
||||
"project",
|
||||
"util",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "waker-fn"
|
||||
version = "1.1.0"
|
||||
|
@ -5929,7 +5957,7 @@ checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
|
|||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.21.0"
|
||||
version = "0.23.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-compression",
|
||||
|
@ -6000,6 +6028,7 @@ dependencies = [
|
|||
"unindent",
|
||||
"url",
|
||||
"util",
|
||||
"vim",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
|
|
|
@ -64,13 +64,13 @@ impl ChatPanel {
|
|||
ix,
|
||||
item_type,
|
||||
is_hovered,
|
||||
&cx.app_state::<Settings>().theme.chat_panel.channel_select,
|
||||
&cx.global::<Settings>().theme.chat_panel.channel_select,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.with_style(move |cx| {
|
||||
let theme = &cx.app_state::<Settings>().theme.chat_panel.channel_select;
|
||||
let theme = &cx.global::<Settings>().theme.chat_panel.channel_select;
|
||||
SelectStyle {
|
||||
header: theme.header.container.clone(),
|
||||
menu: theme.menu.clone(),
|
||||
|
@ -200,7 +200,7 @@ impl ChatPanel {
|
|||
}
|
||||
|
||||
fn render_channel(&self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let theme = &cx.app_state::<Settings>().theme;
|
||||
let theme = &cx.global::<Settings>().theme;
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Container::new(ChildView::new(&self.channel_select).boxed())
|
||||
|
@ -224,7 +224,7 @@ impl ChatPanel {
|
|||
|
||||
fn render_message(&self, message: &ChannelMessage, cx: &AppContext) -> ElementBox {
|
||||
let now = OffsetDateTime::now_utc();
|
||||
let settings = cx.app_state::<Settings>();
|
||||
let settings = cx.global::<Settings>();
|
||||
let theme = if message.is_pending() {
|
||||
&settings.theme.chat_panel.pending_message
|
||||
} else {
|
||||
|
@ -267,7 +267,7 @@ impl ChatPanel {
|
|||
}
|
||||
|
||||
fn render_input_box(&self, cx: &AppContext) -> ElementBox {
|
||||
let theme = &cx.app_state::<Settings>().theme;
|
||||
let theme = &cx.global::<Settings>().theme;
|
||||
Container::new(ChildView::new(&self.input_editor).boxed())
|
||||
.with_style(theme.chat_panel.input_editor.container)
|
||||
.boxed()
|
||||
|
@ -304,7 +304,7 @@ impl ChatPanel {
|
|||
}
|
||||
|
||||
fn render_sign_in_prompt(&self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let theme = cx.app_state::<Settings>().theme.clone();
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
let rpc = self.rpc.clone();
|
||||
let this = cx.handle();
|
||||
|
||||
|
@ -327,7 +327,12 @@ impl ChatPanel {
|
|||
let rpc = rpc.clone();
|
||||
let this = this.clone();
|
||||
cx.spawn(|mut cx| async move {
|
||||
if rpc.authenticate_and_connect(&cx).log_err().await.is_some() {
|
||||
if rpc
|
||||
.authenticate_and_connect(true, &cx)
|
||||
.log_err()
|
||||
.await
|
||||
.is_some()
|
||||
{
|
||||
cx.update(|cx| {
|
||||
if let Some(this) = this.upgrade(cx) {
|
||||
if this.is_focused(cx) {
|
||||
|
@ -385,7 +390,7 @@ impl View for ChatPanel {
|
|||
} else {
|
||||
self.render_sign_in_prompt(cx)
|
||||
};
|
||||
let theme = &cx.app_state::<Settings>().theme;
|
||||
let theme = &cx.global::<Settings>().theme;
|
||||
ConstrainedBox::new(
|
||||
Container::new(element)
|
||||
.with_style(theme.chat_panel.container)
|
||||
|
|
|
@ -181,7 +181,7 @@ impl Entity for Channel {
|
|||
|
||||
impl Channel {
|
||||
pub fn init(rpc: &Arc<Client>) {
|
||||
rpc.add_entity_message_handler(Self::handle_message_sent);
|
||||
rpc.add_model_message_handler(Self::handle_message_sent);
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
|
|
|
@ -13,8 +13,8 @@ use async_tungstenite::tungstenite::{
|
|||
};
|
||||
use futures::{future::LocalBoxFuture, FutureExt, StreamExt};
|
||||
use gpui::{
|
||||
action, AnyModelHandle, AnyWeakModelHandle, AsyncAppContext, Entity, ModelContext, ModelHandle,
|
||||
MutableAppContext, Task,
|
||||
action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, AsyncAppContext,
|
||||
Entity, ModelContext, ModelHandle, MutableAppContext, Task, View, ViewContext, ViewHandle,
|
||||
};
|
||||
use http::HttpClient;
|
||||
use lazy_static::lazy_static;
|
||||
|
@ -45,7 +45,7 @@ pub use user::*;
|
|||
lazy_static! {
|
||||
static ref ZED_SERVER_URL: String =
|
||||
std::env::var("ZED_SERVER_URL").unwrap_or("https://zed.dev".to_string());
|
||||
static ref IMPERSONATE_LOGIN: Option<String> = std::env::var("ZED_IMPERSONATE")
|
||||
pub static ref IMPERSONATE_LOGIN: Option<String> = std::env::var("ZED_IMPERSONATE")
|
||||
.ok()
|
||||
.and_then(|s| if s.is_empty() { None } else { Some(s) });
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ action!(Authenticate);
|
|||
pub fn init(rpc: Arc<Client>, cx: &mut MutableAppContext) {
|
||||
cx.add_global_action(move |_: &Authenticate, cx| {
|
||||
let rpc = rpc.clone();
|
||||
cx.spawn(|cx| async move { rpc.authenticate_and_connect(&cx).log_err().await })
|
||||
cx.spawn(|cx| async move { rpc.authenticate_and_connect(true, &cx).log_err().await })
|
||||
.detach();
|
||||
});
|
||||
}
|
||||
|
@ -136,26 +136,37 @@ impl Status {
|
|||
struct ClientState {
|
||||
credentials: Option<Credentials>,
|
||||
status: (watch::Sender<Status>, watch::Receiver<Status>),
|
||||
entity_id_extractors: HashMap<TypeId, Box<dyn Send + Sync + Fn(&dyn AnyTypedEnvelope) -> u64>>,
|
||||
entity_id_extractors: HashMap<TypeId, fn(&dyn AnyTypedEnvelope) -> u64>,
|
||||
_reconnect_task: Option<Task<()>>,
|
||||
reconnect_interval: Duration,
|
||||
models_by_entity_type_and_remote_id: HashMap<(TypeId, u64), AnyWeakModelHandle>,
|
||||
entities_by_type_and_remote_id: HashMap<(TypeId, u64), AnyWeakEntityHandle>,
|
||||
models_by_message_type: HashMap<TypeId, AnyWeakModelHandle>,
|
||||
model_types_by_message_type: HashMap<TypeId, TypeId>,
|
||||
entity_types_by_message_type: HashMap<TypeId, TypeId>,
|
||||
message_handlers: HashMap<
|
||||
TypeId,
|
||||
Arc<
|
||||
dyn Send
|
||||
+ Sync
|
||||
+ Fn(
|
||||
AnyModelHandle,
|
||||
AnyEntityHandle,
|
||||
Box<dyn AnyTypedEnvelope>,
|
||||
&Arc<Client>,
|
||||
AsyncAppContext,
|
||||
) -> LocalBoxFuture<'static, Result<()>>,
|
||||
>,
|
||||
>,
|
||||
}
|
||||
|
||||
enum AnyWeakEntityHandle {
|
||||
Model(AnyWeakModelHandle),
|
||||
View(AnyWeakViewHandle),
|
||||
}
|
||||
|
||||
enum AnyEntityHandle {
|
||||
Model(AnyModelHandle),
|
||||
View(AnyViewHandle),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Credentials {
|
||||
pub user_id: u64,
|
||||
|
@ -171,8 +182,8 @@ impl Default for ClientState {
|
|||
_reconnect_task: None,
|
||||
reconnect_interval: Duration::from_secs(5),
|
||||
models_by_message_type: Default::default(),
|
||||
models_by_entity_type_and_remote_id: Default::default(),
|
||||
model_types_by_message_type: Default::default(),
|
||||
entities_by_type_and_remote_id: Default::default(),
|
||||
entity_types_by_message_type: Default::default(),
|
||||
message_handlers: Default::default(),
|
||||
}
|
||||
}
|
||||
|
@ -195,13 +206,13 @@ impl Drop for Subscription {
|
|||
Subscription::Entity { client, id } => {
|
||||
if let Some(client) = client.upgrade() {
|
||||
let mut state = client.state.write();
|
||||
let _ = state.models_by_entity_type_and_remote_id.remove(id);
|
||||
let _ = state.entities_by_type_and_remote_id.remove(id);
|
||||
}
|
||||
}
|
||||
Subscription::Message { client, id } => {
|
||||
if let Some(client) = client.upgrade() {
|
||||
let mut state = client.state.write();
|
||||
let _ = state.model_types_by_message_type.remove(id);
|
||||
let _ = state.entity_types_by_message_type.remove(id);
|
||||
let _ = state.message_handlers.remove(id);
|
||||
}
|
||||
}
|
||||
|
@ -239,7 +250,7 @@ impl Client {
|
|||
state._reconnect_task.take();
|
||||
state.message_handlers.clear();
|
||||
state.models_by_message_type.clear();
|
||||
state.models_by_entity_type_and_remote_id.clear();
|
||||
state.entities_by_type_and_remote_id.clear();
|
||||
state.entity_id_extractors.clear();
|
||||
self.peer.reset();
|
||||
}
|
||||
|
@ -291,7 +302,7 @@ impl Client {
|
|||
state._reconnect_task = Some(cx.spawn(|cx| async move {
|
||||
let mut rng = StdRng::from_entropy();
|
||||
let mut delay = Duration::from_millis(100);
|
||||
while let Err(error) = this.authenticate_and_connect(&cx).await {
|
||||
while let Err(error) = this.authenticate_and_connect(true, &cx).await {
|
||||
log::error!("failed to connect {}", error);
|
||||
this.set_status(
|
||||
Status::ReconnectionError {
|
||||
|
@ -313,17 +324,32 @@ impl Client {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn add_view_for_remote_entity<T: View>(
|
||||
self: &Arc<Self>,
|
||||
remote_id: u64,
|
||||
cx: &mut ViewContext<T>,
|
||||
) -> Subscription {
|
||||
let id = (TypeId::of::<T>(), remote_id);
|
||||
self.state
|
||||
.write()
|
||||
.entities_by_type_and_remote_id
|
||||
.insert(id, AnyWeakEntityHandle::View(cx.weak_handle().into()));
|
||||
Subscription::Entity {
|
||||
client: Arc::downgrade(self),
|
||||
id,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_model_for_remote_entity<T: Entity>(
|
||||
self: &Arc<Self>,
|
||||
remote_id: u64,
|
||||
cx: &mut ModelContext<T>,
|
||||
) -> Subscription {
|
||||
let handle = AnyModelHandle::from(cx.handle());
|
||||
let mut state = self.state.write();
|
||||
let id = (TypeId::of::<T>(), remote_id);
|
||||
state
|
||||
.models_by_entity_type_and_remote_id
|
||||
.insert(id, handle.downgrade());
|
||||
self.state
|
||||
.write()
|
||||
.entities_by_type_and_remote_id
|
||||
.insert(id, AnyWeakEntityHandle::Model(cx.weak_handle().into()));
|
||||
Subscription::Entity {
|
||||
client: Arc::downgrade(self),
|
||||
id,
|
||||
|
@ -346,7 +372,6 @@ impl Client {
|
|||
{
|
||||
let message_type_id = TypeId::of::<M>();
|
||||
|
||||
let client = Arc::downgrade(self);
|
||||
let mut state = self.state.write();
|
||||
state
|
||||
.models_by_message_type
|
||||
|
@ -354,14 +379,15 @@ impl Client {
|
|||
|
||||
let prev_handler = state.message_handlers.insert(
|
||||
message_type_id,
|
||||
Arc::new(move |handle, envelope, cx| {
|
||||
Arc::new(move |handle, envelope, client, cx| {
|
||||
let handle = if let AnyEntityHandle::Model(handle) = handle {
|
||||
handle
|
||||
} else {
|
||||
unreachable!();
|
||||
};
|
||||
let model = handle.downcast::<E>().unwrap();
|
||||
let envelope = envelope.into_any().downcast::<TypedEnvelope<M>>().unwrap();
|
||||
if let Some(client) = client.upgrade() {
|
||||
handler(model, *envelope, client.clone(), cx).boxed_local()
|
||||
} else {
|
||||
async move { Ok(()) }.boxed_local()
|
||||
}
|
||||
}),
|
||||
);
|
||||
if prev_handler.is_some() {
|
||||
|
@ -374,7 +400,26 @@ impl Client {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn add_entity_message_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
|
||||
pub fn add_view_message_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
|
||||
where
|
||||
M: EntityMessage,
|
||||
E: View,
|
||||
H: 'static
|
||||
+ Send
|
||||
+ Sync
|
||||
+ Fn(ViewHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
|
||||
F: 'static + Future<Output = Result<()>>,
|
||||
{
|
||||
self.add_entity_message_handler::<M, E, _, _>(move |handle, message, client, cx| {
|
||||
if let AnyEntityHandle::View(handle) = handle {
|
||||
handler(handle.downcast::<E>().unwrap(), message, client, cx)
|
||||
} else {
|
||||
unreachable!();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn add_model_message_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
|
||||
where
|
||||
M: EntityMessage,
|
||||
E: Entity,
|
||||
|
@ -383,38 +428,51 @@ impl Client {
|
|||
+ Sync
|
||||
+ Fn(ModelHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
|
||||
F: 'static + Future<Output = Result<()>>,
|
||||
{
|
||||
self.add_entity_message_handler::<M, E, _, _>(move |handle, message, client, cx| {
|
||||
if let AnyEntityHandle::Model(handle) = handle {
|
||||
handler(handle.downcast::<E>().unwrap(), message, client, cx)
|
||||
} else {
|
||||
unreachable!();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn add_entity_message_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
|
||||
where
|
||||
M: EntityMessage,
|
||||
E: Entity,
|
||||
H: 'static
|
||||
+ Send
|
||||
+ Sync
|
||||
+ Fn(AnyEntityHandle, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
|
||||
F: 'static + Future<Output = Result<()>>,
|
||||
{
|
||||
let model_type_id = TypeId::of::<E>();
|
||||
let message_type_id = TypeId::of::<M>();
|
||||
|
||||
let client = Arc::downgrade(self);
|
||||
let mut state = self.state.write();
|
||||
state
|
||||
.model_types_by_message_type
|
||||
.entity_types_by_message_type
|
||||
.insert(message_type_id, model_type_id);
|
||||
state
|
||||
.entity_id_extractors
|
||||
.entry(message_type_id)
|
||||
.or_insert_with(|| {
|
||||
Box::new(|envelope| {
|
||||
let envelope = envelope
|
||||
|envelope| {
|
||||
envelope
|
||||
.as_any()
|
||||
.downcast_ref::<TypedEnvelope<M>>()
|
||||
.unwrap();
|
||||
envelope.payload.remote_entity_id()
|
||||
})
|
||||
.unwrap()
|
||||
.payload
|
||||
.remote_entity_id()
|
||||
}
|
||||
});
|
||||
|
||||
let prev_handler = state.message_handlers.insert(
|
||||
message_type_id,
|
||||
Arc::new(move |handle, envelope, cx| {
|
||||
let model = handle.downcast::<E>().unwrap();
|
||||
Arc::new(move |handle, envelope, client, cx| {
|
||||
let envelope = envelope.into_any().downcast::<TypedEnvelope<M>>().unwrap();
|
||||
if let Some(client) = client.upgrade() {
|
||||
handler(model, *envelope, client.clone(), cx).boxed_local()
|
||||
} else {
|
||||
async move { Ok(()) }.boxed_local()
|
||||
}
|
||||
handler(handle, *envelope, client.clone(), cx).boxed_local()
|
||||
}),
|
||||
);
|
||||
if prev_handler.is_some() {
|
||||
|
@ -422,7 +480,7 @@ impl Client {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn add_entity_request_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
|
||||
pub fn add_model_request_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
|
||||
where
|
||||
M: EntityMessage + RequestMessage,
|
||||
E: Entity,
|
||||
|
@ -432,10 +490,39 @@ impl Client {
|
|||
+ Fn(ModelHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
|
||||
F: 'static + Future<Output = Result<M::Response>>,
|
||||
{
|
||||
self.add_entity_message_handler(move |model, envelope, client, cx| {
|
||||
let receipt = envelope.receipt();
|
||||
let response = handler(model, envelope, client.clone(), cx);
|
||||
async move {
|
||||
self.add_model_message_handler(move |entity, envelope, client, cx| {
|
||||
Self::respond_to_request::<M, _>(
|
||||
envelope.receipt(),
|
||||
handler(entity, envelope, client.clone(), cx),
|
||||
client,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn add_view_request_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
|
||||
where
|
||||
M: EntityMessage + RequestMessage,
|
||||
E: View,
|
||||
H: 'static
|
||||
+ Send
|
||||
+ Sync
|
||||
+ Fn(ViewHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
|
||||
F: 'static + Future<Output = Result<M::Response>>,
|
||||
{
|
||||
self.add_view_message_handler(move |entity, envelope, client, cx| {
|
||||
Self::respond_to_request::<M, _>(
|
||||
envelope.receipt(),
|
||||
handler(entity, envelope, client.clone(), cx),
|
||||
client,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async fn respond_to_request<T: RequestMessage, F: Future<Output = Result<T::Response>>>(
|
||||
receipt: Receipt<T>,
|
||||
response: F,
|
||||
client: Arc<Self>,
|
||||
) -> Result<()> {
|
||||
match response.await {
|
||||
Ok(response) => {
|
||||
client.respond(receipt, response)?;
|
||||
|
@ -452,8 +539,6 @@ impl Client {
|
|||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn has_keychain_credentials(&self, cx: &AsyncAppContext) -> bool {
|
||||
read_credentials_from_keychain(cx).is_some()
|
||||
|
@ -462,6 +547,7 @@ impl Client {
|
|||
#[async_recursion(?Send)]
|
||||
pub async fn authenticate_and_connect(
|
||||
self: &Arc<Self>,
|
||||
try_keychain: bool,
|
||||
cx: &AsyncAppContext,
|
||||
) -> anyhow::Result<()> {
|
||||
let was_disconnected = match *self.status().borrow() {
|
||||
|
@ -483,23 +569,22 @@ impl Client {
|
|||
self.set_status(Status::Reauthenticating, cx)
|
||||
}
|
||||
|
||||
let mut used_keychain = false;
|
||||
let credentials = self.state.read().credentials.clone();
|
||||
let credentials = if let Some(credentials) = credentials {
|
||||
credentials
|
||||
} else if let Some(credentials) = read_credentials_from_keychain(cx) {
|
||||
used_keychain = true;
|
||||
credentials
|
||||
} else {
|
||||
let credentials = match self.authenticate(&cx).await {
|
||||
let mut read_from_keychain = false;
|
||||
let mut credentials = self.state.read().credentials.clone();
|
||||
if credentials.is_none() && try_keychain {
|
||||
credentials = read_credentials_from_keychain(cx);
|
||||
read_from_keychain = credentials.is_some();
|
||||
}
|
||||
if credentials.is_none() {
|
||||
credentials = Some(match self.authenticate(&cx).await {
|
||||
Ok(credentials) => credentials,
|
||||
Err(err) => {
|
||||
self.set_status(Status::ConnectionError, cx);
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
credentials
|
||||
};
|
||||
});
|
||||
}
|
||||
let credentials = credentials.unwrap();
|
||||
|
||||
if was_disconnected {
|
||||
self.set_status(Status::Connecting, cx);
|
||||
|
@ -510,7 +595,7 @@ impl Client {
|
|||
match self.establish_connection(&credentials, cx).await {
|
||||
Ok(conn) => {
|
||||
self.state.write().credentials = Some(credentials.clone());
|
||||
if !used_keychain && IMPERSONATE_LOGIN.is_none() {
|
||||
if !read_from_keychain && IMPERSONATE_LOGIN.is_none() {
|
||||
write_credentials_to_keychain(&credentials, cx).log_err();
|
||||
}
|
||||
self.set_connection(conn, cx).await;
|
||||
|
@ -518,10 +603,10 @@ impl Client {
|
|||
}
|
||||
Err(EstablishConnectionError::Unauthorized) => {
|
||||
self.state.write().credentials.take();
|
||||
if used_keychain {
|
||||
if read_from_keychain {
|
||||
cx.platform().delete_credentials(&ZED_SERVER_URL).log_err();
|
||||
self.set_status(Status::SignedOut, cx);
|
||||
self.authenticate_and_connect(cx).await
|
||||
self.authenticate_and_connect(false, cx).await
|
||||
} else {
|
||||
self.set_status(Status::ConnectionError, cx);
|
||||
Err(EstablishConnectionError::Unauthorized)?
|
||||
|
@ -561,24 +646,26 @@ impl Client {
|
|||
.models_by_message_type
|
||||
.get(&payload_type_id)
|
||||
.and_then(|model| model.upgrade(&cx))
|
||||
.map(AnyEntityHandle::Model)
|
||||
.or_else(|| {
|
||||
let model_type_id =
|
||||
*state.model_types_by_message_type.get(&payload_type_id)?;
|
||||
let entity_type_id =
|
||||
*state.entity_types_by_message_type.get(&payload_type_id)?;
|
||||
let entity_id = state
|
||||
.entity_id_extractors
|
||||
.get(&message.payload_type_id())
|
||||
.map(|extract_entity_id| {
|
||||
(extract_entity_id)(message.as_ref())
|
||||
})?;
|
||||
let model = state
|
||||
.models_by_entity_type_and_remote_id
|
||||
.get(&(model_type_id, entity_id))?;
|
||||
if let Some(model) = model.upgrade(&cx) {
|
||||
Some(model)
|
||||
|
||||
let entity = state
|
||||
.entities_by_type_and_remote_id
|
||||
.get(&(entity_type_id, entity_id))?;
|
||||
if let Some(entity) = entity.upgrade(&cx) {
|
||||
Some(entity)
|
||||
} else {
|
||||
state
|
||||
.models_by_entity_type_and_remote_id
|
||||
.remove(&(model_type_id, entity_id));
|
||||
.entities_by_type_and_remote_id
|
||||
.remove(&(entity_type_id, entity_id));
|
||||
None
|
||||
}
|
||||
});
|
||||
|
@ -593,7 +680,7 @@ impl Client {
|
|||
if let Some(handler) = state.message_handlers.get(&payload_type_id).cloned()
|
||||
{
|
||||
drop(state); // Avoid deadlocks if the handler interacts with rpc::Client
|
||||
let future = handler(model, message, cx.clone());
|
||||
let future = handler(model, message, &this, cx.clone());
|
||||
|
||||
let client_id = this.id;
|
||||
log::debug!(
|
||||
|
@ -891,6 +978,15 @@ impl Client {
|
|||
}
|
||||
}
|
||||
|
||||
impl AnyWeakEntityHandle {
|
||||
fn upgrade(&self, cx: &AsyncAppContext) -> Option<AnyEntityHandle> {
|
||||
match self {
|
||||
AnyWeakEntityHandle::Model(handle) => handle.upgrade(cx).map(AnyEntityHandle::Model),
|
||||
AnyWeakEntityHandle::View(handle) => handle.upgrade(cx).map(AnyEntityHandle::View),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option<Credentials> {
|
||||
if IMPERSONATE_LOGIN.is_some() {
|
||||
return None;
|
||||
|
@ -994,7 +1090,7 @@ mod tests {
|
|||
|
||||
let (done_tx1, mut done_rx1) = smol::channel::unbounded();
|
||||
let (done_tx2, mut done_rx2) = smol::channel::unbounded();
|
||||
client.add_entity_message_handler(
|
||||
client.add_model_message_handler(
|
||||
move |model: ModelHandle<Model>, _: TypedEnvelope<proto::UnshareProject>, _, cx| {
|
||||
match model.read_with(&cx, |model, _| model.id) {
|
||||
1 => done_tx1.try_send(()).unwrap(),
|
||||
|
|
|
@ -91,7 +91,7 @@ impl FakeServer {
|
|||
});
|
||||
|
||||
client
|
||||
.authenticate_and_connect(&cx.to_async())
|
||||
.authenticate_and_connect(false, &cx.to_async())
|
||||
.await
|
||||
.unwrap();
|
||||
server
|
||||
|
|
|
@ -55,7 +55,7 @@ impl ContactsPanel {
|
|||
app_state: Arc<AppState>,
|
||||
cx: &mut LayoutContext,
|
||||
) -> ElementBox {
|
||||
let theme = cx.app_state::<Settings>().theme.clone();
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
let theme = &theme.contacts_panel;
|
||||
let project_count = collaborator.projects.len();
|
||||
let font_cache = cx.font_cache();
|
||||
|
@ -236,7 +236,7 @@ impl View for ContactsPanel {
|
|||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let theme = &cx.app_state::<Settings>().theme.contacts_panel;
|
||||
let theme = &cx.global::<Settings>().theme.contacts_panel;
|
||||
Container::new(List::new(self.contacts.clone()).boxed())
|
||||
.with_style(theme.container)
|
||||
.boxed()
|
||||
|
|
|
@ -25,7 +25,7 @@ use std::{
|
|||
sync::Arc,
|
||||
};
|
||||
use util::TryFutureExt;
|
||||
use workspace::{ItemHandle, ItemNavHistory, ItemViewHandle as _, Settings, Workspace};
|
||||
use workspace::{ItemHandle as _, ItemNavHistory, Settings, Workspace};
|
||||
|
||||
action!(Deploy);
|
||||
|
||||
|
@ -38,12 +38,8 @@ pub fn init(cx: &mut MutableAppContext) {
|
|||
|
||||
type Event = editor::Event;
|
||||
|
||||
struct ProjectDiagnostics {
|
||||
project: ModelHandle<Project>,
|
||||
}
|
||||
|
||||
struct ProjectDiagnosticsEditor {
|
||||
model: ModelHandle<ProjectDiagnostics>,
|
||||
project: ModelHandle<Project>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
editor: ViewHandle<Editor>,
|
||||
summary: DiagnosticSummary,
|
||||
|
@ -65,16 +61,6 @@ struct DiagnosticGroupState {
|
|||
block_count: usize,
|
||||
}
|
||||
|
||||
impl ProjectDiagnostics {
|
||||
fn new(project: ModelHandle<Project>) -> Self {
|
||||
Self { project }
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for ProjectDiagnostics {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl Entity for ProjectDiagnosticsEditor {
|
||||
type Event = Event;
|
||||
}
|
||||
|
@ -86,7 +72,7 @@ impl View for ProjectDiagnosticsEditor {
|
|||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
if self.path_states.is_empty() {
|
||||
let theme = &cx.app_state::<Settings>().theme.project_diagnostics;
|
||||
let theme = &cx.global::<Settings>().theme.project_diagnostics;
|
||||
Label::new(
|
||||
"No problems in workspace".to_string(),
|
||||
theme.empty_message.clone(),
|
||||
|
@ -109,12 +95,11 @@ impl View for ProjectDiagnosticsEditor {
|
|||
|
||||
impl ProjectDiagnosticsEditor {
|
||||
fn new(
|
||||
model: ModelHandle<ProjectDiagnostics>,
|
||||
project_handle: ModelHandle<Project>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let project = model.read(cx).project.clone();
|
||||
cx.subscribe(&project, |this, _, event, cx| match event {
|
||||
cx.subscribe(&project_handle, |this, _, event, cx| match event {
|
||||
project::Event::DiskBasedDiagnosticsFinished => {
|
||||
this.update_excerpts(cx);
|
||||
this.update_title(cx);
|
||||
|
@ -126,20 +111,22 @@ impl ProjectDiagnosticsEditor {
|
|||
})
|
||||
.detach();
|
||||
|
||||
let excerpts = cx.add_model(|cx| MultiBuffer::new(project.read(cx).replica_id()));
|
||||
let excerpts = cx.add_model(|cx| MultiBuffer::new(project_handle.read(cx).replica_id()));
|
||||
let editor = cx.add_view(|cx| {
|
||||
let mut editor = Editor::for_buffer(excerpts.clone(), Some(project.clone()), cx);
|
||||
let mut editor =
|
||||
Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), cx);
|
||||
editor.set_vertical_scroll_margin(5, cx);
|
||||
editor
|
||||
});
|
||||
cx.subscribe(&editor, |_, _, event, cx| cx.emit(*event))
|
||||
.detach();
|
||||
|
||||
let project = project.read(cx);
|
||||
let project = project_handle.read(cx);
|
||||
let paths_to_update = project.diagnostic_summaries(cx).map(|e| e.0).collect();
|
||||
let summary = project.diagnostic_summary(cx);
|
||||
let mut this = Self {
|
||||
model,
|
||||
summary: project.diagnostic_summary(cx),
|
||||
project: project_handle,
|
||||
summary,
|
||||
workspace,
|
||||
excerpts,
|
||||
editor,
|
||||
|
@ -151,18 +138,20 @@ impl ProjectDiagnosticsEditor {
|
|||
}
|
||||
|
||||
fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
|
||||
if let Some(existing) = workspace.item_of_type::<ProjectDiagnostics>(cx) {
|
||||
if let Some(existing) = workspace.item_of_type::<ProjectDiagnosticsEditor>(cx) {
|
||||
workspace.activate_item(&existing, cx);
|
||||
} else {
|
||||
let diagnostics =
|
||||
cx.add_model(|_| ProjectDiagnostics::new(workspace.project().clone()));
|
||||
workspace.open_item(diagnostics, cx);
|
||||
let workspace_handle = cx.weak_handle();
|
||||
let diagnostics = cx.add_view(|cx| {
|
||||
ProjectDiagnosticsEditor::new(workspace.project().clone(), workspace_handle, cx)
|
||||
});
|
||||
workspace.add_item(Box::new(diagnostics), cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_excerpts(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let paths = mem::take(&mut self.paths_to_update);
|
||||
let project = self.model.read(cx).project.clone();
|
||||
let project = self.project.clone();
|
||||
cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
for path in paths {
|
||||
|
@ -289,7 +278,7 @@ impl ProjectDiagnosticsEditor {
|
|||
prev_excerpt_id = excerpt_id.clone();
|
||||
first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
|
||||
group_state.excerpts.push(excerpt_id.clone());
|
||||
let header_position = (excerpt_id.clone(), language::Anchor::min());
|
||||
let header_position = (excerpt_id.clone(), language::Anchor::MIN);
|
||||
|
||||
if is_first_excerpt_for_group {
|
||||
is_first_excerpt_for_group = false;
|
||||
|
@ -378,8 +367,7 @@ impl ProjectDiagnosticsEditor {
|
|||
range_a
|
||||
.start
|
||||
.cmp(&range_b.start, &snapshot)
|
||||
.unwrap()
|
||||
.then_with(|| range_a.end.cmp(&range_b.end, &snapshot).unwrap())
|
||||
.then_with(|| range_a.end.cmp(&range_b.end, &snapshot))
|
||||
});
|
||||
|
||||
if path_state.diagnostic_groups.is_empty() {
|
||||
|
@ -443,42 +431,17 @@ impl ProjectDiagnosticsEditor {
|
|||
}
|
||||
|
||||
fn update_title(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.summary = self.model.read(cx).project.read(cx).diagnostic_summary(cx);
|
||||
self.summary = self.project.read(cx).diagnostic_summary(cx);
|
||||
cx.emit(Event::TitleChanged);
|
||||
}
|
||||
}
|
||||
|
||||
impl workspace::Item for ProjectDiagnostics {
|
||||
type View = ProjectDiagnosticsEditor;
|
||||
|
||||
fn build_view(
|
||||
handle: ModelHandle<Self>,
|
||||
workspace: &Workspace,
|
||||
nav_history: ItemNavHistory,
|
||||
cx: &mut ViewContext<Self::View>,
|
||||
) -> Self::View {
|
||||
let diagnostics = ProjectDiagnosticsEditor::new(handle, workspace.weak_handle(), cx);
|
||||
diagnostics
|
||||
.editor
|
||||
.update(cx, |editor, _| editor.set_nav_history(Some(nav_history)));
|
||||
diagnostics
|
||||
}
|
||||
|
||||
fn project_path(&self) -> Option<project::ProjectPath> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl workspace::ItemView for ProjectDiagnosticsEditor {
|
||||
fn item(&self, _: &AppContext) -> Box<dyn ItemHandle> {
|
||||
Box::new(self.model.clone())
|
||||
}
|
||||
|
||||
impl workspace::Item for ProjectDiagnosticsEditor {
|
||||
fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox {
|
||||
render_summary(
|
||||
&self.summary,
|
||||
&style.label.text,
|
||||
&cx.app_state::<Settings>().theme.project_diagnostics,
|
||||
&cx.global::<Settings>().theme.project_diagnostics,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -486,9 +449,13 @@ impl workspace::ItemView for ProjectDiagnosticsEditor {
|
|||
None
|
||||
}
|
||||
|
||||
fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) {
|
||||
fn project_entry_id(&self, _: &AppContext) -> Option<project::ProjectEntryId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| editor.navigate(data, cx));
|
||||
.update(cx, |editor, cx| editor.navigate(data, cx))
|
||||
}
|
||||
|
||||
fn is_dirty(&self, cx: &AppContext) -> bool {
|
||||
|
@ -532,20 +499,21 @@ impl workspace::ItemView for ProjectDiagnosticsEditor {
|
|||
matches!(event, Event::Saved | Event::Dirtied | Event::TitleChanged)
|
||||
}
|
||||
|
||||
fn clone_on_split(
|
||||
&self,
|
||||
nav_history: ItemNavHistory,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<Self>
|
||||
fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {
|
||||
self.editor.update(cx, |editor, _| {
|
||||
editor.set_nav_history(Some(nav_history));
|
||||
});
|
||||
}
|
||||
|
||||
fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let diagnostics =
|
||||
ProjectDiagnosticsEditor::new(self.model.clone(), self.workspace.clone(), cx);
|
||||
diagnostics.editor.update(cx, |editor, _| {
|
||||
editor.set_nav_history(Some(nav_history));
|
||||
});
|
||||
Some(diagnostics)
|
||||
Some(ProjectDiagnosticsEditor::new(
|
||||
self.project.clone(),
|
||||
self.workspace.clone(),
|
||||
cx,
|
||||
))
|
||||
}
|
||||
|
||||
fn act_as_type(
|
||||
|
@ -571,7 +539,7 @@ impl workspace::ItemView for ProjectDiagnosticsEditor {
|
|||
fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
|
||||
let (message, highlights) = highlight_diagnostic_message(&diagnostic.message);
|
||||
Arc::new(move |cx| {
|
||||
let settings = cx.app_state::<Settings>();
|
||||
let settings = cx.global::<Settings>();
|
||||
let theme = &settings.theme.editor;
|
||||
let style = &theme.diagnostic_header;
|
||||
let font_size = (style.text_scale_factor * settings.buffer_font_size).round();
|
||||
|
@ -829,9 +797,8 @@ mod tests {
|
|||
});
|
||||
|
||||
// Open the project diagnostics view while there are already diagnostics.
|
||||
let model = cx.add_model(|_| ProjectDiagnostics::new(project.clone()));
|
||||
let view = cx.add_view(0, |cx| {
|
||||
ProjectDiagnosticsEditor::new(model, workspace.downgrade(), cx)
|
||||
ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
|
||||
});
|
||||
|
||||
view.next_notification(&cx).await;
|
||||
|
|
|
@ -49,7 +49,7 @@ impl View for DiagnosticSummary {
|
|||
|
||||
let in_progress = self.in_progress;
|
||||
MouseEventHandler::new::<Tag, _, _>(0, cx, |_, cx| {
|
||||
let theme = &cx.app_state::<Settings>().theme.project_diagnostics;
|
||||
let theme = &cx.global::<Settings>().theme.project_diagnostics;
|
||||
if in_progress {
|
||||
Label::new(
|
||||
"Checking... ".to_string(),
|
||||
|
@ -71,7 +71,7 @@ impl View for DiagnosticSummary {
|
|||
impl StatusItemView for DiagnosticSummary {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
_: Option<&dyn workspace::ItemViewHandle>,
|
||||
_: Option<&dyn workspace::ItemHandle>,
|
||||
_: &mut ViewContext<Self>,
|
||||
) {
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ gpui = { path = "../gpui" }
|
|||
language = { path = "../language" }
|
||||
lsp = { path = "../lsp" }
|
||||
project = { path = "../project" }
|
||||
rpc = { path = "../rpc" }
|
||||
snippet = { path = "../snippet" }
|
||||
sum_tree = { path = "../sum_tree" }
|
||||
theme = { path = "../theme" }
|
||||
|
@ -34,6 +35,7 @@ util = { path = "../util" }
|
|||
workspace = { path = "../workspace" }
|
||||
aho-corasick = "0.7"
|
||||
anyhow = "1.0"
|
||||
futures = "0.3"
|
||||
itertools = "0.10"
|
||||
lazy_static = "1.4"
|
||||
log = "0.4"
|
||||
|
|
|
@ -12,7 +12,7 @@ use gpui::{
|
|||
Entity, ModelContext, ModelHandle,
|
||||
};
|
||||
use language::{Point, Subscription as BufferSubscription};
|
||||
use std::{any::TypeId, ops::Range, sync::Arc};
|
||||
use std::{any::TypeId, fmt::Debug, ops::Range, sync::Arc};
|
||||
use sum_tree::{Bias, TreeMap};
|
||||
use tab_map::TabMap;
|
||||
use wrap_map::WrapMap;
|
||||
|
@ -36,6 +36,7 @@ pub struct DisplayMap {
|
|||
wrap_map: ModelHandle<WrapMap>,
|
||||
block_map: BlockMap,
|
||||
text_highlights: TextHighlights,
|
||||
pub clip_at_line_ends: bool,
|
||||
}
|
||||
|
||||
impl Entity for DisplayMap {
|
||||
|
@ -67,6 +68,7 @@ impl DisplayMap {
|
|||
wrap_map,
|
||||
block_map,
|
||||
text_highlights: Default::default(),
|
||||
clip_at_line_ends: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -87,6 +89,7 @@ impl DisplayMap {
|
|||
wraps_snapshot,
|
||||
blocks_snapshot,
|
||||
text_highlights: self.text_highlights.clone(),
|
||||
clip_at_line_ends: self.clip_at_line_ends,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -114,6 +117,7 @@ impl DisplayMap {
|
|||
pub fn unfold<T: ToOffset>(
|
||||
&mut self,
|
||||
ranges: impl IntoIterator<Item = Range<T>>,
|
||||
inclusive: bool,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
|
@ -124,7 +128,7 @@ impl DisplayMap {
|
|||
.wrap_map
|
||||
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
|
||||
self.block_map.read(snapshot, edits);
|
||||
let (snapshot, edits) = fold_map.unfold(ranges);
|
||||
let (snapshot, edits) = fold_map.unfold(ranges, inclusive);
|
||||
let (snapshot, edits) = self.tab_map.sync(snapshot, edits);
|
||||
let (snapshot, edits) = self
|
||||
.wrap_map
|
||||
|
@ -204,6 +208,7 @@ pub struct DisplaySnapshot {
|
|||
wraps_snapshot: wrap_map::WrapSnapshot,
|
||||
blocks_snapshot: block_map::BlockSnapshot,
|
||||
text_highlights: TextHighlights,
|
||||
clip_at_line_ends: bool,
|
||||
}
|
||||
|
||||
impl DisplaySnapshot {
|
||||
|
@ -331,7 +336,12 @@ impl DisplaySnapshot {
|
|||
}
|
||||
|
||||
pub fn clip_point(&self, point: DisplayPoint, bias: Bias) -> DisplayPoint {
|
||||
DisplayPoint(self.blocks_snapshot.clip_point(point.0, bias))
|
||||
let mut clipped = self.blocks_snapshot.clip_point(point.0, bias);
|
||||
if self.clip_at_line_ends && clipped.column == self.line_len(clipped.row) {
|
||||
clipped.column = clipped.column.saturating_sub(1);
|
||||
clipped = self.blocks_snapshot.clip_point(clipped, Bias::Left);
|
||||
}
|
||||
DisplayPoint(clipped)
|
||||
}
|
||||
|
||||
pub fn folds_in_range<'a, T>(
|
||||
|
@ -414,9 +424,19 @@ impl DisplaySnapshot {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
|
||||
#[derive(Copy, Clone, Default, Eq, Ord, PartialOrd, PartialEq)]
|
||||
pub struct DisplayPoint(BlockPoint);
|
||||
|
||||
impl Debug for DisplayPoint {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_fmt(format_args!(
|
||||
"DisplayPoint({}, {})",
|
||||
self.row(),
|
||||
self.column()
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl DisplayPoint {
|
||||
pub fn new(row: u32, column: u32) -> Self {
|
||||
Self(BlockPoint(Point::new(row, column)))
|
||||
|
@ -426,7 +446,6 @@ impl DisplayPoint {
|
|||
Self::new(0, 0)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn is_zero(&self) -> bool {
|
||||
self.0.is_zero()
|
||||
}
|
||||
|
@ -478,16 +497,16 @@ impl ToDisplayPoint for Anchor {
|
|||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
use crate::movement;
|
||||
use crate::{movement, test::marked_display_snapshot};
|
||||
use gpui::{color::Color, elements::*, test::observe, MutableAppContext};
|
||||
use language::{Buffer, Language, LanguageConfig, RandomCharIter, SelectionGoal};
|
||||
use rand::{prelude::*, Rng};
|
||||
use smol::stream::StreamExt;
|
||||
use std::{env, sync::Arc};
|
||||
use theme::SyntaxTheme;
|
||||
use util::test::sample_text;
|
||||
use util::test::{marked_text_ranges, sample_text};
|
||||
use Bias::*;
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
|
@ -620,7 +639,7 @@ mod tests {
|
|||
if rng.gen() && fold_count > 0 {
|
||||
log::info!("unfolding ranges: {:?}", ranges);
|
||||
map.update(cx, |map, cx| {
|
||||
map.unfold(ranges, cx);
|
||||
map.unfold(ranges, true, cx);
|
||||
});
|
||||
} else {
|
||||
log::info!("folding ranges: {:?}", ranges);
|
||||
|
@ -705,7 +724,7 @@ mod tests {
|
|||
|
||||
log::info!("Moving from point {:?}", point);
|
||||
|
||||
let moved_right = movement::right(&snapshot, point).unwrap();
|
||||
let moved_right = movement::right(&snapshot, point);
|
||||
log::info!("Right {:?}", moved_right);
|
||||
if point < max_point {
|
||||
assert!(moved_right > point);
|
||||
|
@ -719,7 +738,7 @@ mod tests {
|
|||
assert_eq!(moved_right, point);
|
||||
}
|
||||
|
||||
let moved_left = movement::left(&snapshot, point).unwrap();
|
||||
let moved_left = movement::left(&snapshot, point);
|
||||
log::info!("Left {:?}", moved_left);
|
||||
if point > min_point {
|
||||
assert!(moved_left < point);
|
||||
|
@ -777,15 +796,15 @@ mod tests {
|
|||
DisplayPoint::new(1, 0)
|
||||
);
|
||||
assert_eq!(
|
||||
movement::right(&snapshot, DisplayPoint::new(0, 7)).unwrap(),
|
||||
movement::right(&snapshot, DisplayPoint::new(0, 7)),
|
||||
DisplayPoint::new(1, 0)
|
||||
);
|
||||
assert_eq!(
|
||||
movement::left(&snapshot, DisplayPoint::new(1, 0)).unwrap(),
|
||||
movement::left(&snapshot, DisplayPoint::new(1, 0)),
|
||||
DisplayPoint::new(0, 7)
|
||||
);
|
||||
assert_eq!(
|
||||
movement::up(&snapshot, DisplayPoint::new(1, 10), SelectionGoal::None).unwrap(),
|
||||
movement::up(&snapshot, DisplayPoint::new(1, 10), SelectionGoal::None),
|
||||
(DisplayPoint::new(0, 7), SelectionGoal::Column(10))
|
||||
);
|
||||
assert_eq!(
|
||||
|
@ -793,8 +812,7 @@ mod tests {
|
|||
&snapshot,
|
||||
DisplayPoint::new(0, 7),
|
||||
SelectionGoal::Column(10)
|
||||
)
|
||||
.unwrap(),
|
||||
),
|
||||
(DisplayPoint::new(1, 10), SelectionGoal::Column(10))
|
||||
);
|
||||
assert_eq!(
|
||||
|
@ -802,8 +820,7 @@ mod tests {
|
|||
&snapshot,
|
||||
DisplayPoint::new(1, 10),
|
||||
SelectionGoal::Column(10)
|
||||
)
|
||||
.unwrap(),
|
||||
),
|
||||
(DisplayPoint::new(2, 4), SelectionGoal::Column(10))
|
||||
);
|
||||
|
||||
|
@ -922,7 +939,7 @@ mod tests {
|
|||
let map = cx
|
||||
.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx));
|
||||
assert_eq!(
|
||||
cx.update(|cx| chunks(0..5, &map, &theme, cx)),
|
||||
cx.update(|cx| syntax_chunks(0..5, &map, &theme, cx)),
|
||||
vec![
|
||||
("fn ".to_string(), None),
|
||||
("outer".to_string(), Some(Color::blue())),
|
||||
|
@ -933,7 +950,7 @@ mod tests {
|
|||
]
|
||||
);
|
||||
assert_eq!(
|
||||
cx.update(|cx| chunks(3..5, &map, &theme, cx)),
|
||||
cx.update(|cx| syntax_chunks(3..5, &map, &theme, cx)),
|
||||
vec![
|
||||
(" fn ".to_string(), Some(Color::red())),
|
||||
("inner".to_string(), Some(Color::blue())),
|
||||
|
@ -945,7 +962,7 @@ mod tests {
|
|||
map.fold(vec![Point::new(0, 6)..Point::new(3, 2)], cx)
|
||||
});
|
||||
assert_eq!(
|
||||
cx.update(|cx| chunks(0..2, &map, &theme, cx)),
|
||||
cx.update(|cx| syntax_chunks(0..2, &map, &theme, cx)),
|
||||
vec![
|
||||
("fn ".to_string(), None),
|
||||
("out".to_string(), Some(Color::blue())),
|
||||
|
@ -1011,7 +1028,7 @@ mod tests {
|
|||
DisplayMap::new(buffer, tab_size, font_id, font_size, Some(40.0), 1, 1, cx)
|
||||
});
|
||||
assert_eq!(
|
||||
cx.update(|cx| chunks(0..5, &map, &theme, cx)),
|
||||
cx.update(|cx| syntax_chunks(0..5, &map, &theme, cx)),
|
||||
[
|
||||
("fn \n".to_string(), None),
|
||||
("oute\nr".to_string(), Some(Color::blue())),
|
||||
|
@ -1019,7 +1036,7 @@ mod tests {
|
|||
]
|
||||
);
|
||||
assert_eq!(
|
||||
cx.update(|cx| chunks(3..5, &map, &theme, cx)),
|
||||
cx.update(|cx| syntax_chunks(3..5, &map, &theme, cx)),
|
||||
[("{}\n\n".to_string(), None)]
|
||||
);
|
||||
|
||||
|
@ -1027,7 +1044,7 @@ mod tests {
|
|||
map.fold(vec![Point::new(0, 6)..Point::new(3, 2)], cx)
|
||||
});
|
||||
assert_eq!(
|
||||
cx.update(|cx| chunks(1..4, &map, &theme, cx)),
|
||||
cx.update(|cx| syntax_chunks(1..4, &map, &theme, cx)),
|
||||
[
|
||||
("out".to_string(), Some(Color::blue())),
|
||||
("…\n".to_string(), None),
|
||||
|
@ -1038,50 +1055,151 @@ mod tests {
|
|||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_clip_point(cx: &mut gpui::MutableAppContext) {
|
||||
use Bias::{Left, Right};
|
||||
async fn test_chunks_with_text_highlights(cx: &mut gpui::TestAppContext) {
|
||||
cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
|
||||
|
||||
let text = "\n'a', 'α',\t'✋',\t'❎', '🍐'\n";
|
||||
let display_text = "\n'a', 'α', '✋', '❎', '🍐'\n";
|
||||
let buffer = MultiBuffer::build_simple(text, cx);
|
||||
let theme = SyntaxTheme::new(vec![
|
||||
("operator".to_string(), Color::red().into()),
|
||||
("string".to_string(), Color::green().into()),
|
||||
]);
|
||||
let language = Arc::new(
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
name: "Test".into(),
|
||||
path_suffixes: vec![".test".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
)
|
||||
.with_highlights_query(
|
||||
r#"
|
||||
":" @operator
|
||||
(string_literal) @string
|
||||
"#,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
language.set_theme(&theme);
|
||||
|
||||
let (text, highlighted_ranges) = marked_text_ranges(r#"const[] [a]: B = "c [d]""#);
|
||||
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
|
||||
buffer.condition(&cx, |buf, _| !buf.is_parsing()).await;
|
||||
|
||||
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
|
||||
|
||||
let tab_size = 4;
|
||||
let font_cache = cx.font_cache();
|
||||
let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
|
||||
let tab_size = 4;
|
||||
let family_id = font_cache.load_family(&["Courier"]).unwrap();
|
||||
let font_id = font_cache
|
||||
.select_font(family_id, &Default::default())
|
||||
.unwrap();
|
||||
let font_size = 14.0;
|
||||
let map = cx.add_model(|cx| {
|
||||
DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, 1, 1, cx)
|
||||
});
|
||||
let map = map.update(cx, |map, cx| map.snapshot(cx));
|
||||
let font_size = 16.0;
|
||||
let map = cx
|
||||
.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx));
|
||||
|
||||
enum MyType {}
|
||||
|
||||
let style = HighlightStyle {
|
||||
color: Some(Color::blue()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
map.update(cx, |map, _cx| {
|
||||
map.highlight_text(
|
||||
TypeId::of::<MyType>(),
|
||||
highlighted_ranges
|
||||
.into_iter()
|
||||
.map(|range| {
|
||||
buffer_snapshot.anchor_before(range.start)
|
||||
..buffer_snapshot.anchor_before(range.end)
|
||||
})
|
||||
.collect(),
|
||||
style,
|
||||
);
|
||||
});
|
||||
|
||||
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!(
|
||||
map.clip_point(DisplayPoint::new(1, input_column as u32), bias),
|
||||
DisplayPoint::new(1, output_column as u32),
|
||||
"clip_point(({}, {}))",
|
||||
1,
|
||||
input_column,
|
||||
cx.update(|cx| chunks(0..10, &map, &theme, cx)),
|
||||
[
|
||||
("const ".to_string(), None, None),
|
||||
("a".to_string(), None, Some(Color::blue())),
|
||||
(":".to_string(), Some(Color::red()), None),
|
||||
(" B = ".to_string(), None, None),
|
||||
("\"c ".to_string(), Some(Color::green()), None),
|
||||
("d".to_string(), Some(Color::green()), Some(Color::blue())),
|
||||
("\"".to_string(), Some(Color::green()), None),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_clip_point(cx: &mut gpui::MutableAppContext) {
|
||||
fn assert(text: &str, shift_right: bool, bias: Bias, cx: &mut gpui::MutableAppContext) {
|
||||
let (unmarked_snapshot, mut markers) = marked_display_snapshot(text, cx);
|
||||
|
||||
match bias {
|
||||
Bias::Left => {
|
||||
if shift_right {
|
||||
*markers[1].column_mut() += 1;
|
||||
}
|
||||
|
||||
assert_eq!(unmarked_snapshot.clip_point(markers[1], bias), markers[0])
|
||||
}
|
||||
Bias::Right => {
|
||||
if shift_right {
|
||||
*markers[0].column_mut() += 1;
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
unmarked_snapshot.clip_point(dbg!(markers[0]), bias),
|
||||
markers[1]
|
||||
)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
use Bias::{Left, Right};
|
||||
assert("||α", false, Left, cx);
|
||||
assert("||α", true, Left, cx);
|
||||
assert("||α", false, Right, cx);
|
||||
assert("|α|", true, Right, cx);
|
||||
assert("||✋", false, Left, cx);
|
||||
assert("||✋", true, Left, cx);
|
||||
assert("||✋", false, Right, cx);
|
||||
assert("|✋|", true, Right, cx);
|
||||
assert("||🍐", false, Left, cx);
|
||||
assert("||🍐", true, Left, cx);
|
||||
assert("||🍐", false, Right, cx);
|
||||
assert("|🍐|", true, Right, cx);
|
||||
assert("||\t", false, Left, cx);
|
||||
assert("||\t", true, Left, cx);
|
||||
assert("||\t", false, Right, cx);
|
||||
assert("|\t|", true, Right, cx);
|
||||
assert(" ||\t", false, Left, cx);
|
||||
assert(" ||\t", true, Left, cx);
|
||||
assert(" ||\t", false, Right, cx);
|
||||
assert(" |\t|", true, Right, cx);
|
||||
assert(" ||\t", false, Left, cx);
|
||||
assert(" ||\t", false, Right, cx);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_clip_at_line_ends(cx: &mut gpui::MutableAppContext) {
|
||||
fn assert(text: &str, cx: &mut gpui::MutableAppContext) {
|
||||
let (mut unmarked_snapshot, markers) = marked_display_snapshot(text, cx);
|
||||
unmarked_snapshot.clip_at_line_ends = true;
|
||||
assert_eq!(
|
||||
unmarked_snapshot.clip_point(markers[1], Bias::Left),
|
||||
markers[0]
|
||||
);
|
||||
}
|
||||
|
||||
assert("||", cx);
|
||||
assert("|a|", cx);
|
||||
assert("a|b|", cx);
|
||||
assert("a|α|", cx);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
|
@ -1163,27 +1281,38 @@ mod tests {
|
|||
)
|
||||
}
|
||||
|
||||
fn chunks<'a>(
|
||||
fn syntax_chunks<'a>(
|
||||
rows: Range<u32>,
|
||||
map: &ModelHandle<DisplayMap>,
|
||||
theme: &'a SyntaxTheme,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Vec<(String, Option<Color>)> {
|
||||
chunks(rows, map, theme, cx)
|
||||
.into_iter()
|
||||
.map(|(text, color, _)| (text, color))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn chunks<'a>(
|
||||
rows: Range<u32>,
|
||||
map: &ModelHandle<DisplayMap>,
|
||||
theme: &'a SyntaxTheme,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Vec<(String, Option<Color>, Option<Color>)> {
|
||||
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
|
||||
let mut chunks: Vec<(String, Option<Color>)> = Vec::new();
|
||||
let mut chunks: Vec<(String, Option<Color>, Option<Color>)> = Vec::new();
|
||||
for chunk in snapshot.chunks(rows, true) {
|
||||
let color = chunk
|
||||
let syntax_color = chunk
|
||||
.syntax_highlight_id
|
||||
.and_then(|id| id.style(theme)?.color);
|
||||
if let Some((last_chunk, last_color)) = chunks.last_mut() {
|
||||
if color == *last_color {
|
||||
let highlight_color = chunk.highlight_style.and_then(|style| style.color);
|
||||
if let Some((last_chunk, last_syntax_color, last_highlight_color)) = chunks.last_mut() {
|
||||
if syntax_color == *last_syntax_color && highlight_color == *last_highlight_color {
|
||||
last_chunk.push_str(chunk.text);
|
||||
} else {
|
||||
chunks.push((chunk.text.to_string(), color));
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
chunks.push((chunk.text.to_string(), color));
|
||||
}
|
||||
chunks.push((chunk.text.to_string(), syntax_color, highlight_color));
|
||||
}
|
||||
chunks
|
||||
}
|
||||
|
|
|
@ -499,7 +499,7 @@ impl<'a> BlockMapWriter<'a> {
|
|||
let block_ix = match self
|
||||
.0
|
||||
.blocks
|
||||
.binary_search_by(|probe| probe.position.cmp(&position, &buffer).unwrap())
|
||||
.binary_search_by(|probe| probe.position.cmp(&position, &buffer))
|
||||
{
|
||||
Ok(ix) | Err(ix) => ix,
|
||||
};
|
||||
|
|
|
@ -140,13 +140,14 @@ impl<'a> FoldMapWriter<'a> {
|
|||
pub fn unfold<T: ToOffset>(
|
||||
&mut self,
|
||||
ranges: impl IntoIterator<Item = Range<T>>,
|
||||
inclusive: bool,
|
||||
) -> (FoldSnapshot, Vec<FoldEdit>) {
|
||||
let mut edits = Vec::new();
|
||||
let mut fold_ixs_to_delete = Vec::new();
|
||||
let buffer = self.0.buffer.lock().clone();
|
||||
for range in ranges.into_iter() {
|
||||
// Remove intersecting folds and add their ranges to edits that are passed to sync.
|
||||
let mut folds_cursor = intersecting_folds(&buffer, &self.0.folds, range, true);
|
||||
let mut folds_cursor = intersecting_folds(&buffer, &self.0.folds, range, inclusive);
|
||||
while let Some(fold) = folds_cursor.item() {
|
||||
let offset_range = fold.0.start.to_offset(&buffer)..fold.0.end.to_offset(&buffer);
|
||||
if offset_range.end > offset_range.start {
|
||||
|
@ -256,7 +257,7 @@ impl FoldMap {
|
|||
let mut folds = self.folds.iter().peekable();
|
||||
while let Some(fold) = folds.next() {
|
||||
if let Some(next_fold) = folds.peek() {
|
||||
let comparison = fold.0.cmp(&next_fold.0, &self.buffer.lock()).unwrap();
|
||||
let comparison = fold.0.cmp(&next_fold.0, &self.buffer.lock());
|
||||
assert!(comparison.is_le());
|
||||
}
|
||||
}
|
||||
|
@ -699,10 +700,7 @@ impl FoldSnapshot {
|
|||
let ranges = &highlights.1;
|
||||
|
||||
let start_ix = match ranges.binary_search_by(|probe| {
|
||||
let cmp = probe
|
||||
.end
|
||||
.cmp(&transform_start, &self.buffer_snapshot())
|
||||
.unwrap();
|
||||
let cmp = probe.end.cmp(&transform_start, &self.buffer_snapshot());
|
||||
if cmp.is_gt() {
|
||||
Ordering::Greater
|
||||
} else {
|
||||
|
@ -715,7 +713,6 @@ impl FoldSnapshot {
|
|||
if range
|
||||
.start
|
||||
.cmp(&transform_end, &self.buffer_snapshot)
|
||||
.unwrap()
|
||||
.is_ge()
|
||||
{
|
||||
break;
|
||||
|
@ -820,8 +817,8 @@ where
|
|||
let start = buffer.anchor_before(range.start.to_offset(buffer));
|
||||
let end = buffer.anchor_after(range.end.to_offset(buffer));
|
||||
let mut cursor = folds.filter::<_, usize>(move |summary| {
|
||||
let start_cmp = start.cmp(&summary.max_end, buffer).unwrap();
|
||||
let end_cmp = end.cmp(&summary.min_start, buffer).unwrap();
|
||||
let start_cmp = start.cmp(&summary.max_end, buffer);
|
||||
let end_cmp = end.cmp(&summary.min_start, buffer);
|
||||
|
||||
if inclusive {
|
||||
start_cmp <= Ordering::Equal && end_cmp >= Ordering::Equal
|
||||
|
@ -962,19 +959,19 @@ impl sum_tree::Summary for FoldSummary {
|
|||
type Context = MultiBufferSnapshot;
|
||||
|
||||
fn add_summary(&mut self, other: &Self, buffer: &MultiBufferSnapshot) {
|
||||
if other.min_start.cmp(&self.min_start, buffer).unwrap() == Ordering::Less {
|
||||
if other.min_start.cmp(&self.min_start, buffer) == Ordering::Less {
|
||||
self.min_start = other.min_start.clone();
|
||||
}
|
||||
if other.max_end.cmp(&self.max_end, buffer).unwrap() == Ordering::Greater {
|
||||
if other.max_end.cmp(&self.max_end, buffer) == Ordering::Greater {
|
||||
self.max_end = other.max_end.clone();
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
let start_comparison = self.start.cmp(&other.start, buffer).unwrap();
|
||||
let start_comparison = self.start.cmp(&other.start, buffer);
|
||||
assert!(start_comparison <= Ordering::Equal);
|
||||
if start_comparison == Ordering::Equal {
|
||||
assert!(self.end.cmp(&other.end, buffer).unwrap() >= Ordering::Equal);
|
||||
assert!(self.end.cmp(&other.end, buffer) >= Ordering::Equal);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -993,7 +990,7 @@ impl<'a> sum_tree::Dimension<'a, FoldSummary> for Fold {
|
|||
|
||||
impl<'a> sum_tree::SeekTarget<'a, FoldSummary, Fold> for Fold {
|
||||
fn cmp(&self, other: &Self, buffer: &MultiBufferSnapshot) -> Ordering {
|
||||
self.0.cmp(&other.0, buffer).unwrap()
|
||||
self.0.cmp(&other.0, buffer)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1156,7 +1153,7 @@ impl Ord for HighlightEndpoint {
|
|||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.offset
|
||||
.cmp(&other.offset)
|
||||
.then_with(|| self.is_start.cmp(&other.is_start))
|
||||
.then_with(|| other.is_start.cmp(&self.is_start))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1282,9 +1279,14 @@ mod tests {
|
|||
assert_eq!(snapshot4.text(), "123a…c123456eee");
|
||||
|
||||
let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]);
|
||||
writer.unfold(Some(Point::new(0, 4)..Point::new(0, 5)));
|
||||
writer.unfold(Some(Point::new(0, 4)..Point::new(0, 4)), false);
|
||||
let (snapshot5, _) = map.read(buffer_snapshot.clone(), vec![]);
|
||||
assert_eq!(snapshot5.text(), "123aaaaa\nbbbbbb\nccc123456eee");
|
||||
assert_eq!(snapshot5.text(), "123a…c123456eee");
|
||||
|
||||
let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]);
|
||||
writer.unfold(Some(Point::new(0, 4)..Point::new(0, 4)), true);
|
||||
let (snapshot6, _) = map.read(buffer_snapshot.clone(), vec![]);
|
||||
assert_eq!(snapshot6.text(), "123aaaaa\nbbbbbb\nccc123456eee");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
|
@ -1600,9 +1602,8 @@ mod tests {
|
|||
.filter(|fold| {
|
||||
let start = buffer_snapshot.anchor_before(start);
|
||||
let end = buffer_snapshot.anchor_after(end);
|
||||
start.cmp(&fold.0.end, &buffer_snapshot).unwrap() == Ordering::Less
|
||||
&& end.cmp(&fold.0.start, &buffer_snapshot).unwrap()
|
||||
== Ordering::Greater
|
||||
start.cmp(&fold.0.end, &buffer_snapshot) == Ordering::Less
|
||||
&& end.cmp(&fold.0.start, &buffer_snapshot) == Ordering::Greater
|
||||
})
|
||||
.map(|fold| fold.0)
|
||||
.collect::<Vec<_>>();
|
||||
|
@ -1680,7 +1681,7 @@ mod tests {
|
|||
let buffer = self.buffer.lock().clone();
|
||||
let mut folds = self.folds.items(&buffer);
|
||||
// Ensure sorting doesn't change how folds get merged and displayed.
|
||||
folds.sort_by(|a, b| a.0.cmp(&b.0, &buffer).unwrap());
|
||||
folds.sort_by(|a, b| a.0.cmp(&b.0, &buffer));
|
||||
let mut fold_ranges = folds
|
||||
.iter()
|
||||
.map(|fold| fold.0.start.to_offset(&buffer)..fold.0.end.to_offset(&buffer))
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -21,7 +21,7 @@ use gpui::{
|
|||
MutableAppContext, PaintContext, Quad, Scene, SizeConstraint, ViewContext, WeakViewHandle,
|
||||
};
|
||||
use json::json;
|
||||
use language::Bias;
|
||||
use language::{Bias, DiagnosticSeverity};
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
cmp::{self, Ordering},
|
||||
|
@ -665,13 +665,15 @@ impl EditorElement {
|
|||
}
|
||||
}
|
||||
|
||||
let mut diagnostic_highlight = HighlightStyle {
|
||||
..Default::default()
|
||||
};
|
||||
let mut diagnostic_highlight = HighlightStyle::default();
|
||||
|
||||
if chunk.is_unnecessary {
|
||||
diagnostic_highlight.fade_out = Some(style.unnecessary_code_fade);
|
||||
} else if let Some(severity) = chunk.diagnostic_severity {
|
||||
}
|
||||
|
||||
if let Some(severity) = chunk.diagnostic_severity {
|
||||
// Omit underlines for HINT/INFO diagnostics on 'unnecessary' code.
|
||||
if severity <= DiagnosticSeverity::WARNING || !chunk.is_unnecessary {
|
||||
let diagnostic_style = super::diagnostic_style(severity, true, style);
|
||||
diagnostic_highlight.underline = Some(Underline {
|
||||
color: Some(diagnostic_style.message.text.color),
|
||||
|
@ -679,6 +681,7 @@ impl EditorElement {
|
|||
squiggly: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(highlight_style) = highlight_style.as_mut() {
|
||||
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))
|
||||
};
|
||||
|
||||
let mut selections = HashMap::default();
|
||||
let mut selections = Vec::new();
|
||||
let mut active_rows = BTreeMap::new();
|
||||
let mut highlighted_rows = None;
|
||||
let mut highlighted_ranges = Vec::new();
|
||||
|
@ -919,11 +922,32 @@ impl Element for EditorElement {
|
|||
&display_map,
|
||||
);
|
||||
|
||||
let mut remote_selections = HashMap::default();
|
||||
for (replica_id, selection) in display_map
|
||||
.buffer_snapshot
|
||||
.remote_selections_in_range(&(start_anchor.clone()..end_anchor.clone()))
|
||||
{
|
||||
// The local selections match the leader's selections.
|
||||
if Some(replica_id) == view.leader_replica_id {
|
||||
continue;
|
||||
}
|
||||
|
||||
remote_selections
|
||||
.entry(replica_id)
|
||||
.or_insert(Vec::new())
|
||||
.push(crate::Selection {
|
||||
id: selection.id,
|
||||
goal: selection.goal,
|
||||
reversed: selection.reversed,
|
||||
start: selection.start.to_display_point(&display_map),
|
||||
end: selection.end.to_display_point(&display_map),
|
||||
});
|
||||
}
|
||||
selections.extend(remote_selections);
|
||||
|
||||
if view.show_local_selections {
|
||||
let local_selections = view.local_selections_in_range(
|
||||
start_anchor.clone()..end_anchor.clone(),
|
||||
&display_map,
|
||||
);
|
||||
let local_selections =
|
||||
view.local_selections_in_range(start_anchor..end_anchor, &display_map);
|
||||
for selection in &local_selections {
|
||||
let is_empty = selection.start == selection.end;
|
||||
let selection_start = snapshot.prev_line_boundary(selection.start).1;
|
||||
|
@ -936,8 +960,12 @@ impl Element for EditorElement {
|
|||
*contains_non_empty_selection |= !is_empty;
|
||||
}
|
||||
}
|
||||
selections.insert(
|
||||
view.replica_id(cx),
|
||||
|
||||
// Render the local selections in the leader's color when following.
|
||||
let local_replica_id = view.leader_replica_id.unwrap_or(view.replica_id(cx));
|
||||
|
||||
selections.push((
|
||||
local_replica_id,
|
||||
local_selections
|
||||
.into_iter()
|
||||
.map(|selection| crate::Selection {
|
||||
|
@ -948,23 +976,7 @@ impl Element for EditorElement {
|
|||
end: selection.end.to_display_point(&display_map),
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
|
||||
for (replica_id, selection) in display_map
|
||||
.buffer_snapshot
|
||||
.remote_selections_in_range(&(start_anchor..end_anchor))
|
||||
{
|
||||
selections
|
||||
.entry(replica_id)
|
||||
.or_insert(Vec::new())
|
||||
.push(crate::Selection {
|
||||
id: selection.id,
|
||||
goal: selection.goal,
|
||||
reversed: selection.reversed,
|
||||
start: selection.start.to_display_point(&display_map),
|
||||
end: selection.end.to_display_point(&display_map),
|
||||
});
|
||||
));
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -1210,7 +1222,7 @@ pub struct LayoutState {
|
|||
em_width: f32,
|
||||
em_advance: f32,
|
||||
highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
|
||||
selections: HashMap<ReplicaId, Vec<text::Selection<DisplayPoint>>>,
|
||||
selections: Vec<(ReplicaId, Vec<text::Selection<DisplayPoint>>)>,
|
||||
context_menu: Option<(DisplayPoint, ElementBox)>,
|
||||
code_actions_indicator: Option<(u32, ElementBox)>,
|
||||
}
|
||||
|
@ -1280,7 +1292,7 @@ impl PaintState {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub enum CursorShape {
|
||||
Bar,
|
||||
Block,
|
||||
|
@ -1487,7 +1499,7 @@ mod tests {
|
|||
|
||||
#[gpui::test]
|
||||
fn test_layout_line_numbers(cx: &mut gpui::MutableAppContext) {
|
||||
cx.add_app_state(Settings::test(cx));
|
||||
cx.set_global(Settings::test(cx));
|
||||
let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx);
|
||||
let (window_id, editor) = cx.add_window(Default::default(), |cx| {
|
||||
Editor::new(EditorMode::Full, buffer, None, None, cx)
|
||||
|
@ -1509,7 +1521,7 @@ mod tests {
|
|||
|
||||
#[gpui::test]
|
||||
fn test_layout_with_placeholder_text_and_blocks(cx: &mut gpui::MutableAppContext) {
|
||||
cx.add_app_state(Settings::test(cx));
|
||||
cx.set_global(Settings::test(cx));
|
||||
let buffer = MultiBuffer::build_simple("", cx);
|
||||
let (window_id, editor) = cx.add_window(Default::default(), |cx| {
|
||||
Editor::new(EditorMode::Full, buffer, None, None, cx)
|
||||
|
|
|
@ -1,162 +1,251 @@
|
|||
use crate::{Autoscroll, Editor, Event, MultiBuffer, NavigationData, ToOffset, ToPoint as _};
|
||||
use anyhow::Result;
|
||||
use crate::{Anchor, Autoscroll, Editor, Event, ExcerptId, NavigationData, ToOffset, ToPoint as _};
|
||||
use anyhow::{anyhow, Result};
|
||||
use futures::FutureExt;
|
||||
use gpui::{
|
||||
elements::*, AppContext, Entity, ModelContext, ModelHandle, MutableAppContext, RenderContext,
|
||||
Subscription, Task, View, ViewContext, ViewHandle, WeakModelHandle,
|
||||
elements::*, geometry::vector::vec2f, AppContext, Entity, ModelHandle, MutableAppContext,
|
||||
RenderContext, Subscription, Task, View, ViewContext, ViewHandle,
|
||||
};
|
||||
use language::{Bias, Buffer, Diagnostic, File as _};
|
||||
use project::{File, Project, ProjectPath};
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
use std::{cell::RefCell, fmt::Write};
|
||||
use language::{Bias, Buffer, Diagnostic, File as _, SelectionGoal};
|
||||
use project::{File, Project, ProjectEntryId, ProjectPath};
|
||||
use rpc::proto::{self, update_view};
|
||||
use std::{fmt::Write, path::PathBuf, time::Duration};
|
||||
use text::{Point, Selection};
|
||||
use util::ResultExt;
|
||||
use util::TryFutureExt;
|
||||
use workspace::{
|
||||
ItemHandle, ItemNavHistory, ItemView, ItemViewHandle, NavHistory, PathOpener, Settings,
|
||||
StatusItemView, WeakItemHandle, Workspace,
|
||||
FollowableItem, Item, ItemHandle, ItemNavHistory, ProjectItem, Settings, StatusItemView,
|
||||
};
|
||||
|
||||
pub struct BufferOpener;
|
||||
pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct BufferItemHandle(pub ModelHandle<Buffer>);
|
||||
|
||||
#[derive(Clone)]
|
||||
struct WeakBufferItemHandle(WeakModelHandle<Buffer>);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MultiBufferItemHandle(pub ModelHandle<MultiBuffer>);
|
||||
|
||||
#[derive(Clone)]
|
||||
struct WeakMultiBufferItemHandle(WeakModelHandle<MultiBuffer>);
|
||||
|
||||
impl 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>>,
|
||||
impl FollowableItem for Editor {
|
||||
fn from_state_proto(
|
||||
pane: ViewHandle<workspace::Pane>,
|
||||
project: ModelHandle<Project>,
|
||||
state: &mut Option<proto::view::Variant>,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Box<dyn ItemViewHandle> {
|
||||
let buffer = cx.add_model(|cx| MultiBuffer::singleton(self.0.clone(), cx));
|
||||
Box::new(cx.add_view(window_id, |cx| {
|
||||
let mut editor = Editor::for_buffer(buffer, Some(workspace.project().clone()), cx);
|
||||
editor.nav_history = Some(ItemNavHistory::new(nav_history, &cx.handle()));
|
||||
editor
|
||||
) -> Option<Task<Result<ViewHandle<Self>>>> {
|
||||
let state = if matches!(state, Some(proto::view::Variant::Editor(_))) {
|
||||
if let Some(proto::view::Variant::Editor(state)) = state.take() {
|
||||
state
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let buffer = project.update(cx, |project, cx| {
|
||||
project.open_buffer_by_id(state.buffer_id, cx)
|
||||
});
|
||||
Some(cx.spawn(|mut cx| async move {
|
||||
let buffer = buffer.await?;
|
||||
let editor = pane
|
||||
.read_with(&cx, |pane, cx| {
|
||||
pane.items_of_type::<Self>().find(|editor| {
|
||||
editor.read(cx).buffer.read(cx).as_singleton().as_ref() == Some(&buffer)
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
cx.add_view(pane.window_id(), |cx| {
|
||||
Editor::for_buffer(buffer, Some(project), cx)
|
||||
})
|
||||
});
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
let excerpt_id;
|
||||
let buffer_id;
|
||||
{
|
||||
let buffer = editor.buffer.read(cx).read(cx);
|
||||
let singleton = buffer.as_singleton().unwrap();
|
||||
excerpt_id = singleton.0.clone();
|
||||
buffer_id = singleton.1;
|
||||
}
|
||||
let selections = state
|
||||
.selections
|
||||
.into_iter()
|
||||
.map(|selection| {
|
||||
deserialize_selection(&excerpt_id, buffer_id, selection)
|
||||
.ok_or_else(|| anyhow!("invalid selection"))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
if !selections.is_empty() {
|
||||
editor.set_selections_from_remote(selections.into(), cx);
|
||||
}
|
||||
|
||||
if let Some(anchor) = state.scroll_top_anchor {
|
||||
editor.set_scroll_top_anchor(
|
||||
Anchor {
|
||||
buffer_id: Some(state.buffer_id as usize),
|
||||
excerpt_id: excerpt_id.clone(),
|
||||
text_anchor: language::proto::deserialize_anchor(anchor)
|
||||
.ok_or_else(|| anyhow!("invalid scroll top"))?,
|
||||
},
|
||||
vec2f(state.scroll_x, state.scroll_y),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
Ok::<_, anyhow::Error>(())
|
||||
})?;
|
||||
Ok(editor)
|
||||
}))
|
||||
}
|
||||
|
||||
fn boxed_clone(&self) -> Box<dyn ItemHandle> {
|
||||
Box::new(self.clone())
|
||||
fn 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_any(&self) -> gpui::AnyModelHandle {
|
||||
self.0.clone().into()
|
||||
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 downgrade(&self) -> Box<dyn workspace::WeakItemHandle> {
|
||||
Box::new(WeakBufferItemHandle(self.0.downgrade()))
|
||||
fn add_event_to_update_proto(
|
||||
&self,
|
||||
event: &Self::Event,
|
||||
update: &mut Option<proto::update_view::Variant>,
|
||||
_: &AppContext,
|
||||
) -> bool {
|
||||
let update =
|
||||
update.get_or_insert_with(|| proto::update_view::Variant::Editor(Default::default()));
|
||||
|
||||
match update {
|
||||
proto::update_view::Variant::Editor(update) => match event {
|
||||
Event::ScrollPositionChanged { .. } => {
|
||||
update.scroll_top_anchor = Some(language::proto::serialize_anchor(
|
||||
&self.scroll_top_anchor.text_anchor,
|
||||
));
|
||||
update.scroll_x = self.scroll_position.x();
|
||||
update.scroll_y = self.scroll_position.y();
|
||||
true
|
||||
}
|
||||
Event::SelectionsChanged { .. } => {
|
||||
update.selections = self
|
||||
.selections
|
||||
.iter()
|
||||
.chain(self.pending_selection.as_ref().map(|p| &p.selection))
|
||||
.map(serialize_selection)
|
||||
.collect();
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn project_path(&self, 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 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,
|
||||
})
|
||||
}
|
||||
|
||||
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 {
|
||||
Box::new(MultiBufferItemHandle(self.buffer.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) {
|
||||
impl Item for Editor {
|
||||
fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
|
||||
if let Some(data) = data.downcast_ref::<NavigationData>() {
|
||||
let buffer = self.buffer.read(cx).read(cx);
|
||||
let offset = if buffer.can_resolve(&data.anchor) {
|
||||
|
@ -164,11 +253,19 @@ impl ItemView for Editor {
|
|||
} else {
|
||||
buffer.clip_offset(data.offset, Bias::Left)
|
||||
};
|
||||
|
||||
let newest_selection = self.newest_selection_with_snapshot::<usize>(&buffer);
|
||||
drop(buffer);
|
||||
|
||||
if newest_selection.head() == offset {
|
||||
false
|
||||
} else {
|
||||
let nav_history = self.nav_history.take();
|
||||
self.select_ranges([offset..offset], Some(Autoscroll::Fit), cx);
|
||||
self.nav_history = nav_history;
|
||||
true
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -184,15 +281,19 @@ impl ItemView for Editor {
|
|||
})
|
||||
}
|
||||
|
||||
fn clone_on_split(
|
||||
&self,
|
||||
nav_history: ItemNavHistory,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<Self>
|
||||
fn project_entry_id(&self, cx: &AppContext) -> Option<ProjectEntryId> {
|
||||
File::from_dyn(self.buffer().read(cx).file(cx)).and_then(|file| file.project_entry_id(cx))
|
||||
}
|
||||
|
||||
fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Some(self.clone(nav_history, cx))
|
||||
Some(self.clone(cx))
|
||||
}
|
||||
|
||||
fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext<Self>) {
|
||||
self.nav_history = Some(history);
|
||||
}
|
||||
|
||||
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
|
||||
|
@ -219,9 +320,17 @@ impl ItemView for Editor {
|
|||
) -> Task<Result<()>> {
|
||||
let buffer = self.buffer().clone();
|
||||
let buffers = buffer.read(cx).all_buffers();
|
||||
let transaction = project.update(cx, |project, cx| project.format(buffers, true, cx));
|
||||
let mut timeout = cx.background().timer(FORMAT_TIMEOUT).fuse();
|
||||
let format = project.update(cx, |project, cx| project.format(buffers, true, cx));
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let transaction = transaction.await.log_err();
|
||||
let transaction = futures::select_biased! {
|
||||
_ = timeout => {
|
||||
log::warn!("timed out waiting for formatting");
|
||||
None
|
||||
}
|
||||
transaction = format.log_err().fuse() => transaction,
|
||||
};
|
||||
|
||||
this.update(&mut cx, |editor, cx| {
|
||||
editor.request_autoscroll(Autoscroll::Fit, cx)
|
||||
});
|
||||
|
@ -275,6 +384,18 @@ impl ItemView for Editor {
|
|||
}
|
||||
}
|
||||
|
||||
impl ProjectItem for Editor {
|
||||
type Item = Buffer;
|
||||
|
||||
fn for_project_item(
|
||||
project: ModelHandle<Project>,
|
||||
buffer: ModelHandle<Buffer>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
Self::for_buffer(buffer, Some(project), cx)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CursorPosition {
|
||||
position: Option<Point>,
|
||||
selected_count: usize,
|
||||
|
@ -322,7 +443,7 @@ impl View for CursorPosition {
|
|||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
if let Some(position) = self.position {
|
||||
let theme = &cx.app_state::<Settings>().theme.workspace.status_bar;
|
||||
let theme = &cx.global::<Settings>().theme.workspace.status_bar;
|
||||
let mut text = format!("{},{}", position.row + 1, position.column + 1);
|
||||
if self.selected_count > 0 {
|
||||
write!(text, " ({} selected)", self.selected_count).unwrap();
|
||||
|
@ -337,7 +458,7 @@ impl View for CursorPosition {
|
|||
impl StatusItemView for CursorPosition {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
active_pane_item: Option<&dyn ItemViewHandle>,
|
||||
active_pane_item: Option<&dyn ItemHandle>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
|
||||
|
@ -395,7 +516,7 @@ impl View for DiagnosticMessage {
|
|||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
if let Some(diagnostic) = &self.diagnostic {
|
||||
let theme = &cx.app_state::<Settings>().theme.workspace.status_bar;
|
||||
let theme = &cx.global::<Settings>().theme.workspace.status_bar;
|
||||
Label::new(
|
||||
diagnostic.message.split('\n').next().unwrap().to_string(),
|
||||
theme.diagnostic_message.clone(),
|
||||
|
@ -410,7 +531,7 @@ impl View for DiagnosticMessage {
|
|||
impl StatusItemView for DiagnosticMessage {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
active_pane_item: Option<&dyn ItemViewHandle>,
|
||||
active_pane_item: Option<&dyn ItemHandle>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
|
||||
|
|
|
@ -1,20 +1,19 @@
|
|||
use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
|
||||
use crate::{char_kind, CharKind, ToPoint};
|
||||
use anyhow::Result;
|
||||
use language::Point;
|
||||
use std::ops::Range;
|
||||
|
||||
pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> Result<DisplayPoint> {
|
||||
pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
|
||||
if point.column() > 0 {
|
||||
*point.column_mut() -= 1;
|
||||
} else if point.row() > 0 {
|
||||
*point.row_mut() -= 1;
|
||||
*point.column_mut() = map.line_len(point.row());
|
||||
}
|
||||
Ok(map.clip_point(point, Bias::Left))
|
||||
map.clip_point(point, Bias::Left)
|
||||
}
|
||||
|
||||
pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> Result<DisplayPoint> {
|
||||
pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
|
||||
let max_column = map.line_len(point.row());
|
||||
if point.column() < max_column {
|
||||
*point.column_mut() += 1;
|
||||
|
@ -22,14 +21,14 @@ pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> Result<DisplayPo
|
|||
*point.row_mut() += 1;
|
||||
*point.column_mut() = 0;
|
||||
}
|
||||
Ok(map.clip_point(point, Bias::Right))
|
||||
map.clip_point(point, Bias::Right)
|
||||
}
|
||||
|
||||
pub fn up(
|
||||
map: &DisplaySnapshot,
|
||||
start: DisplayPoint,
|
||||
goal: SelectionGoal,
|
||||
) -> Result<(DisplayPoint, SelectionGoal)> {
|
||||
) -> (DisplayPoint, SelectionGoal) {
|
||||
let mut goal_column = if let SelectionGoal::Column(column) = goal {
|
||||
column
|
||||
} else {
|
||||
|
@ -54,17 +53,17 @@ pub fn up(
|
|||
Bias::Right
|
||||
};
|
||||
|
||||
Ok((
|
||||
(
|
||||
map.clip_point(point, clip_bias),
|
||||
SelectionGoal::Column(goal_column),
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
pub fn down(
|
||||
map: &DisplaySnapshot,
|
||||
start: DisplayPoint,
|
||||
goal: SelectionGoal,
|
||||
) -> Result<(DisplayPoint, SelectionGoal)> {
|
||||
) -> (DisplayPoint, SelectionGoal) {
|
||||
let mut goal_column = if let SelectionGoal::Column(column) = goal {
|
||||
column
|
||||
} else {
|
||||
|
@ -86,10 +85,10 @@ pub fn down(
|
|||
Bias::Right
|
||||
};
|
||||
|
||||
Ok((
|
||||
(
|
||||
map.clip_point(point, clip_bias),
|
||||
SelectionGoal::Column(goal_column),
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
pub fn line_beginning(
|
||||
|
@ -132,68 +131,110 @@ pub fn line_end(
|
|||
}
|
||||
}
|
||||
|
||||
pub fn prev_word_boundary(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
|
||||
let mut line_start = 0;
|
||||
pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
|
||||
find_preceding_boundary(map, point, |left, right| {
|
||||
(char_kind(left) != char_kind(right) && !right.is_whitespace()) || left == '\n'
|
||||
})
|
||||
}
|
||||
|
||||
pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
|
||||
find_preceding_boundary(map, point, |left, right| {
|
||||
let is_word_start = char_kind(left) != char_kind(right) && !right.is_whitespace();
|
||||
let is_subword_start =
|
||||
left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
|
||||
is_word_start || is_subword_start || left == '\n'
|
||||
})
|
||||
}
|
||||
|
||||
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) {
|
||||
line_start = indent;
|
||||
*point.column_mut() = indent;
|
||||
}
|
||||
}
|
||||
|
||||
if point.column() == line_start {
|
||||
if point.row() == 0 {
|
||||
return DisplayPoint::new(0, 0);
|
||||
let mut boundary = None;
|
||||
let mut prev_ch = if point.is_zero() { None } else { Some('\n') };
|
||||
for ch in map.chars_at(point) {
|
||||
if point >= end {
|
||||
break;
|
||||
}
|
||||
|
||||
if let Some(prev_ch) = prev_ch {
|
||||
if is_boundary(prev_ch, ch) {
|
||||
boundary = Some(point);
|
||||
}
|
||||
}
|
||||
|
||||
if ch == '\n' {
|
||||
break;
|
||||
}
|
||||
|
||||
prev_ch = Some(ch);
|
||||
*point.column_mut() += ch.len_utf8() as u32;
|
||||
}
|
||||
|
||||
if let Some(boundary) = boundary {
|
||||
return boundary;
|
||||
} else if point.row() == 0 {
|
||||
return DisplayPoint::zero();
|
||||
} else {
|
||||
let row = point.row() - 1;
|
||||
point = map.clip_point(DisplayPoint::new(row, map.line_len(row)), Bias::Left);
|
||||
*point.row_mut() -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
let mut prev_char_kind = None;
|
||||
for c in map.chars_at(point) {
|
||||
let char_kind = char_kind(c);
|
||||
if let Some(prev_char_kind) = prev_char_kind {
|
||||
if c == '\n' {
|
||||
break;
|
||||
}
|
||||
if prev_char_kind != char_kind
|
||||
&& prev_char_kind != CharKind::Whitespace
|
||||
&& prev_char_kind != CharKind::Newline
|
||||
{
|
||||
/// Scans for a boundary following the given start point until a boundary is found, indicated by the
|
||||
/// given predicate returning true. The predicate is called with the character to the left and right
|
||||
/// of the candidate boundary location, and will be called with `\n` characters indicating the start
|
||||
/// or end of a line.
|
||||
pub fn find_boundary(
|
||||
map: &DisplaySnapshot,
|
||||
mut point: DisplayPoint,
|
||||
mut is_boundary: impl FnMut(char, char) -> bool,
|
||||
) -> DisplayPoint {
|
||||
let mut prev_ch = None;
|
||||
for ch in map.chars_at(point) {
|
||||
if let Some(prev_ch) = prev_ch {
|
||||
if is_boundary(prev_ch, ch) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if c == '\n' {
|
||||
if ch == '\n' {
|
||||
*point.row_mut() += 1;
|
||||
*point.column_mut() = 0;
|
||||
} else {
|
||||
*point.column_mut() += c.len_utf8() as u32;
|
||||
*point.column_mut() += ch.len_utf8() as u32;
|
||||
}
|
||||
prev_char_kind = Some(char_kind);
|
||||
prev_ch = Some(ch);
|
||||
}
|
||||
map.clip_point(point, Bias::Right)
|
||||
}
|
||||
|
@ -225,9 +266,205 @@ pub fn surrounding_word(map: &DisplaySnapshot, position: DisplayPoint) -> Range<
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{Buffer, DisplayMap, MultiBuffer};
|
||||
use crate::{test::marked_display_snapshot, Buffer, DisplayMap, MultiBuffer};
|
||||
use language::Point;
|
||||
|
||||
#[gpui::test]
|
||||
fn test_previous_word_start(cx: &mut gpui::MutableAppContext) {
|
||||
fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
|
||||
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
|
||||
assert_eq!(
|
||||
previous_word_start(&snapshot, display_points[1]),
|
||||
display_points[0]
|
||||
);
|
||||
}
|
||||
|
||||
assert("\n| |lorem", cx);
|
||||
assert("|\n| lorem", cx);
|
||||
assert(" |lorem|", cx);
|
||||
assert("| |lorem", cx);
|
||||
assert(" |lor|em", cx);
|
||||
assert("\nlorem\n| |ipsum", cx);
|
||||
assert("\n\n|\n|", cx);
|
||||
assert(" |lorem |ipsum", cx);
|
||||
assert("lorem|-|ipsum", cx);
|
||||
assert("lorem|-#$@|ipsum", cx);
|
||||
assert("|lorem_|ipsum", cx);
|
||||
assert(" |defγ|", cx);
|
||||
assert(" |bcΔ|", cx);
|
||||
assert(" ab|——|cd", cx);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_previous_subword_start(cx: &mut gpui::MutableAppContext) {
|
||||
fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
|
||||
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
|
||||
assert_eq!(
|
||||
previous_subword_start(&snapshot, display_points[1]),
|
||||
display_points[0]
|
||||
);
|
||||
}
|
||||
|
||||
// Subword boundaries are respected
|
||||
assert("lorem_|ip|sum", cx);
|
||||
assert("lorem_|ipsum|", cx);
|
||||
assert("|lorem_|ipsum", cx);
|
||||
assert("lorem_|ipsum_|dolor", cx);
|
||||
assert("lorem|Ip|sum", cx);
|
||||
assert("lorem|Ipsum|", cx);
|
||||
|
||||
// Word boundaries are still respected
|
||||
assert("\n| |lorem", cx);
|
||||
assert(" |lorem|", cx);
|
||||
assert(" |lor|em", cx);
|
||||
assert("\nlorem\n| |ipsum", cx);
|
||||
assert("\n\n|\n|", cx);
|
||||
assert(" |lorem |ipsum", cx);
|
||||
assert("lorem|-|ipsum", cx);
|
||||
assert("lorem|-#$@|ipsum", cx);
|
||||
assert(" |defγ|", cx);
|
||||
assert(" bc|Δ|", cx);
|
||||
assert(" |bcδ|", cx);
|
||||
assert(" ab|——|cd", cx);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_find_preceding_boundary(cx: &mut gpui::MutableAppContext) {
|
||||
fn assert(
|
||||
marked_text: &str,
|
||||
cx: &mut gpui::MutableAppContext,
|
||||
is_boundary: impl FnMut(char, char) -> bool,
|
||||
) {
|
||||
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
|
||||
assert_eq!(
|
||||
find_preceding_boundary(&snapshot, display_points[1], is_boundary),
|
||||
display_points[0]
|
||||
);
|
||||
}
|
||||
|
||||
assert("abc|def\ngh\nij|k", cx, |left, right| {
|
||||
left == 'c' && right == 'd'
|
||||
});
|
||||
assert("abcdef\n|gh\nij|k", cx, |left, right| {
|
||||
left == '\n' && right == 'g'
|
||||
});
|
||||
let mut line_count = 0;
|
||||
assert("abcdef\n|gh\nij|k", cx, |left, _| {
|
||||
if left == '\n' {
|
||||
line_count += 1;
|
||||
line_count == 2
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_next_word_end(cx: &mut gpui::MutableAppContext) {
|
||||
fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
|
||||
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
|
||||
assert_eq!(
|
||||
next_word_end(&snapshot, display_points[0]),
|
||||
display_points[1]
|
||||
);
|
||||
}
|
||||
|
||||
assert("\n| lorem|", cx);
|
||||
assert(" |lorem|", cx);
|
||||
assert(" lor|em|", cx);
|
||||
assert(" lorem| |\nipsum\n", cx);
|
||||
assert("\n|\n|\n\n", cx);
|
||||
assert("lorem| ipsum| ", cx);
|
||||
assert("lorem|-|ipsum", cx);
|
||||
assert("lorem|#$@-|ipsum", cx);
|
||||
assert("lorem|_ipsum|", cx);
|
||||
assert(" |bcΔ|", cx);
|
||||
assert(" ab|——|cd", cx);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_next_subword_end(cx: &mut gpui::MutableAppContext) {
|
||||
fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
|
||||
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
|
||||
assert_eq!(
|
||||
next_subword_end(&snapshot, display_points[0]),
|
||||
display_points[1]
|
||||
);
|
||||
}
|
||||
|
||||
// Subword boundaries are respected
|
||||
assert("lo|rem|_ipsum", cx);
|
||||
assert("|lorem|_ipsum", cx);
|
||||
assert("lorem|_ipsum|", cx);
|
||||
assert("lorem|_ipsum|_dolor", cx);
|
||||
assert("lo|rem|Ipsum", cx);
|
||||
assert("lorem|Ipsum|Dolor", cx);
|
||||
|
||||
// Word boundaries are still respected
|
||||
assert("\n| lorem|", cx);
|
||||
assert(" |lorem|", cx);
|
||||
assert(" lor|em|", cx);
|
||||
assert(" lorem| |\nipsum\n", cx);
|
||||
assert("\n|\n|\n\n", cx);
|
||||
assert("lorem| ipsum| ", cx);
|
||||
assert("lorem|-|ipsum", cx);
|
||||
assert("lorem|#$@-|ipsum", cx);
|
||||
assert("lorem|_ipsum|", cx);
|
||||
assert(" |bc|Δ", cx);
|
||||
assert(" ab|——|cd", cx);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_find_boundary(cx: &mut gpui::MutableAppContext) {
|
||||
fn assert(
|
||||
marked_text: &str,
|
||||
cx: &mut gpui::MutableAppContext,
|
||||
is_boundary: impl FnMut(char, char) -> bool,
|
||||
) {
|
||||
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
|
||||
assert_eq!(
|
||||
find_boundary(&snapshot, display_points[0], is_boundary),
|
||||
display_points[1]
|
||||
);
|
||||
}
|
||||
|
||||
assert("abc|def\ngh\nij|k", cx, |left, right| {
|
||||
left == 'j' && right == 'k'
|
||||
});
|
||||
assert("ab|cdef\ngh\n|ijk", cx, |left, right| {
|
||||
left == '\n' && right == 'i'
|
||||
});
|
||||
let mut line_count = 0;
|
||||
assert("abc|def\ngh\n|ijk", cx, |left, _| {
|
||||
if left == '\n' {
|
||||
line_count += 1;
|
||||
line_count == 2
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_surrounding_word(cx: &mut gpui::MutableAppContext) {
|
||||
fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
|
||||
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
|
||||
assert_eq!(
|
||||
surrounding_word(&snapshot, display_points[1]),
|
||||
display_points[0]..display_points[2]
|
||||
);
|
||||
}
|
||||
|
||||
assert("||lorem| ipsum", cx);
|
||||
assert("|lo|rem| ipsum", cx);
|
||||
assert("|lorem|| ipsum", cx);
|
||||
assert("lorem| | |ipsum", cx);
|
||||
assert("lorem\n|||\nipsum", cx);
|
||||
assert("lorem\n||ipsum|", cx);
|
||||
assert("lorem,|| |ipsum", cx);
|
||||
assert("|lorem||, ipsum", cx);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_move_up_and_down_with_excerpts(cx: &mut gpui::MutableAppContext) {
|
||||
let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
|
||||
|
@ -249,180 +486,50 @@ mod tests {
|
|||
);
|
||||
multibuffer
|
||||
});
|
||||
|
||||
let display_map =
|
||||
cx.add_model(|cx| DisplayMap::new(multibuffer, 2, font_id, 14.0, None, 2, 2, cx));
|
||||
|
||||
let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
|
||||
assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn");
|
||||
|
||||
// Can't move up into the first excerpt's header
|
||||
assert_eq!(
|
||||
up(&snapshot, DisplayPoint::new(2, 2), SelectionGoal::Column(2)).unwrap(),
|
||||
up(&snapshot, DisplayPoint::new(2, 2), SelectionGoal::Column(2)),
|
||||
(DisplayPoint::new(2, 0), SelectionGoal::Column(0)),
|
||||
);
|
||||
assert_eq!(
|
||||
up(&snapshot, DisplayPoint::new(2, 0), SelectionGoal::None).unwrap(),
|
||||
up(&snapshot, DisplayPoint::new(2, 0), SelectionGoal::None),
|
||||
(DisplayPoint::new(2, 0), SelectionGoal::Column(0)),
|
||||
);
|
||||
|
||||
// Move up and down within first excerpt
|
||||
assert_eq!(
|
||||
up(&snapshot, DisplayPoint::new(3, 4), SelectionGoal::Column(4)).unwrap(),
|
||||
up(&snapshot, DisplayPoint::new(3, 4), SelectionGoal::Column(4)),
|
||||
(DisplayPoint::new(2, 3), SelectionGoal::Column(4)),
|
||||
);
|
||||
assert_eq!(
|
||||
down(&snapshot, DisplayPoint::new(2, 3), SelectionGoal::Column(4)).unwrap(),
|
||||
down(&snapshot, DisplayPoint::new(2, 3), SelectionGoal::Column(4)),
|
||||
(DisplayPoint::new(3, 4), SelectionGoal::Column(4)),
|
||||
);
|
||||
|
||||
// Move up and down across second excerpt's header
|
||||
assert_eq!(
|
||||
up(&snapshot, DisplayPoint::new(6, 5), SelectionGoal::Column(5)).unwrap(),
|
||||
up(&snapshot, DisplayPoint::new(6, 5), SelectionGoal::Column(5)),
|
||||
(DisplayPoint::new(3, 4), SelectionGoal::Column(5)),
|
||||
);
|
||||
assert_eq!(
|
||||
down(&snapshot, DisplayPoint::new(3, 4), SelectionGoal::Column(5)).unwrap(),
|
||||
down(&snapshot, DisplayPoint::new(3, 4), SelectionGoal::Column(5)),
|
||||
(DisplayPoint::new(6, 5), SelectionGoal::Column(5)),
|
||||
);
|
||||
|
||||
// Can't move down off the end
|
||||
assert_eq!(
|
||||
down(&snapshot, DisplayPoint::new(7, 0), SelectionGoal::Column(0)).unwrap(),
|
||||
down(&snapshot, DisplayPoint::new(7, 0), SelectionGoal::Column(0)),
|
||||
(DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
|
||||
);
|
||||
assert_eq!(
|
||||
down(&snapshot, DisplayPoint::new(7, 2), SelectionGoal::Column(2)).unwrap(),
|
||||
down(&snapshot, DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
|
||||
(DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_prev_next_word_boundary_multibyte(cx: &mut gpui::MutableAppContext) {
|
||||
let tab_size = 4;
|
||||
let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
|
||||
let font_id = cx
|
||||
.font_cache()
|
||||
.select_font(family_id, &Default::default())
|
||||
.unwrap();
|
||||
let font_size = 14.0;
|
||||
|
||||
let buffer = MultiBuffer::build_simple("a bcΔ defγ hi—jk", cx);
|
||||
let display_map = cx
|
||||
.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx));
|
||||
let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
assert_eq!(
|
||||
prev_word_boundary(&snapshot, DisplayPoint::new(0, 12)),
|
||||
DisplayPoint::new(0, 7)
|
||||
);
|
||||
assert_eq!(
|
||||
prev_word_boundary(&snapshot, DisplayPoint::new(0, 7)),
|
||||
DisplayPoint::new(0, 2)
|
||||
);
|
||||
assert_eq!(
|
||||
prev_word_boundary(&snapshot, DisplayPoint::new(0, 6)),
|
||||
DisplayPoint::new(0, 2)
|
||||
);
|
||||
assert_eq!(
|
||||
prev_word_boundary(&snapshot, DisplayPoint::new(0, 2)),
|
||||
DisplayPoint::new(0, 0)
|
||||
);
|
||||
assert_eq!(
|
||||
prev_word_boundary(&snapshot, DisplayPoint::new(0, 1)),
|
||||
DisplayPoint::new(0, 0)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
next_word_boundary(&snapshot, DisplayPoint::new(0, 0)),
|
||||
DisplayPoint::new(0, 1)
|
||||
);
|
||||
assert_eq!(
|
||||
next_word_boundary(&snapshot, DisplayPoint::new(0, 1)),
|
||||
DisplayPoint::new(0, 6)
|
||||
);
|
||||
assert_eq!(
|
||||
next_word_boundary(&snapshot, DisplayPoint::new(0, 2)),
|
||||
DisplayPoint::new(0, 6)
|
||||
);
|
||||
assert_eq!(
|
||||
next_word_boundary(&snapshot, DisplayPoint::new(0, 6)),
|
||||
DisplayPoint::new(0, 12)
|
||||
);
|
||||
assert_eq!(
|
||||
next_word_boundary(&snapshot, DisplayPoint::new(0, 7)),
|
||||
DisplayPoint::new(0, 12)
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_surrounding_word(cx: &mut gpui::MutableAppContext) {
|
||||
let tab_size = 4;
|
||||
let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
|
||||
let font_id = cx
|
||||
.font_cache()
|
||||
.select_font(family_id, &Default::default())
|
||||
.unwrap();
|
||||
let font_size = 14.0;
|
||||
let buffer = MultiBuffer::build_simple("lorem ipsum dolor\n sit", cx);
|
||||
let display_map = cx
|
||||
.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx));
|
||||
let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
|
||||
assert_eq!(
|
||||
surrounding_word(&snapshot, DisplayPoint::new(0, 0)),
|
||||
DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5),
|
||||
);
|
||||
assert_eq!(
|
||||
surrounding_word(&snapshot, DisplayPoint::new(0, 2)),
|
||||
DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5),
|
||||
);
|
||||
assert_eq!(
|
||||
surrounding_word(&snapshot, DisplayPoint::new(0, 5)),
|
||||
DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5),
|
||||
);
|
||||
assert_eq!(
|
||||
surrounding_word(&snapshot, DisplayPoint::new(0, 6)),
|
||||
DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11),
|
||||
);
|
||||
assert_eq!(
|
||||
surrounding_word(&snapshot, DisplayPoint::new(0, 7)),
|
||||
DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11),
|
||||
);
|
||||
assert_eq!(
|
||||
surrounding_word(&snapshot, DisplayPoint::new(0, 11)),
|
||||
DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11),
|
||||
);
|
||||
assert_eq!(
|
||||
surrounding_word(&snapshot, DisplayPoint::new(0, 13)),
|
||||
DisplayPoint::new(0, 11)..DisplayPoint::new(0, 14),
|
||||
);
|
||||
assert_eq!(
|
||||
surrounding_word(&snapshot, DisplayPoint::new(0, 14)),
|
||||
DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19),
|
||||
);
|
||||
assert_eq!(
|
||||
surrounding_word(&snapshot, DisplayPoint::new(0, 17)),
|
||||
DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19),
|
||||
);
|
||||
assert_eq!(
|
||||
surrounding_word(&snapshot, DisplayPoint::new(0, 19)),
|
||||
DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19),
|
||||
);
|
||||
assert_eq!(
|
||||
surrounding_word(&snapshot, DisplayPoint::new(1, 0)),
|
||||
DisplayPoint::new(1, 0)..DisplayPoint::new(1, 4),
|
||||
);
|
||||
assert_eq!(
|
||||
surrounding_word(&snapshot, DisplayPoint::new(1, 1)),
|
||||
DisplayPoint::new(1, 0)..DisplayPoint::new(1, 4),
|
||||
);
|
||||
assert_eq!(
|
||||
surrounding_word(&snapshot, DisplayPoint::new(1, 6)),
|
||||
DisplayPoint::new(1, 4)..DisplayPoint::new(1, 7),
|
||||
);
|
||||
assert_eq!(
|
||||
surrounding_word(&snapshot, DisplayPoint::new(1, 7)),
|
||||
DisplayPoint::new(1, 4)..DisplayPoint::new(1, 7),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -211,7 +211,7 @@ impl MultiBuffer {
|
|||
pub fn singleton(buffer: ModelHandle<Buffer>, cx: &mut ModelContext<Self>) -> Self {
|
||||
let mut this = Self::new(buffer.read(cx).replica_id());
|
||||
this.singleton = true;
|
||||
this.push_excerpts(buffer, [text::Anchor::min()..text::Anchor::max()], cx);
|
||||
this.push_excerpts(buffer, [text::Anchor::MIN..text::Anchor::MAX], cx);
|
||||
this.snapshot.borrow_mut().singleton = true;
|
||||
this
|
||||
}
|
||||
|
@ -522,24 +522,14 @@ impl MultiBuffer {
|
|||
self.buffers.borrow()[&buffer_id]
|
||||
.buffer
|
||||
.update(cx, |buffer, cx| {
|
||||
selections.sort_unstable_by(|a, b| a.start.cmp(&b.start, buffer).unwrap());
|
||||
selections.sort_unstable_by(|a, b| a.start.cmp(&b.start, buffer));
|
||||
let mut selections = selections.into_iter().peekable();
|
||||
let merged_selections = Arc::from_iter(iter::from_fn(|| {
|
||||
let mut selection = selections.next()?;
|
||||
while let Some(next_selection) = selections.peek() {
|
||||
if selection
|
||||
.end
|
||||
.cmp(&next_selection.start, buffer)
|
||||
.unwrap()
|
||||
.is_ge()
|
||||
{
|
||||
if selection.end.cmp(&next_selection.start, buffer).is_ge() {
|
||||
let next_selection = selections.next().unwrap();
|
||||
if next_selection
|
||||
.end
|
||||
.cmp(&selection.end, buffer)
|
||||
.unwrap()
|
||||
.is_ge()
|
||||
{
|
||||
if next_selection.end.cmp(&selection.end, buffer).is_ge() {
|
||||
selection.end = next_selection.end;
|
||||
}
|
||||
} else {
|
||||
|
@ -814,11 +804,38 @@ impl MultiBuffer {
|
|||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn excerpt_ids_for_buffer(&self, buffer: &ModelHandle<Buffer>) -> Vec<ExcerptId> {
|
||||
pub fn excerpts_for_buffer(
|
||||
&self,
|
||||
buffer: &ModelHandle<Buffer>,
|
||||
cx: &AppContext,
|
||||
) -> Vec<(ExcerptId, Range<text::Anchor>)> {
|
||||
let mut excerpts = Vec::new();
|
||||
let snapshot = self.read(cx);
|
||||
let buffers = self.buffers.borrow();
|
||||
let mut cursor = snapshot.excerpts.cursor::<Option<&ExcerptId>>();
|
||||
for excerpt_id in buffers
|
||||
.get(&buffer.id())
|
||||
.map(|state| &state.excerpts)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
{
|
||||
cursor.seek_forward(&Some(excerpt_id), Bias::Left, &());
|
||||
if let Some(excerpt) = cursor.item() {
|
||||
if excerpt.id == *excerpt_id {
|
||||
excerpts.push((excerpt.id.clone(), excerpt.range.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
excerpts
|
||||
}
|
||||
|
||||
pub fn excerpt_ids(&self) -> Vec<ExcerptId> {
|
||||
self.buffers
|
||||
.borrow()
|
||||
.get(&buffer.id())
|
||||
.map_or(Vec::new(), |state| state.excerpts.clone())
|
||||
.values()
|
||||
.flat_map(|state| state.excerpts.iter().cloned())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn excerpt_containing(
|
||||
|
@ -1407,7 +1424,7 @@ impl MultiBufferSnapshot {
|
|||
);
|
||||
|
||||
for ch in prev_chars {
|
||||
if Some(char_kind(ch)) == word_kind {
|
||||
if Some(char_kind(ch)) == word_kind && ch != '\n' {
|
||||
start -= ch.len_utf8();
|
||||
} else {
|
||||
break;
|
||||
|
@ -1415,7 +1432,7 @@ impl MultiBufferSnapshot {
|
|||
}
|
||||
|
||||
for ch in next_chars {
|
||||
if Some(char_kind(ch)) == word_kind {
|
||||
if Some(char_kind(ch)) == word_kind && ch != '\n' {
|
||||
end += ch.len_utf8();
|
||||
} else {
|
||||
break;
|
||||
|
@ -1909,11 +1926,7 @@ impl MultiBufferSnapshot {
|
|||
.range
|
||||
.start
|
||||
.bias(anchor.text_anchor.bias, &excerpt.buffer);
|
||||
if text_anchor
|
||||
.cmp(&excerpt.range.end, &excerpt.buffer)
|
||||
.unwrap()
|
||||
.is_gt()
|
||||
{
|
||||
if text_anchor.cmp(&excerpt.range.end, &excerpt.buffer).is_gt() {
|
||||
text_anchor = excerpt.range.end.clone();
|
||||
}
|
||||
Anchor {
|
||||
|
@ -1928,7 +1941,6 @@ impl MultiBufferSnapshot {
|
|||
.bias(anchor.text_anchor.bias, &excerpt.buffer);
|
||||
if text_anchor
|
||||
.cmp(&excerpt.range.start, &excerpt.buffer)
|
||||
.unwrap()
|
||||
.is_lt()
|
||||
{
|
||||
text_anchor = excerpt.range.start.clone();
|
||||
|
@ -1948,7 +1960,7 @@ impl MultiBufferSnapshot {
|
|||
result.push((anchor_ix, anchor, kept_position));
|
||||
}
|
||||
}
|
||||
result.sort_unstable_by(|a, b| a.1.cmp(&b.1, self).unwrap());
|
||||
result.sort_unstable_by(|a, b| a.1.cmp(&b.1, self));
|
||||
result
|
||||
}
|
||||
|
||||
|
@ -2295,10 +2307,10 @@ impl MultiBufferSnapshot {
|
|||
excerpt_id: excerpt.id.clone(),
|
||||
text_anchor: selection.end.clone(),
|
||||
};
|
||||
if range.start.cmp(&start, self).unwrap().is_gt() {
|
||||
if range.start.cmp(&start, self).is_gt() {
|
||||
start = range.start.clone();
|
||||
}
|
||||
if range.end.cmp(&end, self).unwrap().is_lt() {
|
||||
if range.end.cmp(&end, self).is_lt() {
|
||||
end = range.end.clone();
|
||||
}
|
||||
|
||||
|
@ -2522,17 +2534,9 @@ impl Excerpt {
|
|||
}
|
||||
|
||||
fn clip_anchor(&self, text_anchor: text::Anchor) -> text::Anchor {
|
||||
if text_anchor
|
||||
.cmp(&self.range.start, &self.buffer)
|
||||
.unwrap()
|
||||
.is_lt()
|
||||
{
|
||||
if text_anchor.cmp(&self.range.start, &self.buffer).is_lt() {
|
||||
self.range.start.clone()
|
||||
} else if text_anchor
|
||||
.cmp(&self.range.end, &self.buffer)
|
||||
.unwrap()
|
||||
.is_gt()
|
||||
{
|
||||
} else if text_anchor.cmp(&self.range.end, &self.buffer).is_gt() {
|
||||
self.range.end.clone()
|
||||
} else {
|
||||
text_anchor
|
||||
|
@ -2545,13 +2549,11 @@ impl Excerpt {
|
|||
.range
|
||||
.start
|
||||
.cmp(&anchor.text_anchor, &self.buffer)
|
||||
.unwrap()
|
||||
.is_le()
|
||||
&& self
|
||||
.range
|
||||
.end
|
||||
.cmp(&anchor.text_anchor, &self.buffer)
|
||||
.unwrap()
|
||||
.is_ge()
|
||||
}
|
||||
}
|
||||
|
@ -3062,7 +3064,8 @@ mod tests {
|
|||
);
|
||||
|
||||
let snapshot = multibuffer.update(cx, |multibuffer, cx| {
|
||||
let buffer_2_excerpt_id = multibuffer.excerpt_ids_for_buffer(&buffer_2)[0].clone();
|
||||
let (buffer_2_excerpt_id, _) =
|
||||
multibuffer.excerpts_for_buffer(&buffer_2, cx)[0].clone();
|
||||
multibuffer.remove_excerpts(&[buffer_2_excerpt_id], cx);
|
||||
multibuffer.snapshot(cx)
|
||||
});
|
||||
|
@ -3357,7 +3360,7 @@ mod tests {
|
|||
let bias = if rng.gen() { Bias::Left } else { Bias::Right };
|
||||
log::info!("Creating anchor at {} with bias {:?}", offset, bias);
|
||||
anchors.push(multibuffer.anchor_at(offset, bias));
|
||||
anchors.sort_by(|a, b| a.cmp(&b, &multibuffer).unwrap());
|
||||
anchors.sort_by(|a, b| a.cmp(&b, &multibuffer));
|
||||
}
|
||||
40..=44 if !anchors.is_empty() => {
|
||||
let multibuffer = multibuffer.read(cx).read(cx);
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
use super::{ExcerptId, MultiBufferSnapshot, ToOffset, ToPoint};
|
||||
use anyhow::Result;
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
ops::{Range, Sub},
|
||||
|
@ -19,7 +18,7 @@ impl Anchor {
|
|||
Self {
|
||||
buffer_id: None,
|
||||
excerpt_id: ExcerptId::min(),
|
||||
text_anchor: text::Anchor::min(),
|
||||
text_anchor: text::Anchor::MIN,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -27,7 +26,7 @@ impl Anchor {
|
|||
Self {
|
||||
buffer_id: None,
|
||||
excerpt_id: ExcerptId::max(),
|
||||
text_anchor: text::Anchor::max(),
|
||||
text_anchor: text::Anchor::MAX,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -35,18 +34,18 @@ impl Anchor {
|
|||
&self.excerpt_id
|
||||
}
|
||||
|
||||
pub fn cmp<'a>(&self, other: &Anchor, snapshot: &MultiBufferSnapshot) -> Result<Ordering> {
|
||||
pub fn cmp<'a>(&self, other: &Anchor, snapshot: &MultiBufferSnapshot) -> Ordering {
|
||||
let excerpt_id_cmp = self.excerpt_id.cmp(&other.excerpt_id);
|
||||
if excerpt_id_cmp.is_eq() {
|
||||
if self.excerpt_id == ExcerptId::min() || self.excerpt_id == ExcerptId::max() {
|
||||
Ok(Ordering::Equal)
|
||||
Ordering::Equal
|
||||
} else if let Some(excerpt) = snapshot.excerpt(&self.excerpt_id) {
|
||||
self.text_anchor.cmp(&other.text_anchor, &excerpt.buffer)
|
||||
} else {
|
||||
Ok(Ordering::Equal)
|
||||
Ordering::Equal
|
||||
}
|
||||
} else {
|
||||
Ok(excerpt_id_cmp)
|
||||
excerpt_id_cmp
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -97,17 +96,17 @@ impl ToPoint for Anchor {
|
|||
}
|
||||
|
||||
pub trait AnchorRangeExt {
|
||||
fn cmp(&self, b: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> Result<Ordering>;
|
||||
fn cmp(&self, b: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> Ordering;
|
||||
fn to_offset(&self, content: &MultiBufferSnapshot) -> Range<usize>;
|
||||
fn to_point(&self, content: &MultiBufferSnapshot) -> Range<Point>;
|
||||
}
|
||||
|
||||
impl AnchorRangeExt for Range<Anchor> {
|
||||
fn cmp(&self, other: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> Result<Ordering> {
|
||||
Ok(match self.start.cmp(&other.start, buffer)? {
|
||||
Ordering::Equal => other.end.cmp(&self.end, buffer)?,
|
||||
fn cmp(&self, other: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> Ordering {
|
||||
match self.start.cmp(&other.start, buffer) {
|
||||
Ordering::Equal => other.end.cmp(&self.end, buffer),
|
||||
ord @ _ => ord,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn to_offset(&self, content: &MultiBufferSnapshot) -> Range<usize> {
|
||||
|
|
|
@ -1,3 +1,10 @@
|
|||
use util::test::marked_text;
|
||||
|
||||
use crate::{
|
||||
display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
|
||||
DisplayPoint, MultiBuffer,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
#[ctor::ctor]
|
||||
fn init_logger() {
|
||||
|
@ -5,3 +12,30 @@ fn init_logger() {
|
|||
env_logger::init();
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a snapshot from text containing '|' character markers with the markers removed, and DisplayPoints for each one.
|
||||
pub fn marked_display_snapshot(
|
||||
text: &str,
|
||||
cx: &mut gpui::MutableAppContext,
|
||||
) -> (DisplaySnapshot, Vec<DisplayPoint>) {
|
||||
let (unmarked_text, markers) = marked_text(text);
|
||||
|
||||
let tab_size = 4;
|
||||
let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
|
||||
let font_id = cx
|
||||
.font_cache()
|
||||
.select_font(family_id, &Default::default())
|
||||
.unwrap();
|
||||
let font_size = 14.0;
|
||||
|
||||
let buffer = MultiBuffer::build_simple(&unmarked_text, cx);
|
||||
let display_map =
|
||||
cx.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx));
|
||||
let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
let markers = markers
|
||||
.into_iter()
|
||||
.map(|offset| offset.to_display_point(&snapshot))
|
||||
.collect();
|
||||
|
||||
(snapshot, markers)
|
||||
}
|
||||
|
|
|
@ -21,3 +21,5 @@ postage = { version = "0.4.1", features = ["futures-traits"] }
|
|||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
serde_json = { version = "1.0.64", features = ["preserve_order"] }
|
||||
workspace = { path = "../workspace", features = ["test-support"] }
|
||||
ctor = "0.1"
|
||||
env_logger = "0.8"
|
||||
|
|
|
@ -67,7 +67,7 @@ impl View for FileFinder {
|
|||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let settings = cx.app_state::<Settings>();
|
||||
let settings = cx.global::<Settings>();
|
||||
Align::new(
|
||||
ConstrainedBox::new(
|
||||
Container::new(
|
||||
|
@ -106,7 +106,7 @@ impl View for FileFinder {
|
|||
impl FileFinder {
|
||||
fn render_matches(&self, cx: &AppContext) -> ElementBox {
|
||||
if self.matches.is_empty() {
|
||||
let settings = cx.app_state::<Settings>();
|
||||
let settings = cx.global::<Settings>();
|
||||
return Container::new(
|
||||
Label::new(
|
||||
"No matches".into(),
|
||||
|
@ -142,7 +142,7 @@ impl FileFinder {
|
|||
|
||||
fn render_match(&self, path_match: &PathMatch, index: usize, cx: &AppContext) -> ElementBox {
|
||||
let selected_index = self.selected_index();
|
||||
let settings = cx.app_state::<Settings>();
|
||||
let settings = cx.global::<Settings>();
|
||||
let style = if index == selected_index {
|
||||
&settings.theme.selector.active_item
|
||||
} else {
|
||||
|
@ -291,7 +291,7 @@ impl FileFinder {
|
|||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
editor::Event::Edited => {
|
||||
editor::Event::BufferEdited { .. } => {
|
||||
let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx));
|
||||
if query.is_empty() {
|
||||
self.latest_search_id = post_inc(&mut self.search_count);
|
||||
|
@ -407,16 +407,21 @@ mod tests {
|
|||
use std::path::PathBuf;
|
||||
use workspace::{Workspace, WorkspaceParams};
|
||||
|
||||
#[ctor::ctor]
|
||||
fn init_logger() {
|
||||
if std::env::var("RUST_LOG").is_ok() {
|
||||
env_logger::init();
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_matching_paths(cx: &mut gpui::TestAppContext) {
|
||||
let mut path_openers = Vec::new();
|
||||
cx.update(|cx| {
|
||||
super::init(cx);
|
||||
editor::init(cx, &mut path_openers);
|
||||
editor::init(cx);
|
||||
});
|
||||
|
||||
let mut params = cx.update(WorkspaceParams::test);
|
||||
params.path_openers = Arc::from(path_openers);
|
||||
let params = cx.update(WorkspaceParams::test);
|
||||
params
|
||||
.fs
|
||||
.as_fake()
|
||||
|
|
|
@ -59,7 +59,8 @@ impl GoToLine {
|
|||
}
|
||||
|
||||
fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
|
||||
if let Some(editor) = workspace.active_item(cx)
|
||||
if let Some(editor) = workspace
|
||||
.active_item(cx)
|
||||
.and_then(|active_item| active_item.downcast::<Editor>())
|
||||
{
|
||||
workspace.toggle_modal(cx, |cx, _| {
|
||||
|
@ -101,7 +102,7 @@ impl GoToLine {
|
|||
) {
|
||||
match event {
|
||||
editor::Event::Blurred => cx.emit(Event::Dismissed),
|
||||
editor::Event::Edited => {
|
||||
editor::Event::BufferEdited { .. } => {
|
||||
let line_editor = self.line_editor.read(cx).buffer().read(cx).read(cx).text();
|
||||
let mut components = line_editor.trim().split(&[',', ':'][..]);
|
||||
let row = components.next().and_then(|row| row.parse::<u32>().ok());
|
||||
|
@ -148,7 +149,7 @@ impl View for GoToLine {
|
|||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let theme = &cx.app_state::<Settings>().theme.selector;
|
||||
let theme = &cx.global::<Settings>().theme.selector;
|
||||
|
||||
let label = format!(
|
||||
"{},{} of {} lines",
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -337,7 +337,7 @@ impl Deterministic {
|
|||
|
||||
if let Some((_, wakeup_time, _)) = state.pending_timers.first() {
|
||||
let wakeup_time = *wakeup_time;
|
||||
if wakeup_time < new_now {
|
||||
if wakeup_time <= new_now {
|
||||
let timer_count = state
|
||||
.pending_timers
|
||||
.iter()
|
||||
|
|
|
@ -224,15 +224,19 @@ impl Keystroke {
|
|||
key: key.unwrap(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn modified(&self) -> bool {
|
||||
self.ctrl || self.alt || self.shift || self.cmd
|
||||
}
|
||||
}
|
||||
|
||||
impl Context {
|
||||
pub fn extend(&mut self, other: Context) {
|
||||
for v in other.set {
|
||||
self.set.insert(v);
|
||||
pub fn extend(&mut self, other: &Context) {
|
||||
for v in &other.set {
|
||||
self.set.insert(v.clone());
|
||||
}
|
||||
for (k, v) in other.map {
|
||||
self.map.insert(k, v);
|
||||
for (k, v) in &other.map {
|
||||
self.map.insert(k.clone(), v.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -561,9 +561,10 @@ impl Renderer {
|
|||
}
|
||||
|
||||
for icon in icons {
|
||||
let origin = icon.bounds.origin() * scale_factor;
|
||||
let target_size = icon.bounds.size() * scale_factor;
|
||||
let source_size = (target_size * 2.).ceil().to_i32();
|
||||
// Snap sprite to pixel grid.
|
||||
let origin = (icon.bounds.origin() * scale_factor).floor();
|
||||
let target_size = (icon.bounds.size() * scale_factor).ceil();
|
||||
let source_size = (target_size * 2.).to_i32();
|
||||
|
||||
let sprite =
|
||||
self.sprite_cache
|
||||
|
|
|
@ -20,7 +20,7 @@ use std::{
|
|||
|
||||
pub struct Presenter {
|
||||
window_id: usize,
|
||||
rendered_views: HashMap<usize, ElementBox>,
|
||||
pub(crate) rendered_views: HashMap<usize, ElementBox>,
|
||||
parents: HashMap<usize, usize>,
|
||||
font_cache: Arc<FontCache>,
|
||||
text_layout_cache: TextLayoutCache,
|
||||
|
@ -63,41 +63,36 @@ impl Presenter {
|
|||
path
|
||||
}
|
||||
|
||||
pub fn invalidate(&mut self, mut invalidation: WindowInvalidation, cx: &mut MutableAppContext) {
|
||||
pub fn invalidate(
|
||||
&mut self,
|
||||
invalidation: &mut WindowInvalidation,
|
||||
cx: &mut MutableAppContext,
|
||||
) {
|
||||
cx.start_frame();
|
||||
for view_id in invalidation.removed {
|
||||
for view_id in &invalidation.removed {
|
||||
invalidation.updated.remove(&view_id);
|
||||
self.rendered_views.remove(&view_id);
|
||||
self.parents.remove(&view_id);
|
||||
}
|
||||
for view_id in invalidation.updated {
|
||||
for view_id in &invalidation.updated {
|
||||
self.rendered_views.insert(
|
||||
view_id,
|
||||
cx.render_view(self.window_id, view_id, self.titlebar_height, false)
|
||||
*view_id,
|
||||
cx.render_view(self.window_id, *view_id, self.titlebar_height, false)
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn refresh(
|
||||
&mut self,
|
||||
invalidation: Option<WindowInvalidation>,
|
||||
cx: &mut MutableAppContext,
|
||||
) {
|
||||
cx.start_frame();
|
||||
if let Some(invalidation) = invalidation {
|
||||
for view_id in invalidation.removed {
|
||||
self.rendered_views.remove(&view_id);
|
||||
self.parents.remove(&view_id);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn refresh(&mut self, invalidation: &mut WindowInvalidation, cx: &mut MutableAppContext) {
|
||||
self.invalidate(invalidation, cx);
|
||||
for (view_id, view) in &mut self.rendered_views {
|
||||
if !invalidation.updated.contains(view_id) {
|
||||
*view = cx
|
||||
.render_view(self.window_id, *view_id, self.titlebar_height, true)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_scene(
|
||||
&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>> {
|
||||
self.app.upgrade_view_handle(handle)
|
||||
}
|
||||
|
||||
fn upgrade_any_view_handle(&self, handle: &crate::AnyWeakViewHandle) -> Option<AnyViewHandle> {
|
||||
self.app.upgrade_any_view_handle(handle)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ElementStateContext for LayoutContext<'a> {
|
||||
|
|
|
@ -271,7 +271,6 @@ pub(crate) struct DiagnosticEndpoint {
|
|||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug)]
|
||||
pub enum CharKind {
|
||||
Newline,
|
||||
Punctuation,
|
||||
Whitespace,
|
||||
Word,
|
||||
|
@ -1621,8 +1620,13 @@ impl BufferSnapshot {
|
|||
let range = range.start.to_offset(self)..range.end.to_offset(self);
|
||||
let mut cursor = tree.root_node().walk();
|
||||
|
||||
// Descend to smallest leaf that touches or exceeds the start of the range.
|
||||
while cursor.goto_first_child_for_byte(range.start).is_some() {}
|
||||
// Descend to the first leaf that touches the start of the range,
|
||||
// and if the range is non-empty, extends beyond the start.
|
||||
while cursor.goto_first_child_for_byte(range.start).is_some() {
|
||||
if !range.is_empty() && cursor.node().end_byte() == range.start {
|
||||
cursor.goto_next_sibling();
|
||||
}
|
||||
}
|
||||
|
||||
// Ascend to the smallest ancestor that strictly contains the range.
|
||||
loop {
|
||||
|
@ -1656,6 +1660,9 @@ impl BufferSnapshot {
|
|||
}
|
||||
}
|
||||
|
||||
// If there is a candidate node on both sides of the (empty) range, then
|
||||
// decide between the two by favoring a named node over an anonymous token.
|
||||
// If both nodes are the same in that regard, favor the right one.
|
||||
if let Some(right_node) = right_node {
|
||||
if right_node.is_named() || !left_node.is_named() {
|
||||
return Some(right_node.byte_range());
|
||||
|
@ -1822,12 +1829,6 @@ impl BufferSnapshot {
|
|||
.min_by_key(|(open_range, close_range)| close_range.end - open_range.start)
|
||||
}
|
||||
|
||||
/*
|
||||
impl BufferSnapshot
|
||||
pub fn remote_selections_in_range(&self, Range<Anchor>) -> impl Iterator<Item = (ReplicaId, impl Iterator<Item = &Selection<Anchor>>)>
|
||||
pub fn remote_selections_in_range(&self, Range<Anchor>) -> impl Iterator<Item = (ReplicaId, i
|
||||
*/
|
||||
|
||||
pub fn remote_selections_in_range<'a>(
|
||||
&'a self,
|
||||
range: Range<Anchor>,
|
||||
|
@ -1840,20 +1841,12 @@ impl BufferSnapshot {
|
|||
})
|
||||
.map(move |(replica_id, set)| {
|
||||
let start_ix = match set.selections.binary_search_by(|probe| {
|
||||
probe
|
||||
.end
|
||||
.cmp(&range.start, self)
|
||||
.unwrap()
|
||||
.then(Ordering::Greater)
|
||||
probe.end.cmp(&range.start, self).then(Ordering::Greater)
|
||||
}) {
|
||||
Ok(ix) | Err(ix) => ix,
|
||||
};
|
||||
let end_ix = match set.selections.binary_search_by(|probe| {
|
||||
probe
|
||||
.start
|
||||
.cmp(&range.end, self)
|
||||
.unwrap()
|
||||
.then(Ordering::Less)
|
||||
probe.start.cmp(&range.end, self).then(Ordering::Less)
|
||||
}) {
|
||||
Ok(ix) | Err(ix) => ix,
|
||||
};
|
||||
|
@ -2280,9 +2273,7 @@ pub fn contiguous_ranges(
|
|||
}
|
||||
|
||||
pub fn char_kind(c: char) -> CharKind {
|
||||
if c == '\n' {
|
||||
CharKind::Newline
|
||||
} else if c.is_whitespace() {
|
||||
if c.is_whitespace() {
|
||||
CharKind::Whitespace
|
||||
} else if c.is_alphanumeric() || c == '_' {
|
||||
CharKind::Word
|
||||
|
|
|
@ -81,8 +81,8 @@ impl DiagnosticSet {
|
|||
let range = buffer.anchor_before(range.start)..buffer.anchor_at(range.end, end_bias);
|
||||
let mut cursor = self.diagnostics.filter::<_, ()>({
|
||||
move |summary: &Summary| {
|
||||
let start_cmp = range.start.cmp(&summary.max_end, buffer).unwrap();
|
||||
let end_cmp = range.end.cmp(&summary.min_start, buffer).unwrap();
|
||||
let start_cmp = range.start.cmp(&summary.max_end, buffer);
|
||||
let end_cmp = range.end.cmp(&summary.min_start, buffer);
|
||||
if inclusive {
|
||||
start_cmp <= Ordering::Equal && end_cmp >= Ordering::Equal
|
||||
} else {
|
||||
|
@ -123,7 +123,7 @@ impl DiagnosticSet {
|
|||
|
||||
let start_ix = output.len();
|
||||
output.extend(groups.into_values().filter_map(|mut entries| {
|
||||
entries.sort_unstable_by(|a, b| a.range.start.cmp(&b.range.start, buffer).unwrap());
|
||||
entries.sort_unstable_by(|a, b| a.range.start.cmp(&b.range.start, buffer));
|
||||
entries
|
||||
.iter()
|
||||
.position(|entry| entry.diagnostic.is_primary)
|
||||
|
@ -137,7 +137,6 @@ impl DiagnosticSet {
|
|||
.range
|
||||
.start
|
||||
.cmp(&b.entries[b.primary_ix].range.start, buffer)
|
||||
.unwrap()
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -187,10 +186,10 @@ impl DiagnosticEntry<Anchor> {
|
|||
impl Default for Summary {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
start: Anchor::min(),
|
||||
end: Anchor::max(),
|
||||
min_start: Anchor::max(),
|
||||
max_end: Anchor::min(),
|
||||
start: Anchor::MIN,
|
||||
end: Anchor::MAX,
|
||||
min_start: Anchor::MAX,
|
||||
max_end: Anchor::MIN,
|
||||
count: 0,
|
||||
}
|
||||
}
|
||||
|
@ -200,15 +199,10 @@ impl sum_tree::Summary for Summary {
|
|||
type Context = text::BufferSnapshot;
|
||||
|
||||
fn add_summary(&mut self, other: &Self, buffer: &Self::Context) {
|
||||
if other
|
||||
.min_start
|
||||
.cmp(&self.min_start, buffer)
|
||||
.unwrap()
|
||||
.is_lt()
|
||||
{
|
||||
if other.min_start.cmp(&self.min_start, buffer).is_lt() {
|
||||
self.min_start = other.min_start.clone();
|
||||
}
|
||||
if other.max_end.cmp(&self.max_end, buffer).unwrap().is_gt() {
|
||||
if other.max_end.cmp(&self.max_end, buffer).is_gt() {
|
||||
self.max_end = other.max_end.clone();
|
||||
}
|
||||
self.start = other.start.clone();
|
||||
|
|
|
@ -100,15 +100,16 @@ pub fn serialize_undo_map_entry(
|
|||
}
|
||||
|
||||
pub fn serialize_selections(selections: &Arc<[Selection<Anchor>]>) -> Vec<proto::Selection> {
|
||||
selections
|
||||
.iter()
|
||||
.map(|selection| proto::Selection {
|
||||
selections.iter().map(serialize_selection).collect()
|
||||
}
|
||||
|
||||
pub fn serialize_selection(selection: &Selection<Anchor>) -> proto::Selection {
|
||||
proto::Selection {
|
||||
id: selection.id as u64,
|
||||
start: Some(serialize_anchor(&selection.start)),
|
||||
end: Some(serialize_anchor(&selection.end)),
|
||||
reversed: selection.reversed,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialize_diagnostics<'a>(
|
||||
|
@ -274,7 +275,12 @@ pub fn deserialize_selections(selections: Vec<proto::Selection>) -> Arc<[Selecti
|
|||
Arc::from(
|
||||
selections
|
||||
.into_iter()
|
||||
.filter_map(|selection| {
|
||||
.filter_map(deserialize_selection)
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn deserialize_selection(selection: proto::Selection) -> Option<Selection<Anchor>> {
|
||||
Some(Selection {
|
||||
id: selection.id as usize,
|
||||
start: deserialize_anchor(selection.start?)?,
|
||||
|
@ -282,9 +288,6 @@ pub fn deserialize_selections(selections: Vec<proto::Selection>) -> Arc<[Selecti
|
|||
reversed: selection.reversed,
|
||||
goal: SelectionGoal::None,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn deserialize_diagnostics(
|
||||
|
|
|
@ -508,6 +508,44 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
|
|||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_range_for_syntax_ancestor(cx: &mut MutableAppContext) {
|
||||
cx.add_model(|cx| {
|
||||
let text = "fn a() { b(|c| {}) }";
|
||||
let buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx);
|
||||
let snapshot = buffer.snapshot();
|
||||
|
||||
assert_eq!(
|
||||
snapshot.range_for_syntax_ancestor(empty_range_at(text, "|")),
|
||||
Some(range_of(text, "|"))
|
||||
);
|
||||
assert_eq!(
|
||||
snapshot.range_for_syntax_ancestor(range_of(text, "|")),
|
||||
Some(range_of(text, "|c|"))
|
||||
);
|
||||
assert_eq!(
|
||||
snapshot.range_for_syntax_ancestor(range_of(text, "|c|")),
|
||||
Some(range_of(text, "|c| {}"))
|
||||
);
|
||||
assert_eq!(
|
||||
snapshot.range_for_syntax_ancestor(range_of(text, "|c| {}")),
|
||||
Some(range_of(text, "(|c| {})"))
|
||||
);
|
||||
|
||||
buffer
|
||||
});
|
||||
|
||||
fn empty_range_at(text: &str, part: &str) -> Range<usize> {
|
||||
let start = text.find(part).unwrap();
|
||||
start..start
|
||||
}
|
||||
|
||||
fn range_of(text: &str, part: &str) -> Range<usize> {
|
||||
let start = text.find(part).unwrap();
|
||||
start..start + part.len()
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_edit_with_autoindent(cx: &mut MutableAppContext) {
|
||||
cx.add_model(|cx| {
|
||||
|
@ -839,7 +877,7 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) {
|
|||
for buffer in &buffers {
|
||||
let buffer = buffer.read(cx).snapshot();
|
||||
let actual_remote_selections = buffer
|
||||
.remote_selections_in_range(Anchor::min()..Anchor::max())
|
||||
.remote_selections_in_range(Anchor::MIN..Anchor::MAX)
|
||||
.map(|(replica_id, selections)| (replica_id, selections.collect::<Vec<_>>()))
|
||||
.collect::<Vec<_>>();
|
||||
let expected_remote_selections = active_selections
|
||||
|
|
|
@ -556,7 +556,14 @@ type FakeLanguageServerHandlers = Arc<
|
|||
Mutex<
|
||||
HashMap<
|
||||
&'static str,
|
||||
Box<dyn Send + FnMut(usize, &[u8], gpui::AsyncAppContext) -> Vec<u8>>,
|
||||
Box<
|
||||
dyn Send
|
||||
+ FnMut(
|
||||
usize,
|
||||
&[u8],
|
||||
gpui::AsyncAppContext,
|
||||
) -> futures::future::BoxFuture<'static, Vec<u8>>,
|
||||
>,
|
||||
>,
|
||||
>,
|
||||
>;
|
||||
|
@ -585,12 +592,17 @@ impl LanguageServer {
|
|||
let (stdout_writer, stdout_reader) = async_pipe::pipe();
|
||||
|
||||
let mut fake = FakeLanguageServer::new(stdin_reader, stdout_writer, cx);
|
||||
fake.handle_request::<request::Initialize, _>({
|
||||
fake.handle_request::<request::Initialize, _, _>({
|
||||
let capabilities = capabilities.clone();
|
||||
move |_, _| InitializeResult {
|
||||
capabilities: capabilities.clone(),
|
||||
move |_, _| {
|
||||
let capabilities = capabilities.clone();
|
||||
async move {
|
||||
InitializeResult {
|
||||
capabilities,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let executor = cx.background().clone();
|
||||
|
@ -628,7 +640,8 @@ impl FakeLanguageServer {
|
|||
let response;
|
||||
if let Some(handler) = handlers.lock().get_mut(request.method) {
|
||||
response =
|
||||
handler(request.id, request.params.get().as_bytes(), cx.clone());
|
||||
handler(request.id, request.params.get().as_bytes(), cx.clone())
|
||||
.await;
|
||||
log::debug!("handled lsp request. method:{}", request.method);
|
||||
} else {
|
||||
response = serde_json::to_vec(&AnyResponse {
|
||||
|
@ -704,19 +717,25 @@ impl FakeLanguageServer {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn handle_request<T, F>(
|
||||
pub fn handle_request<T, F, Fut>(
|
||||
&mut self,
|
||||
mut handler: F,
|
||||
) -> futures::channel::mpsc::UnboundedReceiver<()>
|
||||
where
|
||||
T: 'static + request::Request,
|
||||
F: 'static + Send + FnMut(T::Params, gpui::AsyncAppContext) -> T::Result,
|
||||
F: 'static + Send + FnMut(T::Params, gpui::AsyncAppContext) -> Fut,
|
||||
Fut: 'static + Send + Future<Output = T::Result>,
|
||||
{
|
||||
use futures::FutureExt as _;
|
||||
|
||||
let (responded_tx, responded_rx) = futures::channel::mpsc::unbounded();
|
||||
self.handlers.lock().insert(
|
||||
T::METHOD,
|
||||
Box::new(move |id, params, cx| {
|
||||
let result = handler(serde_json::from_slice::<T::Params>(params).unwrap(), cx);
|
||||
let responded_tx = responded_tx.clone();
|
||||
async move {
|
||||
let result = result.await;
|
||||
let result = serde_json::to_string(&result).unwrap();
|
||||
let result = serde_json::from_str::<&RawValue>(&result).unwrap();
|
||||
let response = AnyResponse {
|
||||
|
@ -726,6 +745,8 @@ impl FakeLanguageServer {
|
|||
};
|
||||
responded_tx.unbounded_send(()).ok();
|
||||
serde_json::to_vec(&response).unwrap()
|
||||
}
|
||||
.boxed()
|
||||
}),
|
||||
);
|
||||
responded_rx
|
||||
|
@ -844,7 +865,7 @@ mod tests {
|
|||
"file://b/c"
|
||||
);
|
||||
|
||||
fake.handle_request::<request::Shutdown, _>(|_, _| ());
|
||||
fake.handle_request::<request::Shutdown, _, _>(|_, _| async move {});
|
||||
|
||||
drop(server);
|
||||
fake.receive_notification::<notification::Exit>().await;
|
||||
|
|
|
@ -69,7 +69,7 @@ impl View for OutlineView {
|
|||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let settings = cx.app_state::<Settings>();
|
||||
let settings = cx.global::<Settings>();
|
||||
|
||||
Flex::new(Axis::Vertical)
|
||||
.with_child(
|
||||
|
@ -124,9 +124,12 @@ impl OutlineView {
|
|||
.active_item(cx)
|
||||
.and_then(|item| item.downcast::<Editor>())
|
||||
{
|
||||
let buffer = editor.read(cx).buffer().read(cx).read(cx).outline(Some(
|
||||
cx.app_state::<Settings>().theme.editor.syntax.as_ref(),
|
||||
));
|
||||
let buffer = editor
|
||||
.read(cx)
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.read(cx)
|
||||
.outline(Some(cx.global::<Settings>().theme.editor.syntax.as_ref()));
|
||||
if let Some(outline) = buffer {
|
||||
workspace.toggle_modal(cx, |cx, _| {
|
||||
let view = cx.add_view(|cx| OutlineView::new(outline, editor, cx));
|
||||
|
@ -221,7 +224,7 @@ impl OutlineView {
|
|||
) {
|
||||
match event {
|
||||
editor::Event::Blurred => cx.emit(Event::Dismissed),
|
||||
editor::Event::Edited => self.update_matches(cx),
|
||||
editor::Event::BufferEdited { .. } => self.update_matches(cx),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
@ -288,7 +291,7 @@ impl OutlineView {
|
|||
|
||||
fn render_matches(&self, cx: &AppContext) -> ElementBox {
|
||||
if self.matches.is_empty() {
|
||||
let settings = cx.app_state::<Settings>();
|
||||
let settings = cx.global::<Settings>();
|
||||
return Container::new(
|
||||
Label::new(
|
||||
"No matches".into(),
|
||||
|
@ -330,7 +333,7 @@ impl OutlineView {
|
|||
index: usize,
|
||||
cx: &AppContext,
|
||||
) -> ElementBox {
|
||||
let settings = cx.app_state::<Settings>();
|
||||
let settings = cx.global::<Settings>();
|
||||
let style = if index == self.selected_match_index {
|
||||
&settings.theme.selector.active_item
|
||||
} else {
|
||||
|
|
|
@ -11,15 +11,15 @@ use collections::{hash_map, BTreeMap, HashMap, HashSet};
|
|||
use futures::{future::Shared, Future, FutureExt, StreamExt, TryFutureExt};
|
||||
use fuzzy::{PathMatch, PathMatchCandidate, PathMatchCandidateSet};
|
||||
use gpui::{
|
||||
AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task,
|
||||
UpgradeModelHandle, WeakModelHandle,
|
||||
AnyModelHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
|
||||
MutableAppContext, Task, UpgradeModelHandle, WeakModelHandle,
|
||||
};
|
||||
use language::{
|
||||
proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
|
||||
range_from_lsp, Anchor, Bias, Buffer, CodeAction, CodeLabel, Completion, Diagnostic,
|
||||
DiagnosticEntry, DiagnosticSet, Event as BufferEvent, File as _, Language, LanguageRegistry,
|
||||
LocalFile, OffsetRangeExt, Operation, PointUtf16, TextBufferSnapshot, ToLspPosition, ToOffset,
|
||||
ToPointUtf16, Transaction,
|
||||
LocalFile, OffsetRangeExt, Operation, Patch, PointUtf16, TextBufferSnapshot, ToLspPosition,
|
||||
ToOffset, ToPointUtf16, Transaction,
|
||||
};
|
||||
use lsp::{DiagnosticSeverity, DiagnosticTag, DocumentHighlightKind, LanguageServer};
|
||||
use lsp_command::*;
|
||||
|
@ -39,7 +39,7 @@ use std::{
|
|||
path::{Component, Path, PathBuf},
|
||||
rc::Rc,
|
||||
sync::{
|
||||
atomic::{AtomicBool, AtomicUsize},
|
||||
atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
|
||||
Arc,
|
||||
},
|
||||
time::Instant,
|
||||
|
@ -49,9 +49,13 @@ use util::{post_inc, ResultExt, TryFutureExt as _};
|
|||
pub use fs::*;
|
||||
pub use worktree::*;
|
||||
|
||||
pub trait Item: Entity {
|
||||
fn entry_id(&self, cx: &AppContext) -> Option<ProjectEntryId>;
|
||||
}
|
||||
|
||||
pub struct Project {
|
||||
worktrees: Vec<WorktreeHandle>,
|
||||
active_entry: Option<ProjectEntry>,
|
||||
active_entry: Option<ProjectEntryId>,
|
||||
languages: Arc<LanguageRegistry>,
|
||||
language_servers: HashMap<(WorktreeId, Arc<str>), Arc<LanguageServer>>,
|
||||
started_language_servers: HashMap<(WorktreeId, Arc<str>), Task<Option<Arc<LanguageServer>>>>,
|
||||
|
@ -114,12 +118,14 @@ pub struct Collaborator {
|
|||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Event {
|
||||
ActiveEntryChanged(Option<ProjectEntry>),
|
||||
ActiveEntryChanged(Option<ProjectEntryId>),
|
||||
WorktreeRemoved(WorktreeId),
|
||||
DiskBasedDiagnosticsStarted,
|
||||
DiskBasedDiagnosticsUpdated,
|
||||
DiskBasedDiagnosticsFinished,
|
||||
DiagnosticsUpdated(ProjectPath),
|
||||
RemoteIdChanged(Option<u64>),
|
||||
CollaboratorLeft(PeerId),
|
||||
}
|
||||
|
||||
enum LanguageServerEvent {
|
||||
|
@ -226,42 +232,58 @@ impl DiagnosticSummary {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct ProjectEntry {
|
||||
pub worktree_id: WorktreeId,
|
||||
pub entry_id: usize,
|
||||
#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct ProjectEntryId(usize);
|
||||
|
||||
impl ProjectEntryId {
|
||||
pub fn new(counter: &AtomicUsize) -> Self {
|
||||
Self(counter.fetch_add(1, SeqCst))
|
||||
}
|
||||
|
||||
pub fn from_proto(id: u64) -> Self {
|
||||
Self(id as usize)
|
||||
}
|
||||
|
||||
pub fn to_proto(&self) -> u64 {
|
||||
self.0 as u64
|
||||
}
|
||||
|
||||
pub fn to_usize(&self) -> usize {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Project {
|
||||
pub fn init(client: &Arc<Client>) {
|
||||
client.add_entity_message_handler(Self::handle_add_collaborator);
|
||||
client.add_entity_message_handler(Self::handle_buffer_reloaded);
|
||||
client.add_entity_message_handler(Self::handle_buffer_saved);
|
||||
client.add_entity_message_handler(Self::handle_start_language_server);
|
||||
client.add_entity_message_handler(Self::handle_update_language_server);
|
||||
client.add_entity_message_handler(Self::handle_remove_collaborator);
|
||||
client.add_entity_message_handler(Self::handle_register_worktree);
|
||||
client.add_entity_message_handler(Self::handle_unregister_worktree);
|
||||
client.add_entity_message_handler(Self::handle_unshare_project);
|
||||
client.add_entity_message_handler(Self::handle_update_buffer_file);
|
||||
client.add_entity_message_handler(Self::handle_update_buffer);
|
||||
client.add_entity_message_handler(Self::handle_update_diagnostic_summary);
|
||||
client.add_entity_message_handler(Self::handle_update_worktree);
|
||||
client.add_entity_request_handler(Self::handle_apply_additional_edits_for_completion);
|
||||
client.add_entity_request_handler(Self::handle_apply_code_action);
|
||||
client.add_entity_request_handler(Self::handle_format_buffers);
|
||||
client.add_entity_request_handler(Self::handle_get_code_actions);
|
||||
client.add_entity_request_handler(Self::handle_get_completions);
|
||||
client.add_entity_request_handler(Self::handle_lsp_command::<GetDefinition>);
|
||||
client.add_entity_request_handler(Self::handle_lsp_command::<GetDocumentHighlights>);
|
||||
client.add_entity_request_handler(Self::handle_lsp_command::<GetReferences>);
|
||||
client.add_entity_request_handler(Self::handle_lsp_command::<PrepareRename>);
|
||||
client.add_entity_request_handler(Self::handle_lsp_command::<PerformRename>);
|
||||
client.add_entity_request_handler(Self::handle_search_project);
|
||||
client.add_entity_request_handler(Self::handle_get_project_symbols);
|
||||
client.add_entity_request_handler(Self::handle_open_buffer_for_symbol);
|
||||
client.add_entity_request_handler(Self::handle_open_buffer);
|
||||
client.add_entity_request_handler(Self::handle_save_buffer);
|
||||
client.add_model_message_handler(Self::handle_add_collaborator);
|
||||
client.add_model_message_handler(Self::handle_buffer_reloaded);
|
||||
client.add_model_message_handler(Self::handle_buffer_saved);
|
||||
client.add_model_message_handler(Self::handle_start_language_server);
|
||||
client.add_model_message_handler(Self::handle_update_language_server);
|
||||
client.add_model_message_handler(Self::handle_remove_collaborator);
|
||||
client.add_model_message_handler(Self::handle_register_worktree);
|
||||
client.add_model_message_handler(Self::handle_unregister_worktree);
|
||||
client.add_model_message_handler(Self::handle_unshare_project);
|
||||
client.add_model_message_handler(Self::handle_update_buffer_file);
|
||||
client.add_model_message_handler(Self::handle_update_buffer);
|
||||
client.add_model_message_handler(Self::handle_update_diagnostic_summary);
|
||||
client.add_model_message_handler(Self::handle_update_worktree);
|
||||
client.add_model_request_handler(Self::handle_apply_additional_edits_for_completion);
|
||||
client.add_model_request_handler(Self::handle_apply_code_action);
|
||||
client.add_model_request_handler(Self::handle_format_buffers);
|
||||
client.add_model_request_handler(Self::handle_get_code_actions);
|
||||
client.add_model_request_handler(Self::handle_get_completions);
|
||||
client.add_model_request_handler(Self::handle_lsp_command::<GetDefinition>);
|
||||
client.add_model_request_handler(Self::handle_lsp_command::<GetDocumentHighlights>);
|
||||
client.add_model_request_handler(Self::handle_lsp_command::<GetReferences>);
|
||||
client.add_model_request_handler(Self::handle_lsp_command::<PrepareRename>);
|
||||
client.add_model_request_handler(Self::handle_lsp_command::<PerformRename>);
|
||||
client.add_model_request_handler(Self::handle_search_project);
|
||||
client.add_model_request_handler(Self::handle_get_project_symbols);
|
||||
client.add_model_request_handler(Self::handle_open_buffer_for_symbol);
|
||||
client.add_model_request_handler(Self::handle_open_buffer_by_id);
|
||||
client.add_model_request_handler(Self::handle_open_buffer_by_path);
|
||||
client.add_model_request_handler(Self::handle_save_buffer);
|
||||
}
|
||||
|
||||
pub fn local(
|
||||
|
@ -280,31 +302,11 @@ impl Project {
|
|||
let mut status = rpc.status();
|
||||
while let Some(status) = status.next().await {
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
let remote_id = if status.is_connected() {
|
||||
let response = rpc.request(proto::RegisterProject {}).await?;
|
||||
Some(response.project_id)
|
||||
if status.is_connected() {
|
||||
this.update(&mut cx, |this, cx| this.register(cx)).await?;
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(project_id) = remote_id {
|
||||
let mut registrations = Vec::new();
|
||||
this.update(&mut cx, |this, cx| {
|
||||
for worktree in this.worktrees(cx).collect::<Vec<_>>() {
|
||||
registrations.push(worktree.update(
|
||||
cx,
|
||||
|worktree, cx| {
|
||||
let worktree = worktree.as_local_mut().unwrap();
|
||||
worktree.register(project_id, cx)
|
||||
},
|
||||
));
|
||||
this.update(&mut cx, |this, cx| this.unregister(cx));
|
||||
}
|
||||
});
|
||||
for registration in registrations {
|
||||
registration.await?;
|
||||
}
|
||||
}
|
||||
this.update(&mut cx, |this, cx| this.set_remote_id(remote_id, cx));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
@ -355,7 +357,7 @@ impl Project {
|
|||
fs: Arc<dyn Fs>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<ModelHandle<Self>> {
|
||||
client.authenticate_and_connect(&cx).await?;
|
||||
client.authenticate_and_connect(true, &cx).await?;
|
||||
|
||||
let response = client
|
||||
.request(proto::JoinProject {
|
||||
|
@ -468,7 +470,6 @@ impl Project {
|
|||
cx.update(|cx| Project::local(client, user_store, languages, fs, cx))
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn buffer_for_id(&self, remote_id: u64, cx: &AppContext) -> Option<ModelHandle<Buffer>> {
|
||||
self.opened_buffers
|
||||
.get(&remote_id)
|
||||
|
@ -537,16 +538,54 @@ impl Project {
|
|||
&self.fs
|
||||
}
|
||||
|
||||
fn set_remote_id(&mut self, remote_id: Option<u64>, cx: &mut ModelContext<Self>) {
|
||||
fn unregister(&mut self, cx: &mut ModelContext<Self>) {
|
||||
self.unshare(cx);
|
||||
for worktree in &self.worktrees {
|
||||
if let Some(worktree) = worktree.upgrade(cx) {
|
||||
worktree.update(cx, |worktree, _| {
|
||||
worktree.as_local_mut().unwrap().unregister();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let ProjectClientState::Local { remote_id_tx, .. } = &mut self.client_state {
|
||||
*remote_id_tx.borrow_mut() = remote_id;
|
||||
*remote_id_tx.borrow_mut() = None;
|
||||
}
|
||||
|
||||
self.subscriptions.clear();
|
||||
if let Some(remote_id) = remote_id {
|
||||
self.subscriptions
|
||||
.push(self.client.add_model_for_remote_entity(remote_id, cx));
|
||||
}
|
||||
|
||||
fn register(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
self.unregister(cx);
|
||||
|
||||
let response = self.client.request(proto::RegisterProject {});
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let remote_id = response.await?.project_id;
|
||||
|
||||
let mut registrations = Vec::new();
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if let ProjectClientState::Local { remote_id_tx, .. } = &mut this.client_state {
|
||||
*remote_id_tx.borrow_mut() = Some(remote_id);
|
||||
}
|
||||
|
||||
cx.emit(Event::RemoteIdChanged(Some(remote_id)));
|
||||
|
||||
this.subscriptions
|
||||
.push(this.client.add_model_for_remote_entity(remote_id, cx));
|
||||
|
||||
for worktree in &this.worktrees {
|
||||
if let Some(worktree) = worktree.upgrade(cx) {
|
||||
registrations.push(worktree.update(cx, |worktree, cx| {
|
||||
let worktree = worktree.as_local_mut().unwrap();
|
||||
worktree.register(remote_id, cx)
|
||||
}));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
futures::future::try_join_all(registrations).await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn remote_id(&self) -> Option<u64> {
|
||||
|
@ -623,6 +662,24 @@ impl Project {
|
|||
.find(|worktree| worktree.read(cx).id() == id)
|
||||
}
|
||||
|
||||
pub fn worktree_for_entry(
|
||||
&self,
|
||||
entry_id: ProjectEntryId,
|
||||
cx: &AppContext,
|
||||
) -> Option<ModelHandle<Worktree>> {
|
||||
self.worktrees(cx)
|
||||
.find(|worktree| worktree.read(cx).contains_entry(entry_id))
|
||||
}
|
||||
|
||||
pub fn worktree_id_for_entry(
|
||||
&self,
|
||||
entry_id: ProjectEntryId,
|
||||
cx: &AppContext,
|
||||
) -> Option<WorktreeId> {
|
||||
self.worktree_for_entry(entry_id, cx)
|
||||
.map(|worktree| worktree.read(cx).id())
|
||||
}
|
||||
|
||||
pub fn share(&self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
let rpc = self.client.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
|
@ -685,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();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let project_id = this.update(&mut cx, |this, cx| {
|
||||
|
||||
if let ProjectClientState::Local {
|
||||
is_shared,
|
||||
remote_id_rx,
|
||||
..
|
||||
} = &mut this.client_state
|
||||
} = &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 {
|
||||
OpenBuffer::Strong(buffer) => {
|
||||
*open_buffer = OpenBuffer::Weak(buffer.downgrade());
|
||||
|
@ -706,38 +779,14 @@ impl Project {
|
|||
}
|
||||
}
|
||||
|
||||
for worktree_handle in this.worktrees.iter_mut() {
|
||||
match worktree_handle {
|
||||
WorktreeHandle::Strong(worktree) => {
|
||||
if !worktree.read(cx).is_visible() {
|
||||
*worktree_handle = WorktreeHandle::Weak(worktree.downgrade());
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if let Some(project_id) = *remote_id_rx.borrow() {
|
||||
rpc.send(proto::UnshareProject { project_id }).log_err();
|
||||
}
|
||||
|
||||
remote_id_rx
|
||||
.borrow()
|
||||
.ok_or_else(|| anyhow!("no project id"))
|
||||
cx.notify();
|
||||
} 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>) {
|
||||
|
@ -785,6 +834,23 @@ impl Project {
|
|||
Ok(buffer)
|
||||
}
|
||||
|
||||
pub fn open_path(
|
||||
&mut self,
|
||||
path: impl Into<ProjectPath>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<(ProjectEntryId, AnyModelHandle)>> {
|
||||
let task = self.open_buffer(path, cx);
|
||||
cx.spawn_weak(|_, cx| async move {
|
||||
let buffer = task.await?;
|
||||
let project_entry_id = buffer
|
||||
.read_with(&cx, |buffer, cx| {
|
||||
File::from_dyn(buffer.file()).and_then(|file| file.project_entry_id(cx))
|
||||
})
|
||||
.ok_or_else(|| anyhow!("no project entry"))?;
|
||||
Ok((project_entry_id, buffer.into()))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn open_buffer(
|
||||
&mut self,
|
||||
path: impl Into<ProjectPath>,
|
||||
|
@ -876,7 +942,7 @@ impl Project {
|
|||
let path_string = path.to_string_lossy().to_string();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let response = rpc
|
||||
.request(proto::OpenBuffer {
|
||||
.request(proto::OpenBufferByPath {
|
||||
project_id,
|
||||
worktree_id: remote_worktree_id.to_proto(),
|
||||
path: path_string,
|
||||
|
@ -925,6 +991,32 @@ impl Project {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn open_buffer_by_id(
|
||||
&mut self,
|
||||
id: u64,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<ModelHandle<Buffer>>> {
|
||||
if let Some(buffer) = self.buffer_for_id(id, cx) {
|
||||
Task::ready(Ok(buffer))
|
||||
} else if self.is_local() {
|
||||
Task::ready(Err(anyhow!("buffer {} does not exist", id)))
|
||||
} else if let Some(project_id) = self.remote_id() {
|
||||
let request = self
|
||||
.client
|
||||
.request(proto::OpenBufferById { project_id, id });
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let buffer = request
|
||||
.await?
|
||||
.buffer
|
||||
.ok_or_else(|| anyhow!("invalid buffer"))?;
|
||||
this.update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx))
|
||||
.await
|
||||
})
|
||||
} else {
|
||||
Task::ready(Err(anyhow!("cannot open buffer while disconnected")))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save_buffer_as(
|
||||
&mut self,
|
||||
buffer: ModelHandle<Buffer>,
|
||||
|
@ -1096,7 +1188,7 @@ impl Project {
|
|||
});
|
||||
cx.background().spawn(request).detach_and_log_err(cx);
|
||||
}
|
||||
BufferEvent::Edited => {
|
||||
BufferEvent::Edited { .. } => {
|
||||
let language_server = self
|
||||
.language_server_for_buffer(buffer.read(cx), cx)?
|
||||
.clone();
|
||||
|
@ -1783,38 +1875,23 @@ impl Project {
|
|||
});
|
||||
|
||||
let mut sanitized_diagnostics = Vec::new();
|
||||
let mut edits_since_save = snapshot
|
||||
let edits_since_save = Patch::new(
|
||||
snapshot
|
||||
.edits_since::<PointUtf16>(buffer.read(cx).saved_version())
|
||||
.peekable();
|
||||
let mut last_edit_old_end = PointUtf16::zero();
|
||||
let mut last_edit_new_end = PointUtf16::zero();
|
||||
'outer: for entry in diagnostics {
|
||||
let mut start = entry.range.start;
|
||||
let mut end = entry.range.end;
|
||||
|
||||
.collect(),
|
||||
);
|
||||
for entry in diagnostics {
|
||||
let start;
|
||||
let end;
|
||||
if entry.diagnostic.is_disk_based {
|
||||
// Some diagnostics are based on files on disk instead of buffers'
|
||||
// current contents. Adjust these diagnostics' ranges to reflect
|
||||
// any unsaved edits.
|
||||
if entry.diagnostic.is_disk_based {
|
||||
while let Some(edit) = edits_since_save.peek() {
|
||||
if edit.old.end <= start {
|
||||
last_edit_old_end = edit.old.end;
|
||||
last_edit_new_end = edit.new.end;
|
||||
edits_since_save.next();
|
||||
} else if edit.old.start <= end && edit.old.end >= start {
|
||||
continue 'outer;
|
||||
start = edits_since_save.old_to_new(entry.range.start);
|
||||
end = edits_since_save.old_to_new(entry.range.end);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let start_overshoot = start - last_edit_old_end;
|
||||
start = last_edit_new_end;
|
||||
start += start_overshoot;
|
||||
|
||||
let end_overshoot = end - last_edit_old_end;
|
||||
end = last_edit_new_end;
|
||||
end += end_overshoot;
|
||||
start = entry.range.start;
|
||||
end = entry.range.end;
|
||||
}
|
||||
|
||||
let mut range = snapshot.clip_point_utf16(start, Bias::Left)
|
||||
|
@ -3163,10 +3240,7 @@ impl Project {
|
|||
let new_active_entry = entry.and_then(|project_path| {
|
||||
let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
|
||||
let entry = worktree.read(cx).entry_for_path(project_path.path)?;
|
||||
Some(ProjectEntry {
|
||||
worktree_id: project_path.worktree_id,
|
||||
entry_id: entry.id,
|
||||
})
|
||||
Some(entry.id)
|
||||
});
|
||||
if new_active_entry != self.active_entry {
|
||||
self.active_entry = new_active_entry;
|
||||
|
@ -3217,10 +3291,25 @@ impl Project {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn active_entry(&self) -> Option<ProjectEntry> {
|
||||
pub fn active_entry(&self) -> Option<ProjectEntryId> {
|
||||
self.active_entry
|
||||
}
|
||||
|
||||
pub fn entry_for_path(&self, path: &ProjectPath, cx: &AppContext) -> Option<ProjectEntryId> {
|
||||
self.worktree_for_id(path.worktree_id, cx)?
|
||||
.read(cx)
|
||||
.entry_for_path(&path.path)
|
||||
.map(|entry| entry.id)
|
||||
}
|
||||
|
||||
pub fn path_for_entry(&self, entry_id: ProjectEntryId, cx: &AppContext) -> Option<ProjectPath> {
|
||||
let worktree = self.worktree_for_entry(entry_id, cx)?;
|
||||
let worktree = worktree.read(cx);
|
||||
let worktree_id = worktree.id();
|
||||
let path = worktree.entry_for_id(entry_id)?.path.clone();
|
||||
Some(ProjectPath { worktree_id, path })
|
||||
}
|
||||
|
||||
// RPC message handlers
|
||||
|
||||
async fn handle_unshare_project(
|
||||
|
@ -3274,6 +3363,7 @@ impl Project {
|
|||
buffer.update(cx, |buffer, cx| buffer.remove_peer(replica_id, cx));
|
||||
}
|
||||
}
|
||||
cx.emit(Event::CollaboratorLeft(peer_id));
|
||||
cx.notify();
|
||||
Ok(())
|
||||
})
|
||||
|
@ -3821,9 +3911,28 @@ impl Project {
|
|||
hasher.finalize().as_slice().try_into().unwrap()
|
||||
}
|
||||
|
||||
async fn handle_open_buffer(
|
||||
async fn handle_open_buffer_by_id(
|
||||
this: ModelHandle<Self>,
|
||||
envelope: TypedEnvelope<proto::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>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<proto::OpenBufferResponse> {
|
||||
|
@ -4477,6 +4586,12 @@ fn relativize_path(base: &Path, path: &Path) -> PathBuf {
|
|||
components.iter().map(|c| c.as_os_str()).collect()
|
||||
}
|
||||
|
||||
impl Item for Buffer {
|
||||
fn entry_id(&self, cx: &AppContext) -> Option<ProjectEntryId> {
|
||||
File::from_dyn(self.file()).and_then(|file| file.project_entry_id(cx))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{Event, *};
|
||||
|
@ -4897,7 +5012,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_transforming_disk_based_diagnostics(cx: &mut gpui::TestAppContext) {
|
||||
async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
|
||||
cx.foreground().forbid_parking();
|
||||
|
||||
let (mut lsp_config, mut fake_servers) = LanguageServerConfig::fake();
|
||||
|
@ -5122,11 +5237,13 @@ mod tests {
|
|||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(Some(Point::new(2, 0)..Point::new(2, 0)), " ", cx);
|
||||
buffer.edit(Some(Point::new(2, 8)..Point::new(2, 10)), "(x: usize)", cx);
|
||||
buffer.edit(Some(Point::new(3, 10)..Point::new(3, 10)), "xxx", cx);
|
||||
});
|
||||
let change_notification_2 =
|
||||
fake_server.receive_notification::<lsp::notification::DidChangeTextDocument>();
|
||||
let change_notification_2 = fake_server
|
||||
.receive_notification::<lsp::notification::DidChangeTextDocument>()
|
||||
.await;
|
||||
assert!(
|
||||
change_notification_2.await.text_document.version
|
||||
change_notification_2.text_document.version
|
||||
> change_notification_1.text_document.version
|
||||
);
|
||||
|
||||
|
@ -5134,7 +5251,7 @@ mod tests {
|
|||
fake_server.notify::<lsp::notification::PublishDiagnostics>(
|
||||
lsp::PublishDiagnosticsParams {
|
||||
uri: lsp::Url::from_file_path("/dir/a.rs").unwrap(),
|
||||
version: Some(open_notification.text_document.version),
|
||||
version: Some(change_notification_2.text_document.version),
|
||||
diagnostics: vec![
|
||||
lsp::Diagnostic {
|
||||
range: lsp::Range::new(lsp::Position::new(1, 9), lsp::Position::new(1, 11)),
|
||||
|
@ -5174,7 +5291,7 @@ mod tests {
|
|||
}
|
||||
},
|
||||
DiagnosticEntry {
|
||||
range: Point::new(3, 9)..Point::new(3, 11),
|
||||
range: Point::new(3, 9)..Point::new(3, 14),
|
||||
diagnostic: Diagnostic {
|
||||
severity: DiagnosticSeverity::ERROR,
|
||||
message: "undefined variable 'BB'".to_string(),
|
||||
|
@ -5672,7 +5789,7 @@ mod tests {
|
|||
.unwrap();
|
||||
|
||||
let mut fake_server = fake_servers.next().await.unwrap();
|
||||
fake_server.handle_request::<lsp::request::GotoDefinition, _>(move |params, _| {
|
||||
fake_server.handle_request::<lsp::request::GotoDefinition, _, _>(|params, _| async move {
|
||||
let params = params.text_document_position_params;
|
||||
assert_eq!(
|
||||
params.text_document.uri.to_file_path().unwrap(),
|
||||
|
@ -6607,7 +6724,7 @@ mod tests {
|
|||
project.prepare_rename(buffer.clone(), 7, cx)
|
||||
});
|
||||
fake_server
|
||||
.handle_request::<lsp::request::PrepareRenameRequest, _>(|params, _| {
|
||||
.handle_request::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
|
||||
assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
|
||||
assert_eq!(params.position, lsp::Position::new(0, 7));
|
||||
Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
|
||||
|
@ -6626,7 +6743,7 @@ mod tests {
|
|||
project.perform_rename(buffer.clone(), 7, "THREE".to_string(), true, cx)
|
||||
});
|
||||
fake_server
|
||||
.handle_request::<lsp::request::Rename, _>(|params, _| {
|
||||
.handle_request::<lsp::request::Rename, _, _>(|params, _| async move {
|
||||
assert_eq!(
|
||||
params.text_document_position.text_document.uri.as_str(),
|
||||
"file:///dir/one.rs"
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
use crate::ProjectEntryId;
|
||||
|
||||
use super::{
|
||||
fs::{self, Fs},
|
||||
ignore::IgnoreStack,
|
||||
|
@ -39,10 +41,7 @@ use std::{
|
|||
future::Future,
|
||||
ops::{Deref, DerefMut},
|
||||
path::{Path, PathBuf},
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering::SeqCst},
|
||||
Arc,
|
||||
},
|
||||
sync::{atomic::AtomicUsize, Arc},
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap};
|
||||
|
@ -101,7 +100,7 @@ pub struct LocalSnapshot {
|
|||
abs_path: Arc<Path>,
|
||||
scan_id: usize,
|
||||
ignores: HashMap<Arc<Path>, (Arc<Gitignore>, usize)>,
|
||||
removed_entry_ids: HashMap<u64, usize>,
|
||||
removed_entry_ids: HashMap<u64, ProjectEntryId>,
|
||||
next_entry_id: Arc<AtomicUsize>,
|
||||
snapshot: Snapshot,
|
||||
}
|
||||
|
@ -712,7 +711,9 @@ impl LocalWorktree {
|
|||
let worktree = this.as_local_mut().unwrap();
|
||||
match response {
|
||||
Ok(_) => {
|
||||
if worktree.registration == Registration::Pending {
|
||||
worktree.registration = Registration::Done { project_id };
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(error) => {
|
||||
|
@ -809,6 +810,11 @@ impl LocalWorktree {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn unregister(&mut self) {
|
||||
self.unshare();
|
||||
self.registration = Registration::None;
|
||||
}
|
||||
|
||||
pub fn unshare(&mut self) {
|
||||
self.share.take();
|
||||
}
|
||||
|
@ -856,13 +862,16 @@ impl Snapshot {
|
|||
self.id
|
||||
}
|
||||
|
||||
pub fn contains_entry(&self, entry_id: ProjectEntryId) -> bool {
|
||||
self.entries_by_id.get(&entry_id, &()).is_some()
|
||||
}
|
||||
|
||||
pub(crate) fn apply_remote_update(&mut self, update: proto::UpdateWorktree) -> Result<()> {
|
||||
let mut entries_by_path_edits = Vec::new();
|
||||
let mut entries_by_id_edits = Vec::new();
|
||||
for entry_id in update.removed_entries {
|
||||
let entry_id = entry_id as usize;
|
||||
let entry = self
|
||||
.entry_for_id(entry_id)
|
||||
.entry_for_id(ProjectEntryId::from_proto(entry_id))
|
||||
.ok_or_else(|| anyhow!("unknown entry"))?;
|
||||
entries_by_path_edits.push(Edit::Remove(PathKey(entry.path.clone())));
|
||||
entries_by_id_edits.push(Edit::Remove(entry.id));
|
||||
|
@ -985,7 +994,7 @@ impl Snapshot {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn entry_for_id(&self, id: usize) -> Option<&Entry> {
|
||||
pub fn entry_for_id(&self, id: ProjectEntryId) -> Option<&Entry> {
|
||||
let entry = self.entries_by_id.get(&id, &())?;
|
||||
self.entry_for_path(&entry.path)
|
||||
}
|
||||
|
@ -1062,7 +1071,7 @@ impl LocalSnapshot {
|
|||
other_entries.next();
|
||||
}
|
||||
Ordering::Greater => {
|
||||
removed_entries.push(other_entry.id as u64);
|
||||
removed_entries.push(other_entry.id.to_proto());
|
||||
other_entries.next();
|
||||
}
|
||||
}
|
||||
|
@ -1073,7 +1082,7 @@ impl LocalSnapshot {
|
|||
self_entries.next();
|
||||
}
|
||||
(None, Some(other_entry)) => {
|
||||
removed_entries.push(other_entry.id as u64);
|
||||
removed_entries.push(other_entry.id.to_proto());
|
||||
other_entries.next();
|
||||
}
|
||||
(None, None) => break,
|
||||
|
@ -1326,7 +1335,7 @@ pub struct File {
|
|||
pub worktree: ModelHandle<Worktree>,
|
||||
pub path: Arc<Path>,
|
||||
pub mtime: SystemTime,
|
||||
pub(crate) entry_id: Option<usize>,
|
||||
pub(crate) entry_id: Option<ProjectEntryId>,
|
||||
pub(crate) is_local: bool,
|
||||
}
|
||||
|
||||
|
@ -1423,7 +1432,7 @@ impl language::File for File {
|
|||
fn to_proto(&self) -> rpc::proto::File {
|
||||
rpc::proto::File {
|
||||
worktree_id: self.worktree.id() as u64,
|
||||
entry_id: self.entry_id.map(|entry_id| entry_id as u64),
|
||||
entry_id: self.entry_id.map(|entry_id| entry_id.to_proto()),
|
||||
path: self.path.to_string_lossy().into(),
|
||||
mtime: Some(self.mtime.into()),
|
||||
}
|
||||
|
@ -1490,7 +1499,7 @@ impl File {
|
|||
worktree,
|
||||
path: Path::new(&proto.path).into(),
|
||||
mtime: proto.mtime.ok_or_else(|| anyhow!("no timestamp"))?.into(),
|
||||
entry_id: proto.entry_id.map(|entry_id| entry_id as usize),
|
||||
entry_id: proto.entry_id.map(ProjectEntryId::from_proto),
|
||||
is_local: false,
|
||||
})
|
||||
}
|
||||
|
@ -1502,11 +1511,15 @@ impl File {
|
|||
pub fn worktree_id(&self, cx: &AppContext) -> WorktreeId {
|
||||
self.worktree.read(cx).id()
|
||||
}
|
||||
|
||||
pub fn project_entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
|
||||
self.entry_id
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Entry {
|
||||
pub id: usize,
|
||||
pub id: ProjectEntryId,
|
||||
pub kind: EntryKind,
|
||||
pub path: Arc<Path>,
|
||||
pub inode: u64,
|
||||
|
@ -1530,7 +1543,7 @@ impl Entry {
|
|||
root_char_bag: CharBag,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: next_entry_id.fetch_add(1, SeqCst),
|
||||
id: ProjectEntryId::new(next_entry_id),
|
||||
kind: if metadata.is_dir {
|
||||
EntryKind::PendingDir
|
||||
} else {
|
||||
|
@ -1620,7 +1633,7 @@ impl sum_tree::Summary for EntrySummary {
|
|||
|
||||
#[derive(Clone, Debug)]
|
||||
struct PathEntry {
|
||||
id: usize,
|
||||
id: ProjectEntryId,
|
||||
path: Arc<Path>,
|
||||
is_ignored: bool,
|
||||
scan_id: usize,
|
||||
|
@ -1635,7 +1648,7 @@ impl sum_tree::Item for PathEntry {
|
|||
}
|
||||
|
||||
impl sum_tree::KeyedItem for PathEntry {
|
||||
type Key = usize;
|
||||
type Key = ProjectEntryId;
|
||||
|
||||
fn key(&self) -> Self::Key {
|
||||
self.id
|
||||
|
@ -1644,7 +1657,7 @@ impl sum_tree::KeyedItem for PathEntry {
|
|||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct PathEntrySummary {
|
||||
max_id: usize,
|
||||
max_id: ProjectEntryId,
|
||||
}
|
||||
|
||||
impl sum_tree::Summary for PathEntrySummary {
|
||||
|
@ -1655,7 +1668,7 @@ impl sum_tree::Summary for PathEntrySummary {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a> sum_tree::Dimension<'a, PathEntrySummary> for usize {
|
||||
impl<'a> sum_tree::Dimension<'a, PathEntrySummary> for ProjectEntryId {
|
||||
fn add_summary(&mut self, summary: &'a PathEntrySummary, _: &()) {
|
||||
*self = summary.max_id;
|
||||
}
|
||||
|
@ -2345,7 +2358,7 @@ impl<'a> Iterator for ChildEntriesIter<'a> {
|
|||
impl<'a> From<&'a Entry> for proto::Entry {
|
||||
fn from(entry: &'a Entry) -> Self {
|
||||
Self {
|
||||
id: entry.id as u64,
|
||||
id: entry.id.to_proto(),
|
||||
is_dir: entry.is_dir(),
|
||||
path: entry.path.to_string_lossy().to_string(),
|
||||
inode: entry.inode,
|
||||
|
@ -2370,7 +2383,7 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry {
|
|||
};
|
||||
let path: Arc<Path> = Arc::from(Path::new(&entry.path));
|
||||
Ok(Entry {
|
||||
id: entry.id as usize,
|
||||
id: ProjectEntryId::from_proto(entry.id),
|
||||
kind,
|
||||
path: path.clone(),
|
||||
inode: entry.inode,
|
||||
|
|
|
@ -9,7 +9,7 @@ use gpui::{
|
|||
AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, View, ViewContext,
|
||||
ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use project::{Project, ProjectEntry, ProjectPath, Worktree, WorktreeId};
|
||||
use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
|
||||
use std::{
|
||||
collections::{hash_map, HashMap},
|
||||
ffi::OsStr,
|
||||
|
@ -24,7 +24,7 @@ pub struct ProjectPanel {
|
|||
project: ModelHandle<Project>,
|
||||
list: UniformListState,
|
||||
visible_entries: Vec<(WorktreeId, Vec<usize>)>,
|
||||
expanded_dir_ids: HashMap<WorktreeId, Vec<usize>>,
|
||||
expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>,
|
||||
selection: Option<Selection>,
|
||||
handle: WeakViewHandle<Self>,
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ pub struct ProjectPanel {
|
|||
#[derive(Copy, Clone)]
|
||||
struct Selection {
|
||||
worktree_id: WorktreeId,
|
||||
entry_id: usize,
|
||||
entry_id: ProjectEntryId,
|
||||
index: usize,
|
||||
}
|
||||
|
||||
|
@ -47,8 +47,8 @@ struct EntryDetails {
|
|||
|
||||
action!(ExpandSelectedEntry);
|
||||
action!(CollapseSelectedEntry);
|
||||
action!(ToggleExpanded, ProjectEntry);
|
||||
action!(Open, ProjectEntry);
|
||||
action!(ToggleExpanded, ProjectEntryId);
|
||||
action!(Open, ProjectEntryId);
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(ProjectPanel::expand_selected_entry);
|
||||
|
@ -64,10 +64,7 @@ pub fn init(cx: &mut MutableAppContext) {
|
|||
}
|
||||
|
||||
pub enum Event {
|
||||
OpenedEntry {
|
||||
worktree_id: WorktreeId,
|
||||
entry_id: usize,
|
||||
},
|
||||
OpenedEntry(ProjectEntryId),
|
||||
}
|
||||
|
||||
impl ProjectPanel {
|
||||
|
@ -78,16 +75,16 @@ impl ProjectPanel {
|
|||
cx.notify();
|
||||
})
|
||||
.detach();
|
||||
cx.subscribe(&project, |this, _, event, cx| match event {
|
||||
project::Event::ActiveEntryChanged(Some(ProjectEntry {
|
||||
worktree_id,
|
||||
entry_id,
|
||||
})) => {
|
||||
this.expand_entry(*worktree_id, *entry_id, cx);
|
||||
this.update_visible_entries(Some((*worktree_id, *entry_id)), cx);
|
||||
cx.subscribe(&project, |this, project, event, cx| match event {
|
||||
project::Event::ActiveEntryChanged(Some(entry_id)) => {
|
||||
if let Some(worktree_id) = project.read(cx).worktree_id_for_entry(*entry_id, cx)
|
||||
{
|
||||
this.expand_entry(worktree_id, *entry_id, cx);
|
||||
this.update_visible_entries(Some((worktree_id, *entry_id)), cx);
|
||||
this.autoscroll();
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
project::Event::WorktreeRemoved(id) => {
|
||||
this.expanded_dir_ids.remove(id);
|
||||
this.update_visible_entries(None, cx);
|
||||
|
@ -109,16 +106,13 @@ impl ProjectPanel {
|
|||
this
|
||||
});
|
||||
cx.subscribe(&project_panel, move |workspace, _, event, cx| match event {
|
||||
&Event::OpenedEntry {
|
||||
worktree_id,
|
||||
entry_id,
|
||||
} => {
|
||||
if let Some(worktree) = project.read(cx).worktree_for_id(worktree_id, cx) {
|
||||
&Event::OpenedEntry(entry_id) => {
|
||||
if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) {
|
||||
if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) {
|
||||
workspace
|
||||
.open_path(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
worktree_id: worktree.read(cx).id(),
|
||||
path: entry.path.clone(),
|
||||
},
|
||||
cx,
|
||||
|
@ -152,10 +146,7 @@ impl ProjectPanel {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
let event = Event::OpenedEntry {
|
||||
worktree_id: worktree.id(),
|
||||
entry_id: entry.id,
|
||||
};
|
||||
let event = Event::OpenedEntry(entry.id);
|
||||
cx.emit(event);
|
||||
}
|
||||
}
|
||||
|
@ -193,11 +184,8 @@ impl ProjectPanel {
|
|||
}
|
||||
|
||||
fn toggle_expanded(&mut self, action: &ToggleExpanded, cx: &mut ViewContext<Self>) {
|
||||
let ProjectEntry {
|
||||
worktree_id,
|
||||
entry_id,
|
||||
} = action.0;
|
||||
|
||||
let entry_id = action.0;
|
||||
if let Some(worktree_id) = self.project.read(cx).worktree_id_for_entry(entry_id, cx) {
|
||||
if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) {
|
||||
match expanded_dir_ids.binary_search(&entry_id) {
|
||||
Ok(ix) => {
|
||||
|
@ -211,6 +199,7 @@ impl ProjectPanel {
|
|||
cx.focus_self();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
|
||||
if let Some(selection) = self.selection {
|
||||
|
@ -229,10 +218,7 @@ impl ProjectPanel {
|
|||
}
|
||||
|
||||
fn open_entry(&mut self, action: &Open, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(Event::OpenedEntry {
|
||||
worktree_id: action.0.worktree_id,
|
||||
entry_id: action.0.entry_id,
|
||||
});
|
||||
cx.emit(Event::OpenedEntry(action.0));
|
||||
}
|
||||
|
||||
fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
|
||||
|
@ -313,7 +299,7 @@ impl ProjectPanel {
|
|||
|
||||
fn update_visible_entries(
|
||||
&mut self,
|
||||
new_selected_entry: Option<(WorktreeId, usize)>,
|
||||
new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let worktrees = self
|
||||
|
@ -379,7 +365,7 @@ impl ProjectPanel {
|
|||
fn expand_entry(
|
||||
&mut self,
|
||||
worktree_id: WorktreeId,
|
||||
entry_id: usize,
|
||||
entry_id: ProjectEntryId,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let project = self.project.read(cx);
|
||||
|
@ -411,7 +397,7 @@ impl ProjectPanel {
|
|||
&self,
|
||||
range: Range<usize>,
|
||||
cx: &mut ViewContext<ProjectPanel>,
|
||||
mut callback: impl FnMut(ProjectEntry, EntryDetails, &mut ViewContext<ProjectPanel>),
|
||||
mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut ViewContext<ProjectPanel>),
|
||||
) {
|
||||
let mut ix = 0;
|
||||
for (worktree_id, visible_worktree_entries) in &self.visible_entries {
|
||||
|
@ -450,11 +436,7 @@ impl ProjectPanel {
|
|||
e.worktree_id == snapshot.id() && e.entry_id == entry.id
|
||||
}),
|
||||
};
|
||||
let entry = ProjectEntry {
|
||||
worktree_id: snapshot.id(),
|
||||
entry_id: entry.id,
|
||||
};
|
||||
callback(entry, details, cx);
|
||||
callback(entry.id, details, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -463,13 +445,13 @@ impl ProjectPanel {
|
|||
}
|
||||
|
||||
fn render_entry(
|
||||
entry: ProjectEntry,
|
||||
entry_id: ProjectEntryId,
|
||||
details: EntryDetails,
|
||||
theme: &theme::ProjectPanel,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> ElementBox {
|
||||
let is_dir = details.is_dir;
|
||||
MouseEventHandler::new::<Self, _, _>(entry.entry_id, cx, |state, _| {
|
||||
MouseEventHandler::new::<Self, _, _>(entry_id.to_usize(), cx, |state, _| {
|
||||
let style = match (details.is_selected, state.hovered) {
|
||||
(false, false) => &theme.entry,
|
||||
(false, true) => &theme.hovered_entry,
|
||||
|
@ -519,9 +501,9 @@ impl ProjectPanel {
|
|||
})
|
||||
.on_click(move |cx| {
|
||||
if is_dir {
|
||||
cx.dispatch_action(ToggleExpanded(entry))
|
||||
cx.dispatch_action(ToggleExpanded(entry_id))
|
||||
} else {
|
||||
cx.dispatch_action(Open(entry))
|
||||
cx.dispatch_action(Open(entry_id))
|
||||
}
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
|
@ -535,7 +517,7 @@ impl View for ProjectPanel {
|
|||
}
|
||||
|
||||
fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
|
||||
let theme = &cx.app_state::<Settings>().theme.project_panel;
|
||||
let theme = &cx.global::<Settings>().theme.project_panel;
|
||||
let mut container_style = theme.container;
|
||||
let padding = std::mem::take(&mut container_style.padding);
|
||||
let handle = self.handle.clone();
|
||||
|
@ -546,7 +528,7 @@ impl View for ProjectPanel {
|
|||
.map(|(_, worktree_entries)| worktree_entries.len())
|
||||
.sum(),
|
||||
move |range, items, cx| {
|
||||
let theme = cx.app_state::<Settings>().theme.clone();
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
let this = handle.upgrade(cx).unwrap();
|
||||
this.update(cx.app, |this, cx| {
|
||||
this.for_each_visible_entry(range.clone(), cx, |entry, details, cx| {
|
||||
|
@ -830,13 +812,7 @@ mod tests {
|
|||
let worktree = worktree.read(cx);
|
||||
if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
|
||||
let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
|
||||
panel.toggle_expanded(
|
||||
&ToggleExpanded(ProjectEntry {
|
||||
worktree_id: worktree.id(),
|
||||
entry_id,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
panel.toggle_expanded(&ToggleExpanded(entry_id), cx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
use editor::{
|
||||
combine_syntax_and_fuzzy_match_highlights, items::BufferItemHandle, styled_runs_for_code_label,
|
||||
Autoscroll, Bias, Editor,
|
||||
combine_syntax_and_fuzzy_match_highlights, styled_runs_for_code_label, Autoscroll, Bias, Editor,
|
||||
};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
|
@ -70,7 +69,7 @@ impl View for ProjectSymbolsView {
|
|||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let settings = cx.app_state::<Settings>();
|
||||
let settings = cx.global::<Settings>();
|
||||
Flex::new(Axis::Vertical)
|
||||
.with_child(
|
||||
Container::new(ChildView::new(&self.query_editor).boxed())
|
||||
|
@ -234,7 +233,7 @@ impl ProjectSymbolsView {
|
|||
|
||||
fn render_matches(&self, cx: &AppContext) -> ElementBox {
|
||||
if self.matches.is_empty() {
|
||||
let settings = cx.app_state::<Settings>();
|
||||
let settings = cx.global::<Settings>();
|
||||
return Container::new(
|
||||
Label::new(
|
||||
"No matches".into(),
|
||||
|
@ -277,7 +276,7 @@ impl ProjectSymbolsView {
|
|||
show_worktree_root_name: bool,
|
||||
cx: &AppContext,
|
||||
) -> ElementBox {
|
||||
let settings = cx.app_state::<Settings>();
|
||||
let settings = cx.global::<Settings>();
|
||||
let style = if index == self.selected_match_index {
|
||||
&settings.theme.selector.active_item
|
||||
} else {
|
||||
|
@ -329,7 +328,7 @@ impl ProjectSymbolsView {
|
|||
) {
|
||||
match event {
|
||||
editor::Event::Blurred => cx.emit(Event::Dismissed),
|
||||
editor::Event::Edited => self.update_matches(cx),
|
||||
editor::Event::BufferEdited { .. } => self.update_matches(cx),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
@ -346,6 +345,7 @@ impl ProjectSymbolsView {
|
|||
let buffer = workspace
|
||||
.project()
|
||||
.update(cx, |project, cx| project.open_buffer_for_symbol(symbol, cx));
|
||||
|
||||
let symbol = symbol.clone();
|
||||
cx.spawn(|workspace, mut cx| async move {
|
||||
let buffer = buffer.await?;
|
||||
|
@ -353,10 +353,8 @@ impl ProjectSymbolsView {
|
|||
let position = buffer
|
||||
.read(cx)
|
||||
.clip_point_utf16(symbol.range.start, Bias::Left);
|
||||
let editor = workspace
|
||||
.open_item(BufferItemHandle(buffer), cx)
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
|
||||
let editor = workspace.open_project_item::<Editor>(buffer, cx);
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.select_ranges(
|
||||
[position..position],
|
||||
|
|
|
@ -40,8 +40,9 @@ message Envelope {
|
|||
StartLanguageServer start_language_server = 33;
|
||||
UpdateLanguageServer update_language_server = 34;
|
||||
|
||||
OpenBuffer open_buffer = 35;
|
||||
OpenBufferResponse open_buffer_response = 36;
|
||||
OpenBufferById open_buffer_by_id = 35;
|
||||
OpenBufferByPath open_buffer_by_path = 36;
|
||||
OpenBufferResponse open_buffer_response = 37;
|
||||
UpdateBuffer update_buffer = 38;
|
||||
UpdateBufferFile update_buffer_file = 39;
|
||||
SaveBuffer save_buffer = 40;
|
||||
|
@ -79,6 +80,11 @@ message Envelope {
|
|||
|
||||
GetUsers get_users = 70;
|
||||
GetUsersResponse get_users_response = 71;
|
||||
|
||||
Follow follow = 72;
|
||||
FollowResponse follow_response = 73;
|
||||
UpdateFollowers update_followers = 74;
|
||||
Unfollow unfollow = 75;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -241,12 +247,17 @@ message OpenBufferForSymbolResponse {
|
|||
Buffer buffer = 1;
|
||||
}
|
||||
|
||||
message OpenBuffer {
|
||||
message OpenBufferByPath {
|
||||
uint64 project_id = 1;
|
||||
uint64 worktree_id = 2;
|
||||
string path = 3;
|
||||
}
|
||||
|
||||
message OpenBufferById {
|
||||
uint64 project_id = 1;
|
||||
uint64 id = 2;
|
||||
}
|
||||
|
||||
message OpenBufferResponse {
|
||||
Buffer buffer = 1;
|
||||
}
|
||||
|
@ -521,8 +532,77 @@ message UpdateContacts {
|
|||
repeated Contact contacts = 1;
|
||||
}
|
||||
|
||||
message UpdateDiagnostics {
|
||||
uint32 replica_id = 1;
|
||||
uint32 lamport_timestamp = 2;
|
||||
repeated Diagnostic diagnostics = 3;
|
||||
}
|
||||
|
||||
message Follow {
|
||||
uint64 project_id = 1;
|
||||
uint32 leader_id = 2;
|
||||
}
|
||||
|
||||
message FollowResponse {
|
||||
optional uint64 active_view_id = 1;
|
||||
repeated View views = 2;
|
||||
}
|
||||
|
||||
message UpdateFollowers {
|
||||
uint64 project_id = 1;
|
||||
repeated uint32 follower_ids = 2;
|
||||
oneof variant {
|
||||
UpdateActiveView update_active_view = 3;
|
||||
View create_view = 4;
|
||||
UpdateView update_view = 5;
|
||||
}
|
||||
}
|
||||
|
||||
message Unfollow {
|
||||
uint64 project_id = 1;
|
||||
uint32 leader_id = 2;
|
||||
}
|
||||
|
||||
// Entities
|
||||
|
||||
message UpdateActiveView {
|
||||
optional uint64 id = 1;
|
||||
optional uint32 leader_id = 2;
|
||||
}
|
||||
|
||||
message UpdateView {
|
||||
uint64 id = 1;
|
||||
optional uint32 leader_id = 2;
|
||||
|
||||
oneof variant {
|
||||
Editor editor = 3;
|
||||
}
|
||||
|
||||
message Editor {
|
||||
repeated Selection selections = 1;
|
||||
Anchor scroll_top_anchor = 2;
|
||||
float scroll_x = 3;
|
||||
float scroll_y = 4;
|
||||
}
|
||||
}
|
||||
|
||||
message View {
|
||||
uint64 id = 1;
|
||||
optional uint32 leader_id = 2;
|
||||
|
||||
oneof variant {
|
||||
Editor editor = 3;
|
||||
}
|
||||
|
||||
message Editor {
|
||||
uint64 buffer_id = 1;
|
||||
repeated Selection selections = 2;
|
||||
Anchor scroll_top_anchor = 3;
|
||||
float scroll_x = 4;
|
||||
float scroll_y = 5;
|
||||
}
|
||||
}
|
||||
|
||||
message Collaborator {
|
||||
uint32 peer_id = 1;
|
||||
uint32 replica_id = 2;
|
||||
|
@ -578,17 +658,6 @@ message BufferState {
|
|||
repeated string completion_triggers = 8;
|
||||
}
|
||||
|
||||
message BufferFragment {
|
||||
uint32 replica_id = 1;
|
||||
uint32 local_timestamp = 2;
|
||||
uint32 lamport_timestamp = 3;
|
||||
uint32 insertion_offset = 4;
|
||||
uint32 len = 5;
|
||||
bool visible = 6;
|
||||
repeated VectorClockEntry deletions = 7;
|
||||
repeated VectorClockEntry max_undos = 8;
|
||||
}
|
||||
|
||||
message SelectionSet {
|
||||
uint32 replica_id = 1;
|
||||
repeated Selection selections = 2;
|
||||
|
@ -614,12 +683,6 @@ enum Bias {
|
|||
Right = 1;
|
||||
}
|
||||
|
||||
message UpdateDiagnostics {
|
||||
uint32 replica_id = 1;
|
||||
uint32 lamport_timestamp = 2;
|
||||
repeated Diagnostic diagnostics = 3;
|
||||
}
|
||||
|
||||
message Diagnostic {
|
||||
Anchor start = 1;
|
||||
Anchor end = 2;
|
||||
|
|
|
@ -96,7 +96,7 @@ pub struct ConnectionState {
|
|||
|
||||
const KEEPALIVE_INTERVAL: Duration = Duration::from_secs(1);
|
||||
const WRITE_TIMEOUT: Duration = Duration::from_secs(2);
|
||||
const RECEIVE_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
pub const RECEIVE_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
impl Peer {
|
||||
pub fn new() -> Arc<Self> {
|
||||
|
|
|
@ -147,6 +147,8 @@ messages!(
|
|||
(BufferSaved, Foreground),
|
||||
(ChannelMessageSent, Foreground),
|
||||
(Error, Foreground),
|
||||
(Follow, Foreground),
|
||||
(FollowResponse, Foreground),
|
||||
(FormatBuffers, Foreground),
|
||||
(FormatBuffersResponse, Foreground),
|
||||
(GetChannelMessages, Foreground),
|
||||
|
@ -175,7 +177,8 @@ messages!(
|
|||
(UpdateLanguageServer, Foreground),
|
||||
(LeaveChannel, Foreground),
|
||||
(LeaveProject, Foreground),
|
||||
(OpenBuffer, Background),
|
||||
(OpenBufferById, Background),
|
||||
(OpenBufferByPath, Background),
|
||||
(OpenBufferForSymbol, Background),
|
||||
(OpenBufferForSymbolResponse, Background),
|
||||
(OpenBufferResponse, Background),
|
||||
|
@ -195,13 +198,15 @@ messages!(
|
|||
(SendChannelMessageResponse, Foreground),
|
||||
(ShareProject, Foreground),
|
||||
(Test, Foreground),
|
||||
(Unfollow, Foreground),
|
||||
(UnregisterProject, Foreground),
|
||||
(UnregisterWorktree, Foreground),
|
||||
(UnshareProject, Foreground),
|
||||
(UpdateBuffer, Background),
|
||||
(UpdateBuffer, Foreground),
|
||||
(UpdateBufferFile, Foreground),
|
||||
(UpdateContacts, Foreground),
|
||||
(UpdateDiagnosticSummary, Foreground),
|
||||
(UpdateFollowers, Foreground),
|
||||
(UpdateWorktree, Foreground),
|
||||
);
|
||||
|
||||
|
@ -211,6 +216,7 @@ request_messages!(
|
|||
ApplyCompletionAdditionalEdits,
|
||||
ApplyCompletionAdditionalEditsResponse
|
||||
),
|
||||
(Follow, FollowResponse),
|
||||
(FormatBuffers, FormatBuffersResponse),
|
||||
(GetChannelMessages, GetChannelMessagesResponse),
|
||||
(GetChannels, GetChannelsResponse),
|
||||
|
@ -223,7 +229,8 @@ request_messages!(
|
|||
(GetUsers, GetUsersResponse),
|
||||
(JoinChannel, JoinChannelResponse),
|
||||
(JoinProject, JoinProjectResponse),
|
||||
(OpenBuffer, OpenBufferResponse),
|
||||
(OpenBufferById, OpenBufferResponse),
|
||||
(OpenBufferByPath, OpenBufferResponse),
|
||||
(OpenBufferForSymbol, OpenBufferForSymbolResponse),
|
||||
(Ping, Ack),
|
||||
(PerformRename, PerformRenameResponse),
|
||||
|
@ -246,6 +253,7 @@ entity_messages!(
|
|||
ApplyCompletionAdditionalEdits,
|
||||
BufferReloaded,
|
||||
BufferSaved,
|
||||
Follow,
|
||||
FormatBuffers,
|
||||
GetCodeActions,
|
||||
GetCompletions,
|
||||
|
@ -255,7 +263,8 @@ entity_messages!(
|
|||
GetProjectSymbols,
|
||||
JoinProject,
|
||||
LeaveProject,
|
||||
OpenBuffer,
|
||||
OpenBufferById,
|
||||
OpenBufferByPath,
|
||||
OpenBufferForSymbol,
|
||||
PerformRename,
|
||||
PrepareRename,
|
||||
|
@ -263,11 +272,13 @@ entity_messages!(
|
|||
SaveBuffer,
|
||||
SearchProject,
|
||||
StartLanguageServer,
|
||||
Unfollow,
|
||||
UnregisterWorktree,
|
||||
UnshareProject,
|
||||
UpdateBuffer,
|
||||
UpdateBufferFile,
|
||||
UpdateDiagnosticSummary,
|
||||
UpdateFollowers,
|
||||
UpdateLanguageServer,
|
||||
RegisterWorktree,
|
||||
UpdateWorktree,
|
||||
|
|
|
@ -5,4 +5,4 @@ pub mod proto;
|
|||
pub use conn::Connection;
|
||||
pub use peer::*;
|
||||
|
||||
pub const PROTOCOL_VERSION: u32 = 11;
|
||||
pub const PROTOCOL_VERSION: u32 = 12;
|
||||
|
|
|
@ -8,7 +8,7 @@ use gpui::{
|
|||
use language::OffsetRangeExt;
|
||||
use project::search::SearchQuery;
|
||||
use std::ops::Range;
|
||||
use workspace::{ItemViewHandle, Pane, Settings, Toolbar, Workspace};
|
||||
use workspace::{ItemHandle, Pane, Settings, Toolbar, Workspace};
|
||||
|
||||
action!(Deploy, bool);
|
||||
action!(Dismiss);
|
||||
|
@ -66,7 +66,7 @@ impl View for SearchBar {
|
|||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let theme = cx.app_state::<Settings>().theme.clone();
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
let editor_container = if self.query_contains_error {
|
||||
theme.search.invalid_editor
|
||||
} else {
|
||||
|
@ -126,7 +126,7 @@ impl View for SearchBar {
|
|||
impl Toolbar for SearchBar {
|
||||
fn active_item_changed(
|
||||
&mut self,
|
||||
item: Option<Box<dyn ItemViewHandle>>,
|
||||
item: Option<Box<dyn ItemHandle>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> bool {
|
||||
self.active_editor_subscription.take();
|
||||
|
@ -197,7 +197,7 @@ impl SearchBar {
|
|||
) -> ElementBox {
|
||||
let is_active = self.is_search_option_enabled(search_option);
|
||||
MouseEventHandler::new::<Self, _, _>(search_option as usize, cx, |state, cx| {
|
||||
let theme = &cx.app_state::<Settings>().theme.search;
|
||||
let theme = &cx.global::<Settings>().theme.search;
|
||||
let style = match (is_active, state.hovered) {
|
||||
(false, false) => &theme.option_button,
|
||||
(false, true) => &theme.hovered_option_button,
|
||||
|
@ -222,7 +222,7 @@ impl SearchBar {
|
|||
) -> ElementBox {
|
||||
enum NavButton {}
|
||||
MouseEventHandler::new::<NavButton, _, _>(direction as usize, cx, |state, cx| {
|
||||
let theme = &cx.app_state::<Settings>().theme.search;
|
||||
let theme = &cx.global::<Settings>().theme.search;
|
||||
let style = if state.hovered {
|
||||
&theme.hovered_option_button
|
||||
} else {
|
||||
|
@ -336,11 +336,9 @@ impl SearchBar {
|
|||
direction,
|
||||
&editor.buffer().read(cx).read(cx),
|
||||
);
|
||||
editor.select_ranges(
|
||||
[ranges[new_index].clone()],
|
||||
Some(Autoscroll::Fit),
|
||||
cx,
|
||||
);
|
||||
let range_to_select = ranges[new_index].clone();
|
||||
editor.unfold_ranges([range_to_select.clone()], false, cx);
|
||||
editor.select_ranges([range_to_select], Some(Autoscroll::Fit), cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -360,7 +358,7 @@ impl SearchBar {
|
|||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
editor::Event::Edited => {
|
||||
editor::Event::BufferEdited { .. } => {
|
||||
self.query_contains_error = false;
|
||||
self.clear_matches(cx);
|
||||
self.update_matches(true, cx);
|
||||
|
@ -377,8 +375,8 @@ impl SearchBar {
|
|||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
editor::Event::Edited => self.update_matches(false, cx),
|
||||
editor::Event::SelectionsChanged => self.update_match_index(cx),
|
||||
editor::Event::BufferEdited { .. } => self.update_matches(false, cx),
|
||||
editor::Event::SelectionsChanged { .. } => self.update_match_index(cx),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
@ -475,7 +473,7 @@ impl SearchBar {
|
|||
}
|
||||
}
|
||||
|
||||
let theme = &cx.app_state::<Settings>().theme.search;
|
||||
let theme = &cx.global::<Settings>().theme.search;
|
||||
editor.highlight_background::<Self>(
|
||||
ranges,
|
||||
theme.match_background,
|
||||
|
@ -510,8 +508,9 @@ impl SearchBar {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use editor::{DisplayPoint, Editor, MultiBuffer};
|
||||
use editor::{DisplayPoint, Editor};
|
||||
use gpui::{color::Color, TestAppContext};
|
||||
use language::Buffer;
|
||||
use std::sync::Arc;
|
||||
use unindent::Unindent as _;
|
||||
|
||||
|
@ -521,11 +520,12 @@ mod tests {
|
|||
let mut theme = gpui::fonts::with_font_cache(fonts.clone(), || theme::Theme::default());
|
||||
theme.search.match_background = Color::red();
|
||||
let settings = Settings::new("Courier", &fonts, Arc::new(theme)).unwrap();
|
||||
cx.update(|cx| cx.add_app_state(settings));
|
||||
cx.update(|cx| cx.set_global(settings));
|
||||
|
||||
let buffer = cx.update(|cx| {
|
||||
MultiBuffer::build_simple(
|
||||
&r#"
|
||||
let buffer = cx.add_model(|cx| {
|
||||
Buffer::new(
|
||||
0,
|
||||
r#"
|
||||
A regular expression (shortened as regex or regexp;[1] also referred to as
|
||||
rational expression[2][3]) is a sequence of characters that specifies a search
|
||||
pattern in text. Usually such patterns are used by string-searching algorithms
|
||||
|
|
|
@ -7,7 +7,7 @@ use editor::{Anchor, Autoscroll, Editor, MultiBuffer, SelectAll};
|
|||
use gpui::{
|
||||
action, elements::*, keymap::Binding, platform::CursorStyle, AppContext, ElementBox, Entity,
|
||||
ModelContext, ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext,
|
||||
ViewHandle, WeakModelHandle,
|
||||
ViewHandle, WeakModelHandle, WeakViewHandle,
|
||||
};
|
||||
use project::{search::SearchQuery, Project};
|
||||
use std::{
|
||||
|
@ -16,7 +16,7 @@ use std::{
|
|||
path::PathBuf,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use workspace::{Item, ItemHandle, ItemNavHistory, ItemView, Settings, Workspace};
|
||||
use workspace::{Item, ItemNavHistory, Settings, Workspace};
|
||||
|
||||
action!(Deploy);
|
||||
action!(Search);
|
||||
|
@ -26,10 +26,10 @@ action!(ToggleFocus);
|
|||
const MAX_TAB_TITLE_LEN: usize = 24;
|
||||
|
||||
#[derive(Default)]
|
||||
struct ActiveSearches(HashMap<WeakModelHandle<Project>, WeakModelHandle<ProjectSearch>>);
|
||||
struct ActiveSearches(HashMap<WeakModelHandle<Project>, WeakViewHandle<ProjectSearchView>>);
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_app_state(ActiveSearches::default());
|
||||
cx.set_global(ActiveSearches::default());
|
||||
cx.add_bindings([
|
||||
Binding::new("cmd-shift-F", ToggleFocus, Some("ProjectSearchView")),
|
||||
Binding::new("cmd-f", ToggleFocus, Some("ProjectSearchView")),
|
||||
|
@ -139,23 +139,6 @@ impl ProjectSearch {
|
|||
}
|
||||
}
|
||||
|
||||
impl Item for ProjectSearch {
|
||||
type View = ProjectSearchView;
|
||||
|
||||
fn build_view(
|
||||
model: ModelHandle<Self>,
|
||||
_: &Workspace,
|
||||
nav_history: ItemNavHistory,
|
||||
cx: &mut gpui::ViewContext<Self::View>,
|
||||
) -> Self::View {
|
||||
ProjectSearchView::new(model, Some(nav_history), cx)
|
||||
}
|
||||
|
||||
fn project_path(&self) -> Option<project::ProjectPath> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
enum ViewEvent {
|
||||
UpdateTab,
|
||||
}
|
||||
|
@ -172,7 +155,7 @@ impl View for ProjectSearchView {
|
|||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let model = &self.model.read(cx);
|
||||
let results = if model.match_ranges.is_empty() {
|
||||
let theme = &cx.app_state::<Settings>().theme;
|
||||
let theme = &cx.global::<Settings>().theme;
|
||||
let text = if self.query_editor.read(cx).text(cx).is_empty() {
|
||||
""
|
||||
} else if model.pending_search.is_some() {
|
||||
|
@ -199,11 +182,11 @@ impl View for ProjectSearchView {
|
|||
}
|
||||
|
||||
fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
|
||||
cx.update_app_state(|state: &mut ActiveSearches, cx| {
|
||||
state.0.insert(
|
||||
self.model.read(cx).project.downgrade(),
|
||||
self.model.downgrade(),
|
||||
)
|
||||
let handle = cx.weak_handle();
|
||||
cx.update_global(|state: &mut ActiveSearches, cx| {
|
||||
state
|
||||
.0
|
||||
.insert(self.model.read(cx).project.downgrade(), handle)
|
||||
});
|
||||
|
||||
if self.model.read(cx).match_ranges.is_empty() {
|
||||
|
@ -214,7 +197,7 @@ impl View for ProjectSearchView {
|
|||
}
|
||||
}
|
||||
|
||||
impl ItemView for ProjectSearchView {
|
||||
impl Item for ProjectSearchView {
|
||||
fn act_as_type(
|
||||
&self,
|
||||
type_id: TypeId,
|
||||
|
@ -235,12 +218,8 @@ impl ItemView for ProjectSearchView {
|
|||
.update(cx, |editor, cx| editor.deactivated(cx));
|
||||
}
|
||||
|
||||
fn item(&self, _: &gpui::AppContext) -> Box<dyn ItemHandle> {
|
||||
Box::new(self.model.clone())
|
||||
}
|
||||
|
||||
fn tab_content(&self, tab_theme: &theme::Tab, cx: &gpui::AppContext) -> ElementBox {
|
||||
let settings = cx.app_state::<Settings>();
|
||||
let settings = cx.global::<Settings>();
|
||||
let search_theme = &settings.theme.search;
|
||||
Flex::row()
|
||||
.with_child(
|
||||
|
@ -271,6 +250,10 @@ impl ItemView for ProjectSearchView {
|
|||
None
|
||||
}
|
||||
|
||||
fn project_entry_id(&self, _: &AppContext) -> Option<project::ProjectEntryId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn can_save(&self, _: &gpui::AppContext) -> bool {
|
||||
true
|
||||
}
|
||||
|
@ -305,21 +288,23 @@ impl ItemView for ProjectSearchView {
|
|||
unreachable!("save_as should not have been called")
|
||||
}
|
||||
|
||||
fn clone_on_split(
|
||||
&self,
|
||||
nav_history: ItemNavHistory,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<Self>
|
||||
fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let model = self.model.update(cx, |model, cx| model.clone(cx));
|
||||
Some(Self::new(model, Some(nav_history), cx))
|
||||
Some(Self::new(model, cx))
|
||||
}
|
||||
|
||||
fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) {
|
||||
fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {
|
||||
self.results_editor.update(cx, |editor, _| {
|
||||
editor.set_nav_history(Some(nav_history));
|
||||
});
|
||||
}
|
||||
|
||||
fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
|
||||
self.results_editor
|
||||
.update(cx, |editor, cx| editor.navigate(data, cx));
|
||||
.update(cx, |editor, cx| editor.navigate(data, cx))
|
||||
}
|
||||
|
||||
fn should_update_tab_on_event(event: &ViewEvent) -> bool {
|
||||
|
@ -328,11 +313,7 @@ impl ItemView for ProjectSearchView {
|
|||
}
|
||||
|
||||
impl ProjectSearchView {
|
||||
fn new(
|
||||
model: ModelHandle<ProjectSearch>,
|
||||
nav_history: Option<ItemNavHistory>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
fn new(model: ModelHandle<ProjectSearch>, cx: &mut ViewContext<Self>) -> Self {
|
||||
let project;
|
||||
let excerpts;
|
||||
let mut query_text = String::new();
|
||||
|
@ -362,15 +343,14 @@ impl ProjectSearchView {
|
|||
});
|
||||
|
||||
let results_editor = cx.add_view(|cx| {
|
||||
let mut editor = Editor::for_buffer(excerpts, Some(project), cx);
|
||||
let mut editor = Editor::for_multibuffer(excerpts, Some(project), cx);
|
||||
editor.set_searchable(false);
|
||||
editor.set_nav_history(nav_history);
|
||||
editor
|
||||
});
|
||||
cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab))
|
||||
.detach();
|
||||
cx.subscribe(&results_editor, |this, _, event, cx| {
|
||||
if matches!(event, editor::Event::SelectionsChanged) {
|
||||
if matches!(event, editor::Event::SelectionsChanged { .. }) {
|
||||
this.update_match_index(cx);
|
||||
}
|
||||
})
|
||||
|
@ -394,28 +374,31 @@ impl ProjectSearchView {
|
|||
// If no search exists in the workspace, create a new one.
|
||||
fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
|
||||
// Clean up entries for dropped projects
|
||||
cx.update_app_state(|state: &mut ActiveSearches, cx| {
|
||||
cx.update_global(|state: &mut ActiveSearches, cx| {
|
||||
state.0.retain(|project, _| project.is_upgradable(cx))
|
||||
});
|
||||
|
||||
let active_search = cx
|
||||
.app_state::<ActiveSearches>()
|
||||
.global::<ActiveSearches>()
|
||||
.0
|
||||
.get(&workspace.project().downgrade());
|
||||
|
||||
let existing = active_search
|
||||
.and_then(|active_search| {
|
||||
workspace
|
||||
.items_of_type::<ProjectSearch>(cx)
|
||||
.items_of_type::<ProjectSearchView>(cx)
|
||||
.find(|search| search == active_search)
|
||||
})
|
||||
.or_else(|| workspace.item_of_type::<ProjectSearch>(cx));
|
||||
.or_else(|| workspace.item_of_type::<ProjectSearchView>(cx));
|
||||
|
||||
if let Some(existing) = existing {
|
||||
workspace.activate_item(&existing, cx);
|
||||
} else {
|
||||
let model = cx.add_model(|cx| ProjectSearch::new(workspace.project().clone(), cx));
|
||||
workspace.open_item(model, cx);
|
||||
workspace.add_item(
|
||||
Box::new(cx.add_view(|cx| ProjectSearchView::new(model, cx))),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -450,7 +433,10 @@ impl ProjectSearchView {
|
|||
model.search(new_query, cx);
|
||||
model
|
||||
});
|
||||
workspace.open_item(model, cx);
|
||||
workspace.add_item(
|
||||
Box::new(cx.add_view(|cx| ProjectSearchView::new(model, cx))),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -503,6 +489,7 @@ impl ProjectSearchView {
|
|||
);
|
||||
let range_to_select = model.match_ranges[new_index].clone();
|
||||
self.results_editor.update(cx, |editor, cx| {
|
||||
editor.unfold_ranges([range_to_select.clone()], false, cx);
|
||||
editor.select_ranges([range_to_select], Some(Autoscroll::Fit), cx);
|
||||
});
|
||||
}
|
||||
|
@ -552,7 +539,7 @@ impl ProjectSearchView {
|
|||
if reset_selections {
|
||||
editor.select_ranges(match_ranges.first().cloned(), Some(Autoscroll::Fit), cx);
|
||||
}
|
||||
let theme = &cx.app_state::<Settings>().theme.search;
|
||||
let theme = &cx.global::<Settings>().theme.search;
|
||||
editor.highlight_background::<Self>(match_ranges, theme.match_background, cx);
|
||||
});
|
||||
if self.query_editor.is_focused(cx) {
|
||||
|
@ -578,7 +565,7 @@ impl ProjectSearchView {
|
|||
}
|
||||
|
||||
fn render_query_editor(&self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let theme = cx.app_state::<Settings>().theme.clone();
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
let editor_container = if self.query_contains_error {
|
||||
theme.search.invalid_editor
|
||||
} else {
|
||||
|
@ -642,7 +629,7 @@ impl ProjectSearchView {
|
|||
) -> ElementBox {
|
||||
let is_active = self.is_option_enabled(option);
|
||||
MouseEventHandler::new::<Self, _, _>(option as usize, cx, |state, cx| {
|
||||
let theme = &cx.app_state::<Settings>().theme.search;
|
||||
let theme = &cx.global::<Settings>().theme.search;
|
||||
let style = match (is_active, state.hovered) {
|
||||
(false, false) => &theme.option_button,
|
||||
(false, true) => &theme.hovered_option_button,
|
||||
|
@ -675,7 +662,7 @@ impl ProjectSearchView {
|
|||
) -> ElementBox {
|
||||
enum NavButton {}
|
||||
MouseEventHandler::new::<NavButton, _, _>(direction as usize, cx, |state, cx| {
|
||||
let theme = &cx.app_state::<Settings>().theme.search;
|
||||
let theme = &cx.global::<Settings>().theme.search;
|
||||
let style = if state.hovered {
|
||||
&theme.hovered_option_button
|
||||
} else {
|
||||
|
@ -707,7 +694,7 @@ mod tests {
|
|||
let mut theme = gpui::fonts::with_font_cache(fonts.clone(), || theme::Theme::default());
|
||||
theme.search.match_background = Color::red();
|
||||
let settings = Settings::new("Courier", &fonts, Arc::new(theme)).unwrap();
|
||||
cx.update(|cx| cx.add_app_state(settings));
|
||||
cx.update(|cx| cx.set_global(settings));
|
||||
|
||||
let fs = FakeFs::new(cx.background());
|
||||
fs.insert_tree(
|
||||
|
@ -732,7 +719,7 @@ mod tests {
|
|||
|
||||
let search = cx.add_model(|cx| ProjectSearch::new(project, cx));
|
||||
let search_view = cx.add_view(Default::default(), |cx| {
|
||||
ProjectSearchView::new(search.clone(), None, cx)
|
||||
ProjectSearchView::new(search.clone(), cx)
|
||||
});
|
||||
|
||||
search_view.update(cx, |search_view, cx| {
|
||||
|
|
|
@ -39,9 +39,9 @@ pub(crate) fn active_match_index(
|
|||
None
|
||||
} else {
|
||||
match ranges.binary_search_by(|probe| {
|
||||
if probe.end.cmp(&cursor, &*buffer).unwrap().is_lt() {
|
||||
if probe.end.cmp(&cursor, &*buffer).is_lt() {
|
||||
Ordering::Less
|
||||
} else if probe.start.cmp(&cursor, &*buffer).unwrap().is_gt() {
|
||||
} else if probe.start.cmp(&cursor, &*buffer).is_gt() {
|
||||
Ordering::Greater
|
||||
} else {
|
||||
Ordering::Equal
|
||||
|
@ -59,7 +59,7 @@ pub(crate) fn match_index_for_direction(
|
|||
direction: Direction,
|
||||
buffer: &MultiBufferSnapshot,
|
||||
) -> usize {
|
||||
if ranges[index].start.cmp(&cursor, &buffer).unwrap().is_gt() {
|
||||
if ranges[index].start.cmp(&cursor, &buffer).is_gt() {
|
||||
if direction == Direction::Prev {
|
||||
if index == 0 {
|
||||
index = ranges.len() - 1;
|
||||
|
@ -67,7 +67,7 @@ pub(crate) fn match_index_for_direction(
|
|||
index -= 1;
|
||||
}
|
||||
}
|
||||
} else if ranges[index].end.cmp(&cursor, &buffer).unwrap().is_lt() {
|
||||
} else if ranges[index].end.cmp(&cursor, &buffer).is_lt() {
|
||||
if direction == Direction::Next {
|
||||
index = 0;
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -12,23 +12,19 @@ pub struct Anchor {
|
|||
}
|
||||
|
||||
impl Anchor {
|
||||
pub fn min() -> Self {
|
||||
Self {
|
||||
pub const MIN: Self = Self {
|
||||
timestamp: clock::Local::MIN,
|
||||
offset: usize::MIN,
|
||||
bias: Bias::Left,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub fn max() -> Self {
|
||||
Self {
|
||||
pub const MAX: Self = Self {
|
||||
timestamp: clock::Local::MAX,
|
||||
offset: usize::MAX,
|
||||
bias: Bias::Right,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub fn cmp(&self, other: &Anchor, buffer: &BufferSnapshot) -> Result<Ordering> {
|
||||
pub fn cmp(&self, other: &Anchor, buffer: &BufferSnapshot) -> Ordering {
|
||||
let fragment_id_comparison = if self.timestamp == other.timestamp {
|
||||
Ordering::Equal
|
||||
} else {
|
||||
|
@ -37,9 +33,25 @@ impl Anchor {
|
|||
.cmp(&buffer.fragment_id_for_anchor(other))
|
||||
};
|
||||
|
||||
Ok(fragment_id_comparison
|
||||
fragment_id_comparison
|
||||
.then_with(|| self.offset.cmp(&other.offset))
|
||||
.then_with(|| self.bias.cmp(&other.bias)))
|
||||
.then_with(|| self.bias.cmp(&other.bias))
|
||||
}
|
||||
|
||||
pub fn min(&self, other: &Self, buffer: &BufferSnapshot) -> Self {
|
||||
if self.cmp(other, buffer).is_le() {
|
||||
self.clone()
|
||||
} else {
|
||||
other.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn max(&self, other: &Self, buffer: &BufferSnapshot) -> Self {
|
||||
if self.cmp(other, buffer).is_ge() {
|
||||
self.clone()
|
||||
} else {
|
||||
other.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bias(&self, bias: Bias, buffer: &BufferSnapshot) -> Anchor {
|
||||
|
@ -105,8 +117,8 @@ pub trait AnchorRangeExt {
|
|||
|
||||
impl AnchorRangeExt for Range<Anchor> {
|
||||
fn cmp(&self, other: &Range<Anchor>, buffer: &BufferSnapshot) -> Result<Ordering> {
|
||||
Ok(match self.start.cmp(&other.start, buffer)? {
|
||||
Ordering::Equal => other.end.cmp(&self.end, buffer)?,
|
||||
Ok(match self.start.cmp(&other.start, buffer) {
|
||||
Ordering::Equal => other.end.cmp(&self.end, buffer),
|
||||
ord @ _ => ord,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -199,6 +199,28 @@ where
|
|||
self.0.push(edit);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn old_to_new(&self, old: T) -> T {
|
||||
let ix = match self.0.binary_search_by(|probe| probe.old.start.cmp(&old)) {
|
||||
Ok(ix) => ix,
|
||||
Err(ix) => {
|
||||
if ix == 0 {
|
||||
return old;
|
||||
} else {
|
||||
ix - 1
|
||||
}
|
||||
}
|
||||
};
|
||||
if let Some(edit) = self.0.get(ix) {
|
||||
if old >= edit.old.end {
|
||||
edit.new.end + (old - edit.old.end)
|
||||
} else {
|
||||
edit.new.start
|
||||
}
|
||||
} else {
|
||||
old
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> IntoIterator for Patch<T> {
|
||||
|
@ -399,26 +421,6 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
// #[test]
|
||||
// fn test_compose_edits() {
|
||||
// assert_eq!(
|
||||
// compose_edits(
|
||||
// &Edit {
|
||||
// old: 3..3,
|
||||
// new: 3..6,
|
||||
// },
|
||||
// &Edit {
|
||||
// old: 2..7,
|
||||
// new: 2..4,
|
||||
// },
|
||||
// ),
|
||||
// Edit {
|
||||
// old: 2..4,
|
||||
// new: 2..4
|
||||
// }
|
||||
// );
|
||||
// }
|
||||
|
||||
#[gpui::test]
|
||||
fn test_two_new_edits_touching_one_old_edit() {
|
||||
assert_patch_composition(
|
||||
|
@ -455,6 +457,30 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_old_to_new() {
|
||||
let patch = Patch(vec![
|
||||
Edit {
|
||||
old: 2..4,
|
||||
new: 2..4,
|
||||
},
|
||||
Edit {
|
||||
old: 7..8,
|
||||
new: 7..11,
|
||||
},
|
||||
]);
|
||||
assert_eq!(patch.old_to_new(0), 0);
|
||||
assert_eq!(patch.old_to_new(1), 1);
|
||||
assert_eq!(patch.old_to_new(2), 2);
|
||||
assert_eq!(patch.old_to_new(3), 2);
|
||||
assert_eq!(patch.old_to_new(4), 4);
|
||||
assert_eq!(patch.old_to_new(5), 5);
|
||||
assert_eq!(patch.old_to_new(6), 6);
|
||||
assert_eq!(patch.old_to_new(7), 7);
|
||||
assert_eq!(patch.old_to_new(8), 11);
|
||||
assert_eq!(patch.old_to_new(9), 12);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_random_patch_compositions(mut rng: StdRng) {
|
||||
let operations = env::var("OPERATIONS")
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::Anchor;
|
||||
use crate::{rope::TextDimension, BufferSnapshot, ToOffset, ToPoint};
|
||||
use crate::{rope::TextDimension, BufferSnapshot};
|
||||
use std::cmp::Ordering;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
|
@ -18,6 +18,12 @@ pub struct Selection<T> {
|
|||
pub goal: SelectionGoal,
|
||||
}
|
||||
|
||||
impl Default for SelectionGoal {
|
||||
fn default() -> Self {
|
||||
Self::None
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> Selection<T> {
|
||||
pub fn head(&self) -> T {
|
||||
if self.reversed {
|
||||
|
@ -34,14 +40,27 @@ impl<T: Clone> Selection<T> {
|
|||
self.start.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn map<F, S>(&self, f: F) -> Selection<S>
|
||||
where
|
||||
F: Fn(T) -> S,
|
||||
{
|
||||
Selection::<S> {
|
||||
id: self.id,
|
||||
start: f(self.start.clone()),
|
||||
end: f(self.end.clone()),
|
||||
reversed: self.reversed,
|
||||
goal: self.goal,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ToOffset + ToPoint + Copy + Ord> Selection<T> {
|
||||
impl<T: Copy + Ord> Selection<T> {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.start == self.end
|
||||
}
|
||||
|
||||
pub fn set_head(&mut self, head: T) {
|
||||
pub fn set_head(&mut self, head: T, new_goal: SelectionGoal) {
|
||||
if head.cmp(&self.tail()) < Ordering::Equal {
|
||||
if !self.reversed {
|
||||
self.end = self.start;
|
||||
|
@ -55,6 +74,14 @@ impl<T: ToOffset + ToPoint + Copy + Ord> Selection<T> {
|
|||
}
|
||||
self.end = head;
|
||||
}
|
||||
self.goal = new_goal;
|
||||
}
|
||||
|
||||
pub fn collapse_to(&mut self, point: T, new_goal: SelectionGoal) {
|
||||
self.start = point;
|
||||
self.end = point;
|
||||
self.goal = new_goal;
|
||||
self.reversed = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -340,59 +340,41 @@ fn test_anchors() {
|
|||
let anchor_at_offset_2 = buffer.anchor_before(2);
|
||||
|
||||
assert_eq!(
|
||||
anchor_at_offset_0
|
||||
.cmp(&anchor_at_offset_0, &buffer)
|
||||
.unwrap(),
|
||||
anchor_at_offset_0.cmp(&anchor_at_offset_0, &buffer),
|
||||
Ordering::Equal
|
||||
);
|
||||
assert_eq!(
|
||||
anchor_at_offset_1
|
||||
.cmp(&anchor_at_offset_1, &buffer)
|
||||
.unwrap(),
|
||||
anchor_at_offset_1.cmp(&anchor_at_offset_1, &buffer),
|
||||
Ordering::Equal
|
||||
);
|
||||
assert_eq!(
|
||||
anchor_at_offset_2
|
||||
.cmp(&anchor_at_offset_2, &buffer)
|
||||
.unwrap(),
|
||||
anchor_at_offset_2.cmp(&anchor_at_offset_2, &buffer),
|
||||
Ordering::Equal
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
anchor_at_offset_0
|
||||
.cmp(&anchor_at_offset_1, &buffer)
|
||||
.unwrap(),
|
||||
anchor_at_offset_0.cmp(&anchor_at_offset_1, &buffer),
|
||||
Ordering::Less
|
||||
);
|
||||
assert_eq!(
|
||||
anchor_at_offset_1
|
||||
.cmp(&anchor_at_offset_2, &buffer)
|
||||
.unwrap(),
|
||||
anchor_at_offset_1.cmp(&anchor_at_offset_2, &buffer),
|
||||
Ordering::Less
|
||||
);
|
||||
assert_eq!(
|
||||
anchor_at_offset_0
|
||||
.cmp(&anchor_at_offset_2, &buffer)
|
||||
.unwrap(),
|
||||
anchor_at_offset_0.cmp(&anchor_at_offset_2, &buffer),
|
||||
Ordering::Less
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
anchor_at_offset_1
|
||||
.cmp(&anchor_at_offset_0, &buffer)
|
||||
.unwrap(),
|
||||
anchor_at_offset_1.cmp(&anchor_at_offset_0, &buffer),
|
||||
Ordering::Greater
|
||||
);
|
||||
assert_eq!(
|
||||
anchor_at_offset_2
|
||||
.cmp(&anchor_at_offset_1, &buffer)
|
||||
.unwrap(),
|
||||
anchor_at_offset_2.cmp(&anchor_at_offset_1, &buffer),
|
||||
Ordering::Greater
|
||||
);
|
||||
assert_eq!(
|
||||
anchor_at_offset_2
|
||||
.cmp(&anchor_at_offset_0, &buffer)
|
||||
.unwrap(),
|
||||
anchor_at_offset_2.cmp(&anchor_at_offset_0, &buffer),
|
||||
Ordering::Greater
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1318,8 +1318,8 @@ impl Buffer {
|
|||
let mut futures = Vec::new();
|
||||
for anchor in anchors {
|
||||
if !self.version.observed(anchor.timestamp)
|
||||
&& *anchor != Anchor::max()
|
||||
&& *anchor != Anchor::min()
|
||||
&& *anchor != Anchor::MAX
|
||||
&& *anchor != Anchor::MIN
|
||||
{
|
||||
let (tx, rx) = oneshot::channel();
|
||||
self.edit_id_resolvers
|
||||
|
@ -1638,9 +1638,9 @@ impl BufferSnapshot {
|
|||
let mut position = D::default();
|
||||
|
||||
anchors.map(move |anchor| {
|
||||
if *anchor == Anchor::min() {
|
||||
if *anchor == Anchor::MIN {
|
||||
return D::default();
|
||||
} else if *anchor == Anchor::max() {
|
||||
} else if *anchor == Anchor::MAX {
|
||||
return D::from_text_summary(&self.visible_text.summary());
|
||||
}
|
||||
|
||||
|
@ -1680,9 +1680,9 @@ impl BufferSnapshot {
|
|||
where
|
||||
D: TextDimension,
|
||||
{
|
||||
if *anchor == Anchor::min() {
|
||||
if *anchor == Anchor::MIN {
|
||||
D::default()
|
||||
} else if *anchor == Anchor::max() {
|
||||
} else if *anchor == Anchor::MAX {
|
||||
D::from_text_summary(&self.visible_text.summary())
|
||||
} else {
|
||||
let anchor_key = InsertionFragmentKey {
|
||||
|
@ -1718,9 +1718,9 @@ impl BufferSnapshot {
|
|||
}
|
||||
|
||||
fn fragment_id_for_anchor(&self, anchor: &Anchor) -> &Locator {
|
||||
if *anchor == Anchor::min() {
|
||||
if *anchor == Anchor::MIN {
|
||||
&locator::MIN
|
||||
} else if *anchor == Anchor::max() {
|
||||
} else if *anchor == Anchor::MAX {
|
||||
&locator::MAX
|
||||
} else {
|
||||
let anchor_key = InsertionFragmentKey {
|
||||
|
@ -1758,9 +1758,9 @@ impl BufferSnapshot {
|
|||
pub fn anchor_at<T: ToOffset>(&self, position: T, bias: Bias) -> Anchor {
|
||||
let offset = position.to_offset(self);
|
||||
if bias == Bias::Left && offset == 0 {
|
||||
Anchor::min()
|
||||
Anchor::MIN
|
||||
} else if bias == Bias::Right && offset == self.len() {
|
||||
Anchor::max()
|
||||
Anchor::MAX
|
||||
} else {
|
||||
let mut fragment_cursor = self.fragments.cursor::<usize>();
|
||||
fragment_cursor.seek(&offset, bias, &None);
|
||||
|
@ -1775,9 +1775,7 @@ impl BufferSnapshot {
|
|||
}
|
||||
|
||||
pub fn can_resolve(&self, anchor: &Anchor) -> bool {
|
||||
*anchor == Anchor::min()
|
||||
|| *anchor == Anchor::max()
|
||||
|| self.version.observed(anchor.timestamp)
|
||||
*anchor == Anchor::MIN || *anchor == Anchor::MAX || self.version.observed(anchor.timestamp)
|
||||
}
|
||||
|
||||
pub fn clip_offset(&self, offset: usize, bias: Bias) -> usize {
|
||||
|
@ -1799,7 +1797,7 @@ impl BufferSnapshot {
|
|||
where
|
||||
D: TextDimension + Ord,
|
||||
{
|
||||
self.edits_since_in_range(since, Anchor::min()..Anchor::max())
|
||||
self.edits_since_in_range(since, Anchor::MIN..Anchor::MAX)
|
||||
}
|
||||
|
||||
pub fn edited_ranges_for_transaction<'a, D>(
|
||||
|
|
|
@ -35,6 +35,8 @@ pub struct Workspace {
|
|||
pub tab: Tab,
|
||||
pub active_tab: Tab,
|
||||
pub pane_divider: Border,
|
||||
pub leader_border_opacity: f32,
|
||||
pub leader_border_width: f32,
|
||||
pub left_sidebar: Sidebar,
|
||||
pub right_sidebar: Sidebar,
|
||||
pub status_bar: StatusBar,
|
||||
|
|
|
@ -54,7 +54,7 @@ impl ThemeSelector {
|
|||
cx.subscribe(&query_editor, Self::on_query_editor_event)
|
||||
.detach();
|
||||
|
||||
let original_theme = cx.app_state::<Settings>().theme.clone();
|
||||
let original_theme = cx.global::<Settings>().theme.clone();
|
||||
|
||||
let mut this = Self {
|
||||
themes: registry,
|
||||
|
@ -82,7 +82,7 @@ impl ThemeSelector {
|
|||
}
|
||||
|
||||
fn reload(_: &mut Workspace, action: &Reload, cx: &mut ViewContext<Workspace>) {
|
||||
let current_theme_name = cx.app_state::<Settings>().theme.name.clone();
|
||||
let current_theme_name = cx.global::<Settings>().theme.name.clone();
|
||||
action.0.clear();
|
||||
match action.0.get(¤t_theme_name) {
|
||||
Ok(theme) => {
|
||||
|
@ -204,9 +204,9 @@ impl ThemeSelector {
|
|||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
editor::Event::Edited => {
|
||||
editor::Event::BufferEdited { .. } => {
|
||||
self.update_matches(cx);
|
||||
self.select_if_matching(&cx.app_state::<Settings>().theme.name);
|
||||
self.select_if_matching(&cx.global::<Settings>().theme.name);
|
||||
self.show_selected_theme(cx);
|
||||
}
|
||||
editor::Event::Blurred => cx.emit(Event::Dismissed),
|
||||
|
@ -216,7 +216,7 @@ impl ThemeSelector {
|
|||
|
||||
fn render_matches(&self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
if self.matches.is_empty() {
|
||||
let settings = cx.app_state::<Settings>();
|
||||
let settings = cx.global::<Settings>();
|
||||
return Container::new(
|
||||
Label::new(
|
||||
"No matches".into(),
|
||||
|
@ -251,7 +251,7 @@ impl ThemeSelector {
|
|||
}
|
||||
|
||||
fn render_match(&self, theme_match: &StringMatch, index: usize, cx: &AppContext) -> ElementBox {
|
||||
let settings = cx.app_state::<Settings>();
|
||||
let settings = cx.global::<Settings>();
|
||||
let theme = &settings.theme;
|
||||
|
||||
let container = Container::new(
|
||||
|
@ -276,7 +276,7 @@ impl ThemeSelector {
|
|||
}
|
||||
|
||||
fn set_theme(theme: Arc<Theme>, cx: &mut MutableAppContext) {
|
||||
cx.update_app_state::<Settings, _, _>(|settings, cx| {
|
||||
cx.update_global::<Settings, _, _>(|settings, cx| {
|
||||
settings.theme = theme;
|
||||
cx.refresh_windows();
|
||||
});
|
||||
|
@ -299,7 +299,7 @@ impl View for ThemeSelector {
|
|||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let theme = cx.app_state::<Settings>().theme.clone();
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
Align::new(
|
||||
ConstrainedBox::new(
|
||||
Container::new(
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use tempdir::TempDir;
|
||||
|
||||
pub fn temp_tree(tree: serde_json::Value) -> TempDir {
|
||||
|
@ -48,3 +52,44 @@ pub fn sample_text(rows: usize, cols: usize, start_char: char) -> String {
|
|||
}
|
||||
text
|
||||
}
|
||||
|
||||
pub fn marked_text_by(
|
||||
marked_text: &str,
|
||||
markers: Vec<char>,
|
||||
) -> (String, HashMap<char, Vec<usize>>) {
|
||||
let mut extracted_markers: HashMap<char, Vec<usize>> = Default::default();
|
||||
let mut unmarked_text = String::new();
|
||||
|
||||
for char in marked_text.chars() {
|
||||
if markers.contains(&char) {
|
||||
let char_offsets = extracted_markers.entry(char).or_insert(Vec::new());
|
||||
char_offsets.push(unmarked_text.len());
|
||||
} else {
|
||||
unmarked_text.push(char);
|
||||
}
|
||||
}
|
||||
|
||||
(unmarked_text, extracted_markers)
|
||||
}
|
||||
|
||||
pub fn marked_text(marked_text: &str) -> (String, Vec<usize>) {
|
||||
let (unmarked_text, mut markers) = marked_text_by(marked_text, vec!['|']);
|
||||
(unmarked_text, markers.remove(&'|').unwrap_or_else(Vec::new))
|
||||
}
|
||||
|
||||
pub fn marked_text_ranges(marked_text: &str) -> (String, Vec<Range<usize>>) {
|
||||
let (unmarked_text, mut markers) = marked_text_by(marked_text, vec!['[', ']']);
|
||||
let opens = markers.remove(&'[').unwrap_or_default();
|
||||
let closes = markers.remove(&']').unwrap_or_default();
|
||||
assert_eq!(opens.len(), closes.len(), "marked ranges are unbalanced");
|
||||
|
||||
let ranges = opens
|
||||
.into_iter()
|
||||
.zip(closes)
|
||||
.map(|(open, close)| {
|
||||
assert!(close >= open, "marked ranges must be disjoint");
|
||||
open..close
|
||||
})
|
||||
.collect();
|
||||
(unmarked_text, ranges)
|
||||
}
|
||||
|
|
25
crates/vim/Cargo.toml
Normal file
25
crates/vim/Cargo.toml
Normal 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"] }
|
53
crates/vim/src/editor_events.rs
Normal file
53
crates/vim/src/editor_events.rs
Normal 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
30
crates/vim/src/insert.rs
Normal 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
36
crates/vim/src/mode.rs
Normal 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
66
crates/vim/src/normal.rs
Normal 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
97
crates/vim/src/vim.rs
Normal 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
253
crates/vim/src/vim_tests.rs
Normal 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(¶ms, cx));
|
||||
params
|
||||
.project
|
||||
.update(cx, |project, cx| {
|
||||
project.find_or_create_local_worktree("/root", true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
|
||||
.await;
|
||||
|
||||
let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
|
||||
let item = workspace
|
||||
.update(cx, |workspace, cx| workspace.open_path(file, cx))
|
||||
.await
|
||||
.expect("Could not open test file");
|
||||
|
||||
let editor = cx.update(|cx| {
|
||||
item.act_as::<Editor>(cx)
|
||||
.expect("Opened test file wasn't an editor")
|
||||
});
|
||||
editor.update(cx, |_, cx| cx.focus_self());
|
||||
|
||||
Self {
|
||||
cx,
|
||||
window_id,
|
||||
editor,
|
||||
}
|
||||
}
|
||||
|
||||
fn enable_vim(&mut self) {
|
||||
self.cx.update(|cx| {
|
||||
cx.update_global(|settings: &mut Settings, _| {
|
||||
settings.vim_mode = true;
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
fn disable_vim(&mut self) {
|
||||
self.cx.update(|cx| {
|
||||
cx.update_global(|settings: &mut Settings, _| {
|
||||
settings.vim_mode = false;
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
fn newest_selection(&mut self) -> Selection<DisplayPoint> {
|
||||
self.editor.update(self.cx, |editor, cx| {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
editor
|
||||
.newest_selection::<Point>(cx)
|
||||
.map(|point| point.to_display_point(&snapshot.display_snapshot))
|
||||
})
|
||||
}
|
||||
|
||||
fn mode(&mut self) -> Mode {
|
||||
self.cx.update(|cx| cx.global::<VimState>().mode)
|
||||
}
|
||||
|
||||
fn editor_text(&mut self) -> String {
|
||||
self.editor
|
||||
.update(self.cx, |editor, cx| editor.snapshot(cx).text())
|
||||
}
|
||||
|
||||
fn simulate_keystroke(&mut self, keystroke_text: &str) {
|
||||
let keystroke = Keystroke::parse(keystroke_text).unwrap();
|
||||
let input = if keystroke.modified() {
|
||||
None
|
||||
} else {
|
||||
Some(keystroke.key.clone())
|
||||
};
|
||||
self.cx
|
||||
.dispatch_keystroke(self.window_id, keystroke, input, false);
|
||||
}
|
||||
|
||||
fn simulate_keystrokes(&mut self, keystroke_texts: &[&str]) {
|
||||
for keystroke_text in keystroke_texts.into_iter() {
|
||||
self.simulate_keystroke(keystroke_text);
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_newest_selection_head(&mut self, text: &str) {
|
||||
let (unmarked_text, markers) = marked_text(&text);
|
||||
assert_eq!(
|
||||
self.editor_text(),
|
||||
unmarked_text,
|
||||
"Unmarked text doesn't match editor text"
|
||||
);
|
||||
let newest_selection = self.newest_selection();
|
||||
let expected_head = self.editor.update(self.cx, |editor, cx| {
|
||||
markers[0].to_display_point(&editor.snapshot(cx))
|
||||
});
|
||||
assert_eq!(newest_selection.head(), expected_head)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Deref for VimTestAppContext<'a> {
|
||||
type Target = gpui::TestAppContext;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.cx
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{ItemViewHandle, Settings, StatusItemView};
|
||||
use crate::{ItemHandle, Settings, StatusItemView};
|
||||
use futures::StreamExt;
|
||||
use gpui::AppContext;
|
||||
use gpui::{
|
||||
|
@ -116,7 +116,7 @@ impl View for LspStatus {
|
|||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let theme = &cx.app_state::<Settings>().theme;
|
||||
let theme = &cx.global::<Settings>().theme;
|
||||
|
||||
let mut pending_work = self.pending_language_server_work(cx);
|
||||
if let Some((lang_server_name, progress_token, progress)) = pending_work.next() {
|
||||
|
@ -166,7 +166,7 @@ impl View for LspStatus {
|
|||
} else if !self.failed.is_empty() {
|
||||
drop(pending_work);
|
||||
MouseEventHandler::new::<Self, _, _>(0, cx, |_, cx| {
|
||||
let theme = &cx.app_state::<Settings>().theme;
|
||||
let theme = &cx.global::<Settings>().theme;
|
||||
Label::new(
|
||||
format!(
|
||||
"Failed to download {} language server{}. Click to dismiss.",
|
||||
|
@ -187,5 +187,5 @@ impl View for LspStatus {
|
|||
}
|
||||
|
||||
impl StatusItemView for LspStatus {
|
||||
fn set_active_pane_item(&mut self, _: Option<&dyn ItemViewHandle>, _: &mut ViewContext<Self>) {}
|
||||
fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext<Self>) {}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use super::{ItemViewHandle, SplitDirection};
|
||||
use crate::{ItemHandle, ItemView, Settings, WeakItemViewHandle, Workspace};
|
||||
use super::{ItemHandle, SplitDirection};
|
||||
use crate::{Item, Settings, WeakItemHandle, Workspace};
|
||||
use collections::{HashMap, VecDeque};
|
||||
use gpui::{
|
||||
action,
|
||||
|
@ -7,10 +7,10 @@ use gpui::{
|
|||
geometry::{rect::RectF, vector::vec2f},
|
||||
keymap::Binding,
|
||||
platform::{CursorStyle, NavigationDirection},
|
||||
AnyViewHandle, Entity, MutableAppContext, Quad, RenderContext, Task, View, ViewContext,
|
||||
ViewHandle, WeakViewHandle,
|
||||
AnyViewHandle, AppContext, Entity, MutableAppContext, Quad, RenderContext, Task, View,
|
||||
ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use project::ProjectPath;
|
||||
use project::{ProjectEntryId, ProjectPath};
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
cell::RefCell,
|
||||
|
@ -33,7 +33,7 @@ const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
|
|||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
|
||||
pane.activate_item(action.0, cx);
|
||||
pane.activate_item(action.0, true, cx);
|
||||
});
|
||||
cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
|
||||
pane.activate_prev_item(cx);
|
||||
|
@ -92,12 +92,13 @@ pub fn init(cx: &mut MutableAppContext) {
|
|||
|
||||
pub enum Event {
|
||||
Activate,
|
||||
ActivateItem { local: bool },
|
||||
Remove,
|
||||
Split(SplitDirection),
|
||||
}
|
||||
|
||||
pub struct Pane {
|
||||
item_views: Vec<(usize, Box<dyn ItemViewHandle>)>,
|
||||
items: Vec<Box<dyn ItemHandle>>,
|
||||
active_item_index: usize,
|
||||
nav_history: Rc<RefCell<NavHistory>>,
|
||||
toolbars: HashMap<TypeId, Box<dyn ToolbarHandle>>,
|
||||
|
@ -108,7 +109,7 @@ pub struct Pane {
|
|||
pub trait Toolbar: View {
|
||||
fn active_item_changed(
|
||||
&mut self,
|
||||
item: Option<Box<dyn ItemViewHandle>>,
|
||||
item: Option<Box<dyn ItemHandle>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> bool;
|
||||
fn on_dismiss(&mut self, cx: &mut ViewContext<Self>);
|
||||
|
@ -117,7 +118,7 @@ pub trait Toolbar: View {
|
|||
trait ToolbarHandle {
|
||||
fn active_item_changed(
|
||||
&self,
|
||||
item: Option<Box<dyn ItemViewHandle>>,
|
||||
item: Option<Box<dyn ItemHandle>>,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> bool;
|
||||
fn on_dismiss(&self, cx: &mut MutableAppContext);
|
||||
|
@ -126,7 +127,7 @@ trait ToolbarHandle {
|
|||
|
||||
pub struct ItemNavHistory {
|
||||
history: Rc<RefCell<NavHistory>>,
|
||||
item_view: Rc<dyn WeakItemViewHandle>,
|
||||
item: Rc<dyn WeakItemHandle>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
|
@ -152,14 +153,14 @@ impl Default for NavigationMode {
|
|||
}
|
||||
|
||||
pub struct NavigationEntry {
|
||||
pub item_view: Rc<dyn WeakItemViewHandle>,
|
||||
pub item: Rc<dyn WeakItemHandle>,
|
||||
pub data: Option<Box<dyn Any>>,
|
||||
}
|
||||
|
||||
impl Pane {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
item_views: Vec::new(),
|
||||
items: Vec::new(),
|
||||
active_item_index: 0,
|
||||
nav_history: Default::default(),
|
||||
toolbars: Default::default(),
|
||||
|
@ -211,40 +212,47 @@ impl Pane {
|
|||
workspace.activate_pane(pane.clone(), cx);
|
||||
|
||||
let to_load = pane.update(cx, |pane, cx| {
|
||||
loop {
|
||||
// Retrieve the weak item handle from the history.
|
||||
let entry = pane.nav_history.borrow_mut().pop(mode)?;
|
||||
|
||||
// If the item is still present in this pane, then activate it.
|
||||
if let Some(index) = entry
|
||||
.item_view
|
||||
.item
|
||||
.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);
|
||||
item_view.deactivated(cx);
|
||||
item.deactivated(cx);
|
||||
pane.nav_history
|
||||
.borrow_mut()
|
||||
.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);
|
||||
let mut navigated = prev_active_index != pane.active_item_index;
|
||||
if let Some(data) = entry.data {
|
||||
pane.active_item()?.navigate(data, cx);
|
||||
navigated |= pane.active_item()?.navigate(data, cx);
|
||||
}
|
||||
|
||||
if navigated {
|
||||
cx.notify();
|
||||
None
|
||||
break None;
|
||||
}
|
||||
}
|
||||
// If the item is no longer present in this pane, then retrieve its
|
||||
// project path in order to reopen it.
|
||||
else {
|
||||
pane.nav_history
|
||||
break pane
|
||||
.nav_history
|
||||
.borrow_mut()
|
||||
.paths_by_item
|
||||
.get(&entry.item_view.id())
|
||||
.get(&entry.item.id())
|
||||
.cloned()
|
||||
.map(|project_path| (project_path, entry))
|
||||
.map(|project_path| (project_path, entry));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -253,18 +261,27 @@ impl Pane {
|
|||
let pane = pane.downgrade();
|
||||
let task = workspace.load_path(project_path, cx);
|
||||
cx.spawn(|workspace, mut cx| async move {
|
||||
let item = task.await;
|
||||
let task = task.await;
|
||||
if let Some(pane) = pane.upgrade(&cx) {
|
||||
if let Some(item) = item.log_err() {
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
pane.update(cx, |p, _| p.nav_history.borrow_mut().set_mode(mode));
|
||||
let item_view = workspace.open_item_in_pane(item, &pane, cx);
|
||||
pane.update(cx, |p, _| {
|
||||
p.nav_history.borrow_mut().set_mode(NavigationMode::Normal)
|
||||
if let Some((project_entry_id, build_item)) = task.log_err() {
|
||||
pane.update(&mut cx, |pane, _| {
|
||||
pane.nav_history.borrow_mut().set_mode(mode);
|
||||
});
|
||||
|
||||
let item = workspace.update(&mut cx, |workspace, cx| {
|
||||
Self::open_item(
|
||||
workspace,
|
||||
pane.clone(),
|
||||
project_entry_id,
|
||||
cx,
|
||||
build_item,
|
||||
)
|
||||
});
|
||||
pane.update(&mut cx, |pane, cx| {
|
||||
pane.nav_history
|
||||
.borrow_mut()
|
||||
.set_mode(NavigationMode::Normal);
|
||||
if let Some(data) = entry.data {
|
||||
item_view.navigate(data, cx);
|
||||
item.navigate(data, cx);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
@ -281,80 +298,115 @@ impl Pane {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn open_item<T>(
|
||||
&mut self,
|
||||
item_handle: T,
|
||||
workspace: &Workspace,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Box<dyn ItemViewHandle>
|
||||
where
|
||||
T: 'static + ItemHandle,
|
||||
{
|
||||
for (ix, (item_id, item_view)) in self.item_views.iter().enumerate() {
|
||||
if *item_id == item_handle.id() {
|
||||
let item_view = item_view.boxed_clone();
|
||||
self.activate_item(ix, cx);
|
||||
return item_view;
|
||||
pub(crate) fn open_item(
|
||||
workspace: &mut Workspace,
|
||||
pane: ViewHandle<Pane>,
|
||||
project_entry_id: ProjectEntryId,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
build_item: impl FnOnce(&mut MutableAppContext) -> Box<dyn ItemHandle>,
|
||||
) -> Box<dyn ItemHandle> {
|
||||
let existing_item = pane.update(cx, |pane, cx| {
|
||||
for (ix, item) in pane.items.iter().enumerate() {
|
||||
if item.project_entry_id(cx) == Some(project_entry_id) {
|
||||
let item = item.boxed_clone();
|
||||
pane.activate_item(ix, true, cx);
|
||||
return Some(item);
|
||||
}
|
||||
}
|
||||
None
|
||||
});
|
||||
if let Some(existing_item) = existing_item {
|
||||
existing_item
|
||||
} else {
|
||||
let item = build_item(cx);
|
||||
Self::add_item(workspace, pane, item.boxed_clone(), true, cx);
|
||||
item
|
||||
}
|
||||
}
|
||||
|
||||
let item_view =
|
||||
item_handle.add_view(cx.window_id(), workspace, self.nav_history.clone(), cx);
|
||||
self.add_item_view(item_view.boxed_clone(), cx);
|
||||
item_view
|
||||
}
|
||||
|
||||
pub fn add_item_view(
|
||||
&mut self,
|
||||
mut item_view: Box<dyn ItemViewHandle>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
pub(crate) fn add_item(
|
||||
workspace: &mut Workspace,
|
||||
pane: ViewHandle<Pane>,
|
||||
item: Box<dyn ItemHandle>,
|
||||
local: bool,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
item_view.added_to_pane(cx);
|
||||
let item_idx = cmp::min(self.active_item_index + 1, self.item_views.len());
|
||||
self.item_views
|
||||
.insert(item_idx, (item_view.item(cx).id(), item_view));
|
||||
self.activate_item(item_idx, cx);
|
||||
// Prevent adding the same item to the pane more than once.
|
||||
if let Some(item_ix) = pane.read(cx).items.iter().position(|i| i.id() == item.id()) {
|
||||
pane.update(cx, |pane, cx| pane.activate_item(item_ix, local, cx));
|
||||
return;
|
||||
}
|
||||
|
||||
item.set_nav_history(pane.read(cx).nav_history.clone(), cx);
|
||||
item.added_to_pane(workspace, pane.clone(), cx);
|
||||
pane.update(cx, |pane, cx| {
|
||||
let item_idx = cmp::min(pane.active_item_index + 1, pane.items.len());
|
||||
pane.items.insert(item_idx, item);
|
||||
pane.activate_item(item_idx, local, cx);
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
|
||||
pub fn contains_item(&self, item: &dyn ItemHandle) -> bool {
|
||||
let item_id = item.id();
|
||||
self.item_views
|
||||
pub fn items(&self) -> impl Iterator<Item = &Box<dyn ItemHandle>> {
|
||||
self.items.iter()
|
||||
}
|
||||
|
||||
pub fn items_of_type<'a, T: View>(&'a self) -> impl 'a + Iterator<Item = ViewHandle<T>> {
|
||||
self.items
|
||||
.iter()
|
||||
.any(|(existing_item_id, _)| *existing_item_id == item_id)
|
||||
.filter_map(|item| item.to_any().downcast())
|
||||
}
|
||||
|
||||
pub fn item_views(&self) -> impl Iterator<Item = &Box<dyn ItemViewHandle>> {
|
||||
self.item_views.iter().map(|(_, view)| view)
|
||||
pub fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
|
||||
self.items.get(self.active_item_index).cloned()
|
||||
}
|
||||
|
||||
pub fn active_item(&self) -> Option<Box<dyn ItemViewHandle>> {
|
||||
self.item_views
|
||||
.get(self.active_item_index)
|
||||
.map(|(_, view)| view.clone())
|
||||
pub fn project_entry_id_for_item(
|
||||
&self,
|
||||
item: &dyn ItemHandle,
|
||||
cx: &AppContext,
|
||||
) -> Option<ProjectEntryId> {
|
||||
self.items.iter().find_map(|existing| {
|
||||
if existing.id() == item.id() {
|
||||
existing.project_entry_id(cx)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn index_for_item_view(&self, item_view: &dyn ItemViewHandle) -> Option<usize> {
|
||||
self.item_views
|
||||
.iter()
|
||||
.position(|(_, i)| i.id() == item_view.id())
|
||||
pub fn item_for_entry(
|
||||
&self,
|
||||
entry_id: ProjectEntryId,
|
||||
cx: &AppContext,
|
||||
) -> Option<Box<dyn ItemHandle>> {
|
||||
self.items.iter().find_map(|item| {
|
||||
if item.project_entry_id(cx) == Some(entry_id) {
|
||||
Some(item.boxed_clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option<usize> {
|
||||
self.item_views.iter().position(|(id, _)| *id == item.id())
|
||||
self.items.iter().position(|i| i.id() == item.id())
|
||||
}
|
||||
|
||||
pub fn activate_item(&mut self, index: usize, cx: &mut ViewContext<Self>) {
|
||||
if index < self.item_views.len() {
|
||||
pub fn activate_item(&mut self, index: usize, local: bool, cx: &mut ViewContext<Self>) {
|
||||
if index < self.items.len() {
|
||||
let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
|
||||
if prev_active_item_ix != self.active_item_index
|
||||
&& prev_active_item_ix < self.item_views.len()
|
||||
&& prev_active_item_ix < self.items.len()
|
||||
{
|
||||
self.item_views[prev_active_item_ix].1.deactivated(cx);
|
||||
self.items[prev_active_item_ix].deactivated(cx);
|
||||
cx.emit(Event::ActivateItem { local });
|
||||
}
|
||||
self.update_active_toolbar(cx);
|
||||
if local {
|
||||
self.focus_active_item(cx);
|
||||
self.activate(cx);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
@ -363,31 +415,31 @@ impl Pane {
|
|||
let mut index = self.active_item_index;
|
||||
if index > 0 {
|
||||
index -= 1;
|
||||
} else if self.item_views.len() > 0 {
|
||||
index = self.item_views.len() - 1;
|
||||
} else if self.items.len() > 0 {
|
||||
index = self.items.len() - 1;
|
||||
}
|
||||
self.activate_item(index, cx);
|
||||
self.activate_item(index, true, cx);
|
||||
}
|
||||
|
||||
pub fn activate_next_item(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let mut index = self.active_item_index;
|
||||
if index + 1 < self.item_views.len() {
|
||||
if index + 1 < self.items.len() {
|
||||
index += 1;
|
||||
} else {
|
||||
index = 0;
|
||||
}
|
||||
self.activate_item(index, cx);
|
||||
self.activate_item(index, true, cx);
|
||||
}
|
||||
|
||||
pub fn close_active_item(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if !self.item_views.is_empty() {
|
||||
self.close_item(self.item_views[self.active_item_index].1.id(), cx)
|
||||
if !self.items.is_empty() {
|
||||
self.close_item(self.items[self.active_item_index].id(), cx)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn close_inactive_items(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if !self.item_views.is_empty() {
|
||||
let active_item_id = self.item_views[self.active_item_index].1.id();
|
||||
if !self.items.is_empty() {
|
||||
let active_item_id = self.items[self.active_item_index].id();
|
||||
self.close_items(cx, |id| id != active_item_id);
|
||||
}
|
||||
}
|
||||
|
@ -403,10 +455,10 @@ impl Pane {
|
|||
) {
|
||||
let mut item_ix = 0;
|
||||
let mut new_active_item_index = self.active_item_index;
|
||||
self.item_views.retain(|(_, item_view)| {
|
||||
if should_close(item_view.id()) {
|
||||
self.items.retain(|item| {
|
||||
if should_close(item.id()) {
|
||||
if item_ix == self.active_item_index {
|
||||
item_view.deactivated(cx);
|
||||
item.deactivated(cx);
|
||||
}
|
||||
|
||||
if item_ix < self.active_item_index {
|
||||
|
@ -414,10 +466,10 @@ impl Pane {
|
|||
}
|
||||
|
||||
let mut nav_history = self.nav_history.borrow_mut();
|
||||
if let Some(path) = item_view.project_path(cx) {
|
||||
nav_history.paths_by_item.insert(item_view.id(), path);
|
||||
if let Some(path) = item.project_path(cx) {
|
||||
nav_history.paths_by_item.insert(item.id(), path);
|
||||
} else {
|
||||
nav_history.paths_by_item.remove(&item_view.id());
|
||||
nav_history.paths_by_item.remove(&item.id());
|
||||
}
|
||||
|
||||
item_ix += 1;
|
||||
|
@ -428,10 +480,10 @@ impl Pane {
|
|||
}
|
||||
});
|
||||
|
||||
if self.item_views.is_empty() {
|
||||
if self.items.is_empty() {
|
||||
cx.emit(Event::Remove);
|
||||
} else {
|
||||
self.active_item_index = cmp::min(new_active_item_index, self.item_views.len() - 1);
|
||||
self.active_item_index = cmp::min(new_active_item_index, self.items.len() - 1);
|
||||
self.focus_active_item(cx);
|
||||
self.activate(cx);
|
||||
}
|
||||
|
@ -440,7 +492,7 @@ impl Pane {
|
|||
cx.notify();
|
||||
}
|
||||
|
||||
fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
|
||||
pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if let Some(active_item) = self.active_item() {
|
||||
cx.focus(active_item);
|
||||
}
|
||||
|
@ -500,9 +552,9 @@ impl Pane {
|
|||
}
|
||||
|
||||
fn update_active_toolbar(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let active_item = self.item_views.get(self.active_item_index);
|
||||
let active_item = self.items.get(self.active_item_index);
|
||||
for (toolbar_type_id, toolbar) in &self.toolbars {
|
||||
let visible = toolbar.active_item_changed(active_item.map(|i| i.1.clone()), cx);
|
||||
let visible = toolbar.active_item_changed(active_item.cloned(), cx);
|
||||
if Some(*toolbar_type_id) == self.active_toolbar_type {
|
||||
self.active_toolbar_visible = visible;
|
||||
}
|
||||
|
@ -510,12 +562,12 @@ impl Pane {
|
|||
}
|
||||
|
||||
fn render_tabs(&self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let theme = cx.app_state::<Settings>().theme.clone();
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
|
||||
enum Tabs {}
|
||||
let tabs = MouseEventHandler::new::<Tabs, _, _>(0, cx, |mouse_state, cx| {
|
||||
let mut row = Flex::row();
|
||||
for (ix, (_, item_view)) in self.item_views.iter().enumerate() {
|
||||
for (ix, item) in self.items.iter().enumerate() {
|
||||
let is_active = ix == self.active_item_index;
|
||||
|
||||
row.add_child({
|
||||
|
@ -524,7 +576,7 @@ impl Pane {
|
|||
} else {
|
||||
theme.workspace.tab.clone()
|
||||
};
|
||||
let title = item_view.tab_content(&tab_style, cx);
|
||||
let title = item.tab_content(&tab_style, cx);
|
||||
|
||||
let mut style = if is_active {
|
||||
theme.workspace.active_tab.clone()
|
||||
|
@ -541,9 +593,9 @@ impl Pane {
|
|||
.with_child(
|
||||
Align::new({
|
||||
let diameter = 7.0;
|
||||
let icon_color = if item_view.has_conflict(cx) {
|
||||
let icon_color = if item.has_conflict(cx) {
|
||||
Some(style.icon_conflict)
|
||||
} else if item_view.is_dirty(cx) {
|
||||
} else if item.is_dirty(cx) {
|
||||
Some(style.icon_dirty)
|
||||
} else {
|
||||
None
|
||||
|
@ -587,7 +639,7 @@ impl Pane {
|
|||
.with_child(
|
||||
Align::new(
|
||||
ConstrainedBox::new(if mouse_state.hovered {
|
||||
let item_id = item_view.id();
|
||||
let item_id = item.id();
|
||||
enum TabCloseButton {}
|
||||
let icon = Svg::new("icons/x.svg");
|
||||
MouseEventHandler::new::<TabCloseButton, _, _>(
|
||||
|
@ -691,7 +743,7 @@ impl View for Pane {
|
|||
impl<T: Toolbar> ToolbarHandle for ViewHandle<T> {
|
||||
fn active_item_changed(
|
||||
&self,
|
||||
item: Option<Box<dyn ItemViewHandle>>,
|
||||
item: Option<Box<dyn ItemHandle>>,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> bool {
|
||||
self.update(cx, |this, cx| this.active_item_changed(item, cx))
|
||||
|
@ -707,10 +759,10 @@ impl<T: Toolbar> ToolbarHandle for ViewHandle<T> {
|
|||
}
|
||||
|
||||
impl ItemNavHistory {
|
||||
pub fn new<T: ItemView>(history: Rc<RefCell<NavHistory>>, item_view: &ViewHandle<T>) -> Self {
|
||||
pub fn new<T: Item>(history: Rc<RefCell<NavHistory>>, item: &ViewHandle<T>) -> Self {
|
||||
Self {
|
||||
history,
|
||||
item_view: Rc::new(item_view.downgrade()),
|
||||
item: Rc::new(item.downgrade()),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -719,7 +771,7 @@ impl ItemNavHistory {
|
|||
}
|
||||
|
||||
pub fn push<D: 'static + Any>(&self, data: Option<D>) {
|
||||
self.history.borrow_mut().push(data, self.item_view.clone());
|
||||
self.history.borrow_mut().push(data, self.item.clone());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -752,11 +804,7 @@ impl NavHistory {
|
|||
self.mode = mode;
|
||||
}
|
||||
|
||||
pub fn push<D: 'static + Any>(
|
||||
&mut self,
|
||||
data: Option<D>,
|
||||
item_view: Rc<dyn WeakItemViewHandle>,
|
||||
) {
|
||||
pub fn push<D: 'static + Any>(&mut self, data: Option<D>, item: Rc<dyn WeakItemHandle>) {
|
||||
match self.mode {
|
||||
NavigationMode::Disabled => {}
|
||||
NavigationMode::Normal => {
|
||||
|
@ -764,7 +812,7 @@ impl NavHistory {
|
|||
self.backward_stack.pop_front();
|
||||
}
|
||||
self.backward_stack.push_back(NavigationEntry {
|
||||
item_view,
|
||||
item,
|
||||
data: data.map(|data| Box::new(data) as Box<dyn Any>),
|
||||
});
|
||||
self.forward_stack.clear();
|
||||
|
@ -774,7 +822,7 @@ impl NavHistory {
|
|||
self.forward_stack.pop_front();
|
||||
}
|
||||
self.forward_stack.push_back(NavigationEntry {
|
||||
item_view,
|
||||
item,
|
||||
data: data.map(|data| Box::new(data) as Box<dyn Any>),
|
||||
});
|
||||
}
|
||||
|
@ -783,7 +831,7 @@ impl NavHistory {
|
|||
self.backward_stack.pop_front();
|
||||
}
|
||||
self.backward_stack.push_back(NavigationEntry {
|
||||
item_view,
|
||||
item,
|
||||
data: data.map(|data| Box::new(data) as Box<dyn Any>),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
use crate::{FollowerStatesByLeader, Pane};
|
||||
use anyhow::{anyhow, Result};
|
||||
use gpui::{elements::*, Axis, ViewHandle};
|
||||
use client::PeerId;
|
||||
use collections::HashMap;
|
||||
use gpui::{elements::*, Axis, Border, ViewHandle};
|
||||
use project::Collaborator;
|
||||
use theme::Theme;
|
||||
|
||||
use crate::Pane;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct PaneGroup {
|
||||
root: Member,
|
||||
|
@ -47,8 +49,19 @@ impl PaneGroup {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn render<'a>(&self, theme: &Theme) -> ElementBox {
|
||||
self.root.render(theme)
|
||||
pub(crate) fn render<'a>(
|
||||
&self,
|
||||
theme: &Theme,
|
||||
follower_states: &FollowerStatesByLeader,
|
||||
collaborators: &HashMap<PeerId, Collaborator>,
|
||||
) -> ElementBox {
|
||||
self.root.render(theme, follower_states, collaborators)
|
||||
}
|
||||
|
||||
pub(crate) fn panes(&self) -> Vec<&ViewHandle<Pane>> {
|
||||
let mut panes = Vec::new();
|
||||
self.root.collect_panes(&mut panes);
|
||||
panes
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -80,10 +93,50 @@ impl Member {
|
|||
Member::Axis(PaneAxis { axis, members })
|
||||
}
|
||||
|
||||
pub fn render(&self, theme: &Theme) -> ElementBox {
|
||||
pub fn render(
|
||||
&self,
|
||||
theme: &Theme,
|
||||
follower_states: &FollowerStatesByLeader,
|
||||
collaborators: &HashMap<PeerId, Collaborator>,
|
||||
) -> ElementBox {
|
||||
match self {
|
||||
Member::Pane(pane) => ChildView::new(pane).boxed(),
|
||||
Member::Axis(axis) => axis.render(theme),
|
||||
Member::Pane(pane) => {
|
||||
let mut border = Border::default();
|
||||
let leader = follower_states
|
||||
.iter()
|
||||
.find_map(|(leader_id, follower_states)| {
|
||||
if follower_states.contains_key(pane) {
|
||||
Some(leader_id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.and_then(|leader_id| collaborators.get(leader_id));
|
||||
if let Some(leader) = leader {
|
||||
let leader_color = theme
|
||||
.editor
|
||||
.replica_selection_style(leader.replica_id)
|
||||
.cursor;
|
||||
border = Border::all(theme.workspace.leader_border_width, leader_color);
|
||||
border
|
||||
.color
|
||||
.fade_out(1. - theme.workspace.leader_border_opacity);
|
||||
border.overlay = true;
|
||||
}
|
||||
ChildView::new(pane).contained().with_border(border).boxed()
|
||||
}
|
||||
Member::Axis(axis) => axis.render(theme, follower_states, collaborators),
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_panes<'a>(&'a self, panes: &mut Vec<&'a ViewHandle<Pane>>) {
|
||||
match self {
|
||||
Member::Axis(axis) => {
|
||||
for member in &axis.members {
|
||||
member.collect_panes(panes);
|
||||
}
|
||||
}
|
||||
Member::Pane(pane) => panes.push(pane),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -172,11 +225,16 @@ impl PaneAxis {
|
|||
}
|
||||
}
|
||||
|
||||
fn render<'a>(&self, theme: &Theme) -> ElementBox {
|
||||
fn render(
|
||||
&self,
|
||||
theme: &Theme,
|
||||
follower_state: &FollowerStatesByLeader,
|
||||
collaborators: &HashMap<PeerId, Collaborator>,
|
||||
) -> ElementBox {
|
||||
let last_member_ix = self.members.len() - 1;
|
||||
Flex::new(self.axis)
|
||||
.with_children(self.members.iter().enumerate().map(|(ix, member)| {
|
||||
let mut member = member.render(theme);
|
||||
let mut member = member.render(theme, follower_state, collaborators);
|
||||
if ix < last_member_ix {
|
||||
let mut border = theme.workspace.pane_divider;
|
||||
border.left = false;
|
||||
|
|
|
@ -17,6 +17,7 @@ use util::ResultExt;
|
|||
pub struct Settings {
|
||||
pub buffer_font_family: FamilyId,
|
||||
pub buffer_font_size: f32,
|
||||
pub vim_mode: bool,
|
||||
pub tab_size: usize,
|
||||
pub soft_wrap: SoftWrap,
|
||||
pub preferred_line_length: u32,
|
||||
|
@ -48,6 +49,8 @@ struct SettingsFileContent {
|
|||
buffer_font_family: Option<String>,
|
||||
#[serde(default)]
|
||||
buffer_font_size: Option<f32>,
|
||||
#[serde(default)]
|
||||
vim_mode: Option<bool>,
|
||||
#[serde(flatten)]
|
||||
editor: LanguageOverride,
|
||||
#[serde(default)]
|
||||
|
@ -130,6 +133,7 @@ impl Settings {
|
|||
Ok(Self {
|
||||
buffer_font_family: font_cache.load_family(&[buffer_font_family])?,
|
||||
buffer_font_size: 15.,
|
||||
vim_mode: false,
|
||||
tab_size: 4,
|
||||
soft_wrap: SoftWrap::None,
|
||||
preferred_line_length: 80,
|
||||
|
@ -174,6 +178,7 @@ impl Settings {
|
|||
Settings {
|
||||
buffer_font_family: cx.font_cache().load_family(&["Monaco"]).unwrap(),
|
||||
buffer_font_size: 14.,
|
||||
vim_mode: false,
|
||||
tab_size: 4,
|
||||
soft_wrap: SoftWrap::None,
|
||||
preferred_line_length: 80,
|
||||
|
@ -200,6 +205,7 @@ impl Settings {
|
|||
}
|
||||
|
||||
merge(&mut self.buffer_font_size, data.buffer_font_size);
|
||||
merge(&mut self.vim_mode, data.vim_mode);
|
||||
merge(&mut self.soft_wrap, data.editor.soft_wrap);
|
||||
merge(&mut self.tab_size, data.editor.tab_size);
|
||||
merge(
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{ItemViewHandle, Pane, Settings};
|
||||
use crate::{ItemHandle, Pane, Settings};
|
||||
use gpui::{
|
||||
elements::*, AnyViewHandle, ElementBox, Entity, MutableAppContext, RenderContext, Subscription,
|
||||
View, ViewContext, ViewHandle,
|
||||
|
@ -7,7 +7,7 @@ use gpui::{
|
|||
pub trait StatusItemView: View {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
active_pane_item: Option<&dyn crate::ItemViewHandle>,
|
||||
active_pane_item: Option<&dyn crate::ItemHandle>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
);
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ trait StatusItemViewHandle {
|
|||
fn to_any(&self) -> AnyViewHandle;
|
||||
fn set_active_pane_item(
|
||||
&self,
|
||||
active_pane_item: Option<&dyn ItemViewHandle>,
|
||||
active_pane_item: Option<&dyn ItemHandle>,
|
||||
cx: &mut MutableAppContext,
|
||||
);
|
||||
}
|
||||
|
@ -38,7 +38,7 @@ impl View for StatusBar {
|
|||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let theme = &cx.app_state::<Settings>().theme.workspace.status_bar;
|
||||
let theme = &cx.global::<Settings>().theme.workspace.status_bar;
|
||||
Flex::row()
|
||||
.with_children(self.left_items.iter().map(|i| {
|
||||
ChildView::new(i.as_ref())
|
||||
|
@ -114,7 +114,7 @@ impl<T: StatusItemView> StatusItemViewHandle for ViewHandle<T> {
|
|||
|
||||
fn set_active_pane_item(
|
||||
&self,
|
||||
active_pane_item: Option<&dyn ItemViewHandle>,
|
||||
active_pane_item: Option<&dyn ItemHandle>,
|
||||
cx: &mut MutableAppContext,
|
||||
) {
|
||||
self.update(cx, |this, cx| {
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
|
|||
description = "The fast, collaborative code editor."
|
||||
edition = "2021"
|
||||
name = "zed"
|
||||
version = "0.21.0"
|
||||
version = "0.23.0"
|
||||
|
||||
[lib]
|
||||
name = "zed"
|
||||
|
@ -55,6 +55,7 @@ text = { path = "../text" }
|
|||
theme = { path = "../theme" }
|
||||
theme_selector = { path = "../theme_selector" }
|
||||
util = { path = "../util" }
|
||||
vim = { path = "../vim" }
|
||||
workspace = { path = "../workspace" }
|
||||
anyhow = "1.0.38"
|
||||
async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
|
||||
|
@ -64,6 +65,7 @@ crossbeam-channel = "0.5.0"
|
|||
ctor = "0.1.20"
|
||||
dirs = "3.0"
|
||||
easy-parallel = "3.1.0"
|
||||
env_logger = "0.8"
|
||||
futures = "0.3"
|
||||
http-auth-basic = "0.1.3"
|
||||
ignore = "0.4"
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -4,6 +4,8 @@ base = { family = "Zed Sans", size = 14 }
|
|||
[workspace]
|
||||
background = "$surface.0"
|
||||
pane_divider = { width = 1, color = "$border.0" }
|
||||
leader_border_opacity = 0.7
|
||||
leader_border_width = 2.0
|
||||
|
||||
[workspace.titlebar]
|
||||
height = 32
|
||||
|
|
|
@ -9,7 +9,6 @@ use gpui::{App, AssetSource, Task};
|
|||
use log::LevelFilter;
|
||||
use parking_lot::Mutex;
|
||||
use project::Fs;
|
||||
use simplelog::SimpleLogger;
|
||||
use smol::process::Command;
|
||||
use std::{env, fs, path::PathBuf, sync::Arc};
|
||||
use theme::{ThemeRegistry, DEFAULT_THEME_NAME};
|
||||
|
@ -61,7 +60,6 @@ fn main() {
|
|||
app.run(move |cx| {
|
||||
let http = http::client();
|
||||
let client = client::Client::new(http.clone());
|
||||
let mut path_openers = Vec::new();
|
||||
let mut languages = language::build_language_registry(login_shell_env_loaded);
|
||||
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
|
||||
let channel_list =
|
||||
|
@ -70,8 +68,8 @@ fn main() {
|
|||
project::Project::init(&client);
|
||||
client::Channel::init(&client);
|
||||
client::init(client.clone(), cx);
|
||||
workspace::init(cx);
|
||||
editor::init(cx, &mut path_openers);
|
||||
workspace::init(&client, cx);
|
||||
editor::init(cx);
|
||||
go_to_line::init(cx);
|
||||
file_finder::init(cx);
|
||||
chat_panel::init(cx);
|
||||
|
@ -80,11 +78,18 @@ fn main() {
|
|||
project_panel::init(cx);
|
||||
diagnostics::init(cx);
|
||||
search::init(cx);
|
||||
vim::init(cx);
|
||||
cx.spawn({
|
||||
let client = client.clone();
|
||||
|cx| async move {
|
||||
if stdout_is_a_pty() {
|
||||
if client::IMPERSONATE_LOGIN.is_some() {
|
||||
client.authenticate_and_connect(false, &cx).await?;
|
||||
}
|
||||
} else {
|
||||
if client.has_keychain_credentials(&cx) {
|
||||
client.authenticate_and_connect(&cx).await?;
|
||||
client.authenticate_and_connect(true, &cx).await?;
|
||||
}
|
||||
}
|
||||
Ok::<_, anyhow::Error>(())
|
||||
}
|
||||
|
@ -102,7 +107,7 @@ fn main() {
|
|||
cx.spawn(|mut cx| async move {
|
||||
while let Some(settings) = settings_rx.next().await {
|
||||
cx.update(|cx| {
|
||||
cx.update_app_state(|s, _| *s = settings);
|
||||
cx.update_global(|s, _| *s = settings);
|
||||
cx.refresh_windows();
|
||||
});
|
||||
}
|
||||
|
@ -111,7 +116,7 @@ fn main() {
|
|||
|
||||
languages.set_language_server_download_dir(zed::ROOT_PATH.clone());
|
||||
languages.set_theme(&settings.theme.editor.syntax);
|
||||
cx.add_app_state(settings);
|
||||
cx.set_global(settings);
|
||||
|
||||
let app_state = Arc::new(AppState {
|
||||
languages: Arc::new(languages),
|
||||
|
@ -120,7 +125,6 @@ fn main() {
|
|||
client,
|
||||
user_store,
|
||||
fs,
|
||||
path_openers: Arc::from(path_openers),
|
||||
build_window_options: &build_window_options,
|
||||
build_workspace: &build_workspace,
|
||||
});
|
||||
|
@ -144,11 +148,10 @@ fn main() {
|
|||
}
|
||||
|
||||
fn init_logger() {
|
||||
let level = LevelFilter::Info;
|
||||
|
||||
if stdout_is_a_pty() {
|
||||
SimpleLogger::init(level, Default::default()).expect("could not initialize logger");
|
||||
env_logger::init();
|
||||
} else {
|
||||
let level = LevelFilter::Info;
|
||||
let log_dir_path = dirs::home_dir()
|
||||
.expect("could not locate home directory for logging")
|
||||
.join("Library/Logs/");
|
||||
|
|
|
@ -17,9 +17,8 @@ fn init_logger() {
|
|||
|
||||
pub fn test_app_state(cx: &mut MutableAppContext) -> Arc<AppState> {
|
||||
let settings = Settings::test(cx);
|
||||
let mut path_openers = Vec::new();
|
||||
editor::init(cx, &mut path_openers);
|
||||
cx.add_app_state(settings);
|
||||
editor::init(cx);
|
||||
cx.set_global(settings);
|
||||
let themes = ThemeRegistry::new(Assets, cx.font_cache().clone());
|
||||
let http = FakeHttpClient::with_404_response();
|
||||
let client = Client::new(http.clone());
|
||||
|
@ -40,7 +39,6 @@ pub fn test_app_state(cx: &mut MutableAppContext) -> Arc<AppState> {
|
|||
client,
|
||||
user_store,
|
||||
fs: FakeFs::new(cx.background().clone()),
|
||||
path_openers: Arc::from(path_openers),
|
||||
build_window_options: &build_window_options,
|
||||
build_workspace: &build_workspace,
|
||||
})
|
||||
|
|
|
@ -43,7 +43,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
|
|||
cx.add_global_action(quit);
|
||||
cx.add_global_action({
|
||||
move |action: &AdjustBufferFontSize, cx| {
|
||||
cx.update_app_state::<Settings, _, _>(|settings, cx| {
|
||||
cx.update_global::<Settings, _, _>(|settings, cx| {
|
||||
settings.buffer_font_size =
|
||||
(settings.buffer_font_size + action.0).max(MIN_FONT_SIZE);
|
||||
cx.refresh_windows();
|
||||
|
@ -111,7 +111,6 @@ pub fn build_workspace(
|
|||
languages: app_state.languages.clone(),
|
||||
user_store: app_state.user_store.clone(),
|
||||
channel_list: app_state.channel_list.clone(),
|
||||
path_openers: app_state.path_openers.clone(),
|
||||
};
|
||||
let mut workspace = Workspace::new(&workspace_params, cx);
|
||||
let project = workspace.project().clone();
|
||||
|
@ -193,7 +192,7 @@ mod tests {
|
|||
use theme::{Theme, ThemeRegistry, DEFAULT_THEME_NAME};
|
||||
use util::test::temp_tree;
|
||||
use workspace::{
|
||||
open_paths, pane, ItemView, ItemViewHandle, OpenNew, Pane, SplitDirection, WorkspaceHandle,
|
||||
open_paths, pane, Item, ItemHandle, OpenNew, Pane, SplitDirection, WorkspaceHandle,
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
|
@ -253,7 +252,7 @@ mod tests {
|
|||
async fn test_new_empty_workspace(cx: &mut TestAppContext) {
|
||||
let app_state = cx.update(test_app_state);
|
||||
cx.update(|cx| {
|
||||
workspace::init(cx);
|
||||
workspace::init(&app_state.client, cx);
|
||||
});
|
||||
cx.dispatch_global_action(workspace::OpenNew(app_state.clone()));
|
||||
let window_id = *cx.window_ids().first().unwrap();
|
||||
|
@ -325,7 +324,7 @@ mod tests {
|
|||
pane.active_item().unwrap().project_path(cx),
|
||||
Some(file1.clone())
|
||||
);
|
||||
assert_eq!(pane.item_views().count(), 1);
|
||||
assert_eq!(pane.items().count(), 1);
|
||||
});
|
||||
|
||||
// Open the second entry
|
||||
|
@ -339,7 +338,7 @@ mod tests {
|
|||
pane.active_item().unwrap().project_path(cx),
|
||||
Some(file2.clone())
|
||||
);
|
||||
assert_eq!(pane.item_views().count(), 2);
|
||||
assert_eq!(pane.items().count(), 2);
|
||||
});
|
||||
|
||||
// Open the first entry again. The existing pane item is activated.
|
||||
|
@ -355,7 +354,7 @@ mod tests {
|
|||
pane.active_item().unwrap().project_path(cx),
|
||||
Some(file1.clone())
|
||||
);
|
||||
assert_eq!(pane.item_views().count(), 2);
|
||||
assert_eq!(pane.items().count(), 2);
|
||||
});
|
||||
|
||||
// Split the pane with the first entry, then open the second entry again.
|
||||
|
@ -394,7 +393,7 @@ mod tests {
|
|||
Some(file3.clone())
|
||||
);
|
||||
let pane_entries = pane
|
||||
.item_views()
|
||||
.items()
|
||||
.map(|i| i.project_path(cx).unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(pane_entries, &[file1, file2, file3]);
|
||||
|
@ -894,6 +893,52 @@ mod tests {
|
|||
(file3.clone(), DisplayPoint::new(0, 0))
|
||||
);
|
||||
|
||||
// Modify file to remove nav history location, and ensure duplicates are skipped
|
||||
editor1.update(cx, |editor, cx| {
|
||||
editor.select_display_ranges(&[DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)], cx)
|
||||
});
|
||||
|
||||
for _ in 0..5 {
|
||||
editor1.update(cx, |editor, cx| {
|
||||
editor
|
||||
.select_display_ranges(&[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)], cx);
|
||||
});
|
||||
editor1.update(cx, |editor, cx| {
|
||||
editor.select_display_ranges(
|
||||
&[DisplayPoint::new(13, 0)..DisplayPoint::new(13, 0)],
|
||||
cx,
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
editor1.update(cx, |editor, cx| {
|
||||
editor.transact(cx, |editor, cx| {
|
||||
editor.select_display_ranges(
|
||||
&[DisplayPoint::new(2, 0)..DisplayPoint::new(14, 0)],
|
||||
cx,
|
||||
);
|
||||
editor.insert("", cx);
|
||||
})
|
||||
});
|
||||
|
||||
editor1.update(cx, |editor, cx| {
|
||||
editor.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx)
|
||||
});
|
||||
workspace
|
||||
.update(cx, |w, cx| Pane::go_back(w, None, cx))
|
||||
.await;
|
||||
assert_eq!(
|
||||
active_location(&workspace, cx),
|
||||
(file1.clone(), DisplayPoint::new(2, 0))
|
||||
);
|
||||
workspace
|
||||
.update(cx, |w, cx| Pane::go_back(w, None, cx))
|
||||
.await;
|
||||
assert_eq!(
|
||||
active_location(&workspace, cx),
|
||||
(file1.clone(), DisplayPoint::new(3, 0))
|
||||
);
|
||||
|
||||
fn active_location(
|
||||
workspace: &ViewHandle<Workspace>,
|
||||
cx: &mut TestAppContext,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue