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 image image --------- 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>> { + use proto::create_image_for_peer::Variant; + match envelope.payload.variant { + Some(Variant::State(state)) => { + let image_id = + ImageId::from(NonZeroU64::new(state.id).context("invalid image id")?); + + self.loading_remote_images_by_id.insert( + image_id, + LoadingRemoteImage { + state, + chunks: Vec::new(), + received_size: 0, + }, + ); + Ok(None) + } + Some(Variant::Chunk(chunk)) => { + let image_id = + ImageId::from(NonZeroU64::new(chunk.image_id).context("invalid image id")?); + + let loading = self + .loading_remote_images_by_id + .get_mut(&image_id) + .context("received chunk for unknown image")?; + + loading.received_size += chunk.data.len() as u64; + loading.chunks.push(chunk.data); + + if loading.received_size == loading.state.content_size { + let loading = self.loading_remote_images_by_id.remove(&image_id).unwrap(); + + let mut content = Vec::with_capacity(loading.received_size as usize); + for chunk_data in loading.chunks { + content.extend_from_slice(&chunk_data); + } + + let image_metadata = ImageItem::compute_metadata_from_bytes(&content).log_err(); + let image = create_gpui_image(content)?; + + let proto_file = loading.state.file.context("missing file in image state")?; + let worktree_id = WorktreeId::from_proto(proto_file.worktree_id); + let worktree = worktree_store + .read(cx) + .worktree_for_id(worktree_id, cx) + .context("worktree not found")?; + + let file = Arc::new( + worktree::File::from_proto(proto_file, worktree, cx) + .context("invalid file in image state")?, + ); + + let entity = cx.new(|_cx| ImageItem { + id: image_id, + file, + image, + image_metadata, + reload_task: None, + }); + + if let Some(listeners) = self.remote_image_listeners.remove(&image_id) { + for listener in listeners { + listener.send(Ok(entity.clone())).ok(); + } + } + + Ok(Some(entity)) + } else { + Ok(None) + } + } + None => { + log::warn!("Received CreateImageForPeer with no variant"); + Ok(None) + } + } + } + + // TODO: subscribe to worktree and update image contents or at least mark as dirty on file changes } impl ImageStoreImpl for Entity { @@ -520,6 +673,64 @@ impl ImageStoreImpl for Entity { fn as_local(&self) -> Option> { Some(self.clone()) } + + fn as_remote(&self) -> Option> { + None + } +} + +impl ImageStoreImpl for Entity { + fn open_image( + &self, + path: Arc, + worktree: Entity, + cx: &mut Context, + ) -> Task>> { + let worktree_id = worktree.read(cx).id().to_proto(); + let (project_id, client) = { + let store = self.read(cx); + (store.project_id, store.upstream_client.clone()) + }; + let remote_store = self.clone(); + + cx.spawn(async move |_image_store, cx| { + let response = client + .request(rpc::proto::OpenImageByPath { + project_id, + worktree_id, + path: path.to_proto(), + }) + .await?; + + let image_id = ImageId::from( + NonZeroU64::new(response.image_id).context("invalid image_id in response")?, + ); + + remote_store + .update(cx, |remote_store, cx| { + remote_store.wait_for_remote_image(image_id, cx) + })? + .await + }) + } + + fn reload_images( + &self, + _images: HashSet>, + _cx: &mut Context, + ) -> Task> { + Task::ready(Err(anyhow::anyhow!( + "Reloading images from remote is not supported" + ))) + } + + fn as_local(&self) -> Option> { + None + } + + fn as_remote(&self) -> Option> { + Some(self.clone()) + } } impl LocalImageStore { @@ -694,33 +905,6 @@ fn create_gpui_image(content: Vec) -> anyhow::Result> { ))) } -impl ImageStoreImpl for Entity { - fn open_image( - &self, - _path: Arc, - _worktree: Entity, - _cx: &mut Context, - ) -> Task>> { - Task::ready(Err(anyhow::anyhow!( - "Opening images from remote is not supported" - ))) - } - - fn reload_images( - &self, - _images: HashSet>, - _cx: &mut Context, - ) -> Task> { - Task::ready(Err(anyhow::anyhow!( - "Reloading images from remote is not supported" - ))) - } - - fn as_local(&self) -> Option> { - None - } -} - #[cfg(test)] mod tests { use super::*; @@ -782,4 +966,24 @@ mod tests { assert_eq!(image1, image2); } + + #[gpui::test] + fn test_compute_metadata_from_bytes() { + // Single white pixel PNG + let png_bytes = vec![ + 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, + 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, + 0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41, 0x54, 0x78, + 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4, 0x00, + 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82, + ]; + + let metadata = ImageItem::compute_metadata_from_bytes(&png_bytes).unwrap(); + + assert_eq!(metadata.width, 1); + assert_eq!(metadata.height, 1); + assert_eq!(metadata.file_size, png_bytes.len() as u64); + assert_eq!(metadata.format, image::ImageFormat::Png); + assert!(metadata.colors.is_some()); + } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 38a75726859c3c4e7d5b7835365e6faac8634e3e..17f811034a5112831bf0d8fe4e21abee15cc7a9c 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1037,6 +1037,7 @@ impl Project { client.add_entity_request_handler(Self::handle_open_new_buffer); client.add_entity_message_handler(Self::handle_create_buffer_for_peer); client.add_entity_message_handler(Self::handle_toggle_lsp_logs); + client.add_entity_message_handler(Self::handle_create_image_for_peer); WorktreeStore::init(&client); BufferStore::init(&client); @@ -1433,6 +1434,7 @@ impl Project { remote_proto.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &this.agent_server_store); remote_proto.add_entity_message_handler(Self::handle_create_buffer_for_peer); + remote_proto.add_entity_message_handler(Self::handle_create_image_for_peer); remote_proto.add_entity_message_handler(Self::handle_update_worktree); remote_proto.add_entity_message_handler(Self::handle_update_project); remote_proto.add_entity_message_handler(Self::handle_toast); @@ -2853,13 +2855,20 @@ impl Project { let weak_project = cx.entity().downgrade(); cx.spawn(async move |_, cx| { let image_item = open_image_task.await?; - let project = weak_project.upgrade().context("Project dropped")?; - let metadata = ImageItem::load_image_metadata(image_item.clone(), project, cx).await?; - image_item.update(cx, |image_item, cx| { - image_item.image_metadata = Some(metadata); - cx.emit(ImageItemEvent::MetadataUpdated); - })?; + // Check if metadata already exists (e.g., for remote images) + let needs_metadata = + cx.read_entity(&image_item, |item, _| item.image_metadata.is_none())?; + + if needs_metadata { + let project = weak_project.upgrade().context("Project dropped")?; + let metadata = + ImageItem::load_image_metadata(image_item.clone(), project, cx).await?; + image_item.update(cx, |image_item, cx| { + image_item.image_metadata = Some(metadata); + cx.emit(ImageItemEvent::MetadataUpdated); + })?; + } Ok(image_item) }) @@ -3323,6 +3332,7 @@ impl Project { event: &ImageItemEvent, cx: &mut Context, ) -> Option<()> { + // TODO: handle image events from remote if let ImageItemEvent::ReloadNeeded = event && !self.is_via_collab() { @@ -5060,6 +5070,20 @@ impl Project { buffer.read(cx).remote_id() } + async fn handle_create_image_for_peer( + this: Entity, + envelope: TypedEnvelope, + mut cx: AsyncApp, + ) -> Result<()> { + this.update(&mut cx, |this, cx| { + this.image_store.update(cx, |image_store, cx| { + image_store.handle_create_image_for_peer(envelope, cx) + }) + })? + .log_err(); + Ok(()) + } + fn synchronize_remote_buffers(&mut self, cx: &mut Context) -> Task> { let project_id = match self.client_state { ProjectClientState::Remote { diff --git a/crates/proto/proto/image.proto b/crates/proto/proto/image.proto new file mode 100644 index 0000000000000000000000000000000000000000..e3232e6847cbc719280bc3ccd5254e5e368dbeb6 --- /dev/null +++ b/crates/proto/proto/image.proto @@ -0,0 +1,36 @@ +syntax = "proto3"; +package zed.messages; + +import "core.proto"; +import "worktree.proto"; + +message OpenImageByPath { + uint64 project_id = 1; + uint64 worktree_id = 2; + string path = 3; +} + +message OpenImageResponse { + uint64 image_id = 1; +} + +message CreateImageForPeer { + uint64 project_id = 1; + PeerId peer_id = 2; + oneof variant { + ImageState state = 3; + ImageChunk chunk = 4; + } +} + +message ImageState { + uint64 id = 1; + optional File file = 2; + uint64 content_size = 3; + string format = 4; // e.g., "png", "jpeg", "webp", etc. +} + +message ImageChunk { + uint64 image_id = 1; + bytes data = 2; +} diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index c1551dca48a6d49eada9a0db65fbf770500d33b7..34987ba06754be2db31aea51b384e7e099dca728 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -9,6 +9,7 @@ import "channel.proto"; import "core.proto"; import "debugger.proto"; import "git.proto"; +import "image.proto"; import "lsp.proto"; import "notification.proto"; import "task.proto"; @@ -431,7 +432,11 @@ message Envelope { GitWorktreesResponse git_worktrees_response = 388; GitGetWorktrees git_get_worktrees = 389; - GitCreateWorktree git_create_worktree = 390; // current max + GitCreateWorktree git_create_worktree = 390; + + OpenImageByPath open_image_by_path = 391; + OpenImageResponse open_image_response = 392; + CreateImageForPeer create_image_for_peer = 393; // current max } reserved 87 to 88; diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index cd125aa70fa440001f514a4d651d85687ce2456b..0d9ffd5e0491a65e5ff39a67af5a2efd015476fc 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -51,6 +51,7 @@ messages!( (Commit, Background), (CopyProjectEntry, Foreground), (CreateBufferForPeer, Foreground), + (CreateImageForPeer, Foreground), (CreateChannel, Foreground), (CreateChannelResponse, Foreground), (CreateContext, Foreground), @@ -179,9 +180,11 @@ messages!( (OnTypeFormattingResponse, Background), (OpenBufferById, Background), (OpenBufferByPath, Background), + (OpenImageByPath, Background), (OpenBufferForSymbol, Background), (OpenBufferForSymbolResponse, Background), (OpenBufferResponse, Background), + (OpenImageResponse, Background), (OpenCommitMessageBuffer, Background), (OpenContext, Foreground), (OpenContextResponse, Foreground), @@ -397,6 +400,7 @@ request_messages!( (OnTypeFormatting, OnTypeFormattingResponse), (OpenBufferById, OpenBufferResponse), (OpenBufferByPath, OpenBufferResponse), + (OpenImageByPath, OpenImageResponse), (OpenBufferForSymbol, OpenBufferForSymbolResponse), (OpenCommitMessageBuffer, OpenBufferResponse), (OpenNewBuffer, OpenBufferResponse), @@ -545,6 +549,7 @@ entity_messages!( GetColorPresentation, CopyProjectEntry, CreateBufferForPeer, + CreateImageForPeer, CreateProjectEntry, GetDocumentColor, DeleteProjectEntry, @@ -581,6 +586,7 @@ entity_messages!( OpenNewBuffer, OpenBufferById, OpenBufferByPath, + OpenImageByPath, OpenBufferForSymbol, OpenCommitMessageBuffer, PerformRename, diff --git a/crates/remote_server/Cargo.toml b/crates/remote_server/Cargo.toml index 5034b24e0661eb87665d9b805b2bbc5d2ca577cd..f03851b9558d85514adde2afc10ad3f4cee77863 100644 --- a/crates/remote_server/Cargo.toml +++ b/crates/remote_server/Cargo.toml @@ -39,6 +39,7 @@ git2 = { workspace = true, features = ["vendored-libgit2"] } gpui.workspace = true gpui_tokio.workspace = true http_client.workspace = true +image.workspace = true json_schema_store.workspace = true language.workspace = true language_extension.workspace = true diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 355e8a3fe272ce95e1f75f3df0d2a2872ae0a1fe..e9d34f4bdb94db401c7786f55fca6c4e429af15d 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -1,4 +1,5 @@ use anyhow::{Context as _, Result, anyhow}; +use language::File; use lsp::LanguageServerId; use extension::ExtensionHostProxy; @@ -15,6 +16,7 @@ use project::{ buffer_store::{BufferStore, BufferStoreEvent}, debugger::{breakpoint_store::BreakpointStore, dap_store::DapStore}, git_store::GitStore, + image_store::ImageId, lsp_store::log_store::{self, GlobalLogStore, LanguageServerKind}, project_settings::SettingsObserver, search::SearchQuery, @@ -29,8 +31,12 @@ use rpc::{ use settings::{Settings as _, initial_server_settings_content}; use smol::stream::StreamExt; use std::{ + num::NonZeroU64, path::{Path, PathBuf}, - sync::{Arc, atomic::AtomicUsize}, + sync::{ + Arc, + atomic::{AtomicU64, AtomicUsize, Ordering}, + }, }; use sysinfo::{ProcessRefreshKind, RefreshKind, System, UpdateKind}; use util::{ResultExt, paths::PathStyle, rel_path::RelPath}; @@ -260,6 +266,7 @@ impl HeadlessProject { session.add_entity_request_handler(Self::handle_open_server_settings); session.add_entity_request_handler(Self::handle_get_directory_environment); session.add_entity_message_handler(Self::handle_toggle_lsp_logs); + session.add_entity_request_handler(Self::handle_open_image_by_path); session.add_entity_request_handler(BufferStore::handle_update_buffer); session.add_entity_message_handler(BufferStore::handle_close_buffer); @@ -525,6 +532,71 @@ impl HeadlessProject { }) } + pub async fn handle_open_image_by_path( + this: Entity, + message: TypedEnvelope, + mut cx: AsyncApp, + ) -> Result { + static NEXT_ID: AtomicU64 = AtomicU64::new(1); + let worktree_id = WorktreeId::from_proto(message.payload.worktree_id); + let path = RelPath::from_proto(&message.payload.path)?; + let project_id = message.payload.project_id; + use proto::create_image_for_peer::Variant; + + let (worktree_store, session) = this.read_with(&cx, |this, _| { + (this.worktree_store.clone(), this.session.clone()) + })?; + + let worktree = worktree_store + .read_with(&cx, |store, cx| store.worktree_for_id(worktree_id, cx))? + .context("worktree not found")?; + + let load_task = worktree.update(&mut cx, |worktree, cx| { + worktree.load_binary_file(path.as_ref(), cx) + })?; + + let loaded_file = load_task.await?; + let content = loaded_file.content; + let file = loaded_file.file; + + let proto_file = worktree.read_with(&cx, |_worktree, cx| file.to_proto(cx))?; + let image_id = + ImageId::from(NonZeroU64::new(NEXT_ID.fetch_add(1, Ordering::Relaxed)).unwrap()); + + let format = image::guess_format(&content) + .map(|f| format!("{:?}", f).to_lowercase()) + .unwrap_or_else(|_| "unknown".to_string()); + + let state = proto::ImageState { + id: image_id.to_proto(), + file: Some(proto_file), + content_size: content.len() as u64, + format, + }; + + session.send(proto::CreateImageForPeer { + project_id, + peer_id: Some(REMOTE_SERVER_PEER_ID), + variant: Some(Variant::State(state)), + })?; + + const CHUNK_SIZE: usize = 1024 * 1024; // 1MB chunks + for chunk in content.chunks(CHUNK_SIZE) { + session.send(proto::CreateImageForPeer { + project_id, + peer_id: Some(REMOTE_SERVER_PEER_ID), + variant: Some(Variant::Chunk(proto::ImageChunk { + image_id: image_id.to_proto(), + data: chunk.to_vec(), + })), + })?; + } + + Ok(proto::OpenImageResponse { + image_id: image_id.to_proto(), + }) + } + pub async fn handle_open_new_buffer( this: Entity, _message: TypedEnvelope,