Detailed changes
@@ -691,6 +691,14 @@ impl Client {
) -> impl Future<Output = Result<()>> {
self.peer.respond(receipt, response)
}
+
+ pub fn respond_with_error<T: RequestMessage>(
+ &self,
+ receipt: Receipt<T>,
+ error: proto::Error,
+ ) -> impl Future<Output = Result<()>> {
+ self.peer.respond_with_error(receipt, error)
+ }
}
fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option<Credentials> {
@@ -1,4 +1,4 @@
-use crate::{Editor, Event};
+use crate::{Autoscroll, Editor, Event};
use crate::{MultiBuffer, ToPoint as _};
use anyhow::Result;
use gpui::{
@@ -11,6 +11,7 @@ use project::{File, ProjectPath, Worktree};
use std::fmt::Write;
use std::path::Path;
use text::{Point, Selection};
+use util::TryFutureExt;
use workspace::{
ItemHandle, ItemView, ItemViewHandle, PathOpener, Settings, StatusItemView, WeakItemHandle,
Workspace,
@@ -141,9 +142,17 @@ impl ItemView for Editor {
}
fn save(&mut self, cx: &mut ViewContext<Self>) -> Result<Task<Result<()>>> {
- let save = self.buffer().update(cx, |b, cx| b.save(cx))?;
- Ok(cx.spawn(|_, _| async move {
- save.await?;
+ let buffer = self.buffer().clone();
+ Ok(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| {
+ editor.request_autoscroll(Autoscroll::Fit, cx)
+ });
+ buffer
+ .update(&mut cx, |buffer, cx| buffer.save(cx))?
+ .await?;
Ok(())
}))
}
@@ -798,6 +798,20 @@ impl MultiBuffer {
cx.emit(event.clone());
}
+ pub fn format(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
+ let mut format_tasks = Vec::new();
+ for BufferState { buffer, .. } in self.buffers.borrow().values() {
+ format_tasks.push(buffer.update(cx, |buffer, cx| buffer.format(cx)));
+ }
+
+ cx.spawn(|_, _| async move {
+ for format in format_tasks {
+ format.await?;
+ }
+ Ok(())
+ })
+ }
+
pub fn save(&mut self, cx: &mut ModelContext<Self>) -> Result<Task<Result<()>>> {
let mut save_tasks = Vec::new();
for BufferState { buffer, .. } in self.buffers.borrow().values() {
@@ -1,10 +1,13 @@
-use crate::diagnostic_set::{DiagnosticEntry, DiagnosticGroup};
pub use crate::{
diagnostic_set::DiagnosticSet,
highlight_map::{HighlightId, HighlightMap},
proto, BracketPair, Grammar, Language, LanguageConfig, LanguageRegistry, LanguageServerConfig,
PLAIN_TEXT,
};
+use crate::{
+ diagnostic_set::{DiagnosticEntry, DiagnosticGroup},
+ range_from_lsp, ToPointUtf16,
+};
use anyhow::{anyhow, Result};
use clock::ReplicaId;
use futures::FutureExt as _;
@@ -180,6 +183,9 @@ pub trait File {
fn load_local(&self, cx: &AppContext) -> Option<Task<Result<String>>>;
+ fn format_remote(&self, buffer_id: u64, cx: &mut MutableAppContext)
+ -> Option<Task<Result<()>>>;
+
fn buffer_updated(&self, buffer_id: u64, operation: Operation, cx: &mut MutableAppContext);
fn buffer_removed(&self, buffer_id: u64, cx: &mut MutableAppContext);
@@ -437,6 +443,65 @@ impl Buffer {
self.file.as_deref()
}
+ pub fn format(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
+ let file = if let Some(file) = self.file.as_ref() {
+ file
+ } else {
+ return Task::ready(Err(anyhow!("buffer has no file")));
+ };
+
+ if let Some(LanguageServerState { server, .. }) = self.language_server.as_ref() {
+ let server = server.clone();
+ let abs_path = file.abs_path().unwrap();
+ let version = self.version();
+ cx.spawn(|this, mut cx| async move {
+ let edits = server
+ .request::<lsp::request::Formatting>(lsp::DocumentFormattingParams {
+ text_document: lsp::TextDocumentIdentifier::new(
+ lsp::Url::from_file_path(&abs_path).unwrap(),
+ ),
+ options: Default::default(),
+ work_done_progress_params: Default::default(),
+ })
+ .await?;
+
+ if let Some(edits) = edits {
+ this.update(&mut cx, |this, cx| {
+ if this.version == version {
+ for edit in &edits {
+ let range = range_from_lsp(edit.range);
+ if this.clip_point_utf16(range.start, Bias::Left) != range.start
+ || this.clip_point_utf16(range.end, Bias::Left) != range.end
+ {
+ return Err(anyhow!(
+ "invalid formatting edits received from language server"
+ ));
+ }
+ }
+
+ for edit in edits.into_iter().rev() {
+ this.edit([range_from_lsp(edit.range)], edit.new_text, cx);
+ }
+ Ok(())
+ } else {
+ Err(anyhow!("buffer edited since starting to format"))
+ }
+ })
+ } else {
+ Ok(())
+ }
+ })
+ } else {
+ let format = file.format_remote(self.remote_id(), cx.as_mut());
+ cx.spawn(|_, _| async move {
+ if let Some(format) = format {
+ format.await?;
+ }
+ Ok(())
+ })
+ }
+ }
+
pub fn save(
&mut self,
cx: &mut ModelContext<Self>,
@@ -15,7 +15,7 @@ use highlight_map::HighlightMap;
use lazy_static::lazy_static;
use parking_lot::Mutex;
use serde::Deserialize;
-use std::{path::Path, str, sync::Arc};
+use std::{ops::Range, path::Path, str, sync::Arc};
use theme::SyntaxTheme;
use tree_sitter::{self, Query};
pub use tree_sitter::{Parser, Tree};
@@ -33,6 +33,10 @@ lazy_static! {
));
}
+pub trait ToPointUtf16 {
+ fn to_point_utf16(self) -> PointUtf16;
+}
+
#[derive(Default, Deserialize)]
pub struct LanguageConfig {
pub name: String,
@@ -244,3 +248,15 @@ impl LanguageServerConfig {
)
}
}
+
+impl ToPointUtf16 for lsp::Position {
+ fn to_point_utf16(self) -> PointUtf16 {
+ PointUtf16::new(self.line, self.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);
+ start..end
+}
@@ -494,17 +494,25 @@ impl FakeLanguageServer {
}
pub async fn receive_request<T: request::Request>(&mut self) -> (RequestId<T>, T::Params) {
- self.receive().await;
- let request = serde_json::from_slice::<Request<T::Params>>(&self.buffer).unwrap();
- assert_eq!(request.method, T::METHOD);
- assert_eq!(request.jsonrpc, JSON_RPC_VERSION);
- (
- RequestId {
- id: request.id,
- _type: std::marker::PhantomData,
- },
- request.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,
+ );
+ } else {
+ println!(
+ "skipping message in fake language server {:?}",
+ std::str::from_utf8(&self.buffer)
+ );
+ }
+ }
}
pub async fn receive_notification<T: notification::Notification>(&mut self) -> T::Params {
@@ -308,6 +308,7 @@ impl Project {
client.subscribe_to_entity(remote_id, cx, Self::handle_update_buffer),
client.subscribe_to_entity(remote_id, cx, Self::handle_save_buffer),
client.subscribe_to_entity(remote_id, cx, Self::handle_buffer_saved),
+ client.subscribe_to_entity(remote_id, cx, Self::handle_format_buffer),
]);
}
}
@@ -808,6 +809,21 @@ impl Project {
Ok(())
}
+ pub fn handle_format_buffer(
+ &mut self,
+ envelope: TypedEnvelope<proto::FormatBuffer>,
+ rpc: Arc<Client>,
+ cx: &mut ModelContext<Self>,
+ ) -> Result<()> {
+ let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
+ if let Some(worktree) = self.worktree_for_id(worktree_id, cx) {
+ worktree.update(cx, |worktree, cx| {
+ worktree.handle_format_buffer(envelope, rpc, cx)
+ })?;
+ }
+ Ok(())
+ }
+
pub fn handle_open_buffer(
&mut self,
envelope: TypedEnvelope<proto::OpenBuffer>,
@@ -15,8 +15,8 @@ use gpui::{
Task, UpgradeModelHandle, WeakModelHandle,
};
use language::{
- Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, File as _, Language, LanguageRegistry,
- Operation, PointUtf16, Rope,
+ range_from_lsp, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, File as _, Language,
+ LanguageRegistry, Operation, PointUtf16, Rope,
};
use lazy_static::lazy_static;
use lsp::LanguageServer;
@@ -34,7 +34,7 @@ use std::{
ffi::{OsStr, OsString},
fmt,
future::Future,
- ops::{Deref, Range},
+ ops::Deref,
path::{Path, PathBuf},
sync::{
atomic::{AtomicUsize, Ordering::SeqCst},
@@ -580,6 +580,49 @@ impl Worktree {
Ok(())
}
+ pub fn handle_format_buffer(
+ &mut self,
+ envelope: TypedEnvelope<proto::FormatBuffer>,
+ rpc: Arc<Client>,
+ cx: &mut ModelContext<Self>,
+ ) -> Result<()> {
+ let sender_id = envelope.original_sender_id()?;
+ let this = self.as_local().unwrap();
+ let buffer = this
+ .shared_buffers
+ .get(&sender_id)
+ .and_then(|shared_buffers| shared_buffers.get(&envelope.payload.buffer_id).cloned())
+ .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id))?;
+
+ let receipt = envelope.receipt();
+ cx.spawn(|_, mut cx| async move {
+ let format = buffer.update(&mut cx, |buffer, cx| buffer.format(cx)).await;
+ // We spawn here in order to enqueue the sending of `Ack` *after* transmission of edits
+ // associated with formatting.
+ cx.spawn(|_| async move {
+ dbg!("responding");
+ match format {
+ Ok(()) => rpc.respond(receipt, proto::Ack {}).await?,
+ Err(error) => {
+ rpc.respond_with_error(
+ receipt,
+ proto::Error {
+ message: error.to_string(),
+ },
+ )
+ .await?
+ }
+ }
+ Ok::<_, anyhow::Error>(())
+ })
+ .await
+ .log_err();
+ })
+ .detach();
+
+ Ok(())
+ }
+
fn poll_snapshot(&mut self, cx: &mut ModelContext<Self>) {
match self {
Self::Local(worktree) => {
@@ -880,6 +923,7 @@ impl Worktree {
)),
} {
cx.spawn(|worktree, mut cx| async move {
+ dbg!(&operation);
if let Err(error) = rpc
.request(proto::UpdateBuffer {
project_id,
@@ -2259,6 +2303,27 @@ 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_id = worktree.id().to_proto();
+ 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,
+ worktree_id,
+ buffer_id,
+ })
+ .await?;
+ Ok(())
+ }))
+ }
+
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);
@@ -3180,22 +3245,6 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry {
}
}
-trait ToPointUtf16 {
- fn to_point_utf16(self) -> PointUtf16;
-}
-
-impl ToPointUtf16 for lsp::Position {
- fn to_point_utf16(self) -> PointUtf16 {
- PointUtf16::new(self.line, self.character)
- }
-}
-
-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);
- start..end
-}
-
#[cfg(test)]
mod tests {
use super::*;
@@ -35,22 +35,23 @@ message Envelope {
UpdateBuffer update_buffer = 27;
SaveBuffer save_buffer = 28;
BufferSaved buffer_saved = 29;
-
- GetChannels get_channels = 30;
- GetChannelsResponse get_channels_response = 31;
- JoinChannel join_channel = 32;
- JoinChannelResponse join_channel_response = 33;
- LeaveChannel leave_channel = 34;
- SendChannelMessage send_channel_message = 35;
- SendChannelMessageResponse send_channel_message_response = 36;
- ChannelMessageSent channel_message_sent = 37;
- GetChannelMessages get_channel_messages = 38;
- GetChannelMessagesResponse get_channel_messages_response = 39;
-
- UpdateContacts update_contacts = 40;
-
- GetUsers get_users = 41;
- GetUsersResponse get_users_response = 42;
+ FormatBuffer format_buffer = 30;
+
+ GetChannels get_channels = 31;
+ GetChannelsResponse get_channels_response = 32;
+ JoinChannel join_channel = 33;
+ JoinChannelResponse join_channel_response = 34;
+ LeaveChannel leave_channel = 35;
+ SendChannelMessage send_channel_message = 36;
+ SendChannelMessageResponse send_channel_message_response = 37;
+ ChannelMessageSent channel_message_sent = 38;
+ GetChannelMessages get_channel_messages = 39;
+ GetChannelMessagesResponse get_channel_messages_response = 40;
+
+ UpdateContacts update_contacts = 41;
+
+ GetUsers get_users = 42;
+ GetUsersResponse get_users_response = 43;
}
}
@@ -168,6 +169,12 @@ message BufferSaved {
Timestamp mtime = 5;
}
+message FormatBuffer {
+ uint64 project_id = 1;
+ uint64 worktree_id = 2;
+ uint64 buffer_id = 3;
+}
+
message UpdateDiagnosticSummary {
uint64 project_id = 1;
uint64 worktree_id = 2;
@@ -398,7 +398,7 @@ mod tests {
proto::OpenBufferResponse {
buffer: Some(proto::Buffer {
id: 101,
- visible_text: "path/one content".to_string(),
+ content: "path/one content".to_string(),
..Default::default()
}),
}
@@ -419,7 +419,7 @@ mod tests {
proto::OpenBufferResponse {
buffer: Some(proto::Buffer {
id: 102,
- visible_text: "path/two content".to_string(),
+ content: "path/two content".to_string(),
..Default::default()
}),
}
@@ -448,7 +448,7 @@ mod tests {
proto::OpenBufferResponse {
buffer: Some(proto::Buffer {
id: 101,
- visible_text: "path/one content".to_string(),
+ content: "path/one content".to_string(),
..Default::default()
}),
}
@@ -458,7 +458,7 @@ mod tests {
proto::OpenBufferResponse {
buffer: Some(proto::Buffer {
id: 102,
- visible_text: "path/two content".to_string(),
+ content: "path/two content".to_string(),
..Default::default()
}),
}
@@ -128,6 +128,7 @@ messages!(
DiskBasedDiagnosticsUpdated,
DiskBasedDiagnosticsUpdating,
Error,
+ FormatBuffer,
GetChannelMessages,
GetChannelMessagesResponse,
GetChannels,
@@ -162,6 +163,7 @@ messages!(
);
request_messages!(
+ (FormatBuffer, Ack),
(GetChannelMessages, GetChannelMessagesResponse),
(GetChannels, GetChannelsResponse),
(GetUsers, GetUsersResponse),
@@ -185,6 +187,7 @@ entity_messages!(
CloseBuffer,
DiskBasedDiagnosticsUpdated,
DiskBasedDiagnosticsUpdating,
+ FormatBuffer,
JoinProject,
LeaveProject,
OpenBuffer,
@@ -79,6 +79,7 @@ impl Server {
.add_handler(Server::update_buffer)
.add_handler(Server::buffer_saved)
.add_handler(Server::save_buffer)
+ .add_handler(Server::format_buffer)
.add_handler(Server::get_channels)
.add_handler(Server::get_users)
.add_handler(Server::join_channel)
@@ -660,6 +661,30 @@ impl Server {
Ok(())
}
+ async fn format_buffer(
+ self: Arc<Server>,
+ request: TypedEnvelope<proto::FormatBuffer>,
+ ) -> tide::Result<()> {
+ let host;
+ {
+ let state = self.state();
+ let project = state
+ .read_project(request.payload.project_id, request.sender_id)
+ .ok_or_else(|| anyhow!(NO_SUCH_PROJECT))?;
+ host = project.host_connection_id;
+ }
+
+ let sender = request.sender_id;
+ let receipt = request.receipt();
+ let response = self
+ .peer
+ .forward_request(sender, host, request.payload.clone())
+ .await?;
+ self.peer.respond(receipt, response).await?;
+
+ Ok(())
+ }
+
async fn update_buffer(
self: Arc<Server>,
request: TypedEnvelope<proto::UpdateBuffer>,
@@ -2001,6 +2026,111 @@ mod tests {
});
}
+ #[gpui::test(iterations = 1, seed = 2)]
+ async fn test_formatting_buffer(mut cx_a: TestAppContext, mut cx_b: TestAppContext) {
+ cx_a.foreground().forbid_parking();
+ let mut lang_registry = Arc::new(LanguageRegistry::new());
+ let fs = Arc::new(FakeFs::new());
+
+ // Set up a fake language server.
+ let (language_server_config, mut fake_language_server) =
+ LanguageServerConfig::fake(cx_a.background()).await;
+ Arc::get_mut(&mut lang_registry)
+ .unwrap()
+ .add(Arc::new(Language::new(
+ LanguageConfig {
+ name: "Rust".to_string(),
+ path_suffixes: vec!["rs".to_string()],
+ language_server: Some(language_server_config),
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ )));
+
+ // Connect to a server as 2 clients.
+ let mut server = TestServer::start(cx_a.foreground()).await;
+ let client_a = server.create_client(&mut cx_a, "user_a").await;
+ let client_b = server.create_client(&mut cx_b, "user_b").await;
+
+ // Share a project as client A
+ fs.insert_tree(
+ "/a",
+ json!({
+ ".zed.toml": r#"collaborators = ["user_b"]"#,
+ "a.rs": "let one = two",
+ }),
+ )
+ .await;
+ let project_a = cx_a.update(|cx| {
+ Project::local(
+ client_a.clone(),
+ client_a.user_store.clone(),
+ lang_registry.clone(),
+ fs.clone(),
+ cx,
+ )
+ });
+ let worktree_a = project_a
+ .update(&mut cx_a, |p, cx| p.add_local_worktree("/a", cx))
+ .await
+ .unwrap();
+ worktree_a
+ .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
+ .await;
+ let project_id = project_a
+ .update(&mut cx_a, |project, _| project.next_remote_id())
+ .await;
+ project_a
+ .update(&mut cx_a, |project, cx| project.share(cx))
+ .await
+ .unwrap();
+
+ // Join the worktree as client B.
+ let project_b = Project::remote(
+ project_id,
+ client_b.clone(),
+ client_b.user_store.clone(),
+ lang_registry.clone(),
+ fs.clone(),
+ &mut cx_b.to_async(),
+ )
+ .await
+ .unwrap();
+
+ // Open the file to be formatted on client B.
+ let worktree_b = project_b.update(&mut cx_b, |p, _| p.worktrees()[0].clone());
+ let buffer_b = cx_b
+ .background()
+ .spawn(worktree_b.update(&mut cx_b, |worktree, cx| worktree.open_buffer("a.rs", cx)))
+ .await
+ .unwrap();
+
+ let format = buffer_b.update(&mut cx_b, |buffer, cx| buffer.format(cx));
+ let (request_id, _) = fake_language_server
+ .receive_request::<lsp::request::Formatting>()
+ .await;
+ fake_language_server
+ .respond(
+ request_id,
+ Some(vec![
+ lsp::TextEdit {
+ range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 4)),
+ new_text: "h".to_string(),
+ },
+ lsp::TextEdit {
+ range: lsp::Range::new(lsp::Position::new(0, 7), lsp::Position::new(0, 7)),
+ new_text: "y".to_string(),
+ },
+ ]),
+ )
+ .await;
+ format.await.unwrap();
+ assert_eq!(
+ buffer_b.read_with(&cx_b, |buffer, _| buffer.text()),
+ "let honey = two"
+ );
+ }
+
#[gpui::test]
async fn test_basic_chat(mut cx_a: TestAppContext, mut cx_b: TestAppContext) {
cx_a.foreground().forbid_parking();
@@ -593,6 +593,12 @@ impl Chunk {
if ch == '\n' {
point.row += 1;
+ if point.row > target.row {
+ panic!(
+ "point {:?} is beyond the end of a line with length {}",
+ target, point.column
+ );
+ }
point.column = 0;
} else {
point.column += ch.len_utf16() as u32;