Merge pull request #446 from zed-industries/assists

Implement code actions
This commit is contained in:
Antonio Scandurra 2022-02-14 16:09:20 +01:00 committed by GitHub
commit f742f63007
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 6257 additions and 3410 deletions

4
Cargo.lock generated
View file

@ -2776,6 +2776,8 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-pipe", "async-pipe",
"ctor",
"env_logger",
"futures", "futures",
"gpui", "gpui",
"log", "log",
@ -2784,7 +2786,6 @@ dependencies = [
"postage", "postage",
"serde", "serde",
"serde_json", "serde_json",
"simplelog",
"smol", "smol",
"unindent", "unindent",
"util", "util",
@ -3523,7 +3524,6 @@ dependencies = [
"rpc", "rpc",
"serde", "serde",
"serde_json", "serde_json",
"simplelog",
"smol", "smol",
"sum_tree", "sum_tree",
"tempdir", "tempdir",

View file

@ -184,7 +184,8 @@ impl Channel {
rpc: Arc<Client>, rpc: Arc<Client>,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> 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(); let user_store = user_store.clone();
@ -398,29 +399,23 @@ impl Channel {
cursor cursor
} }
fn handle_message_sent( async fn handle_message_sent(
&mut self, this: ModelHandle<Self>,
message: TypedEnvelope<proto::ChannelMessageSent>, message: TypedEnvelope<proto::ChannelMessageSent>,
_: Arc<Client>, _: Arc<Client>,
cx: &mut ModelContext<Self>, mut cx: AsyncAppContext,
) -> Result<()> { ) -> Result<()> {
let user_store = self.user_store.clone(); let user_store = this.read_with(&cx, |this, _| this.user_store.clone());
let message = message let message = message
.payload .payload
.message .message
.ok_or_else(|| anyhow!("empty message"))?; .ok_or_else(|| anyhow!("empty message"))?;
cx.spawn(|this, mut cx| { let message = ChannelMessage::from_proto(message, &user_store, &mut cx).await?;
async move { this.update(&mut cx, |this, cx| {
let message = ChannelMessage::from_proto(message, &user_store, &mut cx).await?; this.insert_messages(SumTree::from_item(message, &()), cx)
this.update(&mut cx, |this, cx| { });
this.insert_messages(SumTree::from_item(message, &()), cx)
});
Ok(())
}
.log_err()
})
.detach();
Ok(()) Ok(())
} }

View file

@ -11,8 +11,8 @@ use async_tungstenite::tungstenite::{
error::Error as WebsocketError, error::Error as WebsocketError,
http::{Request, StatusCode}, http::{Request, StatusCode},
}; };
use futures::StreamExt; use futures::{future::LocalBoxFuture, FutureExt, StreamExt};
use gpui::{action, AsyncAppContext, Entity, ModelContext, MutableAppContext, Task}; use gpui::{action, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task};
use http::HttpClient; use http::HttpClient;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use parking_lot::RwLock; use parking_lot::RwLock;
@ -20,10 +20,11 @@ use postage::watch;
use rand::prelude::*; use rand::prelude::*;
use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, RequestMessage}; use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, RequestMessage};
use std::{ use std::{
any::TypeId, any::{type_name, TypeId},
collections::HashMap, collections::HashMap,
convert::TryFrom, convert::TryFrom,
fmt::Write as _, fmt::Write as _,
future::Future,
sync::{ sync::{
atomic::{AtomicUsize, Ordering}, atomic::{AtomicUsize, Ordering},
Arc, Weak, Arc, Weak,
@ -123,14 +124,17 @@ pub enum Status {
ReconnectionError { next_reconnection: Instant }, ReconnectionError { next_reconnection: Instant },
} }
type ModelHandler = Box<
dyn Send
+ Sync
+ FnMut(Box<dyn AnyTypedEnvelope>, &AsyncAppContext) -> LocalBoxFuture<'static, Result<()>>,
>;
struct ClientState { struct ClientState {
credentials: Option<Credentials>, credentials: Option<Credentials>,
status: (watch::Sender<Status>, watch::Receiver<Status>), status: (watch::Sender<Status>, watch::Receiver<Status>),
entity_id_extractors: HashMap<TypeId, Box<dyn Send + Sync + Fn(&dyn AnyTypedEnvelope) -> u64>>, entity_id_extractors: HashMap<TypeId, Box<dyn Send + Sync + Fn(&dyn AnyTypedEnvelope) -> u64>>,
model_handlers: HashMap< model_handlers: HashMap<(TypeId, Option<u64>), Option<ModelHandler>>,
(TypeId, Option<u64>),
Option<Box<dyn Send + Sync + FnMut(Box<dyn AnyTypedEnvelope>, &mut AsyncAppContext)>>,
>,
_maintain_connection: Option<Task<()>>, _maintain_connection: Option<Task<()>>,
heartbeat_interval: Duration, 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>, self: &Arc<Self>,
cx: &mut ModelContext<M>, cx: &mut ModelContext<M>,
mut handler: F, mut handler: F,
@ -273,7 +277,8 @@ impl Client {
F: 'static F: 'static
+ Send + Send
+ Sync + 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 subscription_id = (TypeId::of::<T>(), None);
let client = self.clone(); let client = self.clone();
@ -284,11 +289,15 @@ impl Client {
Some(Box::new(move |envelope, cx| { Some(Box::new(move |envelope, cx| {
if let Some(model) = model.upgrade(cx) { if let Some(model) = model.upgrade(cx) {
let envelope = envelope.into_any().downcast::<TypedEnvelope<T>>().unwrap(); let envelope = envelope.into_any().downcast::<TypedEnvelope<T>>().unwrap();
model.update(cx, |model, cx| { handler(model, *envelope, client.clone(), cx.clone()).boxed_local()
if let Err(error) = handler(model, *envelope, client.clone(), cx) { } else {
log::error!("error handling message: {}", error) 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>, self: &Arc<Self>,
remote_id: u64, remote_id: u64,
cx: &mut ModelContext<M>, cx: &mut ModelContext<M>,
@ -314,7 +323,8 @@ impl Client {
F: 'static F: 'static
+ Send + Send
+ Sync + 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 subscription_id = (TypeId::of::<T>(), Some(remote_id));
let client = self.clone(); let client = self.clone();
@ -337,11 +347,15 @@ impl Client {
Some(Box::new(move |envelope, cx| { Some(Box::new(move |envelope, cx| {
if let Some(model) = model.upgrade(cx) { if let Some(model) = model.upgrade(cx) {
let envelope = envelope.into_any().downcast::<TypedEnvelope<T>>().unwrap(); let envelope = envelope.into_any().downcast::<TypedEnvelope<T>>().unwrap();
model.update(cx, |model, cx| { handler(model, *envelope, client.clone(), cx.clone()).boxed_local()
if let Err(error) = handler(model, *envelope, client.clone(), cx) { } else {
log::error!("error handling message: {}", error) 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 { pub fn has_keychain_credentials(&self, cx: &AsyncAppContext) -> bool {
read_credentials_from_keychain(cx).is_some() 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; let (connection_id, handle_io, mut incoming) = self.peer.add_connection(conn).await;
cx.foreground() cx.foreground()
.spawn({ .spawn({
let mut cx = cx.clone(); let cx = cx.clone();
let this = self.clone(); let this = self.clone();
async move { async move {
while let Some(message) = incoming.next().await { while let Some(message) = incoming.next().await {
@ -462,23 +514,41 @@ impl Client {
if let Some(handler) = state.model_handlers.get_mut(&handler_key) { if let Some(handler) = state.model_handlers.get_mut(&handler_key) {
let mut handler = handler.take().unwrap(); let mut handler = handler.take().unwrap();
drop(state); // Avoid deadlocks if the handler interacts with rpc::Client drop(state); // Avoid deadlocks if the handler interacts with rpc::Client
let future = (handler)(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!( log::debug!(
"rpc message received. client_id:{}, name:{}", "rpc message received. client_id:{}, name:{}",
this.id, client_id,
type_name type_name
); );
(handler)(message, &mut cx); cx.foreground()
log::debug!( .spawn(async move {
"rpc message handled. client_id:{}, name:{}", match future.await {
this.id, Ok(()) => {
type_name log::debug!(
); "rpc message handled. client_id:{}, name:{}",
client_id,
let mut state = this.state.write(); type_name
if state.model_handlers.contains_key(&handler_key) { );
state.model_handlers.insert(handler_key, Some(handler)); }
} Err(error) => {
log::error!(
"error handling rpc message. client_id:{}, name:{}, error:{}",
client_id,
type_name,
error
);
}
}
})
.detach();
} else { } else {
log::info!("unhandled message {}", type_name); log::info!("unhandled message {}", type_name);
} }
@ -715,16 +785,12 @@ impl Client {
response response
} }
pub fn respond<T: RequestMessage>( fn respond<T: RequestMessage>(&self, receipt: Receipt<T>, response: T::Response) -> Result<()> {
&self,
receipt: Receipt<T>,
response: T::Response,
) -> Result<()> {
log::debug!("rpc respond. client_id: {}. name:{}", self.id, T::NAME); log::debug!("rpc respond. client_id: {}. name:{}", self.id, T::NAME);
self.peer.respond(receipt, response) self.peer.respond(receipt, response)
} }
pub fn respond_with_error<T: RequestMessage>( fn respond_with_error<T: RequestMessage>(
&self, &self,
receipt: Receipt<T>, receipt: Receipt<T>,
error: proto::Error, error: proto::Error,
@ -861,22 +927,22 @@ mod tests {
let (mut done_tx1, mut done_rx1) = postage::oneshot::channel(); let (mut done_tx1, mut done_rx1) = postage::oneshot::channel();
let (mut done_tx2, mut done_rx2) = postage::oneshot::channel(); let (mut done_tx2, mut done_rx2) = postage::oneshot::channel();
let _subscription1 = model.update(&mut cx, |_, cx| { let _subscription1 = model.update(&mut cx, |_, cx| {
client.subscribe_to_entity( client.add_entity_message_handler(
1, 1,
cx, cx,
move |_, _: TypedEnvelope<proto::UnshareProject>, _, _| { move |_, _: TypedEnvelope<proto::UnshareProject>, _, _| {
postage::sink::Sink::try_send(&mut done_tx1, ()).unwrap(); postage::sink::Sink::try_send(&mut done_tx1, ()).unwrap();
Ok(()) async { Ok(()) }
}, },
) )
}); });
let _subscription2 = model.update(&mut cx, |_, cx| { let _subscription2 = model.update(&mut cx, |_, cx| {
client.subscribe_to_entity( client.add_entity_message_handler(
2, 2,
cx, cx,
move |_, _: TypedEnvelope<proto::UnshareProject>, _, _| { move |_, _: TypedEnvelope<proto::UnshareProject>, _, _| {
postage::sink::Sink::try_send(&mut done_tx2, ()).unwrap(); 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 // Ensure dropping a subscription for the same entity type still allows receiving of
// messages for other entity IDs of the same type. // messages for other entity IDs of the same type.
let subscription3 = model.update(&mut cx, |_, cx| { let subscription3 = model.update(&mut cx, |_, cx| {
client.subscribe_to_entity( client.add_entity_message_handler(
3, 3,
cx, cx,
move |_, _: TypedEnvelope<proto::UnshareProject>, _, _| Ok(()), |_, _: TypedEnvelope<proto::UnshareProject>, _, _| async { Ok(()) },
) )
}); });
drop(subscription3); drop(subscription3);
@ -910,16 +976,16 @@ mod tests {
let (mut done_tx1, _done_rx1) = postage::oneshot::channel(); let (mut done_tx1, _done_rx1) = postage::oneshot::channel();
let (mut done_tx2, mut done_rx2) = postage::oneshot::channel(); let (mut done_tx2, mut done_rx2) = postage::oneshot::channel();
let subscription1 = model.update(&mut cx, |_, cx| { 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(); postage::sink::Sink::try_send(&mut done_tx1, ()).unwrap();
Ok(()) async { Ok(()) }
}) })
}); });
drop(subscription1); drop(subscription1);
let _subscription2 = model.update(&mut cx, |_, cx| { 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(); postage::sink::Sink::try_send(&mut done_tx2, ()).unwrap();
Ok(()) async { Ok(()) }
}) })
}); });
server.send(proto::Ping {}); server.send(proto::Ping {});
@ -937,12 +1003,12 @@ mod tests {
let model = cx.add_model(|_| Model { subscription: None }); let model = cx.add_model(|_| Model { subscription: None });
let (mut done_tx, mut done_rx) = postage::oneshot::channel(); let (mut done_tx, mut done_rx) = postage::oneshot::channel();
model.update(&mut cx, |model, cx| { model.update(&mut cx, |model, cx| {
model.subscription = Some(client.subscribe( model.subscription = Some(client.add_message_handler(
cx, cx,
move |model, _: TypedEnvelope<proto::Ping>, _, _| { move |model, _: TypedEnvelope<proto::Ping>, _, mut cx| {
model.subscription.take(); model.update(&mut cx, |model, _| model.subscription.take());
postage::sink::Sink::try_send(&mut done_tx, ()).unwrap(); postage::sink::Sink::try_send(&mut done_tx, ()).unwrap();
Ok(()) async { Ok(()) }
}, },
)); ));
}); });

View file

@ -58,11 +58,11 @@ impl UserStore {
let (mut current_user_tx, current_user_rx) = watch::channel(); let (mut current_user_tx, current_user_rx) = watch::channel();
let (mut update_contacts_tx, mut update_contacts_rx) = let (mut update_contacts_tx, mut update_contacts_rx) =
watch::channel::<Option<proto::UpdateContacts>>(); watch::channel::<Option<proto::UpdateContacts>>();
let update_contacts_subscription = client.subscribe( let update_contacts_subscription = client.add_message_handler(
cx, cx,
move |_: &mut Self, msg: TypedEnvelope<proto::UpdateContacts>, _, _| { move |_: ModelHandle<Self>, msg: TypedEnvelope<proto::UpdateContacts>, _, _| {
let _ = update_contacts_tx.blocking_send(Some(msg.payload)); *update_contacts_tx.borrow_mut() = Some(msg.payload);
Ok(()) async move { Ok(()) }
}, },
); );
Self { Self {

View file

@ -7,7 +7,7 @@ use editor::{
display_map::{BlockDisposition, BlockId, BlockProperties, RenderBlock}, display_map::{BlockDisposition, BlockId, BlockProperties, RenderBlock},
highlight_diagnostic_message, highlight_diagnostic_message,
items::BufferItemHandle, items::BufferItemHandle,
Autoscroll, BuildSettings, Editor, ExcerptId, ExcerptProperties, MultiBuffer, ToOffset, Autoscroll, BuildSettings, Editor, ExcerptId, MultiBuffer, ToOffset,
}; };
use gpui::{ use gpui::{
action, elements::*, fonts::TextStyle, keymap::Binding, AnyViewHandle, AppContext, Entity, action, elements::*, fonts::TextStyle, keymap::Binding, AnyViewHandle, AppContext, Entity,
@ -28,7 +28,7 @@ use std::{
sync::Arc, sync::Arc,
}; };
use util::TryFutureExt; use util::TryFutureExt;
use workspace::{ItemNavHistory, Workspace}; use workspace::{ItemNavHistory, ItemViewHandle as _, Workspace};
action!(Deploy); action!(Deploy);
action!(OpenExcerpts); action!(OpenExcerpts);
@ -68,7 +68,6 @@ struct ProjectDiagnosticsEditor {
struct PathState { struct PathState {
path: ProjectPath, path: ProjectPath,
header: Option<BlockId>,
diagnostic_groups: Vec<DiagnosticGroupState>, diagnostic_groups: Vec<DiagnosticGroupState>,
} }
@ -145,7 +144,12 @@ impl ProjectDiagnosticsEditor {
let excerpts = cx.add_model(|cx| MultiBuffer::new(project.read(cx).replica_id())); let excerpts = cx.add_model(|cx| MultiBuffer::new(project.read(cx).replica_id()));
let build_settings = editor::settings_builder(excerpts.downgrade(), settings.clone()); let build_settings = editor::settings_builder(excerpts.downgrade(), settings.clone());
let editor = cx.add_view(|cx| { 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.set_vertical_scroll_margin(5, cx);
editor editor
}); });
@ -187,7 +191,7 @@ impl ProjectDiagnosticsEditor {
for selection in editor.local_selections::<usize>(cx) { for selection in editor.local_selections::<usize>(cx) {
for (buffer, mut range) in 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 { if selection.reversed {
mem::swap(&mut range.start, &mut range.end); mem::swap(&mut range.start, &mut range.end);
@ -253,7 +257,6 @@ impl ProjectDiagnosticsEditor {
ix, ix,
PathState { PathState {
path: path.clone(), path: path.clone(),
header: None,
diagnostic_groups: Default::default(), diagnostic_groups: Default::default(),
}, },
); );
@ -330,14 +333,15 @@ impl ProjectDiagnosticsEditor {
Point::new(range.end.row + CONTEXT_LINE_COUNT, u32::MAX), Point::new(range.end.row + CONTEXT_LINE_COUNT, u32::MAX),
Bias::Left, Bias::Left,
); );
let excerpt_id = excerpts.insert_excerpt_after( let excerpt_id = excerpts
&prev_excerpt_id, .insert_excerpts_after(
ExcerptProperties { &prev_excerpt_id,
buffer: &buffer, buffer.clone(),
range: excerpt_start..excerpt_end, [excerpt_start..excerpt_end],
}, excerpts_cx,
excerpts_cx, )
); .pop()
.unwrap();
prev_excerpt_id = excerpt_id.clone(); prev_excerpt_id = excerpt_id.clone();
first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone()); first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
@ -360,14 +364,6 @@ impl ProjectDiagnosticsEditor {
), ),
disposition: BlockDisposition::Above, 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] { for entry in &group.entries[*start_ix..ix] {
@ -416,27 +412,17 @@ impl ProjectDiagnosticsEditor {
}); });
self.editor.update(cx, |editor, cx| { self.editor.update(cx, |editor, cx| {
blocks_to_remove.extend(path_state.header);
editor.remove_blocks(blocks_to_remove, cx); 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( let block_ids = editor.insert_blocks(
blocks_to_add blocks_to_add.into_iter().map(|block| {
.into_iter() let (excerpt_id, text_anchor) = block.position;
.map(|block| { BlockProperties {
let (excerpt_id, text_anchor) = block.position; position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor),
BlockProperties { height: block.height,
position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor), render: block.render,
height: block.height, disposition: block.disposition,
render: block.render, }
disposition: block.disposition, }),
}
})
.chain(header_block.into_iter()),
cx, cx,
); );
@ -444,7 +430,6 @@ impl ProjectDiagnosticsEditor {
for group_state in &mut groups_to_add { for group_state in &mut groups_to_add {
group_state.blocks = block_ids.by_ref().take(group_state.block_count).collect(); 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() { for ix in group_ixs_to_remove.into_iter().rev() {
@ -554,10 +539,8 @@ impl workspace::Item for ProjectDiagnostics {
} }
impl workspace::ItemView for ProjectDiagnosticsEditor { impl workspace::ItemView for ProjectDiagnosticsEditor {
type ItemHandle = ModelHandle<ProjectDiagnostics>; fn item_id(&self, _: &AppContext) -> usize {
self.model.id()
fn item_handle(&self, _: &AppContext) -> Self::ItemHandle {
self.model.clone()
} }
fn tab_content(&self, style: &theme::Tab, _: &AppContext) -> ElementBox { fn tab_content(&self, style: &theme::Tab, _: &AppContext) -> ElementBox {
@ -589,8 +572,12 @@ impl workspace::ItemView for ProjectDiagnosticsEditor {
true true
} }
fn save(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>> { fn save(
self.excerpts.update(cx, |excerpts, cx| excerpts.save(cx)) &mut self,
project: ModelHandle<Project>,
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
self.editor.save(project, cx)
} }
fn can_save_as(&self, _: &AppContext) -> bool { 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( fn diagnostic_header_renderer(
diagnostic: Diagnostic, diagnostic: Diagnostic,
build_settings: BuildSettings, 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( pub(crate) fn render_summary(
summary: &DiagnosticSummary, summary: &DiagnosticSummary,
text_style: &TextStyle, text_style: &TextStyle,
@ -838,7 +769,10 @@ fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use editor::{display_map::BlockContext, DisplayPoint, EditorSnapshot}; use editor::{
display_map::{BlockContext, TransformBlock},
DisplayPoint, EditorSnapshot,
};
use gpui::TestAppContext; use gpui::TestAppContext;
use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16}; use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16};
use serde_json::json; use serde_json::json;
@ -985,8 +919,9 @@ mod tests {
[ [
(0, "path header block".into()), (0, "path header block".into()),
(2, "diagnostic header".into()), (2, "diagnostic header".into()),
(15, "diagnostic header".into()), (15, "collapsed context".into()),
(24, "collapsed context".into()), (16, "diagnostic header".into()),
(25, "collapsed context".into()),
] ]
); );
assert_eq!( assert_eq!(
@ -1011,6 +946,7 @@ mod tests {
" c(y);\n", " c(y);\n",
"\n", // supporting diagnostic "\n", // supporting diagnostic
" d(x);\n", " d(x);\n",
"\n", // context ellipsis
// diagnostic group 2 // diagnostic group 2
"\n", // primary message "\n", // primary message
"\n", // padding "\n", // padding
@ -1073,8 +1009,9 @@ mod tests {
(2, "diagnostic header".into()), (2, "diagnostic header".into()),
(7, "path header block".into()), (7, "path header block".into()),
(9, "diagnostic header".into()), (9, "diagnostic header".into()),
(22, "diagnostic header".into()), (22, "collapsed context".into()),
(31, "collapsed context".into()), (23, "diagnostic header".into()),
(32, "collapsed context".into()),
] ]
); );
assert_eq!( assert_eq!(
@ -1110,6 +1047,7 @@ mod tests {
" c(y);\n", " c(y);\n",
"\n", // supporting diagnostic "\n", // supporting diagnostic
" d(x);\n", " d(x);\n",
"\n", // collapsed context
// diagnostic group 2 // diagnostic group 2
"\n", // primary message "\n", // primary message
"\n", // filename "\n", // filename
@ -1184,11 +1122,13 @@ mod tests {
[ [
(0, "path header block".into()), (0, "path header block".into()),
(2, "diagnostic header".into()), (2, "diagnostic header".into()),
(7, "diagnostic header".into()), (7, "collapsed context".into()),
(12, "path header block".into()), (8, "diagnostic header".into()),
(14, "diagnostic header".into()), (13, "path header block".into()),
(27, "diagnostic header".into()), (15, "diagnostic header".into()),
(36, "collapsed context".into()), (28, "collapsed context".into()),
(29, "diagnostic header".into()),
(38, "collapsed context".into()),
] ]
); );
assert_eq!( assert_eq!(
@ -1205,6 +1145,7 @@ mod tests {
"const a: i32 = 'a';\n", "const a: i32 = 'a';\n",
"\n", // supporting diagnostic "\n", // supporting diagnostic
"const b: i32 = c;\n", "const b: i32 = c;\n",
"\n", // context ellipsis
// diagnostic group 2 // diagnostic group 2
"\n", // primary message "\n", // primary message
"\n", // padding "\n", // padding
@ -1230,6 +1171,7 @@ mod tests {
" c(y);\n", " c(y);\n",
"\n", // supporting diagnostic "\n", // supporting diagnostic
" d(x);\n", " d(x);\n",
"\n", // context ellipsis
// diagnostic group 2 // diagnostic group 2
"\n", // primary message "\n", // primary message
"\n", // filename "\n", // filename
@ -1254,18 +1196,31 @@ mod tests {
editor editor
.blocks_in_range(0..editor.max_point().row()) .blocks_in_range(0..editor.max_point().row())
.filter_map(|(row, block)| { .filter_map(|(row, block)| {
block let name = match block {
.render(&BlockContext { TransformBlock::Custom(block) => block
cx, .render(&BlockContext {
anchor_x: 0., cx,
scroll_x: 0., anchor_x: 0.,
gutter_padding: 0., scroll_x: 0.,
gutter_width: 0., gutter_padding: 0.,
line_height: 0., gutter_width: 0.,
em_width: 0., line_height: 0.,
}) em_width: 0.,
.name() })
.map(|s| (row, s.to_string())) .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() .collect()
} }

View file

@ -12,6 +12,7 @@ test-support = [
"text/test-support", "text/test-support",
"language/test-support", "language/test-support",
"gpui/test-support", "gpui/test-support",
"project/test-support",
"util/test-support", "util/test-support",
] ]
@ -48,6 +49,7 @@ language = { path = "../language", features = ["test-support"] }
lsp = { path = "../lsp", features = ["test-support"] } lsp = { path = "../lsp", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] } util = { path = "../util", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
ctor = "0.1" ctor = "0.1"
env_logger = "0.8" env_logger = "0.8"
rand = "0.8" rand = "0.8"

View file

@ -15,8 +15,8 @@ use tab_map::TabMap;
use wrap_map::WrapMap; use wrap_map::WrapMap;
pub use block_map::{ pub use block_map::{
AlignedBlock, BlockBufferRows as DisplayBufferRows, BlockChunks as DisplayChunks, BlockContext, BlockBufferRows as DisplayBufferRows, BlockChunks as DisplayChunks, BlockContext,
BlockDisposition, BlockId, BlockProperties, RenderBlock, BlockDisposition, BlockId, BlockProperties, RenderBlock, TransformBlock,
}; };
pub trait ToDisplayPoint { pub trait ToDisplayPoint {
@ -43,13 +43,15 @@ impl DisplayMap {
font_id: FontId, font_id: FontId,
font_size: f32, font_size: f32,
wrap_width: Option<f32>, wrap_width: Option<f32>,
buffer_header_height: u8,
excerpt_header_height: u8,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Self { ) -> Self {
let buffer_subscription = buffer.update(cx, |buffer, _| buffer.subscribe()); let buffer_subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
let (fold_map, snapshot) = FoldMap::new(buffer.read(cx).snapshot(cx)); let (fold_map, snapshot) = FoldMap::new(buffer.read(cx).snapshot(cx));
let (tab_map, snapshot) = TabMap::new(snapshot, tab_size); let (tab_map, snapshot) = TabMap::new(snapshot, tab_size);
let (wrap_map, snapshot) = WrapMap::new(snapshot, font_id, font_size, wrap_width, cx); 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(); cx.observe(&wrap_map, |_, _, cx| cx.notify()).detach();
DisplayMap { DisplayMap {
buffer, buffer,
@ -318,7 +320,7 @@ impl DisplaySnapshot {
pub fn blocks_in_range<'a>( pub fn blocks_in_range<'a>(
&'a self, &'a self,
rows: Range<u32>, rows: Range<u32>,
) -> impl Iterator<Item = (u32, &'a AlignedBlock)> { ) -> impl Iterator<Item = (u32, &'a TransformBlock)> {
self.blocks_snapshot.blocks_in_range(rows) self.blocks_snapshot.blocks_in_range(rows)
} }
@ -471,6 +473,8 @@ mod tests {
let font_cache = cx.font_cache().clone(); let font_cache = cx.font_cache().clone();
let tab_size = rng.gen_range(1..=4); 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 family_id = font_cache.load_family(&["Helvetica"]).unwrap();
let font_id = font_cache let font_id = font_cache
.select_font(family_id, &Default::default()) .select_font(family_id, &Default::default())
@ -497,7 +501,16 @@ mod tests {
}); });
let map = cx.add_model(|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,
buffer_start_excerpt_header_height,
excerpt_header_height,
cx,
)
}); });
let mut notifications = observe(&map, &mut cx); let mut notifications = observe(&map, &mut cx);
let mut fold_count = 0; let mut fold_count = 0;
@ -711,7 +724,16 @@ mod tests {
let text = "one two three four five\nsix seven eight"; let text = "one two three four five\nsix seven eight";
let buffer = MultiBuffer::build_simple(text, cx); let buffer = MultiBuffer::build_simple(text, cx);
let map = cx.add_model(|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)); let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
@ -791,7 +813,7 @@ mod tests {
.unwrap(); .unwrap();
let font_size = 14.0; let font_size = 14.0;
let map = cx.add_model(|cx| { 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.update(cx, |buffer, cx| {
buffer.edit( buffer.edit(
@ -870,8 +892,8 @@ mod tests {
.unwrap(); .unwrap();
let font_size = 14.0; let font_size = 14.0;
let map = let map = cx
cx.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, cx)); .add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx));
assert_eq!( assert_eq!(
cx.update(|cx| chunks(0..5, &map, &theme, cx)), cx.update(|cx| chunks(0..5, &map, &theme, cx)),
vec![ vec![
@ -958,8 +980,9 @@ mod tests {
.unwrap(); .unwrap();
let font_size = 16.0; let font_size = 16.0;
let map = cx let map = cx.add_model(|cx| {
.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, Some(40.0), cx)); DisplayMap::new(buffer, tab_size, font_id, font_size, Some(40.0), 1, 1, cx)
});
assert_eq!( assert_eq!(
cx.update(|cx| chunks(0..5, &map, &theme, cx)), cx.update(|cx| chunks(0..5, &map, &theme, cx)),
[ [
@ -1003,7 +1026,7 @@ mod tests {
.unwrap(); .unwrap();
let font_size = 14.0; let font_size = 14.0;
let map = cx.add_model(|cx| { 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)); let map = map.update(cx, |map, cx| map.snapshot(cx));
@ -1047,7 +1070,7 @@ mod tests {
let font_size = 14.0; let font_size = 14.0;
let map = cx.add_model(|cx| { 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)); let map = map.update(cx, |map, cx| map.snapshot(cx));
assert_eq!(map.text(), "α\nβ \n🏀β γ"); assert_eq!(map.text(), "α\nβ \n🏀β γ");
@ -1105,7 +1128,7 @@ mod tests {
.unwrap(); .unwrap();
let font_size = 14.0; let font_size = 14.0;
let map = cx.add_model(|cx| { 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!( assert_eq!(
map.update(cx, |map, cx| map.snapshot(cx)).max_point(), map.update(cx, |map, cx| map.snapshot(cx)).max_point(),

View file

@ -1,11 +1,12 @@
use super::wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot}; use super::wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot};
use crate::{Anchor, ToPoint as _}; use crate::{Anchor, ToPoint as _};
use collections::{HashMap, HashSet}; use collections::{Bound, HashMap, HashSet};
use gpui::{AppContext, ElementBox}; use gpui::{AppContext, ElementBox};
use language::Chunk; use language::{BufferSnapshot, Chunk, Patch};
use parking_lot::Mutex; use parking_lot::Mutex;
use std::{ use std::{
cmp::{self, Ordering, Reverse}, cell::RefCell,
cmp::{self, Ordering},
fmt::Debug, fmt::Debug,
ops::{Deref, Range}, ops::{Deref, Range},
sync::{ sync::{
@ -20,9 +21,11 @@ const NEWLINES: &'static [u8] = &[b'\n'; u8::MAX as usize];
pub struct BlockMap { pub struct BlockMap {
next_block_id: AtomicUsize, next_block_id: AtomicUsize,
wrap_snapshot: Mutex<WrapSnapshot>, wrap_snapshot: RefCell<WrapSnapshot>,
blocks: Vec<Arc<Block>>, 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); pub struct BlockMapWriter<'a>(&'a mut BlockMap);
@ -84,13 +87,46 @@ pub enum BlockDisposition {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
struct Transform { struct Transform {
summary: TransformSummary, summary: TransformSummary,
block: Option<AlignedBlock>, block: Option<TransformBlock>,
} }
#[derive(Clone, Debug)] #[derive(Clone)]
pub struct AlignedBlock { pub enum TransformBlock {
block: Arc<Block>, Custom(Arc<Block>),
column: u32, 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)] #[derive(Clone, Debug, Default)]
@ -115,40 +151,71 @@ pub struct BlockBufferRows<'a> {
} }
impl BlockMap { impl BlockMap {
pub fn new(wrap_snapshot: WrapSnapshot) -> Self { pub fn new(
Self { 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), next_block_id: AtomicUsize::new(0),
blocks: Vec::new(), blocks: Vec::new(),
transforms: Mutex::new(SumTree::from_item( transforms: RefCell::new(SumTree::from_item(Transform::isomorphic(row_count), &())),
Transform::isomorphic(wrap_snapshot.text_summary().lines.row + 1), wrap_snapshot: RefCell::new(wrap_snapshot.clone()),
&(), buffer_header_height,
)), excerpt_header_height,
wrap_snapshot: Mutex::new(wrap_snapshot), };
} 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.sync(&wrap_snapshot, edits);
*self.wrap_snapshot.lock() = wrap_snapshot.clone(); *self.wrap_snapshot.borrow_mut() = wrap_snapshot.clone();
BlockSnapshot { BlockSnapshot {
wrap_snapshot, 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.sync(&wrap_snapshot, edits);
*self.wrap_snapshot.lock() = wrap_snapshot; *self.wrap_snapshot.borrow_mut() = wrap_snapshot;
BlockMapWriter(self) 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() { if edits.is_empty() {
return; return;
} }
let buffer = wrap_snapshot.buffer_snapshot(); let mut transforms = self.transforms.borrow_mut();
let mut transforms = self.transforms.lock();
let mut new_transforms = SumTree::new(); let mut new_transforms = SumTree::new();
let old_row_count = transforms.summary().input_rows; let old_row_count = transforms.summary().input_rows;
let new_row_count = wrap_snapshot.max_point().row() + 1; let new_row_count = wrap_snapshot.max_point().row() + 1;
@ -170,7 +237,7 @@ impl BlockMap {
if transform if transform
.block .block
.as_ref() .as_ref()
.map_or(false, |b| b.disposition.is_below()) .map_or(false, |b| b.disposition().is_below())
{ {
new_transforms.push(transform.clone(), &()); new_transforms.push(transform.clone(), &());
cursor.next(&()); cursor.next(&());
@ -195,7 +262,7 @@ impl BlockMap {
if transform if transform
.block .block
.as_ref() .as_ref()
.map_or(false, |b| b.disposition.is_below()) .map_or(false, |b| b.disposition().is_below())
{ {
cursor.next(&()); cursor.next(&());
} else { } else {
@ -216,7 +283,7 @@ impl BlockMap {
if transform if transform
.block .block
.as_ref() .as_ref()
.map_or(false, |b| b.disposition.is_below()) .map_or(false, |b| b.disposition().is_below())
{ {
cursor.next(&()); cursor.next(&());
} else { } else {
@ -233,28 +300,30 @@ impl BlockMap {
// Find the blocks within this edited region. // Find the blocks within this edited region.
let new_buffer_start = let new_buffer_start =
wrap_snapshot.to_point(WrapPoint::new(new_start.0, 0), Bias::Left); 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| { let start_block_ix = match self.blocks[last_block_ix..].binary_search_by(|probe| {
probe probe
.position .position
.cmp(&start_anchor, &buffer) .to_point(&buffer)
.unwrap() .cmp(&new_buffer_start)
.then(Ordering::Greater) .then(Ordering::Greater)
}) { }) {
Ok(ix) | Err(ix) => last_block_ix + ix, Ok(ix) | Err(ix) => last_block_ix + ix,
}; };
let end_bound;
let end_block_ix = if new_end.0 > wrap_snapshot.max_point().row() { let end_block_ix = if new_end.0 > wrap_snapshot.max_point().row() {
end_bound = Bound::Unbounded;
self.blocks.len() self.blocks.len()
} else { } else {
let new_buffer_end = let new_buffer_end =
wrap_snapshot.to_point(WrapPoint::new(new_end.0, 0), Bias::Left); 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| { match self.blocks[start_block_ix..].binary_search_by(|probe| {
probe probe
.position .position
.cmp(&end_anchor, &buffer) .to_point(&buffer)
.unwrap() .cmp(&new_buffer_end)
.then(Ordering::Greater) .then(Ordering::Greater)
}) { }) {
Ok(ix) | Err(ix) => start_block_ix + ix, Ok(ix) | Err(ix) => start_block_ix + ix,
@ -268,7 +337,6 @@ impl BlockMap {
.iter() .iter()
.map(|block| { .map(|block| {
let mut position = block.position.to_point(&buffer); let mut position = block.position.to_point(&buffer);
let column = wrap_snapshot.from_point(position, Bias::Left).column();
match block.disposition { match block.disposition {
BlockDisposition::Above => position.column = 0, BlockDisposition::Above => position.column = 0,
BlockDisposition::Below => { BlockDisposition::Below => {
@ -276,25 +344,57 @@ impl BlockMap {
} }
} }
let position = wrap_snapshot.from_point(position, Bias::Left); 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 // Place excerpt headers above custom blocks on the same row.
// blocks. This is arbitrary, but we currently rely on it in ProjectDiagnosticsEditor. blocks_in_edit.sort_unstable_by(|(row_a, block_a), (row_b, block_b)| {
blocks_in_edit row_a.cmp(&row_b).then_with(|| match (block_a, block_b) {
.sort_by_key(|(row, _, block)| (*row, block.disposition, Reverse(block.id))); (
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, // For each of these blocks, insert a new isomorphic transform preceding the block,
// and then insert the block itself. // and then insert the block itself.
for (block_row, column, block) in blocks_in_edit.drain(..) { for (block_row, block) in blocks_in_edit.drain(..) {
let insertion_row = match block.disposition { let insertion_row = match block.disposition() {
BlockDisposition::Above => block_row, BlockDisposition::Above => block_row,
BlockDisposition::Below => block_row + 1, BlockDisposition::Below => block_row + 1,
}; };
let extent_before_block = insertion_row - new_transforms.summary().input_rows; let extent_before_block = insertion_row - new_transforms.summary().input_rows;
push_isomorphic(&mut new_transforms, extent_before_block); 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)); old_end = WrapRow(old_end.0.min(old_row_count));
@ -375,8 +475,8 @@ impl<'a> BlockMapWriter<'a> {
blocks: impl IntoIterator<Item = BlockProperties<Anchor>>, blocks: impl IntoIterator<Item = BlockProperties<Anchor>>,
) -> Vec<BlockId> { ) -> Vec<BlockId> {
let mut ids = Vec::new(); let mut ids = Vec::new();
let mut edits = Vec::<Edit<u32>>::new(); let mut edits = Patch::default();
let wrap_snapshot = &*self.0.wrap_snapshot.lock(); let wrap_snapshot = &*self.0.wrap_snapshot.borrow();
let buffer = wrap_snapshot.buffer_snapshot(); let buffer = wrap_snapshot.buffer_snapshot();
for block in blocks { 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 = edits.compose([Edit {
edits.insert( old: start_row..end_row,
edit_ix, new: start_row..end_row,
Edit { }]);
old: start_row..end_row,
new: start_row..end_row,
},
);
}
} }
self.0.sync(wrap_snapshot, edits); self.0.sync(wrap_snapshot, edits);
@ -427,9 +522,9 @@ impl<'a> BlockMapWriter<'a> {
} }
pub fn remove(&mut self, block_ids: HashSet<BlockId>) { 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 buffer = wrap_snapshot.buffer_snapshot();
let mut edits = Vec::new(); let mut edits = Patch::default();
let mut last_block_buffer_row = None; let mut last_block_buffer_row = None;
self.0.blocks.retain(|block| { self.0.blocks.retain(|block| {
if block_ids.contains(&block.id) { if block_ids.contains(&block.id) {
@ -524,7 +619,7 @@ impl BlockSnapshot {
pub fn blocks_in_range<'a>( pub fn blocks_in_range<'a>(
&'a self, &'a self,
rows: Range<u32>, rows: Range<u32>,
) -> impl Iterator<Item = (u32, &'a AlignedBlock)> { ) -> impl Iterator<Item = (u32, &'a TransformBlock)> {
let mut cursor = self.transforms.cursor::<BlockRow>(); let mut cursor = self.transforms.cursor::<BlockRow>();
cursor.seek(&BlockRow(rows.start), Bias::Right, &()); cursor.seek(&BlockRow(rows.start), Bias::Right, &());
std::iter::from_fn(move || { std::iter::from_fn(move || {
@ -644,7 +739,7 @@ impl BlockSnapshot {
let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(); let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>();
cursor.seek(&BlockRow(block_point.row), Bias::Right, &()); cursor.seek(&BlockRow(block_point.row), Bias::Right, &());
if let Some(transform) = cursor.item() { 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::Above) => WrapPoint::new(cursor.start().1 .0, 0),
Some(BlockDisposition::Below) => { Some(BlockDisposition::Below) => {
let wrap_row = cursor.start().1 .0 - 1; 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 { Self {
summary: TransformSummary { summary: TransformSummary {
input_rows: 0, 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> { impl<'a> Deref for BlockContext<'a> {
type Target = AppContext; 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 { impl Debug for Block {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Block") f.debug_struct("Block")
@ -911,9 +990,9 @@ mod tests {
let (fold_map, folds_snapshot) = FoldMap::new(buffer_snapshot.clone()); let (fold_map, folds_snapshot) = FoldMap::new(buffer_snapshot.clone());
let (tab_map, tabs_snapshot) = TabMap::new(folds_snapshot.clone(), 1); 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 (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![ writer.insert(vec![
BlockProperties { BlockProperties {
position: buffer_snapshot.anchor_after(Point::new(1, 0)), 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"); assert_eq!(snapshot.text(), "aaa\n\n\n\nbbb\nccc\nddd\n\n\n");
let blocks = snapshot let blocks = snapshot
.blocks_in_range(0..8) .blocks_in_range(0..8)
.map(|(start_row, block)| { .map(|(start_row, block)| {
let block = block.as_custom().unwrap();
( (
start_row..start_row + block.height(), start_row..start_row + block.height as u32,
block.column(),
block block
.render(&BlockContext { .render(&BlockContext {
cx, cx,
@ -965,9 +1044,9 @@ mod tests {
assert_eq!( assert_eq!(
blocks, blocks,
&[ &[
(1..3, 2, "block 2".to_string()), (1..2, "block 1".to_string()),
(3..4, 0, "block 1".to_string()), (2..4, "block 2".to_string()),
(7..10, 3, "block 3".to_string()), (7..10, "block 3".to_string()),
] ]
); );
@ -1089,9 +1168,9 @@ mod tests {
let (_, folds_snapshot) = FoldMap::new(buffer_snapshot.clone()); let (_, folds_snapshot) = FoldMap::new(buffer_snapshot.clone());
let (_, tabs_snapshot) = TabMap::new(folds_snapshot.clone(), 1); let (_, tabs_snapshot) = TabMap::new(folds_snapshot.clone(), 1);
let (_, wraps_snapshot) = WrapMap::new(tabs_snapshot, font_id, 14.0, Some(60.), cx); 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![ writer.insert(vec![
BlockProperties { BlockProperties {
position: buffer_snapshot.anchor_after(Point::new(1, 12)), 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 an 'above' disposition go above their corresponding buffer line.
// Blocks with a 'below' disposition go below 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!( assert_eq!(
snapshot.text(), snapshot.text(),
"one two \nthree\n\nfour five \nsix\n\nseven \neight" "one two \nthree\n\nfour five \nsix\n\nseven \neight"
@ -1134,8 +1213,11 @@ mod tests {
.select_font(family_id, &Default::default()) .select_font(family_id, &Default::default())
.unwrap(); .unwrap();
let font_size = 14.0; 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!("Wrap width: {:?}", wrap_width);
log::info!("Excerpt Header Height: {:?}", excerpt_header_height);
let buffer = if rng.gen() { let buffer = if rng.gen() {
let len = rng.gen_range(0..10); 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 (tab_map, tabs_snapshot) = TabMap::new(folds_snapshot.clone(), tab_size);
let (wrap_map, wraps_snapshot) = let (wrap_map, wraps_snapshot) =
WrapMap::new(tabs_snapshot, font_id, font_size, wrap_width, cx); WrapMap::new(tabs_snapshot, font_id, font_size, wrap_width, cx);
let mut block_map = BlockMap::new(wraps_snapshot); let mut block_map = BlockMap::new(
let mut expected_blocks = Vec::new(); wraps_snapshot.clone(),
buffer_start_header_height,
excerpt_header_height,
);
let mut custom_blocks = Vec::new();
for _ in 0..operations { for _ in 0..operations {
let mut buffer_edits = Vec::new(); let mut buffer_edits = Vec::new();
@ -1205,15 +1291,15 @@ mod tests {
let mut block_map = block_map.write(wraps_snapshot, wrap_edits); let mut block_map = block_map.write(wraps_snapshot, wrap_edits);
let block_ids = block_map.insert(block_properties.clone()); let block_ids = block_map.insert(block_properties.clone());
for (block_id, props) in block_ids.into_iter().zip(block_properties) { 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() => { 40..=59 if !custom_blocks.is_empty() => {
let block_count = rng.gen_range(1..=4.min(expected_blocks.len())); let block_count = rng.gen_range(1..=4.min(custom_blocks.len()));
let block_ids_to_remove = (0..block_count) let block_ids_to_remove = (0..block_count)
.map(|_| { .map(|_| {
expected_blocks custom_blocks
.remove(rng.gen_range(0..expected_blocks.len())) .remove(rng.gen_range(0..custom_blocks.len()))
.0 .0
}) })
.collect(); .collect();
@ -1229,9 +1315,9 @@ mod tests {
} }
_ => { _ => {
buffer.update(cx, |buffer, cx| { 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(); 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_snapshot = buffer.snapshot(cx);
buffer_edits.extend(subscription.consume()); buffer_edits.extend(subscription.consume());
log::info!("buffer text: {:?}", buffer_snapshot.text()); log::info!("buffer text: {:?}", buffer_snapshot.text());
@ -1251,36 +1337,46 @@ mod tests {
); );
log::info!("blocks text: {:?}", blocks_snapshot.text()); log::info!("blocks text: {:?}", blocks_snapshot.text());
let mut sorted_blocks = expected_blocks let mut expected_blocks = Vec::new();
.iter() expected_blocks.extend(custom_blocks.iter().map(|(id, block)| {
.cloned() let mut position = block.position.to_point(&buffer_snapshot);
.map(|(id, block)| { match block.disposition {
let mut position = block.position.to_point(&buffer_snapshot); BlockDisposition::Above => {
let column = wraps_snapshot.from_point(position, Bias::Left).column(); position.column = 0;
match block.disposition { }
BlockDisposition::Above => { BlockDisposition::Below => {
position.column = 0; position.column = buffer_snapshot.line_len(position.row);
} }
BlockDisposition::Below => { };
position.column = buffer_snapshot.line_len(position.row); let row = wraps_snapshot.from_point(position, Bias::Left).row();
} (
}; row,
let row = wraps_snapshot.from_point(position, Bias::Left).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, position.row(),
BlockProperties { ExpectedBlock::ExcerptHeader {
position: BlockPoint::new(row, column), height: if boundary.starts_new_buffer {
height: block.height, buffer_start_header_height
disposition: block.disposition, } else {
render: block.render.clone(), excerpt_header_height
},
starts_new_buffer: boundary.starts_new_buffer,
}, },
) )
}) },
.collect::<Vec<_>>(); ));
sorted_blocks.sort_unstable_by_key(|(id, block)| { expected_blocks.sort_unstable();
(block.position.row, block.disposition, Reverse(*id)) let mut sorted_blocks_iter = expected_blocks.into_iter().peekable();
});
let mut sorted_blocks_iter = sorted_blocks.iter().peekable();
let input_buffer_rows = buffer_snapshot.buffer_rows(0).collect::<Vec<_>>(); let input_buffer_rows = buffer_snapshot.buffer_rows(0).collect::<Vec<_>>();
let mut expected_buffer_rows = Vec::new(); let mut expected_buffer_rows = Vec::new();
@ -1297,16 +1393,17 @@ mod tests {
.to_point(WrapPoint::new(row, 0), Bias::Left) .to_point(WrapPoint::new(row, 0), Bias::Left)
.row as usize]; .row as usize];
while let Some((block_id, block)) = sorted_blocks_iter.peek() { while let Some((block_row, block)) = sorted_blocks_iter.peek() {
if block.position.row == row && block.disposition == BlockDisposition::Above { if *block_row == row && block.disposition() == BlockDisposition::Above {
let (_, block) = sorted_blocks_iter.next().unwrap();
let height = block.height() as usize;
expected_block_positions expected_block_positions
.push((expected_text.matches('\n').count() as u32, *block_id)); .push((expected_text.matches('\n').count() as u32, block));
let text = "\n".repeat(block.height as usize); let text = "\n".repeat(height);
expected_text.push_str(&text); expected_text.push_str(&text);
for _ in 0..block.height { for _ in 0..height {
expected_buffer_rows.push(None); expected_buffer_rows.push(None);
} }
sorted_blocks_iter.next();
} else { } else {
break; break;
} }
@ -1316,16 +1413,17 @@ mod tests {
expected_buffer_rows.push(if soft_wrapped { None } else { buffer_row }); expected_buffer_rows.push(if soft_wrapped { None } else { buffer_row });
expected_text.push_str(input_line); expected_text.push_str(input_line);
while let Some((block_id, block)) = sorted_blocks_iter.peek() { while let Some((block_row, block)) = sorted_blocks_iter.peek() {
if block.position.row == row && block.disposition == BlockDisposition::Below { if *block_row == row && block.disposition() == BlockDisposition::Below {
let (_, block) = sorted_blocks_iter.next().unwrap();
let height = block.height() as usize;
expected_block_positions expected_block_positions
.push((expected_text.matches('\n').count() as u32 + 1, *block_id)); .push((expected_text.matches('\n').count() as u32 + 1, block));
let text = "\n".repeat(block.height as usize); let text = "\n".repeat(height);
expected_text.push_str(&text); expected_text.push_str(&text);
for _ in 0..block.height { for _ in 0..height {
expected_buffer_rows.push(None); expected_buffer_rows.push(None);
} }
sorted_blocks_iter.next();
} else { } else {
break; break;
} }
@ -1337,7 +1435,7 @@ mod tests {
for start_row in 0..expected_row_count { for start_row in 0..expected_row_count {
let expected_text = expected_lines[start_row..].join("\n"); let expected_text = expected_lines[start_row..].join("\n");
let actual_text = blocks_snapshot 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) .map(|chunk| chunk.text)
.collect::<String>(); .collect::<String>();
assert_eq!( assert_eq!(
@ -1356,7 +1454,7 @@ mod tests {
assert_eq!( assert_eq!(
blocks_snapshot blocks_snapshot
.blocks_in_range(0..(expected_row_count as u32)) .blocks_in_range(0..(expected_row_count as u32))
.map(|(row, block)| (row, block.id)) .map(|(row, block)| (row, block.clone().into()))
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
expected_block_positions 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 { impl BlockSnapshot {

View file

@ -107,14 +107,23 @@ impl<'a> FoldMapWriter<'a> {
let buffer = self.0.buffer.lock().clone(); let buffer = self.0.buffer.lock().clone();
for range in ranges.into_iter() { for range in ranges.into_iter() {
let range = range.start.to_offset(&buffer)..range.end.to_offset(&buffer); 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)); // Ignore any empty ranges.
folds.push(fold); if range.start == range.end {
edits.push(text::Edit { continue;
old: range.clone(),
new: range,
});
} }
// 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)); 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(); let mut buffer = self.buffer.lock();
if buffer.parse_count() != new_buffer.parse_count() if buffer.parse_count() != new_buffer.parse_count()
|| buffer.diagnostics_update_count() != new_buffer.diagnostics_update_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); self.version.fetch_add(1, SeqCst);
} }
@ -1281,7 +1292,7 @@ mod tests {
_ => buffer.update(cx, |buffer, cx| { _ => buffer.update(cx, |buffer, cx| {
let subscription = buffer.subscribe(); let subscription = buffer.subscribe();
let edit_count = rng.gen_range(1..=5); 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_snapshot = buffer.snapshot(cx);
let edits = subscription.consume().into_inner(); let edits = subscription.consume().into_inner();
log::info!("editing {:?}", edits); log::info!("editing {:?}", edits);
@ -1407,7 +1418,6 @@ mod tests {
fold_row = snapshot fold_row = snapshot
.clip_point(FoldPoint::new(fold_row, 0), Bias::Right) .clip_point(FoldPoint::new(fold_row, 0), Bias::Right)
.row(); .row();
eprintln!("fold_row: {} of {}", fold_row, expected_buffer_rows.len());
assert_eq!( assert_eq!(
snapshot.buffer_rows(fold_row).collect::<Vec<_>>(), snapshot.buffer_rows(fold_row).collect::<Vec<_>>(),
expected_buffer_rows[(fold_row as usize)..], expected_buffer_rows[(fold_row as usize)..],

View file

@ -106,7 +106,7 @@ impl WrapMap {
tab_snapshot: TabSnapshot, tab_snapshot: TabSnapshot,
edits: Vec<TabEdit>, edits: Vec<TabEdit>,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> (WrapSnapshot, Vec<WrapEdit>) { ) -> (WrapSnapshot, Patch<u32>) {
if self.wrap_width.is_some() { if self.wrap_width.is_some() {
self.pending_edits.push_back((tab_snapshot, edits)); self.pending_edits.push_back((tab_snapshot, edits));
self.flush_edits(cx); self.flush_edits(cx);
@ -117,10 +117,7 @@ impl WrapMap {
self.snapshot.interpolated = false; self.snapshot.interpolated = false;
} }
( (self.snapshot.clone(), mem::take(&mut self.edits_since_sync))
self.snapshot.clone(),
mem::take(&mut self.edits_since_sync).into_inner(),
)
} }
pub fn set_font(&mut self, font_id: FontId, font_size: f32, cx: &mut ModelContext<Self>) { 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 { pub fn max_point(&self) -> WrapPoint {
WrapPoint(self.transforms.summary().output.lines) WrapPoint(self.transforms.summary().output.lines)
} }
@ -955,10 +948,6 @@ impl WrapPoint {
&mut self.0.row &mut self.0.row
} }
pub fn column(&self) -> u32 {
self.0.column
}
pub fn column_mut(&mut self) -> &mut u32 { pub fn column_mut(&mut self) -> &mut u32 {
&mut self.0.column &mut self.0.column
} }
@ -1118,7 +1107,7 @@ mod tests {
buffer.update(&mut cx, |buffer, cx| { buffer.update(&mut cx, |buffer, cx| {
let subscription = buffer.subscribe(); let subscription = buffer.subscribe();
let edit_count = rng.gen_range(1..=5); 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_snapshot = buffer.snapshot(cx);
buffer_edits.extend(subscription.consume()); buffer_edits.extend(subscription.consume());
}); });

File diff suppressed because it is too large Load diff

View file

@ -3,11 +3,12 @@ use super::{
Anchor, DisplayPoint, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, Input, Anchor, DisplayPoint, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, Input,
Scroll, Select, SelectPhase, SoftWrap, ToPoint, MAX_LINE_LEN, Scroll, Select, SelectPhase, SoftWrap, ToPoint, MAX_LINE_LEN,
}; };
use crate::display_map::TransformBlock;
use clock::ReplicaId; use clock::ReplicaId;
use collections::{BTreeMap, HashMap}; use collections::{BTreeMap, HashMap};
use gpui::{ use gpui::{
color::Color, color::Color,
elements::layout_highlighted_chunks, elements::*,
fonts::{HighlightStyle, Underline}, fonts::{HighlightStyle, Underline},
geometry::{ geometry::{
rect::RectF, rect::RectF,
@ -280,7 +281,7 @@ impl EditorElement {
&mut self, &mut self,
bounds: RectF, bounds: RectF,
visible_bounds: RectF, visible_bounds: RectF,
layout: &LayoutState, layout: &mut LayoutState,
cx: &mut PaintContext, cx: &mut PaintContext,
) { ) {
let scroll_top = layout.snapshot.scroll_position().y() * layout.line_height; 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); 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( fn paint_text(
@ -392,20 +401,20 @@ impl EditorElement {
} }
cx.scene.pop_layer(); 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); cx.scene.push_stacking_context(None);
let cursor_row_layout = &layout.line_layouts[(position.row() - start_row) as usize]; 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 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 y = (position.row() + 1) as f32 * layout.line_height - scroll_top;
let mut list_origin = content_origin + vec2f(x, y); 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() { if list_origin.y() + list_height > bounds.lower_left().y() {
list_origin.set_y(list_origin.y() - layout.line_height - list_height); list_origin.set_y(list_origin.y() - layout.line_height - list_height);
} }
completions_list.paint( context_menu.paint(
list_origin, list_origin,
RectF::from_points(Vector2F::zero(), vec2f(f32::MAX, f32::MAX)), // Let content bleed outside of editor RectF::from_points(Vector2F::zero(), vec2f(f32::MAX, f32::MAX)), // Let content bleed outside of editor
cx, cx,
@ -649,33 +658,91 @@ impl EditorElement {
line_layouts: &[text_layout::Line], line_layouts: &[text_layout::Line],
cx: &mut LayoutContext, cx: &mut LayoutContext,
) -> Vec<(u32, ElementBox)> { ) -> Vec<(u32, ElementBox)> {
let scroll_x = snapshot.scroll_position.x();
snapshot snapshot
.blocks_in_range(rows.clone()) .blocks_in_range(rows.clone())
.map(|(start_row, block)| { .map(|(block_row, block)| {
let anchor_row = block let mut element = match block {
.position() TransformBlock::Custom(block) => {
.to_point(&snapshot.buffer_snapshot) let align_to = block
.to_display_point(snapshot) .position()
.row(); .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 block.render(&BlockContext {
+ if rows.contains(&anchor_row) { cx,
line_layouts[(anchor_row - rows.start) as usize] anchor_x,
.x_for_index(block.column() as usize) gutter_padding,
} else { line_height,
layout_line(anchor_row, snapshot, style, cx.text_layout_cache) scroll_x,
.x_for_index(block.column() as usize) 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( element.layout(
SizeConstraint { SizeConstraint {
min: Vector2F::zero(), min: Vector2F::zero(),
@ -683,7 +750,7 @@ impl EditorElement {
}, },
cx, cx,
); );
(start_row, element) (block_row, element)
}) })
.collect() .collect()
} }
@ -859,7 +926,8 @@ impl Element for EditorElement {
max_row.saturating_sub(1) as f32, 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| { self.update_view(cx.app, |view, cx| {
let clamped = view.clamp_scroll_left(scroll_max.x()); let clamped = view.clamp_scroll_left(scroll_max.x());
let autoscrolled; let autoscrolled;
@ -880,21 +948,24 @@ impl Element for EditorElement {
snapshot = view.snapshot(cx); snapshot = view.snapshot(cx);
} }
if view.has_completions() { let newest_selection_head = view
let newest_selection_head = view .newest_selection::<usize>(&snapshot.buffer_snapshot)
.newest_selection::<usize>(&snapshot.buffer_snapshot) .head()
.head() .to_display_point(&snapshot);
.to_display_point(&snapshot);
if (start_row..end_row).contains(&newest_selection_head.row()) { if (start_row..end_row).contains(&newest_selection_head.row()) {
let list = view.render_completions(cx).unwrap(); if view.context_menu_visible() {
completions = Some((newest_selection_head, list)); 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() { if let Some((_, context_menu)) = context_menu.as_mut() {
completions_list.layout( context_menu.layout(
SizeConstraint { SizeConstraint {
min: Vector2F::zero(), min: Vector2F::zero(),
max: vec2f( 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( let blocks = self.layout_blocks(
start_row..end_row, start_row..end_row,
&snapshot, &snapshot,
@ -940,7 +1018,8 @@ impl Element for EditorElement {
em_width, em_width,
em_advance, em_advance,
selections, selections,
completions, context_menu,
code_actions_indicator,
}, },
) )
} }
@ -989,8 +1068,14 @@ impl Element for EditorElement {
paint: &mut PaintState, paint: &mut PaintState,
cx: &mut EventContext, cx: &mut EventContext,
) -> bool { ) -> bool {
if let Some((_, completion_list)) = &mut layout.completions { if let Some((_, context_menu)) = &mut layout.context_menu {
if completion_list.dispatch_event(event, cx) { 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; return true;
} }
} }
@ -1051,7 +1136,8 @@ pub struct LayoutState {
highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>, highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
selections: HashMap<ReplicaId, Vec<text::Selection<DisplayPoint>>>, selections: HashMap<ReplicaId, Vec<text::Selection<DisplayPoint>>>,
text_offset: Vector2F, text_offset: Vector2F,
completions: Option<(DisplayPoint, ElementBox)>, context_menu: Option<(DisplayPoint, ElementBox)>,
code_actions_indicator: Option<(u32, ElementBox)>,
} }
fn layout_line( fn layout_line(
@ -1298,6 +1384,7 @@ mod tests {
let settings = settings.clone(); let settings = settings.clone();
Arc::new(move |_| settings.clone()) Arc::new(move |_| settings.clone())
}, },
None,
cx, cx,
) )
}); });

View file

@ -11,7 +11,7 @@ use std::path::PathBuf;
use std::rc::Rc; use std::rc::Rc;
use std::{cell::RefCell, fmt::Write}; use std::{cell::RefCell, fmt::Write};
use text::{Point, Selection}; use text::{Point, Selection};
use util::TryFutureExt; use util::ResultExt;
use workspace::{ use workspace::{
ItemHandle, ItemNavHistory, ItemView, ItemViewHandle, NavHistory, PathOpener, Settings, ItemHandle, ItemNavHistory, ItemView, ItemViewHandle, NavHistory, PathOpener, Settings,
StatusItemView, WeakItemHandle, Workspace, StatusItemView, WeakItemHandle, Workspace,
@ -25,6 +25,12 @@ pub struct BufferItemHandle(pub ModelHandle<Buffer>);
#[derive(Clone)] #[derive(Clone)]
struct WeakBufferItemHandle(WeakModelHandle<Buffer>); struct WeakBufferItemHandle(WeakModelHandle<Buffer>);
#[derive(Clone)]
pub struct MultiBufferItemHandle(pub ModelHandle<MultiBuffer>);
#[derive(Clone)]
struct WeakMultiBufferItemHandle(WeakModelHandle<MultiBuffer>);
impl PathOpener for BufferOpener { impl PathOpener for BufferOpener {
fn open( fn open(
&self, &self,
@ -55,6 +61,7 @@ impl ItemHandle for BufferItemHandle {
let mut editor = Editor::for_buffer( let mut editor = Editor::for_buffer(
buffer, buffer,
crate::settings_builder(weak_buffer, workspace.settings()), crate::settings_builder(weak_buffer, workspace.settings()),
Some(workspace.project().clone()),
cx, cx,
); );
editor.nav_history = Some(ItemNavHistory::new(nav_history, &cx.handle())); 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 { impl WeakItemHandle for WeakBufferItemHandle {
fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> { fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
self.0 self.0
@ -98,11 +147,25 @@ impl WeakItemHandle for WeakBufferItemHandle {
} }
} }
impl ItemView for Editor { impl WeakItemHandle for WeakMultiBufferItemHandle {
type ItemHandle = BufferItemHandle; 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 { fn id(&self) -> usize {
BufferItemHandle(self.buffer.read(cx).as_singleton().unwrap()) 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>) { 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>) { fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
if let Some(selection) = self.newest_anchor_selection() { let selection = self.newest_anchor_selection();
self.push_to_nav_history(selection.head(), None, cx); self.push_to_nav_history(selection.head(), None, cx);
}
} }
fn is_dirty(&self, cx: &AppContext) -> bool { fn is_dirty(&self, cx: &AppContext) -> bool {
@ -155,25 +217,39 @@ impl ItemView for Editor {
} }
fn can_save(&self, cx: &AppContext) -> bool { 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(); let buffer = self.buffer().clone();
cx.spawn(|editor, mut cx| async move { let buffers = buffer.read(cx).all_buffers();
buffer let transaction = project.update(cx, |project, cx| project.format(buffers, true, cx));
.update(&mut cx, |buffer, cx| buffer.format(cx).log_err()) cx.spawn(|this, mut cx| async move {
.await; let transaction = transaction.await.log_err();
editor.update(&mut cx, |editor, cx| { this.update(&mut cx, |editor, cx| {
editor.request_autoscroll(Autoscroll::Fit, 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(()) Ok(())
}) })
} }
fn can_save_as(&self, _: &AppContext) -> bool { fn can_save_as(&self, cx: &AppContext) -> bool {
true self.buffer().read(cx).is_singleton()
} }
fn save_as( fn save_as(
@ -331,7 +407,7 @@ impl View for DiagnosticMessage {
if let Some(diagnostic) = &self.diagnostic { if let Some(diagnostic) = &self.diagnostic {
let theme = &self.settings.borrow().theme.workspace.status_bar; let theme = &self.settings.borrow().theme.workspace.status_bar;
Label::new( Label::new(
diagnostic.message.lines().next().unwrap().to_string(), diagnostic.message.split('\n').next().unwrap().to_string(),
theme.diagnostic_message.clone(), theme.diagnostic_message.clone(),
) )
.contained() .contained()

View file

@ -225,13 +225,8 @@ pub fn surrounding_word(map: &DisplaySnapshot, position: DisplayPoint) -> Range<
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::{ use crate::{Buffer, DisplayMap, MultiBuffer};
display_map::{BlockDisposition, BlockProperties},
Buffer, DisplayMap, ExcerptProperties, MultiBuffer,
};
use gpui::{elements::Empty, Element};
use language::Point; use language::Point;
use std::sync::Arc;
#[gpui::test] #[gpui::test]
fn test_move_up_and_down_with_excerpts(cx: &mut gpui::MutableAppContext) { fn test_move_up_and_down_with_excerpts(cx: &mut gpui::MutableAppContext) {
@ -242,62 +237,24 @@ mod tests {
.unwrap(); .unwrap();
let buffer = cx.add_model(|cx| Buffer::new(0, "abc\ndefg\nhijkl\nmn", cx)); 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 multibuffer = cx.add_model(|cx| {
let mut multibuffer = MultiBuffer::new(0); let mut multibuffer = MultiBuffer::new(0);
let excerpt1_id = multibuffer.push_excerpt( multibuffer.push_excerpts(
ExcerptProperties { buffer.clone(),
buffer: &buffer, [
range: Point::new(0, 0)..Point::new(1, 4), Point::new(0, 0)..Point::new(1, 4),
}, Point::new(2, 0)..Point::new(3, 2),
],
cx, 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 multibuffer
}); });
let display_map = let display_map =
cx.add_model(|cx| DisplayMap::new(multibuffer, 2, font_id, 14.0, None, cx)); cx.add_model(|cx| DisplayMap::new(multibuffer, 2, font_id, 14.0, None, 2, 2, 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,
)
});
let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx)); let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\n\nhijkl\nmn"); assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn");
// Can't move up into the first excerpt's header // Can't move up into the first excerpt's header
assert_eq!( assert_eq!(
@ -321,22 +278,22 @@ mod tests {
// Move up and down across second excerpt's header // Move up and down across second excerpt's header
assert_eq!( assert_eq!(
up(&snapshot, DisplayPoint::new(7, 5), SelectionGoal::Column(5)).unwrap(), up(&snapshot, DisplayPoint::new(6, 5), SelectionGoal::Column(5)).unwrap(),
(DisplayPoint::new(3, 4), SelectionGoal::Column(5)), (DisplayPoint::new(3, 4), SelectionGoal::Column(5)),
); );
assert_eq!( assert_eq!(
down(&snapshot, DisplayPoint::new(3, 4), SelectionGoal::Column(5)).unwrap(), down(&snapshot, DisplayPoint::new(3, 4), SelectionGoal::Column(5)).unwrap(),
(DisplayPoint::new(7, 5), SelectionGoal::Column(5)), (DisplayPoint::new(6, 5), SelectionGoal::Column(5)),
); );
// Can't move down off the end // Can't move down off the end
assert_eq!( assert_eq!(
down(&snapshot, DisplayPoint::new(8, 0), SelectionGoal::Column(0)).unwrap(), down(&snapshot, DisplayPoint::new(7, 0), SelectionGoal::Column(0)).unwrap(),
(DisplayPoint::new(8, 2), SelectionGoal::Column(2)), (DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
); );
assert_eq!( assert_eq!(
down(&snapshot, DisplayPoint::new(8, 2), SelectionGoal::Column(2)).unwrap(), down(&snapshot, DisplayPoint::new(7, 2), SelectionGoal::Column(2)).unwrap(),
(DisplayPoint::new(8, 2), SelectionGoal::Column(2)), (DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
); );
} }
@ -351,8 +308,8 @@ mod tests {
let font_size = 14.0; let font_size = 14.0;
let buffer = MultiBuffer::build_simple("a bcΔ defγ hi—jk", cx); let buffer = MultiBuffer::build_simple("a bcΔ defγ hi—jk", cx);
let display_map = let display_map = cx
cx.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, cx)); .add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx));
let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx)); let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
assert_eq!( assert_eq!(
prev_word_boundary(&snapshot, DisplayPoint::new(0, 12)), prev_word_boundary(&snapshot, DisplayPoint::new(0, 12)),
@ -407,8 +364,8 @@ mod tests {
.unwrap(); .unwrap();
let font_size = 14.0; let font_size = 14.0;
let buffer = MultiBuffer::build_simple("lorem ipsum dolor\n sit", cx); let buffer = MultiBuffer::build_simple("lorem ipsum dolor\n sit", cx);
let display_map = let display_map = cx
cx.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, cx)); .add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx));
let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx)); let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
assert_eq!( assert_eq!(

File diff suppressed because it is too large Load diff

View file

@ -9,7 +9,7 @@ use text::{rope::TextDimension, Point};
#[derive(Clone, Eq, PartialEq, Debug, Hash)] #[derive(Clone, Eq, PartialEq, Debug, Hash)]
pub struct Anchor { pub struct Anchor {
pub(crate) buffer_id: usize, pub(crate) buffer_id: Option<usize>,
pub(crate) excerpt_id: ExcerptId, pub(crate) excerpt_id: ExcerptId,
pub(crate) text_anchor: text::Anchor, pub(crate) text_anchor: text::Anchor,
} }
@ -17,7 +17,7 @@ pub struct Anchor {
impl Anchor { impl Anchor {
pub fn min() -> Self { pub fn min() -> Self {
Self { Self {
buffer_id: 0, buffer_id: None,
excerpt_id: ExcerptId::min(), excerpt_id: ExcerptId::min(),
text_anchor: text::Anchor::min(), text_anchor: text::Anchor::min(),
} }
@ -25,7 +25,7 @@ impl Anchor {
pub fn max() -> Self { pub fn max() -> Self {
Self { Self {
buffer_id: 0, buffer_id: None,
excerpt_id: ExcerptId::max(), excerpt_id: ExcerptId::max(),
text_anchor: text::Anchor::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 // 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 // changed. In that case, treat the anchor as if it were at the start of that
// excerpt. // 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) 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) Ok(Ordering::Greater)
} else if other.buffer_id == buffer_id { } else if other.buffer_id == Some(buffer_id) {
Ok(Ordering::Less) Ok(Ordering::Less)
} else { } else {
Ok(Ordering::Equal) Ok(Ordering::Equal)
@ -68,7 +68,7 @@ impl Anchor {
if let Some((buffer_id, buffer_snapshot)) = if let Some((buffer_id, buffer_snapshot)) =
snapshot.buffer_snapshot_for_excerpt(&self.excerpt_id) snapshot.buffer_snapshot_for_excerpt(&self.excerpt_id)
{ {
if self.buffer_id == buffer_id { if self.buffer_id == Some(buffer_id) {
return Self { return Self {
buffer_id: self.buffer_id, buffer_id: self.buffer_id,
excerpt_id: self.excerpt_id.clone(), excerpt_id: self.excerpt_id.clone(),
@ -85,7 +85,7 @@ impl Anchor {
if let Some((buffer_id, buffer_snapshot)) = if let Some((buffer_id, buffer_snapshot)) =
snapshot.buffer_snapshot_for_excerpt(&self.excerpt_id) snapshot.buffer_snapshot_for_excerpt(&self.excerpt_id)
{ {
if self.buffer_id == buffer_id { if self.buffer_id == Some(buffer_id) {
return Self { return Self {
buffer_id: self.buffer_id, buffer_id: self.buffer_id,
excerpt_id: self.excerpt_id.clone(), excerpt_id: self.excerpt_id.clone(),

View file

@ -355,11 +355,8 @@ impl FindBar {
if let Some(mut index) = self.active_match_index { if let Some(mut index) = self.active_match_index {
if let Some(editor) = self.active_editor.as_ref() { if let Some(editor) = self.active_editor.as_ref() {
editor.update(cx, |editor, cx| { editor.update(cx, |editor, cx| {
let newest_selection = editor.newest_anchor_selection().cloned(); let newest_selection = editor.newest_anchor_selection().clone();
if let Some(((_, ranges), newest_selection)) = editor if let Some((_, ranges)) = editor.highlighted_ranges_for_type::<Self>() {
.highlighted_ranges_for_type::<Self>()
.zip(newest_selection)
{
let position = newest_selection.head(); let position = newest_selection.head();
let buffer = editor.buffer().read(cx).read(cx); let buffer = editor.buffer().read(cx).read(cx);
if ranges[index].start.cmp(&position, &buffer).unwrap().is_gt() { 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 { self.pending_search = Some(cx.spawn(|this, mut cx| async move {
match ranges.await { match ranges.await {
Ok(ranges) => { 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.update(&mut cx, |this, cx| {
this.highlighted_editors.insert(editor.downgrade()); this.highlighted_editors.insert(editor.downgrade());
editor.update(cx, |editor, cx| { editor.update(cx, |editor, cx| {
@ -502,7 +499,7 @@ impl FindBar {
fn active_match_index(&mut self, cx: &mut ViewContext<Self>) -> Option<usize> { fn active_match_index(&mut self, cx: &mut ViewContext<Self>) -> Option<usize> {
let editor = self.active_editor.as_ref()?; let editor = self.active_editor.as_ref()?;
let editor = editor.read(cx); 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; let ranges = editor.highlighted_ranges_for_type::<Self>()?.1;
if ranges.is_empty() { if ranges.is_empty() {
None None
@ -655,7 +652,7 @@ mod tests {
) )
}); });
let editor = cx.add_view(Default::default(), |cx| { 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| { let find_bar = cx.add_view(Default::default(), |cx| {

View file

@ -80,8 +80,14 @@ pub trait UpdateModel {
} }
pub trait UpgradeModelHandle { pub trait UpgradeModelHandle {
fn upgrade_model_handle<T: Entity>(&self, handle: WeakModelHandle<T>) fn upgrade_model_handle<T: Entity>(
-> Option<ModelHandle<T>>; &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 { pub trait ReadView {
@ -558,12 +564,18 @@ impl UpdateModel for AsyncAppContext {
impl UpgradeModelHandle for AsyncAppContext { impl UpgradeModelHandle for AsyncAppContext {
fn upgrade_model_handle<T: Entity>( fn upgrade_model_handle<T: Entity>(
&self, &self,
handle: WeakModelHandle<T>, handle: &WeakModelHandle<T>,
) -> Option<ModelHandle<T>> { ) -> Option<ModelHandle<T>> {
self.0.borrow_mut().upgrade_model_handle(handle) 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 { impl ReadModelWith for AsyncAppContext {
fn read_model_with<E: Entity, T>( fn read_model_with<E: Entity, T>(
&self, &self,
@ -831,6 +843,17 @@ impl MutableAppContext {
.push(handler); .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) pub fn add_global_action<A, F>(&mut self, mut handler: F)
where where
A: Action, A: Action,
@ -1721,12 +1744,18 @@ impl UpdateModel for MutableAppContext {
impl UpgradeModelHandle for MutableAppContext { impl UpgradeModelHandle for MutableAppContext {
fn upgrade_model_handle<T: Entity>( fn upgrade_model_handle<T: Entity>(
&self, &self,
handle: WeakModelHandle<T>, handle: &WeakModelHandle<T>,
) -> Option<ModelHandle<T>> { ) -> Option<ModelHandle<T>> {
self.cx.upgrade_model_handle(handle) 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 { impl ReadView for MutableAppContext {
fn read_view<T: View>(&self, handle: &ViewHandle<T>) -> &T { fn read_view<T: View>(&self, handle: &ViewHandle<T>) -> &T {
if let Some(view) = self.cx.views.get(&(handle.window_id, handle.view_id)) { 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 { impl UpgradeModelHandle for AppContext {
fn upgrade_model_handle<T: Entity>( fn upgrade_model_handle<T: Entity>(
&self, &self,
handle: WeakModelHandle<T>, handle: &WeakModelHandle<T>,
) -> Option<ModelHandle<T>> { ) -> Option<ModelHandle<T>> {
if self.models.contains_key(&handle.model_id) { if self.models.contains_key(&handle.model_id) {
Some(ModelHandle::new(handle.model_id, &self.ref_counts)) 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 { impl ReadView for AppContext {
fn read_view<T: View>(&self, handle: &ViewHandle<T>) -> &T { fn read_view<T: View>(&self, handle: &ViewHandle<T>) -> &T {
if let Some(view) = self.views.get(&(handle.window_id, handle.view_id)) { 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> { impl<M> UpgradeModelHandle for ModelContext<'_, M> {
fn upgrade_model_handle<T: Entity>( fn upgrade_model_handle<T: Entity>(
&self, &self,
handle: WeakModelHandle<T>, handle: &WeakModelHandle<T>,
) -> Option<ModelHandle<T>> { ) -> Option<ModelHandle<T>> {
self.cx.upgrade_model_handle(handle) self.cx.upgrade_model_handle(handle)
} }
@ -2547,12 +2590,18 @@ impl<V> ReadModel for ViewContext<'_, V> {
impl<V> UpgradeModelHandle for ViewContext<'_, V> { impl<V> UpgradeModelHandle for ViewContext<'_, V> {
fn upgrade_model_handle<T: Entity>( fn upgrade_model_handle<T: Entity>(
&self, &self,
handle: WeakModelHandle<T>, handle: &WeakModelHandle<T>,
) -> Option<ModelHandle<T>> { ) -> Option<ModelHandle<T>> {
self.cx.upgrade_model_handle(handle) 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> { impl<V: View> UpdateModel for ViewContext<'_, V> {
fn update_model<T: Entity, O>( fn update_model<T: Entity, O>(
&mut self, &mut self,
@ -2654,7 +2703,7 @@ impl<T: Entity> ModelHandle<T> {
let (mut tx, mut rx) = mpsc::channel(1); let (mut tx, mut rx) = mpsc::channel(1);
let mut cx = cx.cx.borrow_mut(); let mut cx = cx.cx.borrow_mut();
let subscription = cx.observe(self, move |_, _| { let subscription = cx.observe(self, move |_, _| {
tx.blocking_send(()).ok(); tx.try_send(()).ok();
}); });
let duration = if std::env::var("CI").is_ok() { let duration = if std::env::var("CI").is_ok() {
@ -2850,7 +2899,7 @@ impl<T: Entity> WeakModelHandle<T> {
self.model_id 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) cx.upgrade_model_handle(self)
} }
} }
@ -2958,7 +3007,7 @@ impl<T: View> ViewHandle<T> {
let (mut tx, mut rx) = mpsc::channel(1); let (mut tx, mut rx) = mpsc::channel(1);
let mut cx = cx.cx.borrow_mut(); let mut cx = cx.cx.borrow_mut();
let subscription = cx.observe(self, move |_, _| { let subscription = cx.observe(self, move |_, _| {
tx.blocking_send(()).ok(); tx.try_send(()).ok();
}); });
let duration = if std::env::var("CI").is_ok() { let duration = if std::env::var("CI").is_ok() {
@ -3266,16 +3315,8 @@ impl<T: View> WeakViewHandle<T> {
self.view_id self.view_id
} }
pub fn upgrade(&self, cx: &AppContext) -> Option<ViewHandle<T>> { pub fn upgrade(&self, cx: &impl UpgradeViewHandle) -> Option<ViewHandle<T>> {
if cx.ref_counts.lock().is_entity_alive(self.view_id) { cx.upgrade_view_handle(self)
Some(ViewHandle::new(
self.window_id,
self.view_id,
&cx.ref_counts,
))
} else {
None
}
} }
} }

View file

@ -550,8 +550,11 @@ impl Background {
pub async fn simulate_random_delay(&self) { pub async fn simulate_random_delay(&self) {
match self { match self {
Self::Deterministic { executor, .. } => { Self::Deterministic { executor, .. } => {
if executor.state.lock().rng.gen_range(0..100) < 20 { if executor.state.lock().rng.gen_bool(0.2) {
yield_now().await; 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"), _ => panic!("this method can only be called on a deterministic executor"),

View file

@ -107,6 +107,10 @@ unsafe fn build_classes() {
sel!(scrollWheel:), sel!(scrollWheel:),
handle_view_event as extern "C" fn(&Object, Sel, id), 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( decl.add_method(
sel!(makeBackingLayer), 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) { extern "C" fn send_event(this: &Object, _: Sel, native_event: id) {
unsafe { unsafe {
let () = msg_send![super(this, class!(NSWindow)), sendEvent: native_event]; let () = msg_send![super(this, class!(NSWindow)), sendEvent: native_event];

View file

@ -7,7 +7,8 @@ use crate::{
platform::Event, platform::Event,
text_layout::TextLayoutCache, text_layout::TextLayoutCache,
Action, AnyAction, AnyViewHandle, AssetCache, ElementBox, Entity, FontSystem, ModelHandle, 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 pathfinder_geometry::vector::{vec2f, Vector2F};
use serde_json::json; 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> { pub struct PaintContext<'a> {
rendered_views: &'a mut HashMap<usize, ElementBox>, rendered_views: &'a mut HashMap<usize, ElementBox>,
pub scene: &'a mut Scene, pub scene: &'a mut Scene,

File diff suppressed because it is too large Load diff

View file

@ -7,19 +7,20 @@ pub mod proto;
mod tests; mod tests;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
pub use buffer::Operation;
pub use buffer::*;
use collections::HashSet; use collections::HashSet;
pub use diagnostic_set::DiagnosticEntry;
use gpui::AppContext; use gpui::AppContext;
use highlight_map::HighlightMap; use highlight_map::HighlightMap;
use lazy_static::lazy_static; use lazy_static::lazy_static;
pub use outline::{Outline, OutlineItem};
use parking_lot::Mutex; use parking_lot::Mutex;
use serde::Deserialize; use serde::Deserialize;
use std::{cell::RefCell, ops::Range, path::Path, str, sync::Arc}; use std::{cell::RefCell, ops::Range, path::Path, str, sync::Arc};
use theme::SyntaxTheme; use theme::SyntaxTheme;
use tree_sitter::{self, Query}; 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}; pub use tree_sitter::{Parser, Tree};
thread_local! { thread_local! {
@ -39,10 +40,6 @@ lazy_static! {
)); ));
} }
pub trait ToPointUtf16 {
fn to_point_utf16(self) -> PointUtf16;
}
pub trait ToLspPosition { pub trait ToLspPosition {
fn to_lsp_position(self) -> lsp::Position; fn to_lsp_position(self) -> lsp::Position;
} }
@ -360,18 +357,15 @@ impl CompletionLabel {
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
impl LanguageServerConfig { impl LanguageServerConfig {
pub async fn fake( pub async fn fake(cx: &gpui::TestAppContext) -> (Self, lsp::FakeLanguageServer) {
executor: Arc<gpui::executor::Background>, Self::fake_with_capabilities(Default::default(), cx).await
) -> (Self, lsp::FakeLanguageServer) {
Self::fake_with_capabilities(Default::default(), executor).await
} }
pub async fn fake_with_capabilities( pub async fn fake_with_capabilities(
capabilites: lsp::ServerCapabilities, capabilites: lsp::ServerCapabilities,
executor: Arc<gpui::executor::Background>, cx: &gpui::TestAppContext,
) -> (Self, lsp::FakeLanguageServer) { ) -> (Self, lsp::FakeLanguageServer) {
let (server, fake) = let (server, fake) = lsp::LanguageServer::fake_with_capabilities(capabilites, cx).await;
lsp::LanguageServer::fake_with_capabilities(capabilites, executor).await;
fake.started fake.started
.store(false, std::sync::atomic::Ordering::SeqCst); .store(false, std::sync::atomic::Ordering::SeqCst);
let started = fake.started.clone(); 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 { impl ToLspPosition for PointUtf16 {
fn to_lsp_position(self) -> lsp::Position { fn to_lsp_position(self) -> lsp::Position {
lsp::Position::new(self.row, self.column) 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> { pub fn range_from_lsp(range: lsp::Range) -> Range<PointUtf16> {
let start = PointUtf16::new(range.start.line, range.start.character); let start = PointUtf16::new(range.start.line, range.start.character);
let end = PointUtf16::new(range.end.line, range.end.character); let end = PointUtf16::new(range.end.line, range.end.character);

View file

@ -1,12 +1,13 @@
use crate::{ use crate::{
diagnostic_set::DiagnosticEntry, Completion, CompletionLabel, Diagnostic, Language, Operation, diagnostic_set::DiagnosticEntry, CodeAction, Completion, CompletionLabel, Diagnostic, Language,
Operation,
}; };
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use clock::ReplicaId; use clock::ReplicaId;
use collections::HashSet; use collections::HashSet;
use lsp::DiagnosticSeverity; use lsp::DiagnosticSeverity;
use rpc::proto; use rpc::proto;
use std::sync::Arc; use std::{ops::Range, sync::Arc};
use text::*; use text::*;
pub use proto::{Buffer, BufferState, SelectionSet}; 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, replica_id: undo.id.replica_id as u32,
local_timestamp: undo.id.value, local_timestamp: undo.id.value,
lamport_timestamp: lamport_timestamp.value, lamport_timestamp: lamport_timestamp.value,
ranges: undo ranges: undo.ranges.iter().map(serialize_range).collect(),
.ranges
.iter()
.map(|r| proto::Range {
start: r.start.0 as u64,
end: r.end.0 as u64,
})
.collect(),
counts: undo counts: undo
.counts .counts
.iter() .iter()
@ -44,11 +38,10 @@ pub fn serialize_operation(operation: &Operation) -> proto::Operation {
version: From::from(&undo.version), version: From::from(&undo.version),
}), }),
Operation::UpdateSelections { Operation::UpdateSelections {
replica_id,
selections, selections,
lamport_timestamp, lamport_timestamp,
} => proto::operation::Variant::UpdateSelections(proto::operation::UpdateSelections { } => 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, lamport_timestamp: lamport_timestamp.value,
selections: serialize_selections(selections), selections: serialize_selections(selections),
}), }),
@ -60,32 +53,27 @@ pub fn serialize_operation(operation: &Operation) -> proto::Operation {
lamport_timestamp: lamport_timestamp.value, lamport_timestamp: lamport_timestamp.value,
diagnostics: serialize_diagnostics(diagnostics.iter()), diagnostics: serialize_diagnostics(diagnostics.iter()),
}), }),
Operation::UpdateCompletionTriggers { triggers } => { Operation::UpdateCompletionTriggers {
proto::operation::Variant::UpdateCompletionTriggers( triggers,
proto::operation::UpdateCompletionTriggers { lamport_timestamp,
triggers: triggers.clone(), } => 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 { 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 { proto::operation::Edit {
replica_id: operation.timestamp.replica_id as u32, replica_id: operation.timestamp.replica_id as u32,
local_timestamp: operation.timestamp.local, local_timestamp: operation.timestamp.local,
lamport_timestamp: operation.timestamp.lamport, lamport_timestamp: operation.timestamp.lamport,
version: From::from(&operation.version), version: From::from(&operation.version),
ranges, ranges: operation.ranges.iter().map(serialize_range).collect(),
new_text: operation.new_text.clone(), new_text: operation.new_text.clone(),
} }
} }
@ -208,11 +196,7 @@ pub fn deserialize_operation(message: proto::Operation) -> Result<Operation> {
) )
}) })
.collect(), .collect(),
ranges: undo ranges: undo.ranges.into_iter().map(deserialize_range).collect(),
.ranges
.into_iter()
.map(|r| FullOffset(r.start as usize)..FullOffset(r.end as usize))
.collect(),
version: undo.version.into(), version: undo.version.into(),
}, },
}), }),
@ -232,7 +216,6 @@ pub fn deserialize_operation(message: proto::Operation) -> Result<Operation> {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
Operation::UpdateSelections { Operation::UpdateSelections {
replica_id: message.replica_id as ReplicaId,
lamport_timestamp: clock::Lamport { lamport_timestamp: clock::Lamport {
replica_id: message.replica_id as ReplicaId, replica_id: message.replica_id as ReplicaId,
value: message.lamport_timestamp, value: message.lamport_timestamp,
@ -250,6 +233,10 @@ pub fn deserialize_operation(message: proto::Operation) -> Result<Operation> {
proto::operation::Variant::UpdateCompletionTriggers(message) => { proto::operation::Variant::UpdateCompletionTriggers(message) => {
Operation::UpdateCompletionTriggers { Operation::UpdateCompletionTriggers {
triggers: message.triggers, 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 { 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 { EditOperation {
timestamp: InsertionTimestamp { timestamp: InsertionTimestamp {
replica_id: edit.replica_id as ReplicaId, 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, lamport: edit.lamport_timestamp,
}, },
version: edit.version.into(), version: edit.version.into(),
ranges, ranges: edit.ranges.into_iter().map(deserialize_range).collect(),
new_text: edit.new_text, 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 { proto::Completion {
old_start: Some(serialize_anchor(&completion.old_range.start)), old_start: Some(serialize_anchor(&completion.old_range.start)),
old_end: Some(serialize_anchor(&completion.old_range.end)), 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( pub fn deserialize_completion(
completion: proto::Completion, completion: proto::Completion,
language: Option<&Arc<Language>>, language: Option<&Arc<Language>>,
) -> Result<Completion<Anchor>> { ) -> Result<Completion> {
let old_start = completion let old_start = completion
.old_start .old_start
.and_then(deserialize_anchor) .and_then(deserialize_anchor)
@ -411,3 +425,89 @@ pub fn deserialize_completion(
lsp_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)
}

View file

@ -557,7 +557,7 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppConte
#[gpui::test] #[gpui::test]
async fn test_diagnostics(mut cx: gpui::TestAppContext) { 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(); let mut rust_lang = rust_lang();
rust_lang.config.language_server = Some(LanguageServerConfig { rust_lang.config.language_server = Some(LanguageServerConfig {
disk_based_diagnostic_sources: HashSet::from_iter(["disk".to_string()]), disk_based_diagnostic_sources: HashSet::from_iter(["disk".to_string()]),
@ -572,7 +572,7 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
.unindent(); .unindent();
let buffer = cx.add_model(|cx| { 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(Arc::new(rust_lang), cx)
.with_language_server(language_server, 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. // Receive diagnostics for an earlier version of the buffer.
buffer buffer
.update_diagnostics( .update_diagnostics(
Some(open_notification.text_document.version),
vec![ vec![
DiagnosticEntry { DiagnosticEntry {
range: PointUtf16::new(0, 9)..PointUtf16::new(0, 10), 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, cx,
) )
.unwrap(); .unwrap();
@ -687,7 +687,6 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
// Ensure overlapping diagnostics are highlighted correctly. // Ensure overlapping diagnostics are highlighted correctly.
buffer buffer
.update_diagnostics( .update_diagnostics(
Some(open_notification.text_document.version),
vec![ vec![
DiagnosticEntry { DiagnosticEntry {
range: PointUtf16::new(0, 9)..PointUtf16::new(0, 10), 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, cx,
) )
.unwrap(); .unwrap();
@ -777,7 +777,6 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
buffer.update(&mut cx, |buffer, cx| { buffer.update(&mut cx, |buffer, cx| {
buffer buffer
.update_diagnostics( .update_diagnostics(
Some(change_notification_2.text_document.version),
vec![ vec![
DiagnosticEntry { DiagnosticEntry {
range: PointUtf16::new(1, 9)..PointUtf16::new(1, 11), 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, cx,
) )
.unwrap(); .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] #[gpui::test]
async fn test_empty_diagnostic_ranges(mut cx: gpui::TestAppContext) { async fn test_empty_diagnostic_ranges(mut cx: gpui::TestAppContext) {
cx.add_model(|cx| { 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.set_language(Some(Arc::new(rust_lang())), cx);
buffer buffer
.update_diagnostics( .update_diagnostics(
None,
vec![ vec![
DiagnosticEntry { DiagnosticEntry {
range: PointUtf16::new(0, 10)..PointUtf16::new(0, 10), 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, cx,
) )
.unwrap(); .unwrap();
@ -1073,15 +1290,15 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) {
for buffer in &buffers { for buffer in &buffers {
let buffer = buffer.read(cx).snapshot(); let buffer = buffer.read(cx).snapshot();
let actual_remote_selections = buffer
.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 let expected_remote_selections = active_selections
.iter() .iter()
.filter(|(replica_id, _)| **replica_id != buffer.replica_id()) .filter(|(replica_id, _)| **replica_id != buffer.replica_id())
.map(|(replica_id, selections)| (*replica_id, selections.iter().collect::<Vec<_>>())) .map(|(replica_id, selections)| (*replica_id, selections.iter().collect::<Vec<_>>()))
.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); assert_eq!(actual_remote_selections, expected_remote_selections);
} }
} }

View file

@ -27,5 +27,6 @@ smol = "1.2"
gpui = { path = "../gpui", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] } util = { path = "../util", features = ["test-support"] }
async-pipe = { git = "https://github.com/routerify/async-pipe-rs", rev = "feeb77e83142a9ff837d0767652ae41bfc5d8e47" } 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" unindent = "0.1.7"

View file

@ -56,6 +56,18 @@ struct Request<'a, T> {
params: 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)] #[derive(Serialize, Deserialize)]
struct AnyResponse<'a> { struct AnyResponse<'a> {
id: usize, id: usize,
@ -238,6 +250,21 @@ impl LanguageServer {
link_support: Some(true), link_support: Some(true),
..Default::default() ..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: Some(CompletionClientCapabilities {
completion_item: Some(CompletionItemCapability { completion_item: Some(CompletionItemCapability {
snippet_support: Some(true), snippet_support: Some(true),
@ -454,48 +481,41 @@ impl Drop for Subscription {
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
pub struct FakeLanguageServer { pub struct FakeLanguageServer {
buffer: Vec<u8>, handlers: Arc<
stdin: smol::io::BufReader<async_pipe::PipeReader>, Mutex<
stdout: smol::io::BufWriter<async_pipe::PipeWriter>, 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>, 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"))] #[cfg(any(test, feature = "test-support"))]
impl LanguageServer { impl LanguageServer {
pub async fn fake(executor: Arc<executor::Background>) -> (Arc<Self>, FakeLanguageServer) { pub async fn fake(cx: &gpui::TestAppContext) -> (Arc<Self>, FakeLanguageServer) {
Self::fake_with_capabilities(Default::default(), executor).await Self::fake_with_capabilities(Default::default(), cx).await
} }
pub async fn fake_with_capabilities( pub async fn fake_with_capabilities(
capabilities: ServerCapabilities, capabilities: ServerCapabilities,
executor: Arc<executor::Background>, cx: &gpui::TestAppContext,
) -> (Arc<Self>, FakeLanguageServer) { ) -> (Arc<Self>, FakeLanguageServer) {
let stdin = async_pipe::pipe(); let (stdin_writer, stdin_reader) = async_pipe::pipe();
let stdout = async_pipe::pipe(); let (stdout_writer, stdout_reader) = 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 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; let server =
fake.respond( Self::new_internal(stdin_writer, stdout_reader, Path::new("/"), cx.background())
init_id, .unwrap();
InitializeResult {
capabilities,
..Default::default()
},
)
.await;
fake.receive_notification::<notification::Initialized>() fake.receive_notification::<notification::Initialized>()
.await; .await;
@ -505,6 +525,75 @@ impl LanguageServer {
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
impl FakeLanguageServer { 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, &notification).await;
}
})
.detach();
this
}
pub async fn notify<T: notification::Notification>(&mut self, params: T::Params) { pub async fn notify<T: notification::Notification>(&mut self, params: T::Params) {
if !self.started.load(std::sync::atomic::Ordering::SeqCst) { if !self.started.load(std::sync::atomic::Ordering::SeqCst) {
panic!("can't simulate an LSP notification before the server has been started"); panic!("can't simulate an LSP notification before the server has been started");
@ -515,51 +604,53 @@ impl FakeLanguageServer {
params, params,
}) })
.unwrap(); .unwrap();
self.send(message).await; self.outgoing_tx.send(message).await.unwrap();
} }
pub async fn respond<'a, T: request::Request>( pub async fn receive_notification<T: notification::Notification>(&mut self) -> T::Params {
&mut self, use futures::StreamExt as _;
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_request<T: request::Request>(&mut self) -> (RequestId<T>, T::Params) {
loop { loop {
self.receive().await; let bytes = self.incoming_rx.next().await.unwrap();
if let Ok(request) = serde_json::from_slice::<Request<T::Params>>(&self.buffer) { if let Ok(notification) = serde_json::from_slice::<Notification<T::Params>>(&bytes) {
assert_eq!(request.method, T::METHOD); assert_eq!(notification.method, T::METHOD);
assert_eq!(request.jsonrpc, JSON_RPC_VERSION); return notification.params;
return (
RequestId {
id: request.id,
_type: std::marker::PhantomData,
},
request.params,
);
} else { } else {
log::info!( log::info!(
"skipping message in fake language server {:?}", "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 { pub fn handle_request<T, F>(&mut self, handler: F) -> barrier::Receiver
self.receive().await; where
let notification = serde_json::from_slice::<Notification<T::Params>>(&self.buffer).unwrap(); T: 'static + request::Request,
assert_eq!(notification.method, T::METHOD); F: 'static + Send + FnOnce(T::Params) -> T::Result,
notification.params {
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>) { pub async fn start_progress(&mut self, token: impl Into<String>) {
@ -578,39 +669,37 @@ impl FakeLanguageServer {
.await; .await;
} }
async fn send(&mut self, message: Vec<u8>) { async fn send(stdout: &mut smol::io::BufWriter<async_pipe::PipeWriter>, message: &[u8]) {
self.stdout stdout
.write_all(CONTENT_LEN_HEADER.as_bytes()) .write_all(CONTENT_LEN_HEADER.as_bytes())
.await .await
.unwrap(); .unwrap();
self.stdout stdout
.write_all((format!("{}", message.len())).as_bytes()) .write_all((format!("{}", message.len())).as_bytes())
.await .await
.unwrap(); .unwrap();
self.stdout.write_all("\r\n\r\n".as_bytes()).await.unwrap(); stdout.write_all("\r\n\r\n".as_bytes()).await.unwrap();
self.stdout.write_all(&message).await.unwrap(); stdout.write_all(&message).await.unwrap();
self.stdout.flush().await.unwrap(); stdout.flush().await.unwrap();
} }
async fn receive(&mut self) { async fn receive(
self.buffer.clear(); stdin: &mut smol::io::BufReader<async_pipe::PipeReader>,
self.stdin buffer: &mut Vec<u8>,
.read_until(b'\n', &mut self.buffer) ) -> Result<()> {
.await buffer.clear();
.unwrap(); stdin.read_until(b'\n', buffer).await?;
self.stdin stdin.read_until(b'\n', buffer).await?;
.read_until(b'\n', &mut self.buffer) let message_len: usize = std::str::from_utf8(buffer)
.await
.unwrap();
let message_len: usize = std::str::from_utf8(&self.buffer)
.unwrap() .unwrap()
.strip_prefix(CONTENT_LEN_HEADER) .strip_prefix(CONTENT_LEN_HEADER)
.unwrap() .unwrap()
.trim_end() .trim_end()
.parse() .parse()
.unwrap(); .unwrap();
self.buffer.resize(message_len, 0); buffer.resize(message_len, 0);
self.stdin.read_exact(&mut self.buffer).await.unwrap(); stdin.read_exact(buffer).await?;
Ok(())
} }
} }
@ -618,10 +707,16 @@ impl FakeLanguageServer {
mod tests { mod tests {
use super::*; use super::*;
use gpui::TestAppContext; use gpui::TestAppContext;
use simplelog::SimpleLogger;
use unindent::Unindent; use unindent::Unindent;
use util::test::temp_tree; use util::test::temp_tree;
#[ctor::ctor]
fn init_logger() {
if std::env::var("RUST_LOG").is_ok() {
env_logger::init();
}
}
#[gpui::test] #[gpui::test]
async fn test_rust_analyzer(cx: TestAppContext) { async fn test_rust_analyzer(cx: TestAppContext) {
let lib_source = r#" 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 lib_file_uri = Url::from_file_path(root_dir.path().join("src/lib.rs")).unwrap();
let server = cx.read(|cx| { let server =
LanguageServer::new( LanguageServer::new(Path::new("rust-analyzer"), root_dir.path(), cx.background())
Path::new("rust-analyzer"), .unwrap();
root_dir.path(),
cx.background().clone(),
)
.unwrap()
});
server.next_idle_notification().await; server.next_idle_notification().await;
server server
@ -687,9 +777,7 @@ mod tests {
#[gpui::test] #[gpui::test]
async fn test_fake(cx: TestAppContext) { async fn test_fake(cx: TestAppContext) {
SimpleLogger::init(log::LevelFilter::Info, Default::default()).unwrap(); let (server, mut fake) = LanguageServer::fake(&cx).await;
let (server, mut fake) = LanguageServer::fake(cx.background()).await;
let (message_tx, message_rx) = channel::unbounded(); let (message_tx, message_rx) = channel::unbounded();
let (diagnostics_tx, diagnostics_rx) = channel::unbounded(); let (diagnostics_tx, diagnostics_rx) = channel::unbounded();
@ -741,9 +829,9 @@ mod tests {
"file://b/c" "file://b/c"
); );
fake.handle_request::<request::Shutdown, _>(|_| ());
drop(server); drop(server);
let (shutdown_request, _) = fake.receive_request::<request::Shutdown>().await;
fake.respond(shutdown_request, ()).await;
fake.receive_notification::<notification::Exit>().await; fake.receive_notification::<notification::Exit>().await;
} }

View file

@ -7,7 +7,11 @@ edition = "2021"
path = "src/project.rs" path = "src/project.rs"
[features] [features]
test-support = ["language/test-support", "text/test-support"] test-support = [
"client/test-support",
"language/test-support",
"text/test-support",
]
[dependencies] [dependencies]
text = { path = "../text" } text = { path = "../text" }
@ -45,6 +49,5 @@ lsp = { path = "../lsp", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] } util = { path = "../util", features = ["test-support"] }
rpc = { path = "../rpc", features = ["test-support"] } rpc = { path = "../rpc", features = ["test-support"] }
rand = "0.8.3" rand = "0.8.3"
simplelog = "0.9"
tempdir = { version = "0.3.7" } tempdir = { version = "0.3.7" }
unindent = "0.1.7" unindent = "0.1.7"

View file

@ -13,6 +13,11 @@ use text::Rope;
#[async_trait::async_trait] #[async_trait::async_trait]
pub trait Fs: Send + Sync { 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 load(&self, path: &Path) -> Result<String>;
async fn save(&self, path: &Path, text: &Rope) -> Result<()>; async fn save(&self, path: &Path, text: &Rope) -> Result<()>;
async fn canonicalize(&self, path: &Path) -> Result<PathBuf>; async fn canonicalize(&self, path: &Path) -> Result<PathBuf>;
@ -32,6 +37,24 @@ pub trait Fs: Send + Sync {
fn as_fake(&self) -> &FakeFs; 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)] #[derive(Clone, Debug)]
pub struct Metadata { pub struct Metadata {
pub inode: u64, pub inode: u64,
@ -44,6 +67,60 @@ pub struct RealFs;
#[async_trait::async_trait] #[async_trait::async_trait]
impl Fs for RealFs { 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> { async fn load(&self, path: &Path) -> Result<String> {
let mut file = smol::fs::File::open(path).await?; let mut file = smol::fs::File::open(path).await?;
let mut text = String::new(); 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 _; use postage::prelude::Sink as _;
let events = paths let events = paths
.iter() .into_iter()
.map(|path| fsevent::Event { .map(|path| fsevent::Event {
event_id: 0, event_id: 0,
flags: fsevent::StreamFlags::empty(), flags: fsevent::StreamFlags::empty(),
path: path.to_path_buf(), path: path.into(),
}) })
.collect(); .collect();
@ -292,46 +373,163 @@ impl FakeFs {
} }
.boxed() .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"))] #[cfg(any(test, feature = "test-support"))]
#[async_trait::async_trait] #[async_trait::async_trait]
impl Fs for FakeFs { 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> { async fn load(&self, path: &Path) -> Result<String> {
self.executor.simulate_random_delay().await; self.executor.simulate_random_delay().await;
let state = self.state.lock().await; let state = self.state.lock().await;

File diff suppressed because it is too large Load diff

View file

@ -14,9 +14,7 @@ use gpui::{
executor, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, executor, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext,
Task, Task,
}; };
use language::{ use language::{Buffer, DiagnosticEntry, Operation, PointUtf16, Rope};
Anchor, Buffer, Completion, DiagnosticEntry, Language, Operation, PointUtf16, Rope,
};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use parking_lot::Mutex; use parking_lot::Mutex;
use postage::{ use postage::{
@ -293,7 +291,7 @@ impl Worktree {
let this = worktree_handle.downgrade(); let this = worktree_handle.downgrade();
cx.spawn(|mut cx| async move { cx.spawn(|mut cx| async move {
while let Some(_) = snapshot_rx.recv().await { 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)); this.update(&mut cx, |this, cx| this.poll_snapshot(cx));
} else { } else {
break; break;
@ -518,7 +516,7 @@ impl LocalWorktree {
cx.spawn_weak(|this, mut cx| async move { cx.spawn_weak(|this, mut cx| async move {
while let Ok(scan_state) = scan_states_rx.recv().await { 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| { let to_send = handle.update(&mut cx, |this, cx| {
last_scan_state_tx.blocking_send(scan_state).ok(); last_scan_state_tx.blocking_send(scan_state).ok();
this.poll_snapshot(cx); this.poll_snapshot(cx);
@ -820,7 +818,7 @@ impl RemoteWorktree {
) -> Result<()> { ) -> Result<()> {
let mut tx = self.updates_tx.clone(); let mut tx = self.updates_tx.clone();
let payload = envelope.payload.clone(); let payload = envelope.payload.clone();
cx.background() cx.foreground()
.spawn(async move { .spawn(async move {
tx.send(payload).await.expect("receiver runs to completion"); 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) { fn buffer_updated(&self, buffer_id: u64, operation: Operation, cx: &mut MutableAppContext) {
self.worktree.update(cx, |worktree, cx| { self.worktree.update(cx, |worktree, cx| {
worktree.send_buffer_update(buffer_id, operation, cx); worktree.send_buffer_update(buffer_id, operation, cx);
@ -2216,7 +2124,7 @@ struct UpdateIgnoreStatusJob {
} }
pub trait WorktreeHandle { pub trait WorktreeHandle {
#[cfg(test)] #[cfg(any(test, feature = "test-support"))]
fn flush_fs_events<'a>( fn flush_fs_events<'a>(
&self, &self,
cx: &'a gpui::TestAppContext, 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, // 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. // to ensure that all redundant FS events have already been processed.
#[cfg(test)] #[cfg(any(test, feature = "test-support"))]
fn flush_fs_events<'a>( fn flush_fs_events<'a>(
&self, &self,
cx: &'a gpui::TestAppContext, cx: &'a gpui::TestAppContext,
@ -2238,14 +2146,22 @@ impl WorktreeHandle for ModelHandle<Worktree> {
use smol::future::FutureExt; use smol::future::FutureExt;
let filename = "fs-event-sentinel"; 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 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 { 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()) tree.condition(&cx, |tree, _| tree.entry_for_path(filename).is_some())
.await; .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()) tree.condition(&cx, |tree, _| tree.entry_for_path(filename).is_none())
.await; .await;

View file

@ -39,27 +39,32 @@ message Envelope {
SaveBuffer save_buffer = 31; SaveBuffer save_buffer = 31;
BufferSaved buffer_saved = 32; BufferSaved buffer_saved = 32;
BufferReloaded buffer_reloaded = 33; BufferReloaded buffer_reloaded = 33;
FormatBuffer format_buffer = 34; FormatBuffers format_buffers = 34;
GetCompletions get_completions = 35; FormatBuffersResponse format_buffers_response = 35;
GetCompletionsResponse get_completions_response = 36; GetCompletions get_completions = 36;
ApplyCompletionAdditionalEdits apply_completion_additional_edits = 37; GetCompletionsResponse get_completions_response = 37;
ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 38; 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; GetChannels get_channels = 44;
GetChannelsResponse get_channels_response = 40; GetChannelsResponse get_channels_response = 45;
JoinChannel join_channel = 41; JoinChannel join_channel = 46;
JoinChannelResponse join_channel_response = 42; JoinChannelResponse join_channel_response = 47;
LeaveChannel leave_channel = 43; LeaveChannel leave_channel = 48;
SendChannelMessage send_channel_message = 44; SendChannelMessage send_channel_message = 49;
SendChannelMessageResponse send_channel_message_response = 45; SendChannelMessageResponse send_channel_message_response = 50;
ChannelMessageSent channel_message_sent = 46; ChannelMessageSent channel_message_sent = 51;
GetChannelMessages get_channel_messages = 47; GetChannelMessages get_channel_messages = 52;
GetChannelMessagesResponse get_channel_messages_response = 48; GetChannelMessagesResponse get_channel_messages_response = 53;
UpdateContacts update_contacts = 49; UpdateContacts update_contacts = 54;
GetUsers get_users = 50; GetUsers get_users = 55;
GetUsersResponse get_users_response = 51; GetUsersResponse get_users_response = 56;
} }
} }
@ -202,9 +207,13 @@ message BufferReloaded {
Timestamp mtime = 4; Timestamp mtime = 4;
} }
message FormatBuffer { message FormatBuffers {
uint64 project_id = 1; uint64 project_id = 1;
uint64 buffer_id = 2; repeated uint64 buffer_ids = 2;
}
message FormatBuffersResponse {
ProjectTransaction transaction = 1;
} }
message GetCompletions { message GetCompletions {
@ -224,12 +233,7 @@ message ApplyCompletionAdditionalEdits {
} }
message ApplyCompletionAdditionalEditsResponse { message ApplyCompletionAdditionalEditsResponse {
repeated AdditionalEdit additional_edits = 1; Transaction transaction = 1;
}
message AdditionalEdit {
uint32 replica_id = 1;
uint32 local_timestamp = 2;
} }
message Completion { message Completion {
@ -239,6 +243,51 @@ message Completion {
bytes lsp_completion = 4; 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 { message UpdateDiagnosticSummary {
uint64 project_id = 1; uint64 project_id = 1;
uint64 worktree_id = 2; uint64 worktree_id = 2;
@ -366,16 +415,11 @@ message Buffer {
message BufferState { message BufferState {
uint64 id = 1; uint64 id = 1;
optional File file = 2; optional File file = 2;
string visible_text = 3; string base_text = 3;
string deleted_text = 4; repeated Operation operations = 4;
repeated BufferFragment fragments = 5; repeated SelectionSet selections = 5;
repeated UndoMapEntry undo_map = 6; repeated Diagnostic diagnostics = 6;
repeated VectorClockEntry version = 7; repeated string completion_triggers = 7;
repeated SelectionSet selections = 8;
repeated Diagnostic diagnostics = 9;
uint32 lamport_timestamp = 10;
repeated Operation deferred_operations = 11;
repeated string completion_triggers = 12;
} }
message BufferFragment { message BufferFragment {
@ -474,7 +518,9 @@ message Operation {
} }
message UpdateCompletionTriggers { message UpdateCompletionTriggers {
repeated string triggers = 1; uint32 replica_id = 1;
uint32 lamport_timestamp = 2;
repeated string triggers = 3;
} }
} }

View file

@ -179,7 +179,16 @@ impl Peer {
let channel = response_channels.lock().as_mut()?.remove(&responding_to); let channel = response_channels.lock().as_mut()?.remove(&responding_to);
if let Some(mut tx) = channel { if let Some(mut tx) = channel {
let mut requester_resumed = barrier::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; requester_resumed.1.recv().await;
} else { } else {
log::warn!("received RPC response to unknown request {}", responding_to); log::warn!("received RPC response to unknown request {}", responding_to);
@ -337,7 +346,7 @@ mod tests {
use async_tungstenite::tungstenite::Message as WebSocketMessage; use async_tungstenite::tungstenite::Message as WebSocketMessage;
use gpui::TestAppContext; use gpui::TestAppContext;
#[gpui::test(iterations = 10)] #[gpui::test(iterations = 50)]
async fn test_request_response(cx: TestAppContext) { async fn test_request_response(cx: TestAppContext) {
let executor = cx.foreground(); 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) { async fn test_order_of_response_and_incoming(cx: TestAppContext) {
let executor = cx.foreground(); let executor = cx.foreground();
let server = Peer::new(); 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) { async fn test_disconnect(cx: TestAppContext) {
let executor = cx.foreground(); let executor = cx.foreground();
@ -611,7 +732,7 @@ mod tests {
.is_err()); .is_err());
} }
#[gpui::test(iterations = 10)] #[gpui::test(iterations = 50)]
async fn test_io_error(cx: TestAppContext) { async fn test_io_error(cx: TestAppContext) {
let executor = cx.foreground(); let executor = cx.foreground();
let (client_conn, mut server_conn, _) = Connection::in_memory(cx.background()); let (client_conn, mut server_conn, _) = Connection::in_memory(cx.background());

View file

@ -122,6 +122,8 @@ macro_rules! entity_messages {
messages!( messages!(
Ack, Ack,
AddProjectCollaborator, AddProjectCollaborator,
ApplyCodeAction,
ApplyCodeActionResponse,
ApplyCompletionAdditionalEdits, ApplyCompletionAdditionalEdits,
ApplyCompletionAdditionalEditsResponse, ApplyCompletionAdditionalEditsResponse,
BufferReloaded, BufferReloaded,
@ -131,11 +133,14 @@ messages!(
DiskBasedDiagnosticsUpdated, DiskBasedDiagnosticsUpdated,
DiskBasedDiagnosticsUpdating, DiskBasedDiagnosticsUpdating,
Error, Error,
FormatBuffer, FormatBuffers,
FormatBuffersResponse,
GetChannelMessages, GetChannelMessages,
GetChannelMessagesResponse, GetChannelMessagesResponse,
GetChannels, GetChannels,
GetChannelsResponse, GetChannelsResponse,
GetCodeActions,
GetCodeActionsResponse,
GetCompletions, GetCompletions,
GetCompletionsResponse, GetCompletionsResponse,
GetDefinition, GetDefinition,
@ -171,13 +176,15 @@ messages!(
); );
request_messages!( request_messages!(
(ApplyCodeAction, ApplyCodeActionResponse),
( (
ApplyCompletionAdditionalEdits, ApplyCompletionAdditionalEdits,
ApplyCompletionAdditionalEditsResponse ApplyCompletionAdditionalEditsResponse
), ),
(FormatBuffer, Ack), (FormatBuffers, FormatBuffersResponse),
(GetChannelMessages, GetChannelMessagesResponse), (GetChannelMessages, GetChannelMessagesResponse),
(GetChannels, GetChannelsResponse), (GetChannels, GetChannelsResponse),
(GetCodeActions, GetCodeActionsResponse),
(GetCompletions, GetCompletionsResponse), (GetCompletions, GetCompletionsResponse),
(GetDefinition, GetDefinitionResponse), (GetDefinition, GetDefinitionResponse),
(GetUsers, GetUsersResponse), (GetUsers, GetUsersResponse),
@ -197,13 +204,15 @@ request_messages!(
entity_messages!( entity_messages!(
project_id, project_id,
AddProjectCollaborator, AddProjectCollaborator,
ApplyCodeAction,
ApplyCompletionAdditionalEdits, ApplyCompletionAdditionalEdits,
BufferReloaded, BufferReloaded,
BufferSaved, BufferSaved,
CloseBuffer, CloseBuffer,
DiskBasedDiagnosticsUpdated, DiskBasedDiagnosticsUpdated,
DiskBasedDiagnosticsUpdating, DiskBasedDiagnosticsUpdating,
FormatBuffer, FormatBuffers,
GetCodeActions,
GetCompletions, GetCompletions,
GetDefinition, GetDefinition,
JoinProject, JoinProject,

View file

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

File diff suppressed because it is too large Load diff

View file

@ -122,10 +122,10 @@ impl Store {
let mut result = RemovedConnectionState::default(); let mut result = RemovedConnectionState::default();
for project_id in connection.projects.clone() { 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.contact_ids.extend(project.authorized_user_ids());
result.hosted_projects.insert(project_id, project); 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 result
.guest_project_ids .guest_project_ids
.insert(project_id, project.connection_ids); .insert(project_id, project.connection_ids);
@ -254,9 +254,14 @@ impl Store {
&mut self, &mut self,
project_id: u64, project_id: u64,
worktree_id: u64, worktree_id: u64,
connection_id: ConnectionId,
worktree: Worktree, worktree: Worktree,
) -> bool { ) -> tide::Result<()> {
if let Some(project) = self.projects.get_mut(&project_id) { 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 { for authorized_user_id in &worktree.authorized_user_ids {
self.visible_projects_by_user_id self.visible_projects_by_user_id
.entry(*authorized_user_id) .entry(*authorized_user_id)
@ -270,9 +275,9 @@ impl Store {
#[cfg(test)] #[cfg(test)]
self.check_invariants(); self.check_invariants();
true Ok(())
} else { } else {
false Err(anyhow!("no such project"))?
} }
} }
@ -280,7 +285,7 @@ impl Store {
&mut self, &mut self,
project_id: u64, project_id: u64,
connection_id: ConnectionId, connection_id: ConnectionId,
) -> Option<Project> { ) -> tide::Result<Project> {
match self.projects.entry(project_id) { match self.projects.entry(project_id) {
hash_map::Entry::Occupied(e) => { hash_map::Entry::Occupied(e) => {
if e.get().host_connection_id == connection_id { if e.get().host_connection_id == connection_id {
@ -292,12 +297,12 @@ impl Store {
} }
} }
Some(e.remove()) Ok(e.remove())
} else { } 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, connection_id: ConnectionId,
entries: HashMap<u64, proto::Entry>, entries: HashMap<u64, proto::Entry>,
diagnostic_summaries: BTreeMap<PathBuf, proto::DiagnosticSummary>, diagnostic_summaries: BTreeMap<PathBuf, proto::DiagnosticSummary>,
) -> Option<SharedWorktree> { ) -> tide::Result<SharedWorktree> {
let project = self.projects.get_mut(&project_id)?; let project = self
let worktree = project.worktrees.get_mut(&worktree_id)?; .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() { if project.host_connection_id == connection_id && project.share.is_some() {
worktree.share = Some(WorktreeShare { worktree.share = Some(WorktreeShare {
entries, entries,
diagnostic_summaries, diagnostic_summaries,
}); });
Some(SharedWorktree { Ok(SharedWorktree {
authorized_user_ids: project.authorized_user_ids(), authorized_user_ids: project.authorized_user_ids(),
connection_ids: project.guest_connection_ids(), connection_ids: project.guest_connection_ids(),
}) })
} else { } else {
None Err(anyhow!("no such worktree"))?
} }
} }
@ -421,19 +432,25 @@ impl Store {
worktree_id: u64, worktree_id: u64,
connection_id: ConnectionId, connection_id: ConnectionId,
summary: proto::DiagnosticSummary, summary: proto::DiagnosticSummary,
) -> Option<Vec<ConnectionId>> { ) -> tide::Result<Vec<ConnectionId>> {
let project = self.projects.get_mut(&project_id)?; let project = self
let worktree = project.worktrees.get_mut(&worktree_id)?; .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 project.host_connection_id == connection_id {
if let Some(share) = worktree.share.as_mut() { if let Some(share) = worktree.share.as_mut() {
share share
.diagnostic_summaries .diagnostic_summaries
.insert(summary.path.clone().into(), summary); .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( pub fn join_project(
@ -481,10 +498,19 @@ impl Store {
&mut self, &mut self,
connection_id: ConnectionId, connection_id: ConnectionId,
project_id: u64, project_id: u64,
) -> Option<LeftProject> { ) -> tide::Result<LeftProject> {
let project = self.projects.get_mut(&project_id)?; let project = self
let share = project.share.as_mut()?; .projects
let (replica_id, _) = share.guests.remove(&connection_id)?; .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); share.active_replica_ids.remove(&replica_id);
if let Some(connection) = self.connections.get_mut(&connection_id) { if let Some(connection) = self.connections.get_mut(&connection_id) {
@ -497,7 +523,7 @@ impl Store {
#[cfg(test)] #[cfg(test)]
self.check_invariants(); self.check_invariants();
Some(LeftProject { Ok(LeftProject {
connection_ids, connection_ids,
authorized_user_ids, authorized_user_ids,
}) })
@ -510,31 +536,40 @@ impl Store {
worktree_id: u64, worktree_id: u64,
removed_entries: &[u64], removed_entries: &[u64],
updated_entries: &[proto::Entry], updated_entries: &[proto::Entry],
) -> Option<Vec<ConnectionId>> { ) -> tide::Result<Vec<ConnectionId>> {
let project = self.write_project(project_id, connection_id)?; 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 { for entry_id in removed_entries {
share.entries.remove(&entry_id); share.entries.remove(&entry_id);
} }
for entry in updated_entries { for entry in updated_entries {
share.entries.insert(entry.id, entry.clone()); share.entries.insert(entry.id, entry.clone());
} }
Some(project.connection_ids()) Ok(project.connection_ids())
} }
pub fn project_connection_ids( pub fn project_connection_ids(
&self, &self,
project_id: u64, project_id: u64,
acting_connection_id: ConnectionId, acting_connection_id: ConnectionId,
) -> Option<Vec<ConnectionId>> { ) -> tide::Result<Vec<ConnectionId>> {
Some( Ok(self
self.read_project(project_id, acting_connection_id)? .read_project(project_id, acting_connection_id)?
.connection_ids(), .connection_ids())
)
} }
pub fn channel_connection_ids(&self, channel_id: ChannelId) -> Option<Vec<ConnectionId>> { pub fn channel_connection_ids(&self, channel_id: ChannelId) -> tide::Result<Vec<ConnectionId>> {
Some(self.channels.get(&channel_id)?.connection_ids()) Ok(self
.channels
.get(&channel_id)
.ok_or_else(|| anyhow!("no such channel"))?
.connection_ids())
} }
#[cfg(test)] #[cfg(test)]
@ -542,14 +577,26 @@ impl Store {
self.projects.get(&project_id) self.projects.get(&project_id)
} }
pub fn read_project(&self, project_id: u64, connection_id: ConnectionId) -> Option<&Project> { pub fn read_project(
let project = self.projects.get(&project_id)?; &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 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 { } else {
None Err(anyhow!("no such project"))?
} }
} }
@ -557,14 +604,22 @@ impl Store {
&mut self, &mut self,
project_id: u64, project_id: u64,
connection_id: ConnectionId, connection_id: ConnectionId,
) -> Option<&mut Project> { ) -> tide::Result<&mut Project> {
let project = self.projects.get_mut(&project_id)?; let project = self
.projects
.get_mut(&project_id)
.ok_or_else(|| anyhow!("no such project"))?;
if project.host_connection_id == connection_id 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 { } else {
None Err(anyhow!("no such project"))?
} }
} }

View file

@ -1,5 +1,5 @@
use super::{Point, ToOffset}; use super::{Point, ToOffset};
use crate::{rope::TextDimension, BufferSnapshot}; use crate::{rope::TextDimension, BufferSnapshot, PointUtf16, ToPointUtf16};
use anyhow::Result; use anyhow::Result;
use std::{cmp::Ordering, fmt::Debug, ops::Range}; use std::{cmp::Ordering, fmt::Debug, ops::Range};
use sum_tree::Bias; use sum_tree::Bias;
@ -78,6 +78,7 @@ pub trait AnchorRangeExt {
fn cmp(&self, b: &Range<Anchor>, buffer: &BufferSnapshot) -> Result<Ordering>; fn cmp(&self, b: &Range<Anchor>, buffer: &BufferSnapshot) -> Result<Ordering>;
fn to_offset(&self, content: &BufferSnapshot) -> Range<usize>; fn to_offset(&self, content: &BufferSnapshot) -> Range<usize>;
fn to_point(&self, content: &BufferSnapshot) -> Range<Point>; fn to_point(&self, content: &BufferSnapshot) -> Range<Point>;
fn to_point_utf16(&self, content: &BufferSnapshot) -> Range<PointUtf16>;
} }
impl AnchorRangeExt for Range<Anchor> { impl AnchorRangeExt for Range<Anchor> {
@ -95,4 +96,8 @@ impl AnchorRangeExt for Range<Anchor> {
fn to_point(&self, content: &BufferSnapshot) -> Range<Point> { fn to_point(&self, content: &BufferSnapshot) -> Range<Point> {
self.start.summary::<Point>(&content)..self.end.summary::<Point>(&content) 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)
}
} }

View file

@ -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 { pub fn point_to_offset(&self, point: Point) -> usize {
if point >= self.summary().lines { if point >= self.summary().lines {
return self.summary().bytes; return self.summary().bytes;
@ -580,6 +593,27 @@ impl Chunk {
offset 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 { fn point_utf16_to_offset(&self, target: PointUtf16) -> usize {
let mut offset = 0; let mut offset = 0;
let mut point = PointUtf16::new(0, 0); let mut point = PointUtf16::new(0, 0);

View file

@ -432,28 +432,28 @@ fn test_undo_redo() {
buffer.edit(vec![3..5], "cd"); buffer.edit(vec![3..5], "cd");
assert_eq!(buffer.text(), "1abcdef234"); assert_eq!(buffer.text(), "1abcdef234");
let transactions = buffer.history.undo_stack.clone(); let entries = buffer.history.undo_stack.clone();
assert_eq!(transactions.len(), 3); 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"); 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"); 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"); 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"); 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"); 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"); 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"); 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"); 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"); assert_eq!(buffer.text(), "1234");
} }
@ -502,7 +502,7 @@ fn test_history() {
} }
#[test] #[test]
fn test_avoid_grouping_next_transaction() { fn test_finalize_last_transaction() {
let now = Instant::now(); let now = Instant::now();
let mut buffer = Buffer::new(0, 0, History::new("123456".into())); 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); buffer.end_transaction_at(now);
assert_eq!(buffer.text(), "12cd56"); assert_eq!(buffer.text(), "12cd56");
buffer.avoid_grouping_next_transaction(); buffer.finalize_last_transaction();
buffer.start_transaction_at(now); buffer.start_transaction_at(now);
buffer.edit(vec![4..5], "e"); buffer.edit(vec![4..5], "e");
buffer.end_transaction_at(now).unwrap(); buffer.end_transaction_at(now).unwrap();
@ -536,6 +536,44 @@ fn test_avoid_grouping_next_transaction() {
assert_eq!(buffer.text(), "ab2cde6"); 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] #[test]
fn test_concurrent_edits() { fn test_concurrent_edits() {
let text = "abcdef"; let text = "abcdef";
@ -551,12 +589,12 @@ fn test_concurrent_edits() {
let buf3_op = buffer3.edit(vec![5..6], "56"); let buf3_op = buffer3.edit(vec![5..6], "56");
assert_eq!(buffer3.text(), "abcde56"); assert_eq!(buffer3.text(), "abcde56");
buffer1.apply_op(Operation::Edit(buf2_op.clone())).unwrap(); buffer1.apply_op(buf2_op.clone()).unwrap();
buffer1.apply_op(Operation::Edit(buf3_op.clone())).unwrap(); buffer1.apply_op(buf3_op.clone()).unwrap();
buffer2.apply_op(Operation::Edit(buf1_op.clone())).unwrap(); buffer2.apply_op(buf1_op.clone()).unwrap();
buffer2.apply_op(Operation::Edit(buf3_op.clone())).unwrap(); buffer2.apply_op(buf3_op.clone()).unwrap();
buffer3.apply_op(Operation::Edit(buf1_op.clone())).unwrap(); buffer3.apply_op(buf1_op.clone()).unwrap();
buffer3.apply_op(Operation::Edit(buf2_op.clone())).unwrap(); buffer3.apply_op(buf2_op.clone()).unwrap();
assert_eq!(buffer1.text(), "a12c34e56"); assert_eq!(buffer1.text(), "a12c34e56");
assert_eq!(buffer2.text(), "a12c34e56"); assert_eq!(buffer2.text(), "a12c34e56");

View file

@ -40,7 +40,7 @@ pub use subscription::*;
pub use sum_tree::Bias; pub use sum_tree::Bias;
use sum_tree::{FilterCursor, SumTree}; use sum_tree::{FilterCursor, SumTree};
pub type TransactionId = usize; pub type TransactionId = clock::Local;
pub struct Buffer { pub struct Buffer {
snapshot: BufferSnapshot, snapshot: BufferSnapshot,
@ -67,28 +67,37 @@ pub struct BufferSnapshot {
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Transaction { pub struct HistoryEntry {
id: TransactionId, transaction: Transaction,
start: clock::Global,
end: clock::Global,
edits: Vec<clock::Local>,
ranges: Vec<Range<FullOffset>>,
first_edit_at: Instant, first_edit_at: Instant,
last_edit_at: Instant, last_edit_at: Instant,
suppress_grouping: bool, 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) { fn push_edit(&mut self, edit: &EditOperation) {
self.edits.push(edit.timestamp.local()); self.transaction.edit_ids.push(edit.timestamp.local());
self.end.observe(edit.timestamp.local()); self.transaction.end.observe(edit.timestamp.local());
let mut other_ranges = edit.ranges.iter().peekable(); let mut other_ranges = edit.ranges.iter().peekable();
let mut new_ranges = Vec::new(); let mut new_ranges = Vec::new();
let insertion_len = edit.new_text.as_ref().map_or(0, |t| t.len()); let insertion_len = edit.new_text.as_ref().map_or(0, |t| t.len());
let mut delta = 0; 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.start += delta;
self_range.end += delta; self_range.end += delta;
@ -122,7 +131,7 @@ impl Transaction {
delta += insertion_len; delta += insertion_len;
} }
self.ranges = new_ranges; self.transaction.ranges = new_ranges;
} }
} }
@ -130,42 +139,46 @@ impl Transaction {
pub struct History { pub struct History {
// TODO: Turn this into a String or Rope, maybe. // TODO: Turn this into a String or Rope, maybe.
pub base_text: Arc<str>, pub base_text: Arc<str>,
ops: HashMap<clock::Local, EditOperation>, operations: HashMap<clock::Local, Operation>,
undo_stack: Vec<Transaction>, undo_stack: Vec<HistoryEntry>,
redo_stack: Vec<Transaction>, redo_stack: Vec<HistoryEntry>,
transaction_depth: usize, transaction_depth: usize,
group_interval: Duration, group_interval: Duration,
next_transaction_id: TransactionId,
} }
impl History { impl History {
pub fn new(base_text: Arc<str>) -> Self { pub fn new(base_text: Arc<str>) -> Self {
Self { Self {
base_text, base_text,
ops: Default::default(), operations: Default::default(),
undo_stack: Vec::new(), undo_stack: Vec::new(),
redo_stack: Vec::new(), redo_stack: Vec::new(),
transaction_depth: 0, transaction_depth: 0,
group_interval: Duration::from_millis(300), group_interval: Duration::from_millis(300),
next_transaction_id: 0,
} }
} }
fn push(&mut self, op: EditOperation) { fn push(&mut self, op: Operation) {
self.ops.insert(op.timestamp.local(), op); 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; self.transaction_depth += 1;
if self.transaction_depth == 1 { if self.transaction_depth == 1 {
let id = self.next_transaction_id; let id = local_clock.tick();
self.next_transaction_id += 1; self.undo_stack.push(HistoryEntry {
self.undo_stack.push(Transaction { transaction: Transaction {
id, id,
start: start.clone(), start: start.clone(),
end: start, end: start,
edits: Vec::new(), edit_ids: Default::default(),
ranges: Vec::new(), ranges: Default::default(),
},
first_edit_at: now, first_edit_at: now,
last_edit_at: now, last_edit_at: now,
suppress_grouping: false, 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); assert_ne!(self.transaction_depth, 0);
self.transaction_depth -= 1; self.transaction_depth -= 1;
if self.transaction_depth == 0 { 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(); self.undo_stack.pop();
None None
} else { } else {
let transaction = self.undo_stack.last_mut().unwrap(); let entry = self.undo_stack.last_mut().unwrap();
transaction.last_edit_at = now; entry.last_edit_at = now;
Some(transaction) Some(entry)
} }
} else { } else {
None None
@ -195,16 +215,15 @@ impl History {
fn group(&mut self) -> Option<TransactionId> { fn group(&mut self) -> Option<TransactionId> {
let mut new_len = self.undo_stack.len(); 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() { if let Some(mut entry) = entries.next_back() {
while let Some(prev_transaction) = transactions.next_back() { while let Some(prev_entry) = entries.next_back() {
if !prev_transaction.suppress_grouping if !prev_entry.suppress_grouping
&& transaction.first_edit_at - prev_transaction.last_edit_at && entry.first_edit_at - prev_entry.last_edit_at <= self.group_interval
<= self.group_interval && entry.transaction.start == prev_entry.transaction.end
&& transaction.start == prev_transaction.end
{ {
transaction = prev_transaction; entry = prev_entry;
new_len -= 1; new_len -= 1;
} else { } else {
break; break;
@ -212,101 +231,114 @@ impl History {
} }
} }
let (transactions_to_keep, transactions_to_merge) = self.undo_stack.split_at_mut(new_len); let (entries_to_keep, entries_to_merge) = self.undo_stack.split_at_mut(new_len);
if let Some(last_transaction) = transactions_to_keep.last_mut() { if let Some(last_entry) = entries_to_keep.last_mut() {
for transaction in &*transactions_to_merge { for entry in &*entries_to_merge {
for edit_id in &transaction.edits { for edit_id in &entry.transaction.edit_ids {
last_transaction.push_edit(&self.ops[edit_id]); last_entry.push_edit(self.operations[edit_id].as_edit().unwrap());
} }
} }
if let Some(transaction) = transactions_to_merge.last_mut() { if let Some(entry) = entries_to_merge.last_mut() {
last_transaction.last_edit_at = transaction.last_edit_at; last_entry.last_edit_at = entry.last_edit_at;
last_transaction.end = transaction.end.clone(); last_entry.transaction.end = entry.transaction.end.clone();
} }
} }
self.undo_stack.truncate(new_len); 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) { fn finalize_last_transaction(&mut self) -> Option<&Transaction> {
if let Some(transaction) = self.undo_stack.last_mut() { self.undo_stack.last_mut().map(|entry| {
transaction.suppress_grouping = true; 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); assert_eq!(self.transaction_depth, 0);
let mut edit_ids = edit_ids.into_iter().peekable(); self.undo_stack.push(HistoryEntry {
transaction,
if let Some(first_edit_id) = edit_ids.peek() { first_edit_at: now,
let version = self.ops[first_edit_id].version.clone(); last_edit_at: now,
self.start_transaction(version, now); suppress_grouping: false,
for edit_id in edit_ids { });
self.push_undo(edit_id);
}
self.end_transaction(now);
}
} }
fn push_undo(&mut self, edit_id: clock::Local) { fn push_undo(&mut self, op_id: clock::Local) {
assert_ne!(self.transaction_depth, 0); assert_ne!(self.transaction_depth, 0);
let last_transaction = self.undo_stack.last_mut().unwrap(); if let Some(Operation::Edit(edit)) = self.operations.get(&op_id) {
last_transaction.push_edit(&self.ops[&edit_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); assert_eq!(self.transaction_depth, 0);
if let Some(transaction) = self.undo_stack.pop() { if let Some(entry) = self.undo_stack.pop() {
self.redo_stack.push(transaction); self.redo_stack.push(entry);
self.redo_stack.last() self.redo_stack.last()
} else { } else {
None 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); 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); let redo_stack_start_len = self.redo_stack.len();
self.redo_stack.push(transaction); if let Some(entry_ix) = self
self.redo_stack.last() .undo_stack
} else { .iter()
None .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) { fn forget(&mut self, transaction_id: TransactionId) {
assert_eq!(self.transaction_depth, 0); assert_eq!(self.transaction_depth, 0);
if let Some(transaction_ix) = self.undo_stack.iter().rposition(|t| t.id == transaction_id) { if let Some(entry_ix) = self
self.undo_stack.remove(transaction_ix); .undo_stack
} else if let Some(transaction_ix) = .iter()
self.redo_stack.iter().rposition(|t| t.id == transaction_id) .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); assert_eq!(self.transaction_depth, 0);
if let Some(transaction) = self.redo_stack.pop() { if let Some(entry) = self.redo_stack.pop() {
self.undo_stack.push(transaction); self.undo_stack.push(entry);
self.undo_stack.last() self.undo_stack.last()
} else { } else {
None 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); 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); let undo_stack_start_len = self.undo_stack.len();
self.undo_stack.push(transaction); if let Some(entry_ix) = self
self.undo_stack.last() .redo_stack
} else { .iter()
None .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 { pub fn version(&self) -> clock::Global {
self.version.clone() self.version.clone()
} }
@ -620,7 +601,7 @@ impl Buffer {
self.history.group_interval 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 where
R: IntoIterator<IntoIter = I>, R: IntoIterator<IntoIter = I>,
I: ExactSizeIterator<Item = Range<S>>, I: ExactSizeIterator<Item = Range<S>>,
@ -641,13 +622,14 @@ impl Buffer {
local: self.local_clock.tick().value, local: self.local_clock.tick().value,
lamport: self.lamport_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(operation.clone());
self.history.push_undo(edit.timestamp.local()); self.history.push_undo(operation.local_timestamp());
self.snapshot.version.observe(edit.timestamp.local()); self.snapshot.version.observe(operation.local_timestamp());
self.end_transaction(); self.end_transaction();
edit operation
} }
fn apply_local_edit<S: ToOffset>( 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<()> { pub fn apply_ops<I: IntoIterator<Item = Operation>>(&mut self, ops: I) -> Result<()> {
let mut deferred_ops = Vec::new(); let mut deferred_ops = Vec::new();
for op in ops { for op in ops {
self.history.push(op.clone());
if self.can_apply_op(&op) { if self.can_apply_op(&op) {
self.apply_op(op)?; self.apply_op(op)?;
} else { } else {
@ -839,7 +822,6 @@ impl Buffer {
); );
self.snapshot.version.observe(edit.timestamp.local()); self.snapshot.version.observe(edit.timestamp.local());
self.resolve_edit(edit.timestamp.local()); self.resolve_edit(edit.timestamp.local());
self.history.push(edit);
} }
} }
Operation::Undo { Operation::Undo {
@ -1142,10 +1124,6 @@ impl Buffer {
Ok(()) Ok(())
} }
pub fn deferred_ops(&self) -> impl Iterator<Item = &Operation> {
self.deferred_ops.iter()
}
fn flush_deferred_ops(&mut self) -> Result<()> { fn flush_deferred_ops(&mut self) -> Result<()> {
self.deferred_replicas.clear(); self.deferred_replicas.clear();
let mut deferred_ops = Vec::new(); 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() 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> { pub fn start_transaction(&mut self) -> Option<TransactionId> {
self.start_transaction_at(Instant::now()) self.start_transaction_at(Instant::now())
} }
pub fn start_transaction_at(&mut self, now: Instant) -> Option<TransactionId> { 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)> { 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)> { pub fn end_transaction_at(&mut self, now: Instant) -> Option<(TransactionId, clock::Global)> {
if let Some(transaction) = self.history.end_transaction(now) { if let Some(entry) = self.history.end_transaction(now) {
let since = transaction.start.clone(); let since = entry.transaction.start.clone();
let id = self.history.group().unwrap(); let id = self.history.group().unwrap();
Some((id, since)) Some((id, since))
} else { } else {
@ -1198,16 +1181,16 @@ impl Buffer {
} }
} }
pub fn avoid_grouping_next_transaction(&mut self) { pub fn finalize_last_transaction(&mut self) -> Option<&Transaction> {
self.history.avoid_grouping_next_transaction() self.history.finalize_last_transaction()
} }
pub fn base_text(&self) -> &Arc<str> { pub fn base_text(&self) -> &Arc<str> {
&self.history.base_text &self.history.base_text
} }
pub fn history(&self) -> impl Iterator<Item = &EditOperation> { pub fn history(&self) -> impl Iterator<Item = &Operation> {
self.history.ops.values() self.history.operations.values()
} }
pub fn undo_history(&self) -> impl Iterator<Item = (&clock::Local, &[(clock::Local, u32)])> { 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)> { 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 transaction_id = transaction.id;
let op = self.undo_or_redo(transaction).unwrap(); let op = self.undo_or_redo(transaction).unwrap();
Some((transaction_id, op)) Some((transaction_id, op))
@ -1227,13 +1211,18 @@ impl Buffer {
} }
} }
pub fn undo_transaction(&mut self, transaction_id: TransactionId) -> Option<Operation> { pub fn undo_to_transaction(&mut self, transaction_id: TransactionId) -> Vec<Operation> {
if let Some(transaction) = self.history.remove_from_undo(transaction_id).cloned() { let transactions = self
let op = self.undo_or_redo(transaction).unwrap(); .history
Some(op) .remove_from_undo(transaction_id)
} else { .iter()
None .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) { pub fn forget_transaction(&mut self, transaction_id: TransactionId) {
@ -1241,7 +1230,8 @@ impl Buffer {
} }
pub fn redo(&mut self) -> Option<(TransactionId, Operation)> { 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 transaction_id = transaction.id;
let op = self.undo_or_redo(transaction).unwrap(); let op = self.undo_or_redo(transaction).unwrap();
Some((transaction_id, op)) Some((transaction_id, op))
@ -1250,18 +1240,23 @@ impl Buffer {
} }
} }
pub fn redo_transaction(&mut self, transaction_id: TransactionId) -> Option<Operation> { pub fn redo_to_transaction(&mut self, transaction_id: TransactionId) -> Vec<Operation> {
if let Some(transaction) = self.history.remove_from_redo(transaction_id).cloned() { let transactions = self
let op = self.undo_or_redo(transaction).unwrap(); .history
Some(op) .remove_from_redo(transaction_id)
} else { .iter()
None .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> { fn undo_or_redo(&mut self, transaction: Transaction) -> Result<Operation> {
let mut counts = HashMap::default(); 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); counts.insert(edit_id, self.undo_map.undo_count(edit_id) + 1);
} }
@ -1272,20 +1267,18 @@ impl Buffer {
version: transaction.start.clone(), version: transaction.start.clone(),
}; };
self.apply_undo(&undo)?; self.apply_undo(&undo)?;
self.snapshot.version.observe(undo.id); let operation = Operation::Undo {
Ok(Operation::Undo {
undo, undo,
lamport_timestamp: self.lamport_clock.tick(), lamport_timestamp: self.lamport_clock.tick(),
}) };
self.snapshot.version.observe(operation.local_timestamp());
self.history.push(operation.clone());
Ok(operation)
} }
pub fn push_transaction( pub fn push_transaction(&mut self, transaction: Transaction, now: Instant) {
&mut self, self.history.push_transaction(transaction, now);
edit_ids: impl IntoIterator<Item = clock::Local>, self.history.finalize_last_transaction();
now: Instant,
) {
self.history.push_transaction(edit_ids, now);
} }
pub fn subscribe(&mut self) -> Subscription { pub fn subscribe(&mut self) -> Subscription {
@ -1294,13 +1287,13 @@ impl Buffer {
pub fn wait_for_edits( pub fn wait_for_edits(
&mut self, &mut self,
edit_ids: &[clock::Local], edit_ids: impl IntoIterator<Item = clock::Local>,
) -> impl 'static + Future<Output = ()> { ) -> impl 'static + Future<Output = ()> {
let mut futures = Vec::new(); let mut futures = Vec::new();
for edit_id in edit_ids { for edit_id in edit_ids {
if !self.version.observed(*edit_id) { if !self.version.observed(edit_id) {
let (tx, rx) = oneshot::channel(); 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); futures.push(rx);
} }
} }
@ -1404,7 +1397,7 @@ impl Buffer {
new_text new_text
); );
let op = self.edit(old_ranges.iter().cloned(), new_text.as_str()); 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> { 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(); let mut ops = Vec::new();
for _ in 0..rng.gen_range(1..=5) { 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!( log::info!(
"undoing buffer {} transaction {:?}", "undoing buffer {} transaction {:?}",
self.replica_id, self.replica_id,
@ -1512,6 +1506,10 @@ impl BufferSnapshot {
self.visible_text.offset_to_point_utf16(offset) 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 { pub fn version(&self) -> &clock::Global {
&self.version &self.version
} }
@ -1748,14 +1746,6 @@ impl BufferSnapshot {
self.visible_text.clip_point_utf16(point, bias) 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>( pub fn edits_since<'a, D>(
&'a self, &'a self,
since: &'a clock::Global, since: &'a clock::Global,
@ -1766,6 +1756,42 @@ impl BufferSnapshot {
self.edits_since_in_range(since, Anchor::min()..Anchor::max()) self.edits_since_in_range(since, Anchor::min()..Anchor::max())
} }
pub fn edited_ranges_for_transaction<'a, D>(
&'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>( pub fn edits_since_in_range<'a, D>(
&'a self, &'a self,
since: &'a clock::Global, since: &'a clock::Global,
@ -2178,6 +2204,20 @@ impl Operation {
operation_queue::Operation::lamport_timestamp(self).replica_id 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 { pub fn is_edit(&self) -> bool {
match self { match self {
Operation::Edit { .. } => true, 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 { pub trait Clip {
fn clip(&self, bias: Bias, snapshot: &BufferSnapshot) -> Self; fn clip(&self, bias: Bias, snapshot: &BufferSnapshot) -> Self;
} }

View file

@ -293,6 +293,7 @@ pub struct EditorStyle {
pub hint_diagnostic: DiagnosticStyle, pub hint_diagnostic: DiagnosticStyle,
pub invalid_hint_diagnostic: DiagnosticStyle, pub invalid_hint_diagnostic: DiagnosticStyle,
pub autocomplete: AutocompleteStyle, pub autocomplete: AutocompleteStyle,
pub code_actions_indicator: Color,
} }
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default)]
@ -420,6 +421,7 @@ impl InputEditorStyle {
hint_diagnostic: default_diagnostic_style.clone(), hint_diagnostic: default_diagnostic_style.clone(),
invalid_hint_diagnostic: default_diagnostic_style.clone(), invalid_hint_diagnostic: default_diagnostic_style.clone(),
autocomplete: Default::default(), autocomplete: Default::default(),
code_actions_indicator: Default::default(),
} }
} }
} }

View file

@ -221,7 +221,7 @@ impl Pane {
let task = workspace.load_path(project_path, cx); let task = workspace.load_path(project_path, cx);
cx.spawn(|workspace, mut cx| async move { cx.spawn(|workspace, mut cx| async move {
let item = task.await; let 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() { if let Some(item) = item.log_err() {
workspace.update(&mut cx, |workspace, cx| { workspace.update(&mut cx, |workspace, cx| {
pane.update(cx, |p, _| p.nav_history.borrow_mut().set_mode(mode)); pane.update(cx, |p, _| p.nav_history.borrow_mut().set_mode(mode));
@ -279,7 +279,7 @@ impl Pane {
item_view.added_to_pane(cx); item_view.added_to_pane(cx);
let item_idx = cmp::min(self.active_item_index + 1, self.item_views.len()); let item_idx = cmp::min(self.active_item_index + 1, self.item_views.len());
self.item_views 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); self.activate_item(item_idx, cx);
cx.notify(); cx.notify();
} }

View file

@ -150,11 +150,9 @@ pub trait Item: Entity + Sized {
} }
pub trait ItemView: View { pub trait ItemView: View {
type ItemHandle: ItemHandle;
fn deactivated(&mut self, _: &mut ViewContext<Self>) {} fn deactivated(&mut self, _: &mut ViewContext<Self>) {}
fn navigate(&mut self, _: Box<dyn Any>, _: &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 tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox;
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>; fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self> fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
@ -170,7 +168,11 @@ pub trait ItemView: View {
false false
} }
fn can_save(&self, cx: &AppContext) -> bool; 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 can_save_as(&self, cx: &AppContext) -> bool;
fn save_as( fn save_as(
&mut self, &mut self,
@ -222,7 +224,7 @@ pub trait WeakItemHandle {
} }
pub trait ItemViewHandle: 'static { 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 tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox;
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>; fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
fn boxed_clone(&self) -> Box<dyn ItemViewHandle>; fn boxed_clone(&self) -> Box<dyn ItemViewHandle>;
@ -236,7 +238,7 @@ pub trait ItemViewHandle: 'static {
fn has_conflict(&self, cx: &AppContext) -> bool; fn has_conflict(&self, cx: &AppContext) -> bool;
fn can_save(&self, cx: &AppContext) -> bool; fn can_save(&self, cx: &AppContext) -> bool;
fn can_save_as(&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( fn save_as(
&self, &self,
project: ModelHandle<Project>, project: ModelHandle<Project>,
@ -324,7 +326,7 @@ impl<T: Item> WeakItemHandle for WeakModelHandle<T> {
} }
fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> { 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> { impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
fn item_handle(&self, cx: &AppContext) -> Box<dyn ItemHandle> { fn item_id(&self, cx: &AppContext) -> usize {
Box::new(self.read(cx).item_handle(cx)) self.read(cx).item_id(cx)
} }
fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox { 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)); self.update(cx, |this, cx| this.navigate(data, cx));
} }
fn save(&self, cx: &mut MutableAppContext) -> Task<Result<()>> { fn save(&self, project: ModelHandle<Project>, cx: &mut MutableAppContext) -> Task<Result<()>> {
self.update(cx, |item, cx| item.save(cx)) self.update(cx, |item, cx| item.save(project, cx))
} }
fn save_as( fn save_as(
@ -589,7 +591,7 @@ impl Workspace {
while stream.recv().await.is_some() { while stream.recv().await.is_some() {
cx.update(|cx| { cx.update(|cx| {
if let Some(this) = this.upgrade(&cx) { if let Some(this) = this.upgrade(cx) {
this.update(cx, |_, cx| cx.notify()); this.update(cx, |_, cx| cx.notify());
} }
}) })
@ -772,7 +774,7 @@ impl Workspace {
let item = load_task.await?; let item = load_task.await?;
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
let pane = pane let pane = pane
.upgrade(&cx) .upgrade(cx)
.ok_or_else(|| anyhow!("could not upgrade pane reference"))?; .ok_or_else(|| anyhow!("could not upgrade pane reference"))?;
Ok(this.open_item_in_pane(item, &pane, cx)) 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<()>> { 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 let Some(item) = self.active_item(cx) {
if item.can_save(cx) { if item.can_save(cx) {
if item.has_conflict(cx.as_ref()) { if item.has_conflict(cx.as_ref()) {
@ -835,12 +838,12 @@ impl Workspace {
cx.spawn(|_, mut cx| async move { cx.spawn(|_, mut cx| async move {
let answer = answer.recv().await; let answer = answer.recv().await;
if answer == Some(0) { if answer == Some(0) {
cx.update(|cx| item.save(cx)).await?; cx.update(|cx| item.save(project, cx)).await?;
} }
Ok(()) Ok(())
}) })
} else { } else {
item.save(cx) item.save(project, cx)
} }
} else if item.can_save_as(cx) { } else if item.can_save_as(cx) {
let worktree = self.worktrees(cx).next(); let worktree = self.worktrees(cx).next();
@ -849,9 +852,8 @@ impl Workspace {
.map_or(Path::new(""), |w| w.abs_path()) .map_or(Path::new(""), |w| w.abs_path())
.to_path_buf(); .to_path_buf();
let mut abs_path = cx.prompt_for_new_path(&start_abs_path); 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() { 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?; cx.update(|cx| item.save_as(project, abs_path, cx)).await?;
} }
Ok(()) Ok(())

View 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

View file

@ -253,6 +253,7 @@ line_number_active = "$text.0.color"
selection = "$selection.host" selection = "$selection.host"
guest_selections = "$selection.guests" guest_selections = "$selection.guests"
error_color = "$status.bad" error_color = "$status.bad"
code_actions_indicator = "$text.3.color"
[editor.diagnostic_path_header] [editor.diagnostic_path_header]
background = "$state.active_line" background = "$state.active_line"

View file

@ -126,7 +126,7 @@ mod tests {
use super::*; use super::*;
use editor::{DisplayPoint, Editor}; use editor::{DisplayPoint, Editor};
use gpui::{MutableAppContext, TestAppContext, ViewHandle}; use gpui::{MutableAppContext, TestAppContext, ViewHandle};
use project::ProjectPath; use project::{Fs, ProjectPath};
use serde_json::json; use serde_json::json;
use std::{ use std::{
collections::HashSet, collections::HashSet,
@ -817,7 +817,10 @@ mod tests {
.active_pane() .active_pane()
.update(cx, |pane, cx| pane.close_item(editor2.id(), cx)); .update(cx, |pane, cx| pane.close_item(editor2.id(), cx));
drop(editor2); 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 .await
.unwrap(); .unwrap();

16
script/drop-test-dbs Executable file
View 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