Implement Buffer::format

Co-Authored-By: Nathan Sobo <nathan@zed.dev>
This commit is contained in:
Antonio Scandurra 2022-01-12 18:01:20 +01:00
parent 67991b413c
commit 310def2923
13 changed files with 384 additions and 53 deletions

View file

@ -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> {

View file

@ -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(())
}))
}

View file

@ -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() {

View file

@ -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>,

View file

@ -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
}

View file

@ -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 {

View file

@ -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>,

View file

@ -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::*;

View file

@ -35,22 +35,23 @@ message Envelope {
UpdateBuffer update_buffer = 27;
SaveBuffer save_buffer = 28;
BufferSaved buffer_saved = 29;
FormatBuffer format_buffer = 30;
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;
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 = 40;
UpdateContacts update_contacts = 41;
GetUsers get_users = 41;
GetUsersResponse get_users_response = 42;
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;

View file

@ -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()
}),
}

View file

@ -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,

View file

@ -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();

View file

@ -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;