Implement Buffer::format
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
This commit is contained in:
parent
67991b413c
commit
310def2923
13 changed files with 384 additions and 53 deletions
|
@ -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) {
|
||||
loop {
|
||||
self.receive().await;
|
||||
let request = serde_json::from_slice::<Request<T::Params>>(&self.buffer).unwrap();
|
||||
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;
|
||||
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;
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue