diff --git a/crates/agent_ui/src/conversation_view.rs b/crates/agent_ui/src/conversation_view.rs index 1d0276c4d9..281e4797ab 100644 --- a/crates/agent_ui/src/conversation_view.rs +++ b/crates/agent_ui/src/conversation_view.rs @@ -3465,7 +3465,7 @@ fn render_agent_markdown( wrap_button_visibility: markdown::WrapButtonVisibility::VisibleOnHover, border: false, }) - .image_resolver(move |dest_url| resolve_agent_image(dest_url, &worktree_roots)) + .image_resolver(move |dest_url, _cx| resolve_agent_image(dest_url, &worktree_roots)) .on_url_click(move |text, window, cx| { thread_view::open_link(text, &workspace, window, cx); }) diff --git a/crates/agent_ui/src/inline_prompt_editor.rs b/crates/agent_ui/src/inline_prompt_editor.rs index b046a5dfed..9d506aa5e4 100644 --- a/crates/agent_ui/src/inline_prompt_editor.rs +++ b/crates/agent_ui/src/inline_prompt_editor.rs @@ -1160,7 +1160,7 @@ impl PromptEditor { fn render_markdown(&self, markdown: Entity, style: MarkdownStyle) -> MarkdownElement { MarkdownElement::new(markdown, style) - .image_resolver(|dest_url| crate::resolve_agent_image(dest_url, &[])) + .image_resolver(|dest_url, _cx| crate::resolve_agent_image(dest_url, &[])) } } diff --git a/crates/markdown/src/html/html_rendering.rs b/crates/markdown/src/html/html_rendering.rs index 00b73f1e44..f9c1568445 100644 --- a/crates/markdown/src/html/html_rendering.rs +++ b/crates/markdown/src/html/html_rendering.rs @@ -136,7 +136,7 @@ impl MarkdownElement { self.render_html_table(table, source_allocator, builder, markdown_end, cx); } ParsedHtmlElement::Image(image) => { - self.render_html_image(image, builder); + self.render_html_image(image, builder, cx); } } } @@ -358,7 +358,7 @@ impl MarkdownElement { self.render_html_text(text, source_allocator, builder, cx); } HtmlParagraphChunk::Image(image) => { - self.render_html_image(image, builder); + self.render_html_image(image, builder, cx); } } } @@ -447,11 +447,11 @@ impl MarkdownElement { } } - fn render_html_image(&self, image: &HtmlImage, builder: &mut MarkdownElementBuilder) { + fn render_html_image(&self, image: &HtmlImage, builder: &mut MarkdownElementBuilder, cx: &App) { let Some(source) = self .image_resolver .as_ref() - .and_then(|resolve| resolve(image.dest_url.as_ref())) + .and_then(|resolve| resolve(image.dest_url.as_ref(), cx)) else { return; }; diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index 07b4d9e962..55ccd4dfea 100644 --- a/crates/markdown/src/markdown.rs +++ b/crates/markdown/src/markdown.rs @@ -1511,7 +1511,7 @@ pub struct MarkdownElement { on_source_click: Option, on_checkbox_toggle: Option, on_mermaid_zoom: Option, - image_resolver: Option Option>>, + image_resolver: Option Option>>, show_root_block_markers: bool, autoscroll: AutoscrollBehavior, /// Test-only hook to observe the laid-out text when this element is @@ -1629,7 +1629,7 @@ impl MarkdownElement { pub fn image_resolver( mut self, - resolver: impl Fn(&str) -> Option + 'static, + resolver: impl Fn(&str, &App) -> Option + 'static, ) -> Self { self.image_resolver = Some(Box::new(resolver)); self @@ -2499,7 +2499,7 @@ impl Element for MarkdownElement { } else if let Some(source) = self .image_resolver .as_ref() - .and_then(|resolve| resolve(dest_url.as_ref())) + .and_then(|resolve| resolve(dest_url.as_ref(), cx)) { current_img_block_range = Some(range.clone()); self.push_markdown_image( @@ -4823,7 +4823,7 @@ mod tests { fn render_markdown_with_image_resolver( markdown: &str, options: MarkdownOptions, - resolver: impl Fn(&str) -> Option + 'static, + resolver: impl Fn(&str, &App) -> Option + 'static, cx: &mut TestAppContext, ) -> RenderedText { ensure_theme_initialized(cx); @@ -4866,7 +4866,7 @@ mod tests { let rendered = render_markdown_with_image_resolver( "Here is an image ![alt](https://example.com/a.png) and more text\nthat continues", MarkdownOptions::default(), - move |_| Some(ImageSource::Render(image.clone())), + move |_, _| Some(ImageSource::Render(image.clone())), cx, ); let text: String = rendered @@ -4890,7 +4890,7 @@ mod tests { let rendered = render_markdown_with_image_resolver( "![alt](https://example.com/a.png)\ncaption", MarkdownOptions::default(), - move |_| Some(ImageSource::Render(image.clone())), + move |_, _| Some(ImageSource::Render(image.clone())), cx, ); let text: String = rendered @@ -4913,7 +4913,7 @@ mod tests { parse_html: true, ..Default::default() }, - move |_| Some(ImageSource::Render(image.clone())), + move |_, _| Some(ImageSource::Render(image.clone())), cx, ); let stray_whitespace_line = rendered.lines.iter().find_map(|line| { @@ -4936,7 +4936,7 @@ mod tests { parse_html: true, ..Default::default() }, - move |_| Some(ImageSource::Render(image.clone())), + move |_, _| Some(ImageSource::Render(image.clone())), cx, ); let text: String = rendered @@ -4979,7 +4979,7 @@ mod tests { size(px(600.0), px(600.0)), |_window, _cx| { MarkdownElement::new(markdown.clone(), style) - .image_resolver(move |_| Some(ImageSource::Render(image.clone()))) + .image_resolver(move |_, _| Some(ImageSource::Render(image.clone()))) .code_block_renderer(CodeBlockRenderer::Default { copy_button_visibility: CopyButtonVisibility::Hidden, wrap_button_visibility: WrapButtonVisibility::Hidden, @@ -5818,7 +5818,7 @@ mod tests { let hovered_urls = self.hovered_urls.clone(); div().size_full().child( MarkdownElement::new(self.markdown.clone(), MarkdownStyle::default()) - .image_resolver(|_| Some(loaded_image_source())) + .image_resolver(|_, _| Some(loaded_image_source())) .on_url_hover(move |url, _, _| { hovered_urls.borrow_mut().push(url); }), @@ -5961,7 +5961,7 @@ mod tests { let image_source = self.image_source.clone(); div().size_full().child( MarkdownElement::new(self.markdown.clone(), MarkdownStyle::default()) - .image_resolver(move |_| Some(image_source.clone())), + .image_resolver(move |_, _| Some(image_source.clone())), ) } } diff --git a/crates/markdown_preview/src/markdown_preview_view.rs b/crates/markdown_preview/src/markdown_preview_view.rs index 4c824b574d..d6946a877a 100644 --- a/crates/markdown_preview/src/markdown_preview_view.rs +++ b/crates/markdown_preview/src/markdown_preview_view.rs @@ -23,7 +23,7 @@ use markdown::{ MarkdownOptions, MarkdownStyle, }; use project::search::SearchQuery; -use project::{Project, ProjectPath}; +use project::{Project, ProjectPath, image_store}; use settings::{SeedQuerySetting, Settings, update_settings_file}; use theme::{SystemAppearance, Theme, ThemeRegistry}; use theme_settings::ThemeSettings; @@ -34,7 +34,8 @@ use ui::{ use util::{ ResultExt, markdown::{source_position_from_fragment, split_local_url_fragment}, - paths::PathWithPosition, + paths::{PathStyle, PathWithPosition}, + rel_path::RelPath, }; use workspace::item::{Item, ItemBufferKind, ItemHandle, SaveOptions, SerializableItem}; use workspace::notifications::{NotifyResultExt, NotifyTaskExt}; @@ -701,21 +702,19 @@ impl MarkdownPreviewView { /// The absolute path of the file that is currently being previewed. fn get_folder_for_active_editor(editor: &Editor, cx: &App) -> Option { let file = editor.file_at(MultiBufferOffset(0), cx)?; + let project_path = ProjectPath::from_file(file.as_ref(), cx); let absolute_path = editor .project() - .and_then(|project| { - project.read(cx).absolute_path( - &ProjectPath { - worktree_id: file.worktree_id(cx), - path: file.path().clone(), - }, - cx, - ) - }) + .and_then(|project| project.read(cx).absolute_path(&project_path, cx)) .or_else(|| file.as_local().map(|file| file.abs_path(cx)))?; absolute_path.parent().map(Path::to_path_buf) } + fn project_path_for_active_editor(editor: &Editor, cx: &App) -> Option { + let file = editor.file_at(MultiBufferOffset(0), cx)?; + Some(ProjectPath::from_file(file.as_ref(), cx)) + } + fn line_scroll_amount(&self, cx: &App) -> Pixels { let settings = ThemeSettings::get_global(cx); settings.markdown_preview_font_size(cx) * settings.buffer_line_height.value() @@ -945,13 +944,18 @@ impl MarkdownPreviewView { .active_editor .as_ref() .map(|state| state.editor.clone()); + let source_project_path = active_editor + .as_ref() + .and_then(|editor| Self::project_path_for_active_editor(editor.read(cx), cx)); + let mut project = None; let mut workspace_directory = None; if let Some(workspace_entity) = self.workspace.upgrade() { - let project = workspace_entity.read(cx).project(); - if let Some(tree) = project.read(cx).worktrees(cx).next() { + let project_entity = workspace_entity.read(cx).project().clone(); + if let Some(tree) = project_entity.read(cx).worktrees(cx).next() { workspace_directory = Some(tree.read(cx).abs_path().to_path_buf()); } + project = Some(project_entity); } let markdown_style = if let Some(theme) = preview_theme { @@ -976,11 +980,15 @@ impl MarkdownPreviewView { .show_root_block_markers() .image_resolver({ let base_directory = self.base_directory.clone(); - move |dest_url| { + move |dest_url, cx| { resolve_preview_image( dest_url, base_directory.as_deref(), workspace_directory.as_deref(), + project.as_ref(), + source_project_path.as_ref(), + PathStyle::local(), + cx, ) } }) @@ -1372,6 +1380,10 @@ fn resolve_preview_image( dest_url: &str, base_directory: Option<&Path>, workspace_directory: Option<&Path>, + project: Option<&Entity>, + source_project_path: Option<&ProjectPath>, + client_path_style: PathStyle, + cx: &App, ) -> Option { if dest_url.starts_with("data:") { return None; @@ -1386,33 +1398,88 @@ fn resolve_preview_image( let decoded = urlencoding::decode(dest_url) .map(|decoded| decoded.into_owned()) .unwrap_or_else(|_| dest_url.to_string()); - - if let Some(stripped) = ['/', '\\'] + let workspace_relative_path = ['/', '\\'] .iter() - .find_map(|prefix| decoded.strip_prefix(*prefix)) - { - if let Some(root) = workspace_directory { - let absolute_path = root.join(stripped); - if absolute_path.exists() { - return Some(ImageSource::Resource(Resource::Path(Arc::from( - absolute_path.as_path(), - )))); - } else { - return None; + .find_map(|prefix| decoded.strip_prefix(*prefix)); + + if let (Some(project), Some(source_project_path)) = (project, source_project_path) { + let project_path = resolve_project_path_for_preview_image( + &decoded, + workspace_relative_path, + source_project_path, + project, + cx, + ); + if let Some(project_path) = project_path { + let is_remote = project + .read(cx) + .worktree_for_id(project_path.worktree_id, cx) + .is_some_and(|worktree| !worktree.read(cx).is_local()); + if is_remote { + return Some(image_store::project_image_source( + project.downgrade(), + project_path, + )); } + + let path = project.read(cx).absolute_path(&project_path, cx)?; + return path + .exists() + .then(|| ImageSource::Resource(Resource::Path(Arc::from(path.as_path())))); + } else if project.read(cx).is_remote() { + return None; } } - let path = if Path::new(&decoded).is_absolute() { + let path = if let (Some(stripped), Some(root)) = (workspace_relative_path, workspace_directory) + { + client_path_style.join_path(root, stripped).ok()? + } else if client_path_style.is_absolute(&decoded) { PathBuf::from(decoded) } else { - base_directory?.join(decoded) + client_path_style.join_path(base_directory?, decoded).ok()? }; path.exists() .then(|| ImageSource::Resource(Resource::Path(Arc::from(path.as_path())))) } +fn resolve_project_path_for_preview_image( + decoded_path: &str, + workspace_relative_path: Option<&str>, + source_project_path: &ProjectPath, + project: &Entity, + cx: &App, +) -> Option { + let worktree = project + .read(cx) + .worktree_for_id(source_project_path.worktree_id, cx)?; + let path_style = worktree.read(cx).path_style(); + + if workspace_relative_path.is_none() && path_style.is_absolute(decoded_path) { + return project + .read(cx) + .project_path_for_absolute_path(Path::new(decoded_path), cx); + } + + let source_directory = source_project_path.path.parent()?; + let path = if let Some(workspace_relative_path) = workspace_relative_path { + RelPath::new(Path::new(workspace_relative_path), path_style) + .ok()? + .into_owned() + } else { + let joined_path = path_style + .join_path(source_directory.as_std_path(), decoded_path) + .ok()?; + RelPath::new(&joined_path, path_style).ok()?.into_owned() + }; + + Some(ProjectPath { + worktree_id: source_project_path.worktree_id, + path: path.into(), + }) +} + impl Focusable for MarkdownPreviewView { fn focus_handle(&self, _: &App) -> FocusHandle { self.focus_handle.clone() @@ -1990,19 +2057,21 @@ mod tests { use crate::markdown_preview_view::ImageSource; use crate::markdown_preview_view::Resource; use crate::markdown_preview_view::resolve_preview_image; + use crate::markdown_preview_view::resolve_project_path_for_preview_image; use buffer_diff::BufferDiff; use editor::Editor; use fs::FakeFs; use gpui::{ - AppContext as _, Entity, Focusable as _, Modifiers, TestAppContext, WindowHandle, px, + App, AppContext as _, Entity, Focusable as _, Modifiers, TestAppContext, WindowHandle, px, }; use language::{Buffer, DiskState, Point}; - use project::Project; + use project::{Project, ProjectPath}; use serde_json::json; - use std::path::PathBuf; + use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::Duration; use util::path; + use util::paths::PathStyle; use util::rel_path::{RelPath, rel_path}; use util::test::TempTree; use workspace::item::SerializableItem; @@ -2012,8 +2081,8 @@ mod tests { use super::{MarkdownPreviewView, open_preview_url}; - #[test] - fn resolves_workspace_absolute_preview_image_path_and_rejects_missing() { + #[gpui::test] + fn resolves_workspace_absolute_preview_image_path_and_rejects_missing(cx: &mut App) { let tree = TempTree::new(json!({ "docs": {}, "test_image.png": "mock data" @@ -2027,6 +2096,10 @@ mod tests { workspace_root_relative_path, Some(&base_directory), Some(workspace_directory), + None, + None, + PathStyle::local(), + cx, ); assert_resolved_preview_image_path(resolved, image_file.as_path()); } @@ -2035,10 +2108,141 @@ mod tests { "/missing_image.png", Some(&base_directory), Some(workspace_directory), + None, + None, + PathStyle::local(), + cx, ); assert!(missing.is_none()); } + #[gpui::test] + async fn resolves_remote_preview_image_through_project(cx: &mut TestAppContext) { + init_test(cx); + let project = Project::test(FakeFs::new(cx.executor()), [], cx).await; + let worktree = project.update(cx, |project, cx| { + project.add_test_remote_worktree("/remote/project", cx) + }); + let source_project_path = ProjectPath { + worktree_id: worktree.read_with(cx, |worktree, _cx| worktree.id()), + path: rel_path("docs/readme.md").into(), + }; + + let resolved = cx.update(|cx| { + resolve_preview_image( + "images/example.png", + Some(Path::new("/remote/project/docs")), + Some(Path::new("/remote/project")), + Some(&project), + Some(&source_project_path), + PathStyle::local(), + cx, + ) + }); + assert!(matches!(resolved, Some(ImageSource::Custom(_)))); + + let outside_worktree = cx.update(|cx| { + resolve_preview_image( + "../../outside/example.png", + Some(Path::new("/remote/project/docs")), + Some(Path::new("/remote/project")), + Some(&project), + Some(&source_project_path), + PathStyle::local(), + cx, + ) + }); + assert!(outside_worktree.is_none()); + } + + #[gpui::test] + async fn resolves_remote_preview_image_with_different_client_path_style( + cx: &mut TestAppContext, + ) { + init_test(cx); + let project = Project::test(FakeFs::new(cx.executor()), [], cx).await; + let worktree = project.update(cx, |project, cx| { + project.add_test_remote_worktree("/remote/project", cx) + }); + let source_project_path = ProjectPath { + worktree_id: worktree.read_with(cx, |worktree, _cx| worktree.id()), + path: rel_path("docs/readme.md").into(), + }; + + let resolved = cx.update(|cx| { + resolve_preview_image( + "images/example.png", + Some(Path::new("/remote/project/docs")), + Some(Path::new("/remote/project")), + Some(&project), + Some(&source_project_path), + PathStyle::Windows, + cx, + ) + }); + + assert!(matches!(resolved, Some(ImageSource::Custom(_)))); + let project_path = cx.update(|cx| { + resolve_project_path_for_preview_image( + "images/example.png", + None, + &source_project_path, + &project, + cx, + ) + }); + assert_eq!( + project_path + .expect("image should resolve within the remote worktree") + .path, + rel_path("docs/images/example.png").into() + ); + } + + #[gpui::test] + async fn resolves_workspace_relative_remote_image_in_source_worktree(cx: &mut TestAppContext) { + init_test(cx); + let project = Project::test(FakeFs::new(cx.executor()), [], cx).await; + let source_worktree = project.update(cx, |project, cx| { + project.add_test_remote_worktree("/remote/other", cx); + project.add_test_remote_worktree("/remote/project", cx) + }); + let source_project_path = ProjectPath { + worktree_id: source_worktree.read_with(cx, |worktree, _cx| worktree.id()), + path: rel_path("docs/readme.md").into(), + }; + + let resolved = cx.update(|cx| { + resolve_preview_image( + "/images/root.png", + Some(Path::new("/remote/project/docs")), + Some(Path::new("/remote/other")), + Some(&project), + Some(&source_project_path), + PathStyle::Windows, + cx, + ) + }); + assert!(matches!(resolved, Some(ImageSource::Custom(_)))); + + let project_path = cx.update(|cx| { + resolve_project_path_for_preview_image( + "/images/root.png", + Some("images/root.png"), + &source_project_path, + &project, + cx, + ) + }); + assert_eq!( + project_path, + Some(ProjectPath { + worktree_id: source_project_path.worktree_id, + path: rel_path("images/root.png").into(), + }) + ); + } + #[gpui::test] async fn opens_preview_file_links_at_positions(cx: &mut TestAppContext) { init_test(cx); diff --git a/crates/project/src/image_store.rs b/crates/project/src/image_store.rs index 667440a437..a667f21788 100644 --- a/crates/project/src/image_store.rs +++ b/crates/project/src/image_store.rs @@ -6,12 +6,14 @@ use anyhow::{Context as _, Result}; use collections::{HashMap, HashSet, hash_map}; use futures::{StreamExt, channel::oneshot}; use gpui::{ - App, AsyncApp, Context, Entity, EventEmitter, Img, Subscription, Task, WeakEntity, prelude::*, + App, Asset, AssetLogger, AsyncApp, Context, Entity, EventEmitter, ImageCacheError, ImageSource, + Img, RenderImage, Subscription, Task, WeakEntity, prelude::*, }; pub use image::ImageFormat; use image::{ExtendedColorType, GenericImageView, ImageReader}; use language::{DiskState, File}; use rpc::{AnyProtoClient, ErrorExt as _, TypedEnvelope, proto}; +use std::future::Future; use std::num::NonZeroU64; use std::path::PathBuf; use std::sync::Arc; @@ -107,6 +109,45 @@ pub struct ImageItem { pub image_metadata: Option, } +#[derive(Clone, Eq, Hash, PartialEq)] +struct ProjectImageSource { + project: WeakEntity, + path: ProjectPath, +} + +enum ProjectImageAsset {} + +impl Asset for ProjectImageAsset { + type Source = ProjectImageSource; + type Output = Result, ImageCacheError>; + + fn load( + source: Self::Source, + cx: &mut App, + ) -> impl Future + Send + 'static { + let svg_renderer = cx.svg_renderer(); + let load_image = cx.spawn(async move |cx| { + let open_image = source + .project + .update(cx, |project, cx| project.open_image(source.path, cx))?; + let image = open_image.await?; + Ok::<_, anyhow::Error>(image.read_with(cx, |image, _cx| image.image.clone())) + }); + + async move { + let image = load_image.await?; + image.to_image_data(svg_renderer).map_err(Into::into) + } + } +} + +pub fn project_image_source(project: WeakEntity, path: ProjectPath) -> ImageSource { + let source = ProjectImageSource { project, path }; + ImageSource::from(move |window: &mut gpui::Window, cx: &mut App| { + window.use_asset::>(&source, cx) + }) +} + impl ImageItem { pub fn compute_metadata_from_bytes(image_bytes: &[u8]) -> Result { let image_format = image::guess_format(image_bytes)?; diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index 271ce2ad27..7948e1b6a7 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/crates/remote_server/src/remote_editing_tests.rs @@ -24,7 +24,10 @@ use git::{ Oid, repository::{CommitData, GitCommitTemplate, RepoPath, Worktree as GitWorktree}, }; -use gpui::{AppContext as _, Entity, SharedString, TestAppContext, UpdateGlobal, VisualContext}; +use gpui::{ + AppContext as _, Entity, ImageSource, IntoElement as _, SharedString, TestAppContext, + UpdateGlobal, VisualContext, img, px, size, +}; use http_client::{BlockedHttpClient, FakeHttpClient}; use language::{ Buffer, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageRegistry, LineEnding, Point, @@ -36,8 +39,9 @@ use lsp::{ }; use node_runtime::NodeRuntime; use project::{ - ProgressToken, Project, + ProgressToken, Project, ProjectPath, agent_server_store::AgentServerCommand, + image_store, search::{SearchQuery, SearchResult}, }; use remote::RemoteClient; @@ -340,6 +344,87 @@ async fn do_search_and_assert( buffers } +struct RemoteImageTestView { + source: ImageSource, +} + +impl gpui::Render for RemoteImageTestView { + fn render( + &mut self, + _window: &mut gpui::Window, + _cx: &mut gpui::Context, + ) -> impl gpui::IntoElement { + img(self.source.clone()) + } +} + +#[gpui::test] +async fn test_remote_project_image_source(cx: &mut TestAppContext, server_cx: &mut TestAppContext) { + let fs = FakeFs::new(server_cx.executor()); + fs.insert_tree( + path!("/code"), + json!({ + "project": { + "docs": { + "image.ppm": "P3\n1 1\n255\n255 0 0\n" + } + } + }), + ) + .await; + + let (project, _headless) = init_test(&fs, cx, server_cx).await; + let (worktree, _) = project + .update(cx, |project, cx| { + project.find_or_create_worktree(path!("/code/project"), true, cx) + }) + .await + .expect("remote worktree should open"); + let worktree_id = worktree.read_with(cx, |worktree, _cx| worktree.id()); + let source = image_store::project_image_source( + project.downgrade(), + ProjectPath { + worktree_id, + path: rel_path("docs/image.ppm").into(), + }, + ); + let ImageSource::Custom(load_image) = source else { + panic!("expected a project-backed image source"); + }; + let loaded_bytes = Arc::new(std::sync::Mutex::new(None)); + let observed_source = ImageSource::from({ + let loaded_bytes = loaded_bytes.clone(); + move |window: &mut gpui::Window, cx: &mut gpui::App| { + let result = load_image(window, cx); + if let Some(Ok(image)) = &result { + *loaded_bytes.lock().expect("loaded image mutex poisoned") = + image.as_bytes(0).map(ToOwned::to_owned); + } + result + } + }); + + let (view, cx) = cx.add_window_view(|_window, _cx| RemoteImageTestView { + source: observed_source, + }); + cx.draw(Default::default(), size(px(10.), px(10.)), { + let view = view.clone(); + move |_, _| view.into_any_element() + }); + cx.run_until_parked(); + cx.draw(Default::default(), size(px(10.), px(10.)), move |_, _| { + view.into_any_element() + }); + + assert_eq!( + loaded_bytes + .lock() + .expect("loaded image mutex poisoned") + .as_deref(), + Some([0, 0, 255, 255].as_slice()) + ); +} + #[gpui::test] async fn test_remote_project_search(cx: &mut TestAppContext, server_cx: &mut TestAppContext) { let fs = FakeFs::new(server_cx.executor());