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 = [
"anyhow",
"async-pipe",
"ctor",
"env_logger",
"futures",
"gpui",
"log",
@ -2784,7 +2786,6 @@ dependencies = [
"postage",
"serde",
"serde_json",
"simplelog",
"smol",
"unindent",
"util",
@ -3523,7 +3524,6 @@ dependencies = [
"rpc",
"serde",
"serde_json",
"simplelog",
"smol",
"sum_tree",
"tempdir",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -107,14 +107,23 @@ impl<'a> FoldMapWriter<'a> {
let buffer = self.0.buffer.lock().clone();
for range in ranges.into_iter() {
let range = range.start.to_offset(&buffer)..range.end.to_offset(&buffer);
if range.start != range.end {
let fold = Fold(buffer.anchor_after(range.start)..buffer.anchor_before(range.end));
folds.push(fold);
edits.push(text::Edit {
old: range.clone(),
new: range,
});
// Ignore any empty ranges.
if range.start == range.end {
continue;
}
// For now, ignore any ranges that span an excerpt boundary.
let fold = Fold(buffer.anchor_after(range.start)..buffer.anchor_before(range.end));
if fold.0.start.excerpt_id() != fold.0.end.excerpt_id() {
continue;
}
folds.push(fold);
edits.push(text::Edit {
old: range.clone(),
new: range,
});
}
folds.sort_unstable_by(|a, b| sum_tree::SeekTarget::cmp(a, b, &buffer));
@ -268,6 +277,8 @@ impl FoldMap {
let mut buffer = self.buffer.lock();
if buffer.parse_count() != new_buffer.parse_count()
|| buffer.diagnostics_update_count() != new_buffer.diagnostics_update_count()
|| buffer.trailing_excerpt_update_count()
!= new_buffer.trailing_excerpt_update_count()
{
self.version.fetch_add(1, SeqCst);
}
@ -1281,7 +1292,7 @@ mod tests {
_ => buffer.update(cx, |buffer, cx| {
let subscription = buffer.subscribe();
let edit_count = rng.gen_range(1..=5);
buffer.randomly_edit(&mut rng, edit_count, cx);
buffer.randomly_mutate(&mut rng, edit_count, cx);
buffer_snapshot = buffer.snapshot(cx);
let edits = subscription.consume().into_inner();
log::info!("editing {:?}", edits);
@ -1407,7 +1418,6 @@ mod tests {
fold_row = snapshot
.clip_point(FoldPoint::new(fold_row, 0), Bias::Right)
.row();
eprintln!("fold_row: {} of {}", fold_row, expected_buffer_rows.len());
assert_eq!(
snapshot.buffer_rows(fold_row).collect::<Vec<_>>(),
expected_buffer_rows[(fold_row as usize)..],

View file

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

File diff suppressed because it is too large Load diff

View file

@ -3,11 +3,12 @@ use super::{
Anchor, DisplayPoint, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, Input,
Scroll, Select, SelectPhase, SoftWrap, ToPoint, MAX_LINE_LEN,
};
use crate::display_map::TransformBlock;
use clock::ReplicaId;
use collections::{BTreeMap, HashMap};
use gpui::{
color::Color,
elements::layout_highlighted_chunks,
elements::*,
fonts::{HighlightStyle, Underline},
geometry::{
rect::RectF,
@ -280,7 +281,7 @@ impl EditorElement {
&mut self,
bounds: RectF,
visible_bounds: RectF,
layout: &LayoutState,
layout: &mut LayoutState,
cx: &mut PaintContext,
) {
let scroll_top = layout.snapshot.scroll_position().y() * layout.line_height;
@ -294,6 +295,14 @@ impl EditorElement {
line.paint(line_origin, visible_bounds, layout.line_height, cx);
}
}
if let Some((row, indicator)) = layout.code_actions_indicator.as_mut() {
let mut x = bounds.width() - layout.gutter_padding;
let mut y = *row as f32 * layout.line_height - scroll_top;
x += ((layout.gutter_padding + layout.text_offset.x()) - indicator.size().x()) / 2.;
y += (layout.line_height - indicator.size().y()) / 2.;
indicator.paint(bounds.origin() + vec2f(x, y), visible_bounds, cx);
}
}
fn paint_text(
@ -392,20 +401,20 @@ impl EditorElement {
}
cx.scene.pop_layer();
if let Some((position, completions_list)) = layout.completions.as_mut() {
if let Some((position, context_menu)) = layout.context_menu.as_mut() {
cx.scene.push_stacking_context(None);
let cursor_row_layout = &layout.line_layouts[(position.row() - start_row) as usize];
let x = cursor_row_layout.x_for_index(position.column() as usize) - scroll_left;
let y = (position.row() + 1) as f32 * layout.line_height - scroll_top;
let mut list_origin = content_origin + vec2f(x, y);
let list_height = completions_list.size().y();
let list_height = context_menu.size().y();
if list_origin.y() + list_height > bounds.lower_left().y() {
list_origin.set_y(list_origin.y() - layout.line_height - list_height);
}
completions_list.paint(
context_menu.paint(
list_origin,
RectF::from_points(Vector2F::zero(), vec2f(f32::MAX, f32::MAX)), // Let content bleed outside of editor
cx,
@ -649,33 +658,91 @@ impl EditorElement {
line_layouts: &[text_layout::Line],
cx: &mut LayoutContext,
) -> Vec<(u32, ElementBox)> {
let scroll_x = snapshot.scroll_position.x();
snapshot
.blocks_in_range(rows.clone())
.map(|(start_row, block)| {
let anchor_row = block
.position()
.to_point(&snapshot.buffer_snapshot)
.to_display_point(snapshot)
.row();
.map(|(block_row, block)| {
let mut element = match block {
TransformBlock::Custom(block) => {
let align_to = block
.position()
.to_point(&snapshot.buffer_snapshot)
.to_display_point(snapshot);
let anchor_x = text_x
+ if rows.contains(&align_to.row()) {
line_layouts[(align_to.row() - rows.start) as usize]
.x_for_index(align_to.column() as usize)
} else {
layout_line(align_to.row(), snapshot, style, cx.text_layout_cache)
.x_for_index(align_to.column() as usize)
};
let anchor_x = text_x
+ if rows.contains(&anchor_row) {
line_layouts[(anchor_row - rows.start) as usize]
.x_for_index(block.column() as usize)
} else {
layout_line(anchor_row, snapshot, style, cx.text_layout_cache)
.x_for_index(block.column() as usize)
};
block.render(&BlockContext {
cx,
anchor_x,
gutter_padding,
line_height,
scroll_x,
gutter_width,
em_width,
})
}
TransformBlock::ExcerptHeader {
buffer,
starts_new_buffer,
..
} => {
if *starts_new_buffer {
let style = &self.settings.style.diagnostic_path_header;
let font_size = (style.text_scale_factor
* self.settings.style.text.font_size)
.round();
let mut filename = None;
let mut parent_path = None;
if let Some(path) = buffer.path() {
filename =
path.file_name().map(|f| f.to_string_lossy().to_string());
parent_path =
path.parent().map(|p| p.to_string_lossy().to_string() + "/");
}
Flex::row()
.with_child(
Label::new(
filename.unwrap_or_else(|| "untitled".to_string()),
style.filename.text.clone().with_font_size(font_size),
)
.contained()
.with_style(style.filename.container)
.boxed(),
)
.with_children(parent_path.map(|path| {
Label::new(
path,
style.path.text.clone().with_font_size(font_size),
)
.contained()
.with_style(style.path.container)
.boxed()
}))
.aligned()
.left()
.contained()
.with_style(style.container)
.with_padding_left(gutter_padding + scroll_x * em_width)
.expanded()
.named("path header block")
} else {
let text_style = self.settings.style.text.clone();
Label::new("".to_string(), text_style)
.contained()
.with_padding_left(gutter_padding + scroll_x * em_width)
.named("collapsed context")
}
}
};
let mut element = block.render(&BlockContext {
cx,
anchor_x,
gutter_padding,
line_height,
scroll_x: snapshot.scroll_position.x(),
gutter_width,
em_width,
});
element.layout(
SizeConstraint {
min: Vector2F::zero(),
@ -683,7 +750,7 @@ impl EditorElement {
},
cx,
);
(start_row, element)
(block_row, element)
})
.collect()
}
@ -859,7 +926,8 @@ impl Element for EditorElement {
max_row.saturating_sub(1) as f32,
);
let mut completions = None;
let mut context_menu = None;
let mut code_actions_indicator = None;
self.update_view(cx.app, |view, cx| {
let clamped = view.clamp_scroll_left(scroll_max.x());
let autoscrolled;
@ -880,21 +948,24 @@ impl Element for EditorElement {
snapshot = view.snapshot(cx);
}
if view.has_completions() {
let newest_selection_head = view
.newest_selection::<usize>(&snapshot.buffer_snapshot)
.head()
.to_display_point(&snapshot);
let newest_selection_head = view
.newest_selection::<usize>(&snapshot.buffer_snapshot)
.head()
.to_display_point(&snapshot);
if (start_row..end_row).contains(&newest_selection_head.row()) {
let list = view.render_completions(cx).unwrap();
completions = Some((newest_selection_head, list));
if (start_row..end_row).contains(&newest_selection_head.row()) {
if view.context_menu_visible() {
context_menu = view.render_context_menu(newest_selection_head, cx);
}
code_actions_indicator = view
.render_code_actions_indicator(cx)
.map(|indicator| (newest_selection_head.row(), indicator));
}
});
if let Some((_, completions_list)) = completions.as_mut() {
completions_list.layout(
if let Some((_, context_menu)) = context_menu.as_mut() {
context_menu.layout(
SizeConstraint {
min: Vector2F::zero(),
max: vec2f(
@ -906,6 +977,13 @@ impl Element for EditorElement {
);
}
if let Some((_, indicator)) = code_actions_indicator.as_mut() {
indicator.layout(
SizeConstraint::strict_along(Axis::Vertical, line_height * 0.618),
cx,
);
}
let blocks = self.layout_blocks(
start_row..end_row,
&snapshot,
@ -940,7 +1018,8 @@ impl Element for EditorElement {
em_width,
em_advance,
selections,
completions,
context_menu,
code_actions_indicator,
},
)
}
@ -989,8 +1068,14 @@ impl Element for EditorElement {
paint: &mut PaintState,
cx: &mut EventContext,
) -> bool {
if let Some((_, completion_list)) = &mut layout.completions {
if completion_list.dispatch_event(event, cx) {
if let Some((_, context_menu)) = &mut layout.context_menu {
if context_menu.dispatch_event(event, cx) {
return true;
}
}
if let Some((_, indicator)) = &mut layout.code_actions_indicator {
if indicator.dispatch_event(event, cx) {
return true;
}
}
@ -1051,7 +1136,8 @@ pub struct LayoutState {
highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
selections: HashMap<ReplicaId, Vec<text::Selection<DisplayPoint>>>,
text_offset: Vector2F,
completions: Option<(DisplayPoint, ElementBox)>,
context_menu: Option<(DisplayPoint, ElementBox)>,
code_actions_indicator: Option<(u32, ElementBox)>,
}
fn layout_line(
@ -1298,6 +1384,7 @@ mod tests {
let settings = settings.clone();
Arc::new(move |_| settings.clone())
},
None,
cx,
)
});

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

@ -107,6 +107,10 @@ unsafe fn build_classes() {
sel!(scrollWheel:),
handle_view_event as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(cancelOperation:),
cancel_operation as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(makeBackingLayer),
@ -538,6 +542,34 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
}
}
// Allows us to receive `cmd-.` (the shortcut for closing a dialog)
// https://bugs.eclipse.org/bugs/show_bug.cgi?id=300620#c6
extern "C" fn cancel_operation(this: &Object, _sel: Sel, _sender: id) {
let window_state = unsafe { get_window_state(this) };
let mut window_state_borrow = window_state.as_ref().borrow_mut();
let chars = ".".to_string();
let keystroke = Keystroke {
cmd: true,
ctrl: false,
alt: false,
shift: false,
key: chars.clone(),
};
let event = Event::KeyDown {
keystroke: keystroke.clone(),
chars: chars.clone(),
is_held: false,
};
window_state_borrow.last_fresh_keydown = Some((keystroke, chars));
if let Some(mut callback) = window_state_borrow.event_callback.take() {
drop(window_state_borrow);
callback(event);
window_state.borrow_mut().event_callback = Some(callback);
}
}
extern "C" fn send_event(this: &Object, _: Sel, native_event: id) {
unsafe {
let () = msg_send![super(this, class!(NSWindow)), sendEvent: native_event];

View file

@ -7,7 +7,8 @@ use crate::{
platform::Event,
text_layout::TextLayoutCache,
Action, AnyAction, AnyViewHandle, AssetCache, ElementBox, Entity, FontSystem, ModelHandle,
ReadModel, ReadView, Scene, View, ViewHandle,
ReadModel, ReadView, Scene, UpgradeModelHandle, UpgradeViewHandle, View, ViewHandle,
WeakModelHandle, WeakViewHandle,
};
use pathfinder_geometry::vector::{vec2f, Vector2F};
use serde_json::json;
@ -270,6 +271,21 @@ impl<'a> ReadModel for LayoutContext<'a> {
}
}
impl<'a> UpgradeModelHandle for LayoutContext<'a> {
fn upgrade_model_handle<T: Entity>(
&self,
handle: &WeakModelHandle<T>,
) -> Option<ModelHandle<T>> {
self.app.upgrade_model_handle(handle)
}
}
impl<'a> UpgradeViewHandle for LayoutContext<'a> {
fn upgrade_view_handle<T: View>(&self, handle: &WeakViewHandle<T>) -> Option<ViewHandle<T>> {
self.app.upgrade_view_handle(handle)
}
}
pub struct PaintContext<'a> {
rendered_views: &'a mut HashMap<usize, ElementBox>,
pub scene: &'a mut Scene,

File diff suppressed because it is too large Load diff

View file

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

View file

@ -1,12 +1,13 @@
use crate::{
diagnostic_set::DiagnosticEntry, Completion, CompletionLabel, Diagnostic, Language, Operation,
diagnostic_set::DiagnosticEntry, CodeAction, Completion, CompletionLabel, Diagnostic, Language,
Operation,
};
use anyhow::{anyhow, Result};
use clock::ReplicaId;
use collections::HashSet;
use lsp::DiagnosticSeverity;
use rpc::proto;
use std::sync::Arc;
use std::{ops::Range, sync::Arc};
use text::*;
pub use proto::{Buffer, BufferState, SelectionSet};
@ -24,14 +25,7 @@ pub fn serialize_operation(operation: &Operation) -> proto::Operation {
replica_id: undo.id.replica_id as u32,
local_timestamp: undo.id.value,
lamport_timestamp: lamport_timestamp.value,
ranges: undo
.ranges
.iter()
.map(|r| proto::Range {
start: r.start.0 as u64,
end: r.end.0 as u64,
})
.collect(),
ranges: undo.ranges.iter().map(serialize_range).collect(),
counts: undo
.counts
.iter()
@ -44,11 +38,10 @@ pub fn serialize_operation(operation: &Operation) -> proto::Operation {
version: From::from(&undo.version),
}),
Operation::UpdateSelections {
replica_id,
selections,
lamport_timestamp,
} => proto::operation::Variant::UpdateSelections(proto::operation::UpdateSelections {
replica_id: *replica_id as u32,
replica_id: lamport_timestamp.replica_id as u32,
lamport_timestamp: lamport_timestamp.value,
selections: serialize_selections(selections),
}),
@ -60,32 +53,27 @@ pub fn serialize_operation(operation: &Operation) -> proto::Operation {
lamport_timestamp: lamport_timestamp.value,
diagnostics: serialize_diagnostics(diagnostics.iter()),
}),
Operation::UpdateCompletionTriggers { triggers } => {
proto::operation::Variant::UpdateCompletionTriggers(
proto::operation::UpdateCompletionTriggers {
triggers: triggers.clone(),
},
)
}
Operation::UpdateCompletionTriggers {
triggers,
lamport_timestamp,
} => proto::operation::Variant::UpdateCompletionTriggers(
proto::operation::UpdateCompletionTriggers {
replica_id: lamport_timestamp.replica_id as u32,
lamport_timestamp: lamport_timestamp.value,
triggers: triggers.clone(),
},
),
}),
}
}
pub fn serialize_edit_operation(operation: &EditOperation) -> proto::operation::Edit {
let ranges = operation
.ranges
.iter()
.map(|range| proto::Range {
start: range.start.0 as u64,
end: range.end.0 as u64,
})
.collect();
proto::operation::Edit {
replica_id: operation.timestamp.replica_id as u32,
local_timestamp: operation.timestamp.local,
lamport_timestamp: operation.timestamp.lamport,
version: From::from(&operation.version),
ranges,
ranges: operation.ranges.iter().map(serialize_range).collect(),
new_text: operation.new_text.clone(),
}
}
@ -208,11 +196,7 @@ pub fn deserialize_operation(message: proto::Operation) -> Result<Operation> {
)
})
.collect(),
ranges: undo
.ranges
.into_iter()
.map(|r| FullOffset(r.start as usize)..FullOffset(r.end as usize))
.collect(),
ranges: undo.ranges.into_iter().map(deserialize_range).collect(),
version: undo.version.into(),
},
}),
@ -232,7 +216,6 @@ pub fn deserialize_operation(message: proto::Operation) -> Result<Operation> {
.collect::<Vec<_>>();
Operation::UpdateSelections {
replica_id: message.replica_id as ReplicaId,
lamport_timestamp: clock::Lamport {
replica_id: message.replica_id as ReplicaId,
value: message.lamport_timestamp,
@ -250,6 +233,10 @@ pub fn deserialize_operation(message: proto::Operation) -> Result<Operation> {
proto::operation::Variant::UpdateCompletionTriggers(message) => {
Operation::UpdateCompletionTriggers {
triggers: message.triggers,
lamport_timestamp: clock::Lamport {
replica_id: message.replica_id as ReplicaId,
value: message.lamport_timestamp,
},
}
}
},
@ -257,11 +244,6 @@ pub fn deserialize_operation(message: proto::Operation) -> Result<Operation> {
}
pub fn deserialize_edit_operation(edit: proto::operation::Edit) -> EditOperation {
let ranges = edit
.ranges
.into_iter()
.map(|range| FullOffset(range.start as usize)..FullOffset(range.end as usize))
.collect();
EditOperation {
timestamp: InsertionTimestamp {
replica_id: edit.replica_id as ReplicaId,
@ -269,7 +251,7 @@ pub fn deserialize_edit_operation(edit: proto::operation::Edit) -> EditOperation
lamport: edit.lamport_timestamp,
},
version: edit.version.into(),
ranges,
ranges: edit.ranges.into_iter().map(deserialize_range).collect(),
new_text: edit.new_text,
}
}
@ -380,7 +362,39 @@ pub fn deserialize_anchor(anchor: proto::Anchor) -> Option<Anchor> {
})
}
pub fn serialize_completion(completion: &Completion<Anchor>) -> proto::Completion {
pub fn lamport_timestamp_for_operation(operation: &proto::Operation) -> Option<clock::Lamport> {
let replica_id;
let value;
match operation.variant.as_ref()? {
proto::operation::Variant::Edit(op) => {
replica_id = op.replica_id;
value = op.lamport_timestamp;
}
proto::operation::Variant::Undo(op) => {
replica_id = op.replica_id;
value = op.lamport_timestamp;
}
proto::operation::Variant::UpdateDiagnostics(op) => {
replica_id = op.replica_id;
value = op.lamport_timestamp;
}
proto::operation::Variant::UpdateSelections(op) => {
replica_id = op.replica_id;
value = op.lamport_timestamp;
}
proto::operation::Variant::UpdateCompletionTriggers(op) => {
replica_id = op.replica_id;
value = op.lamport_timestamp;
}
}
Some(clock::Lamport {
replica_id: replica_id as ReplicaId,
value,
})
}
pub fn serialize_completion(completion: &Completion) -> proto::Completion {
proto::Completion {
old_start: Some(serialize_anchor(&completion.old_range.start)),
old_end: Some(serialize_anchor(&completion.old_range.end)),
@ -392,7 +406,7 @@ pub fn serialize_completion(completion: &Completion<Anchor>) -> proto::Completio
pub fn deserialize_completion(
completion: proto::Completion,
language: Option<&Arc<Language>>,
) -> Result<Completion<Anchor>> {
) -> Result<Completion> {
let old_start = completion
.old_start
.and_then(deserialize_anchor)
@ -411,3 +425,89 @@ pub fn deserialize_completion(
lsp_completion,
})
}
pub fn serialize_code_action(action: &CodeAction) -> proto::CodeAction {
proto::CodeAction {
start: Some(serialize_anchor(&action.range.start)),
end: Some(serialize_anchor(&action.range.end)),
lsp_action: serde_json::to_vec(&action.lsp_action).unwrap(),
}
}
pub fn deserialize_code_action(action: proto::CodeAction) -> Result<CodeAction> {
let start = action
.start
.and_then(deserialize_anchor)
.ok_or_else(|| anyhow!("invalid start"))?;
let end = action
.end
.and_then(deserialize_anchor)
.ok_or_else(|| anyhow!("invalid end"))?;
let lsp_action = serde_json::from_slice(&action.lsp_action)?;
Ok(CodeAction {
range: start..end,
lsp_action,
})
}
pub fn serialize_transaction(transaction: &Transaction) -> proto::Transaction {
proto::Transaction {
id: Some(serialize_local_timestamp(transaction.id)),
edit_ids: transaction
.edit_ids
.iter()
.copied()
.map(serialize_local_timestamp)
.collect(),
start: (&transaction.start).into(),
end: (&transaction.end).into(),
ranges: transaction.ranges.iter().map(serialize_range).collect(),
}
}
pub fn deserialize_transaction(transaction: proto::Transaction) -> Result<Transaction> {
Ok(Transaction {
id: deserialize_local_timestamp(
transaction
.id
.ok_or_else(|| anyhow!("missing transaction id"))?,
),
edit_ids: transaction
.edit_ids
.into_iter()
.map(deserialize_local_timestamp)
.collect(),
start: transaction.start.into(),
end: transaction.end.into(),
ranges: transaction
.ranges
.into_iter()
.map(deserialize_range)
.collect(),
})
}
pub fn serialize_local_timestamp(timestamp: clock::Local) -> proto::LocalTimestamp {
proto::LocalTimestamp {
replica_id: timestamp.replica_id as u32,
value: timestamp.value,
}
}
pub fn deserialize_local_timestamp(timestamp: proto::LocalTimestamp) -> clock::Local {
clock::Local {
replica_id: timestamp.replica_id as ReplicaId,
value: timestamp.value,
}
}
pub fn serialize_range(range: &Range<FullOffset>) -> proto::Range {
proto::Range {
start: range.start.0 as u64,
end: range.end.0 as u64,
}
}
pub fn deserialize_range(range: proto::Range) -> Range<FullOffset> {
FullOffset(range.start as usize)..FullOffset(range.end as usize)
}

View file

@ -557,7 +557,7 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppConte
#[gpui::test]
async fn test_diagnostics(mut cx: gpui::TestAppContext) {
let (language_server, mut fake) = lsp::LanguageServer::fake(cx.background()).await;
let (language_server, mut fake) = lsp::LanguageServer::fake(&cx).await;
let mut rust_lang = rust_lang();
rust_lang.config.language_server = Some(LanguageServerConfig {
disk_based_diagnostic_sources: HashSet::from_iter(["disk".to_string()]),
@ -572,7 +572,7 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
.unindent();
let buffer = cx.add_model(|cx| {
Buffer::new(0, text, cx)
Buffer::from_file(0, text, Box::new(FakeFile::new("/some/path")), cx)
.with_language(Arc::new(rust_lang), cx)
.with_language_server(language_server, cx)
});
@ -592,7 +592,6 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
// Receive diagnostics for an earlier version of the buffer.
buffer
.update_diagnostics(
Some(open_notification.text_document.version),
vec![
DiagnosticEntry {
range: PointUtf16::new(0, 9)..PointUtf16::new(0, 10),
@ -628,6 +627,7 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
},
},
],
Some(open_notification.text_document.version),
cx,
)
.unwrap();
@ -687,7 +687,6 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
// Ensure overlapping diagnostics are highlighted correctly.
buffer
.update_diagnostics(
Some(open_notification.text_document.version),
vec![
DiagnosticEntry {
range: PointUtf16::new(0, 9)..PointUtf16::new(0, 10),
@ -711,6 +710,7 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
},
},
],
Some(open_notification.text_document.version),
cx,
)
.unwrap();
@ -777,7 +777,6 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
buffer.update(&mut cx, |buffer, cx| {
buffer
.update_diagnostics(
Some(change_notification_2.text_document.version),
vec![
DiagnosticEntry {
range: PointUtf16::new(1, 9)..PointUtf16::new(1, 11),
@ -802,6 +801,7 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
},
},
],
Some(change_notification_2.text_document.version),
cx,
)
.unwrap();
@ -838,6 +838,223 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
});
}
#[gpui::test]
async fn test_edits_from_lsp_with_past_version(mut cx: gpui::TestAppContext) {
let (language_server, mut fake) = lsp::LanguageServer::fake(&cx).await;
let text = "
fn a() {
f1();
}
fn b() {
f2();
}
fn c() {
f3();
}
"
.unindent();
let buffer = cx.add_model(|cx| {
Buffer::from_file(0, text, Box::new(FakeFile::new("/some/path")), cx)
.with_language(Arc::new(rust_lang()), cx)
.with_language_server(language_server, cx)
});
let lsp_document_version = fake
.receive_notification::<lsp::notification::DidOpenTextDocument>()
.await
.text_document
.version;
// Simulate editing the buffer after the language server computes some edits.
buffer.update(&mut cx, |buffer, cx| {
buffer.edit(
[Point::new(0, 0)..Point::new(0, 0)],
"// above first function\n",
cx,
);
buffer.edit(
[Point::new(2, 0)..Point::new(2, 0)],
" // inside first function\n",
cx,
);
buffer.edit(
[Point::new(6, 4)..Point::new(6, 4)],
"// inside second function ",
cx,
);
assert_eq!(
buffer.text(),
"
// above first function
fn a() {
// inside first function
f1();
}
fn b() {
// inside second function f2();
}
fn c() {
f3();
}
"
.unindent()
);
});
let edits = buffer
.update(&mut cx, |buffer, cx| {
buffer.edits_from_lsp(
vec![
// replace body of first function
lsp::TextEdit {
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(3, 0)),
new_text: "
fn a() {
f10();
}
"
.unindent(),
},
// edit inside second function
lsp::TextEdit {
range: lsp::Range::new(lsp::Position::new(4, 6), lsp::Position::new(4, 6)),
new_text: "00".into(),
},
// edit inside third function via two distinct edits
lsp::TextEdit {
range: lsp::Range::new(lsp::Position::new(7, 5), lsp::Position::new(7, 5)),
new_text: "4000".into(),
},
lsp::TextEdit {
range: lsp::Range::new(lsp::Position::new(7, 5), lsp::Position::new(7, 6)),
new_text: "".into(),
},
],
Some(lsp_document_version),
cx,
)
})
.await
.unwrap();
buffer.update(&mut cx, |buffer, cx| {
for (range, new_text) in edits {
buffer.edit([range], new_text, cx);
}
assert_eq!(
buffer.text(),
"
// above first function
fn a() {
// inside first function
f10();
}
fn b() {
// inside second function f200();
}
fn c() {
f4000();
}
"
.unindent()
);
});
}
#[gpui::test]
async fn test_edits_from_lsp_with_edits_on_adjacent_lines(mut cx: gpui::TestAppContext) {
let text = "
use a::b;
use a::c;
fn f() {
b();
c();
}
"
.unindent();
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
// Simulate the language server sending us a small edit in the form of a very large diff.
// Rust-analyzer does this when performing a merge-imports code action.
let edits = buffer
.update(&mut cx, |buffer, cx| {
buffer.edits_from_lsp(
[
// Replace the first use statement without editing the semicolon.
lsp::TextEdit {
range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 8)),
new_text: "a::{b, c}".into(),
},
// Reinsert the remainder of the file between the semicolon and the final
// newline of the file.
lsp::TextEdit {
range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)),
new_text: "\n\n".into(),
},
lsp::TextEdit {
range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)),
new_text: "
fn f() {
b();
c();
}"
.unindent(),
},
// Delete everything after the first newline of the file.
lsp::TextEdit {
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(7, 0)),
new_text: "".into(),
},
],
None,
cx,
)
})
.await
.unwrap();
buffer.update(&mut cx, |buffer, cx| {
let edits = edits
.into_iter()
.map(|(range, text)| {
(
range.start.to_point(&buffer)..range.end.to_point(&buffer),
text,
)
})
.collect::<Vec<_>>();
assert_eq!(
edits,
[
(Point::new(0, 4)..Point::new(0, 8), "a::{b, c}".into()),
(Point::new(1, 0)..Point::new(2, 0), "".into())
]
);
for (range, new_text) in edits {
buffer.edit([range], new_text, cx);
}
assert_eq!(
buffer.text(),
"
use a::{b, c};
fn f() {
b();
c();
}
"
.unindent()
);
});
}
#[gpui::test]
async fn test_empty_diagnostic_ranges(mut cx: gpui::TestAppContext) {
cx.add_model(|cx| {
@ -851,7 +1068,6 @@ async fn test_empty_diagnostic_ranges(mut cx: gpui::TestAppContext) {
buffer.set_language(Some(Arc::new(rust_lang())), cx);
buffer
.update_diagnostics(
None,
vec![
DiagnosticEntry {
range: PointUtf16::new(0, 10)..PointUtf16::new(0, 10),
@ -870,6 +1086,7 @@ async fn test_empty_diagnostic_ranges(mut cx: gpui::TestAppContext) {
},
},
],
None,
cx,
)
.unwrap();
@ -1073,15 +1290,15 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) {
for buffer in &buffers {
let buffer = buffer.read(cx).snapshot();
let actual_remote_selections = buffer
.remote_selections_in_range(Anchor::min()..Anchor::max())
.map(|(replica_id, selections)| (replica_id, selections.collect::<Vec<_>>()))
.collect::<Vec<_>>();
let expected_remote_selections = active_selections
.iter()
.filter(|(replica_id, _)| **replica_id != buffer.replica_id())
.map(|(replica_id, selections)| (*replica_id, selections.iter().collect::<Vec<_>>()))
.collect::<Vec<_>>();
let actual_remote_selections = buffer
.remote_selections_in_range(Anchor::min()..Anchor::max())
.map(|(replica_id, selections)| (replica_id, selections.collect::<Vec<_>>()))
.collect::<Vec<_>>();
assert_eq!(actual_remote_selections, expected_remote_selections);
}
}

View file

@ -27,5 +27,6 @@ smol = "1.2"
gpui = { path = "../gpui", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }
async-pipe = { git = "https://github.com/routerify/async-pipe-rs", rev = "feeb77e83142a9ff837d0767652ae41bfc5d8e47" }
simplelog = "0.9"
ctor = "0.1"
env_logger = "0.8"
unindent = "0.1.7"

View file

@ -56,6 +56,18 @@ struct Request<'a, T> {
params: T,
}
#[cfg(any(test, feature = "test-support"))]
#[derive(Deserialize)]
struct AnyRequest<'a> {
id: usize,
#[serde(borrow)]
jsonrpc: &'a str,
#[serde(borrow)]
method: &'a str,
#[serde(borrow)]
params: &'a RawValue,
}
#[derive(Serialize, Deserialize)]
struct AnyResponse<'a> {
id: usize,
@ -238,6 +250,21 @@ impl LanguageServer {
link_support: Some(true),
..Default::default()
}),
code_action: Some(CodeActionClientCapabilities {
code_action_literal_support: Some(CodeActionLiteralSupport {
code_action_kind: CodeActionKindLiteralSupport {
value_set: vec![
CodeActionKind::REFACTOR.as_str().into(),
CodeActionKind::QUICKFIX.as_str().into(),
],
},
}),
data_support: Some(true),
resolve_support: Some(CodeActionCapabilityResolveSupport {
properties: vec!["edit".to_string()],
}),
..Default::default()
}),
completion: Some(CompletionClientCapabilities {
completion_item: Some(CompletionItemCapability {
snippet_support: Some(true),
@ -454,48 +481,41 @@ impl Drop for Subscription {
#[cfg(any(test, feature = "test-support"))]
pub struct FakeLanguageServer {
buffer: Vec<u8>,
stdin: smol::io::BufReader<async_pipe::PipeReader>,
stdout: smol::io::BufWriter<async_pipe::PipeWriter>,
handlers: Arc<
Mutex<
HashMap<
&'static str,
Box<dyn Send + FnOnce(usize, &[u8]) -> (Vec<u8>, barrier::Sender)>,
>,
>,
>,
outgoing_tx: channel::Sender<Vec<u8>>,
incoming_rx: channel::Receiver<Vec<u8>>,
pub started: Arc<std::sync::atomic::AtomicBool>,
}
#[cfg(any(test, feature = "test-support"))]
pub struct RequestId<T> {
id: usize,
_type: std::marker::PhantomData<T>,
}
#[cfg(any(test, feature = "test-support"))]
impl LanguageServer {
pub async fn fake(executor: Arc<executor::Background>) -> (Arc<Self>, FakeLanguageServer) {
Self::fake_with_capabilities(Default::default(), executor).await
pub async fn fake(cx: &gpui::TestAppContext) -> (Arc<Self>, FakeLanguageServer) {
Self::fake_with_capabilities(Default::default(), cx).await
}
pub async fn fake_with_capabilities(
capabilities: ServerCapabilities,
executor: Arc<executor::Background>,
cx: &gpui::TestAppContext,
) -> (Arc<Self>, FakeLanguageServer) {
let stdin = async_pipe::pipe();
let stdout = async_pipe::pipe();
let mut fake = FakeLanguageServer {
stdin: smol::io::BufReader::new(stdin.1),
stdout: smol::io::BufWriter::new(stdout.0),
buffer: Vec::new(),
started: Arc::new(std::sync::atomic::AtomicBool::new(true)),
};
let (stdin_writer, stdin_reader) = async_pipe::pipe();
let (stdout_writer, stdout_reader) = async_pipe::pipe();
let server = Self::new_internal(stdin.0, stdout.1, Path::new("/"), executor).unwrap();
let mut fake = FakeLanguageServer::new(cx, stdin_reader, stdout_writer);
fake.handle_request::<request::Initialize, _>(move |_| InitializeResult {
capabilities,
..Default::default()
});
let (init_id, _) = fake.receive_request::<request::Initialize>().await;
fake.respond(
init_id,
InitializeResult {
capabilities,
..Default::default()
},
)
.await;
let server =
Self::new_internal(stdin_writer, stdout_reader, Path::new("/"), cx.background())
.unwrap();
fake.receive_notification::<notification::Initialized>()
.await;
@ -505,6 +525,75 @@ impl LanguageServer {
#[cfg(any(test, feature = "test-support"))]
impl FakeLanguageServer {
fn new(
cx: &gpui::TestAppContext,
stdin: async_pipe::PipeReader,
stdout: async_pipe::PipeWriter,
) -> Self {
use futures::StreamExt as _;
let (incoming_tx, incoming_rx) = channel::unbounded();
let (outgoing_tx, mut outgoing_rx) = channel::unbounded();
let this = Self {
outgoing_tx: outgoing_tx.clone(),
incoming_rx,
handlers: Default::default(),
started: Arc::new(std::sync::atomic::AtomicBool::new(true)),
};
// Receive incoming messages
let handlers = this.handlers.clone();
cx.background()
.spawn(async move {
let mut buffer = Vec::new();
let mut stdin = smol::io::BufReader::new(stdin);
while Self::receive(&mut stdin, &mut buffer).await.is_ok() {
if let Ok(request) = serde_json::from_slice::<AnyRequest>(&mut buffer) {
assert_eq!(request.jsonrpc, JSON_RPC_VERSION);
let handler = handlers.lock().remove(request.method);
if let Some(handler) = handler {
let (response, sent) =
handler(request.id, request.params.get().as_bytes());
log::debug!("handled lsp request. method:{}", request.method);
outgoing_tx.send(response).await.unwrap();
drop(sent);
} else {
log::debug!("unhandled lsp request. method:{}", request.method);
outgoing_tx
.send(
serde_json::to_vec(&AnyResponse {
id: request.id,
error: Some(Error {
message: "no handler".to_string(),
}),
result: None,
})
.unwrap(),
)
.await
.unwrap();
}
} else {
incoming_tx.send(buffer.clone()).await.unwrap();
}
}
})
.detach();
// Send outgoing messages
cx.background()
.spawn(async move {
let mut stdout = smol::io::BufWriter::new(stdout);
while let Some(notification) = outgoing_rx.next().await {
Self::send(&mut stdout, &notification).await;
}
})
.detach();
this
}
pub async fn notify<T: notification::Notification>(&mut self, params: T::Params) {
if !self.started.load(std::sync::atomic::Ordering::SeqCst) {
panic!("can't simulate an LSP notification before the server has been started");
@ -515,51 +604,53 @@ impl FakeLanguageServer {
params,
})
.unwrap();
self.send(message).await;
self.outgoing_tx.send(message).await.unwrap();
}
pub async fn respond<'a, T: request::Request>(
&mut self,
request_id: RequestId<T>,
result: T::Result,
) {
let result = serde_json::to_string(&result).unwrap();
let message = serde_json::to_vec(&AnyResponse {
id: request_id.id,
error: None,
result: Some(&RawValue::from_string(result).unwrap()),
})
.unwrap();
self.send(message).await;
}
pub async fn receive_notification<T: notification::Notification>(&mut self) -> T::Params {
use futures::StreamExt as _;
pub async fn receive_request<T: request::Request>(&mut self) -> (RequestId<T>, T::Params) {
loop {
self.receive().await;
if let Ok(request) = serde_json::from_slice::<Request<T::Params>>(&self.buffer) {
assert_eq!(request.method, T::METHOD);
assert_eq!(request.jsonrpc, JSON_RPC_VERSION);
return (
RequestId {
id: request.id,
_type: std::marker::PhantomData,
},
request.params,
);
let bytes = self.incoming_rx.next().await.unwrap();
if let Ok(notification) = serde_json::from_slice::<Notification<T::Params>>(&bytes) {
assert_eq!(notification.method, T::METHOD);
return notification.params;
} else {
log::info!(
"skipping message in fake language server {:?}",
std::str::from_utf8(&self.buffer)
std::str::from_utf8(&bytes)
);
}
}
}
pub async fn receive_notification<T: notification::Notification>(&mut self) -> T::Params {
self.receive().await;
let notification = serde_json::from_slice::<Notification<T::Params>>(&self.buffer).unwrap();
assert_eq!(notification.method, T::METHOD);
notification.params
pub fn handle_request<T, F>(&mut self, handler: F) -> barrier::Receiver
where
T: 'static + request::Request,
F: 'static + Send + FnOnce(T::Params) -> T::Result,
{
let (responded_tx, responded_rx) = barrier::channel();
let prev_handler = self.handlers.lock().insert(
T::METHOD,
Box::new(|id, params| {
let result = handler(serde_json::from_slice::<T::Params>(params).unwrap());
let result = serde_json::to_string(&result).unwrap();
let result = serde_json::from_str::<&RawValue>(&result).unwrap();
let response = AnyResponse {
id,
error: None,
result: Some(result),
};
(serde_json::to_vec(&response).unwrap(), responded_tx)
}),
);
if prev_handler.is_some() {
panic!(
"registered a new handler for LSP method '{}' before the previous handler was called",
T::METHOD
);
}
responded_rx
}
pub async fn start_progress(&mut self, token: impl Into<String>) {
@ -578,39 +669,37 @@ impl FakeLanguageServer {
.await;
}
async fn send(&mut self, message: Vec<u8>) {
self.stdout
async fn send(stdout: &mut smol::io::BufWriter<async_pipe::PipeWriter>, message: &[u8]) {
stdout
.write_all(CONTENT_LEN_HEADER.as_bytes())
.await
.unwrap();
self.stdout
stdout
.write_all((format!("{}", message.len())).as_bytes())
.await
.unwrap();
self.stdout.write_all("\r\n\r\n".as_bytes()).await.unwrap();
self.stdout.write_all(&message).await.unwrap();
self.stdout.flush().await.unwrap();
stdout.write_all("\r\n\r\n".as_bytes()).await.unwrap();
stdout.write_all(&message).await.unwrap();
stdout.flush().await.unwrap();
}
async fn receive(&mut self) {
self.buffer.clear();
self.stdin
.read_until(b'\n', &mut self.buffer)
.await
.unwrap();
self.stdin
.read_until(b'\n', &mut self.buffer)
.await
.unwrap();
let message_len: usize = std::str::from_utf8(&self.buffer)
async fn receive(
stdin: &mut smol::io::BufReader<async_pipe::PipeReader>,
buffer: &mut Vec<u8>,
) -> Result<()> {
buffer.clear();
stdin.read_until(b'\n', buffer).await?;
stdin.read_until(b'\n', buffer).await?;
let message_len: usize = std::str::from_utf8(buffer)
.unwrap()
.strip_prefix(CONTENT_LEN_HEADER)
.unwrap()
.trim_end()
.parse()
.unwrap();
self.buffer.resize(message_len, 0);
self.stdin.read_exact(&mut self.buffer).await.unwrap();
buffer.resize(message_len, 0);
stdin.read_exact(buffer).await?;
Ok(())
}
}
@ -618,10 +707,16 @@ impl FakeLanguageServer {
mod tests {
use super::*;
use gpui::TestAppContext;
use simplelog::SimpleLogger;
use unindent::Unindent;
use util::test::temp_tree;
#[ctor::ctor]
fn init_logger() {
if std::env::var("RUST_LOG").is_ok() {
env_logger::init();
}
}
#[gpui::test]
async fn test_rust_analyzer(cx: TestAppContext) {
let lib_source = r#"
@ -643,14 +738,9 @@ mod tests {
}));
let lib_file_uri = Url::from_file_path(root_dir.path().join("src/lib.rs")).unwrap();
let server = cx.read(|cx| {
LanguageServer::new(
Path::new("rust-analyzer"),
root_dir.path(),
cx.background().clone(),
)
.unwrap()
});
let server =
LanguageServer::new(Path::new("rust-analyzer"), root_dir.path(), cx.background())
.unwrap();
server.next_idle_notification().await;
server
@ -687,9 +777,7 @@ mod tests {
#[gpui::test]
async fn test_fake(cx: TestAppContext) {
SimpleLogger::init(log::LevelFilter::Info, Default::default()).unwrap();
let (server, mut fake) = LanguageServer::fake(cx.background()).await;
let (server, mut fake) = LanguageServer::fake(&cx).await;
let (message_tx, message_rx) = channel::unbounded();
let (diagnostics_tx, diagnostics_rx) = channel::unbounded();
@ -741,9 +829,9 @@ mod tests {
"file://b/c"
);
fake.handle_request::<request::Shutdown, _>(|_| ());
drop(server);
let (shutdown_request, _) = fake.receive_request::<request::Shutdown>().await;
fake.respond(shutdown_request, ()).await;
fake.receive_notification::<notification::Exit>().await;
}

View file

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

View file

@ -13,6 +13,11 @@ use text::Rope;
#[async_trait::async_trait]
pub trait Fs: Send + Sync {
async fn create_dir(&self, path: &Path) -> Result<()>;
async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()>;
async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()>;
async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()>;
async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()>;
async fn load(&self, path: &Path) -> Result<String>;
async fn save(&self, path: &Path, text: &Rope) -> Result<()>;
async fn canonicalize(&self, path: &Path) -> Result<PathBuf>;
@ -32,6 +37,24 @@ pub trait Fs: Send + Sync {
fn as_fake(&self) -> &FakeFs;
}
#[derive(Copy, Clone, Default)]
pub struct CreateOptions {
pub overwrite: bool,
pub ignore_if_exists: bool,
}
#[derive(Copy, Clone, Default)]
pub struct RenameOptions {
pub overwrite: bool,
pub ignore_if_exists: bool,
}
#[derive(Copy, Clone, Default)]
pub struct RemoveOptions {
pub recursive: bool,
pub ignore_if_not_exists: bool,
}
#[derive(Clone, Debug)]
pub struct Metadata {
pub inode: u64,
@ -44,6 +67,60 @@ pub struct RealFs;
#[async_trait::async_trait]
impl Fs for RealFs {
async fn create_dir(&self, path: &Path) -> Result<()> {
Ok(smol::fs::create_dir_all(path).await?)
}
async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()> {
let mut open_options = smol::fs::OpenOptions::new();
open_options.write(true).create(true);
if options.overwrite {
open_options.truncate(true);
} else if !options.ignore_if_exists {
open_options.create_new(true);
}
open_options.open(path).await?;
Ok(())
}
async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()> {
if !options.overwrite && smol::fs::metadata(target).await.is_ok() {
if options.ignore_if_exists {
return Ok(());
} else {
return Err(anyhow!("{target:?} already exists"));
}
}
smol::fs::rename(source, target).await?;
Ok(())
}
async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {
let result = if options.recursive {
smol::fs::remove_dir_all(path).await
} else {
smol::fs::remove_dir(path).await
};
match result {
Ok(()) => Ok(()),
Err(err) if err.kind() == io::ErrorKind::NotFound && options.ignore_if_not_exists => {
Ok(())
}
Err(err) => Err(err)?,
}
}
async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()> {
match smol::fs::remove_file(path).await {
Ok(()) => Ok(()),
Err(err) if err.kind() == io::ErrorKind::NotFound && options.ignore_if_not_exists => {
Ok(())
}
Err(err) => Err(err)?,
}
}
async fn load(&self, path: &Path) -> Result<String> {
let mut file = smol::fs::File::open(path).await?;
let mut text = String::new();
@ -162,15 +239,19 @@ impl FakeFsState {
}
}
async fn emit_event(&mut self, paths: &[&Path]) {
async fn emit_event<I, T>(&mut self, paths: I)
where
I: IntoIterator<Item = T>,
T: Into<PathBuf>,
{
use postage::prelude::Sink as _;
let events = paths
.iter()
.into_iter()
.map(|path| fsevent::Event {
event_id: 0,
flags: fsevent::StreamFlags::empty(),
path: path.to_path_buf(),
path: path.into(),
})
.collect();
@ -292,46 +373,163 @@ impl FakeFs {
}
.boxed()
}
pub async fn remove(&self, path: &Path) -> Result<()> {
let mut state = self.state.lock().await;
state.validate_path(path)?;
state.entries.retain(|path, _| !path.starts_with(path));
state.emit_event(&[path]).await;
Ok(())
}
pub async fn rename(&self, source: &Path, target: &Path) -> Result<()> {
let mut state = self.state.lock().await;
state.validate_path(source)?;
state.validate_path(target)?;
if state.entries.contains_key(target) {
Err(anyhow!("target path already exists"))
} else {
let mut removed = Vec::new();
state.entries.retain(|path, entry| {
if let Ok(relative_path) = path.strip_prefix(source) {
removed.push((relative_path.to_path_buf(), entry.clone()));
false
} else {
true
}
});
for (relative_path, entry) in removed {
let new_path = target.join(relative_path);
state.entries.insert(new_path, entry);
}
state.emit_event(&[source, target]).await;
Ok(())
}
}
}
#[cfg(any(test, feature = "test-support"))]
#[async_trait::async_trait]
impl Fs for FakeFs {
async fn create_dir(&self, path: &Path) -> Result<()> {
self.executor.simulate_random_delay().await;
let state = &mut *self.state.lock().await;
let mut ancestor_path = PathBuf::new();
let mut created_dir_paths = Vec::new();
for component in path.components() {
ancestor_path.push(component);
let entry = state
.entries
.entry(ancestor_path.clone())
.or_insert_with(|| {
let inode = state.next_inode;
state.next_inode += 1;
created_dir_paths.push(ancestor_path.clone());
FakeFsEntry {
metadata: Metadata {
inode,
mtime: SystemTime::now(),
is_dir: true,
is_symlink: false,
},
content: None,
}
});
if !entry.metadata.is_dir {
return Err(anyhow!(
"cannot create directory because {:?} is a file",
ancestor_path
));
}
}
state.emit_event(&created_dir_paths).await;
Ok(())
}
async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()> {
self.executor.simulate_random_delay().await;
let mut state = self.state.lock().await;
state.validate_path(path)?;
if let Some(entry) = state.entries.get_mut(path) {
if entry.metadata.is_dir || entry.metadata.is_symlink {
return Err(anyhow!(
"cannot create file because {:?} is a dir or a symlink",
path
));
}
if options.overwrite {
entry.metadata.mtime = SystemTime::now();
entry.content = Some(Default::default());
} else if !options.ignore_if_exists {
return Err(anyhow!(
"cannot create file because {:?} already exists",
path
));
}
} else {
let inode = state.next_inode;
state.next_inode += 1;
let entry = FakeFsEntry {
metadata: Metadata {
inode,
mtime: SystemTime::now(),
is_dir: false,
is_symlink: false,
},
content: Some(Default::default()),
};
state.entries.insert(path.to_path_buf(), entry);
}
state.emit_event(&[path]).await;
Ok(())
}
async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()> {
let mut state = self.state.lock().await;
state.validate_path(source)?;
state.validate_path(target)?;
if !options.overwrite && state.entries.contains_key(target) {
if options.ignore_if_exists {
return Ok(());
} else {
return Err(anyhow!("{target:?} already exists"));
}
}
let mut removed = Vec::new();
state.entries.retain(|path, entry| {
if let Ok(relative_path) = path.strip_prefix(source) {
removed.push((relative_path.to_path_buf(), entry.clone()));
false
} else {
true
}
});
for (relative_path, entry) in removed {
let new_path = target.join(relative_path);
state.entries.insert(new_path, entry);
}
state.emit_event(&[source, target]).await;
Ok(())
}
async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {
let mut state = self.state.lock().await;
state.validate_path(path)?;
if let Some(entry) = state.entries.get(path) {
if !entry.metadata.is_dir {
return Err(anyhow!("cannot remove {path:?} because it is not a dir"));
}
if !options.recursive {
let descendants = state
.entries
.keys()
.filter(|path| path.starts_with(path))
.count();
if descendants > 1 {
return Err(anyhow!("{path:?} is not empty"));
}
}
state.entries.retain(|path, _| !path.starts_with(path));
state.emit_event(&[path]).await;
} else if !options.ignore_if_not_exists {
return Err(anyhow!("{path:?} does not exist"));
}
Ok(())
}
async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()> {
let mut state = self.state.lock().await;
state.validate_path(path)?;
if let Some(entry) = state.entries.get(path) {
if entry.metadata.is_dir {
return Err(anyhow!("cannot remove {path:?} because it is not a file"));
}
state.entries.remove(path);
state.emit_event(&[path]).await;
} else if !options.ignore_if_not_exists {
return Err(anyhow!("{path:?} does not exist"));
}
Ok(())
}
async fn load(&self, path: &Path) -> Result<String> {
self.executor.simulate_random_delay().await;
let state = self.state.lock().await;

File diff suppressed because it is too large Load diff

View file

@ -14,9 +14,7 @@ use gpui::{
executor, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext,
Task,
};
use language::{
Anchor, Buffer, Completion, DiagnosticEntry, Language, Operation, PointUtf16, Rope,
};
use language::{Buffer, DiagnosticEntry, Operation, PointUtf16, Rope};
use lazy_static::lazy_static;
use parking_lot::Mutex;
use postage::{
@ -293,7 +291,7 @@ impl Worktree {
let this = worktree_handle.downgrade();
cx.spawn(|mut cx| async move {
while let Some(_) = snapshot_rx.recv().await {
if let Some(this) = cx.read(|cx| this.upgrade(cx)) {
if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, cx| this.poll_snapshot(cx));
} else {
break;
@ -518,7 +516,7 @@ impl LocalWorktree {
cx.spawn_weak(|this, mut cx| async move {
while let Ok(scan_state) = scan_states_rx.recv().await {
if let Some(handle) = cx.read(|cx| this.upgrade(cx)) {
if let Some(handle) = this.upgrade(&cx) {
let to_send = handle.update(&mut cx, |this, cx| {
last_scan_state_tx.blocking_send(scan_state).ok();
this.poll_snapshot(cx);
@ -820,7 +818,7 @@ impl RemoteWorktree {
) -> Result<()> {
let mut tx = self.updates_tx.clone();
let payload = envelope.payload.clone();
cx.background()
cx.foreground()
.spawn(async move {
tx.send(payload).await.expect("receiver runs to completion");
})
@ -1387,96 +1385,6 @@ impl language::File for File {
})
}
fn format_remote(
&self,
buffer_id: u64,
cx: &mut MutableAppContext,
) -> Option<Task<Result<()>>> {
let worktree = self.worktree.read(cx);
let worktree = worktree.as_remote()?;
let rpc = worktree.client.clone();
let project_id = worktree.project_id;
Some(cx.foreground().spawn(async move {
rpc.request(proto::FormatBuffer {
project_id,
buffer_id,
})
.await?;
Ok(())
}))
}
fn completions(
&self,
buffer_id: u64,
position: Anchor,
language: Option<Arc<Language>>,
cx: &mut MutableAppContext,
) -> Task<Result<Vec<Completion<Anchor>>>> {
let worktree = self.worktree.read(cx);
let worktree = if let Some(worktree) = worktree.as_remote() {
worktree
} else {
return Task::ready(Err(anyhow!(
"remote completions requested on a local worktree"
)));
};
let rpc = worktree.client.clone();
let project_id = worktree.project_id;
cx.foreground().spawn(async move {
let response = rpc
.request(proto::GetCompletions {
project_id,
buffer_id,
position: Some(language::proto::serialize_anchor(&position)),
})
.await?;
response
.completions
.into_iter()
.map(|completion| {
language::proto::deserialize_completion(completion, language.as_ref())
})
.collect()
})
}
fn apply_additional_edits_for_completion(
&self,
buffer_id: u64,
completion: Completion<Anchor>,
cx: &mut MutableAppContext,
) -> Task<Result<Vec<clock::Local>>> {
let worktree = self.worktree.read(cx);
let worktree = if let Some(worktree) = worktree.as_remote() {
worktree
} else {
return Task::ready(Err(anyhow!(
"remote additional edits application requested on a local worktree"
)));
};
let rpc = worktree.client.clone();
let project_id = worktree.project_id;
cx.foreground().spawn(async move {
let response = rpc
.request(proto::ApplyCompletionAdditionalEdits {
project_id,
buffer_id,
completion: Some(language::proto::serialize_completion(&completion)),
})
.await?;
Ok(response
.additional_edits
.into_iter()
.map(|edit| clock::Local {
replica_id: edit.replica_id as ReplicaId,
value: edit.local_timestamp,
})
.collect())
})
}
fn buffer_updated(&self, buffer_id: u64, operation: Operation, cx: &mut MutableAppContext) {
self.worktree.update(cx, |worktree, cx| {
worktree.send_buffer_update(buffer_id, operation, cx);
@ -2216,7 +2124,7 @@ struct UpdateIgnoreStatusJob {
}
pub trait WorktreeHandle {
#[cfg(test)]
#[cfg(any(test, feature = "test-support"))]
fn flush_fs_events<'a>(
&self,
cx: &'a gpui::TestAppContext,
@ -2230,7 +2138,7 @@ impl WorktreeHandle for ModelHandle<Worktree> {
//
// This function mutates the worktree's directory and waits for those mutations to be picked up,
// to ensure that all redundant FS events have already been processed.
#[cfg(test)]
#[cfg(any(test, feature = "test-support"))]
fn flush_fs_events<'a>(
&self,
cx: &'a gpui::TestAppContext,
@ -2238,14 +2146,22 @@ impl WorktreeHandle for ModelHandle<Worktree> {
use smol::future::FutureExt;
let filename = "fs-event-sentinel";
let root_path = cx.read(|cx| self.read(cx).as_local().unwrap().abs_path().clone());
let tree = self.clone();
let (fs, root_path) = self.read_with(cx, |tree, _| {
let tree = tree.as_local().unwrap();
(tree.fs.clone(), tree.abs_path().clone())
});
async move {
std::fs::write(root_path.join(filename), "").unwrap();
fs.create_file(&root_path.join(filename), Default::default())
.await
.unwrap();
tree.condition(&cx, |tree, _| tree.entry_for_path(filename).is_some())
.await;
std::fs::remove_file(root_path.join(filename)).unwrap();
fs.remove_file(&root_path.join(filename), Default::default())
.await
.unwrap();
tree.condition(&cx, |tree, _| tree.entry_for_path(filename).is_none())
.await;

View file

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

View file

@ -179,7 +179,16 @@ impl Peer {
let channel = response_channels.lock().as_mut()?.remove(&responding_to);
if let Some(mut tx) = channel {
let mut requester_resumed = barrier::channel();
tx.send((incoming, requester_resumed.0)).await.ok();
if let Err(error) = tx.send((incoming, requester_resumed.0)).await {
log::debug!(
"received RPC but request future was dropped {:?}",
error.0 .0
);
}
// Drop response channel before awaiting on the barrier. This allows the
// barrier to get dropped even if the request's future is dropped before it
// has a chance to observe the response.
drop(tx);
requester_resumed.1.recv().await;
} else {
log::warn!("received RPC response to unknown request {}", responding_to);
@ -337,7 +346,7 @@ mod tests {
use async_tungstenite::tungstenite::Message as WebSocketMessage;
use gpui::TestAppContext;
#[gpui::test(iterations = 10)]
#[gpui::test(iterations = 50)]
async fn test_request_response(cx: TestAppContext) {
let executor = cx.foreground();
@ -478,7 +487,7 @@ mod tests {
}
}
#[gpui::test(iterations = 10)]
#[gpui::test(iterations = 50)]
async fn test_order_of_response_and_incoming(cx: TestAppContext) {
let executor = cx.foreground();
let server = Peer::new();
@ -576,7 +585,119 @@ mod tests {
);
}
#[gpui::test(iterations = 10)]
#[gpui::test(iterations = 50)]
async fn test_dropping_request_before_completion(cx: TestAppContext) {
let executor = cx.foreground();
let server = Peer::new();
let client = Peer::new();
let (client_to_server_conn, server_to_client_conn, _) =
Connection::in_memory(cx.background());
let (client_to_server_conn_id, io_task1, mut client_incoming) =
client.add_connection(client_to_server_conn).await;
let (server_to_client_conn_id, io_task2, mut server_incoming) =
server.add_connection(server_to_client_conn).await;
executor.spawn(io_task1).detach();
executor.spawn(io_task2).detach();
executor
.spawn(async move {
let request1 = server_incoming
.next()
.await
.unwrap()
.into_any()
.downcast::<TypedEnvelope<proto::Ping>>()
.unwrap();
let request2 = server_incoming
.next()
.await
.unwrap()
.into_any()
.downcast::<TypedEnvelope<proto::Ping>>()
.unwrap();
server
.send(
server_to_client_conn_id,
proto::Error {
message: "message 1".to_string(),
},
)
.unwrap();
server
.send(
server_to_client_conn_id,
proto::Error {
message: "message 2".to_string(),
},
)
.unwrap();
server.respond(request1.receipt(), proto::Ack {}).unwrap();
server.respond(request2.receipt(), proto::Ack {}).unwrap();
// Prevent the connection from being dropped
server_incoming.next().await;
})
.detach();
let events = Arc::new(Mutex::new(Vec::new()));
let request1 = client.request(client_to_server_conn_id, proto::Ping {});
let request1_task = executor.spawn(request1);
let request2 = client.request(client_to_server_conn_id, proto::Ping {});
let request2_task = executor.spawn({
let events = events.clone();
async move {
request2.await.unwrap();
events.lock().push("response 2".to_string());
}
});
executor
.spawn({
let events = events.clone();
async move {
let incoming1 = client_incoming
.next()
.await
.unwrap()
.into_any()
.downcast::<TypedEnvelope<proto::Error>>()
.unwrap();
events.lock().push(incoming1.payload.message);
let incoming2 = client_incoming
.next()
.await
.unwrap()
.into_any()
.downcast::<TypedEnvelope<proto::Error>>()
.unwrap();
events.lock().push(incoming2.payload.message);
// Prevent the connection from being dropped
client_incoming.next().await;
}
})
.detach();
// Allow the request to make some progress before dropping it.
cx.background().simulate_random_delay().await;
drop(request1_task);
request2_task.await;
assert_eq!(
&*events.lock(),
&[
"message 1".to_string(),
"message 2".to_string(),
"response 2".to_string()
]
);
}
#[gpui::test(iterations = 50)]
async fn test_disconnect(cx: TestAppContext) {
let executor = cx.foreground();
@ -611,7 +732,7 @@ mod tests {
.is_err());
}
#[gpui::test(iterations = 10)]
#[gpui::test(iterations = 50)]
async fn test_io_error(cx: TestAppContext) {
let executor = cx.foreground();
let (client_conn, mut server_conn, _) = Connection::in_memory(cx.background());

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

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 {
if point >= self.summary().lines {
return self.summary().bytes;
@ -580,6 +593,27 @@ impl Chunk {
offset
}
fn point_to_point_utf16(&self, target: Point) -> PointUtf16 {
let mut point = Point::zero();
let mut point_utf16 = PointUtf16::new(0, 0);
for ch in self.0.chars() {
if point >= target {
break;
}
if ch == '\n' {
point_utf16.row += 1;
point_utf16.column = 0;
point.row += 1;
point.column = 0;
} else {
point_utf16.column += ch.len_utf16() as u32;
point.column += ch.len_utf8() as u32;
}
}
point_utf16
}
fn point_utf16_to_offset(&self, target: PointUtf16) -> usize {
let mut offset = 0;
let mut point = PointUtf16::new(0, 0);

View file

@ -432,28 +432,28 @@ fn test_undo_redo() {
buffer.edit(vec![3..5], "cd");
assert_eq!(buffer.text(), "1abcdef234");
let transactions = buffer.history.undo_stack.clone();
assert_eq!(transactions.len(), 3);
let entries = buffer.history.undo_stack.clone();
assert_eq!(entries.len(), 3);
buffer.undo_or_redo(transactions[0].clone()).unwrap();
buffer.undo_or_redo(entries[0].transaction.clone()).unwrap();
assert_eq!(buffer.text(), "1cdef234");
buffer.undo_or_redo(transactions[0].clone()).unwrap();
buffer.undo_or_redo(entries[0].transaction.clone()).unwrap();
assert_eq!(buffer.text(), "1abcdef234");
buffer.undo_or_redo(transactions[1].clone()).unwrap();
buffer.undo_or_redo(entries[1].transaction.clone()).unwrap();
assert_eq!(buffer.text(), "1abcdx234");
buffer.undo_or_redo(transactions[2].clone()).unwrap();
buffer.undo_or_redo(entries[2].transaction.clone()).unwrap();
assert_eq!(buffer.text(), "1abx234");
buffer.undo_or_redo(transactions[1].clone()).unwrap();
buffer.undo_or_redo(entries[1].transaction.clone()).unwrap();
assert_eq!(buffer.text(), "1abyzef234");
buffer.undo_or_redo(transactions[2].clone()).unwrap();
buffer.undo_or_redo(entries[2].transaction.clone()).unwrap();
assert_eq!(buffer.text(), "1abcdef234");
buffer.undo_or_redo(transactions[2].clone()).unwrap();
buffer.undo_or_redo(entries[2].transaction.clone()).unwrap();
assert_eq!(buffer.text(), "1abyzef234");
buffer.undo_or_redo(transactions[0].clone()).unwrap();
buffer.undo_or_redo(entries[0].transaction.clone()).unwrap();
assert_eq!(buffer.text(), "1yzef234");
buffer.undo_or_redo(transactions[1].clone()).unwrap();
buffer.undo_or_redo(entries[1].transaction.clone()).unwrap();
assert_eq!(buffer.text(), "1234");
}
@ -502,7 +502,7 @@ fn test_history() {
}
#[test]
fn test_avoid_grouping_next_transaction() {
fn test_finalize_last_transaction() {
let now = Instant::now();
let mut buffer = Buffer::new(0, 0, History::new("123456".into()));
@ -511,7 +511,7 @@ fn test_avoid_grouping_next_transaction() {
buffer.end_transaction_at(now);
assert_eq!(buffer.text(), "12cd56");
buffer.avoid_grouping_next_transaction();
buffer.finalize_last_transaction();
buffer.start_transaction_at(now);
buffer.edit(vec![4..5], "e");
buffer.end_transaction_at(now).unwrap();
@ -536,6 +536,44 @@ fn test_avoid_grouping_next_transaction() {
assert_eq!(buffer.text(), "ab2cde6");
}
#[test]
fn test_edited_ranges_for_transaction() {
let now = Instant::now();
let mut buffer = Buffer::new(0, 0, History::new("1234567".into()));
buffer.start_transaction_at(now);
buffer.edit(vec![2..4], "cd");
buffer.edit(vec![6..6], "efg");
buffer.end_transaction_at(now);
assert_eq!(buffer.text(), "12cd56efg7");
let tx = buffer.finalize_last_transaction().unwrap().clone();
assert_eq!(
buffer
.edited_ranges_for_transaction::<usize>(&tx)
.collect::<Vec<_>>(),
[2..4, 6..9]
);
buffer.edit(vec![5..5], "hijk");
assert_eq!(buffer.text(), "12cd5hijk6efg7");
assert_eq!(
buffer
.edited_ranges_for_transaction::<usize>(&tx)
.collect::<Vec<_>>(),
[2..4, 10..13]
);
buffer.edit(vec![4..4], "l");
assert_eq!(buffer.text(), "12cdl5hijk6efg7");
assert_eq!(
buffer
.edited_ranges_for_transaction::<usize>(&tx)
.collect::<Vec<_>>(),
[2..4, 11..14]
);
}
#[test]
fn test_concurrent_edits() {
let text = "abcdef";
@ -551,12 +589,12 @@ fn test_concurrent_edits() {
let buf3_op = buffer3.edit(vec![5..6], "56");
assert_eq!(buffer3.text(), "abcde56");
buffer1.apply_op(Operation::Edit(buf2_op.clone())).unwrap();
buffer1.apply_op(Operation::Edit(buf3_op.clone())).unwrap();
buffer2.apply_op(Operation::Edit(buf1_op.clone())).unwrap();
buffer2.apply_op(Operation::Edit(buf3_op.clone())).unwrap();
buffer3.apply_op(Operation::Edit(buf1_op.clone())).unwrap();
buffer3.apply_op(Operation::Edit(buf2_op.clone())).unwrap();
buffer1.apply_op(buf2_op.clone()).unwrap();
buffer1.apply_op(buf3_op.clone()).unwrap();
buffer2.apply_op(buf1_op.clone()).unwrap();
buffer2.apply_op(buf3_op.clone()).unwrap();
buffer3.apply_op(buf1_op.clone()).unwrap();
buffer3.apply_op(buf2_op.clone()).unwrap();
assert_eq!(buffer1.text(), "a12c34e56");
assert_eq!(buffer2.text(), "a12c34e56");

View file

@ -40,7 +40,7 @@ pub use subscription::*;
pub use sum_tree::Bias;
use sum_tree::{FilterCursor, SumTree};
pub type TransactionId = usize;
pub type TransactionId = clock::Local;
pub struct Buffer {
snapshot: BufferSnapshot,
@ -67,28 +67,37 @@ pub struct BufferSnapshot {
}
#[derive(Clone, Debug)]
pub struct Transaction {
id: TransactionId,
start: clock::Global,
end: clock::Global,
edits: Vec<clock::Local>,
ranges: Vec<Range<FullOffset>>,
pub struct HistoryEntry {
transaction: Transaction,
first_edit_at: Instant,
last_edit_at: Instant,
suppress_grouping: bool,
}
impl Transaction {
#[derive(Clone, Debug)]
pub struct Transaction {
pub id: TransactionId,
pub edit_ids: Vec<clock::Local>,
pub start: clock::Global,
pub end: clock::Global,
pub ranges: Vec<Range<FullOffset>>,
}
impl HistoryEntry {
pub fn transaction_id(&self) -> TransactionId {
self.transaction.id
}
fn push_edit(&mut self, edit: &EditOperation) {
self.edits.push(edit.timestamp.local());
self.end.observe(edit.timestamp.local());
self.transaction.edit_ids.push(edit.timestamp.local());
self.transaction.end.observe(edit.timestamp.local());
let mut other_ranges = edit.ranges.iter().peekable();
let mut new_ranges = Vec::new();
let insertion_len = edit.new_text.as_ref().map_or(0, |t| t.len());
let mut delta = 0;
for mut self_range in self.ranges.iter().cloned() {
for mut self_range in self.transaction.ranges.iter().cloned() {
self_range.start += delta;
self_range.end += delta;
@ -122,7 +131,7 @@ impl Transaction {
delta += insertion_len;
}
self.ranges = new_ranges;
self.transaction.ranges = new_ranges;
}
}
@ -130,42 +139,46 @@ impl Transaction {
pub struct History {
// TODO: Turn this into a String or Rope, maybe.
pub base_text: Arc<str>,
ops: HashMap<clock::Local, EditOperation>,
undo_stack: Vec<Transaction>,
redo_stack: Vec<Transaction>,
operations: HashMap<clock::Local, Operation>,
undo_stack: Vec<HistoryEntry>,
redo_stack: Vec<HistoryEntry>,
transaction_depth: usize,
group_interval: Duration,
next_transaction_id: TransactionId,
}
impl History {
pub fn new(base_text: Arc<str>) -> Self {
Self {
base_text,
ops: Default::default(),
operations: Default::default(),
undo_stack: Vec::new(),
redo_stack: Vec::new(),
transaction_depth: 0,
group_interval: Duration::from_millis(300),
next_transaction_id: 0,
}
}
fn push(&mut self, op: EditOperation) {
self.ops.insert(op.timestamp.local(), op);
fn push(&mut self, op: Operation) {
self.operations.insert(op.local_timestamp(), op);
}
fn start_transaction(&mut self, start: clock::Global, now: Instant) -> Option<TransactionId> {
fn start_transaction(
&mut self,
start: clock::Global,
now: Instant,
local_clock: &mut clock::Local,
) -> Option<TransactionId> {
self.transaction_depth += 1;
if self.transaction_depth == 1 {
let id = self.next_transaction_id;
self.next_transaction_id += 1;
self.undo_stack.push(Transaction {
id,
start: start.clone(),
end: start,
edits: Vec::new(),
ranges: Vec::new(),
let id = local_clock.tick();
self.undo_stack.push(HistoryEntry {
transaction: Transaction {
id,
start: start.clone(),
end: start,
edit_ids: Default::default(),
ranges: Default::default(),
},
first_edit_at: now,
last_edit_at: now,
suppress_grouping: false,
@ -176,17 +189,24 @@ impl History {
}
}
fn end_transaction(&mut self, now: Instant) -> Option<&Transaction> {
fn end_transaction(&mut self, now: Instant) -> Option<&HistoryEntry> {
assert_ne!(self.transaction_depth, 0);
self.transaction_depth -= 1;
if self.transaction_depth == 0 {
if self.undo_stack.last().unwrap().ranges.is_empty() {
if self
.undo_stack
.last()
.unwrap()
.transaction
.ranges
.is_empty()
{
self.undo_stack.pop();
None
} else {
let transaction = self.undo_stack.last_mut().unwrap();
transaction.last_edit_at = now;
Some(transaction)
let entry = self.undo_stack.last_mut().unwrap();
entry.last_edit_at = now;
Some(entry)
}
} else {
None
@ -195,16 +215,15 @@ impl History {
fn group(&mut self) -> Option<TransactionId> {
let mut new_len = self.undo_stack.len();
let mut transactions = self.undo_stack.iter_mut();
let mut entries = self.undo_stack.iter_mut();
if let Some(mut transaction) = transactions.next_back() {
while let Some(prev_transaction) = transactions.next_back() {
if !prev_transaction.suppress_grouping
&& transaction.first_edit_at - prev_transaction.last_edit_at
<= self.group_interval
&& transaction.start == prev_transaction.end
if let Some(mut entry) = entries.next_back() {
while let Some(prev_entry) = entries.next_back() {
if !prev_entry.suppress_grouping
&& entry.first_edit_at - prev_entry.last_edit_at <= self.group_interval
&& entry.transaction.start == prev_entry.transaction.end
{
transaction = prev_transaction;
entry = prev_entry;
new_len -= 1;
} else {
break;
@ -212,101 +231,114 @@ impl History {
}
}
let (transactions_to_keep, transactions_to_merge) = self.undo_stack.split_at_mut(new_len);
if let Some(last_transaction) = transactions_to_keep.last_mut() {
for transaction in &*transactions_to_merge {
for edit_id in &transaction.edits {
last_transaction.push_edit(&self.ops[edit_id]);
let (entries_to_keep, entries_to_merge) = self.undo_stack.split_at_mut(new_len);
if let Some(last_entry) = entries_to_keep.last_mut() {
for entry in &*entries_to_merge {
for edit_id in &entry.transaction.edit_ids {
last_entry.push_edit(self.operations[edit_id].as_edit().unwrap());
}
}
if let Some(transaction) = transactions_to_merge.last_mut() {
last_transaction.last_edit_at = transaction.last_edit_at;
last_transaction.end = transaction.end.clone();
if let Some(entry) = entries_to_merge.last_mut() {
last_entry.last_edit_at = entry.last_edit_at;
last_entry.transaction.end = entry.transaction.end.clone();
}
}
self.undo_stack.truncate(new_len);
self.undo_stack.last().map(|t| t.id)
self.undo_stack.last().map(|e| e.transaction.id)
}
fn avoid_grouping_next_transaction(&mut self) {
if let Some(transaction) = self.undo_stack.last_mut() {
transaction.suppress_grouping = true;
}
fn finalize_last_transaction(&mut self) -> Option<&Transaction> {
self.undo_stack.last_mut().map(|entry| {
entry.suppress_grouping = true;
&entry.transaction
})
}
fn push_transaction(&mut self, edit_ids: impl IntoIterator<Item = clock::Local>, now: Instant) {
fn push_transaction(&mut self, transaction: Transaction, now: Instant) {
assert_eq!(self.transaction_depth, 0);
let mut edit_ids = edit_ids.into_iter().peekable();
if let Some(first_edit_id) = edit_ids.peek() {
let version = self.ops[first_edit_id].version.clone();
self.start_transaction(version, now);
for edit_id in edit_ids {
self.push_undo(edit_id);
}
self.end_transaction(now);
}
self.undo_stack.push(HistoryEntry {
transaction,
first_edit_at: now,
last_edit_at: now,
suppress_grouping: false,
});
}
fn push_undo(&mut self, edit_id: clock::Local) {
fn push_undo(&mut self, op_id: clock::Local) {
assert_ne!(self.transaction_depth, 0);
let last_transaction = self.undo_stack.last_mut().unwrap();
last_transaction.push_edit(&self.ops[&edit_id]);
if let Some(Operation::Edit(edit)) = self.operations.get(&op_id) {
let last_transaction = self.undo_stack.last_mut().unwrap();
last_transaction.push_edit(&edit);
}
}
fn pop_undo(&mut self) -> Option<&Transaction> {
fn pop_undo(&mut self) -> Option<&HistoryEntry> {
assert_eq!(self.transaction_depth, 0);
if let Some(transaction) = self.undo_stack.pop() {
self.redo_stack.push(transaction);
if let Some(entry) = self.undo_stack.pop() {
self.redo_stack.push(entry);
self.redo_stack.last()
} else {
None
}
}
fn remove_from_undo(&mut self, transaction_id: TransactionId) -> Option<&Transaction> {
fn remove_from_undo(&mut self, transaction_id: TransactionId) -> &[HistoryEntry] {
assert_eq!(self.transaction_depth, 0);
if let Some(transaction_ix) = self.undo_stack.iter().rposition(|t| t.id == transaction_id) {
let transaction = self.undo_stack.remove(transaction_ix);
self.redo_stack.push(transaction);
self.redo_stack.last()
} else {
None
let redo_stack_start_len = self.redo_stack.len();
if let Some(entry_ix) = self
.undo_stack
.iter()
.rposition(|entry| entry.transaction.id == transaction_id)
{
self.redo_stack
.extend(self.undo_stack.drain(entry_ix..).rev());
}
&self.redo_stack[redo_stack_start_len..]
}
fn forget(&mut self, transaction_id: TransactionId) {
assert_eq!(self.transaction_depth, 0);
if let Some(transaction_ix) = self.undo_stack.iter().rposition(|t| t.id == transaction_id) {
self.undo_stack.remove(transaction_ix);
} else if let Some(transaction_ix) =
self.redo_stack.iter().rposition(|t| t.id == transaction_id)
if let Some(entry_ix) = self
.undo_stack
.iter()
.rposition(|entry| entry.transaction.id == transaction_id)
{
self.undo_stack.remove(transaction_ix);
self.undo_stack.remove(entry_ix);
} else if let Some(entry_ix) = self
.redo_stack
.iter()
.rposition(|entry| entry.transaction.id == transaction_id)
{
self.undo_stack.remove(entry_ix);
}
}
fn pop_redo(&mut self) -> Option<&Transaction> {
fn pop_redo(&mut self) -> Option<&HistoryEntry> {
assert_eq!(self.transaction_depth, 0);
if let Some(transaction) = self.redo_stack.pop() {
self.undo_stack.push(transaction);
if let Some(entry) = self.redo_stack.pop() {
self.undo_stack.push(entry);
self.undo_stack.last()
} else {
None
}
}
fn remove_from_redo(&mut self, transaction_id: TransactionId) -> Option<&Transaction> {
fn remove_from_redo(&mut self, transaction_id: TransactionId) -> &[HistoryEntry] {
assert_eq!(self.transaction_depth, 0);
if let Some(transaction_ix) = self.redo_stack.iter().rposition(|t| t.id == transaction_id) {
let transaction = self.redo_stack.remove(transaction_ix);
self.undo_stack.push(transaction);
self.undo_stack.last()
} else {
None
let undo_stack_start_len = self.undo_stack.len();
if let Some(entry_ix) = self
.redo_stack
.iter()
.rposition(|entry| entry.transaction.id == transaction_id)
{
self.undo_stack
.extend(self.redo_stack.drain(entry_ix..).rev());
}
&self.undo_stack[undo_stack_start_len..]
}
}
@ -545,57 +577,6 @@ impl Buffer {
}
}
pub fn from_parts(
replica_id: u16,
remote_id: u64,
visible_text: &str,
deleted_text: &str,
undo_map: impl Iterator<Item = (clock::Local, Vec<(clock::Local, u32)>)>,
fragments: impl ExactSizeIterator<Item = Fragment>,
lamport_timestamp: u32,
version: clock::Global,
) -> Self {
let visible_text = visible_text.into();
let deleted_text = deleted_text.into();
let fragments = SumTree::from_iter(fragments, &None);
let mut insertions = fragments
.iter()
.map(|fragment| InsertionFragment {
timestamp: fragment.insertion_timestamp.local(),
split_offset: fragment.insertion_offset,
fragment_id: fragment.id.clone(),
})
.collect::<Vec<_>>();
insertions.sort_unstable_by_key(|i| (i.timestamp, i.split_offset));
Self {
remote_id,
replica_id,
history: History::new("".into()),
deferred_ops: OperationQueue::new(),
deferred_replicas: Default::default(),
local_clock: clock::Local {
replica_id,
value: version.get(replica_id) + 1,
},
lamport_clock: clock::Lamport {
replica_id,
value: lamport_timestamp,
},
subscriptions: Default::default(),
edit_id_resolvers: Default::default(),
snapshot: BufferSnapshot {
replica_id,
visible_text,
deleted_text,
undo_map: UndoMap(undo_map.collect()),
fragments,
insertions: SumTree::from_iter(insertions, &()),
version,
},
}
}
pub fn version(&self) -> clock::Global {
self.version.clone()
}
@ -620,7 +601,7 @@ impl Buffer {
self.history.group_interval
}
pub fn edit<R, I, S, T>(&mut self, ranges: R, new_text: T) -> EditOperation
pub fn edit<R, I, S, T>(&mut self, ranges: R, new_text: T) -> Operation
where
R: IntoIterator<IntoIter = I>,
I: ExactSizeIterator<Item = Range<S>>,
@ -641,13 +622,14 @@ impl Buffer {
local: self.local_clock.tick().value,
lamport: self.lamport_clock.tick().value,
};
let edit = self.apply_local_edit(ranges.into_iter(), new_text, timestamp);
let operation =
Operation::Edit(self.apply_local_edit(ranges.into_iter(), new_text, timestamp));
self.history.push(edit.clone());
self.history.push_undo(edit.timestamp.local());
self.snapshot.version.observe(edit.timestamp.local());
self.history.push(operation.clone());
self.history.push_undo(operation.local_timestamp());
self.snapshot.version.observe(operation.local_timestamp());
self.end_transaction();
edit
operation
}
fn apply_local_edit<S: ToOffset>(
@ -815,6 +797,7 @@ impl Buffer {
pub fn apply_ops<I: IntoIterator<Item = Operation>>(&mut self, ops: I) -> Result<()> {
let mut deferred_ops = Vec::new();
for op in ops {
self.history.push(op.clone());
if self.can_apply_op(&op) {
self.apply_op(op)?;
} else {
@ -839,7 +822,6 @@ impl Buffer {
);
self.snapshot.version.observe(edit.timestamp.local());
self.resolve_edit(edit.timestamp.local());
self.history.push(edit);
}
}
Operation::Undo {
@ -1142,10 +1124,6 @@ impl Buffer {
Ok(())
}
pub fn deferred_ops(&self) -> impl Iterator<Item = &Operation> {
self.deferred_ops.iter()
}
fn flush_deferred_ops(&mut self) -> Result<()> {
self.deferred_replicas.clear();
let mut deferred_ops = Vec::new();
@ -1172,16 +1150,21 @@ impl Buffer {
}
}
pub fn peek_undo_stack(&self) -> Option<&Transaction> {
pub fn peek_undo_stack(&self) -> Option<&HistoryEntry> {
self.history.undo_stack.last()
}
pub fn peek_redo_stack(&self) -> Option<&HistoryEntry> {
self.history.redo_stack.last()
}
pub fn start_transaction(&mut self) -> Option<TransactionId> {
self.start_transaction_at(Instant::now())
}
pub fn start_transaction_at(&mut self, now: Instant) -> Option<TransactionId> {
self.history.start_transaction(self.version.clone(), now)
self.history
.start_transaction(self.version.clone(), now, &mut self.local_clock)
}
pub fn end_transaction(&mut self) -> Option<(TransactionId, clock::Global)> {
@ -1189,8 +1172,8 @@ impl Buffer {
}
pub fn end_transaction_at(&mut self, now: Instant) -> Option<(TransactionId, clock::Global)> {
if let Some(transaction) = self.history.end_transaction(now) {
let since = transaction.start.clone();
if let Some(entry) = self.history.end_transaction(now) {
let since = entry.transaction.start.clone();
let id = self.history.group().unwrap();
Some((id, since))
} else {
@ -1198,16 +1181,16 @@ impl Buffer {
}
}
pub fn avoid_grouping_next_transaction(&mut self) {
self.history.avoid_grouping_next_transaction()
pub fn finalize_last_transaction(&mut self) -> Option<&Transaction> {
self.history.finalize_last_transaction()
}
pub fn base_text(&self) -> &Arc<str> {
&self.history.base_text
}
pub fn history(&self) -> impl Iterator<Item = &EditOperation> {
self.history.ops.values()
pub fn history(&self) -> impl Iterator<Item = &Operation> {
self.history.operations.values()
}
pub fn undo_history(&self) -> impl Iterator<Item = (&clock::Local, &[(clock::Local, u32)])> {
@ -1218,7 +1201,8 @@ impl Buffer {
}
pub fn undo(&mut self) -> Option<(TransactionId, Operation)> {
if let Some(transaction) = self.history.pop_undo().cloned() {
if let Some(entry) = self.history.pop_undo() {
let transaction = entry.transaction.clone();
let transaction_id = transaction.id;
let op = self.undo_or_redo(transaction).unwrap();
Some((transaction_id, op))
@ -1227,13 +1211,18 @@ impl Buffer {
}
}
pub fn undo_transaction(&mut self, transaction_id: TransactionId) -> Option<Operation> {
if let Some(transaction) = self.history.remove_from_undo(transaction_id).cloned() {
let op = self.undo_or_redo(transaction).unwrap();
Some(op)
} else {
None
}
pub fn undo_to_transaction(&mut self, transaction_id: TransactionId) -> Vec<Operation> {
let transactions = self
.history
.remove_from_undo(transaction_id)
.iter()
.map(|entry| entry.transaction.clone())
.collect::<Vec<_>>();
transactions
.into_iter()
.map(|transaction| self.undo_or_redo(transaction).unwrap())
.collect()
}
pub fn forget_transaction(&mut self, transaction_id: TransactionId) {
@ -1241,7 +1230,8 @@ impl Buffer {
}
pub fn redo(&mut self) -> Option<(TransactionId, Operation)> {
if let Some(transaction) = self.history.pop_redo().cloned() {
if let Some(entry) = self.history.pop_redo() {
let transaction = entry.transaction.clone();
let transaction_id = transaction.id;
let op = self.undo_or_redo(transaction).unwrap();
Some((transaction_id, op))
@ -1250,18 +1240,23 @@ impl Buffer {
}
}
pub fn redo_transaction(&mut self, transaction_id: TransactionId) -> Option<Operation> {
if let Some(transaction) = self.history.remove_from_redo(transaction_id).cloned() {
let op = self.undo_or_redo(transaction).unwrap();
Some(op)
} else {
None
}
pub fn redo_to_transaction(&mut self, transaction_id: TransactionId) -> Vec<Operation> {
let transactions = self
.history
.remove_from_redo(transaction_id)
.iter()
.map(|entry| entry.transaction.clone())
.collect::<Vec<_>>();
transactions
.into_iter()
.map(|transaction| self.undo_or_redo(transaction).unwrap())
.collect()
}
fn undo_or_redo(&mut self, transaction: Transaction) -> Result<Operation> {
let mut counts = HashMap::default();
for edit_id in transaction.edits {
for edit_id in transaction.edit_ids {
counts.insert(edit_id, self.undo_map.undo_count(edit_id) + 1);
}
@ -1272,20 +1267,18 @@ impl Buffer {
version: transaction.start.clone(),
};
self.apply_undo(&undo)?;
self.snapshot.version.observe(undo.id);
Ok(Operation::Undo {
let operation = Operation::Undo {
undo,
lamport_timestamp: self.lamport_clock.tick(),
})
};
self.snapshot.version.observe(operation.local_timestamp());
self.history.push(operation.clone());
Ok(operation)
}
pub fn push_transaction(
&mut self,
edit_ids: impl IntoIterator<Item = clock::Local>,
now: Instant,
) {
self.history.push_transaction(edit_ids, now);
pub fn push_transaction(&mut self, transaction: Transaction, now: Instant) {
self.history.push_transaction(transaction, now);
self.history.finalize_last_transaction();
}
pub fn subscribe(&mut self) -> Subscription {
@ -1294,13 +1287,13 @@ impl Buffer {
pub fn wait_for_edits(
&mut self,
edit_ids: &[clock::Local],
edit_ids: impl IntoIterator<Item = clock::Local>,
) -> impl 'static + Future<Output = ()> {
let mut futures = Vec::new();
for edit_id in edit_ids {
if !self.version.observed(*edit_id) {
if !self.version.observed(edit_id) {
let (tx, rx) = oneshot::channel();
self.edit_id_resolvers.entry(*edit_id).or_default().push(tx);
self.edit_id_resolvers.entry(edit_id).or_default().push(tx);
futures.push(rx);
}
}
@ -1404,7 +1397,7 @@ impl Buffer {
new_text
);
let op = self.edit(old_ranges.iter().cloned(), new_text.as_str());
(old_ranges, new_text, Operation::Edit(op))
(old_ranges, new_text, op)
}
pub fn randomly_undo_redo(&mut self, rng: &mut impl rand::Rng) -> Vec<Operation> {
@ -1412,7 +1405,8 @@ impl Buffer {
let mut ops = Vec::new();
for _ in 0..rng.gen_range(1..=5) {
if let Some(transaction) = self.history.undo_stack.choose(rng).cloned() {
if let Some(entry) = self.history.undo_stack.choose(rng) {
let transaction = entry.transaction.clone();
log::info!(
"undoing buffer {} transaction {:?}",
self.replica_id,
@ -1512,6 +1506,10 @@ impl BufferSnapshot {
self.visible_text.offset_to_point_utf16(offset)
}
pub fn point_to_point_utf16(&self, point: Point) -> PointUtf16 {
self.visible_text.point_to_point_utf16(point)
}
pub fn version(&self) -> &clock::Global {
&self.version
}
@ -1748,14 +1746,6 @@ impl BufferSnapshot {
self.visible_text.clip_point_utf16(point, bias)
}
// pub fn point_for_offset(&self, offset: usize) -> Result<Point> {
// if offset <= self.len() {
// Ok(self.text_summary_for_range(0..offset))
// } else {
// Err(anyhow!("offset out of bounds"))
// }
// }
pub fn edits_since<'a, D>(
&'a self,
since: &'a clock::Global,
@ -1766,6 +1756,42 @@ impl BufferSnapshot {
self.edits_since_in_range(since, Anchor::min()..Anchor::max())
}
pub fn edited_ranges_for_transaction<'a, D>(
&'a self,
transaction: &'a Transaction,
) -> impl 'a + Iterator<Item = Range<D>>
where
D: TextDimension,
{
let mut cursor = self.fragments.cursor::<(VersionedFullOffset, usize)>();
let mut rope_cursor = self.visible_text.cursor(0);
let cx = Some(transaction.end.clone());
let mut position = D::default();
transaction.ranges.iter().map(move |range| {
cursor.seek_forward(&VersionedFullOffset::Offset(range.start), Bias::Right, &cx);
let mut start_offset = cursor.start().1;
if cursor
.item()
.map_or(false, |fragment| fragment.is_visible(&self.undo_map))
{
start_offset += range.start - cursor.start().0.full_offset()
}
position.add_assign(&rope_cursor.summary(start_offset));
let start = position.clone();
cursor.seek_forward(&VersionedFullOffset::Offset(range.end), Bias::Left, &cx);
let mut end_offset = cursor.start().1;
if cursor
.item()
.map_or(false, |fragment| fragment.is_visible(&self.undo_map))
{
end_offset += range.end - cursor.start().0.full_offset();
}
position.add_assign(&rope_cursor.summary(end_offset));
start..position.clone()
})
}
pub fn edits_since_in_range<'a, D>(
&'a self,
since: &'a clock::Global,
@ -2178,6 +2204,20 @@ impl Operation {
operation_queue::Operation::lamport_timestamp(self).replica_id
}
pub fn local_timestamp(&self) -> clock::Local {
match self {
Operation::Edit(edit) => edit.timestamp.local(),
Operation::Undo { undo, .. } => undo.id,
}
}
pub fn as_edit(&self) -> Option<&EditOperation> {
match self {
Operation::Edit(edit) => Some(edit),
_ => None,
}
}
pub fn is_edit(&self) -> bool {
match self {
Operation::Edit { .. } => true,
@ -2260,6 +2300,34 @@ impl ToPoint for Point {
}
}
pub trait ToPointUtf16 {
fn to_point_utf16<'a>(&self, snapshot: &BufferSnapshot) -> PointUtf16;
}
impl ToPointUtf16 for Anchor {
fn to_point_utf16<'a>(&self, snapshot: &BufferSnapshot) -> PointUtf16 {
snapshot.summary_for_anchor(self)
}
}
impl ToPointUtf16 for usize {
fn to_point_utf16<'a>(&self, snapshot: &BufferSnapshot) -> PointUtf16 {
snapshot.offset_to_point_utf16(*self)
}
}
impl ToPointUtf16 for PointUtf16 {
fn to_point_utf16<'a>(&self, _: &BufferSnapshot) -> PointUtf16 {
*self
}
}
impl ToPointUtf16 for Point {
fn to_point_utf16<'a>(&self, snapshot: &BufferSnapshot) -> PointUtf16 {
snapshot.point_to_point_utf16(*self)
}
}
pub trait Clip {
fn clip(&self, bias: Bias, snapshot: &BufferSnapshot) -> Self;
}

View file

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

View file

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

View file

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

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"
guest_selections = "$selection.guests"
error_color = "$status.bad"
code_actions_indicator = "$text.3.color"
[editor.diagnostic_path_header]
background = "$state.active_line"

View file

@ -126,7 +126,7 @@ mod tests {
use super::*;
use editor::{DisplayPoint, Editor};
use gpui::{MutableAppContext, TestAppContext, ViewHandle};
use project::ProjectPath;
use project::{Fs, ProjectPath};
use serde_json::json;
use std::{
collections::HashSet,
@ -817,7 +817,10 @@ mod tests {
.active_pane()
.update(cx, |pane, cx| pane.close_item(editor2.id(), cx));
drop(editor2);
app_state.fs.as_fake().remove(Path::new("/root/a/file2"))
app_state
.fs
.as_fake()
.remove_file(Path::new("/root/a/file2"), Default::default())
})
.await
.unwrap();

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