Merge pull request #446 from zed-industries/assists
Implement code actions
This commit is contained in:
commit
f742f63007
48 changed files with 6257 additions and 3410 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
@ -2776,6 +2776,8 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"anyhow",
|
||||
"async-pipe",
|
||||
"ctor",
|
||||
"env_logger",
|
||||
"futures",
|
||||
"gpui",
|
||||
"log",
|
||||
|
@ -2784,7 +2786,6 @@ dependencies = [
|
|||
"postage",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"simplelog",
|
||||
"smol",
|
||||
"unindent",
|
||||
"util",
|
||||
|
@ -3523,7 +3524,6 @@ dependencies = [
|
|||
"rpc",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"simplelog",
|
||||
"smol",
|
||||
"sum_tree",
|
||||
"tempdir",
|
||||
|
|
|
@ -184,7 +184,8 @@ impl Channel {
|
|||
rpc: Arc<Client>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let _subscription = rpc.subscribe_to_entity(details.id, cx, Self::handle_message_sent);
|
||||
let _subscription =
|
||||
rpc.add_entity_message_handler(details.id, cx, Self::handle_message_sent);
|
||||
|
||||
{
|
||||
let user_store = user_store.clone();
|
||||
|
@ -398,29 +399,23 @@ impl Channel {
|
|||
cursor
|
||||
}
|
||||
|
||||
fn handle_message_sent(
|
||||
&mut self,
|
||||
async fn handle_message_sent(
|
||||
this: ModelHandle<Self>,
|
||||
message: TypedEnvelope<proto::ChannelMessageSent>,
|
||||
_: Arc<Client>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
let user_store = self.user_store.clone();
|
||||
let user_store = this.read_with(&cx, |this, _| this.user_store.clone());
|
||||
let message = message
|
||||
.payload
|
||||
.message
|
||||
.ok_or_else(|| anyhow!("empty message"))?;
|
||||
|
||||
cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
let message = ChannelMessage::from_proto(message, &user_store, &mut cx).await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.insert_messages(SumTree::from_item(message, &()), cx)
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
.log_err()
|
||||
})
|
||||
.detach();
|
||||
let message = ChannelMessage::from_proto(message, &user_store, &mut cx).await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.insert_messages(SumTree::from_item(message, &()), cx)
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
@ -11,8 +11,8 @@ use async_tungstenite::tungstenite::{
|
|||
error::Error as WebsocketError,
|
||||
http::{Request, StatusCode},
|
||||
};
|
||||
use futures::StreamExt;
|
||||
use gpui::{action, AsyncAppContext, Entity, ModelContext, MutableAppContext, Task};
|
||||
use futures::{future::LocalBoxFuture, FutureExt, StreamExt};
|
||||
use gpui::{action, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task};
|
||||
use http::HttpClient;
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::RwLock;
|
||||
|
@ -20,10 +20,11 @@ use postage::watch;
|
|||
use rand::prelude::*;
|
||||
use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, RequestMessage};
|
||||
use std::{
|
||||
any::TypeId,
|
||||
any::{type_name, TypeId},
|
||||
collections::HashMap,
|
||||
convert::TryFrom,
|
||||
fmt::Write as _,
|
||||
future::Future,
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc, Weak,
|
||||
|
@ -123,14 +124,17 @@ pub enum Status {
|
|||
ReconnectionError { next_reconnection: Instant },
|
||||
}
|
||||
|
||||
type ModelHandler = Box<
|
||||
dyn Send
|
||||
+ Sync
|
||||
+ FnMut(Box<dyn AnyTypedEnvelope>, &AsyncAppContext) -> LocalBoxFuture<'static, Result<()>>,
|
||||
>;
|
||||
|
||||
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>>,
|
||||
model_handlers: HashMap<
|
||||
(TypeId, Option<u64>),
|
||||
Option<Box<dyn Send + Sync + FnMut(Box<dyn AnyTypedEnvelope>, &mut AsyncAppContext)>>,
|
||||
>,
|
||||
model_handlers: HashMap<(TypeId, Option<u64>), Option<ModelHandler>>,
|
||||
_maintain_connection: Option<Task<()>>,
|
||||
heartbeat_interval: Duration,
|
||||
}
|
||||
|
@ -262,7 +266,7 @@ impl Client {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn subscribe<T, M, F>(
|
||||
pub fn add_message_handler<T, M, F, Fut>(
|
||||
self: &Arc<Self>,
|
||||
cx: &mut ModelContext<M>,
|
||||
mut handler: F,
|
||||
|
@ -273,7 +277,8 @@ impl Client {
|
|||
F: 'static
|
||||
+ Send
|
||||
+ Sync
|
||||
+ FnMut(&mut M, TypedEnvelope<T>, Arc<Self>, &mut ModelContext<M>) -> Result<()>,
|
||||
+ FnMut(ModelHandle<M>, TypedEnvelope<T>, Arc<Self>, AsyncAppContext) -> Fut,
|
||||
Fut: 'static + Future<Output = Result<()>>,
|
||||
{
|
||||
let subscription_id = (TypeId::of::<T>(), None);
|
||||
let client = self.clone();
|
||||
|
@ -284,11 +289,15 @@ impl Client {
|
|||
Some(Box::new(move |envelope, cx| {
|
||||
if let Some(model) = model.upgrade(cx) {
|
||||
let envelope = envelope.into_any().downcast::<TypedEnvelope<T>>().unwrap();
|
||||
model.update(cx, |model, cx| {
|
||||
if let Err(error) = handler(model, *envelope, client.clone(), cx) {
|
||||
log::error!("error handling message: {}", error)
|
||||
}
|
||||
});
|
||||
handler(model, *envelope, client.clone(), cx.clone()).boxed_local()
|
||||
} else {
|
||||
async move {
|
||||
Err(anyhow!(
|
||||
"received message for {:?} but model was dropped",
|
||||
type_name::<M>()
|
||||
))
|
||||
}
|
||||
.boxed_local()
|
||||
}
|
||||
})),
|
||||
);
|
||||
|
@ -302,7 +311,7 @@ impl Client {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn subscribe_to_entity<T, M, F>(
|
||||
pub fn add_entity_message_handler<T, M, F, Fut>(
|
||||
self: &Arc<Self>,
|
||||
remote_id: u64,
|
||||
cx: &mut ModelContext<M>,
|
||||
|
@ -314,7 +323,8 @@ impl Client {
|
|||
F: 'static
|
||||
+ Send
|
||||
+ Sync
|
||||
+ FnMut(&mut M, TypedEnvelope<T>, Arc<Self>, &mut ModelContext<M>) -> Result<()>,
|
||||
+ FnMut(ModelHandle<M>, TypedEnvelope<T>, Arc<Self>, AsyncAppContext) -> Fut,
|
||||
Fut: 'static + Future<Output = Result<()>>,
|
||||
{
|
||||
let subscription_id = (TypeId::of::<T>(), Some(remote_id));
|
||||
let client = self.clone();
|
||||
|
@ -337,11 +347,15 @@ impl Client {
|
|||
Some(Box::new(move |envelope, cx| {
|
||||
if let Some(model) = model.upgrade(cx) {
|
||||
let envelope = envelope.into_any().downcast::<TypedEnvelope<T>>().unwrap();
|
||||
model.update(cx, |model, cx| {
|
||||
if let Err(error) = handler(model, *envelope, client.clone(), cx) {
|
||||
log::error!("error handling message: {}", error)
|
||||
}
|
||||
});
|
||||
handler(model, *envelope, client.clone(), cx.clone()).boxed_local()
|
||||
} else {
|
||||
async move {
|
||||
Err(anyhow!(
|
||||
"received message for {:?} but model was dropped",
|
||||
type_name::<M>()
|
||||
))
|
||||
}
|
||||
.boxed_local()
|
||||
}
|
||||
})),
|
||||
);
|
||||
|
@ -355,6 +369,44 @@ impl Client {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn add_entity_request_handler<T, M, F, Fut>(
|
||||
self: &Arc<Self>,
|
||||
remote_id: u64,
|
||||
cx: &mut ModelContext<M>,
|
||||
mut handler: F,
|
||||
) -> Subscription
|
||||
where
|
||||
T: EntityMessage + RequestMessage,
|
||||
M: Entity,
|
||||
F: 'static
|
||||
+ Send
|
||||
+ Sync
|
||||
+ FnMut(ModelHandle<M>, TypedEnvelope<T>, Arc<Self>, AsyncAppContext) -> Fut,
|
||||
Fut: 'static + Future<Output = Result<T::Response>>,
|
||||
{
|
||||
self.add_entity_message_handler(remote_id, cx, move |model, envelope, client, cx| {
|
||||
let receipt = envelope.receipt();
|
||||
let response = handler(model, envelope, client.clone(), cx);
|
||||
async move {
|
||||
match response.await {
|
||||
Ok(response) => {
|
||||
client.respond(receipt, response)?;
|
||||
Ok(())
|
||||
}
|
||||
Err(error) => {
|
||||
client.respond_with_error(
|
||||
receipt,
|
||||
proto::Error {
|
||||
message: error.to_string(),
|
||||
},
|
||||
)?;
|
||||
Err(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn has_keychain_credentials(&self, cx: &AsyncAppContext) -> bool {
|
||||
read_credentials_from_keychain(cx).is_some()
|
||||
}
|
||||
|
@ -442,7 +494,7 @@ impl Client {
|
|||
let (connection_id, handle_io, mut incoming) = self.peer.add_connection(conn).await;
|
||||
cx.foreground()
|
||||
.spawn({
|
||||
let mut cx = cx.clone();
|
||||
let cx = cx.clone();
|
||||
let this = self.clone();
|
||||
async move {
|
||||
while let Some(message) = incoming.next().await {
|
||||
|
@ -462,23 +514,41 @@ impl Client {
|
|||
if let Some(handler) = state.model_handlers.get_mut(&handler_key) {
|
||||
let mut handler = handler.take().unwrap();
|
||||
drop(state); // Avoid deadlocks if the handler interacts with rpc::Client
|
||||
let future = (handler)(message, &cx);
|
||||
{
|
||||
let mut state = this.state.write();
|
||||
if state.model_handlers.contains_key(&handler_key) {
|
||||
state.model_handlers.insert(handler_key, Some(handler));
|
||||
}
|
||||
}
|
||||
|
||||
let client_id = this.id;
|
||||
log::debug!(
|
||||
"rpc message received. client_id:{}, name:{}",
|
||||
this.id,
|
||||
client_id,
|
||||
type_name
|
||||
);
|
||||
(handler)(message, &mut cx);
|
||||
log::debug!(
|
||||
"rpc message handled. client_id:{}, name:{}",
|
||||
this.id,
|
||||
type_name
|
||||
);
|
||||
|
||||
let mut state = this.state.write();
|
||||
if state.model_handlers.contains_key(&handler_key) {
|
||||
state.model_handlers.insert(handler_key, Some(handler));
|
||||
}
|
||||
cx.foreground()
|
||||
.spawn(async move {
|
||||
match future.await {
|
||||
Ok(()) => {
|
||||
log::debug!(
|
||||
"rpc message handled. client_id:{}, name:{}",
|
||||
client_id,
|
||||
type_name
|
||||
);
|
||||
}
|
||||
Err(error) => {
|
||||
log::error!(
|
||||
"error handling rpc message. client_id:{}, name:{}, error:{}",
|
||||
client_id,
|
||||
type_name,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
} else {
|
||||
log::info!("unhandled message {}", type_name);
|
||||
}
|
||||
|
@ -715,16 +785,12 @@ impl Client {
|
|||
response
|
||||
}
|
||||
|
||||
pub fn respond<T: RequestMessage>(
|
||||
&self,
|
||||
receipt: Receipt<T>,
|
||||
response: T::Response,
|
||||
) -> Result<()> {
|
||||
fn respond<T: RequestMessage>(&self, receipt: Receipt<T>, response: T::Response) -> Result<()> {
|
||||
log::debug!("rpc respond. client_id: {}. name:{}", self.id, T::NAME);
|
||||
self.peer.respond(receipt, response)
|
||||
}
|
||||
|
||||
pub fn respond_with_error<T: RequestMessage>(
|
||||
fn respond_with_error<T: RequestMessage>(
|
||||
&self,
|
||||
receipt: Receipt<T>,
|
||||
error: proto::Error,
|
||||
|
@ -861,22 +927,22 @@ mod tests {
|
|||
let (mut done_tx1, mut done_rx1) = postage::oneshot::channel();
|
||||
let (mut done_tx2, mut done_rx2) = postage::oneshot::channel();
|
||||
let _subscription1 = model.update(&mut cx, |_, cx| {
|
||||
client.subscribe_to_entity(
|
||||
client.add_entity_message_handler(
|
||||
1,
|
||||
cx,
|
||||
move |_, _: TypedEnvelope<proto::UnshareProject>, _, _| {
|
||||
postage::sink::Sink::try_send(&mut done_tx1, ()).unwrap();
|
||||
Ok(())
|
||||
async { Ok(()) }
|
||||
},
|
||||
)
|
||||
});
|
||||
let _subscription2 = model.update(&mut cx, |_, cx| {
|
||||
client.subscribe_to_entity(
|
||||
client.add_entity_message_handler(
|
||||
2,
|
||||
cx,
|
||||
move |_, _: TypedEnvelope<proto::UnshareProject>, _, _| {
|
||||
postage::sink::Sink::try_send(&mut done_tx2, ()).unwrap();
|
||||
Ok(())
|
||||
async { Ok(()) }
|
||||
},
|
||||
)
|
||||
});
|
||||
|
@ -884,10 +950,10 @@ mod tests {
|
|||
// Ensure dropping a subscription for the same entity type still allows receiving of
|
||||
// messages for other entity IDs of the same type.
|
||||
let subscription3 = model.update(&mut cx, |_, cx| {
|
||||
client.subscribe_to_entity(
|
||||
client.add_entity_message_handler(
|
||||
3,
|
||||
cx,
|
||||
move |_, _: TypedEnvelope<proto::UnshareProject>, _, _| Ok(()),
|
||||
|_, _: TypedEnvelope<proto::UnshareProject>, _, _| async { Ok(()) },
|
||||
)
|
||||
});
|
||||
drop(subscription3);
|
||||
|
@ -910,16 +976,16 @@ mod tests {
|
|||
let (mut done_tx1, _done_rx1) = postage::oneshot::channel();
|
||||
let (mut done_tx2, mut done_rx2) = postage::oneshot::channel();
|
||||
let subscription1 = model.update(&mut cx, |_, cx| {
|
||||
client.subscribe(cx, move |_, _: TypedEnvelope<proto::Ping>, _, _| {
|
||||
client.add_message_handler(cx, move |_, _: TypedEnvelope<proto::Ping>, _, _| {
|
||||
postage::sink::Sink::try_send(&mut done_tx1, ()).unwrap();
|
||||
Ok(())
|
||||
async { Ok(()) }
|
||||
})
|
||||
});
|
||||
drop(subscription1);
|
||||
let _subscription2 = model.update(&mut cx, |_, cx| {
|
||||
client.subscribe(cx, move |_, _: TypedEnvelope<proto::Ping>, _, _| {
|
||||
client.add_message_handler(cx, move |_, _: TypedEnvelope<proto::Ping>, _, _| {
|
||||
postage::sink::Sink::try_send(&mut done_tx2, ()).unwrap();
|
||||
Ok(())
|
||||
async { Ok(()) }
|
||||
})
|
||||
});
|
||||
server.send(proto::Ping {});
|
||||
|
@ -937,12 +1003,12 @@ mod tests {
|
|||
let model = cx.add_model(|_| Model { subscription: None });
|
||||
let (mut done_tx, mut done_rx) = postage::oneshot::channel();
|
||||
model.update(&mut cx, |model, cx| {
|
||||
model.subscription = Some(client.subscribe(
|
||||
model.subscription = Some(client.add_message_handler(
|
||||
cx,
|
||||
move |model, _: TypedEnvelope<proto::Ping>, _, _| {
|
||||
model.subscription.take();
|
||||
move |model, _: TypedEnvelope<proto::Ping>, _, mut cx| {
|
||||
model.update(&mut cx, |model, _| model.subscription.take());
|
||||
postage::sink::Sink::try_send(&mut done_tx, ()).unwrap();
|
||||
Ok(())
|
||||
async { Ok(()) }
|
||||
},
|
||||
));
|
||||
});
|
||||
|
|
|
@ -58,11 +58,11 @@ impl UserStore {
|
|||
let (mut current_user_tx, current_user_rx) = watch::channel();
|
||||
let (mut update_contacts_tx, mut update_contacts_rx) =
|
||||
watch::channel::<Option<proto::UpdateContacts>>();
|
||||
let update_contacts_subscription = client.subscribe(
|
||||
let update_contacts_subscription = client.add_message_handler(
|
||||
cx,
|
||||
move |_: &mut Self, msg: TypedEnvelope<proto::UpdateContacts>, _, _| {
|
||||
let _ = update_contacts_tx.blocking_send(Some(msg.payload));
|
||||
Ok(())
|
||||
move |_: ModelHandle<Self>, msg: TypedEnvelope<proto::UpdateContacts>, _, _| {
|
||||
*update_contacts_tx.borrow_mut() = Some(msg.payload);
|
||||
async move { Ok(()) }
|
||||
},
|
||||
);
|
||||
Self {
|
||||
|
|
|
@ -7,7 +7,7 @@ use editor::{
|
|||
display_map::{BlockDisposition, BlockId, BlockProperties, RenderBlock},
|
||||
highlight_diagnostic_message,
|
||||
items::BufferItemHandle,
|
||||
Autoscroll, BuildSettings, Editor, ExcerptId, ExcerptProperties, MultiBuffer, ToOffset,
|
||||
Autoscroll, BuildSettings, Editor, ExcerptId, MultiBuffer, ToOffset,
|
||||
};
|
||||
use gpui::{
|
||||
action, elements::*, fonts::TextStyle, keymap::Binding, AnyViewHandle, AppContext, Entity,
|
||||
|
@ -28,7 +28,7 @@ use std::{
|
|||
sync::Arc,
|
||||
};
|
||||
use util::TryFutureExt;
|
||||
use workspace::{ItemNavHistory, Workspace};
|
||||
use workspace::{ItemNavHistory, ItemViewHandle as _, Workspace};
|
||||
|
||||
action!(Deploy);
|
||||
action!(OpenExcerpts);
|
||||
|
@ -68,7 +68,6 @@ struct ProjectDiagnosticsEditor {
|
|||
|
||||
struct PathState {
|
||||
path: ProjectPath,
|
||||
header: Option<BlockId>,
|
||||
diagnostic_groups: Vec<DiagnosticGroupState>,
|
||||
}
|
||||
|
||||
|
@ -145,7 +144,12 @@ impl ProjectDiagnosticsEditor {
|
|||
let excerpts = cx.add_model(|cx| MultiBuffer::new(project.read(cx).replica_id()));
|
||||
let build_settings = editor::settings_builder(excerpts.downgrade(), settings.clone());
|
||||
let editor = cx.add_view(|cx| {
|
||||
let mut editor = Editor::for_buffer(excerpts.clone(), build_settings.clone(), cx);
|
||||
let mut editor = Editor::for_buffer(
|
||||
excerpts.clone(),
|
||||
build_settings.clone(),
|
||||
Some(project.clone()),
|
||||
cx,
|
||||
);
|
||||
editor.set_vertical_scroll_margin(5, cx);
|
||||
editor
|
||||
});
|
||||
|
@ -187,7 +191,7 @@ impl ProjectDiagnosticsEditor {
|
|||
|
||||
for selection in editor.local_selections::<usize>(cx) {
|
||||
for (buffer, mut range) in
|
||||
excerpts.excerpted_buffers(selection.start..selection.end, cx)
|
||||
excerpts.range_to_buffer_ranges(selection.start..selection.end, cx)
|
||||
{
|
||||
if selection.reversed {
|
||||
mem::swap(&mut range.start, &mut range.end);
|
||||
|
@ -253,7 +257,6 @@ impl ProjectDiagnosticsEditor {
|
|||
ix,
|
||||
PathState {
|
||||
path: path.clone(),
|
||||
header: None,
|
||||
diagnostic_groups: Default::default(),
|
||||
},
|
||||
);
|
||||
|
@ -330,14 +333,15 @@ impl ProjectDiagnosticsEditor {
|
|||
Point::new(range.end.row + CONTEXT_LINE_COUNT, u32::MAX),
|
||||
Bias::Left,
|
||||
);
|
||||
let excerpt_id = excerpts.insert_excerpt_after(
|
||||
&prev_excerpt_id,
|
||||
ExcerptProperties {
|
||||
buffer: &buffer,
|
||||
range: excerpt_start..excerpt_end,
|
||||
},
|
||||
excerpts_cx,
|
||||
);
|
||||
let excerpt_id = excerpts
|
||||
.insert_excerpts_after(
|
||||
&prev_excerpt_id,
|
||||
buffer.clone(),
|
||||
[excerpt_start..excerpt_end],
|
||||
excerpts_cx,
|
||||
)
|
||||
.pop()
|
||||
.unwrap();
|
||||
|
||||
prev_excerpt_id = excerpt_id.clone();
|
||||
first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
|
||||
|
@ -360,14 +364,6 @@ impl ProjectDiagnosticsEditor {
|
|||
),
|
||||
disposition: BlockDisposition::Above,
|
||||
});
|
||||
} else {
|
||||
group_state.block_count += 1;
|
||||
blocks_to_add.push(BlockProperties {
|
||||
position: header_position,
|
||||
height: 1,
|
||||
render: context_header_renderer(self.build_settings.clone()),
|
||||
disposition: BlockDisposition::Above,
|
||||
});
|
||||
}
|
||||
|
||||
for entry in &group.entries[*start_ix..ix] {
|
||||
|
@ -416,27 +412,17 @@ impl ProjectDiagnosticsEditor {
|
|||
});
|
||||
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
blocks_to_remove.extend(path_state.header);
|
||||
editor.remove_blocks(blocks_to_remove, cx);
|
||||
let header_block = first_excerpt_id.map(|excerpt_id| BlockProperties {
|
||||
position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, language::Anchor::min()),
|
||||
height: 2,
|
||||
render: path_header_renderer(buffer, self.build_settings.clone()),
|
||||
disposition: BlockDisposition::Above,
|
||||
});
|
||||
let block_ids = editor.insert_blocks(
|
||||
blocks_to_add
|
||||
.into_iter()
|
||||
.map(|block| {
|
||||
let (excerpt_id, text_anchor) = block.position;
|
||||
BlockProperties {
|
||||
position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor),
|
||||
height: block.height,
|
||||
render: block.render,
|
||||
disposition: block.disposition,
|
||||
}
|
||||
})
|
||||
.chain(header_block.into_iter()),
|
||||
blocks_to_add.into_iter().map(|block| {
|
||||
let (excerpt_id, text_anchor) = block.position;
|
||||
BlockProperties {
|
||||
position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor),
|
||||
height: block.height,
|
||||
render: block.render,
|
||||
disposition: block.disposition,
|
||||
}
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
|
||||
|
@ -444,7 +430,6 @@ impl ProjectDiagnosticsEditor {
|
|||
for group_state in &mut groups_to_add {
|
||||
group_state.blocks = block_ids.by_ref().take(group_state.block_count).collect();
|
||||
}
|
||||
path_state.header = block_ids.next();
|
||||
});
|
||||
|
||||
for ix in group_ixs_to_remove.into_iter().rev() {
|
||||
|
@ -554,10 +539,8 @@ impl workspace::Item for ProjectDiagnostics {
|
|||
}
|
||||
|
||||
impl workspace::ItemView for ProjectDiagnosticsEditor {
|
||||
type ItemHandle = ModelHandle<ProjectDiagnostics>;
|
||||
|
||||
fn item_handle(&self, _: &AppContext) -> Self::ItemHandle {
|
||||
self.model.clone()
|
||||
fn item_id(&self, _: &AppContext) -> usize {
|
||||
self.model.id()
|
||||
}
|
||||
|
||||
fn tab_content(&self, style: &theme::Tab, _: &AppContext) -> ElementBox {
|
||||
|
@ -589,8 +572,12 @@ impl workspace::ItemView for ProjectDiagnosticsEditor {
|
|||
true
|
||||
}
|
||||
|
||||
fn save(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
|
||||
self.excerpts.update(cx, |excerpts, cx| excerpts.save(cx))
|
||||
fn save(
|
||||
&mut self,
|
||||
project: ModelHandle<Project>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
self.editor.save(project, cx)
|
||||
}
|
||||
|
||||
fn can_save_as(&self, _: &AppContext) -> bool {
|
||||
|
@ -655,51 +642,6 @@ impl workspace::ItemView for ProjectDiagnosticsEditor {
|
|||
}
|
||||
}
|
||||
|
||||
fn path_header_renderer(buffer: ModelHandle<Buffer>, build_settings: BuildSettings) -> RenderBlock {
|
||||
Arc::new(move |cx| {
|
||||
let settings = build_settings(cx);
|
||||
let style = settings.style.diagnostic_path_header;
|
||||
let font_size = (style.text_scale_factor * settings.style.text.font_size).round();
|
||||
|
||||
let mut filename = None;
|
||||
let mut path = None;
|
||||
if let Some(file) = buffer.read(&**cx).file() {
|
||||
filename = file
|
||||
.path()
|
||||
.file_name()
|
||||
.map(|f| f.to_string_lossy().to_string());
|
||||
path = file
|
||||
.path()
|
||||
.parent()
|
||||
.map(|p| p.to_string_lossy().to_string() + "/");
|
||||
}
|
||||
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Label::new(
|
||||
filename.unwrap_or_else(|| "untitled".to_string()),
|
||||
style.filename.text.clone().with_font_size(font_size),
|
||||
)
|
||||
.contained()
|
||||
.with_style(style.filename.container)
|
||||
.boxed(),
|
||||
)
|
||||
.with_children(path.map(|path| {
|
||||
Label::new(path, style.path.text.clone().with_font_size(font_size))
|
||||
.contained()
|
||||
.with_style(style.path.container)
|
||||
.boxed()
|
||||
}))
|
||||
.aligned()
|
||||
.left()
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.with_padding_left(cx.gutter_padding + cx.scroll_x * cx.em_width)
|
||||
.expanded()
|
||||
.named("path header block")
|
||||
})
|
||||
}
|
||||
|
||||
fn diagnostic_header_renderer(
|
||||
diagnostic: Diagnostic,
|
||||
build_settings: BuildSettings,
|
||||
|
@ -753,17 +695,6 @@ fn diagnostic_header_renderer(
|
|||
})
|
||||
}
|
||||
|
||||
fn context_header_renderer(build_settings: BuildSettings) -> RenderBlock {
|
||||
Arc::new(move |cx| {
|
||||
let settings = build_settings(cx);
|
||||
let text_style = settings.style.text.clone();
|
||||
Label::new("…".to_string(), text_style)
|
||||
.contained()
|
||||
.with_padding_left(cx.gutter_padding + cx.scroll_x * cx.em_width)
|
||||
.named("collapsed context")
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn render_summary(
|
||||
summary: &DiagnosticSummary,
|
||||
text_style: &TextStyle,
|
||||
|
@ -838,7 +769,10 @@ fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use editor::{display_map::BlockContext, DisplayPoint, EditorSnapshot};
|
||||
use editor::{
|
||||
display_map::{BlockContext, TransformBlock},
|
||||
DisplayPoint, EditorSnapshot,
|
||||
};
|
||||
use gpui::TestAppContext;
|
||||
use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16};
|
||||
use serde_json::json;
|
||||
|
@ -985,8 +919,9 @@ mod tests {
|
|||
[
|
||||
(0, "path header block".into()),
|
||||
(2, "diagnostic header".into()),
|
||||
(15, "diagnostic header".into()),
|
||||
(24, "collapsed context".into()),
|
||||
(15, "collapsed context".into()),
|
||||
(16, "diagnostic header".into()),
|
||||
(25, "collapsed context".into()),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
|
@ -1011,6 +946,7 @@ mod tests {
|
|||
" c(y);\n",
|
||||
"\n", // supporting diagnostic
|
||||
" d(x);\n",
|
||||
"\n", // context ellipsis
|
||||
// diagnostic group 2
|
||||
"\n", // primary message
|
||||
"\n", // padding
|
||||
|
@ -1073,8 +1009,9 @@ mod tests {
|
|||
(2, "diagnostic header".into()),
|
||||
(7, "path header block".into()),
|
||||
(9, "diagnostic header".into()),
|
||||
(22, "diagnostic header".into()),
|
||||
(31, "collapsed context".into()),
|
||||
(22, "collapsed context".into()),
|
||||
(23, "diagnostic header".into()),
|
||||
(32, "collapsed context".into()),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
|
@ -1110,6 +1047,7 @@ mod tests {
|
|||
" c(y);\n",
|
||||
"\n", // supporting diagnostic
|
||||
" d(x);\n",
|
||||
"\n", // collapsed context
|
||||
// diagnostic group 2
|
||||
"\n", // primary message
|
||||
"\n", // filename
|
||||
|
@ -1184,11 +1122,13 @@ mod tests {
|
|||
[
|
||||
(0, "path header block".into()),
|
||||
(2, "diagnostic header".into()),
|
||||
(7, "diagnostic header".into()),
|
||||
(12, "path header block".into()),
|
||||
(14, "diagnostic header".into()),
|
||||
(27, "diagnostic header".into()),
|
||||
(36, "collapsed context".into()),
|
||||
(7, "collapsed context".into()),
|
||||
(8, "diagnostic header".into()),
|
||||
(13, "path header block".into()),
|
||||
(15, "diagnostic header".into()),
|
||||
(28, "collapsed context".into()),
|
||||
(29, "diagnostic header".into()),
|
||||
(38, "collapsed context".into()),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
|
@ -1205,6 +1145,7 @@ mod tests {
|
|||
"const a: i32 = 'a';\n",
|
||||
"\n", // supporting diagnostic
|
||||
"const b: i32 = c;\n",
|
||||
"\n", // context ellipsis
|
||||
// diagnostic group 2
|
||||
"\n", // primary message
|
||||
"\n", // padding
|
||||
|
@ -1230,6 +1171,7 @@ mod tests {
|
|||
" c(y);\n",
|
||||
"\n", // supporting diagnostic
|
||||
" d(x);\n",
|
||||
"\n", // context ellipsis
|
||||
// diagnostic group 2
|
||||
"\n", // primary message
|
||||
"\n", // filename
|
||||
|
@ -1254,18 +1196,31 @@ mod tests {
|
|||
editor
|
||||
.blocks_in_range(0..editor.max_point().row())
|
||||
.filter_map(|(row, block)| {
|
||||
block
|
||||
.render(&BlockContext {
|
||||
cx,
|
||||
anchor_x: 0.,
|
||||
scroll_x: 0.,
|
||||
gutter_padding: 0.,
|
||||
gutter_width: 0.,
|
||||
line_height: 0.,
|
||||
em_width: 0.,
|
||||
})
|
||||
.name()
|
||||
.map(|s| (row, s.to_string()))
|
||||
let name = match block {
|
||||
TransformBlock::Custom(block) => block
|
||||
.render(&BlockContext {
|
||||
cx,
|
||||
anchor_x: 0.,
|
||||
scroll_x: 0.,
|
||||
gutter_padding: 0.,
|
||||
gutter_width: 0.,
|
||||
line_height: 0.,
|
||||
em_width: 0.,
|
||||
})
|
||||
.name()?
|
||||
.to_string(),
|
||||
TransformBlock::ExcerptHeader {
|
||||
starts_new_buffer, ..
|
||||
} => {
|
||||
if *starts_new_buffer {
|
||||
"path header block".to_string()
|
||||
} else {
|
||||
"collapsed context".to_string()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Some((row, name))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ test-support = [
|
|||
"text/test-support",
|
||||
"language/test-support",
|
||||
"gpui/test-support",
|
||||
"project/test-support",
|
||||
"util/test-support",
|
||||
]
|
||||
|
||||
|
@ -48,6 +49,7 @@ language = { path = "../language", features = ["test-support"] }
|
|||
lsp = { path = "../lsp", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
util = { path = "../util", features = ["test-support"] }
|
||||
project = { path = "../project", features = ["test-support"] }
|
||||
ctor = "0.1"
|
||||
env_logger = "0.8"
|
||||
rand = "0.8"
|
||||
|
|
|
@ -15,8 +15,8 @@ use tab_map::TabMap;
|
|||
use wrap_map::WrapMap;
|
||||
|
||||
pub use block_map::{
|
||||
AlignedBlock, BlockBufferRows as DisplayBufferRows, BlockChunks as DisplayChunks, BlockContext,
|
||||
BlockDisposition, BlockId, BlockProperties, RenderBlock,
|
||||
BlockBufferRows as DisplayBufferRows, BlockChunks as DisplayChunks, BlockContext,
|
||||
BlockDisposition, BlockId, BlockProperties, RenderBlock, TransformBlock,
|
||||
};
|
||||
|
||||
pub trait ToDisplayPoint {
|
||||
|
@ -43,13 +43,15 @@ impl DisplayMap {
|
|||
font_id: FontId,
|
||||
font_size: f32,
|
||||
wrap_width: Option<f32>,
|
||||
buffer_header_height: u8,
|
||||
excerpt_header_height: u8,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let buffer_subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
|
||||
let (fold_map, snapshot) = FoldMap::new(buffer.read(cx).snapshot(cx));
|
||||
let (tab_map, snapshot) = TabMap::new(snapshot, tab_size);
|
||||
let (wrap_map, snapshot) = WrapMap::new(snapshot, font_id, font_size, wrap_width, cx);
|
||||
let block_map = BlockMap::new(snapshot);
|
||||
let block_map = BlockMap::new(snapshot, buffer_header_height, excerpt_header_height);
|
||||
cx.observe(&wrap_map, |_, _, cx| cx.notify()).detach();
|
||||
DisplayMap {
|
||||
buffer,
|
||||
|
@ -318,7 +320,7 @@ impl DisplaySnapshot {
|
|||
pub fn blocks_in_range<'a>(
|
||||
&'a self,
|
||||
rows: Range<u32>,
|
||||
) -> impl Iterator<Item = (u32, &'a AlignedBlock)> {
|
||||
) -> impl Iterator<Item = (u32, &'a TransformBlock)> {
|
||||
self.blocks_snapshot.blocks_in_range(rows)
|
||||
}
|
||||
|
||||
|
@ -471,6 +473,8 @@ mod tests {
|
|||
|
||||
let font_cache = cx.font_cache().clone();
|
||||
let tab_size = rng.gen_range(1..=4);
|
||||
let buffer_start_excerpt_header_height = rng.gen_range(1..=5);
|
||||
let excerpt_header_height = rng.gen_range(1..=5);
|
||||
let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
|
||||
let font_id = font_cache
|
||||
.select_font(family_id, &Default::default())
|
||||
|
@ -497,7 +501,16 @@ mod tests {
|
|||
});
|
||||
|
||||
let map = cx.add_model(|cx| {
|
||||
DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, wrap_width, cx)
|
||||
DisplayMap::new(
|
||||
buffer.clone(),
|
||||
tab_size,
|
||||
font_id,
|
||||
font_size,
|
||||
wrap_width,
|
||||
buffer_start_excerpt_header_height,
|
||||
excerpt_header_height,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let mut notifications = observe(&map, &mut cx);
|
||||
let mut fold_count = 0;
|
||||
|
@ -711,7 +724,16 @@ mod tests {
|
|||
let text = "one two three four five\nsix seven eight";
|
||||
let buffer = MultiBuffer::build_simple(text, cx);
|
||||
let map = cx.add_model(|cx| {
|
||||
DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, wrap_width, cx)
|
||||
DisplayMap::new(
|
||||
buffer.clone(),
|
||||
tab_size,
|
||||
font_id,
|
||||
font_size,
|
||||
wrap_width,
|
||||
1,
|
||||
1,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
|
||||
|
@ -791,7 +813,7 @@ mod tests {
|
|||
.unwrap();
|
||||
let font_size = 14.0;
|
||||
let map = cx.add_model(|cx| {
|
||||
DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, cx)
|
||||
DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, 1, 1, cx)
|
||||
});
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(
|
||||
|
@ -870,8 +892,8 @@ mod tests {
|
|||
.unwrap();
|
||||
let font_size = 14.0;
|
||||
|
||||
let map =
|
||||
cx.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, cx));
|
||||
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)),
|
||||
vec![
|
||||
|
@ -958,8 +980,9 @@ mod tests {
|
|||
.unwrap();
|
||||
let font_size = 16.0;
|
||||
|
||||
let map = cx
|
||||
.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, Some(40.0), cx));
|
||||
let map = cx.add_model(|cx| {
|
||||
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)),
|
||||
[
|
||||
|
@ -1003,7 +1026,7 @@ mod tests {
|
|||
.unwrap();
|
||||
let font_size = 14.0;
|
||||
let map = cx.add_model(|cx| {
|
||||
DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, 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));
|
||||
|
||||
|
@ -1047,7 +1070,7 @@ mod tests {
|
|||
let font_size = 14.0;
|
||||
|
||||
let map = cx.add_model(|cx| {
|
||||
DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, 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));
|
||||
assert_eq!(map.text(), "✅ α\nβ \n🏀β γ");
|
||||
|
@ -1105,7 +1128,7 @@ mod tests {
|
|||
.unwrap();
|
||||
let font_size = 14.0;
|
||||
let map = cx.add_model(|cx| {
|
||||
DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, cx)
|
||||
DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, 1, 1, cx)
|
||||
});
|
||||
assert_eq!(
|
||||
map.update(cx, |map, cx| map.snapshot(cx)).max_point(),
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
use super::wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot};
|
||||
use crate::{Anchor, ToPoint as _};
|
||||
use collections::{HashMap, HashSet};
|
||||
use collections::{Bound, HashMap, HashSet};
|
||||
use gpui::{AppContext, ElementBox};
|
||||
use language::Chunk;
|
||||
use language::{BufferSnapshot, Chunk, Patch};
|
||||
use parking_lot::Mutex;
|
||||
use std::{
|
||||
cmp::{self, Ordering, Reverse},
|
||||
cell::RefCell,
|
||||
cmp::{self, Ordering},
|
||||
fmt::Debug,
|
||||
ops::{Deref, Range},
|
||||
sync::{
|
||||
|
@ -20,9 +21,11 @@ const NEWLINES: &'static [u8] = &[b'\n'; u8::MAX as usize];
|
|||
|
||||
pub struct BlockMap {
|
||||
next_block_id: AtomicUsize,
|
||||
wrap_snapshot: Mutex<WrapSnapshot>,
|
||||
wrap_snapshot: RefCell<WrapSnapshot>,
|
||||
blocks: Vec<Arc<Block>>,
|
||||
transforms: Mutex<SumTree<Transform>>,
|
||||
transforms: RefCell<SumTree<Transform>>,
|
||||
buffer_header_height: u8,
|
||||
excerpt_header_height: u8,
|
||||
}
|
||||
|
||||
pub struct BlockMapWriter<'a>(&'a mut BlockMap);
|
||||
|
@ -84,13 +87,46 @@ pub enum BlockDisposition {
|
|||
#[derive(Clone, Debug)]
|
||||
struct Transform {
|
||||
summary: TransformSummary,
|
||||
block: Option<AlignedBlock>,
|
||||
block: Option<TransformBlock>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AlignedBlock {
|
||||
block: Arc<Block>,
|
||||
column: u32,
|
||||
#[derive(Clone)]
|
||||
pub enum TransformBlock {
|
||||
Custom(Arc<Block>),
|
||||
ExcerptHeader {
|
||||
buffer: BufferSnapshot,
|
||||
range: Range<text::Anchor>,
|
||||
height: u8,
|
||||
starts_new_buffer: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl TransformBlock {
|
||||
fn disposition(&self) -> BlockDisposition {
|
||||
match self {
|
||||
TransformBlock::Custom(block) => block.disposition,
|
||||
TransformBlock::ExcerptHeader { .. } => BlockDisposition::Above,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn height(&self) -> u8 {
|
||||
match self {
|
||||
TransformBlock::Custom(block) => block.height,
|
||||
TransformBlock::ExcerptHeader { height, .. } => *height,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for TransformBlock {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Custom(block) => f.debug_struct("Custom").field("block", block).finish(),
|
||||
Self::ExcerptHeader { buffer, .. } => f
|
||||
.debug_struct("ExcerptHeader")
|
||||
.field("path", &buffer.path())
|
||||
.finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
|
@ -115,40 +151,71 @@ pub struct BlockBufferRows<'a> {
|
|||
}
|
||||
|
||||
impl BlockMap {
|
||||
pub fn new(wrap_snapshot: WrapSnapshot) -> Self {
|
||||
Self {
|
||||
pub fn new(
|
||||
wrap_snapshot: WrapSnapshot,
|
||||
buffer_header_height: u8,
|
||||
excerpt_header_height: u8,
|
||||
) -> Self {
|
||||
let row_count = wrap_snapshot.max_point().row() + 1;
|
||||
let map = Self {
|
||||
next_block_id: AtomicUsize::new(0),
|
||||
blocks: Vec::new(),
|
||||
transforms: Mutex::new(SumTree::from_item(
|
||||
Transform::isomorphic(wrap_snapshot.text_summary().lines.row + 1),
|
||||
&(),
|
||||
)),
|
||||
wrap_snapshot: Mutex::new(wrap_snapshot),
|
||||
}
|
||||
transforms: RefCell::new(SumTree::from_item(Transform::isomorphic(row_count), &())),
|
||||
wrap_snapshot: RefCell::new(wrap_snapshot.clone()),
|
||||
buffer_header_height,
|
||||
excerpt_header_height,
|
||||
};
|
||||
map.sync(
|
||||
&wrap_snapshot,
|
||||
Patch::new(vec![Edit {
|
||||
old: 0..row_count,
|
||||
new: 0..row_count,
|
||||
}]),
|
||||
);
|
||||
map
|
||||
}
|
||||
|
||||
pub fn read(&self, wrap_snapshot: WrapSnapshot, edits: Vec<WrapEdit>) -> BlockSnapshot {
|
||||
pub fn read(&self, wrap_snapshot: WrapSnapshot, edits: Patch<u32>) -> BlockSnapshot {
|
||||
self.sync(&wrap_snapshot, edits);
|
||||
*self.wrap_snapshot.lock() = wrap_snapshot.clone();
|
||||
*self.wrap_snapshot.borrow_mut() = wrap_snapshot.clone();
|
||||
BlockSnapshot {
|
||||
wrap_snapshot,
|
||||
transforms: self.transforms.lock().clone(),
|
||||
transforms: self.transforms.borrow().clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write(&mut self, wrap_snapshot: WrapSnapshot, edits: Vec<WrapEdit>) -> BlockMapWriter {
|
||||
pub fn write(&mut self, wrap_snapshot: WrapSnapshot, edits: Patch<u32>) -> BlockMapWriter {
|
||||
self.sync(&wrap_snapshot, edits);
|
||||
*self.wrap_snapshot.lock() = wrap_snapshot;
|
||||
*self.wrap_snapshot.borrow_mut() = wrap_snapshot;
|
||||
BlockMapWriter(self)
|
||||
}
|
||||
|
||||
fn sync(&self, wrap_snapshot: &WrapSnapshot, edits: Vec<WrapEdit>) {
|
||||
fn sync(&self, wrap_snapshot: &WrapSnapshot, mut edits: Patch<u32>) {
|
||||
let buffer = wrap_snapshot.buffer_snapshot();
|
||||
|
||||
// Handle changing the last excerpt if it is empty.
|
||||
if buffer.trailing_excerpt_update_count()
|
||||
!= self
|
||||
.wrap_snapshot
|
||||
.borrow()
|
||||
.buffer_snapshot()
|
||||
.trailing_excerpt_update_count()
|
||||
{
|
||||
let max_point = wrap_snapshot.max_point();
|
||||
let edit_start = wrap_snapshot.prev_row_boundary(max_point);
|
||||
let edit_end = max_point.row() + 1;
|
||||
edits = edits.compose([WrapEdit {
|
||||
old: edit_start..edit_end,
|
||||
new: edit_start..edit_end,
|
||||
}]);
|
||||
}
|
||||
|
||||
let edits = edits.into_inner();
|
||||
if edits.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let buffer = wrap_snapshot.buffer_snapshot();
|
||||
let mut transforms = self.transforms.lock();
|
||||
let mut transforms = self.transforms.borrow_mut();
|
||||
let mut new_transforms = SumTree::new();
|
||||
let old_row_count = transforms.summary().input_rows;
|
||||
let new_row_count = wrap_snapshot.max_point().row() + 1;
|
||||
|
@ -170,7 +237,7 @@ impl BlockMap {
|
|||
if transform
|
||||
.block
|
||||
.as_ref()
|
||||
.map_or(false, |b| b.disposition.is_below())
|
||||
.map_or(false, |b| b.disposition().is_below())
|
||||
{
|
||||
new_transforms.push(transform.clone(), &());
|
||||
cursor.next(&());
|
||||
|
@ -195,7 +262,7 @@ impl BlockMap {
|
|||
if transform
|
||||
.block
|
||||
.as_ref()
|
||||
.map_or(false, |b| b.disposition.is_below())
|
||||
.map_or(false, |b| b.disposition().is_below())
|
||||
{
|
||||
cursor.next(&());
|
||||
} else {
|
||||
|
@ -216,7 +283,7 @@ impl BlockMap {
|
|||
if transform
|
||||
.block
|
||||
.as_ref()
|
||||
.map_or(false, |b| b.disposition.is_below())
|
||||
.map_or(false, |b| b.disposition().is_below())
|
||||
{
|
||||
cursor.next(&());
|
||||
} else {
|
||||
|
@ -233,28 +300,30 @@ impl BlockMap {
|
|||
// Find the blocks within this edited region.
|
||||
let new_buffer_start =
|
||||
wrap_snapshot.to_point(WrapPoint::new(new_start.0, 0), Bias::Left);
|
||||
let start_anchor = buffer.anchor_before(new_buffer_start);
|
||||
let start_bound = Bound::Included(new_buffer_start);
|
||||
let start_block_ix = match self.blocks[last_block_ix..].binary_search_by(|probe| {
|
||||
probe
|
||||
.position
|
||||
.cmp(&start_anchor, &buffer)
|
||||
.unwrap()
|
||||
.to_point(&buffer)
|
||||
.cmp(&new_buffer_start)
|
||||
.then(Ordering::Greater)
|
||||
}) {
|
||||
Ok(ix) | Err(ix) => last_block_ix + ix,
|
||||
};
|
||||
|
||||
let end_bound;
|
||||
let end_block_ix = if new_end.0 > wrap_snapshot.max_point().row() {
|
||||
end_bound = Bound::Unbounded;
|
||||
self.blocks.len()
|
||||
} else {
|
||||
let new_buffer_end =
|
||||
wrap_snapshot.to_point(WrapPoint::new(new_end.0, 0), Bias::Left);
|
||||
let end_anchor = buffer.anchor_before(new_buffer_end);
|
||||
end_bound = Bound::Excluded(new_buffer_end);
|
||||
match self.blocks[start_block_ix..].binary_search_by(|probe| {
|
||||
probe
|
||||
.position
|
||||
.cmp(&end_anchor, &buffer)
|
||||
.unwrap()
|
||||
.to_point(&buffer)
|
||||
.cmp(&new_buffer_end)
|
||||
.then(Ordering::Greater)
|
||||
}) {
|
||||
Ok(ix) | Err(ix) => start_block_ix + ix,
|
||||
|
@ -268,7 +337,6 @@ impl BlockMap {
|
|||
.iter()
|
||||
.map(|block| {
|
||||
let mut position = block.position.to_point(&buffer);
|
||||
let column = wrap_snapshot.from_point(position, Bias::Left).column();
|
||||
match block.disposition {
|
||||
BlockDisposition::Above => position.column = 0,
|
||||
BlockDisposition::Below => {
|
||||
|
@ -276,25 +344,57 @@ impl BlockMap {
|
|||
}
|
||||
}
|
||||
let position = wrap_snapshot.from_point(position, Bias::Left);
|
||||
(position.row(), column, block.clone())
|
||||
(position.row(), TransformBlock::Custom(block.clone()))
|
||||
}),
|
||||
);
|
||||
blocks_in_edit.extend(
|
||||
buffer
|
||||
.excerpt_boundaries_in_range((start_bound, end_bound))
|
||||
.map(|excerpt_boundary| {
|
||||
(
|
||||
wrap_snapshot
|
||||
.from_point(Point::new(excerpt_boundary.row, 0), Bias::Left)
|
||||
.row(),
|
||||
TransformBlock::ExcerptHeader {
|
||||
buffer: excerpt_boundary.buffer,
|
||||
range: excerpt_boundary.range,
|
||||
height: if excerpt_boundary.starts_new_buffer {
|
||||
self.buffer_header_height
|
||||
} else {
|
||||
self.excerpt_header_height
|
||||
},
|
||||
starts_new_buffer: excerpt_boundary.starts_new_buffer,
|
||||
},
|
||||
)
|
||||
}),
|
||||
);
|
||||
|
||||
// When multiple blocks are on the same row, newer blocks appear above older
|
||||
// blocks. This is arbitrary, but we currently rely on it in ProjectDiagnosticsEditor.
|
||||
blocks_in_edit
|
||||
.sort_by_key(|(row, _, block)| (*row, block.disposition, Reverse(block.id)));
|
||||
// Place excerpt headers above custom blocks on the same row.
|
||||
blocks_in_edit.sort_unstable_by(|(row_a, block_a), (row_b, block_b)| {
|
||||
row_a.cmp(&row_b).then_with(|| match (block_a, block_b) {
|
||||
(
|
||||
TransformBlock::ExcerptHeader { .. },
|
||||
TransformBlock::ExcerptHeader { .. },
|
||||
) => Ordering::Equal,
|
||||
(TransformBlock::ExcerptHeader { .. }, _) => Ordering::Less,
|
||||
(_, TransformBlock::ExcerptHeader { .. }) => Ordering::Greater,
|
||||
(TransformBlock::Custom(block_a), TransformBlock::Custom(block_b)) => block_a
|
||||
.disposition
|
||||
.cmp(&block_b.disposition)
|
||||
.then_with(|| block_a.id.cmp(&block_b.id)),
|
||||
})
|
||||
});
|
||||
|
||||
// For each of these blocks, insert a new isomorphic transform preceding the block,
|
||||
// and then insert the block itself.
|
||||
for (block_row, column, block) in blocks_in_edit.drain(..) {
|
||||
let insertion_row = match block.disposition {
|
||||
for (block_row, block) in blocks_in_edit.drain(..) {
|
||||
let insertion_row = match block.disposition() {
|
||||
BlockDisposition::Above => block_row,
|
||||
BlockDisposition::Below => block_row + 1,
|
||||
};
|
||||
let extent_before_block = insertion_row - new_transforms.summary().input_rows;
|
||||
push_isomorphic(&mut new_transforms, extent_before_block);
|
||||
new_transforms.push(Transform::block(block, column), &());
|
||||
new_transforms.push(Transform::block(block), &());
|
||||
}
|
||||
|
||||
old_end = WrapRow(old_end.0.min(old_row_count));
|
||||
|
@ -375,8 +475,8 @@ impl<'a> BlockMapWriter<'a> {
|
|||
blocks: impl IntoIterator<Item = BlockProperties<Anchor>>,
|
||||
) -> Vec<BlockId> {
|
||||
let mut ids = Vec::new();
|
||||
let mut edits = Vec::<Edit<u32>>::new();
|
||||
let wrap_snapshot = &*self.0.wrap_snapshot.lock();
|
||||
let mut edits = Patch::default();
|
||||
let wrap_snapshot = &*self.0.wrap_snapshot.borrow();
|
||||
let buffer = wrap_snapshot.buffer_snapshot();
|
||||
|
||||
for block in blocks {
|
||||
|
@ -411,15 +511,10 @@ impl<'a> BlockMapWriter<'a> {
|
|||
}),
|
||||
);
|
||||
|
||||
if let Err(edit_ix) = edits.binary_search_by_key(&start_row, |edit| edit.old.start) {
|
||||
edits.insert(
|
||||
edit_ix,
|
||||
Edit {
|
||||
old: start_row..end_row,
|
||||
new: start_row..end_row,
|
||||
},
|
||||
);
|
||||
}
|
||||
edits = edits.compose([Edit {
|
||||
old: start_row..end_row,
|
||||
new: start_row..end_row,
|
||||
}]);
|
||||
}
|
||||
|
||||
self.0.sync(wrap_snapshot, edits);
|
||||
|
@ -427,9 +522,9 @@ impl<'a> BlockMapWriter<'a> {
|
|||
}
|
||||
|
||||
pub fn remove(&mut self, block_ids: HashSet<BlockId>) {
|
||||
let wrap_snapshot = &*self.0.wrap_snapshot.lock();
|
||||
let wrap_snapshot = &*self.0.wrap_snapshot.borrow();
|
||||
let buffer = wrap_snapshot.buffer_snapshot();
|
||||
let mut edits = Vec::new();
|
||||
let mut edits = Patch::default();
|
||||
let mut last_block_buffer_row = None;
|
||||
self.0.blocks.retain(|block| {
|
||||
if block_ids.contains(&block.id) {
|
||||
|
@ -524,7 +619,7 @@ impl BlockSnapshot {
|
|||
pub fn blocks_in_range<'a>(
|
||||
&'a self,
|
||||
rows: Range<u32>,
|
||||
) -> impl Iterator<Item = (u32, &'a AlignedBlock)> {
|
||||
) -> impl Iterator<Item = (u32, &'a TransformBlock)> {
|
||||
let mut cursor = self.transforms.cursor::<BlockRow>();
|
||||
cursor.seek(&BlockRow(rows.start), Bias::Right, &());
|
||||
std::iter::from_fn(move || {
|
||||
|
@ -644,7 +739,7 @@ impl BlockSnapshot {
|
|||
let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>();
|
||||
cursor.seek(&BlockRow(block_point.row), Bias::Right, &());
|
||||
if let Some(transform) = cursor.item() {
|
||||
match transform.block.as_ref().map(|b| b.disposition) {
|
||||
match transform.block.as_ref().map(|b| b.disposition()) {
|
||||
Some(BlockDisposition::Above) => WrapPoint::new(cursor.start().1 .0, 0),
|
||||
Some(BlockDisposition::Below) => {
|
||||
let wrap_row = cursor.start().1 .0 - 1;
|
||||
|
@ -673,13 +768,13 @@ impl Transform {
|
|||
}
|
||||
}
|
||||
|
||||
fn block(block: Arc<Block>, column: u32) -> Self {
|
||||
fn block(block: TransformBlock) -> Self {
|
||||
Self {
|
||||
summary: TransformSummary {
|
||||
input_rows: 0,
|
||||
output_rows: block.height as u32,
|
||||
output_rows: block.height() as u32,
|
||||
},
|
||||
block: Some(AlignedBlock { block, column }),
|
||||
block: Some(block),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -809,32 +904,6 @@ impl BlockDisposition {
|
|||
}
|
||||
}
|
||||
|
||||
impl AlignedBlock {
|
||||
pub fn height(&self) -> u32 {
|
||||
self.height as u32
|
||||
}
|
||||
|
||||
pub fn column(&self) -> u32 {
|
||||
self.column
|
||||
}
|
||||
|
||||
pub fn render(&self, cx: &BlockContext) -> ElementBox {
|
||||
self.render.lock()(cx)
|
||||
}
|
||||
|
||||
pub fn position(&self) -> &Anchor {
|
||||
&self.block.position
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for AlignedBlock {
|
||||
type Target = Block;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.block.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Deref for BlockContext<'a> {
|
||||
type Target = AppContext;
|
||||
|
||||
|
@ -843,6 +912,16 @@ impl<'a> Deref for BlockContext<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
impl Block {
|
||||
pub fn render(&self, cx: &BlockContext) -> ElementBox {
|
||||
self.render.lock()(cx)
|
||||
}
|
||||
|
||||
pub fn position(&self) -> &Anchor {
|
||||
&self.position
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Block {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Block")
|
||||
|
@ -911,9 +990,9 @@ mod tests {
|
|||
let (fold_map, folds_snapshot) = FoldMap::new(buffer_snapshot.clone());
|
||||
let (tab_map, tabs_snapshot) = TabMap::new(folds_snapshot.clone(), 1);
|
||||
let (wrap_map, wraps_snapshot) = WrapMap::new(tabs_snapshot, font_id, 14.0, None, cx);
|
||||
let mut block_map = BlockMap::new(wraps_snapshot.clone());
|
||||
let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
|
||||
|
||||
let mut writer = block_map.write(wraps_snapshot.clone(), vec![]);
|
||||
let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
|
||||
writer.insert(vec![
|
||||
BlockProperties {
|
||||
position: buffer_snapshot.anchor_after(Point::new(1, 0)),
|
||||
|
@ -935,15 +1014,15 @@ mod tests {
|
|||
},
|
||||
]);
|
||||
|
||||
let snapshot = block_map.read(wraps_snapshot, vec![]);
|
||||
let snapshot = block_map.read(wraps_snapshot, Default::default());
|
||||
assert_eq!(snapshot.text(), "aaa\n\n\n\nbbb\nccc\nddd\n\n\n");
|
||||
|
||||
let blocks = snapshot
|
||||
.blocks_in_range(0..8)
|
||||
.map(|(start_row, block)| {
|
||||
let block = block.as_custom().unwrap();
|
||||
(
|
||||
start_row..start_row + block.height(),
|
||||
block.column(),
|
||||
start_row..start_row + block.height as u32,
|
||||
block
|
||||
.render(&BlockContext {
|
||||
cx,
|
||||
|
@ -965,9 +1044,9 @@ mod tests {
|
|||
assert_eq!(
|
||||
blocks,
|
||||
&[
|
||||
(1..3, 2, "block 2".to_string()),
|
||||
(3..4, 0, "block 1".to_string()),
|
||||
(7..10, 3, "block 3".to_string()),
|
||||
(1..2, "block 1".to_string()),
|
||||
(2..4, "block 2".to_string()),
|
||||
(7..10, "block 3".to_string()),
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -1089,9 +1168,9 @@ mod tests {
|
|||
let (_, folds_snapshot) = FoldMap::new(buffer_snapshot.clone());
|
||||
let (_, tabs_snapshot) = TabMap::new(folds_snapshot.clone(), 1);
|
||||
let (_, wraps_snapshot) = WrapMap::new(tabs_snapshot, font_id, 14.0, Some(60.), cx);
|
||||
let mut block_map = BlockMap::new(wraps_snapshot.clone());
|
||||
let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
|
||||
|
||||
let mut writer = block_map.write(wraps_snapshot.clone(), vec![]);
|
||||
let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
|
||||
writer.insert(vec![
|
||||
BlockProperties {
|
||||
position: buffer_snapshot.anchor_after(Point::new(1, 12)),
|
||||
|
@ -1109,7 +1188,7 @@ mod tests {
|
|||
|
||||
// Blocks with an 'above' disposition go above their corresponding buffer line.
|
||||
// Blocks with a 'below' disposition go below their corresponding buffer line.
|
||||
let snapshot = block_map.read(wraps_snapshot, vec![]);
|
||||
let snapshot = block_map.read(wraps_snapshot, Default::default());
|
||||
assert_eq!(
|
||||
snapshot.text(),
|
||||
"one two \nthree\n\nfour five \nsix\n\nseven \neight"
|
||||
|
@ -1134,8 +1213,11 @@ mod tests {
|
|||
.select_font(family_id, &Default::default())
|
||||
.unwrap();
|
||||
let font_size = 14.0;
|
||||
let buffer_start_header_height = rng.gen_range(1..=5);
|
||||
let excerpt_header_height = rng.gen_range(1..=5);
|
||||
|
||||
log::info!("Wrap width: {:?}", wrap_width);
|
||||
log::info!("Excerpt Header Height: {:?}", excerpt_header_height);
|
||||
|
||||
let buffer = if rng.gen() {
|
||||
let len = rng.gen_range(0..10);
|
||||
|
@ -1151,8 +1233,12 @@ mod tests {
|
|||
let (tab_map, tabs_snapshot) = TabMap::new(folds_snapshot.clone(), tab_size);
|
||||
let (wrap_map, wraps_snapshot) =
|
||||
WrapMap::new(tabs_snapshot, font_id, font_size, wrap_width, cx);
|
||||
let mut block_map = BlockMap::new(wraps_snapshot);
|
||||
let mut expected_blocks = Vec::new();
|
||||
let mut block_map = BlockMap::new(
|
||||
wraps_snapshot.clone(),
|
||||
buffer_start_header_height,
|
||||
excerpt_header_height,
|
||||
);
|
||||
let mut custom_blocks = Vec::new();
|
||||
|
||||
for _ in 0..operations {
|
||||
let mut buffer_edits = Vec::new();
|
||||
|
@ -1205,15 +1291,15 @@ mod tests {
|
|||
let mut block_map = block_map.write(wraps_snapshot, wrap_edits);
|
||||
let block_ids = block_map.insert(block_properties.clone());
|
||||
for (block_id, props) in block_ids.into_iter().zip(block_properties) {
|
||||
expected_blocks.push((block_id, props));
|
||||
custom_blocks.push((block_id, props));
|
||||
}
|
||||
}
|
||||
40..=59 if !expected_blocks.is_empty() => {
|
||||
let block_count = rng.gen_range(1..=4.min(expected_blocks.len()));
|
||||
40..=59 if !custom_blocks.is_empty() => {
|
||||
let block_count = rng.gen_range(1..=4.min(custom_blocks.len()));
|
||||
let block_ids_to_remove = (0..block_count)
|
||||
.map(|_| {
|
||||
expected_blocks
|
||||
.remove(rng.gen_range(0..expected_blocks.len()))
|
||||
custom_blocks
|
||||
.remove(rng.gen_range(0..custom_blocks.len()))
|
||||
.0
|
||||
})
|
||||
.collect();
|
||||
|
@ -1229,9 +1315,9 @@ mod tests {
|
|||
}
|
||||
_ => {
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
let edit_count = rng.gen_range(1..=5);
|
||||
let mutation_count = rng.gen_range(1..=5);
|
||||
let subscription = buffer.subscribe();
|
||||
buffer.randomly_edit(&mut rng, edit_count, cx);
|
||||
buffer.randomly_mutate(&mut rng, mutation_count, cx);
|
||||
buffer_snapshot = buffer.snapshot(cx);
|
||||
buffer_edits.extend(subscription.consume());
|
||||
log::info!("buffer text: {:?}", buffer_snapshot.text());
|
||||
|
@ -1251,36 +1337,46 @@ mod tests {
|
|||
);
|
||||
log::info!("blocks text: {:?}", blocks_snapshot.text());
|
||||
|
||||
let mut sorted_blocks = expected_blocks
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|(id, block)| {
|
||||
let mut position = block.position.to_point(&buffer_snapshot);
|
||||
let column = wraps_snapshot.from_point(position, Bias::Left).column();
|
||||
match block.disposition {
|
||||
BlockDisposition::Above => {
|
||||
position.column = 0;
|
||||
}
|
||||
BlockDisposition::Below => {
|
||||
position.column = buffer_snapshot.line_len(position.row);
|
||||
}
|
||||
};
|
||||
let row = wraps_snapshot.from_point(position, Bias::Left).row();
|
||||
let mut expected_blocks = Vec::new();
|
||||
expected_blocks.extend(custom_blocks.iter().map(|(id, block)| {
|
||||
let mut position = block.position.to_point(&buffer_snapshot);
|
||||
match block.disposition {
|
||||
BlockDisposition::Above => {
|
||||
position.column = 0;
|
||||
}
|
||||
BlockDisposition::Below => {
|
||||
position.column = buffer_snapshot.line_len(position.row);
|
||||
}
|
||||
};
|
||||
let row = wraps_snapshot.from_point(position, Bias::Left).row();
|
||||
(
|
||||
row,
|
||||
ExpectedBlock::Custom {
|
||||
disposition: block.disposition,
|
||||
id: *id,
|
||||
height: block.height,
|
||||
},
|
||||
)
|
||||
}));
|
||||
expected_blocks.extend(buffer_snapshot.excerpt_boundaries_in_range(0..).map(
|
||||
|boundary| {
|
||||
let position =
|
||||
wraps_snapshot.from_point(Point::new(boundary.row, 0), Bias::Left);
|
||||
(
|
||||
id,
|
||||
BlockProperties {
|
||||
position: BlockPoint::new(row, column),
|
||||
height: block.height,
|
||||
disposition: block.disposition,
|
||||
render: block.render.clone(),
|
||||
position.row(),
|
||||
ExpectedBlock::ExcerptHeader {
|
||||
height: if boundary.starts_new_buffer {
|
||||
buffer_start_header_height
|
||||
} else {
|
||||
excerpt_header_height
|
||||
},
|
||||
starts_new_buffer: boundary.starts_new_buffer,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
sorted_blocks.sort_unstable_by_key(|(id, block)| {
|
||||
(block.position.row, block.disposition, Reverse(*id))
|
||||
});
|
||||
let mut sorted_blocks_iter = sorted_blocks.iter().peekable();
|
||||
},
|
||||
));
|
||||
expected_blocks.sort_unstable();
|
||||
let mut sorted_blocks_iter = expected_blocks.into_iter().peekable();
|
||||
|
||||
let input_buffer_rows = buffer_snapshot.buffer_rows(0).collect::<Vec<_>>();
|
||||
let mut expected_buffer_rows = Vec::new();
|
||||
|
@ -1297,16 +1393,17 @@ mod tests {
|
|||
.to_point(WrapPoint::new(row, 0), Bias::Left)
|
||||
.row as usize];
|
||||
|
||||
while let Some((block_id, block)) = sorted_blocks_iter.peek() {
|
||||
if block.position.row == row && block.disposition == BlockDisposition::Above {
|
||||
while let Some((block_row, block)) = sorted_blocks_iter.peek() {
|
||||
if *block_row == row && block.disposition() == BlockDisposition::Above {
|
||||
let (_, block) = sorted_blocks_iter.next().unwrap();
|
||||
let height = block.height() as usize;
|
||||
expected_block_positions
|
||||
.push((expected_text.matches('\n').count() as u32, *block_id));
|
||||
let text = "\n".repeat(block.height as usize);
|
||||
.push((expected_text.matches('\n').count() as u32, block));
|
||||
let text = "\n".repeat(height);
|
||||
expected_text.push_str(&text);
|
||||
for _ in 0..block.height {
|
||||
for _ in 0..height {
|
||||
expected_buffer_rows.push(None);
|
||||
}
|
||||
sorted_blocks_iter.next();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
@ -1316,16 +1413,17 @@ mod tests {
|
|||
expected_buffer_rows.push(if soft_wrapped { None } else { buffer_row });
|
||||
expected_text.push_str(input_line);
|
||||
|
||||
while let Some((block_id, block)) = sorted_blocks_iter.peek() {
|
||||
if block.position.row == row && block.disposition == BlockDisposition::Below {
|
||||
while let Some((block_row, block)) = sorted_blocks_iter.peek() {
|
||||
if *block_row == row && block.disposition() == BlockDisposition::Below {
|
||||
let (_, block) = sorted_blocks_iter.next().unwrap();
|
||||
let height = block.height() as usize;
|
||||
expected_block_positions
|
||||
.push((expected_text.matches('\n').count() as u32 + 1, *block_id));
|
||||
let text = "\n".repeat(block.height as usize);
|
||||
.push((expected_text.matches('\n').count() as u32 + 1, block));
|
||||
let text = "\n".repeat(height);
|
||||
expected_text.push_str(&text);
|
||||
for _ in 0..block.height {
|
||||
for _ in 0..height {
|
||||
expected_buffer_rows.push(None);
|
||||
}
|
||||
sorted_blocks_iter.next();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
@ -1337,7 +1435,7 @@ mod tests {
|
|||
for start_row in 0..expected_row_count {
|
||||
let expected_text = expected_lines[start_row..].join("\n");
|
||||
let actual_text = blocks_snapshot
|
||||
.chunks(start_row as u32..expected_row_count as u32, false)
|
||||
.chunks(start_row as u32..blocks_snapshot.max_point().row + 1, false)
|
||||
.map(|chunk| chunk.text)
|
||||
.collect::<String>();
|
||||
assert_eq!(
|
||||
|
@ -1356,7 +1454,7 @@ mod tests {
|
|||
assert_eq!(
|
||||
blocks_snapshot
|
||||
.blocks_in_range(0..(expected_row_count as u32))
|
||||
.map(|(row, block)| (row, block.id))
|
||||
.map(|(row, block)| (row, block.clone().into()))
|
||||
.collect::<Vec<_>>(),
|
||||
expected_block_positions
|
||||
);
|
||||
|
@ -1435,6 +1533,64 @@ mod tests {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
|
||||
enum ExpectedBlock {
|
||||
ExcerptHeader {
|
||||
height: u8,
|
||||
starts_new_buffer: bool,
|
||||
},
|
||||
Custom {
|
||||
disposition: BlockDisposition,
|
||||
id: BlockId,
|
||||
height: u8,
|
||||
},
|
||||
}
|
||||
|
||||
impl ExpectedBlock {
|
||||
fn height(&self) -> u8 {
|
||||
match self {
|
||||
ExpectedBlock::ExcerptHeader { height, .. } => *height,
|
||||
ExpectedBlock::Custom { height, .. } => *height,
|
||||
}
|
||||
}
|
||||
|
||||
fn disposition(&self) -> BlockDisposition {
|
||||
match self {
|
||||
ExpectedBlock::ExcerptHeader { .. } => BlockDisposition::Above,
|
||||
ExpectedBlock::Custom { disposition, .. } => *disposition,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TransformBlock> for ExpectedBlock {
|
||||
fn from(block: TransformBlock) -> Self {
|
||||
match block {
|
||||
TransformBlock::Custom(block) => ExpectedBlock::Custom {
|
||||
id: block.id,
|
||||
disposition: block.disposition,
|
||||
height: block.height,
|
||||
},
|
||||
TransformBlock::ExcerptHeader {
|
||||
height,
|
||||
starts_new_buffer,
|
||||
..
|
||||
} => ExpectedBlock::ExcerptHeader {
|
||||
height,
|
||||
starts_new_buffer,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TransformBlock {
|
||||
fn as_custom(&self) -> Option<&Block> {
|
||||
match self {
|
||||
TransformBlock::Custom(block) => Some(block),
|
||||
TransformBlock::ExcerptHeader { .. } => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BlockSnapshot {
|
||||
|
|
|
@ -107,14 +107,23 @@ impl<'a> FoldMapWriter<'a> {
|
|||
let buffer = self.0.buffer.lock().clone();
|
||||
for range in ranges.into_iter() {
|
||||
let range = range.start.to_offset(&buffer)..range.end.to_offset(&buffer);
|
||||
if range.start != range.end {
|
||||
let fold = Fold(buffer.anchor_after(range.start)..buffer.anchor_before(range.end));
|
||||
folds.push(fold);
|
||||
edits.push(text::Edit {
|
||||
old: range.clone(),
|
||||
new: range,
|
||||
});
|
||||
|
||||
// Ignore any empty ranges.
|
||||
if range.start == range.end {
|
||||
continue;
|
||||
}
|
||||
|
||||
// For now, ignore any ranges that span an excerpt boundary.
|
||||
let fold = Fold(buffer.anchor_after(range.start)..buffer.anchor_before(range.end));
|
||||
if fold.0.start.excerpt_id() != fold.0.end.excerpt_id() {
|
||||
continue;
|
||||
}
|
||||
|
||||
folds.push(fold);
|
||||
edits.push(text::Edit {
|
||||
old: range.clone(),
|
||||
new: range,
|
||||
});
|
||||
}
|
||||
|
||||
folds.sort_unstable_by(|a, b| sum_tree::SeekTarget::cmp(a, b, &buffer));
|
||||
|
@ -268,6 +277,8 @@ impl FoldMap {
|
|||
let mut buffer = self.buffer.lock();
|
||||
if buffer.parse_count() != new_buffer.parse_count()
|
||||
|| buffer.diagnostics_update_count() != new_buffer.diagnostics_update_count()
|
||||
|| buffer.trailing_excerpt_update_count()
|
||||
!= new_buffer.trailing_excerpt_update_count()
|
||||
{
|
||||
self.version.fetch_add(1, SeqCst);
|
||||
}
|
||||
|
@ -1281,7 +1292,7 @@ mod tests {
|
|||
_ => buffer.update(cx, |buffer, cx| {
|
||||
let subscription = buffer.subscribe();
|
||||
let edit_count = rng.gen_range(1..=5);
|
||||
buffer.randomly_edit(&mut rng, edit_count, cx);
|
||||
buffer.randomly_mutate(&mut rng, edit_count, cx);
|
||||
buffer_snapshot = buffer.snapshot(cx);
|
||||
let edits = subscription.consume().into_inner();
|
||||
log::info!("editing {:?}", edits);
|
||||
|
@ -1407,7 +1418,6 @@ mod tests {
|
|||
fold_row = snapshot
|
||||
.clip_point(FoldPoint::new(fold_row, 0), Bias::Right)
|
||||
.row();
|
||||
eprintln!("fold_row: {} of {}", fold_row, expected_buffer_rows.len());
|
||||
assert_eq!(
|
||||
snapshot.buffer_rows(fold_row).collect::<Vec<_>>(),
|
||||
expected_buffer_rows[(fold_row as usize)..],
|
||||
|
|
|
@ -106,7 +106,7 @@ impl WrapMap {
|
|||
tab_snapshot: TabSnapshot,
|
||||
edits: Vec<TabEdit>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> (WrapSnapshot, Vec<WrapEdit>) {
|
||||
) -> (WrapSnapshot, Patch<u32>) {
|
||||
if self.wrap_width.is_some() {
|
||||
self.pending_edits.push_back((tab_snapshot, edits));
|
||||
self.flush_edits(cx);
|
||||
|
@ -117,10 +117,7 @@ impl WrapMap {
|
|||
self.snapshot.interpolated = false;
|
||||
}
|
||||
|
||||
(
|
||||
self.snapshot.clone(),
|
||||
mem::take(&mut self.edits_since_sync).into_inner(),
|
||||
)
|
||||
(self.snapshot.clone(), mem::take(&mut self.edits_since_sync))
|
||||
}
|
||||
|
||||
pub fn set_font(&mut self, font_id: FontId, font_size: f32, cx: &mut ModelContext<Self>) {
|
||||
|
@ -588,10 +585,6 @@ impl WrapSnapshot {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn text_summary(&self) -> TextSummary {
|
||||
self.transforms.summary().output
|
||||
}
|
||||
|
||||
pub fn max_point(&self) -> WrapPoint {
|
||||
WrapPoint(self.transforms.summary().output.lines)
|
||||
}
|
||||
|
@ -955,10 +948,6 @@ impl WrapPoint {
|
|||
&mut self.0.row
|
||||
}
|
||||
|
||||
pub fn column(&self) -> u32 {
|
||||
self.0.column
|
||||
}
|
||||
|
||||
pub fn column_mut(&mut self) -> &mut u32 {
|
||||
&mut self.0.column
|
||||
}
|
||||
|
@ -1118,7 +1107,7 @@ mod tests {
|
|||
buffer.update(&mut cx, |buffer, cx| {
|
||||
let subscription = buffer.subscribe();
|
||||
let edit_count = rng.gen_range(1..=5);
|
||||
buffer.randomly_edit(&mut rng, edit_count, cx);
|
||||
buffer.randomly_mutate(&mut rng, edit_count, cx);
|
||||
buffer_snapshot = buffer.snapshot(cx);
|
||||
buffer_edits.extend(subscription.consume());
|
||||
});
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -3,11 +3,12 @@ use super::{
|
|||
Anchor, DisplayPoint, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, Input,
|
||||
Scroll, Select, SelectPhase, SoftWrap, ToPoint, MAX_LINE_LEN,
|
||||
};
|
||||
use crate::display_map::TransformBlock;
|
||||
use clock::ReplicaId;
|
||||
use collections::{BTreeMap, HashMap};
|
||||
use gpui::{
|
||||
color::Color,
|
||||
elements::layout_highlighted_chunks,
|
||||
elements::*,
|
||||
fonts::{HighlightStyle, Underline},
|
||||
geometry::{
|
||||
rect::RectF,
|
||||
|
@ -280,7 +281,7 @@ impl EditorElement {
|
|||
&mut self,
|
||||
bounds: RectF,
|
||||
visible_bounds: RectF,
|
||||
layout: &LayoutState,
|
||||
layout: &mut LayoutState,
|
||||
cx: &mut PaintContext,
|
||||
) {
|
||||
let scroll_top = layout.snapshot.scroll_position().y() * layout.line_height;
|
||||
|
@ -294,6 +295,14 @@ impl EditorElement {
|
|||
line.paint(line_origin, visible_bounds, layout.line_height, cx);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((row, indicator)) = layout.code_actions_indicator.as_mut() {
|
||||
let mut x = bounds.width() - layout.gutter_padding;
|
||||
let mut y = *row as f32 * layout.line_height - scroll_top;
|
||||
x += ((layout.gutter_padding + layout.text_offset.x()) - indicator.size().x()) / 2.;
|
||||
y += (layout.line_height - indicator.size().y()) / 2.;
|
||||
indicator.paint(bounds.origin() + vec2f(x, y), visible_bounds, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_text(
|
||||
|
@ -392,20 +401,20 @@ impl EditorElement {
|
|||
}
|
||||
cx.scene.pop_layer();
|
||||
|
||||
if let Some((position, completions_list)) = layout.completions.as_mut() {
|
||||
if let Some((position, context_menu)) = layout.context_menu.as_mut() {
|
||||
cx.scene.push_stacking_context(None);
|
||||
|
||||
let cursor_row_layout = &layout.line_layouts[(position.row() - start_row) as usize];
|
||||
let x = cursor_row_layout.x_for_index(position.column() as usize) - scroll_left;
|
||||
let y = (position.row() + 1) as f32 * layout.line_height - scroll_top;
|
||||
let mut list_origin = content_origin + vec2f(x, y);
|
||||
let list_height = completions_list.size().y();
|
||||
let list_height = context_menu.size().y();
|
||||
|
||||
if list_origin.y() + list_height > bounds.lower_left().y() {
|
||||
list_origin.set_y(list_origin.y() - layout.line_height - list_height);
|
||||
}
|
||||
|
||||
completions_list.paint(
|
||||
context_menu.paint(
|
||||
list_origin,
|
||||
RectF::from_points(Vector2F::zero(), vec2f(f32::MAX, f32::MAX)), // Let content bleed outside of editor
|
||||
cx,
|
||||
|
@ -649,33 +658,91 @@ impl EditorElement {
|
|||
line_layouts: &[text_layout::Line],
|
||||
cx: &mut LayoutContext,
|
||||
) -> Vec<(u32, ElementBox)> {
|
||||
let scroll_x = snapshot.scroll_position.x();
|
||||
snapshot
|
||||
.blocks_in_range(rows.clone())
|
||||
.map(|(start_row, block)| {
|
||||
let anchor_row = block
|
||||
.position()
|
||||
.to_point(&snapshot.buffer_snapshot)
|
||||
.to_display_point(snapshot)
|
||||
.row();
|
||||
.map(|(block_row, block)| {
|
||||
let mut element = match block {
|
||||
TransformBlock::Custom(block) => {
|
||||
let align_to = block
|
||||
.position()
|
||||
.to_point(&snapshot.buffer_snapshot)
|
||||
.to_display_point(snapshot);
|
||||
let anchor_x = text_x
|
||||
+ if rows.contains(&align_to.row()) {
|
||||
line_layouts[(align_to.row() - rows.start) as usize]
|
||||
.x_for_index(align_to.column() as usize)
|
||||
} else {
|
||||
layout_line(align_to.row(), snapshot, style, cx.text_layout_cache)
|
||||
.x_for_index(align_to.column() as usize)
|
||||
};
|
||||
|
||||
let anchor_x = text_x
|
||||
+ if rows.contains(&anchor_row) {
|
||||
line_layouts[(anchor_row - rows.start) as usize]
|
||||
.x_for_index(block.column() as usize)
|
||||
} else {
|
||||
layout_line(anchor_row, snapshot, style, cx.text_layout_cache)
|
||||
.x_for_index(block.column() as usize)
|
||||
};
|
||||
block.render(&BlockContext {
|
||||
cx,
|
||||
anchor_x,
|
||||
gutter_padding,
|
||||
line_height,
|
||||
scroll_x,
|
||||
gutter_width,
|
||||
em_width,
|
||||
})
|
||||
}
|
||||
TransformBlock::ExcerptHeader {
|
||||
buffer,
|
||||
starts_new_buffer,
|
||||
..
|
||||
} => {
|
||||
if *starts_new_buffer {
|
||||
let style = &self.settings.style.diagnostic_path_header;
|
||||
let font_size = (style.text_scale_factor
|
||||
* self.settings.style.text.font_size)
|
||||
.round();
|
||||
|
||||
let mut filename = None;
|
||||
let mut parent_path = None;
|
||||
if let Some(path) = buffer.path() {
|
||||
filename =
|
||||
path.file_name().map(|f| f.to_string_lossy().to_string());
|
||||
parent_path =
|
||||
path.parent().map(|p| p.to_string_lossy().to_string() + "/");
|
||||
}
|
||||
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Label::new(
|
||||
filename.unwrap_or_else(|| "untitled".to_string()),
|
||||
style.filename.text.clone().with_font_size(font_size),
|
||||
)
|
||||
.contained()
|
||||
.with_style(style.filename.container)
|
||||
.boxed(),
|
||||
)
|
||||
.with_children(parent_path.map(|path| {
|
||||
Label::new(
|
||||
path,
|
||||
style.path.text.clone().with_font_size(font_size),
|
||||
)
|
||||
.contained()
|
||||
.with_style(style.path.container)
|
||||
.boxed()
|
||||
}))
|
||||
.aligned()
|
||||
.left()
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.with_padding_left(gutter_padding + scroll_x * em_width)
|
||||
.expanded()
|
||||
.named("path header block")
|
||||
} else {
|
||||
let text_style = self.settings.style.text.clone();
|
||||
Label::new("…".to_string(), text_style)
|
||||
.contained()
|
||||
.with_padding_left(gutter_padding + scroll_x * em_width)
|
||||
.named("collapsed context")
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let mut element = block.render(&BlockContext {
|
||||
cx,
|
||||
anchor_x,
|
||||
gutter_padding,
|
||||
line_height,
|
||||
scroll_x: snapshot.scroll_position.x(),
|
||||
gutter_width,
|
||||
em_width,
|
||||
});
|
||||
element.layout(
|
||||
SizeConstraint {
|
||||
min: Vector2F::zero(),
|
||||
|
@ -683,7 +750,7 @@ impl EditorElement {
|
|||
},
|
||||
cx,
|
||||
);
|
||||
(start_row, element)
|
||||
(block_row, element)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
@ -859,7 +926,8 @@ impl Element for EditorElement {
|
|||
max_row.saturating_sub(1) as f32,
|
||||
);
|
||||
|
||||
let mut completions = None;
|
||||
let mut context_menu = None;
|
||||
let mut code_actions_indicator = None;
|
||||
self.update_view(cx.app, |view, cx| {
|
||||
let clamped = view.clamp_scroll_left(scroll_max.x());
|
||||
let autoscrolled;
|
||||
|
@ -880,21 +948,24 @@ impl Element for EditorElement {
|
|||
snapshot = view.snapshot(cx);
|
||||
}
|
||||
|
||||
if view.has_completions() {
|
||||
let newest_selection_head = view
|
||||
.newest_selection::<usize>(&snapshot.buffer_snapshot)
|
||||
.head()
|
||||
.to_display_point(&snapshot);
|
||||
let newest_selection_head = view
|
||||
.newest_selection::<usize>(&snapshot.buffer_snapshot)
|
||||
.head()
|
||||
.to_display_point(&snapshot);
|
||||
|
||||
if (start_row..end_row).contains(&newest_selection_head.row()) {
|
||||
let list = view.render_completions(cx).unwrap();
|
||||
completions = Some((newest_selection_head, list));
|
||||
if (start_row..end_row).contains(&newest_selection_head.row()) {
|
||||
if view.context_menu_visible() {
|
||||
context_menu = view.render_context_menu(newest_selection_head, cx);
|
||||
}
|
||||
|
||||
code_actions_indicator = view
|
||||
.render_code_actions_indicator(cx)
|
||||
.map(|indicator| (newest_selection_head.row(), indicator));
|
||||
}
|
||||
});
|
||||
|
||||
if let Some((_, completions_list)) = completions.as_mut() {
|
||||
completions_list.layout(
|
||||
if let Some((_, context_menu)) = context_menu.as_mut() {
|
||||
context_menu.layout(
|
||||
SizeConstraint {
|
||||
min: Vector2F::zero(),
|
||||
max: vec2f(
|
||||
|
@ -906,6 +977,13 @@ impl Element for EditorElement {
|
|||
);
|
||||
}
|
||||
|
||||
if let Some((_, indicator)) = code_actions_indicator.as_mut() {
|
||||
indicator.layout(
|
||||
SizeConstraint::strict_along(Axis::Vertical, line_height * 0.618),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
let blocks = self.layout_blocks(
|
||||
start_row..end_row,
|
||||
&snapshot,
|
||||
|
@ -940,7 +1018,8 @@ impl Element for EditorElement {
|
|||
em_width,
|
||||
em_advance,
|
||||
selections,
|
||||
completions,
|
||||
context_menu,
|
||||
code_actions_indicator,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -989,8 +1068,14 @@ impl Element for EditorElement {
|
|||
paint: &mut PaintState,
|
||||
cx: &mut EventContext,
|
||||
) -> bool {
|
||||
if let Some((_, completion_list)) = &mut layout.completions {
|
||||
if completion_list.dispatch_event(event, cx) {
|
||||
if let Some((_, context_menu)) = &mut layout.context_menu {
|
||||
if context_menu.dispatch_event(event, cx) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((_, indicator)) = &mut layout.code_actions_indicator {
|
||||
if indicator.dispatch_event(event, cx) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -1051,7 +1136,8 @@ pub struct LayoutState {
|
|||
highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
|
||||
selections: HashMap<ReplicaId, Vec<text::Selection<DisplayPoint>>>,
|
||||
text_offset: Vector2F,
|
||||
completions: Option<(DisplayPoint, ElementBox)>,
|
||||
context_menu: Option<(DisplayPoint, ElementBox)>,
|
||||
code_actions_indicator: Option<(u32, ElementBox)>,
|
||||
}
|
||||
|
||||
fn layout_line(
|
||||
|
@ -1298,6 +1384,7 @@ mod tests {
|
|||
let settings = settings.clone();
|
||||
Arc::new(move |_| settings.clone())
|
||||
},
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
|
|
@ -11,7 +11,7 @@ use std::path::PathBuf;
|
|||
use std::rc::Rc;
|
||||
use std::{cell::RefCell, fmt::Write};
|
||||
use text::{Point, Selection};
|
||||
use util::TryFutureExt;
|
||||
use util::ResultExt;
|
||||
use workspace::{
|
||||
ItemHandle, ItemNavHistory, ItemView, ItemViewHandle, NavHistory, PathOpener, Settings,
|
||||
StatusItemView, WeakItemHandle, Workspace,
|
||||
|
@ -25,6 +25,12 @@ 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,
|
||||
|
@ -55,6 +61,7 @@ impl ItemHandle for BufferItemHandle {
|
|||
let mut editor = Editor::for_buffer(
|
||||
buffer,
|
||||
crate::settings_builder(weak_buffer, workspace.settings()),
|
||||
Some(workspace.project().clone()),
|
||||
cx,
|
||||
);
|
||||
editor.nav_history = Some(ItemNavHistory::new(nav_history, &cx.handle()));
|
||||
|
@ -86,6 +93,48 @@ impl ItemHandle for BufferItemHandle {
|
|||
}
|
||||
}
|
||||
|
||||
impl ItemHandle for MultiBufferItemHandle {
|
||||
fn add_view(
|
||||
&self,
|
||||
window_id: usize,
|
||||
workspace: &Workspace,
|
||||
nav_history: Rc<RefCell<NavHistory>>,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Box<dyn ItemViewHandle> {
|
||||
let weak_buffer = self.0.downgrade();
|
||||
Box::new(cx.add_view(window_id, |cx| {
|
||||
let mut editor = Editor::for_buffer(
|
||||
self.0.clone(),
|
||||
crate::settings_builder(weak_buffer, workspace.settings()),
|
||||
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
|
||||
|
@ -98,11 +147,25 @@ impl WeakItemHandle for WeakBufferItemHandle {
|
|||
}
|
||||
}
|
||||
|
||||
impl ItemView for Editor {
|
||||
type ItemHandle = BufferItemHandle;
|
||||
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 item_handle(&self, cx: &AppContext) -> Self::ItemHandle {
|
||||
BufferItemHandle(self.buffer.read(cx).as_singleton().unwrap())
|
||||
fn id(&self) -> usize {
|
||||
self.0.id()
|
||||
}
|
||||
}
|
||||
|
||||
impl ItemView for Editor {
|
||||
fn item_id(&self, cx: &AppContext) -> usize {
|
||||
if let Some(buffer) = self.buffer.read(cx).as_singleton() {
|
||||
buffer.id()
|
||||
} else {
|
||||
self.buffer.id()
|
||||
}
|
||||
}
|
||||
|
||||
fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) {
|
||||
|
@ -141,9 +204,8 @@ impl ItemView for Editor {
|
|||
}
|
||||
|
||||
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if let Some(selection) = self.newest_anchor_selection() {
|
||||
self.push_to_nav_history(selection.head(), None, cx);
|
||||
}
|
||||
let selection = self.newest_anchor_selection();
|
||||
self.push_to_nav_history(selection.head(), None, cx);
|
||||
}
|
||||
|
||||
fn is_dirty(&self, cx: &AppContext) -> bool {
|
||||
|
@ -155,25 +217,39 @@ impl ItemView for Editor {
|
|||
}
|
||||
|
||||
fn can_save(&self, cx: &AppContext) -> bool {
|
||||
self.project_path(cx).is_some()
|
||||
!self.buffer().read(cx).is_singleton() || self.project_path(cx).is_some()
|
||||
}
|
||||
|
||||
fn save(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
|
||||
fn save(
|
||||
&mut self,
|
||||
project: ModelHandle<Project>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let buffer = self.buffer().clone();
|
||||
cx.spawn(|editor, mut cx| async move {
|
||||
buffer
|
||||
.update(&mut cx, |buffer, cx| buffer.format(cx).log_err())
|
||||
.await;
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
let buffers = buffer.read(cx).all_buffers();
|
||||
let transaction = project.update(cx, |project, cx| project.format(buffers, true, cx));
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let transaction = transaction.await.log_err();
|
||||
this.update(&mut cx, |editor, cx| {
|
||||
editor.request_autoscroll(Autoscroll::Fit, cx)
|
||||
});
|
||||
buffer.update(&mut cx, |buffer, cx| buffer.save(cx)).await?;
|
||||
buffer
|
||||
.update(&mut cx, |buffer, cx| {
|
||||
if let Some(transaction) = transaction {
|
||||
if !buffer.is_singleton() {
|
||||
buffer.push_transaction(&transaction.0);
|
||||
}
|
||||
}
|
||||
|
||||
buffer.save(cx)
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn can_save_as(&self, _: &AppContext) -> bool {
|
||||
true
|
||||
fn can_save_as(&self, cx: &AppContext) -> bool {
|
||||
self.buffer().read(cx).is_singleton()
|
||||
}
|
||||
|
||||
fn save_as(
|
||||
|
@ -331,7 +407,7 @@ impl View for DiagnosticMessage {
|
|||
if let Some(diagnostic) = &self.diagnostic {
|
||||
let theme = &self.settings.borrow().theme.workspace.status_bar;
|
||||
Label::new(
|
||||
diagnostic.message.lines().next().unwrap().to_string(),
|
||||
diagnostic.message.split('\n').next().unwrap().to_string(),
|
||||
theme.diagnostic_message.clone(),
|
||||
)
|
||||
.contained()
|
||||
|
|
|
@ -225,13 +225,8 @@ pub fn surrounding_word(map: &DisplaySnapshot, position: DisplayPoint) -> Range<
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
display_map::{BlockDisposition, BlockProperties},
|
||||
Buffer, DisplayMap, ExcerptProperties, MultiBuffer,
|
||||
};
|
||||
use gpui::{elements::Empty, Element};
|
||||
use crate::{Buffer, DisplayMap, MultiBuffer};
|
||||
use language::Point;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[gpui::test]
|
||||
fn test_move_up_and_down_with_excerpts(cx: &mut gpui::MutableAppContext) {
|
||||
|
@ -242,62 +237,24 @@ mod tests {
|
|||
.unwrap();
|
||||
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, "abc\ndefg\nhijkl\nmn", cx));
|
||||
let mut excerpt1_header_position = None;
|
||||
let mut excerpt2_header_position = None;
|
||||
let multibuffer = cx.add_model(|cx| {
|
||||
let mut multibuffer = MultiBuffer::new(0);
|
||||
let excerpt1_id = multibuffer.push_excerpt(
|
||||
ExcerptProperties {
|
||||
buffer: &buffer,
|
||||
range: Point::new(0, 0)..Point::new(1, 4),
|
||||
},
|
||||
multibuffer.push_excerpts(
|
||||
buffer.clone(),
|
||||
[
|
||||
Point::new(0, 0)..Point::new(1, 4),
|
||||
Point::new(2, 0)..Point::new(3, 2),
|
||||
],
|
||||
cx,
|
||||
);
|
||||
let excerpt2_id = multibuffer.push_excerpt(
|
||||
ExcerptProperties {
|
||||
buffer: &buffer,
|
||||
range: Point::new(2, 0)..Point::new(3, 2),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
|
||||
excerpt1_header_position = Some(
|
||||
multibuffer
|
||||
.read(cx)
|
||||
.anchor_in_excerpt(excerpt1_id, language::Anchor::min()),
|
||||
);
|
||||
excerpt2_header_position = Some(
|
||||
multibuffer
|
||||
.read(cx)
|
||||
.anchor_in_excerpt(excerpt2_id, language::Anchor::min()),
|
||||
);
|
||||
multibuffer
|
||||
});
|
||||
|
||||
let display_map =
|
||||
cx.add_model(|cx| DisplayMap::new(multibuffer, 2, font_id, 14.0, None, cx));
|
||||
display_map.update(cx, |display_map, cx| {
|
||||
display_map.insert_blocks(
|
||||
[
|
||||
BlockProperties {
|
||||
position: excerpt1_header_position.unwrap(),
|
||||
height: 2,
|
||||
render: Arc::new(|_| Empty::new().boxed()),
|
||||
disposition: BlockDisposition::Above,
|
||||
},
|
||||
BlockProperties {
|
||||
position: excerpt2_header_position.unwrap(),
|
||||
height: 3,
|
||||
render: Arc::new(|_| Empty::new().boxed()),
|
||||
disposition: BlockDisposition::Above,
|
||||
},
|
||||
],
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.add_model(|cx| DisplayMap::new(multibuffer, 2, font_id, 14.0, None, 2, 2, cx));
|
||||
|
||||
let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\n\nhijkl\nmn");
|
||||
assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn");
|
||||
|
||||
// Can't move up into the first excerpt's header
|
||||
assert_eq!(
|
||||
|
@ -321,22 +278,22 @@ mod tests {
|
|||
|
||||
// Move up and down across second excerpt's header
|
||||
assert_eq!(
|
||||
up(&snapshot, DisplayPoint::new(7, 5), SelectionGoal::Column(5)).unwrap(),
|
||||
up(&snapshot, DisplayPoint::new(6, 5), SelectionGoal::Column(5)).unwrap(),
|
||||
(DisplayPoint::new(3, 4), SelectionGoal::Column(5)),
|
||||
);
|
||||
assert_eq!(
|
||||
down(&snapshot, DisplayPoint::new(3, 4), SelectionGoal::Column(5)).unwrap(),
|
||||
(DisplayPoint::new(7, 5), SelectionGoal::Column(5)),
|
||||
(DisplayPoint::new(6, 5), SelectionGoal::Column(5)),
|
||||
);
|
||||
|
||||
// Can't move down off the end
|
||||
assert_eq!(
|
||||
down(&snapshot, DisplayPoint::new(8, 0), SelectionGoal::Column(0)).unwrap(),
|
||||
(DisplayPoint::new(8, 2), SelectionGoal::Column(2)),
|
||||
down(&snapshot, DisplayPoint::new(7, 0), SelectionGoal::Column(0)).unwrap(),
|
||||
(DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
|
||||
);
|
||||
assert_eq!(
|
||||
down(&snapshot, DisplayPoint::new(8, 2), SelectionGoal::Column(2)).unwrap(),
|
||||
(DisplayPoint::new(8, 2), SelectionGoal::Column(2)),
|
||||
down(&snapshot, DisplayPoint::new(7, 2), SelectionGoal::Column(2)).unwrap(),
|
||||
(DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -351,8 +308,8 @@ mod tests {
|
|||
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, 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)),
|
||||
|
@ -407,8 +364,8 @@ mod tests {
|
|||
.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, 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!(
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -9,7 +9,7 @@ use text::{rope::TextDimension, Point};
|
|||
|
||||
#[derive(Clone, Eq, PartialEq, Debug, Hash)]
|
||||
pub struct Anchor {
|
||||
pub(crate) buffer_id: usize,
|
||||
pub(crate) buffer_id: Option<usize>,
|
||||
pub(crate) excerpt_id: ExcerptId,
|
||||
pub(crate) text_anchor: text::Anchor,
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ pub struct Anchor {
|
|||
impl Anchor {
|
||||
pub fn min() -> Self {
|
||||
Self {
|
||||
buffer_id: 0,
|
||||
buffer_id: None,
|
||||
excerpt_id: ExcerptId::min(),
|
||||
text_anchor: text::Anchor::min(),
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ impl Anchor {
|
|||
|
||||
pub fn max() -> Self {
|
||||
Self {
|
||||
buffer_id: 0,
|
||||
buffer_id: None,
|
||||
excerpt_id: ExcerptId::max(),
|
||||
text_anchor: text::Anchor::max(),
|
||||
}
|
||||
|
@ -46,11 +46,11 @@ impl Anchor {
|
|||
// Even though the anchor refers to a valid excerpt the underlying buffer might have
|
||||
// changed. In that case, treat the anchor as if it were at the start of that
|
||||
// excerpt.
|
||||
if self.buffer_id == buffer_id && other.buffer_id == buffer_id {
|
||||
if self.buffer_id == Some(buffer_id) && other.buffer_id == Some(buffer_id) {
|
||||
self.text_anchor.cmp(&other.text_anchor, buffer_snapshot)
|
||||
} else if self.buffer_id == buffer_id {
|
||||
} else if self.buffer_id == Some(buffer_id) {
|
||||
Ok(Ordering::Greater)
|
||||
} else if other.buffer_id == buffer_id {
|
||||
} else if other.buffer_id == Some(buffer_id) {
|
||||
Ok(Ordering::Less)
|
||||
} else {
|
||||
Ok(Ordering::Equal)
|
||||
|
@ -68,7 +68,7 @@ impl Anchor {
|
|||
if let Some((buffer_id, buffer_snapshot)) =
|
||||
snapshot.buffer_snapshot_for_excerpt(&self.excerpt_id)
|
||||
{
|
||||
if self.buffer_id == buffer_id {
|
||||
if self.buffer_id == Some(buffer_id) {
|
||||
return Self {
|
||||
buffer_id: self.buffer_id,
|
||||
excerpt_id: self.excerpt_id.clone(),
|
||||
|
@ -85,7 +85,7 @@ impl Anchor {
|
|||
if let Some((buffer_id, buffer_snapshot)) =
|
||||
snapshot.buffer_snapshot_for_excerpt(&self.excerpt_id)
|
||||
{
|
||||
if self.buffer_id == buffer_id {
|
||||
if self.buffer_id == Some(buffer_id) {
|
||||
return Self {
|
||||
buffer_id: self.buffer_id,
|
||||
excerpt_id: self.excerpt_id.clone(),
|
||||
|
|
|
@ -355,11 +355,8 @@ impl FindBar {
|
|||
if let Some(mut index) = self.active_match_index {
|
||||
if let Some(editor) = self.active_editor.as_ref() {
|
||||
editor.update(cx, |editor, cx| {
|
||||
let newest_selection = editor.newest_anchor_selection().cloned();
|
||||
if let Some(((_, ranges), newest_selection)) = editor
|
||||
.highlighted_ranges_for_type::<Self>()
|
||||
.zip(newest_selection)
|
||||
{
|
||||
let newest_selection = editor.newest_anchor_selection().clone();
|
||||
if let Some((_, ranges)) = editor.highlighted_ranges_for_type::<Self>() {
|
||||
let position = newest_selection.head();
|
||||
let buffer = editor.buffer().read(cx).read(cx);
|
||||
if ranges[index].start.cmp(&position, &buffer).unwrap().is_gt() {
|
||||
|
@ -467,7 +464,7 @@ impl FindBar {
|
|||
self.pending_search = Some(cx.spawn(|this, mut cx| async move {
|
||||
match ranges.await {
|
||||
Ok(ranges) => {
|
||||
if let Some(editor) = cx.read(|cx| editor.upgrade(cx)) {
|
||||
if let Some(editor) = editor.upgrade(&cx) {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.highlighted_editors.insert(editor.downgrade());
|
||||
editor.update(cx, |editor, cx| {
|
||||
|
@ -502,7 +499,7 @@ impl FindBar {
|
|||
fn active_match_index(&mut self, cx: &mut ViewContext<Self>) -> Option<usize> {
|
||||
let editor = self.active_editor.as_ref()?;
|
||||
let editor = editor.read(cx);
|
||||
let position = editor.newest_anchor_selection()?.head();
|
||||
let position = editor.newest_anchor_selection().head();
|
||||
let ranges = editor.highlighted_ranges_for_type::<Self>()?.1;
|
||||
if ranges.is_empty() {
|
||||
None
|
||||
|
@ -655,7 +652,7 @@ mod tests {
|
|||
)
|
||||
});
|
||||
let editor = cx.add_view(Default::default(), |cx| {
|
||||
Editor::new(buffer.clone(), Arc::new(EditorSettings::test), cx)
|
||||
Editor::new(buffer.clone(), Arc::new(EditorSettings::test), None, cx)
|
||||
});
|
||||
|
||||
let find_bar = cx.add_view(Default::default(), |cx| {
|
||||
|
|
|
@ -80,8 +80,14 @@ pub trait UpdateModel {
|
|||
}
|
||||
|
||||
pub trait UpgradeModelHandle {
|
||||
fn upgrade_model_handle<T: Entity>(&self, handle: WeakModelHandle<T>)
|
||||
-> Option<ModelHandle<T>>;
|
||||
fn upgrade_model_handle<T: Entity>(
|
||||
&self,
|
||||
handle: &WeakModelHandle<T>,
|
||||
) -> Option<ModelHandle<T>>;
|
||||
}
|
||||
|
||||
pub trait UpgradeViewHandle {
|
||||
fn upgrade_view_handle<T: View>(&self, handle: &WeakViewHandle<T>) -> Option<ViewHandle<T>>;
|
||||
}
|
||||
|
||||
pub trait ReadView {
|
||||
|
@ -558,12 +564,18 @@ impl UpdateModel for AsyncAppContext {
|
|||
impl UpgradeModelHandle for AsyncAppContext {
|
||||
fn upgrade_model_handle<T: Entity>(
|
||||
&self,
|
||||
handle: WeakModelHandle<T>,
|
||||
handle: &WeakModelHandle<T>,
|
||||
) -> Option<ModelHandle<T>> {
|
||||
self.0.borrow_mut().upgrade_model_handle(handle)
|
||||
}
|
||||
}
|
||||
|
||||
impl UpgradeViewHandle for AsyncAppContext {
|
||||
fn upgrade_view_handle<T: View>(&self, handle: &WeakViewHandle<T>) -> Option<ViewHandle<T>> {
|
||||
self.0.borrow_mut().upgrade_view_handle(handle)
|
||||
}
|
||||
}
|
||||
|
||||
impl ReadModelWith for AsyncAppContext {
|
||||
fn read_model_with<E: Entity, T>(
|
||||
&self,
|
||||
|
@ -831,6 +843,17 @@ impl MutableAppContext {
|
|||
.push(handler);
|
||||
}
|
||||
|
||||
pub fn add_async_action<A, V, F>(&mut self, mut handler: F)
|
||||
where
|
||||
A: Action,
|
||||
V: View,
|
||||
F: 'static + FnMut(&mut V, &A, &mut ViewContext<V>) -> Option<Task<Result<()>>>,
|
||||
{
|
||||
self.add_action(move |view, action, cx| {
|
||||
handler(view, action, cx).map(|task| task.detach_and_log_err(cx));
|
||||
})
|
||||
}
|
||||
|
||||
pub fn add_global_action<A, F>(&mut self, mut handler: F)
|
||||
where
|
||||
A: Action,
|
||||
|
@ -1721,12 +1744,18 @@ impl UpdateModel for MutableAppContext {
|
|||
impl UpgradeModelHandle for MutableAppContext {
|
||||
fn upgrade_model_handle<T: Entity>(
|
||||
&self,
|
||||
handle: WeakModelHandle<T>,
|
||||
handle: &WeakModelHandle<T>,
|
||||
) -> Option<ModelHandle<T>> {
|
||||
self.cx.upgrade_model_handle(handle)
|
||||
}
|
||||
}
|
||||
|
||||
impl UpgradeViewHandle for MutableAppContext {
|
||||
fn upgrade_view_handle<T: View>(&self, handle: &WeakViewHandle<T>) -> Option<ViewHandle<T>> {
|
||||
self.cx.upgrade_view_handle(handle)
|
||||
}
|
||||
}
|
||||
|
||||
impl ReadView for MutableAppContext {
|
||||
fn read_view<T: View>(&self, handle: &ViewHandle<T>) -> &T {
|
||||
if let Some(view) = self.cx.views.get(&(handle.window_id, handle.view_id)) {
|
||||
|
@ -1835,7 +1864,7 @@ impl ReadModel for AppContext {
|
|||
impl UpgradeModelHandle for AppContext {
|
||||
fn upgrade_model_handle<T: Entity>(
|
||||
&self,
|
||||
handle: WeakModelHandle<T>,
|
||||
handle: &WeakModelHandle<T>,
|
||||
) -> Option<ModelHandle<T>> {
|
||||
if self.models.contains_key(&handle.model_id) {
|
||||
Some(ModelHandle::new(handle.model_id, &self.ref_counts))
|
||||
|
@ -1845,6 +1874,20 @@ impl UpgradeModelHandle for AppContext {
|
|||
}
|
||||
}
|
||||
|
||||
impl UpgradeViewHandle for AppContext {
|
||||
fn upgrade_view_handle<T: View>(&self, handle: &WeakViewHandle<T>) -> Option<ViewHandle<T>> {
|
||||
if self.ref_counts.lock().is_entity_alive(handle.view_id) {
|
||||
Some(ViewHandle::new(
|
||||
handle.window_id,
|
||||
handle.view_id,
|
||||
&self.ref_counts,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ReadView for AppContext {
|
||||
fn read_view<T: View>(&self, handle: &ViewHandle<T>) -> &T {
|
||||
if let Some(view) = self.views.get(&(handle.window_id, handle.view_id)) {
|
||||
|
@ -2217,7 +2260,7 @@ impl<M> UpdateModel for ModelContext<'_, M> {
|
|||
impl<M> UpgradeModelHandle for ModelContext<'_, M> {
|
||||
fn upgrade_model_handle<T: Entity>(
|
||||
&self,
|
||||
handle: WeakModelHandle<T>,
|
||||
handle: &WeakModelHandle<T>,
|
||||
) -> Option<ModelHandle<T>> {
|
||||
self.cx.upgrade_model_handle(handle)
|
||||
}
|
||||
|
@ -2547,12 +2590,18 @@ impl<V> ReadModel for ViewContext<'_, V> {
|
|||
impl<V> UpgradeModelHandle for ViewContext<'_, V> {
|
||||
fn upgrade_model_handle<T: Entity>(
|
||||
&self,
|
||||
handle: WeakModelHandle<T>,
|
||||
handle: &WeakModelHandle<T>,
|
||||
) -> Option<ModelHandle<T>> {
|
||||
self.cx.upgrade_model_handle(handle)
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> UpgradeViewHandle for ViewContext<'_, V> {
|
||||
fn upgrade_view_handle<T: View>(&self, handle: &WeakViewHandle<T>) -> Option<ViewHandle<T>> {
|
||||
self.cx.upgrade_view_handle(handle)
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: View> UpdateModel for ViewContext<'_, V> {
|
||||
fn update_model<T: Entity, O>(
|
||||
&mut self,
|
||||
|
@ -2654,7 +2703,7 @@ impl<T: Entity> ModelHandle<T> {
|
|||
let (mut tx, mut rx) = mpsc::channel(1);
|
||||
let mut cx = cx.cx.borrow_mut();
|
||||
let subscription = cx.observe(self, move |_, _| {
|
||||
tx.blocking_send(()).ok();
|
||||
tx.try_send(()).ok();
|
||||
});
|
||||
|
||||
let duration = if std::env::var("CI").is_ok() {
|
||||
|
@ -2850,7 +2899,7 @@ impl<T: Entity> WeakModelHandle<T> {
|
|||
self.model_id
|
||||
}
|
||||
|
||||
pub fn upgrade(self, cx: &impl UpgradeModelHandle) -> Option<ModelHandle<T>> {
|
||||
pub fn upgrade(&self, cx: &impl UpgradeModelHandle) -> Option<ModelHandle<T>> {
|
||||
cx.upgrade_model_handle(self)
|
||||
}
|
||||
}
|
||||
|
@ -2958,7 +3007,7 @@ impl<T: View> ViewHandle<T> {
|
|||
let (mut tx, mut rx) = mpsc::channel(1);
|
||||
let mut cx = cx.cx.borrow_mut();
|
||||
let subscription = cx.observe(self, move |_, _| {
|
||||
tx.blocking_send(()).ok();
|
||||
tx.try_send(()).ok();
|
||||
});
|
||||
|
||||
let duration = if std::env::var("CI").is_ok() {
|
||||
|
@ -3266,16 +3315,8 @@ impl<T: View> WeakViewHandle<T> {
|
|||
self.view_id
|
||||
}
|
||||
|
||||
pub fn upgrade(&self, cx: &AppContext) -> Option<ViewHandle<T>> {
|
||||
if cx.ref_counts.lock().is_entity_alive(self.view_id) {
|
||||
Some(ViewHandle::new(
|
||||
self.window_id,
|
||||
self.view_id,
|
||||
&cx.ref_counts,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
pub fn upgrade(&self, cx: &impl UpgradeViewHandle) -> Option<ViewHandle<T>> {
|
||||
cx.upgrade_view_handle(self)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -550,8 +550,11 @@ impl Background {
|
|||
pub async fn simulate_random_delay(&self) {
|
||||
match self {
|
||||
Self::Deterministic { executor, .. } => {
|
||||
if executor.state.lock().rng.gen_range(0..100) < 20 {
|
||||
yield_now().await;
|
||||
if executor.state.lock().rng.gen_bool(0.2) {
|
||||
let yields = executor.state.lock().rng.gen_range(1..=10);
|
||||
for _ in 0..yields {
|
||||
yield_now().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => panic!("this method can only be called on a deterministic executor"),
|
||||
|
|
|
@ -107,6 +107,10 @@ unsafe fn build_classes() {
|
|||
sel!(scrollWheel:),
|
||||
handle_view_event as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(cancelOperation:),
|
||||
cancel_operation as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
|
||||
decl.add_method(
|
||||
sel!(makeBackingLayer),
|
||||
|
@ -538,6 +542,34 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
|
|||
}
|
||||
}
|
||||
|
||||
// Allows us to receive `cmd-.` (the shortcut for closing a dialog)
|
||||
// https://bugs.eclipse.org/bugs/show_bug.cgi?id=300620#c6
|
||||
extern "C" fn cancel_operation(this: &Object, _sel: Sel, _sender: id) {
|
||||
let window_state = unsafe { get_window_state(this) };
|
||||
let mut window_state_borrow = window_state.as_ref().borrow_mut();
|
||||
|
||||
let chars = ".".to_string();
|
||||
let keystroke = Keystroke {
|
||||
cmd: true,
|
||||
ctrl: false,
|
||||
alt: false,
|
||||
shift: false,
|
||||
key: chars.clone(),
|
||||
};
|
||||
let event = Event::KeyDown {
|
||||
keystroke: keystroke.clone(),
|
||||
chars: chars.clone(),
|
||||
is_held: false,
|
||||
};
|
||||
|
||||
window_state_borrow.last_fresh_keydown = Some((keystroke, chars));
|
||||
if let Some(mut callback) = window_state_borrow.event_callback.take() {
|
||||
drop(window_state_borrow);
|
||||
callback(event);
|
||||
window_state.borrow_mut().event_callback = Some(callback);
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn send_event(this: &Object, _: Sel, native_event: id) {
|
||||
unsafe {
|
||||
let () = msg_send![super(this, class!(NSWindow)), sendEvent: native_event];
|
||||
|
|
|
@ -7,7 +7,8 @@ use crate::{
|
|||
platform::Event,
|
||||
text_layout::TextLayoutCache,
|
||||
Action, AnyAction, AnyViewHandle, AssetCache, ElementBox, Entity, FontSystem, ModelHandle,
|
||||
ReadModel, ReadView, Scene, View, ViewHandle,
|
||||
ReadModel, ReadView, Scene, UpgradeModelHandle, UpgradeViewHandle, View, ViewHandle,
|
||||
WeakModelHandle, WeakViewHandle,
|
||||
};
|
||||
use pathfinder_geometry::vector::{vec2f, Vector2F};
|
||||
use serde_json::json;
|
||||
|
@ -270,6 +271,21 @@ impl<'a> ReadModel for LayoutContext<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a> UpgradeModelHandle for LayoutContext<'a> {
|
||||
fn upgrade_model_handle<T: Entity>(
|
||||
&self,
|
||||
handle: &WeakModelHandle<T>,
|
||||
) -> Option<ModelHandle<T>> {
|
||||
self.app.upgrade_model_handle(handle)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PaintContext<'a> {
|
||||
rendered_views: &'a mut HashMap<usize, ElementBox>,
|
||||
pub scene: &'a mut Scene,
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -7,19 +7,20 @@ pub mod proto;
|
|||
mod tests;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
pub use buffer::Operation;
|
||||
pub use buffer::*;
|
||||
use collections::HashSet;
|
||||
pub use diagnostic_set::DiagnosticEntry;
|
||||
use gpui::AppContext;
|
||||
use highlight_map::HighlightMap;
|
||||
use lazy_static::lazy_static;
|
||||
pub use outline::{Outline, OutlineItem};
|
||||
use parking_lot::Mutex;
|
||||
use serde::Deserialize;
|
||||
use std::{cell::RefCell, ops::Range, path::Path, str, sync::Arc};
|
||||
use theme::SyntaxTheme;
|
||||
use tree_sitter::{self, Query};
|
||||
|
||||
pub use buffer::Operation;
|
||||
pub use buffer::*;
|
||||
pub use diagnostic_set::DiagnosticEntry;
|
||||
pub use outline::{Outline, OutlineItem};
|
||||
pub use tree_sitter::{Parser, Tree};
|
||||
|
||||
thread_local! {
|
||||
|
@ -39,10 +40,6 @@ lazy_static! {
|
|||
));
|
||||
}
|
||||
|
||||
pub trait ToPointUtf16 {
|
||||
fn to_point_utf16(self) -> PointUtf16;
|
||||
}
|
||||
|
||||
pub trait ToLspPosition {
|
||||
fn to_lsp_position(self) -> lsp::Position;
|
||||
}
|
||||
|
@ -360,18 +357,15 @@ impl CompletionLabel {
|
|||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl LanguageServerConfig {
|
||||
pub async fn fake(
|
||||
executor: Arc<gpui::executor::Background>,
|
||||
) -> (Self, lsp::FakeLanguageServer) {
|
||||
Self::fake_with_capabilities(Default::default(), executor).await
|
||||
pub async fn fake(cx: &gpui::TestAppContext) -> (Self, lsp::FakeLanguageServer) {
|
||||
Self::fake_with_capabilities(Default::default(), cx).await
|
||||
}
|
||||
|
||||
pub async fn fake_with_capabilities(
|
||||
capabilites: lsp::ServerCapabilities,
|
||||
executor: Arc<gpui::executor::Background>,
|
||||
cx: &gpui::TestAppContext,
|
||||
) -> (Self, lsp::FakeLanguageServer) {
|
||||
let (server, fake) =
|
||||
lsp::LanguageServer::fake_with_capabilities(capabilites, executor).await;
|
||||
let (server, fake) = lsp::LanguageServer::fake_with_capabilities(capabilites, cx).await;
|
||||
fake.started
|
||||
.store(false, std::sync::atomic::Ordering::SeqCst);
|
||||
let started = fake.started.clone();
|
||||
|
@ -386,18 +380,16 @@ impl LanguageServerConfig {
|
|||
}
|
||||
}
|
||||
|
||||
impl ToPointUtf16 for lsp::Position {
|
||||
fn to_point_utf16(self) -> PointUtf16 {
|
||||
PointUtf16::new(self.line, self.character)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToLspPosition for PointUtf16 {
|
||||
fn to_lsp_position(self) -> lsp::Position {
|
||||
lsp::Position::new(self.row, self.column)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn point_from_lsp(point: lsp::Position) -> PointUtf16 {
|
||||
PointUtf16::new(point.line, point.character)
|
||||
}
|
||||
|
||||
pub fn range_from_lsp(range: lsp::Range) -> Range<PointUtf16> {
|
||||
let start = PointUtf16::new(range.start.line, range.start.character);
|
||||
let end = PointUtf16::new(range.end.line, range.end.character);
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
use crate::{
|
||||
diagnostic_set::DiagnosticEntry, Completion, CompletionLabel, Diagnostic, Language, Operation,
|
||||
diagnostic_set::DiagnosticEntry, CodeAction, Completion, CompletionLabel, Diagnostic, Language,
|
||||
Operation,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use clock::ReplicaId;
|
||||
use collections::HashSet;
|
||||
use lsp::DiagnosticSeverity;
|
||||
use rpc::proto;
|
||||
use std::sync::Arc;
|
||||
use std::{ops::Range, sync::Arc};
|
||||
use text::*;
|
||||
|
||||
pub use proto::{Buffer, BufferState, SelectionSet};
|
||||
|
@ -24,14 +25,7 @@ pub fn serialize_operation(operation: &Operation) -> proto::Operation {
|
|||
replica_id: undo.id.replica_id as u32,
|
||||
local_timestamp: undo.id.value,
|
||||
lamport_timestamp: lamport_timestamp.value,
|
||||
ranges: undo
|
||||
.ranges
|
||||
.iter()
|
||||
.map(|r| proto::Range {
|
||||
start: r.start.0 as u64,
|
||||
end: r.end.0 as u64,
|
||||
})
|
||||
.collect(),
|
||||
ranges: undo.ranges.iter().map(serialize_range).collect(),
|
||||
counts: undo
|
||||
.counts
|
||||
.iter()
|
||||
|
@ -44,11 +38,10 @@ pub fn serialize_operation(operation: &Operation) -> proto::Operation {
|
|||
version: From::from(&undo.version),
|
||||
}),
|
||||
Operation::UpdateSelections {
|
||||
replica_id,
|
||||
selections,
|
||||
lamport_timestamp,
|
||||
} => proto::operation::Variant::UpdateSelections(proto::operation::UpdateSelections {
|
||||
replica_id: *replica_id as u32,
|
||||
replica_id: lamport_timestamp.replica_id as u32,
|
||||
lamport_timestamp: lamport_timestamp.value,
|
||||
selections: serialize_selections(selections),
|
||||
}),
|
||||
|
@ -60,32 +53,27 @@ pub fn serialize_operation(operation: &Operation) -> proto::Operation {
|
|||
lamport_timestamp: lamport_timestamp.value,
|
||||
diagnostics: serialize_diagnostics(diagnostics.iter()),
|
||||
}),
|
||||
Operation::UpdateCompletionTriggers { triggers } => {
|
||||
proto::operation::Variant::UpdateCompletionTriggers(
|
||||
proto::operation::UpdateCompletionTriggers {
|
||||
triggers: triggers.clone(),
|
||||
},
|
||||
)
|
||||
}
|
||||
Operation::UpdateCompletionTriggers {
|
||||
triggers,
|
||||
lamport_timestamp,
|
||||
} => proto::operation::Variant::UpdateCompletionTriggers(
|
||||
proto::operation::UpdateCompletionTriggers {
|
||||
replica_id: lamport_timestamp.replica_id as u32,
|
||||
lamport_timestamp: lamport_timestamp.value,
|
||||
triggers: triggers.clone(),
|
||||
},
|
||||
),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialize_edit_operation(operation: &EditOperation) -> proto::operation::Edit {
|
||||
let ranges = operation
|
||||
.ranges
|
||||
.iter()
|
||||
.map(|range| proto::Range {
|
||||
start: range.start.0 as u64,
|
||||
end: range.end.0 as u64,
|
||||
})
|
||||
.collect();
|
||||
proto::operation::Edit {
|
||||
replica_id: operation.timestamp.replica_id as u32,
|
||||
local_timestamp: operation.timestamp.local,
|
||||
lamport_timestamp: operation.timestamp.lamport,
|
||||
version: From::from(&operation.version),
|
||||
ranges,
|
||||
ranges: operation.ranges.iter().map(serialize_range).collect(),
|
||||
new_text: operation.new_text.clone(),
|
||||
}
|
||||
}
|
||||
|
@ -208,11 +196,7 @@ pub fn deserialize_operation(message: proto::Operation) -> Result<Operation> {
|
|||
)
|
||||
})
|
||||
.collect(),
|
||||
ranges: undo
|
||||
.ranges
|
||||
.into_iter()
|
||||
.map(|r| FullOffset(r.start as usize)..FullOffset(r.end as usize))
|
||||
.collect(),
|
||||
ranges: undo.ranges.into_iter().map(deserialize_range).collect(),
|
||||
version: undo.version.into(),
|
||||
},
|
||||
}),
|
||||
|
@ -232,7 +216,6 @@ pub fn deserialize_operation(message: proto::Operation) -> Result<Operation> {
|
|||
.collect::<Vec<_>>();
|
||||
|
||||
Operation::UpdateSelections {
|
||||
replica_id: message.replica_id as ReplicaId,
|
||||
lamport_timestamp: clock::Lamport {
|
||||
replica_id: message.replica_id as ReplicaId,
|
||||
value: message.lamport_timestamp,
|
||||
|
@ -250,6 +233,10 @@ pub fn deserialize_operation(message: proto::Operation) -> Result<Operation> {
|
|||
proto::operation::Variant::UpdateCompletionTriggers(message) => {
|
||||
Operation::UpdateCompletionTriggers {
|
||||
triggers: message.triggers,
|
||||
lamport_timestamp: clock::Lamport {
|
||||
replica_id: message.replica_id as ReplicaId,
|
||||
value: message.lamport_timestamp,
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -257,11 +244,6 @@ pub fn deserialize_operation(message: proto::Operation) -> Result<Operation> {
|
|||
}
|
||||
|
||||
pub fn deserialize_edit_operation(edit: proto::operation::Edit) -> EditOperation {
|
||||
let ranges = edit
|
||||
.ranges
|
||||
.into_iter()
|
||||
.map(|range| FullOffset(range.start as usize)..FullOffset(range.end as usize))
|
||||
.collect();
|
||||
EditOperation {
|
||||
timestamp: InsertionTimestamp {
|
||||
replica_id: edit.replica_id as ReplicaId,
|
||||
|
@ -269,7 +251,7 @@ pub fn deserialize_edit_operation(edit: proto::operation::Edit) -> EditOperation
|
|||
lamport: edit.lamport_timestamp,
|
||||
},
|
||||
version: edit.version.into(),
|
||||
ranges,
|
||||
ranges: edit.ranges.into_iter().map(deserialize_range).collect(),
|
||||
new_text: edit.new_text,
|
||||
}
|
||||
}
|
||||
|
@ -380,7 +362,39 @@ pub fn deserialize_anchor(anchor: proto::Anchor) -> Option<Anchor> {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn serialize_completion(completion: &Completion<Anchor>) -> proto::Completion {
|
||||
pub fn lamport_timestamp_for_operation(operation: &proto::Operation) -> Option<clock::Lamport> {
|
||||
let replica_id;
|
||||
let value;
|
||||
match operation.variant.as_ref()? {
|
||||
proto::operation::Variant::Edit(op) => {
|
||||
replica_id = op.replica_id;
|
||||
value = op.lamport_timestamp;
|
||||
}
|
||||
proto::operation::Variant::Undo(op) => {
|
||||
replica_id = op.replica_id;
|
||||
value = op.lamport_timestamp;
|
||||
}
|
||||
proto::operation::Variant::UpdateDiagnostics(op) => {
|
||||
replica_id = op.replica_id;
|
||||
value = op.lamport_timestamp;
|
||||
}
|
||||
proto::operation::Variant::UpdateSelections(op) => {
|
||||
replica_id = op.replica_id;
|
||||
value = op.lamport_timestamp;
|
||||
}
|
||||
proto::operation::Variant::UpdateCompletionTriggers(op) => {
|
||||
replica_id = op.replica_id;
|
||||
value = op.lamport_timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
Some(clock::Lamport {
|
||||
replica_id: replica_id as ReplicaId,
|
||||
value,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn serialize_completion(completion: &Completion) -> proto::Completion {
|
||||
proto::Completion {
|
||||
old_start: Some(serialize_anchor(&completion.old_range.start)),
|
||||
old_end: Some(serialize_anchor(&completion.old_range.end)),
|
||||
|
@ -392,7 +406,7 @@ pub fn serialize_completion(completion: &Completion<Anchor>) -> proto::Completio
|
|||
pub fn deserialize_completion(
|
||||
completion: proto::Completion,
|
||||
language: Option<&Arc<Language>>,
|
||||
) -> Result<Completion<Anchor>> {
|
||||
) -> Result<Completion> {
|
||||
let old_start = completion
|
||||
.old_start
|
||||
.and_then(deserialize_anchor)
|
||||
|
@ -411,3 +425,89 @@ pub fn deserialize_completion(
|
|||
lsp_completion,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn serialize_code_action(action: &CodeAction) -> proto::CodeAction {
|
||||
proto::CodeAction {
|
||||
start: Some(serialize_anchor(&action.range.start)),
|
||||
end: Some(serialize_anchor(&action.range.end)),
|
||||
lsp_action: serde_json::to_vec(&action.lsp_action).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deserialize_code_action(action: proto::CodeAction) -> Result<CodeAction> {
|
||||
let start = action
|
||||
.start
|
||||
.and_then(deserialize_anchor)
|
||||
.ok_or_else(|| anyhow!("invalid start"))?;
|
||||
let end = action
|
||||
.end
|
||||
.and_then(deserialize_anchor)
|
||||
.ok_or_else(|| anyhow!("invalid end"))?;
|
||||
let lsp_action = serde_json::from_slice(&action.lsp_action)?;
|
||||
Ok(CodeAction {
|
||||
range: start..end,
|
||||
lsp_action,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn serialize_transaction(transaction: &Transaction) -> proto::Transaction {
|
||||
proto::Transaction {
|
||||
id: Some(serialize_local_timestamp(transaction.id)),
|
||||
edit_ids: transaction
|
||||
.edit_ids
|
||||
.iter()
|
||||
.copied()
|
||||
.map(serialize_local_timestamp)
|
||||
.collect(),
|
||||
start: (&transaction.start).into(),
|
||||
end: (&transaction.end).into(),
|
||||
ranges: transaction.ranges.iter().map(serialize_range).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deserialize_transaction(transaction: proto::Transaction) -> Result<Transaction> {
|
||||
Ok(Transaction {
|
||||
id: deserialize_local_timestamp(
|
||||
transaction
|
||||
.id
|
||||
.ok_or_else(|| anyhow!("missing transaction id"))?,
|
||||
),
|
||||
edit_ids: transaction
|
||||
.edit_ids
|
||||
.into_iter()
|
||||
.map(deserialize_local_timestamp)
|
||||
.collect(),
|
||||
start: transaction.start.into(),
|
||||
end: transaction.end.into(),
|
||||
ranges: transaction
|
||||
.ranges
|
||||
.into_iter()
|
||||
.map(deserialize_range)
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn serialize_local_timestamp(timestamp: clock::Local) -> proto::LocalTimestamp {
|
||||
proto::LocalTimestamp {
|
||||
replica_id: timestamp.replica_id as u32,
|
||||
value: timestamp.value,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deserialize_local_timestamp(timestamp: proto::LocalTimestamp) -> clock::Local {
|
||||
clock::Local {
|
||||
replica_id: timestamp.replica_id as ReplicaId,
|
||||
value: timestamp.value,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialize_range(range: &Range<FullOffset>) -> proto::Range {
|
||||
proto::Range {
|
||||
start: range.start.0 as u64,
|
||||
end: range.end.0 as u64,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deserialize_range(range: proto::Range) -> Range<FullOffset> {
|
||||
FullOffset(range.start as usize)..FullOffset(range.end as usize)
|
||||
}
|
||||
|
|
|
@ -557,7 +557,7 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppConte
|
|||
|
||||
#[gpui::test]
|
||||
async fn test_diagnostics(mut cx: gpui::TestAppContext) {
|
||||
let (language_server, mut fake) = lsp::LanguageServer::fake(cx.background()).await;
|
||||
let (language_server, mut fake) = lsp::LanguageServer::fake(&cx).await;
|
||||
let mut rust_lang = rust_lang();
|
||||
rust_lang.config.language_server = Some(LanguageServerConfig {
|
||||
disk_based_diagnostic_sources: HashSet::from_iter(["disk".to_string()]),
|
||||
|
@ -572,7 +572,7 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
|
|||
.unindent();
|
||||
|
||||
let buffer = cx.add_model(|cx| {
|
||||
Buffer::new(0, text, cx)
|
||||
Buffer::from_file(0, text, Box::new(FakeFile::new("/some/path")), cx)
|
||||
.with_language(Arc::new(rust_lang), cx)
|
||||
.with_language_server(language_server, cx)
|
||||
});
|
||||
|
@ -592,7 +592,6 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
|
|||
// Receive diagnostics for an earlier version of the buffer.
|
||||
buffer
|
||||
.update_diagnostics(
|
||||
Some(open_notification.text_document.version),
|
||||
vec![
|
||||
DiagnosticEntry {
|
||||
range: PointUtf16::new(0, 9)..PointUtf16::new(0, 10),
|
||||
|
@ -628,6 +627,7 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
|
|||
},
|
||||
},
|
||||
],
|
||||
Some(open_notification.text_document.version),
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
|
@ -687,7 +687,6 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
|
|||
// Ensure overlapping diagnostics are highlighted correctly.
|
||||
buffer
|
||||
.update_diagnostics(
|
||||
Some(open_notification.text_document.version),
|
||||
vec![
|
||||
DiagnosticEntry {
|
||||
range: PointUtf16::new(0, 9)..PointUtf16::new(0, 10),
|
||||
|
@ -711,6 +710,7 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
|
|||
},
|
||||
},
|
||||
],
|
||||
Some(open_notification.text_document.version),
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
|
@ -777,7 +777,6 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
|
|||
buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer
|
||||
.update_diagnostics(
|
||||
Some(change_notification_2.text_document.version),
|
||||
vec![
|
||||
DiagnosticEntry {
|
||||
range: PointUtf16::new(1, 9)..PointUtf16::new(1, 11),
|
||||
|
@ -802,6 +801,7 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
|
|||
},
|
||||
},
|
||||
],
|
||||
Some(change_notification_2.text_document.version),
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
|
@ -838,6 +838,223 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
|
|||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_edits_from_lsp_with_past_version(mut cx: gpui::TestAppContext) {
|
||||
let (language_server, mut fake) = lsp::LanguageServer::fake(&cx).await;
|
||||
|
||||
let text = "
|
||||
fn a() {
|
||||
f1();
|
||||
}
|
||||
fn b() {
|
||||
f2();
|
||||
}
|
||||
fn c() {
|
||||
f3();
|
||||
}
|
||||
"
|
||||
.unindent();
|
||||
|
||||
let buffer = cx.add_model(|cx| {
|
||||
Buffer::from_file(0, text, Box::new(FakeFile::new("/some/path")), cx)
|
||||
.with_language(Arc::new(rust_lang()), cx)
|
||||
.with_language_server(language_server, cx)
|
||||
});
|
||||
|
||||
let lsp_document_version = fake
|
||||
.receive_notification::<lsp::notification::DidOpenTextDocument>()
|
||||
.await
|
||||
.text_document
|
||||
.version;
|
||||
|
||||
// Simulate editing the buffer after the language server computes some edits.
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.edit(
|
||||
[Point::new(0, 0)..Point::new(0, 0)],
|
||||
"// above first function\n",
|
||||
cx,
|
||||
);
|
||||
buffer.edit(
|
||||
[Point::new(2, 0)..Point::new(2, 0)],
|
||||
" // inside first function\n",
|
||||
cx,
|
||||
);
|
||||
buffer.edit(
|
||||
[Point::new(6, 4)..Point::new(6, 4)],
|
||||
"// inside second function ",
|
||||
cx,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
buffer.text(),
|
||||
"
|
||||
// above first function
|
||||
fn a() {
|
||||
// inside first function
|
||||
f1();
|
||||
}
|
||||
fn b() {
|
||||
// inside second function f2();
|
||||
}
|
||||
fn c() {
|
||||
f3();
|
||||
}
|
||||
"
|
||||
.unindent()
|
||||
);
|
||||
});
|
||||
|
||||
let edits = buffer
|
||||
.update(&mut cx, |buffer, cx| {
|
||||
buffer.edits_from_lsp(
|
||||
vec![
|
||||
// replace body of first function
|
||||
lsp::TextEdit {
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(3, 0)),
|
||||
new_text: "
|
||||
fn a() {
|
||||
f10();
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
},
|
||||
// edit inside second function
|
||||
lsp::TextEdit {
|
||||
range: lsp::Range::new(lsp::Position::new(4, 6), lsp::Position::new(4, 6)),
|
||||
new_text: "00".into(),
|
||||
},
|
||||
// edit inside third function via two distinct edits
|
||||
lsp::TextEdit {
|
||||
range: lsp::Range::new(lsp::Position::new(7, 5), lsp::Position::new(7, 5)),
|
||||
new_text: "4000".into(),
|
||||
},
|
||||
lsp::TextEdit {
|
||||
range: lsp::Range::new(lsp::Position::new(7, 5), lsp::Position::new(7, 6)),
|
||||
new_text: "".into(),
|
||||
},
|
||||
],
|
||||
Some(lsp_document_version),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
for (range, new_text) in edits {
|
||||
buffer.edit([range], new_text, cx);
|
||||
}
|
||||
assert_eq!(
|
||||
buffer.text(),
|
||||
"
|
||||
// above first function
|
||||
fn a() {
|
||||
// inside first function
|
||||
f10();
|
||||
}
|
||||
fn b() {
|
||||
// inside second function f200();
|
||||
}
|
||||
fn c() {
|
||||
f4000();
|
||||
}
|
||||
"
|
||||
.unindent()
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_edits_from_lsp_with_edits_on_adjacent_lines(mut cx: gpui::TestAppContext) {
|
||||
let text = "
|
||||
use a::b;
|
||||
use a::c;
|
||||
|
||||
fn f() {
|
||||
b();
|
||||
c();
|
||||
}
|
||||
"
|
||||
.unindent();
|
||||
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
|
||||
|
||||
// Simulate the language server sending us a small edit in the form of a very large diff.
|
||||
// Rust-analyzer does this when performing a merge-imports code action.
|
||||
let edits = buffer
|
||||
.update(&mut cx, |buffer, cx| {
|
||||
buffer.edits_from_lsp(
|
||||
[
|
||||
// Replace the first use statement without editing the semicolon.
|
||||
lsp::TextEdit {
|
||||
range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 8)),
|
||||
new_text: "a::{b, c}".into(),
|
||||
},
|
||||
// Reinsert the remainder of the file between the semicolon and the final
|
||||
// newline of the file.
|
||||
lsp::TextEdit {
|
||||
range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)),
|
||||
new_text: "\n\n".into(),
|
||||
},
|
||||
lsp::TextEdit {
|
||||
range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)),
|
||||
new_text: "
|
||||
fn f() {
|
||||
b();
|
||||
c();
|
||||
}"
|
||||
.unindent(),
|
||||
},
|
||||
// Delete everything after the first newline of the file.
|
||||
lsp::TextEdit {
|
||||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(7, 0)),
|
||||
new_text: "".into(),
|
||||
},
|
||||
],
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
let edits = edits
|
||||
.into_iter()
|
||||
.map(|(range, text)| {
|
||||
(
|
||||
range.start.to_point(&buffer)..range.end.to_point(&buffer),
|
||||
text,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
edits,
|
||||
[
|
||||
(Point::new(0, 4)..Point::new(0, 8), "a::{b, c}".into()),
|
||||
(Point::new(1, 0)..Point::new(2, 0), "".into())
|
||||
]
|
||||
);
|
||||
|
||||
for (range, new_text) in edits {
|
||||
buffer.edit([range], new_text, cx);
|
||||
}
|
||||
assert_eq!(
|
||||
buffer.text(),
|
||||
"
|
||||
use a::{b, c};
|
||||
|
||||
fn f() {
|
||||
b();
|
||||
c();
|
||||
}
|
||||
"
|
||||
.unindent()
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_empty_diagnostic_ranges(mut cx: gpui::TestAppContext) {
|
||||
cx.add_model(|cx| {
|
||||
|
@ -851,7 +1068,6 @@ async fn test_empty_diagnostic_ranges(mut cx: gpui::TestAppContext) {
|
|||
buffer.set_language(Some(Arc::new(rust_lang())), cx);
|
||||
buffer
|
||||
.update_diagnostics(
|
||||
None,
|
||||
vec![
|
||||
DiagnosticEntry {
|
||||
range: PointUtf16::new(0, 10)..PointUtf16::new(0, 10),
|
||||
|
@ -870,6 +1086,7 @@ async fn test_empty_diagnostic_ranges(mut cx: gpui::TestAppContext) {
|
|||
},
|
||||
},
|
||||
],
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
|
@ -1073,15 +1290,15 @@ 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())
|
||||
.map(|(replica_id, selections)| (replica_id, selections.collect::<Vec<_>>()))
|
||||
.collect::<Vec<_>>();
|
||||
let expected_remote_selections = active_selections
|
||||
.iter()
|
||||
.filter(|(replica_id, _)| **replica_id != buffer.replica_id())
|
||||
.map(|(replica_id, selections)| (*replica_id, selections.iter().collect::<Vec<_>>()))
|
||||
.collect::<Vec<_>>();
|
||||
let actual_remote_selections = buffer
|
||||
.remote_selections_in_range(Anchor::min()..Anchor::max())
|
||||
.map(|(replica_id, selections)| (replica_id, selections.collect::<Vec<_>>()))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(actual_remote_selections, expected_remote_selections);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,5 +27,6 @@ smol = "1.2"
|
|||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
util = { path = "../util", features = ["test-support"] }
|
||||
async-pipe = { git = "https://github.com/routerify/async-pipe-rs", rev = "feeb77e83142a9ff837d0767652ae41bfc5d8e47" }
|
||||
simplelog = "0.9"
|
||||
ctor = "0.1"
|
||||
env_logger = "0.8"
|
||||
unindent = "0.1.7"
|
||||
|
|
|
@ -56,6 +56,18 @@ struct Request<'a, T> {
|
|||
params: T,
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
#[derive(Deserialize)]
|
||||
struct AnyRequest<'a> {
|
||||
id: usize,
|
||||
#[serde(borrow)]
|
||||
jsonrpc: &'a str,
|
||||
#[serde(borrow)]
|
||||
method: &'a str,
|
||||
#[serde(borrow)]
|
||||
params: &'a RawValue,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct AnyResponse<'a> {
|
||||
id: usize,
|
||||
|
@ -238,6 +250,21 @@ impl LanguageServer {
|
|||
link_support: Some(true),
|
||||
..Default::default()
|
||||
}),
|
||||
code_action: Some(CodeActionClientCapabilities {
|
||||
code_action_literal_support: Some(CodeActionLiteralSupport {
|
||||
code_action_kind: CodeActionKindLiteralSupport {
|
||||
value_set: vec![
|
||||
CodeActionKind::REFACTOR.as_str().into(),
|
||||
CodeActionKind::QUICKFIX.as_str().into(),
|
||||
],
|
||||
},
|
||||
}),
|
||||
data_support: Some(true),
|
||||
resolve_support: Some(CodeActionCapabilityResolveSupport {
|
||||
properties: vec!["edit".to_string()],
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
completion: Some(CompletionClientCapabilities {
|
||||
completion_item: Some(CompletionItemCapability {
|
||||
snippet_support: Some(true),
|
||||
|
@ -454,48 +481,41 @@ impl Drop for Subscription {
|
|||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub struct FakeLanguageServer {
|
||||
buffer: Vec<u8>,
|
||||
stdin: smol::io::BufReader<async_pipe::PipeReader>,
|
||||
stdout: smol::io::BufWriter<async_pipe::PipeWriter>,
|
||||
handlers: Arc<
|
||||
Mutex<
|
||||
HashMap<
|
||||
&'static str,
|
||||
Box<dyn Send + FnOnce(usize, &[u8]) -> (Vec<u8>, barrier::Sender)>,
|
||||
>,
|
||||
>,
|
||||
>,
|
||||
outgoing_tx: channel::Sender<Vec<u8>>,
|
||||
incoming_rx: channel::Receiver<Vec<u8>>,
|
||||
pub started: Arc<std::sync::atomic::AtomicBool>,
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub struct RequestId<T> {
|
||||
id: usize,
|
||||
_type: std::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl LanguageServer {
|
||||
pub async fn fake(executor: Arc<executor::Background>) -> (Arc<Self>, FakeLanguageServer) {
|
||||
Self::fake_with_capabilities(Default::default(), executor).await
|
||||
pub async fn fake(cx: &gpui::TestAppContext) -> (Arc<Self>, FakeLanguageServer) {
|
||||
Self::fake_with_capabilities(Default::default(), cx).await
|
||||
}
|
||||
|
||||
pub async fn fake_with_capabilities(
|
||||
capabilities: ServerCapabilities,
|
||||
executor: Arc<executor::Background>,
|
||||
cx: &gpui::TestAppContext,
|
||||
) -> (Arc<Self>, FakeLanguageServer) {
|
||||
let stdin = async_pipe::pipe();
|
||||
let stdout = async_pipe::pipe();
|
||||
let mut fake = FakeLanguageServer {
|
||||
stdin: smol::io::BufReader::new(stdin.1),
|
||||
stdout: smol::io::BufWriter::new(stdout.0),
|
||||
buffer: Vec::new(),
|
||||
started: Arc::new(std::sync::atomic::AtomicBool::new(true)),
|
||||
};
|
||||
let (stdin_writer, stdin_reader) = async_pipe::pipe();
|
||||
let (stdout_writer, stdout_reader) = async_pipe::pipe();
|
||||
|
||||
let server = Self::new_internal(stdin.0, stdout.1, Path::new("/"), executor).unwrap();
|
||||
let mut fake = FakeLanguageServer::new(cx, stdin_reader, stdout_writer);
|
||||
fake.handle_request::<request::Initialize, _>(move |_| InitializeResult {
|
||||
capabilities,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let (init_id, _) = fake.receive_request::<request::Initialize>().await;
|
||||
fake.respond(
|
||||
init_id,
|
||||
InitializeResult {
|
||||
capabilities,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await;
|
||||
let server =
|
||||
Self::new_internal(stdin_writer, stdout_reader, Path::new("/"), cx.background())
|
||||
.unwrap();
|
||||
fake.receive_notification::<notification::Initialized>()
|
||||
.await;
|
||||
|
||||
|
@ -505,6 +525,75 @@ impl LanguageServer {
|
|||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl FakeLanguageServer {
|
||||
fn new(
|
||||
cx: &gpui::TestAppContext,
|
||||
stdin: async_pipe::PipeReader,
|
||||
stdout: async_pipe::PipeWriter,
|
||||
) -> Self {
|
||||
use futures::StreamExt as _;
|
||||
|
||||
let (incoming_tx, incoming_rx) = channel::unbounded();
|
||||
let (outgoing_tx, mut outgoing_rx) = channel::unbounded();
|
||||
let this = Self {
|
||||
outgoing_tx: outgoing_tx.clone(),
|
||||
incoming_rx,
|
||||
handlers: Default::default(),
|
||||
started: Arc::new(std::sync::atomic::AtomicBool::new(true)),
|
||||
};
|
||||
|
||||
// Receive incoming messages
|
||||
let handlers = this.handlers.clone();
|
||||
cx.background()
|
||||
.spawn(async move {
|
||||
let mut buffer = Vec::new();
|
||||
let mut stdin = smol::io::BufReader::new(stdin);
|
||||
while Self::receive(&mut stdin, &mut buffer).await.is_ok() {
|
||||
if let Ok(request) = serde_json::from_slice::<AnyRequest>(&mut buffer) {
|
||||
assert_eq!(request.jsonrpc, JSON_RPC_VERSION);
|
||||
|
||||
let handler = handlers.lock().remove(request.method);
|
||||
if let Some(handler) = handler {
|
||||
let (response, sent) =
|
||||
handler(request.id, request.params.get().as_bytes());
|
||||
log::debug!("handled lsp request. method:{}", request.method);
|
||||
outgoing_tx.send(response).await.unwrap();
|
||||
drop(sent);
|
||||
} else {
|
||||
log::debug!("unhandled lsp request. method:{}", request.method);
|
||||
outgoing_tx
|
||||
.send(
|
||||
serde_json::to_vec(&AnyResponse {
|
||||
id: request.id,
|
||||
error: Some(Error {
|
||||
message: "no handler".to_string(),
|
||||
}),
|
||||
result: None,
|
||||
})
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
} else {
|
||||
incoming_tx.send(buffer.clone()).await.unwrap();
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
// Send outgoing messages
|
||||
cx.background()
|
||||
.spawn(async move {
|
||||
let mut stdout = smol::io::BufWriter::new(stdout);
|
||||
while let Some(notification) = outgoing_rx.next().await {
|
||||
Self::send(&mut stdout, ¬ification).await;
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
pub async fn notify<T: notification::Notification>(&mut self, params: T::Params) {
|
||||
if !self.started.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
panic!("can't simulate an LSP notification before the server has been started");
|
||||
|
@ -515,51 +604,53 @@ impl FakeLanguageServer {
|
|||
params,
|
||||
})
|
||||
.unwrap();
|
||||
self.send(message).await;
|
||||
self.outgoing_tx.send(message).await.unwrap();
|
||||
}
|
||||
|
||||
pub async fn respond<'a, T: request::Request>(
|
||||
&mut self,
|
||||
request_id: RequestId<T>,
|
||||
result: T::Result,
|
||||
) {
|
||||
let result = serde_json::to_string(&result).unwrap();
|
||||
let message = serde_json::to_vec(&AnyResponse {
|
||||
id: request_id.id,
|
||||
error: None,
|
||||
result: Some(&RawValue::from_string(result).unwrap()),
|
||||
})
|
||||
.unwrap();
|
||||
self.send(message).await;
|
||||
}
|
||||
pub async fn receive_notification<T: notification::Notification>(&mut self) -> T::Params {
|
||||
use futures::StreamExt as _;
|
||||
|
||||
pub async fn receive_request<T: request::Request>(&mut self) -> (RequestId<T>, T::Params) {
|
||||
loop {
|
||||
self.receive().await;
|
||||
if let Ok(request) = serde_json::from_slice::<Request<T::Params>>(&self.buffer) {
|
||||
assert_eq!(request.method, T::METHOD);
|
||||
assert_eq!(request.jsonrpc, JSON_RPC_VERSION);
|
||||
return (
|
||||
RequestId {
|
||||
id: request.id,
|
||||
_type: std::marker::PhantomData,
|
||||
},
|
||||
request.params,
|
||||
);
|
||||
let bytes = self.incoming_rx.next().await.unwrap();
|
||||
if let Ok(notification) = serde_json::from_slice::<Notification<T::Params>>(&bytes) {
|
||||
assert_eq!(notification.method, T::METHOD);
|
||||
return notification.params;
|
||||
} else {
|
||||
log::info!(
|
||||
"skipping message in fake language server {:?}",
|
||||
std::str::from_utf8(&self.buffer)
|
||||
std::str::from_utf8(&bytes)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn receive_notification<T: notification::Notification>(&mut self) -> T::Params {
|
||||
self.receive().await;
|
||||
let notification = serde_json::from_slice::<Notification<T::Params>>(&self.buffer).unwrap();
|
||||
assert_eq!(notification.method, T::METHOD);
|
||||
notification.params
|
||||
pub fn handle_request<T, F>(&mut self, handler: F) -> barrier::Receiver
|
||||
where
|
||||
T: 'static + request::Request,
|
||||
F: 'static + Send + FnOnce(T::Params) -> T::Result,
|
||||
{
|
||||
let (responded_tx, responded_rx) = barrier::channel();
|
||||
let prev_handler = self.handlers.lock().insert(
|
||||
T::METHOD,
|
||||
Box::new(|id, params| {
|
||||
let result = handler(serde_json::from_slice::<T::Params>(params).unwrap());
|
||||
let result = serde_json::to_string(&result).unwrap();
|
||||
let result = serde_json::from_str::<&RawValue>(&result).unwrap();
|
||||
let response = AnyResponse {
|
||||
id,
|
||||
error: None,
|
||||
result: Some(result),
|
||||
};
|
||||
(serde_json::to_vec(&response).unwrap(), responded_tx)
|
||||
}),
|
||||
);
|
||||
if prev_handler.is_some() {
|
||||
panic!(
|
||||
"registered a new handler for LSP method '{}' before the previous handler was called",
|
||||
T::METHOD
|
||||
);
|
||||
}
|
||||
responded_rx
|
||||
}
|
||||
|
||||
pub async fn start_progress(&mut self, token: impl Into<String>) {
|
||||
|
@ -578,39 +669,37 @@ impl FakeLanguageServer {
|
|||
.await;
|
||||
}
|
||||
|
||||
async fn send(&mut self, message: Vec<u8>) {
|
||||
self.stdout
|
||||
async fn send(stdout: &mut smol::io::BufWriter<async_pipe::PipeWriter>, message: &[u8]) {
|
||||
stdout
|
||||
.write_all(CONTENT_LEN_HEADER.as_bytes())
|
||||
.await
|
||||
.unwrap();
|
||||
self.stdout
|
||||
stdout
|
||||
.write_all((format!("{}", message.len())).as_bytes())
|
||||
.await
|
||||
.unwrap();
|
||||
self.stdout.write_all("\r\n\r\n".as_bytes()).await.unwrap();
|
||||
self.stdout.write_all(&message).await.unwrap();
|
||||
self.stdout.flush().await.unwrap();
|
||||
stdout.write_all("\r\n\r\n".as_bytes()).await.unwrap();
|
||||
stdout.write_all(&message).await.unwrap();
|
||||
stdout.flush().await.unwrap();
|
||||
}
|
||||
|
||||
async fn receive(&mut self) {
|
||||
self.buffer.clear();
|
||||
self.stdin
|
||||
.read_until(b'\n', &mut self.buffer)
|
||||
.await
|
||||
.unwrap();
|
||||
self.stdin
|
||||
.read_until(b'\n', &mut self.buffer)
|
||||
.await
|
||||
.unwrap();
|
||||
let message_len: usize = std::str::from_utf8(&self.buffer)
|
||||
async fn receive(
|
||||
stdin: &mut smol::io::BufReader<async_pipe::PipeReader>,
|
||||
buffer: &mut Vec<u8>,
|
||||
) -> Result<()> {
|
||||
buffer.clear();
|
||||
stdin.read_until(b'\n', buffer).await?;
|
||||
stdin.read_until(b'\n', buffer).await?;
|
||||
let message_len: usize = std::str::from_utf8(buffer)
|
||||
.unwrap()
|
||||
.strip_prefix(CONTENT_LEN_HEADER)
|
||||
.unwrap()
|
||||
.trim_end()
|
||||
.parse()
|
||||
.unwrap();
|
||||
self.buffer.resize(message_len, 0);
|
||||
self.stdin.read_exact(&mut self.buffer).await.unwrap();
|
||||
buffer.resize(message_len, 0);
|
||||
stdin.read_exact(buffer).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -618,10 +707,16 @@ impl FakeLanguageServer {
|
|||
mod tests {
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
use simplelog::SimpleLogger;
|
||||
use unindent::Unindent;
|
||||
use util::test::temp_tree;
|
||||
|
||||
#[ctor::ctor]
|
||||
fn init_logger() {
|
||||
if std::env::var("RUST_LOG").is_ok() {
|
||||
env_logger::init();
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_rust_analyzer(cx: TestAppContext) {
|
||||
let lib_source = r#"
|
||||
|
@ -643,14 +738,9 @@ mod tests {
|
|||
}));
|
||||
let lib_file_uri = Url::from_file_path(root_dir.path().join("src/lib.rs")).unwrap();
|
||||
|
||||
let server = cx.read(|cx| {
|
||||
LanguageServer::new(
|
||||
Path::new("rust-analyzer"),
|
||||
root_dir.path(),
|
||||
cx.background().clone(),
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
let server =
|
||||
LanguageServer::new(Path::new("rust-analyzer"), root_dir.path(), cx.background())
|
||||
.unwrap();
|
||||
server.next_idle_notification().await;
|
||||
|
||||
server
|
||||
|
@ -687,9 +777,7 @@ mod tests {
|
|||
|
||||
#[gpui::test]
|
||||
async fn test_fake(cx: TestAppContext) {
|
||||
SimpleLogger::init(log::LevelFilter::Info, Default::default()).unwrap();
|
||||
|
||||
let (server, mut fake) = LanguageServer::fake(cx.background()).await;
|
||||
let (server, mut fake) = LanguageServer::fake(&cx).await;
|
||||
|
||||
let (message_tx, message_rx) = channel::unbounded();
|
||||
let (diagnostics_tx, diagnostics_rx) = channel::unbounded();
|
||||
|
@ -741,9 +829,9 @@ mod tests {
|
|||
"file://b/c"
|
||||
);
|
||||
|
||||
fake.handle_request::<request::Shutdown, _>(|_| ());
|
||||
|
||||
drop(server);
|
||||
let (shutdown_request, _) = fake.receive_request::<request::Shutdown>().await;
|
||||
fake.respond(shutdown_request, ()).await;
|
||||
fake.receive_notification::<notification::Exit>().await;
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,11 @@ edition = "2021"
|
|||
path = "src/project.rs"
|
||||
|
||||
[features]
|
||||
test-support = ["language/test-support", "text/test-support"]
|
||||
test-support = [
|
||||
"client/test-support",
|
||||
"language/test-support",
|
||||
"text/test-support",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
text = { path = "../text" }
|
||||
|
@ -45,6 +49,5 @@ lsp = { path = "../lsp", features = ["test-support"] }
|
|||
util = { path = "../util", features = ["test-support"] }
|
||||
rpc = { path = "../rpc", features = ["test-support"] }
|
||||
rand = "0.8.3"
|
||||
simplelog = "0.9"
|
||||
tempdir = { version = "0.3.7" }
|
||||
unindent = "0.1.7"
|
||||
|
|
|
@ -13,6 +13,11 @@ use text::Rope;
|
|||
|
||||
#[async_trait::async_trait]
|
||||
pub trait Fs: Send + Sync {
|
||||
async fn create_dir(&self, path: &Path) -> Result<()>;
|
||||
async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()>;
|
||||
async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()>;
|
||||
async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()>;
|
||||
async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()>;
|
||||
async fn load(&self, path: &Path) -> Result<String>;
|
||||
async fn save(&self, path: &Path, text: &Rope) -> Result<()>;
|
||||
async fn canonicalize(&self, path: &Path) -> Result<PathBuf>;
|
||||
|
@ -32,6 +37,24 @@ pub trait Fs: Send + Sync {
|
|||
fn as_fake(&self) -> &FakeFs;
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Default)]
|
||||
pub struct CreateOptions {
|
||||
pub overwrite: bool,
|
||||
pub ignore_if_exists: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Default)]
|
||||
pub struct RenameOptions {
|
||||
pub overwrite: bool,
|
||||
pub ignore_if_exists: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Default)]
|
||||
pub struct RemoveOptions {
|
||||
pub recursive: bool,
|
||||
pub ignore_if_not_exists: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Metadata {
|
||||
pub inode: u64,
|
||||
|
@ -44,6 +67,60 @@ pub struct RealFs;
|
|||
|
||||
#[async_trait::async_trait]
|
||||
impl Fs for RealFs {
|
||||
async fn create_dir(&self, path: &Path) -> Result<()> {
|
||||
Ok(smol::fs::create_dir_all(path).await?)
|
||||
}
|
||||
|
||||
async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()> {
|
||||
let mut open_options = smol::fs::OpenOptions::new();
|
||||
open_options.write(true).create(true);
|
||||
if options.overwrite {
|
||||
open_options.truncate(true);
|
||||
} else if !options.ignore_if_exists {
|
||||
open_options.create_new(true);
|
||||
}
|
||||
open_options.open(path).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()> {
|
||||
if !options.overwrite && smol::fs::metadata(target).await.is_ok() {
|
||||
if options.ignore_if_exists {
|
||||
return Ok(());
|
||||
} else {
|
||||
return Err(anyhow!("{target:?} already exists"));
|
||||
}
|
||||
}
|
||||
|
||||
smol::fs::rename(source, target).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {
|
||||
let result = if options.recursive {
|
||||
smol::fs::remove_dir_all(path).await
|
||||
} else {
|
||||
smol::fs::remove_dir(path).await
|
||||
};
|
||||
match result {
|
||||
Ok(()) => Ok(()),
|
||||
Err(err) if err.kind() == io::ErrorKind::NotFound && options.ignore_if_not_exists => {
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => Err(err)?,
|
||||
}
|
||||
}
|
||||
|
||||
async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()> {
|
||||
match smol::fs::remove_file(path).await {
|
||||
Ok(()) => Ok(()),
|
||||
Err(err) if err.kind() == io::ErrorKind::NotFound && options.ignore_if_not_exists => {
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => Err(err)?,
|
||||
}
|
||||
}
|
||||
|
||||
async fn load(&self, path: &Path) -> Result<String> {
|
||||
let mut file = smol::fs::File::open(path).await?;
|
||||
let mut text = String::new();
|
||||
|
@ -162,15 +239,19 @@ impl FakeFsState {
|
|||
}
|
||||
}
|
||||
|
||||
async fn emit_event(&mut self, paths: &[&Path]) {
|
||||
async fn emit_event<I, T>(&mut self, paths: I)
|
||||
where
|
||||
I: IntoIterator<Item = T>,
|
||||
T: Into<PathBuf>,
|
||||
{
|
||||
use postage::prelude::Sink as _;
|
||||
|
||||
let events = paths
|
||||
.iter()
|
||||
.into_iter()
|
||||
.map(|path| fsevent::Event {
|
||||
event_id: 0,
|
||||
flags: fsevent::StreamFlags::empty(),
|
||||
path: path.to_path_buf(),
|
||||
path: path.into(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
@ -292,46 +373,163 @@ impl FakeFs {
|
|||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
pub async fn remove(&self, path: &Path) -> Result<()> {
|
||||
let mut state = self.state.lock().await;
|
||||
state.validate_path(path)?;
|
||||
state.entries.retain(|path, _| !path.starts_with(path));
|
||||
state.emit_event(&[path]).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn rename(&self, source: &Path, target: &Path) -> Result<()> {
|
||||
let mut state = self.state.lock().await;
|
||||
state.validate_path(source)?;
|
||||
state.validate_path(target)?;
|
||||
if state.entries.contains_key(target) {
|
||||
Err(anyhow!("target path already exists"))
|
||||
} else {
|
||||
let mut removed = Vec::new();
|
||||
state.entries.retain(|path, entry| {
|
||||
if let Ok(relative_path) = path.strip_prefix(source) {
|
||||
removed.push((relative_path.to_path_buf(), entry.clone()));
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
|
||||
for (relative_path, entry) in removed {
|
||||
let new_path = target.join(relative_path);
|
||||
state.entries.insert(new_path, entry);
|
||||
}
|
||||
|
||||
state.emit_event(&[source, target]).await;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
#[async_trait::async_trait]
|
||||
impl Fs for FakeFs {
|
||||
async fn create_dir(&self, path: &Path) -> Result<()> {
|
||||
self.executor.simulate_random_delay().await;
|
||||
let state = &mut *self.state.lock().await;
|
||||
let mut ancestor_path = PathBuf::new();
|
||||
let mut created_dir_paths = Vec::new();
|
||||
for component in path.components() {
|
||||
ancestor_path.push(component);
|
||||
let entry = state
|
||||
.entries
|
||||
.entry(ancestor_path.clone())
|
||||
.or_insert_with(|| {
|
||||
let inode = state.next_inode;
|
||||
state.next_inode += 1;
|
||||
created_dir_paths.push(ancestor_path.clone());
|
||||
FakeFsEntry {
|
||||
metadata: Metadata {
|
||||
inode,
|
||||
mtime: SystemTime::now(),
|
||||
is_dir: true,
|
||||
is_symlink: false,
|
||||
},
|
||||
content: None,
|
||||
}
|
||||
});
|
||||
if !entry.metadata.is_dir {
|
||||
return Err(anyhow!(
|
||||
"cannot create directory because {:?} is a file",
|
||||
ancestor_path
|
||||
));
|
||||
}
|
||||
}
|
||||
state.emit_event(&created_dir_paths).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()> {
|
||||
self.executor.simulate_random_delay().await;
|
||||
let mut state = self.state.lock().await;
|
||||
state.validate_path(path)?;
|
||||
if let Some(entry) = state.entries.get_mut(path) {
|
||||
if entry.metadata.is_dir || entry.metadata.is_symlink {
|
||||
return Err(anyhow!(
|
||||
"cannot create file because {:?} is a dir or a symlink",
|
||||
path
|
||||
));
|
||||
}
|
||||
|
||||
if options.overwrite {
|
||||
entry.metadata.mtime = SystemTime::now();
|
||||
entry.content = Some(Default::default());
|
||||
} else if !options.ignore_if_exists {
|
||||
return Err(anyhow!(
|
||||
"cannot create file because {:?} already exists",
|
||||
path
|
||||
));
|
||||
}
|
||||
} else {
|
||||
let inode = state.next_inode;
|
||||
state.next_inode += 1;
|
||||
let entry = FakeFsEntry {
|
||||
metadata: Metadata {
|
||||
inode,
|
||||
mtime: SystemTime::now(),
|
||||
is_dir: false,
|
||||
is_symlink: false,
|
||||
},
|
||||
content: Some(Default::default()),
|
||||
};
|
||||
state.entries.insert(path.to_path_buf(), entry);
|
||||
}
|
||||
state.emit_event(&[path]).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()> {
|
||||
let mut state = self.state.lock().await;
|
||||
state.validate_path(source)?;
|
||||
state.validate_path(target)?;
|
||||
|
||||
if !options.overwrite && state.entries.contains_key(target) {
|
||||
if options.ignore_if_exists {
|
||||
return Ok(());
|
||||
} else {
|
||||
return Err(anyhow!("{target:?} already exists"));
|
||||
}
|
||||
}
|
||||
|
||||
let mut removed = Vec::new();
|
||||
state.entries.retain(|path, entry| {
|
||||
if let Ok(relative_path) = path.strip_prefix(source) {
|
||||
removed.push((relative_path.to_path_buf(), entry.clone()));
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
|
||||
for (relative_path, entry) in removed {
|
||||
let new_path = target.join(relative_path);
|
||||
state.entries.insert(new_path, entry);
|
||||
}
|
||||
|
||||
state.emit_event(&[source, target]).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {
|
||||
let mut state = self.state.lock().await;
|
||||
state.validate_path(path)?;
|
||||
if let Some(entry) = state.entries.get(path) {
|
||||
if !entry.metadata.is_dir {
|
||||
return Err(anyhow!("cannot remove {path:?} because it is not a dir"));
|
||||
}
|
||||
|
||||
if !options.recursive {
|
||||
let descendants = state
|
||||
.entries
|
||||
.keys()
|
||||
.filter(|path| path.starts_with(path))
|
||||
.count();
|
||||
if descendants > 1 {
|
||||
return Err(anyhow!("{path:?} is not empty"));
|
||||
}
|
||||
}
|
||||
|
||||
state.entries.retain(|path, _| !path.starts_with(path));
|
||||
state.emit_event(&[path]).await;
|
||||
} else if !options.ignore_if_not_exists {
|
||||
return Err(anyhow!("{path:?} does not exist"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()> {
|
||||
let mut state = self.state.lock().await;
|
||||
state.validate_path(path)?;
|
||||
if let Some(entry) = state.entries.get(path) {
|
||||
if entry.metadata.is_dir {
|
||||
return Err(anyhow!("cannot remove {path:?} because it is not a file"));
|
||||
}
|
||||
|
||||
state.entries.remove(path);
|
||||
state.emit_event(&[path]).await;
|
||||
} else if !options.ignore_if_not_exists {
|
||||
return Err(anyhow!("{path:?} does not exist"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn load(&self, path: &Path) -> Result<String> {
|
||||
self.executor.simulate_random_delay().await;
|
||||
let state = self.state.lock().await;
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -14,9 +14,7 @@ use gpui::{
|
|||
executor, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext,
|
||||
Task,
|
||||
};
|
||||
use language::{
|
||||
Anchor, Buffer, Completion, DiagnosticEntry, Language, Operation, PointUtf16, Rope,
|
||||
};
|
||||
use language::{Buffer, DiagnosticEntry, Operation, PointUtf16, Rope};
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
use postage::{
|
||||
|
@ -293,7 +291,7 @@ impl Worktree {
|
|||
let this = worktree_handle.downgrade();
|
||||
cx.spawn(|mut cx| async move {
|
||||
while let Some(_) = snapshot_rx.recv().await {
|
||||
if let Some(this) = cx.read(|cx| this.upgrade(cx)) {
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
this.update(&mut cx, |this, cx| this.poll_snapshot(cx));
|
||||
} else {
|
||||
break;
|
||||
|
@ -518,7 +516,7 @@ impl LocalWorktree {
|
|||
|
||||
cx.spawn_weak(|this, mut cx| async move {
|
||||
while let Ok(scan_state) = scan_states_rx.recv().await {
|
||||
if let Some(handle) = cx.read(|cx| this.upgrade(cx)) {
|
||||
if let Some(handle) = this.upgrade(&cx) {
|
||||
let to_send = handle.update(&mut cx, |this, cx| {
|
||||
last_scan_state_tx.blocking_send(scan_state).ok();
|
||||
this.poll_snapshot(cx);
|
||||
|
@ -820,7 +818,7 @@ impl RemoteWorktree {
|
|||
) -> Result<()> {
|
||||
let mut tx = self.updates_tx.clone();
|
||||
let payload = envelope.payload.clone();
|
||||
cx.background()
|
||||
cx.foreground()
|
||||
.spawn(async move {
|
||||
tx.send(payload).await.expect("receiver runs to completion");
|
||||
})
|
||||
|
@ -1387,96 +1385,6 @@ impl language::File for File {
|
|||
})
|
||||
}
|
||||
|
||||
fn format_remote(
|
||||
&self,
|
||||
buffer_id: u64,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Option<Task<Result<()>>> {
|
||||
let worktree = self.worktree.read(cx);
|
||||
let worktree = worktree.as_remote()?;
|
||||
let rpc = worktree.client.clone();
|
||||
let project_id = worktree.project_id;
|
||||
Some(cx.foreground().spawn(async move {
|
||||
rpc.request(proto::FormatBuffer {
|
||||
project_id,
|
||||
buffer_id,
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
|
||||
fn completions(
|
||||
&self,
|
||||
buffer_id: u64,
|
||||
position: Anchor,
|
||||
language: Option<Arc<Language>>,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Task<Result<Vec<Completion<Anchor>>>> {
|
||||
let worktree = self.worktree.read(cx);
|
||||
let worktree = if let Some(worktree) = worktree.as_remote() {
|
||||
worktree
|
||||
} else {
|
||||
return Task::ready(Err(anyhow!(
|
||||
"remote completions requested on a local worktree"
|
||||
)));
|
||||
};
|
||||
let rpc = worktree.client.clone();
|
||||
let project_id = worktree.project_id;
|
||||
cx.foreground().spawn(async move {
|
||||
let response = rpc
|
||||
.request(proto::GetCompletions {
|
||||
project_id,
|
||||
buffer_id,
|
||||
position: Some(language::proto::serialize_anchor(&position)),
|
||||
})
|
||||
.await?;
|
||||
response
|
||||
.completions
|
||||
.into_iter()
|
||||
.map(|completion| {
|
||||
language::proto::deserialize_completion(completion, language.as_ref())
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
fn apply_additional_edits_for_completion(
|
||||
&self,
|
||||
buffer_id: u64,
|
||||
completion: Completion<Anchor>,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Task<Result<Vec<clock::Local>>> {
|
||||
let worktree = self.worktree.read(cx);
|
||||
let worktree = if let Some(worktree) = worktree.as_remote() {
|
||||
worktree
|
||||
} else {
|
||||
return Task::ready(Err(anyhow!(
|
||||
"remote additional edits application requested on a local worktree"
|
||||
)));
|
||||
};
|
||||
let rpc = worktree.client.clone();
|
||||
let project_id = worktree.project_id;
|
||||
cx.foreground().spawn(async move {
|
||||
let response = rpc
|
||||
.request(proto::ApplyCompletionAdditionalEdits {
|
||||
project_id,
|
||||
buffer_id,
|
||||
completion: Some(language::proto::serialize_completion(&completion)),
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(response
|
||||
.additional_edits
|
||||
.into_iter()
|
||||
.map(|edit| clock::Local {
|
||||
replica_id: edit.replica_id as ReplicaId,
|
||||
value: edit.local_timestamp,
|
||||
})
|
||||
.collect())
|
||||
})
|
||||
}
|
||||
|
||||
fn buffer_updated(&self, buffer_id: u64, operation: Operation, cx: &mut MutableAppContext) {
|
||||
self.worktree.update(cx, |worktree, cx| {
|
||||
worktree.send_buffer_update(buffer_id, operation, cx);
|
||||
|
@ -2216,7 +2124,7 @@ struct UpdateIgnoreStatusJob {
|
|||
}
|
||||
|
||||
pub trait WorktreeHandle {
|
||||
#[cfg(test)]
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
fn flush_fs_events<'a>(
|
||||
&self,
|
||||
cx: &'a gpui::TestAppContext,
|
||||
|
@ -2230,7 +2138,7 @@ impl WorktreeHandle for ModelHandle<Worktree> {
|
|||
//
|
||||
// This function mutates the worktree's directory and waits for those mutations to be picked up,
|
||||
// to ensure that all redundant FS events have already been processed.
|
||||
#[cfg(test)]
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
fn flush_fs_events<'a>(
|
||||
&self,
|
||||
cx: &'a gpui::TestAppContext,
|
||||
|
@ -2238,14 +2146,22 @@ impl WorktreeHandle for ModelHandle<Worktree> {
|
|||
use smol::future::FutureExt;
|
||||
|
||||
let filename = "fs-event-sentinel";
|
||||
let root_path = cx.read(|cx| self.read(cx).as_local().unwrap().abs_path().clone());
|
||||
let tree = self.clone();
|
||||
let (fs, root_path) = self.read_with(cx, |tree, _| {
|
||||
let tree = tree.as_local().unwrap();
|
||||
(tree.fs.clone(), tree.abs_path().clone())
|
||||
});
|
||||
|
||||
async move {
|
||||
std::fs::write(root_path.join(filename), "").unwrap();
|
||||
fs.create_file(&root_path.join(filename), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
tree.condition(&cx, |tree, _| tree.entry_for_path(filename).is_some())
|
||||
.await;
|
||||
|
||||
std::fs::remove_file(root_path.join(filename)).unwrap();
|
||||
fs.remove_file(&root_path.join(filename), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
tree.condition(&cx, |tree, _| tree.entry_for_path(filename).is_none())
|
||||
.await;
|
||||
|
||||
|
|
|
@ -39,27 +39,32 @@ message Envelope {
|
|||
SaveBuffer save_buffer = 31;
|
||||
BufferSaved buffer_saved = 32;
|
||||
BufferReloaded buffer_reloaded = 33;
|
||||
FormatBuffer format_buffer = 34;
|
||||
GetCompletions get_completions = 35;
|
||||
GetCompletionsResponse get_completions_response = 36;
|
||||
ApplyCompletionAdditionalEdits apply_completion_additional_edits = 37;
|
||||
ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 38;
|
||||
FormatBuffers format_buffers = 34;
|
||||
FormatBuffersResponse format_buffers_response = 35;
|
||||
GetCompletions get_completions = 36;
|
||||
GetCompletionsResponse get_completions_response = 37;
|
||||
ApplyCompletionAdditionalEdits apply_completion_additional_edits = 38;
|
||||
ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 39;
|
||||
GetCodeActions get_code_actions = 40;
|
||||
GetCodeActionsResponse get_code_actions_response = 41;
|
||||
ApplyCodeAction apply_code_action = 42;
|
||||
ApplyCodeActionResponse apply_code_action_response = 43;
|
||||
|
||||
GetChannels get_channels = 39;
|
||||
GetChannelsResponse get_channels_response = 40;
|
||||
JoinChannel join_channel = 41;
|
||||
JoinChannelResponse join_channel_response = 42;
|
||||
LeaveChannel leave_channel = 43;
|
||||
SendChannelMessage send_channel_message = 44;
|
||||
SendChannelMessageResponse send_channel_message_response = 45;
|
||||
ChannelMessageSent channel_message_sent = 46;
|
||||
GetChannelMessages get_channel_messages = 47;
|
||||
GetChannelMessagesResponse get_channel_messages_response = 48;
|
||||
GetChannels get_channels = 44;
|
||||
GetChannelsResponse get_channels_response = 45;
|
||||
JoinChannel join_channel = 46;
|
||||
JoinChannelResponse join_channel_response = 47;
|
||||
LeaveChannel leave_channel = 48;
|
||||
SendChannelMessage send_channel_message = 49;
|
||||
SendChannelMessageResponse send_channel_message_response = 50;
|
||||
ChannelMessageSent channel_message_sent = 51;
|
||||
GetChannelMessages get_channel_messages = 52;
|
||||
GetChannelMessagesResponse get_channel_messages_response = 53;
|
||||
|
||||
UpdateContacts update_contacts = 49;
|
||||
UpdateContacts update_contacts = 54;
|
||||
|
||||
GetUsers get_users = 50;
|
||||
GetUsersResponse get_users_response = 51;
|
||||
GetUsers get_users = 55;
|
||||
GetUsersResponse get_users_response = 56;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -202,9 +207,13 @@ message BufferReloaded {
|
|||
Timestamp mtime = 4;
|
||||
}
|
||||
|
||||
message FormatBuffer {
|
||||
message FormatBuffers {
|
||||
uint64 project_id = 1;
|
||||
uint64 buffer_id = 2;
|
||||
repeated uint64 buffer_ids = 2;
|
||||
}
|
||||
|
||||
message FormatBuffersResponse {
|
||||
ProjectTransaction transaction = 1;
|
||||
}
|
||||
|
||||
message GetCompletions {
|
||||
|
@ -224,12 +233,7 @@ message ApplyCompletionAdditionalEdits {
|
|||
}
|
||||
|
||||
message ApplyCompletionAdditionalEditsResponse {
|
||||
repeated AdditionalEdit additional_edits = 1;
|
||||
}
|
||||
|
||||
message AdditionalEdit {
|
||||
uint32 replica_id = 1;
|
||||
uint32 local_timestamp = 2;
|
||||
Transaction transaction = 1;
|
||||
}
|
||||
|
||||
message Completion {
|
||||
|
@ -239,6 +243,51 @@ message Completion {
|
|||
bytes lsp_completion = 4;
|
||||
}
|
||||
|
||||
message GetCodeActions {
|
||||
uint64 project_id = 1;
|
||||
uint64 buffer_id = 2;
|
||||
Anchor start = 3;
|
||||
Anchor end = 4;
|
||||
}
|
||||
|
||||
message GetCodeActionsResponse {
|
||||
repeated CodeAction actions = 1;
|
||||
}
|
||||
|
||||
message ApplyCodeAction {
|
||||
uint64 project_id = 1;
|
||||
uint64 buffer_id = 2;
|
||||
CodeAction action = 3;
|
||||
}
|
||||
|
||||
message ApplyCodeActionResponse {
|
||||
ProjectTransaction transaction = 1;
|
||||
}
|
||||
|
||||
message CodeAction {
|
||||
Anchor start = 1;
|
||||
Anchor end = 2;
|
||||
bytes lsp_action = 3;
|
||||
}
|
||||
|
||||
message ProjectTransaction {
|
||||
repeated Buffer buffers = 1;
|
||||
repeated Transaction transactions = 2;
|
||||
}
|
||||
|
||||
message Transaction {
|
||||
LocalTimestamp id = 1;
|
||||
repeated LocalTimestamp edit_ids = 2;
|
||||
repeated VectorClockEntry start = 3;
|
||||
repeated VectorClockEntry end = 4;
|
||||
repeated Range ranges = 5;
|
||||
}
|
||||
|
||||
message LocalTimestamp {
|
||||
uint32 replica_id = 1;
|
||||
uint32 value = 2;
|
||||
}
|
||||
|
||||
message UpdateDiagnosticSummary {
|
||||
uint64 project_id = 1;
|
||||
uint64 worktree_id = 2;
|
||||
|
@ -366,16 +415,11 @@ message Buffer {
|
|||
message BufferState {
|
||||
uint64 id = 1;
|
||||
optional File file = 2;
|
||||
string visible_text = 3;
|
||||
string deleted_text = 4;
|
||||
repeated BufferFragment fragments = 5;
|
||||
repeated UndoMapEntry undo_map = 6;
|
||||
repeated VectorClockEntry version = 7;
|
||||
repeated SelectionSet selections = 8;
|
||||
repeated Diagnostic diagnostics = 9;
|
||||
uint32 lamport_timestamp = 10;
|
||||
repeated Operation deferred_operations = 11;
|
||||
repeated string completion_triggers = 12;
|
||||
string base_text = 3;
|
||||
repeated Operation operations = 4;
|
||||
repeated SelectionSet selections = 5;
|
||||
repeated Diagnostic diagnostics = 6;
|
||||
repeated string completion_triggers = 7;
|
||||
}
|
||||
|
||||
message BufferFragment {
|
||||
|
@ -474,7 +518,9 @@ message Operation {
|
|||
}
|
||||
|
||||
message UpdateCompletionTriggers {
|
||||
repeated string triggers = 1;
|
||||
uint32 replica_id = 1;
|
||||
uint32 lamport_timestamp = 2;
|
||||
repeated string triggers = 3;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -179,7 +179,16 @@ impl Peer {
|
|||
let channel = response_channels.lock().as_mut()?.remove(&responding_to);
|
||||
if let Some(mut tx) = channel {
|
||||
let mut requester_resumed = barrier::channel();
|
||||
tx.send((incoming, requester_resumed.0)).await.ok();
|
||||
if let Err(error) = tx.send((incoming, requester_resumed.0)).await {
|
||||
log::debug!(
|
||||
"received RPC but request future was dropped {:?}",
|
||||
error.0 .0
|
||||
);
|
||||
}
|
||||
// Drop response channel before awaiting on the barrier. This allows the
|
||||
// barrier to get dropped even if the request's future is dropped before it
|
||||
// has a chance to observe the response.
|
||||
drop(tx);
|
||||
requester_resumed.1.recv().await;
|
||||
} else {
|
||||
log::warn!("received RPC response to unknown request {}", responding_to);
|
||||
|
@ -337,7 +346,7 @@ mod tests {
|
|||
use async_tungstenite::tungstenite::Message as WebSocketMessage;
|
||||
use gpui::TestAppContext;
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
#[gpui::test(iterations = 50)]
|
||||
async fn test_request_response(cx: TestAppContext) {
|
||||
let executor = cx.foreground();
|
||||
|
||||
|
@ -478,7 +487,7 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
#[gpui::test(iterations = 50)]
|
||||
async fn test_order_of_response_and_incoming(cx: TestAppContext) {
|
||||
let executor = cx.foreground();
|
||||
let server = Peer::new();
|
||||
|
@ -576,7 +585,119 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
#[gpui::test(iterations = 50)]
|
||||
async fn test_dropping_request_before_completion(cx: TestAppContext) {
|
||||
let executor = cx.foreground();
|
||||
let server = Peer::new();
|
||||
let client = Peer::new();
|
||||
|
||||
let (client_to_server_conn, server_to_client_conn, _) =
|
||||
Connection::in_memory(cx.background());
|
||||
let (client_to_server_conn_id, io_task1, mut client_incoming) =
|
||||
client.add_connection(client_to_server_conn).await;
|
||||
let (server_to_client_conn_id, io_task2, mut server_incoming) =
|
||||
server.add_connection(server_to_client_conn).await;
|
||||
|
||||
executor.spawn(io_task1).detach();
|
||||
executor.spawn(io_task2).detach();
|
||||
|
||||
executor
|
||||
.spawn(async move {
|
||||
let request1 = server_incoming
|
||||
.next()
|
||||
.await
|
||||
.unwrap()
|
||||
.into_any()
|
||||
.downcast::<TypedEnvelope<proto::Ping>>()
|
||||
.unwrap();
|
||||
let request2 = server_incoming
|
||||
.next()
|
||||
.await
|
||||
.unwrap()
|
||||
.into_any()
|
||||
.downcast::<TypedEnvelope<proto::Ping>>()
|
||||
.unwrap();
|
||||
|
||||
server
|
||||
.send(
|
||||
server_to_client_conn_id,
|
||||
proto::Error {
|
||||
message: "message 1".to_string(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
server
|
||||
.send(
|
||||
server_to_client_conn_id,
|
||||
proto::Error {
|
||||
message: "message 2".to_string(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
server.respond(request1.receipt(), proto::Ack {}).unwrap();
|
||||
server.respond(request2.receipt(), proto::Ack {}).unwrap();
|
||||
|
||||
// Prevent the connection from being dropped
|
||||
server_incoming.next().await;
|
||||
})
|
||||
.detach();
|
||||
|
||||
let events = Arc::new(Mutex::new(Vec::new()));
|
||||
|
||||
let request1 = client.request(client_to_server_conn_id, proto::Ping {});
|
||||
let request1_task = executor.spawn(request1);
|
||||
let request2 = client.request(client_to_server_conn_id, proto::Ping {});
|
||||
let request2_task = executor.spawn({
|
||||
let events = events.clone();
|
||||
async move {
|
||||
request2.await.unwrap();
|
||||
events.lock().push("response 2".to_string());
|
||||
}
|
||||
});
|
||||
|
||||
executor
|
||||
.spawn({
|
||||
let events = events.clone();
|
||||
async move {
|
||||
let incoming1 = client_incoming
|
||||
.next()
|
||||
.await
|
||||
.unwrap()
|
||||
.into_any()
|
||||
.downcast::<TypedEnvelope<proto::Error>>()
|
||||
.unwrap();
|
||||
events.lock().push(incoming1.payload.message);
|
||||
let incoming2 = client_incoming
|
||||
.next()
|
||||
.await
|
||||
.unwrap()
|
||||
.into_any()
|
||||
.downcast::<TypedEnvelope<proto::Error>>()
|
||||
.unwrap();
|
||||
events.lock().push(incoming2.payload.message);
|
||||
|
||||
// Prevent the connection from being dropped
|
||||
client_incoming.next().await;
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
// Allow the request to make some progress before dropping it.
|
||||
cx.background().simulate_random_delay().await;
|
||||
drop(request1_task);
|
||||
|
||||
request2_task.await;
|
||||
assert_eq!(
|
||||
&*events.lock(),
|
||||
&[
|
||||
"message 1".to_string(),
|
||||
"message 2".to_string(),
|
||||
"response 2".to_string()
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 50)]
|
||||
async fn test_disconnect(cx: TestAppContext) {
|
||||
let executor = cx.foreground();
|
||||
|
||||
|
@ -611,7 +732,7 @@ mod tests {
|
|||
.is_err());
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
#[gpui::test(iterations = 50)]
|
||||
async fn test_io_error(cx: TestAppContext) {
|
||||
let executor = cx.foreground();
|
||||
let (client_conn, mut server_conn, _) = Connection::in_memory(cx.background());
|
||||
|
|
|
@ -122,6 +122,8 @@ macro_rules! entity_messages {
|
|||
messages!(
|
||||
Ack,
|
||||
AddProjectCollaborator,
|
||||
ApplyCodeAction,
|
||||
ApplyCodeActionResponse,
|
||||
ApplyCompletionAdditionalEdits,
|
||||
ApplyCompletionAdditionalEditsResponse,
|
||||
BufferReloaded,
|
||||
|
@ -131,11 +133,14 @@ messages!(
|
|||
DiskBasedDiagnosticsUpdated,
|
||||
DiskBasedDiagnosticsUpdating,
|
||||
Error,
|
||||
FormatBuffer,
|
||||
FormatBuffers,
|
||||
FormatBuffersResponse,
|
||||
GetChannelMessages,
|
||||
GetChannelMessagesResponse,
|
||||
GetChannels,
|
||||
GetChannelsResponse,
|
||||
GetCodeActions,
|
||||
GetCodeActionsResponse,
|
||||
GetCompletions,
|
||||
GetCompletionsResponse,
|
||||
GetDefinition,
|
||||
|
@ -171,13 +176,15 @@ messages!(
|
|||
);
|
||||
|
||||
request_messages!(
|
||||
(ApplyCodeAction, ApplyCodeActionResponse),
|
||||
(
|
||||
ApplyCompletionAdditionalEdits,
|
||||
ApplyCompletionAdditionalEditsResponse
|
||||
),
|
||||
(FormatBuffer, Ack),
|
||||
(FormatBuffers, FormatBuffersResponse),
|
||||
(GetChannelMessages, GetChannelMessagesResponse),
|
||||
(GetChannels, GetChannelsResponse),
|
||||
(GetCodeActions, GetCodeActionsResponse),
|
||||
(GetCompletions, GetCompletionsResponse),
|
||||
(GetDefinition, GetDefinitionResponse),
|
||||
(GetUsers, GetUsersResponse),
|
||||
|
@ -197,13 +204,15 @@ request_messages!(
|
|||
entity_messages!(
|
||||
project_id,
|
||||
AddProjectCollaborator,
|
||||
ApplyCodeAction,
|
||||
ApplyCompletionAdditionalEdits,
|
||||
BufferReloaded,
|
||||
BufferSaved,
|
||||
CloseBuffer,
|
||||
DiskBasedDiagnosticsUpdated,
|
||||
DiskBasedDiagnosticsUpdating,
|
||||
FormatBuffer,
|
||||
FormatBuffers,
|
||||
GetCodeActions,
|
||||
GetCompletions,
|
||||
GetDefinition,
|
||||
JoinProject,
|
||||
|
|
|
@ -5,4 +5,4 @@ pub mod proto;
|
|||
pub use conn::Connection;
|
||||
pub use peer::*;
|
||||
|
||||
pub const PROTOCOL_VERSION: u32 = 5;
|
||||
pub const PROTOCOL_VERSION: u32 = 6;
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -122,10 +122,10 @@ impl Store {
|
|||
|
||||
let mut result = RemovedConnectionState::default();
|
||||
for project_id in connection.projects.clone() {
|
||||
if let Some(project) = self.unregister_project(project_id, connection_id) {
|
||||
if let Ok(project) = self.unregister_project(project_id, connection_id) {
|
||||
result.contact_ids.extend(project.authorized_user_ids());
|
||||
result.hosted_projects.insert(project_id, project);
|
||||
} else if let Some(project) = self.leave_project(connection_id, project_id) {
|
||||
} else if let Ok(project) = self.leave_project(connection_id, project_id) {
|
||||
result
|
||||
.guest_project_ids
|
||||
.insert(project_id, project.connection_ids);
|
||||
|
@ -254,9 +254,14 @@ impl Store {
|
|||
&mut self,
|
||||
project_id: u64,
|
||||
worktree_id: u64,
|
||||
connection_id: ConnectionId,
|
||||
worktree: Worktree,
|
||||
) -> bool {
|
||||
if let Some(project) = self.projects.get_mut(&project_id) {
|
||||
) -> tide::Result<()> {
|
||||
let project = self
|
||||
.projects
|
||||
.get_mut(&project_id)
|
||||
.ok_or_else(|| anyhow!("no such project"))?;
|
||||
if project.host_connection_id == connection_id {
|
||||
for authorized_user_id in &worktree.authorized_user_ids {
|
||||
self.visible_projects_by_user_id
|
||||
.entry(*authorized_user_id)
|
||||
|
@ -270,9 +275,9 @@ impl Store {
|
|||
|
||||
#[cfg(test)]
|
||||
self.check_invariants();
|
||||
true
|
||||
Ok(())
|
||||
} else {
|
||||
false
|
||||
Err(anyhow!("no such project"))?
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -280,7 +285,7 @@ impl Store {
|
|||
&mut self,
|
||||
project_id: u64,
|
||||
connection_id: ConnectionId,
|
||||
) -> Option<Project> {
|
||||
) -> tide::Result<Project> {
|
||||
match self.projects.entry(project_id) {
|
||||
hash_map::Entry::Occupied(e) => {
|
||||
if e.get().host_connection_id == connection_id {
|
||||
|
@ -292,12 +297,12 @@ impl Store {
|
|||
}
|
||||
}
|
||||
|
||||
Some(e.remove())
|
||||
Ok(e.remove())
|
||||
} else {
|
||||
None
|
||||
Err(anyhow!("no such project"))?
|
||||
}
|
||||
}
|
||||
hash_map::Entry::Vacant(_) => None,
|
||||
hash_map::Entry::Vacant(_) => Err(anyhow!("no such project"))?,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -398,20 +403,26 @@ impl Store {
|
|||
connection_id: ConnectionId,
|
||||
entries: HashMap<u64, proto::Entry>,
|
||||
diagnostic_summaries: BTreeMap<PathBuf, proto::DiagnosticSummary>,
|
||||
) -> Option<SharedWorktree> {
|
||||
let project = self.projects.get_mut(&project_id)?;
|
||||
let worktree = project.worktrees.get_mut(&worktree_id)?;
|
||||
) -> tide::Result<SharedWorktree> {
|
||||
let project = self
|
||||
.projects
|
||||
.get_mut(&project_id)
|
||||
.ok_or_else(|| anyhow!("no such project"))?;
|
||||
let worktree = project
|
||||
.worktrees
|
||||
.get_mut(&worktree_id)
|
||||
.ok_or_else(|| anyhow!("no such worktree"))?;
|
||||
if project.host_connection_id == connection_id && project.share.is_some() {
|
||||
worktree.share = Some(WorktreeShare {
|
||||
entries,
|
||||
diagnostic_summaries,
|
||||
});
|
||||
Some(SharedWorktree {
|
||||
Ok(SharedWorktree {
|
||||
authorized_user_ids: project.authorized_user_ids(),
|
||||
connection_ids: project.guest_connection_ids(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
Err(anyhow!("no such worktree"))?
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -421,19 +432,25 @@ impl Store {
|
|||
worktree_id: u64,
|
||||
connection_id: ConnectionId,
|
||||
summary: proto::DiagnosticSummary,
|
||||
) -> Option<Vec<ConnectionId>> {
|
||||
let project = self.projects.get_mut(&project_id)?;
|
||||
let worktree = project.worktrees.get_mut(&worktree_id)?;
|
||||
) -> tide::Result<Vec<ConnectionId>> {
|
||||
let project = self
|
||||
.projects
|
||||
.get_mut(&project_id)
|
||||
.ok_or_else(|| anyhow!("no such project"))?;
|
||||
let worktree = project
|
||||
.worktrees
|
||||
.get_mut(&worktree_id)
|
||||
.ok_or_else(|| anyhow!("no such worktree"))?;
|
||||
if project.host_connection_id == connection_id {
|
||||
if let Some(share) = worktree.share.as_mut() {
|
||||
share
|
||||
.diagnostic_summaries
|
||||
.insert(summary.path.clone().into(), summary);
|
||||
return Some(project.connection_ids());
|
||||
return Ok(project.connection_ids());
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
Err(anyhow!("no such worktree"))?
|
||||
}
|
||||
|
||||
pub fn join_project(
|
||||
|
@ -481,10 +498,19 @@ impl Store {
|
|||
&mut self,
|
||||
connection_id: ConnectionId,
|
||||
project_id: u64,
|
||||
) -> Option<LeftProject> {
|
||||
let project = self.projects.get_mut(&project_id)?;
|
||||
let share = project.share.as_mut()?;
|
||||
let (replica_id, _) = share.guests.remove(&connection_id)?;
|
||||
) -> tide::Result<LeftProject> {
|
||||
let project = self
|
||||
.projects
|
||||
.get_mut(&project_id)
|
||||
.ok_or_else(|| anyhow!("no such project"))?;
|
||||
let share = project
|
||||
.share
|
||||
.as_mut()
|
||||
.ok_or_else(|| anyhow!("project is not shared"))?;
|
||||
let (replica_id, _) = share
|
||||
.guests
|
||||
.remove(&connection_id)
|
||||
.ok_or_else(|| anyhow!("cannot leave a project before joining it"))?;
|
||||
share.active_replica_ids.remove(&replica_id);
|
||||
|
||||
if let Some(connection) = self.connections.get_mut(&connection_id) {
|
||||
|
@ -497,7 +523,7 @@ impl Store {
|
|||
#[cfg(test)]
|
||||
self.check_invariants();
|
||||
|
||||
Some(LeftProject {
|
||||
Ok(LeftProject {
|
||||
connection_ids,
|
||||
authorized_user_ids,
|
||||
})
|
||||
|
@ -510,31 +536,40 @@ impl Store {
|
|||
worktree_id: u64,
|
||||
removed_entries: &[u64],
|
||||
updated_entries: &[proto::Entry],
|
||||
) -> Option<Vec<ConnectionId>> {
|
||||
) -> tide::Result<Vec<ConnectionId>> {
|
||||
let project = self.write_project(project_id, connection_id)?;
|
||||
let share = project.worktrees.get_mut(&worktree_id)?.share.as_mut()?;
|
||||
let share = project
|
||||
.worktrees
|
||||
.get_mut(&worktree_id)
|
||||
.ok_or_else(|| anyhow!("no such worktree"))?
|
||||
.share
|
||||
.as_mut()
|
||||
.ok_or_else(|| anyhow!("worktree is not shared"))?;
|
||||
for entry_id in removed_entries {
|
||||
share.entries.remove(&entry_id);
|
||||
}
|
||||
for entry in updated_entries {
|
||||
share.entries.insert(entry.id, entry.clone());
|
||||
}
|
||||
Some(project.connection_ids())
|
||||
Ok(project.connection_ids())
|
||||
}
|
||||
|
||||
pub fn project_connection_ids(
|
||||
&self,
|
||||
project_id: u64,
|
||||
acting_connection_id: ConnectionId,
|
||||
) -> Option<Vec<ConnectionId>> {
|
||||
Some(
|
||||
self.read_project(project_id, acting_connection_id)?
|
||||
.connection_ids(),
|
||||
)
|
||||
) -> tide::Result<Vec<ConnectionId>> {
|
||||
Ok(self
|
||||
.read_project(project_id, acting_connection_id)?
|
||||
.connection_ids())
|
||||
}
|
||||
|
||||
pub fn channel_connection_ids(&self, channel_id: ChannelId) -> Option<Vec<ConnectionId>> {
|
||||
Some(self.channels.get(&channel_id)?.connection_ids())
|
||||
pub fn channel_connection_ids(&self, channel_id: ChannelId) -> tide::Result<Vec<ConnectionId>> {
|
||||
Ok(self
|
||||
.channels
|
||||
.get(&channel_id)
|
||||
.ok_or_else(|| anyhow!("no such channel"))?
|
||||
.connection_ids())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -542,14 +577,26 @@ impl Store {
|
|||
self.projects.get(&project_id)
|
||||
}
|
||||
|
||||
pub fn read_project(&self, project_id: u64, connection_id: ConnectionId) -> Option<&Project> {
|
||||
let project = self.projects.get(&project_id)?;
|
||||
pub fn read_project(
|
||||
&self,
|
||||
project_id: u64,
|
||||
connection_id: ConnectionId,
|
||||
) -> tide::Result<&Project> {
|
||||
let project = self
|
||||
.projects
|
||||
.get(&project_id)
|
||||
.ok_or_else(|| anyhow!("no such project"))?;
|
||||
if project.host_connection_id == connection_id
|
||||
|| project.share.as_ref()?.guests.contains_key(&connection_id)
|
||||
|| project
|
||||
.share
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("project is not shared"))?
|
||||
.guests
|
||||
.contains_key(&connection_id)
|
||||
{
|
||||
Some(project)
|
||||
Ok(project)
|
||||
} else {
|
||||
None
|
||||
Err(anyhow!("no such project"))?
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -557,14 +604,22 @@ impl Store {
|
|||
&mut self,
|
||||
project_id: u64,
|
||||
connection_id: ConnectionId,
|
||||
) -> Option<&mut Project> {
|
||||
let project = self.projects.get_mut(&project_id)?;
|
||||
) -> tide::Result<&mut Project> {
|
||||
let project = self
|
||||
.projects
|
||||
.get_mut(&project_id)
|
||||
.ok_or_else(|| anyhow!("no such project"))?;
|
||||
if project.host_connection_id == connection_id
|
||||
|| project.share.as_ref()?.guests.contains_key(&connection_id)
|
||||
|| project
|
||||
.share
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("project is not shared"))?
|
||||
.guests
|
||||
.contains_key(&connection_id)
|
||||
{
|
||||
Some(project)
|
||||
Ok(project)
|
||||
} else {
|
||||
None
|
||||
Err(anyhow!("no such project"))?
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use super::{Point, ToOffset};
|
||||
use crate::{rope::TextDimension, BufferSnapshot};
|
||||
use crate::{rope::TextDimension, BufferSnapshot, PointUtf16, ToPointUtf16};
|
||||
use anyhow::Result;
|
||||
use std::{cmp::Ordering, fmt::Debug, ops::Range};
|
||||
use sum_tree::Bias;
|
||||
|
@ -78,6 +78,7 @@ pub trait AnchorRangeExt {
|
|||
fn cmp(&self, b: &Range<Anchor>, buffer: &BufferSnapshot) -> Result<Ordering>;
|
||||
fn to_offset(&self, content: &BufferSnapshot) -> Range<usize>;
|
||||
fn to_point(&self, content: &BufferSnapshot) -> Range<Point>;
|
||||
fn to_point_utf16(&self, content: &BufferSnapshot) -> Range<PointUtf16>;
|
||||
}
|
||||
|
||||
impl AnchorRangeExt for Range<Anchor> {
|
||||
|
@ -95,4 +96,8 @@ impl AnchorRangeExt for Range<Anchor> {
|
|||
fn to_point(&self, content: &BufferSnapshot) -> Range<Point> {
|
||||
self.start.summary::<Point>(&content)..self.end.summary::<Point>(&content)
|
||||
}
|
||||
|
||||
fn to_point_utf16(&self, content: &BufferSnapshot) -> Range<PointUtf16> {
|
||||
self.start.to_point_utf16(content)..self.end.to_point_utf16(content)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -179,6 +179,19 @@ impl Rope {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn point_to_point_utf16(&self, point: Point) -> PointUtf16 {
|
||||
if point >= self.summary().lines {
|
||||
return self.summary().lines_utf16;
|
||||
}
|
||||
let mut cursor = self.chunks.cursor::<(Point, PointUtf16)>();
|
||||
cursor.seek(&point, Bias::Left, &());
|
||||
let overshoot = point - cursor.start().0;
|
||||
cursor.start().1
|
||||
+ cursor.item().map_or(PointUtf16::zero(), |chunk| {
|
||||
chunk.point_to_point_utf16(overshoot)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn point_to_offset(&self, point: Point) -> usize {
|
||||
if point >= self.summary().lines {
|
||||
return self.summary().bytes;
|
||||
|
@ -580,6 +593,27 @@ impl Chunk {
|
|||
offset
|
||||
}
|
||||
|
||||
fn point_to_point_utf16(&self, target: Point) -> PointUtf16 {
|
||||
let mut point = Point::zero();
|
||||
let mut point_utf16 = PointUtf16::new(0, 0);
|
||||
for ch in self.0.chars() {
|
||||
if point >= target {
|
||||
break;
|
||||
}
|
||||
|
||||
if ch == '\n' {
|
||||
point_utf16.row += 1;
|
||||
point_utf16.column = 0;
|
||||
point.row += 1;
|
||||
point.column = 0;
|
||||
} else {
|
||||
point_utf16.column += ch.len_utf16() as u32;
|
||||
point.column += ch.len_utf8() as u32;
|
||||
}
|
||||
}
|
||||
point_utf16
|
||||
}
|
||||
|
||||
fn point_utf16_to_offset(&self, target: PointUtf16) -> usize {
|
||||
let mut offset = 0;
|
||||
let mut point = PointUtf16::new(0, 0);
|
||||
|
|
|
@ -432,28 +432,28 @@ fn test_undo_redo() {
|
|||
buffer.edit(vec![3..5], "cd");
|
||||
assert_eq!(buffer.text(), "1abcdef234");
|
||||
|
||||
let transactions = buffer.history.undo_stack.clone();
|
||||
assert_eq!(transactions.len(), 3);
|
||||
let entries = buffer.history.undo_stack.clone();
|
||||
assert_eq!(entries.len(), 3);
|
||||
|
||||
buffer.undo_or_redo(transactions[0].clone()).unwrap();
|
||||
buffer.undo_or_redo(entries[0].transaction.clone()).unwrap();
|
||||
assert_eq!(buffer.text(), "1cdef234");
|
||||
buffer.undo_or_redo(transactions[0].clone()).unwrap();
|
||||
buffer.undo_or_redo(entries[0].transaction.clone()).unwrap();
|
||||
assert_eq!(buffer.text(), "1abcdef234");
|
||||
|
||||
buffer.undo_or_redo(transactions[1].clone()).unwrap();
|
||||
buffer.undo_or_redo(entries[1].transaction.clone()).unwrap();
|
||||
assert_eq!(buffer.text(), "1abcdx234");
|
||||
buffer.undo_or_redo(transactions[2].clone()).unwrap();
|
||||
buffer.undo_or_redo(entries[2].transaction.clone()).unwrap();
|
||||
assert_eq!(buffer.text(), "1abx234");
|
||||
buffer.undo_or_redo(transactions[1].clone()).unwrap();
|
||||
buffer.undo_or_redo(entries[1].transaction.clone()).unwrap();
|
||||
assert_eq!(buffer.text(), "1abyzef234");
|
||||
buffer.undo_or_redo(transactions[2].clone()).unwrap();
|
||||
buffer.undo_or_redo(entries[2].transaction.clone()).unwrap();
|
||||
assert_eq!(buffer.text(), "1abcdef234");
|
||||
|
||||
buffer.undo_or_redo(transactions[2].clone()).unwrap();
|
||||
buffer.undo_or_redo(entries[2].transaction.clone()).unwrap();
|
||||
assert_eq!(buffer.text(), "1abyzef234");
|
||||
buffer.undo_or_redo(transactions[0].clone()).unwrap();
|
||||
buffer.undo_or_redo(entries[0].transaction.clone()).unwrap();
|
||||
assert_eq!(buffer.text(), "1yzef234");
|
||||
buffer.undo_or_redo(transactions[1].clone()).unwrap();
|
||||
buffer.undo_or_redo(entries[1].transaction.clone()).unwrap();
|
||||
assert_eq!(buffer.text(), "1234");
|
||||
}
|
||||
|
||||
|
@ -502,7 +502,7 @@ fn test_history() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_avoid_grouping_next_transaction() {
|
||||
fn test_finalize_last_transaction() {
|
||||
let now = Instant::now();
|
||||
let mut buffer = Buffer::new(0, 0, History::new("123456".into()));
|
||||
|
||||
|
@ -511,7 +511,7 @@ fn test_avoid_grouping_next_transaction() {
|
|||
buffer.end_transaction_at(now);
|
||||
assert_eq!(buffer.text(), "12cd56");
|
||||
|
||||
buffer.avoid_grouping_next_transaction();
|
||||
buffer.finalize_last_transaction();
|
||||
buffer.start_transaction_at(now);
|
||||
buffer.edit(vec![4..5], "e");
|
||||
buffer.end_transaction_at(now).unwrap();
|
||||
|
@ -536,6 +536,44 @@ fn test_avoid_grouping_next_transaction() {
|
|||
assert_eq!(buffer.text(), "ab2cde6");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edited_ranges_for_transaction() {
|
||||
let now = Instant::now();
|
||||
let mut buffer = Buffer::new(0, 0, History::new("1234567".into()));
|
||||
|
||||
buffer.start_transaction_at(now);
|
||||
buffer.edit(vec![2..4], "cd");
|
||||
buffer.edit(vec![6..6], "efg");
|
||||
buffer.end_transaction_at(now);
|
||||
assert_eq!(buffer.text(), "12cd56efg7");
|
||||
|
||||
let tx = buffer.finalize_last_transaction().unwrap().clone();
|
||||
assert_eq!(
|
||||
buffer
|
||||
.edited_ranges_for_transaction::<usize>(&tx)
|
||||
.collect::<Vec<_>>(),
|
||||
[2..4, 6..9]
|
||||
);
|
||||
|
||||
buffer.edit(vec![5..5], "hijk");
|
||||
assert_eq!(buffer.text(), "12cd5hijk6efg7");
|
||||
assert_eq!(
|
||||
buffer
|
||||
.edited_ranges_for_transaction::<usize>(&tx)
|
||||
.collect::<Vec<_>>(),
|
||||
[2..4, 10..13]
|
||||
);
|
||||
|
||||
buffer.edit(vec![4..4], "l");
|
||||
assert_eq!(buffer.text(), "12cdl5hijk6efg7");
|
||||
assert_eq!(
|
||||
buffer
|
||||
.edited_ranges_for_transaction::<usize>(&tx)
|
||||
.collect::<Vec<_>>(),
|
||||
[2..4, 11..14]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_concurrent_edits() {
|
||||
let text = "abcdef";
|
||||
|
@ -551,12 +589,12 @@ fn test_concurrent_edits() {
|
|||
let buf3_op = buffer3.edit(vec![5..6], "56");
|
||||
assert_eq!(buffer3.text(), "abcde56");
|
||||
|
||||
buffer1.apply_op(Operation::Edit(buf2_op.clone())).unwrap();
|
||||
buffer1.apply_op(Operation::Edit(buf3_op.clone())).unwrap();
|
||||
buffer2.apply_op(Operation::Edit(buf1_op.clone())).unwrap();
|
||||
buffer2.apply_op(Operation::Edit(buf3_op.clone())).unwrap();
|
||||
buffer3.apply_op(Operation::Edit(buf1_op.clone())).unwrap();
|
||||
buffer3.apply_op(Operation::Edit(buf2_op.clone())).unwrap();
|
||||
buffer1.apply_op(buf2_op.clone()).unwrap();
|
||||
buffer1.apply_op(buf3_op.clone()).unwrap();
|
||||
buffer2.apply_op(buf1_op.clone()).unwrap();
|
||||
buffer2.apply_op(buf3_op.clone()).unwrap();
|
||||
buffer3.apply_op(buf1_op.clone()).unwrap();
|
||||
buffer3.apply_op(buf2_op.clone()).unwrap();
|
||||
|
||||
assert_eq!(buffer1.text(), "a12c34e56");
|
||||
assert_eq!(buffer2.text(), "a12c34e56");
|
||||
|
|
|
@ -40,7 +40,7 @@ pub use subscription::*;
|
|||
pub use sum_tree::Bias;
|
||||
use sum_tree::{FilterCursor, SumTree};
|
||||
|
||||
pub type TransactionId = usize;
|
||||
pub type TransactionId = clock::Local;
|
||||
|
||||
pub struct Buffer {
|
||||
snapshot: BufferSnapshot,
|
||||
|
@ -67,28 +67,37 @@ pub struct BufferSnapshot {
|
|||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Transaction {
|
||||
id: TransactionId,
|
||||
start: clock::Global,
|
||||
end: clock::Global,
|
||||
edits: Vec<clock::Local>,
|
||||
ranges: Vec<Range<FullOffset>>,
|
||||
pub struct HistoryEntry {
|
||||
transaction: Transaction,
|
||||
first_edit_at: Instant,
|
||||
last_edit_at: Instant,
|
||||
suppress_grouping: bool,
|
||||
}
|
||||
|
||||
impl Transaction {
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Transaction {
|
||||
pub id: TransactionId,
|
||||
pub edit_ids: Vec<clock::Local>,
|
||||
pub start: clock::Global,
|
||||
pub end: clock::Global,
|
||||
pub ranges: Vec<Range<FullOffset>>,
|
||||
}
|
||||
|
||||
impl HistoryEntry {
|
||||
pub fn transaction_id(&self) -> TransactionId {
|
||||
self.transaction.id
|
||||
}
|
||||
|
||||
fn push_edit(&mut self, edit: &EditOperation) {
|
||||
self.edits.push(edit.timestamp.local());
|
||||
self.end.observe(edit.timestamp.local());
|
||||
self.transaction.edit_ids.push(edit.timestamp.local());
|
||||
self.transaction.end.observe(edit.timestamp.local());
|
||||
|
||||
let mut other_ranges = edit.ranges.iter().peekable();
|
||||
let mut new_ranges = Vec::new();
|
||||
let insertion_len = edit.new_text.as_ref().map_or(0, |t| t.len());
|
||||
let mut delta = 0;
|
||||
|
||||
for mut self_range in self.ranges.iter().cloned() {
|
||||
for mut self_range in self.transaction.ranges.iter().cloned() {
|
||||
self_range.start += delta;
|
||||
self_range.end += delta;
|
||||
|
||||
|
@ -122,7 +131,7 @@ impl Transaction {
|
|||
delta += insertion_len;
|
||||
}
|
||||
|
||||
self.ranges = new_ranges;
|
||||
self.transaction.ranges = new_ranges;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -130,42 +139,46 @@ impl Transaction {
|
|||
pub struct History {
|
||||
// TODO: Turn this into a String or Rope, maybe.
|
||||
pub base_text: Arc<str>,
|
||||
ops: HashMap<clock::Local, EditOperation>,
|
||||
undo_stack: Vec<Transaction>,
|
||||
redo_stack: Vec<Transaction>,
|
||||
operations: HashMap<clock::Local, Operation>,
|
||||
undo_stack: Vec<HistoryEntry>,
|
||||
redo_stack: Vec<HistoryEntry>,
|
||||
transaction_depth: usize,
|
||||
group_interval: Duration,
|
||||
next_transaction_id: TransactionId,
|
||||
}
|
||||
|
||||
impl History {
|
||||
pub fn new(base_text: Arc<str>) -> Self {
|
||||
Self {
|
||||
base_text,
|
||||
ops: Default::default(),
|
||||
operations: Default::default(),
|
||||
undo_stack: Vec::new(),
|
||||
redo_stack: Vec::new(),
|
||||
transaction_depth: 0,
|
||||
group_interval: Duration::from_millis(300),
|
||||
next_transaction_id: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn push(&mut self, op: EditOperation) {
|
||||
self.ops.insert(op.timestamp.local(), op);
|
||||
fn push(&mut self, op: Operation) {
|
||||
self.operations.insert(op.local_timestamp(), op);
|
||||
}
|
||||
|
||||
fn start_transaction(&mut self, start: clock::Global, now: Instant) -> Option<TransactionId> {
|
||||
fn start_transaction(
|
||||
&mut self,
|
||||
start: clock::Global,
|
||||
now: Instant,
|
||||
local_clock: &mut clock::Local,
|
||||
) -> Option<TransactionId> {
|
||||
self.transaction_depth += 1;
|
||||
if self.transaction_depth == 1 {
|
||||
let id = self.next_transaction_id;
|
||||
self.next_transaction_id += 1;
|
||||
self.undo_stack.push(Transaction {
|
||||
id,
|
||||
start: start.clone(),
|
||||
end: start,
|
||||
edits: Vec::new(),
|
||||
ranges: Vec::new(),
|
||||
let id = local_clock.tick();
|
||||
self.undo_stack.push(HistoryEntry {
|
||||
transaction: Transaction {
|
||||
id,
|
||||
start: start.clone(),
|
||||
end: start,
|
||||
edit_ids: Default::default(),
|
||||
ranges: Default::default(),
|
||||
},
|
||||
first_edit_at: now,
|
||||
last_edit_at: now,
|
||||
suppress_grouping: false,
|
||||
|
@ -176,17 +189,24 @@ impl History {
|
|||
}
|
||||
}
|
||||
|
||||
fn end_transaction(&mut self, now: Instant) -> Option<&Transaction> {
|
||||
fn end_transaction(&mut self, now: Instant) -> Option<&HistoryEntry> {
|
||||
assert_ne!(self.transaction_depth, 0);
|
||||
self.transaction_depth -= 1;
|
||||
if self.transaction_depth == 0 {
|
||||
if self.undo_stack.last().unwrap().ranges.is_empty() {
|
||||
if self
|
||||
.undo_stack
|
||||
.last()
|
||||
.unwrap()
|
||||
.transaction
|
||||
.ranges
|
||||
.is_empty()
|
||||
{
|
||||
self.undo_stack.pop();
|
||||
None
|
||||
} else {
|
||||
let transaction = self.undo_stack.last_mut().unwrap();
|
||||
transaction.last_edit_at = now;
|
||||
Some(transaction)
|
||||
let entry = self.undo_stack.last_mut().unwrap();
|
||||
entry.last_edit_at = now;
|
||||
Some(entry)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
|
@ -195,16 +215,15 @@ impl History {
|
|||
|
||||
fn group(&mut self) -> Option<TransactionId> {
|
||||
let mut new_len = self.undo_stack.len();
|
||||
let mut transactions = self.undo_stack.iter_mut();
|
||||
let mut entries = self.undo_stack.iter_mut();
|
||||
|
||||
if let Some(mut transaction) = transactions.next_back() {
|
||||
while let Some(prev_transaction) = transactions.next_back() {
|
||||
if !prev_transaction.suppress_grouping
|
||||
&& transaction.first_edit_at - prev_transaction.last_edit_at
|
||||
<= self.group_interval
|
||||
&& transaction.start == prev_transaction.end
|
||||
if let Some(mut entry) = entries.next_back() {
|
||||
while let Some(prev_entry) = entries.next_back() {
|
||||
if !prev_entry.suppress_grouping
|
||||
&& entry.first_edit_at - prev_entry.last_edit_at <= self.group_interval
|
||||
&& entry.transaction.start == prev_entry.transaction.end
|
||||
{
|
||||
transaction = prev_transaction;
|
||||
entry = prev_entry;
|
||||
new_len -= 1;
|
||||
} else {
|
||||
break;
|
||||
|
@ -212,101 +231,114 @@ impl History {
|
|||
}
|
||||
}
|
||||
|
||||
let (transactions_to_keep, transactions_to_merge) = self.undo_stack.split_at_mut(new_len);
|
||||
if let Some(last_transaction) = transactions_to_keep.last_mut() {
|
||||
for transaction in &*transactions_to_merge {
|
||||
for edit_id in &transaction.edits {
|
||||
last_transaction.push_edit(&self.ops[edit_id]);
|
||||
let (entries_to_keep, entries_to_merge) = self.undo_stack.split_at_mut(new_len);
|
||||
if let Some(last_entry) = entries_to_keep.last_mut() {
|
||||
for entry in &*entries_to_merge {
|
||||
for edit_id in &entry.transaction.edit_ids {
|
||||
last_entry.push_edit(self.operations[edit_id].as_edit().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(transaction) = transactions_to_merge.last_mut() {
|
||||
last_transaction.last_edit_at = transaction.last_edit_at;
|
||||
last_transaction.end = transaction.end.clone();
|
||||
if let Some(entry) = entries_to_merge.last_mut() {
|
||||
last_entry.last_edit_at = entry.last_edit_at;
|
||||
last_entry.transaction.end = entry.transaction.end.clone();
|
||||
}
|
||||
}
|
||||
|
||||
self.undo_stack.truncate(new_len);
|
||||
self.undo_stack.last().map(|t| t.id)
|
||||
self.undo_stack.last().map(|e| e.transaction.id)
|
||||
}
|
||||
|
||||
fn avoid_grouping_next_transaction(&mut self) {
|
||||
if let Some(transaction) = self.undo_stack.last_mut() {
|
||||
transaction.suppress_grouping = true;
|
||||
}
|
||||
fn finalize_last_transaction(&mut self) -> Option<&Transaction> {
|
||||
self.undo_stack.last_mut().map(|entry| {
|
||||
entry.suppress_grouping = true;
|
||||
&entry.transaction
|
||||
})
|
||||
}
|
||||
|
||||
fn push_transaction(&mut self, edit_ids: impl IntoIterator<Item = clock::Local>, now: Instant) {
|
||||
fn push_transaction(&mut self, transaction: Transaction, now: Instant) {
|
||||
assert_eq!(self.transaction_depth, 0);
|
||||
let mut edit_ids = edit_ids.into_iter().peekable();
|
||||
|
||||
if let Some(first_edit_id) = edit_ids.peek() {
|
||||
let version = self.ops[first_edit_id].version.clone();
|
||||
self.start_transaction(version, now);
|
||||
for edit_id in edit_ids {
|
||||
self.push_undo(edit_id);
|
||||
}
|
||||
self.end_transaction(now);
|
||||
}
|
||||
self.undo_stack.push(HistoryEntry {
|
||||
transaction,
|
||||
first_edit_at: now,
|
||||
last_edit_at: now,
|
||||
suppress_grouping: false,
|
||||
});
|
||||
}
|
||||
|
||||
fn push_undo(&mut self, edit_id: clock::Local) {
|
||||
fn push_undo(&mut self, op_id: clock::Local) {
|
||||
assert_ne!(self.transaction_depth, 0);
|
||||
let last_transaction = self.undo_stack.last_mut().unwrap();
|
||||
last_transaction.push_edit(&self.ops[&edit_id]);
|
||||
if let Some(Operation::Edit(edit)) = self.operations.get(&op_id) {
|
||||
let last_transaction = self.undo_stack.last_mut().unwrap();
|
||||
last_transaction.push_edit(&edit);
|
||||
}
|
||||
}
|
||||
|
||||
fn pop_undo(&mut self) -> Option<&Transaction> {
|
||||
fn pop_undo(&mut self) -> Option<&HistoryEntry> {
|
||||
assert_eq!(self.transaction_depth, 0);
|
||||
if let Some(transaction) = self.undo_stack.pop() {
|
||||
self.redo_stack.push(transaction);
|
||||
if let Some(entry) = self.undo_stack.pop() {
|
||||
self.redo_stack.push(entry);
|
||||
self.redo_stack.last()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_from_undo(&mut self, transaction_id: TransactionId) -> Option<&Transaction> {
|
||||
fn remove_from_undo(&mut self, transaction_id: TransactionId) -> &[HistoryEntry] {
|
||||
assert_eq!(self.transaction_depth, 0);
|
||||
if let Some(transaction_ix) = self.undo_stack.iter().rposition(|t| t.id == transaction_id) {
|
||||
let transaction = self.undo_stack.remove(transaction_ix);
|
||||
self.redo_stack.push(transaction);
|
||||
self.redo_stack.last()
|
||||
} else {
|
||||
None
|
||||
|
||||
let redo_stack_start_len = self.redo_stack.len();
|
||||
if let Some(entry_ix) = self
|
||||
.undo_stack
|
||||
.iter()
|
||||
.rposition(|entry| entry.transaction.id == transaction_id)
|
||||
{
|
||||
self.redo_stack
|
||||
.extend(self.undo_stack.drain(entry_ix..).rev());
|
||||
}
|
||||
&self.redo_stack[redo_stack_start_len..]
|
||||
}
|
||||
|
||||
fn forget(&mut self, transaction_id: TransactionId) {
|
||||
assert_eq!(self.transaction_depth, 0);
|
||||
if let Some(transaction_ix) = self.undo_stack.iter().rposition(|t| t.id == transaction_id) {
|
||||
self.undo_stack.remove(transaction_ix);
|
||||
} else if let Some(transaction_ix) =
|
||||
self.redo_stack.iter().rposition(|t| t.id == transaction_id)
|
||||
if let Some(entry_ix) = self
|
||||
.undo_stack
|
||||
.iter()
|
||||
.rposition(|entry| entry.transaction.id == transaction_id)
|
||||
{
|
||||
self.undo_stack.remove(transaction_ix);
|
||||
self.undo_stack.remove(entry_ix);
|
||||
} else if let Some(entry_ix) = self
|
||||
.redo_stack
|
||||
.iter()
|
||||
.rposition(|entry| entry.transaction.id == transaction_id)
|
||||
{
|
||||
self.undo_stack.remove(entry_ix);
|
||||
}
|
||||
}
|
||||
|
||||
fn pop_redo(&mut self) -> Option<&Transaction> {
|
||||
fn pop_redo(&mut self) -> Option<&HistoryEntry> {
|
||||
assert_eq!(self.transaction_depth, 0);
|
||||
if let Some(transaction) = self.redo_stack.pop() {
|
||||
self.undo_stack.push(transaction);
|
||||
if let Some(entry) = self.redo_stack.pop() {
|
||||
self.undo_stack.push(entry);
|
||||
self.undo_stack.last()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_from_redo(&mut self, transaction_id: TransactionId) -> Option<&Transaction> {
|
||||
fn remove_from_redo(&mut self, transaction_id: TransactionId) -> &[HistoryEntry] {
|
||||
assert_eq!(self.transaction_depth, 0);
|
||||
if let Some(transaction_ix) = self.redo_stack.iter().rposition(|t| t.id == transaction_id) {
|
||||
let transaction = self.redo_stack.remove(transaction_ix);
|
||||
self.undo_stack.push(transaction);
|
||||
self.undo_stack.last()
|
||||
} else {
|
||||
None
|
||||
|
||||
let undo_stack_start_len = self.undo_stack.len();
|
||||
if let Some(entry_ix) = self
|
||||
.redo_stack
|
||||
.iter()
|
||||
.rposition(|entry| entry.transaction.id == transaction_id)
|
||||
{
|
||||
self.undo_stack
|
||||
.extend(self.redo_stack.drain(entry_ix..).rev());
|
||||
}
|
||||
&self.undo_stack[undo_stack_start_len..]
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -545,57 +577,6 @@ impl Buffer {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn from_parts(
|
||||
replica_id: u16,
|
||||
remote_id: u64,
|
||||
visible_text: &str,
|
||||
deleted_text: &str,
|
||||
undo_map: impl Iterator<Item = (clock::Local, Vec<(clock::Local, u32)>)>,
|
||||
fragments: impl ExactSizeIterator<Item = Fragment>,
|
||||
lamport_timestamp: u32,
|
||||
version: clock::Global,
|
||||
) -> Self {
|
||||
let visible_text = visible_text.into();
|
||||
let deleted_text = deleted_text.into();
|
||||
let fragments = SumTree::from_iter(fragments, &None);
|
||||
let mut insertions = fragments
|
||||
.iter()
|
||||
.map(|fragment| InsertionFragment {
|
||||
timestamp: fragment.insertion_timestamp.local(),
|
||||
split_offset: fragment.insertion_offset,
|
||||
fragment_id: fragment.id.clone(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
insertions.sort_unstable_by_key(|i| (i.timestamp, i.split_offset));
|
||||
Self {
|
||||
remote_id,
|
||||
replica_id,
|
||||
|
||||
history: History::new("".into()),
|
||||
deferred_ops: OperationQueue::new(),
|
||||
deferred_replicas: Default::default(),
|
||||
local_clock: clock::Local {
|
||||
replica_id,
|
||||
value: version.get(replica_id) + 1,
|
||||
},
|
||||
lamport_clock: clock::Lamport {
|
||||
replica_id,
|
||||
value: lamport_timestamp,
|
||||
},
|
||||
subscriptions: Default::default(),
|
||||
edit_id_resolvers: Default::default(),
|
||||
snapshot: BufferSnapshot {
|
||||
replica_id,
|
||||
visible_text,
|
||||
deleted_text,
|
||||
undo_map: UndoMap(undo_map.collect()),
|
||||
fragments,
|
||||
insertions: SumTree::from_iter(insertions, &()),
|
||||
version,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn version(&self) -> clock::Global {
|
||||
self.version.clone()
|
||||
}
|
||||
|
@ -620,7 +601,7 @@ impl Buffer {
|
|||
self.history.group_interval
|
||||
}
|
||||
|
||||
pub fn edit<R, I, S, T>(&mut self, ranges: R, new_text: T) -> EditOperation
|
||||
pub fn edit<R, I, S, T>(&mut self, ranges: R, new_text: T) -> Operation
|
||||
where
|
||||
R: IntoIterator<IntoIter = I>,
|
||||
I: ExactSizeIterator<Item = Range<S>>,
|
||||
|
@ -641,13 +622,14 @@ impl Buffer {
|
|||
local: self.local_clock.tick().value,
|
||||
lamport: self.lamport_clock.tick().value,
|
||||
};
|
||||
let edit = self.apply_local_edit(ranges.into_iter(), new_text, timestamp);
|
||||
let operation =
|
||||
Operation::Edit(self.apply_local_edit(ranges.into_iter(), new_text, timestamp));
|
||||
|
||||
self.history.push(edit.clone());
|
||||
self.history.push_undo(edit.timestamp.local());
|
||||
self.snapshot.version.observe(edit.timestamp.local());
|
||||
self.history.push(operation.clone());
|
||||
self.history.push_undo(operation.local_timestamp());
|
||||
self.snapshot.version.observe(operation.local_timestamp());
|
||||
self.end_transaction();
|
||||
edit
|
||||
operation
|
||||
}
|
||||
|
||||
fn apply_local_edit<S: ToOffset>(
|
||||
|
@ -815,6 +797,7 @@ impl Buffer {
|
|||
pub fn apply_ops<I: IntoIterator<Item = Operation>>(&mut self, ops: I) -> Result<()> {
|
||||
let mut deferred_ops = Vec::new();
|
||||
for op in ops {
|
||||
self.history.push(op.clone());
|
||||
if self.can_apply_op(&op) {
|
||||
self.apply_op(op)?;
|
||||
} else {
|
||||
|
@ -839,7 +822,6 @@ impl Buffer {
|
|||
);
|
||||
self.snapshot.version.observe(edit.timestamp.local());
|
||||
self.resolve_edit(edit.timestamp.local());
|
||||
self.history.push(edit);
|
||||
}
|
||||
}
|
||||
Operation::Undo {
|
||||
|
@ -1142,10 +1124,6 @@ impl Buffer {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn deferred_ops(&self) -> impl Iterator<Item = &Operation> {
|
||||
self.deferred_ops.iter()
|
||||
}
|
||||
|
||||
fn flush_deferred_ops(&mut self) -> Result<()> {
|
||||
self.deferred_replicas.clear();
|
||||
let mut deferred_ops = Vec::new();
|
||||
|
@ -1172,16 +1150,21 @@ impl Buffer {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn peek_undo_stack(&self) -> Option<&Transaction> {
|
||||
pub fn peek_undo_stack(&self) -> Option<&HistoryEntry> {
|
||||
self.history.undo_stack.last()
|
||||
}
|
||||
|
||||
pub fn peek_redo_stack(&self) -> Option<&HistoryEntry> {
|
||||
self.history.redo_stack.last()
|
||||
}
|
||||
|
||||
pub fn start_transaction(&mut self) -> Option<TransactionId> {
|
||||
self.start_transaction_at(Instant::now())
|
||||
}
|
||||
|
||||
pub fn start_transaction_at(&mut self, now: Instant) -> Option<TransactionId> {
|
||||
self.history.start_transaction(self.version.clone(), now)
|
||||
self.history
|
||||
.start_transaction(self.version.clone(), now, &mut self.local_clock)
|
||||
}
|
||||
|
||||
pub fn end_transaction(&mut self) -> Option<(TransactionId, clock::Global)> {
|
||||
|
@ -1189,8 +1172,8 @@ impl Buffer {
|
|||
}
|
||||
|
||||
pub fn end_transaction_at(&mut self, now: Instant) -> Option<(TransactionId, clock::Global)> {
|
||||
if let Some(transaction) = self.history.end_transaction(now) {
|
||||
let since = transaction.start.clone();
|
||||
if let Some(entry) = self.history.end_transaction(now) {
|
||||
let since = entry.transaction.start.clone();
|
||||
let id = self.history.group().unwrap();
|
||||
Some((id, since))
|
||||
} else {
|
||||
|
@ -1198,16 +1181,16 @@ impl Buffer {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn avoid_grouping_next_transaction(&mut self) {
|
||||
self.history.avoid_grouping_next_transaction()
|
||||
pub fn finalize_last_transaction(&mut self) -> Option<&Transaction> {
|
||||
self.history.finalize_last_transaction()
|
||||
}
|
||||
|
||||
pub fn base_text(&self) -> &Arc<str> {
|
||||
&self.history.base_text
|
||||
}
|
||||
|
||||
pub fn history(&self) -> impl Iterator<Item = &EditOperation> {
|
||||
self.history.ops.values()
|
||||
pub fn history(&self) -> impl Iterator<Item = &Operation> {
|
||||
self.history.operations.values()
|
||||
}
|
||||
|
||||
pub fn undo_history(&self) -> impl Iterator<Item = (&clock::Local, &[(clock::Local, u32)])> {
|
||||
|
@ -1218,7 +1201,8 @@ impl Buffer {
|
|||
}
|
||||
|
||||
pub fn undo(&mut self) -> Option<(TransactionId, Operation)> {
|
||||
if let Some(transaction) = self.history.pop_undo().cloned() {
|
||||
if let Some(entry) = self.history.pop_undo() {
|
||||
let transaction = entry.transaction.clone();
|
||||
let transaction_id = transaction.id;
|
||||
let op = self.undo_or_redo(transaction).unwrap();
|
||||
Some((transaction_id, op))
|
||||
|
@ -1227,13 +1211,18 @@ impl Buffer {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn undo_transaction(&mut self, transaction_id: TransactionId) -> Option<Operation> {
|
||||
if let Some(transaction) = self.history.remove_from_undo(transaction_id).cloned() {
|
||||
let op = self.undo_or_redo(transaction).unwrap();
|
||||
Some(op)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
pub fn undo_to_transaction(&mut self, transaction_id: TransactionId) -> Vec<Operation> {
|
||||
let transactions = self
|
||||
.history
|
||||
.remove_from_undo(transaction_id)
|
||||
.iter()
|
||||
.map(|entry| entry.transaction.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
transactions
|
||||
.into_iter()
|
||||
.map(|transaction| self.undo_or_redo(transaction).unwrap())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn forget_transaction(&mut self, transaction_id: TransactionId) {
|
||||
|
@ -1241,7 +1230,8 @@ impl Buffer {
|
|||
}
|
||||
|
||||
pub fn redo(&mut self) -> Option<(TransactionId, Operation)> {
|
||||
if let Some(transaction) = self.history.pop_redo().cloned() {
|
||||
if let Some(entry) = self.history.pop_redo() {
|
||||
let transaction = entry.transaction.clone();
|
||||
let transaction_id = transaction.id;
|
||||
let op = self.undo_or_redo(transaction).unwrap();
|
||||
Some((transaction_id, op))
|
||||
|
@ -1250,18 +1240,23 @@ impl Buffer {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn redo_transaction(&mut self, transaction_id: TransactionId) -> Option<Operation> {
|
||||
if let Some(transaction) = self.history.remove_from_redo(transaction_id).cloned() {
|
||||
let op = self.undo_or_redo(transaction).unwrap();
|
||||
Some(op)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
pub fn redo_to_transaction(&mut self, transaction_id: TransactionId) -> Vec<Operation> {
|
||||
let transactions = self
|
||||
.history
|
||||
.remove_from_redo(transaction_id)
|
||||
.iter()
|
||||
.map(|entry| entry.transaction.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
transactions
|
||||
.into_iter()
|
||||
.map(|transaction| self.undo_or_redo(transaction).unwrap())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn undo_or_redo(&mut self, transaction: Transaction) -> Result<Operation> {
|
||||
let mut counts = HashMap::default();
|
||||
for edit_id in transaction.edits {
|
||||
for edit_id in transaction.edit_ids {
|
||||
counts.insert(edit_id, self.undo_map.undo_count(edit_id) + 1);
|
||||
}
|
||||
|
||||
|
@ -1272,20 +1267,18 @@ impl Buffer {
|
|||
version: transaction.start.clone(),
|
||||
};
|
||||
self.apply_undo(&undo)?;
|
||||
self.snapshot.version.observe(undo.id);
|
||||
|
||||
Ok(Operation::Undo {
|
||||
let operation = Operation::Undo {
|
||||
undo,
|
||||
lamport_timestamp: self.lamport_clock.tick(),
|
||||
})
|
||||
};
|
||||
self.snapshot.version.observe(operation.local_timestamp());
|
||||
self.history.push(operation.clone());
|
||||
Ok(operation)
|
||||
}
|
||||
|
||||
pub fn push_transaction(
|
||||
&mut self,
|
||||
edit_ids: impl IntoIterator<Item = clock::Local>,
|
||||
now: Instant,
|
||||
) {
|
||||
self.history.push_transaction(edit_ids, now);
|
||||
pub fn push_transaction(&mut self, transaction: Transaction, now: Instant) {
|
||||
self.history.push_transaction(transaction, now);
|
||||
self.history.finalize_last_transaction();
|
||||
}
|
||||
|
||||
pub fn subscribe(&mut self) -> Subscription {
|
||||
|
@ -1294,13 +1287,13 @@ impl Buffer {
|
|||
|
||||
pub fn wait_for_edits(
|
||||
&mut self,
|
||||
edit_ids: &[clock::Local],
|
||||
edit_ids: impl IntoIterator<Item = clock::Local>,
|
||||
) -> impl 'static + Future<Output = ()> {
|
||||
let mut futures = Vec::new();
|
||||
for edit_id in edit_ids {
|
||||
if !self.version.observed(*edit_id) {
|
||||
if !self.version.observed(edit_id) {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
self.edit_id_resolvers.entry(*edit_id).or_default().push(tx);
|
||||
self.edit_id_resolvers.entry(edit_id).or_default().push(tx);
|
||||
futures.push(rx);
|
||||
}
|
||||
}
|
||||
|
@ -1404,7 +1397,7 @@ impl Buffer {
|
|||
new_text
|
||||
);
|
||||
let op = self.edit(old_ranges.iter().cloned(), new_text.as_str());
|
||||
(old_ranges, new_text, Operation::Edit(op))
|
||||
(old_ranges, new_text, op)
|
||||
}
|
||||
|
||||
pub fn randomly_undo_redo(&mut self, rng: &mut impl rand::Rng) -> Vec<Operation> {
|
||||
|
@ -1412,7 +1405,8 @@ impl Buffer {
|
|||
|
||||
let mut ops = Vec::new();
|
||||
for _ in 0..rng.gen_range(1..=5) {
|
||||
if let Some(transaction) = self.history.undo_stack.choose(rng).cloned() {
|
||||
if let Some(entry) = self.history.undo_stack.choose(rng) {
|
||||
let transaction = entry.transaction.clone();
|
||||
log::info!(
|
||||
"undoing buffer {} transaction {:?}",
|
||||
self.replica_id,
|
||||
|
@ -1512,6 +1506,10 @@ impl BufferSnapshot {
|
|||
self.visible_text.offset_to_point_utf16(offset)
|
||||
}
|
||||
|
||||
pub fn point_to_point_utf16(&self, point: Point) -> PointUtf16 {
|
||||
self.visible_text.point_to_point_utf16(point)
|
||||
}
|
||||
|
||||
pub fn version(&self) -> &clock::Global {
|
||||
&self.version
|
||||
}
|
||||
|
@ -1748,14 +1746,6 @@ impl BufferSnapshot {
|
|||
self.visible_text.clip_point_utf16(point, bias)
|
||||
}
|
||||
|
||||
// pub fn point_for_offset(&self, offset: usize) -> Result<Point> {
|
||||
// if offset <= self.len() {
|
||||
// Ok(self.text_summary_for_range(0..offset))
|
||||
// } else {
|
||||
// Err(anyhow!("offset out of bounds"))
|
||||
// }
|
||||
// }
|
||||
|
||||
pub fn edits_since<'a, D>(
|
||||
&'a self,
|
||||
since: &'a clock::Global,
|
||||
|
@ -1766,6 +1756,42 @@ impl BufferSnapshot {
|
|||
self.edits_since_in_range(since, Anchor::min()..Anchor::max())
|
||||
}
|
||||
|
||||
pub fn edited_ranges_for_transaction<'a, D>(
|
||||
&'a self,
|
||||
transaction: &'a Transaction,
|
||||
) -> impl 'a + Iterator<Item = Range<D>>
|
||||
where
|
||||
D: TextDimension,
|
||||
{
|
||||
let mut cursor = self.fragments.cursor::<(VersionedFullOffset, usize)>();
|
||||
let mut rope_cursor = self.visible_text.cursor(0);
|
||||
let cx = Some(transaction.end.clone());
|
||||
let mut position = D::default();
|
||||
transaction.ranges.iter().map(move |range| {
|
||||
cursor.seek_forward(&VersionedFullOffset::Offset(range.start), Bias::Right, &cx);
|
||||
let mut start_offset = cursor.start().1;
|
||||
if cursor
|
||||
.item()
|
||||
.map_or(false, |fragment| fragment.is_visible(&self.undo_map))
|
||||
{
|
||||
start_offset += range.start - cursor.start().0.full_offset()
|
||||
}
|
||||
position.add_assign(&rope_cursor.summary(start_offset));
|
||||
let start = position.clone();
|
||||
|
||||
cursor.seek_forward(&VersionedFullOffset::Offset(range.end), Bias::Left, &cx);
|
||||
let mut end_offset = cursor.start().1;
|
||||
if cursor
|
||||
.item()
|
||||
.map_or(false, |fragment| fragment.is_visible(&self.undo_map))
|
||||
{
|
||||
end_offset += range.end - cursor.start().0.full_offset();
|
||||
}
|
||||
position.add_assign(&rope_cursor.summary(end_offset));
|
||||
start..position.clone()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn edits_since_in_range<'a, D>(
|
||||
&'a self,
|
||||
since: &'a clock::Global,
|
||||
|
@ -2178,6 +2204,20 @@ impl Operation {
|
|||
operation_queue::Operation::lamport_timestamp(self).replica_id
|
||||
}
|
||||
|
||||
pub fn local_timestamp(&self) -> clock::Local {
|
||||
match self {
|
||||
Operation::Edit(edit) => edit.timestamp.local(),
|
||||
Operation::Undo { undo, .. } => undo.id,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_edit(&self) -> Option<&EditOperation> {
|
||||
match self {
|
||||
Operation::Edit(edit) => Some(edit),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_edit(&self) -> bool {
|
||||
match self {
|
||||
Operation::Edit { .. } => true,
|
||||
|
@ -2260,6 +2300,34 @@ impl ToPoint for Point {
|
|||
}
|
||||
}
|
||||
|
||||
pub trait ToPointUtf16 {
|
||||
fn to_point_utf16<'a>(&self, snapshot: &BufferSnapshot) -> PointUtf16;
|
||||
}
|
||||
|
||||
impl ToPointUtf16 for Anchor {
|
||||
fn to_point_utf16<'a>(&self, snapshot: &BufferSnapshot) -> PointUtf16 {
|
||||
snapshot.summary_for_anchor(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToPointUtf16 for usize {
|
||||
fn to_point_utf16<'a>(&self, snapshot: &BufferSnapshot) -> PointUtf16 {
|
||||
snapshot.offset_to_point_utf16(*self)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToPointUtf16 for PointUtf16 {
|
||||
fn to_point_utf16<'a>(&self, _: &BufferSnapshot) -> PointUtf16 {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
impl ToPointUtf16 for Point {
|
||||
fn to_point_utf16<'a>(&self, snapshot: &BufferSnapshot) -> PointUtf16 {
|
||||
snapshot.point_to_point_utf16(*self)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Clip {
|
||||
fn clip(&self, bias: Bias, snapshot: &BufferSnapshot) -> Self;
|
||||
}
|
||||
|
|
|
@ -293,6 +293,7 @@ pub struct EditorStyle {
|
|||
pub hint_diagnostic: DiagnosticStyle,
|
||||
pub invalid_hint_diagnostic: DiagnosticStyle,
|
||||
pub autocomplete: AutocompleteStyle,
|
||||
pub code_actions_indicator: Color,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
|
@ -420,6 +421,7 @@ impl InputEditorStyle {
|
|||
hint_diagnostic: default_diagnostic_style.clone(),
|
||||
invalid_hint_diagnostic: default_diagnostic_style.clone(),
|
||||
autocomplete: Default::default(),
|
||||
code_actions_indicator: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -221,7 +221,7 @@ impl Pane {
|
|||
let task = workspace.load_path(project_path, cx);
|
||||
cx.spawn(|workspace, mut cx| async move {
|
||||
let item = task.await;
|
||||
if let Some(pane) = cx.read(|cx| pane.upgrade(cx)) {
|
||||
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));
|
||||
|
@ -279,7 +279,7 @@ impl Pane {
|
|||
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_handle(cx).id(), item_view));
|
||||
.insert(item_idx, (item_view.item_id(cx), item_view));
|
||||
self.activate_item(item_idx, cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
|
|
@ -150,11 +150,9 @@ pub trait Item: Entity + Sized {
|
|||
}
|
||||
|
||||
pub trait ItemView: View {
|
||||
type ItemHandle: ItemHandle;
|
||||
|
||||
fn deactivated(&mut self, _: &mut ViewContext<Self>) {}
|
||||
fn navigate(&mut self, _: Box<dyn Any>, _: &mut ViewContext<Self>) {}
|
||||
fn item_handle(&self, cx: &AppContext) -> Self::ItemHandle;
|
||||
fn item_id(&self, cx: &AppContext) -> usize;
|
||||
fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox;
|
||||
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
|
||||
fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
|
||||
|
@ -170,7 +168,11 @@ pub trait ItemView: View {
|
|||
false
|
||||
}
|
||||
fn can_save(&self, cx: &AppContext) -> bool;
|
||||
fn save(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>>;
|
||||
fn save(
|
||||
&mut self,
|
||||
project: ModelHandle<Project>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>>;
|
||||
fn can_save_as(&self, cx: &AppContext) -> bool;
|
||||
fn save_as(
|
||||
&mut self,
|
||||
|
@ -222,7 +224,7 @@ pub trait WeakItemHandle {
|
|||
}
|
||||
|
||||
pub trait ItemViewHandle: 'static {
|
||||
fn item_handle(&self, cx: &AppContext) -> Box<dyn ItemHandle>;
|
||||
fn item_id(&self, cx: &AppContext) -> usize;
|
||||
fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox;
|
||||
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
|
||||
fn boxed_clone(&self) -> Box<dyn ItemViewHandle>;
|
||||
|
@ -236,7 +238,7 @@ pub trait ItemViewHandle: 'static {
|
|||
fn has_conflict(&self, cx: &AppContext) -> bool;
|
||||
fn can_save(&self, cx: &AppContext) -> bool;
|
||||
fn can_save_as(&self, cx: &AppContext) -> bool;
|
||||
fn save(&self, cx: &mut MutableAppContext) -> Task<Result<()>>;
|
||||
fn save(&self, project: ModelHandle<Project>, cx: &mut MutableAppContext) -> Task<Result<()>>;
|
||||
fn save_as(
|
||||
&self,
|
||||
project: ModelHandle<Project>,
|
||||
|
@ -324,7 +326,7 @@ impl<T: Item> WeakItemHandle for WeakModelHandle<T> {
|
|||
}
|
||||
|
||||
fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
|
||||
WeakModelHandle::<T>::upgrade(*self, cx).map(|i| Box::new(i) as Box<dyn ItemHandle>)
|
||||
WeakModelHandle::<T>::upgrade(self, cx).map(|i| Box::new(i) as Box<dyn ItemHandle>)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -354,8 +356,8 @@ impl dyn ItemViewHandle {
|
|||
}
|
||||
|
||||
impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
|
||||
fn item_handle(&self, cx: &AppContext) -> Box<dyn ItemHandle> {
|
||||
Box::new(self.read(cx).item_handle(cx))
|
||||
fn item_id(&self, cx: &AppContext) -> usize {
|
||||
self.read(cx).item_id(cx)
|
||||
}
|
||||
|
||||
fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox {
|
||||
|
@ -404,8 +406,8 @@ impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
|
|||
self.update(cx, |this, cx| this.navigate(data, cx));
|
||||
}
|
||||
|
||||
fn save(&self, cx: &mut MutableAppContext) -> Task<Result<()>> {
|
||||
self.update(cx, |item, cx| item.save(cx))
|
||||
fn save(&self, project: ModelHandle<Project>, cx: &mut MutableAppContext) -> Task<Result<()>> {
|
||||
self.update(cx, |item, cx| item.save(project, cx))
|
||||
}
|
||||
|
||||
fn save_as(
|
||||
|
@ -589,7 +591,7 @@ impl Workspace {
|
|||
|
||||
while stream.recv().await.is_some() {
|
||||
cx.update(|cx| {
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
if let Some(this) = this.upgrade(cx) {
|
||||
this.update(cx, |_, cx| cx.notify());
|
||||
}
|
||||
})
|
||||
|
@ -772,7 +774,7 @@ impl Workspace {
|
|||
let item = load_task.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let pane = pane
|
||||
.upgrade(&cx)
|
||||
.upgrade(cx)
|
||||
.ok_or_else(|| anyhow!("could not upgrade pane reference"))?;
|
||||
Ok(this.open_item_in_pane(item, &pane, cx))
|
||||
})
|
||||
|
@ -822,6 +824,7 @@ impl Workspace {
|
|||
}
|
||||
|
||||
pub fn save_active_item(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
|
||||
let project = self.project.clone();
|
||||
if let Some(item) = self.active_item(cx) {
|
||||
if item.can_save(cx) {
|
||||
if item.has_conflict(cx.as_ref()) {
|
||||
|
@ -835,12 +838,12 @@ impl Workspace {
|
|||
cx.spawn(|_, mut cx| async move {
|
||||
let answer = answer.recv().await;
|
||||
if answer == Some(0) {
|
||||
cx.update(|cx| item.save(cx)).await?;
|
||||
cx.update(|cx| item.save(project, cx)).await?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
} else {
|
||||
item.save(cx)
|
||||
item.save(project, cx)
|
||||
}
|
||||
} else if item.can_save_as(cx) {
|
||||
let worktree = self.worktrees(cx).next();
|
||||
|
@ -849,9 +852,8 @@ impl Workspace {
|
|||
.map_or(Path::new(""), |w| w.abs_path())
|
||||
.to_path_buf();
|
||||
let mut abs_path = cx.prompt_for_new_path(&start_abs_path);
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
if let Some(abs_path) = abs_path.recv().await.flatten() {
|
||||
let project = this.read_with(&cx, |this, _| this.project().clone());
|
||||
cx.update(|cx| item.save_as(project, abs_path, cx)).await?;
|
||||
}
|
||||
Ok(())
|
||||
|
|
3
crates/zed/assets/icons/zap.svg
Normal file
3
crates/zed/assets/icons/zap.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="8" height="12" viewBox="0 0 8 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.00262 12L2.89358 7.9886C2.95207 7.71862 2.77658 7.49963 2.5021 7.49963H0.000671387L6.00037 0L5.10792 4.0108C5.04792 4.27929 5.22341 4.49828 5.4994 4.49828H7.99932L1.99962 11.9979L2.00262 12Z" fill="#FDE047"/>
|
||||
</svg>
|
After Width: | Height: | Size: 322 B |
|
@ -253,6 +253,7 @@ line_number_active = "$text.0.color"
|
|||
selection = "$selection.host"
|
||||
guest_selections = "$selection.guests"
|
||||
error_color = "$status.bad"
|
||||
code_actions_indicator = "$text.3.color"
|
||||
|
||||
[editor.diagnostic_path_header]
|
||||
background = "$state.active_line"
|
||||
|
|
|
@ -126,7 +126,7 @@ mod tests {
|
|||
use super::*;
|
||||
use editor::{DisplayPoint, Editor};
|
||||
use gpui::{MutableAppContext, TestAppContext, ViewHandle};
|
||||
use project::ProjectPath;
|
||||
use project::{Fs, ProjectPath};
|
||||
use serde_json::json;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
|
@ -817,7 +817,10 @@ mod tests {
|
|||
.active_pane()
|
||||
.update(cx, |pane, cx| pane.close_item(editor2.id(), cx));
|
||||
drop(editor2);
|
||||
app_state.fs.as_fake().remove(Path::new("/root/a/file2"))
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.remove_file(Path::new("/root/a/file2"), Default::default())
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
|
16
script/drop-test-dbs
Executable file
16
script/drop-test-dbs
Executable file
|
@ -0,0 +1,16 @@
|
|||
#!/bin/bash
|
||||
|
||||
databases=$(psql --tuples-only --command "
|
||||
SELECT
|
||||
datname
|
||||
FROM
|
||||
pg_database
|
||||
WHERE
|
||||
datistemplate = false
|
||||
AND datname like 'zed-test-%'
|
||||
")
|
||||
|
||||
for database in $databases; do
|
||||
echo $database
|
||||
dropdb $database
|
||||
done
|
Loading…
Add table
Add a link
Reference in a new issue