project: Implement range formatting for remoting (#47194)

Closes https://github.com/zed-industries/zed/issues/42859

Release Notes:

- Added support for language range formatting in remoting setups
This commit is contained in:
Lukas Wirth authored and GitHub committed 2026-01-20 09:10:18 +00:00
1 parent 0abb39777d
commit 8e493cb434
3 files changed
+672 -555

No files matched your search

+92 -1
View File
@@ -11,7 +11,7 @@ use assistant_text_thread::TextThreadStore;
use buffer_diff::{DiffHunkSecondaryStatus, DiffHunkStatus, assert_hunks};
use call::{ActiveCall, ParticipantLocation, Room, room};
use client::{RECEIVE_TIMEOUT, User};
use collections::{HashMap, HashSet};
use collections::{BTreeMap, HashMap, HashSet};
use fs::{FakeFs, Fs as _, RemoveOptions};
use futures::{StreamExt as _, channel::mpsc};
use git::{
@@ -4641,6 +4641,97 @@ async fn test_formatting_buffer(
}
}
#[gpui::test(iterations = 10)]
async fn test_range_formatting_buffer(
executor: BackgroundExecutor,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
let mut server = TestServer::start(executor.clone()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.await;
let active_call_a = cx_a.read(ActiveCall::global);
client_a.language_registry().add(rust_lang());
let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
"Rust",
FakeLspAdapter {
capabilities: lsp::ServerCapabilities {
document_range_formatting_provider: Some(OneOf::Left(true)),
..Default::default()
},
..Default::default()
},
);
let directory = env::current_dir().unwrap();
client_a
.fs()
.insert_tree(&directory, json!({ "a.rs": "one\ntwo\nthree\n" }))
.await;
let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
let project_b = client_b.join_remote_project(project_id, cx_b).await;
let buffer_b = project_b
.update(cx_b, |p, cx| {
p.open_buffer((worktree_id, rel_path("a.rs")), cx)
})
.await
.unwrap();
let _handle = project_b.update(cx_b, |project, cx| {
project.register_buffer_with_language_servers(&buffer_b, cx)
});
let fake_language_server = fake_language_servers.next().await.unwrap();
executor.run_until_parked();
fake_language_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
|params, _| async move {
assert_eq!(params.range.start, lsp::Position::new(0, 0));
assert_eq!(params.range.end, lsp::Position::new(1, 3));
Ok(Some(vec![lsp::TextEdit::new(
lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
", ".to_string(),
)]))
},
);
let buffer_id = buffer_b.read_with(cx_b, |buffer, _| buffer.remote_id());
let ranges = buffer_b.read_with(cx_b, |buffer, _| {
let start = buffer.anchor_before(0);
let end = buffer.anchor_after(7);
vec![start..end]
});
let mut ranges_map = BTreeMap::new();
ranges_map.insert(buffer_id, ranges);
project_b
.update(cx_b, |project, cx| {
project.format(
HashSet::from_iter([buffer_b.clone()]),
LspFormatTarget::Ranges(ranges_map),
true,
FormatTrigger::Manual,
cx,
)
})
.await
.unwrap();
assert_eq!(
buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
"one, two\nthree\n"
);
}
#[gpui::test(iterations = 10)]
async fn test_prettier_formatting_buffer(
executor: BackgroundExecutor,
+34 -11
View File
@@ -69,8 +69,8 @@ use language::{
language_settings::{FormatOnSave, Formatter, LanguageSettings, language_settings},
point_to_lsp,
proto::{
deserialize_anchor, deserialize_lsp_edit, deserialize_version, serialize_anchor,
serialize_lsp_edit, serialize_version,
deserialize_anchor, deserialize_anchor_range, deserialize_lsp_edit, deserialize_version,
serialize_anchor, serialize_anchor_range, serialize_lsp_edit, serialize_version,
},
range_from_lsp, range_to_lsp,
row_chunk::RowChunk,
@@ -10748,14 +10748,17 @@ impl LspStore {
} else if let Some((client, project_id)) = self.upstream_client() {
zlog::trace!(logger => "Formatting remotely");
let logger = zlog::scoped!(logger => "remote");
// Don't support formatting ranges via remote
match target {
LspFormatTarget::Buffers => {}
LspFormatTarget::Ranges(_) => {
zlog::trace!(logger => "Ignoring unsupported remote range formatting request");
return Task::ready(Ok(ProjectTransaction::default()));
}
}
let buffer_ranges = match &target {
LspFormatTarget::Buffers => Vec::new(),
LspFormatTarget::Ranges(ranges) => ranges
.iter()
.map(|(buffer_id, ranges)| proto::BufferFormatRanges {
buffer_id: buffer_id.to_proto(),
ranges: ranges.iter().cloned().map(serialize_anchor_range).collect(),
})
.collect(),
};
let buffer_store = self.buffer_store();
cx.spawn(async move |lsp_store, cx| {
@@ -10769,6 +10772,7 @@ impl LspStore {
.iter()
.map(|buffer| buffer.read_with(cx, |buffer, _| buffer.remote_id().to_proto()))
.collect(),
buffer_ranges,
})
.await
.and_then(|result| result.transaction.context("missing transaction"));
@@ -10810,8 +10814,27 @@ impl LspStore {
let buffer_id = BufferId::new(*buffer_id)?;
buffers.insert(this.buffer_store.read(cx).get_existing(buffer_id)?);
}
let target = if envelope.payload.buffer_ranges.is_empty() {
LspFormatTarget::Buffers
} else {
let mut ranges_map = BTreeMap::new();
for buffer_range in &envelope.payload.buffer_ranges {
let buffer_id = BufferId::new(buffer_range.buffer_id)?;
let ranges: Result<Vec<_>> = buffer_range
.ranges
.iter()
.map(|range| {
deserialize_anchor_range(range.clone()).context("invalid anchor range")
})
.collect();
ranges_map.insert(buffer_id, ranges?);
}
LspFormatTarget::Ranges(ranges_map)
};
let trigger = FormatTrigger::from_proto(envelope.payload.trigger);
anyhow::Ok(this.format(buffers, LspFormatTarget::Buffers, false, trigger, cx))
anyhow::Ok(this.format(buffers, target, false, trigger, cx))
})?;
let project_transaction = format.await?;
File diff suppressed because it is too large. Load diff