From a112153a2e8a18be8550c76f8532766acf15b3a6 Mon Sep 17 00:00:00 2001
From: Cave Bats Of Ware <556437+cavebatsofware@users.noreply.github.com>
Date: Thu, 6 Nov 2025 14:31:32 -0500
Subject: [PATCH] Enable image support in remote projects (#39158)
Adds support for opening and displaying images in remote projects. The
server streams image data to the client in chunks, where the client then
reconstructs the image and displays it. This change includes:
- Adding `image` crate as a dependency for remote_server
- Implementing `ImageStore` for remote access
- Creating proto definitions for image-related messages
- Adding handlers for creating images for peers
- Computing image metadata from bytes instead of reading from disk for
remote images
Closes #20430
Closes #39104
Closes #40445
Release Notes:
- Added support for image preview in remote sessions.
- Fixed #39104
---------
Co-authored-by: Julia Ryan
---
Cargo.lock | 1 +
crates/collab/src/rpc.rs | 22 ++
crates/project/src/image_store.rs | 312 +++++++++++++++----
crates/project/src/project.rs | 36 ++-
crates/proto/proto/image.proto | 36 +++
crates/proto/proto/zed.proto | 7 +-
crates/proto/src/proto.rs | 6 +
crates/remote_server/Cargo.toml | 1 +
crates/remote_server/src/headless_project.rs | 74 ++++-
9 files changed, 433 insertions(+), 62 deletions(-)
create mode 100644 crates/proto/proto/image.proto
diff --git a/Cargo.lock b/Cargo.lock
index bcfd7c3875d27f20cbe647c34555c4e915762fbe..ddc18ba3c0e5ce089d12139a28a737c05ca8de03 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -13969,6 +13969,7 @@ dependencies = [
"gpui",
"gpui_tokio",
"http_client",
+ "image",
"json_schema_store",
"language",
"language_extension",
diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs
index bfcab578f4b30357594cb460dfff53fd94d0ec05..f73631bb19c80a463ed38b78031dd0fe4d452681 100644
--- a/crates/collab/src/rpc.rs
+++ b/crates/collab/src/rpc.rs
@@ -346,6 +346,7 @@ impl Server {
.add_request_handler(forward_read_only_project_request::)
.add_request_handler(forward_read_only_project_request::)
.add_request_handler(forward_read_only_project_request::)
+ .add_request_handler(forward_read_only_project_request::)
.add_request_handler(forward_read_only_project_request::)
.add_request_handler(forward_read_only_project_request::)
.add_request_handler(forward_read_only_project_request::)
@@ -395,6 +396,7 @@ impl Server {
.add_request_handler(forward_mutating_project_request::)
.add_request_handler(forward_mutating_project_request::)
.add_message_handler(create_buffer_for_peer)
+ .add_message_handler(create_image_for_peer)
.add_request_handler(update_buffer)
.add_message_handler(broadcast_project_message_from_host::)
.add_message_handler(broadcast_project_message_from_host::)
@@ -2389,6 +2391,26 @@ async fn create_buffer_for_peer(
Ok(())
}
+/// Notify other participants that a new image has been created
+async fn create_image_for_peer(
+ request: proto::CreateImageForPeer,
+ session: MessageContext,
+) -> Result<()> {
+ session
+ .db()
+ .await
+ .check_user_is_project_host(
+ ProjectId::from_proto(request.project_id),
+ session.connection_id,
+ )
+ .await?;
+ let peer_id = request.peer_id.context("invalid peer id")?;
+ session
+ .peer
+ .forward_send(session.connection_id, peer_id.into(), request)?;
+ Ok(())
+}
+
/// Notify other participants that a buffer has been updated. This is
/// allowed for guests as long as the update is limited to selections.
async fn update_buffer(
diff --git a/crates/project/src/image_store.rs b/crates/project/src/image_store.rs
index 8fcf9c8a6172f866d819e34cbf3b0b4810a8fc8d..b6dcf32b1bd84757cdc0c9c453e7743bbbe3d909 100644
--- a/crates/project/src/image_store.rs
+++ b/crates/project/src/image_store.rs
@@ -11,16 +11,22 @@ use gpui::{
pub use image::ImageFormat;
use image::{ExtendedColorType, GenericImageView, ImageReader};
use language::{DiskState, File};
-use rpc::{AnyProtoClient, ErrorExt as _};
+use rpc::{AnyProtoClient, ErrorExt as _, TypedEnvelope, proto};
use std::num::NonZeroU64;
use std::path::PathBuf;
use std::sync::Arc;
use util::{ResultExt, rel_path::RelPath};
-use worktree::{LoadedBinaryFile, PathChange, Worktree};
+use worktree::{LoadedBinaryFile, PathChange, Worktree, WorktreeId};
#[derive(Clone, Copy, Debug, Hash, PartialEq, PartialOrd, Ord, Eq)]
pub struct ImageId(NonZeroU64);
+impl ImageId {
+ pub fn to_proto(&self) -> u64 {
+ self.0.get()
+ }
+}
+
impl std::fmt::Display for ImageId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
@@ -102,6 +108,24 @@ pub struct ImageItem {
}
impl ImageItem {
+ fn compute_metadata_from_bytes(image_bytes: &[u8]) -> Result {
+ let image_format = image::guess_format(image_bytes)?;
+
+ let mut image_reader = ImageReader::new(std::io::Cursor::new(image_bytes));
+ image_reader.set_format(image_format);
+ let image = image_reader.decode()?;
+
+ let (width, height) = image.dimensions();
+
+ Ok(ImageMetadata {
+ width,
+ height,
+ file_size: image_bytes.len() as u64,
+ format: image_format,
+ colors: ImageColorInfo::from_color_type(image.color()),
+ })
+ }
+
pub async fn load_image_metadata(
image: Entity,
project: Entity,
@@ -117,25 +141,7 @@ impl ImageItem {
})??;
let image_bytes = fs.load_bytes(&image_path).await?;
- let image_format = image::guess_format(&image_bytes)?;
-
- let mut image_reader = ImageReader::new(std::io::Cursor::new(image_bytes));
- image_reader.set_format(image_format);
- let image = image_reader.decode()?;
-
- let (width, height) = image.dimensions();
- let file_metadata = fs
- .metadata(image_path.as_path())
- .await?
- .context("failed to load image metadata")?;
-
- Ok(ImageMetadata {
- width,
- height,
- file_size: file_metadata.len,
- format: image_format,
- colors: ImageColorInfo::from_color_type(image.color()),
- })
+ Self::compute_metadata_from_bytes(&image_bytes)
}
pub fn project_path(&self, cx: &App) -> ProjectPath {
@@ -265,9 +271,23 @@ trait ImageStoreImpl {
) -> Task>;
fn as_local(&self) -> Option>;
+ fn as_remote(&self) -> Option>;
}
-struct RemoteImageStore {}
+struct RemoteImageStore {
+ upstream_client: AnyProtoClient,
+ project_id: u64,
+ loading_remote_images_by_id: HashMap,
+ remote_image_listeners:
+ HashMap>>>>,
+ loaded_images: HashMap>,
+}
+
+struct LoadingRemoteImage {
+ state: proto::ImageState,
+ chunks: Vec>,
+ received_size: u64,
+}
struct LocalImageStore {
local_image_ids_by_path: HashMap,
@@ -316,12 +336,18 @@ impl ImageStore {
pub fn remote(
worktree_store: Entity,
- _upstream_client: AnyProtoClient,
- _remote_id: u64,
+ upstream_client: AnyProtoClient,
+ project_id: u64,
cx: &mut Context,
) -> Self {
Self {
- state: Box::new(cx.new(|_| RemoteImageStore {})),
+ state: Box::new(cx.new(|_| RemoteImageStore {
+ upstream_client,
+ project_id,
+ loading_remote_images_by_id: Default::default(),
+ remote_image_listeners: Default::default(),
+ loaded_images: Default::default(),
+ })),
opened_images: Default::default(),
loading_images_by_path: Default::default(),
worktree_store,
@@ -429,9 +455,7 @@ impl ImageStore {
fn add_image(&mut self, image: Entity, cx: &mut Context) -> Result<()> {
let image_id = image.read(cx).id;
-
self.opened_images.insert(image_id, image.downgrade());
-
cx.subscribe(&image, Self::on_image_event).detach();
cx.emit(ImageStoreEvent::ImageAdded(image));
Ok(())
@@ -451,6 +475,135 @@ impl ImageStore {
})
}
}
+
+ pub fn handle_create_image_for_peer(
+ &mut self,
+ envelope: TypedEnvelope,
+ cx: &mut Context,
+ ) -> Result<()> {
+ if let Some(remote) = self.state.as_remote() {
+ let worktree_store = self.worktree_store.clone();
+ let image = remote.update(cx, |remote, cx| {
+ remote.handle_create_image_for_peer(envelope, &worktree_store, cx)
+ })?;
+ if let Some(image) = image {
+ remote.update(cx, |this, cx| {
+ let image = image.clone();
+ let image_id = image.read(cx).id;
+ this.loaded_images.insert(image_id, image)
+ });
+
+ self.add_image(image, cx)?;
+ }
+ }
+
+ Ok(())
+ }
+}
+
+impl RemoteImageStore {
+ pub fn wait_for_remote_image(
+ &mut self,
+ id: ImageId,
+ cx: &mut Context,
+ ) -> Task>> {
+ if let Some(image) = self.loaded_images.remove(&id) {
+ return Task::ready(Ok(image));
+ }
+
+ let (tx, rx) = oneshot::channel();
+ self.remote_image_listeners.entry(id).or_default().push(tx);
+
+ cx.spawn(async move |_this, cx| {
+ let result = cx.background_spawn(async move { rx.await? }).await;
+ result
+ })
+ }
+
+ pub fn handle_create_image_for_peer(
+ &mut self,
+ envelope: TypedEnvelope,
+ worktree_store: &Entity,
+ cx: &mut Context,
+ ) -> Result