diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index efda8097707033106ce15b86aa3b6cfe88a9ea1d..f8512b6550c0c45f2f9e5ef6baf2562e4703638c 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -691,6 +691,14 @@ impl Client { ) -> impl Future> { self.peer.respond(receipt, response) } + + pub fn respond_with_error( + &self, + receipt: Receipt, + error: proto::Error, + ) -> impl Future> { + self.peer.respond_with_error(receipt, error) + } } fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option { diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index b97f01ce69608944a229a07eadb496520d380e9d..8abcc76ef9fa20290e3919b1ee295e3daf666aed 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -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) -> 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(()) })) } diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index cd4a3207df053a1d27284b5993f3a3a495b36d9d..c7192cd622c51dbd43dbf58f5f561e045f64a2b9 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -798,6 +798,20 @@ impl MultiBuffer { cx.emit(event.clone()); } + pub fn format(&mut self, cx: &mut ModelContext) -> Task> { + 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) -> Result>> { let mut save_tasks = Vec::new(); for BufferState { buffer, .. } in self.buffers.borrow().values() { diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index c0c3d9c5f149894a3f8fdb16138c1362a1ca59da..e6b593f70d63f3c9411336a1faffca3e53f898bd 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -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>>; + fn format_remote(&self, buffer_id: u64, cx: &mut MutableAppContext) + -> Option>>; + 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) -> Task> { + 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::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, diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 9f7f9f75ac4d6b190210b940b2cec422308d6685..769bcbe69c03de41a4e61a417707a7c63dff9f62 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -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 { + let start = PointUtf16::new(range.start.line, range.start.character); + let end = PointUtf16::new(range.end.line, range.end.character); + start..end +} diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index c3d264e8a99f227156378c07e25a4dca726204fa..6d975e8e9fa87fd06a6112ca5694937fcdb09bf5 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -494,17 +494,25 @@ impl FakeLanguageServer { } pub async fn receive_request(&mut self) -> (RequestId, T::Params) { - self.receive().await; - let request = serde_json::from_slice::>(&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::>(&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(&mut self) -> T::Params { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 5f0c966f6f675a748cd6f936f4a7688e68f99ff2..af7a3d5939238c9afc1e47c6d6ddc6405dfb3b89 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -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, + rpc: Arc, + cx: &mut ModelContext, + ) -> 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, diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 7c16cf5f414238243a82dc5795b8444374261a2b..3d924b331072b238433186dee594e69c25c5b8a2 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -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, + rpc: Arc, + cx: &mut ModelContext, + ) -> 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) { 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>> { + 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 { - 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::*; diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index f6300c44952a0935d3eb1f3d90ccc15062b8d9e9..47774bf360f92dcac041b561540c40d20483245b 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -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; diff --git a/crates/rpc/src/peer.rs b/crates/rpc/src/peer.rs index 091a0c1555dc9a6cca7019780ae95894d4488c9d..9b6d8c8786a21078ad76452775944b7dc15db457 100644 --- a/crates/rpc/src/peer.rs +++ b/crates/rpc/src/peer.rs @@ -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() }), } diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 91abc2523d3c026567b0a3c4f83fa00115ab3cdd..8860bc5f0549b0a4341e5fe85526c299a5f1fa24 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -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, diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 76698c3a19171cb34a76b196db5dbb95ed805f7b..220c01ef1a67f3d79a7b31f7da962570349bbc12 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -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, + request: TypedEnvelope, + ) -> 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, request: TypedEnvelope, @@ -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::() + .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(); diff --git a/crates/text/src/rope.rs b/crates/text/src/rope.rs index 89ce278de1a65a2f53658b188cf6452c7d973960..d9c900d8bc40541128a619d8cd24219122e6b04b 100644 --- a/crates/text/src/rope.rs +++ b/crates/text/src/rope.rs @@ -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;