1use crate::{
2 Project, ProjectEntryId, ProjectItem, ProjectPath,
3 worktree_store::{WorktreeStore, WorktreeStoreEvent},
4};
5use anyhow::{Context as _, Result, anyhow};
6use collections::{HashMap, HashSet, hash_map};
7use futures::{StreamExt, channel::oneshot};
8use gpui::{
9 App, AsyncApp, Context, Entity, EventEmitter, Img, Subscription, Task, WeakEntity, hash,
10 prelude::*,
11};
12pub use image::ImageFormat;
13use image::{ExtendedColorType, GenericImageView, ImageReader};
14use language::{DiskState, File};
15use rpc::{AnyProtoClient, ErrorExt as _};
16use std::ffi::OsStr;
17use std::num::NonZeroU64;
18use std::path::Path;
19use std::sync::Arc;
20use util::ResultExt;
21use worktree::{LoadedBinaryFile, PathChange, Worktree};
22
23#[derive(Clone, Copy, Debug, Hash, PartialEq, PartialOrd, Ord, Eq)]
24pub struct ImageId(NonZeroU64);
25
26impl std::fmt::Display for ImageId {
27 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 write!(f, "{}", self.0)
29 }
30}
31
32impl From<NonZeroU64> for ImageId {
33 fn from(id: NonZeroU64) -> Self {
34 ImageId(id)
35 }
36}
37
38#[derive(Debug)]
39pub enum ImageItemEvent {
40 ReloadNeeded,
41 Reloaded,
42 FileHandleChanged,
43 MetadataUpdated,
44}
45
46impl EventEmitter<ImageItemEvent> for ImageItem {}
47
48pub enum ImageStoreEvent {
49 ImageAdded(Entity<ImageItem>),
50}
51
52impl EventEmitter<ImageStoreEvent> for ImageStore {}
53
54#[derive(Debug, Clone, Copy)]
55pub struct ImageMetadata {
56 pub width: u32,
57 pub height: u32,
58 pub file_size: u64,
59 pub colors: Option<ImageColorInfo>,
60 pub format: ImageFormat,
61}
62
63#[derive(Debug, Clone, Copy)]
64pub struct ImageColorInfo {
65 pub channels: u8,
66 pub bits_per_channel: u8,
67}
68
69impl ImageColorInfo {
70 pub fn from_color_type(color_type: impl Into<ExtendedColorType>) -> Option<Self> {
71 let (channels, bits_per_channel) = match color_type.into() {
72 ExtendedColorType::L8 => (1, 8),
73 ExtendedColorType::L16 => (1, 16),
74 ExtendedColorType::La8 => (2, 8),
75 ExtendedColorType::La16 => (2, 16),
76 ExtendedColorType::Rgb8 => (3, 8),
77 ExtendedColorType::Rgb16 => (3, 16),
78 ExtendedColorType::Rgba8 => (4, 8),
79 ExtendedColorType::Rgba16 => (4, 16),
80 ExtendedColorType::A8 => (1, 8),
81 ExtendedColorType::Bgr8 => (3, 8),
82 ExtendedColorType::Bgra8 => (4, 8),
83 ExtendedColorType::Cmyk8 => (4, 8),
84 _ => return None,
85 };
86
87 Some(Self {
88 channels,
89 bits_per_channel,
90 })
91 }
92
93 pub const fn bits_per_pixel(&self) -> u8 {
94 self.channels * self.bits_per_channel
95 }
96}
97
98pub struct ImageItem {
99 pub id: ImageId,
100 pub file: Arc<dyn File>,
101 pub image: Arc<gpui::Image>,
102 reload_task: Option<Task<()>>,
103 pub image_metadata: Option<ImageMetadata>,
104}
105
106impl ImageItem {
107 pub async fn load_image_metadata(
108 image: Entity<ImageItem>,
109 project: Entity<Project>,
110 cx: &mut AsyncApp,
111 ) -> Result<ImageMetadata> {
112 let (fs, image_path) = cx.update(|cx| {
113 let project_path = image.read(cx).project_path(cx);
114
115 let worktree = project
116 .read(cx)
117 .worktree_for_id(project_path.worktree_id, cx)
118 .ok_or_else(|| anyhow!("worktree not found"))?;
119 let worktree_root = worktree.read(cx).abs_path();
120 let image_path = image.read(cx).path();
121 let image_path = if image_path.is_absolute() {
122 image_path.to_path_buf()
123 } else {
124 worktree_root.join(image_path)
125 };
126
127 let fs = project.read(cx).fs().clone();
128
129 anyhow::Ok((fs, image_path))
130 })??;
131
132 let image_bytes = fs.load_bytes(&image_path).await?;
133 let image_format = image::guess_format(&image_bytes)?;
134
135 let mut image_reader = ImageReader::new(std::io::Cursor::new(image_bytes));
136 image_reader.set_format(image_format);
137 let image = image_reader.decode()?;
138
139 let (width, height) = image.dimensions();
140 let file_metadata = fs
141 .metadata(image_path.as_path())
142 .await?
143 .ok_or_else(|| anyhow!("failed to load image metadata"))?;
144
145 Ok(ImageMetadata {
146 width,
147 height,
148 file_size: file_metadata.len,
149 format: image_format,
150 colors: ImageColorInfo::from_color_type(image.color()),
151 })
152 }
153
154 pub fn project_path(&self, cx: &App) -> ProjectPath {
155 ProjectPath {
156 worktree_id: self.file.worktree_id(cx),
157 path: self.file.path().clone(),
158 }
159 }
160
161 pub fn path(&self) -> &Arc<Path> {
162 self.file.path()
163 }
164
165 fn file_updated(&mut self, new_file: Arc<dyn File>, cx: &mut Context<Self>) {
166 let mut file_changed = false;
167
168 let old_file = self.file.as_ref();
169 if new_file.path() != old_file.path() {
170 file_changed = true;
171 }
172
173 let old_state = old_file.disk_state();
174 let new_state = new_file.disk_state();
175 if old_state != new_state {
176 file_changed = true;
177 if matches!(new_state, DiskState::Present { .. }) {
178 cx.emit(ImageItemEvent::ReloadNeeded)
179 }
180 }
181
182 self.file = new_file;
183 if file_changed {
184 cx.emit(ImageItemEvent::FileHandleChanged);
185 cx.notify();
186 }
187 }
188
189 fn reload(&mut self, cx: &mut Context<Self>) -> Option<oneshot::Receiver<()>> {
190 let local_file = self.file.as_local()?;
191 let (tx, rx) = futures::channel::oneshot::channel();
192
193 let content = local_file.load_bytes(cx);
194 self.reload_task = Some(cx.spawn(async move |this, cx| {
195 if let Some(image) = content
196 .await
197 .context("Failed to load image content")
198 .and_then(create_gpui_image)
199 .log_err()
200 {
201 this.update(cx, |this, cx| {
202 this.image = image;
203 cx.emit(ImageItemEvent::Reloaded);
204 })
205 .log_err();
206 }
207 _ = tx.send(());
208 }));
209 Some(rx)
210 }
211}
212
213impl ProjectItem for ImageItem {
214 fn try_open(
215 project: &Entity<Project>,
216 path: &ProjectPath,
217 cx: &mut App,
218 ) -> Option<Task<gpui::Result<Entity<Self>>>> {
219 let path = path.clone();
220 let project = project.clone();
221
222 let worktree_abs_path = project
223 .read(cx)
224 .worktree_for_id(path.worktree_id, cx)?
225 .read(cx)
226 .abs_path();
227
228 // Resolve the file extension from either the worktree path (if it's a single file)
229 // or from the project path's subpath.
230 let ext = worktree_abs_path
231 .extension()
232 .or_else(|| path.path.extension())
233 .and_then(OsStr::to_str)
234 .map(str::to_lowercase)
235 .unwrap_or_default();
236 let ext = ext.as_str();
237
238 // Only open the item if it's a binary image (no SVGs, etc.)
239 // Since we do not have a way to toggle to an editor
240 if Img::extensions().contains(&ext) && !ext.contains("svg") {
241 Some(cx.spawn(async move |cx| {
242 project
243 .update(cx, |project, cx| project.open_image(path, cx))?
244 .await
245 }))
246 } else {
247 None
248 }
249 }
250
251 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
252 worktree::File::from_dyn(Some(&self.file))?.entry_id
253 }
254
255 fn project_path(&self, cx: &App) -> Option<ProjectPath> {
256 Some(self.project_path(cx).clone())
257 }
258
259 fn is_dirty(&self) -> bool {
260 false
261 }
262}
263
264trait ImageStoreImpl {
265 fn open_image(
266 &self,
267 path: Arc<Path>,
268 worktree: Entity<Worktree>,
269 cx: &mut Context<ImageStore>,
270 ) -> Task<Result<Entity<ImageItem>>>;
271
272 fn reload_images(
273 &self,
274 images: HashSet<Entity<ImageItem>>,
275 cx: &mut Context<ImageStore>,
276 ) -> Task<Result<()>>;
277
278 fn as_local(&self) -> Option<Entity<LocalImageStore>>;
279}
280
281struct RemoteImageStore {}
282
283struct LocalImageStore {
284 local_image_ids_by_path: HashMap<ProjectPath, ImageId>,
285 local_image_ids_by_entry_id: HashMap<ProjectEntryId, ImageId>,
286 image_store: WeakEntity<ImageStore>,
287 _subscription: Subscription,
288}
289
290pub struct ImageStore {
291 state: Box<dyn ImageStoreImpl>,
292 opened_images: HashMap<ImageId, WeakEntity<ImageItem>>,
293 worktree_store: Entity<WorktreeStore>,
294 #[allow(clippy::type_complexity)]
295 loading_images_by_path: HashMap<
296 ProjectPath,
297 postage::watch::Receiver<Option<Result<Entity<ImageItem>, Arc<anyhow::Error>>>>,
298 >,
299}
300
301impl ImageStore {
302 pub fn local(worktree_store: Entity<WorktreeStore>, cx: &mut Context<Self>) -> Self {
303 let this = cx.weak_entity();
304 Self {
305 state: Box::new(cx.new(|cx| {
306 let subscription = cx.subscribe(
307 &worktree_store,
308 |this: &mut LocalImageStore, _, event, cx| {
309 if let WorktreeStoreEvent::WorktreeAdded(worktree) = event {
310 this.subscribe_to_worktree(worktree, cx);
311 }
312 },
313 );
314
315 LocalImageStore {
316 local_image_ids_by_path: Default::default(),
317 local_image_ids_by_entry_id: Default::default(),
318 image_store: this,
319 _subscription: subscription,
320 }
321 })),
322 opened_images: Default::default(),
323 loading_images_by_path: Default::default(),
324 worktree_store,
325 }
326 }
327
328 pub fn remote(
329 worktree_store: Entity<WorktreeStore>,
330 _upstream_client: AnyProtoClient,
331 _remote_id: u64,
332 cx: &mut Context<Self>,
333 ) -> Self {
334 Self {
335 state: Box::new(cx.new(|_| RemoteImageStore {})),
336 opened_images: Default::default(),
337 loading_images_by_path: Default::default(),
338 worktree_store,
339 }
340 }
341
342 pub fn images(&self) -> impl '_ + Iterator<Item = Entity<ImageItem>> {
343 self.opened_images
344 .values()
345 .filter_map(|image| image.upgrade())
346 }
347
348 pub fn get(&self, image_id: ImageId) -> Option<Entity<ImageItem>> {
349 self.opened_images
350 .get(&image_id)
351 .and_then(|image| image.upgrade())
352 }
353
354 pub fn get_by_path(&self, path: &ProjectPath, cx: &App) -> Option<Entity<ImageItem>> {
355 self.images()
356 .find(|image| &image.read(cx).project_path(cx) == path)
357 }
358
359 pub fn open_image(
360 &mut self,
361 project_path: ProjectPath,
362 cx: &mut Context<Self>,
363 ) -> Task<Result<Entity<ImageItem>>> {
364 let existing_image = self.get_by_path(&project_path, cx);
365 if let Some(existing_image) = existing_image {
366 return Task::ready(Ok(existing_image));
367 }
368
369 let Some(worktree) = self
370 .worktree_store
371 .read(cx)
372 .worktree_for_id(project_path.worktree_id, cx)
373 else {
374 return Task::ready(Err(anyhow::anyhow!("no such worktree")));
375 };
376
377 let loading_watch = match self.loading_images_by_path.entry(project_path.clone()) {
378 // If the given path is already being loaded, then wait for that existing
379 // task to complete and return the same image.
380 hash_map::Entry::Occupied(e) => e.get().clone(),
381
382 // Otherwise, record the fact that this path is now being loaded.
383 hash_map::Entry::Vacant(entry) => {
384 let (mut tx, rx) = postage::watch::channel();
385 entry.insert(rx.clone());
386
387 let project_path = project_path.clone();
388 let load_image = self
389 .state
390 .open_image(project_path.path.clone(), worktree, cx);
391
392 cx.spawn(async move |this, cx| {
393 let load_result = load_image.await;
394 *tx.borrow_mut() = Some(this.update(cx, |this, _cx| {
395 // Record the fact that the image is no longer loading.
396 this.loading_images_by_path.remove(&project_path);
397 let image = load_result.map_err(Arc::new)?;
398 Ok(image)
399 })?);
400 anyhow::Ok(())
401 })
402 .detach();
403 rx
404 }
405 };
406
407 cx.background_spawn(async move {
408 Self::wait_for_loading_image(loading_watch)
409 .await
410 .map_err(|e| e.cloned())
411 })
412 }
413
414 pub async fn wait_for_loading_image(
415 mut receiver: postage::watch::Receiver<
416 Option<Result<Entity<ImageItem>, Arc<anyhow::Error>>>,
417 >,
418 ) -> Result<Entity<ImageItem>, Arc<anyhow::Error>> {
419 loop {
420 if let Some(result) = receiver.borrow().as_ref() {
421 match result {
422 Ok(image) => return Ok(image.to_owned()),
423 Err(e) => return Err(e.to_owned()),
424 }
425 }
426 receiver.next().await;
427 }
428 }
429
430 pub fn reload_images(
431 &self,
432 images: HashSet<Entity<ImageItem>>,
433 cx: &mut Context<ImageStore>,
434 ) -> Task<Result<()>> {
435 if images.is_empty() {
436 return Task::ready(Ok(()));
437 }
438
439 self.state.reload_images(images, cx)
440 }
441
442 fn add_image(&mut self, image: Entity<ImageItem>, cx: &mut Context<ImageStore>) -> Result<()> {
443 let image_id = image.read(cx).id;
444
445 self.opened_images.insert(image_id, image.downgrade());
446
447 cx.subscribe(&image, Self::on_image_event).detach();
448 cx.emit(ImageStoreEvent::ImageAdded(image));
449 Ok(())
450 }
451
452 fn on_image_event(
453 &mut self,
454 image: Entity<ImageItem>,
455 event: &ImageItemEvent,
456 cx: &mut Context<Self>,
457 ) {
458 match event {
459 ImageItemEvent::FileHandleChanged => {
460 if let Some(local) = self.state.as_local() {
461 local.update(cx, |local, cx| {
462 local.image_changed_file(image, cx);
463 })
464 }
465 }
466 _ => {}
467 }
468 }
469}
470
471impl ImageStoreImpl for Entity<LocalImageStore> {
472 fn open_image(
473 &self,
474 path: Arc<Path>,
475 worktree: Entity<Worktree>,
476 cx: &mut Context<ImageStore>,
477 ) -> Task<Result<Entity<ImageItem>>> {
478 let this = self.clone();
479
480 let load_file = worktree.update(cx, |worktree, cx| {
481 worktree.load_binary_file(path.as_ref(), cx)
482 });
483 cx.spawn(async move |image_store, cx| {
484 let LoadedBinaryFile { file, content } = load_file.await?;
485 let image = create_gpui_image(content)?;
486
487 let entity = cx.new(|cx| ImageItem {
488 id: cx.entity_id().as_non_zero_u64().into(),
489 file: file.clone(),
490 image,
491 image_metadata: None,
492 reload_task: None,
493 })?;
494
495 let image_id = cx.read_entity(&entity, |model, _| model.id)?;
496
497 this.update(cx, |this, cx| {
498 image_store.update(cx, |image_store, cx| {
499 image_store.add_image(entity.clone(), cx)
500 })??;
501 this.local_image_ids_by_path.insert(
502 ProjectPath {
503 worktree_id: file.worktree_id(cx),
504 path: file.path.clone(),
505 },
506 image_id,
507 );
508
509 if let Some(entry_id) = file.entry_id {
510 this.local_image_ids_by_entry_id.insert(entry_id, image_id);
511 }
512
513 anyhow::Ok(())
514 })??;
515
516 Ok(entity)
517 })
518 }
519
520 fn reload_images(
521 &self,
522 images: HashSet<Entity<ImageItem>>,
523 cx: &mut Context<ImageStore>,
524 ) -> Task<Result<()>> {
525 cx.spawn(async move |_, cx| {
526 for image in images {
527 if let Some(rec) = image.update(cx, |image, cx| image.reload(cx))? {
528 rec.await?
529 }
530 }
531 Ok(())
532 })
533 }
534
535 fn as_local(&self) -> Option<Entity<LocalImageStore>> {
536 Some(self.clone())
537 }
538}
539
540impl LocalImageStore {
541 fn subscribe_to_worktree(&mut self, worktree: &Entity<Worktree>, cx: &mut Context<Self>) {
542 cx.subscribe(worktree, |this, worktree, event, cx| {
543 if worktree.read(cx).is_local() {
544 match event {
545 worktree::Event::UpdatedEntries(changes) => {
546 this.local_worktree_entries_changed(&worktree, changes, cx);
547 }
548 _ => {}
549 }
550 }
551 })
552 .detach();
553 }
554
555 fn local_worktree_entries_changed(
556 &mut self,
557 worktree_handle: &Entity<Worktree>,
558 changes: &[(Arc<Path>, ProjectEntryId, PathChange)],
559 cx: &mut Context<Self>,
560 ) {
561 let snapshot = worktree_handle.read(cx).snapshot();
562 for (path, entry_id, _) in changes {
563 self.local_worktree_entry_changed(*entry_id, path, worktree_handle, &snapshot, cx);
564 }
565 }
566
567 fn local_worktree_entry_changed(
568 &mut self,
569 entry_id: ProjectEntryId,
570 path: &Arc<Path>,
571 worktree: &Entity<worktree::Worktree>,
572 snapshot: &worktree::Snapshot,
573 cx: &mut Context<Self>,
574 ) -> Option<()> {
575 let project_path = ProjectPath {
576 worktree_id: snapshot.id(),
577 path: path.clone(),
578 };
579 let image_id = match self.local_image_ids_by_entry_id.get(&entry_id) {
580 Some(&image_id) => image_id,
581 None => self.local_image_ids_by_path.get(&project_path).copied()?,
582 };
583
584 let image = self
585 .image_store
586 .update(cx, |image_store, _| {
587 if let Some(image) = image_store.get(image_id) {
588 Some(image)
589 } else {
590 image_store.opened_images.remove(&image_id);
591 None
592 }
593 })
594 .ok()
595 .flatten();
596 let image = if let Some(image) = image {
597 image
598 } else {
599 self.local_image_ids_by_path.remove(&project_path);
600 self.local_image_ids_by_entry_id.remove(&entry_id);
601 return None;
602 };
603
604 image.update(cx, |image, cx| {
605 let Some(old_file) = worktree::File::from_dyn(Some(&image.file)) else {
606 return;
607 };
608 if old_file.worktree != *worktree {
609 return;
610 }
611
612 let snapshot_entry = old_file
613 .entry_id
614 .and_then(|entry_id| snapshot.entry_for_id(entry_id))
615 .or_else(|| snapshot.entry_for_path(old_file.path.as_ref()));
616
617 let new_file = if let Some(entry) = snapshot_entry {
618 worktree::File {
619 disk_state: match entry.mtime {
620 Some(mtime) => DiskState::Present { mtime },
621 None => old_file.disk_state,
622 },
623 is_local: true,
624 entry_id: Some(entry.id),
625 path: entry.path.clone(),
626 worktree: worktree.clone(),
627 is_private: entry.is_private,
628 }
629 } else {
630 worktree::File {
631 disk_state: DiskState::Deleted,
632 is_local: true,
633 entry_id: old_file.entry_id,
634 path: old_file.path.clone(),
635 worktree: worktree.clone(),
636 is_private: old_file.is_private,
637 }
638 };
639
640 if new_file == *old_file {
641 return;
642 }
643
644 if new_file.path != old_file.path {
645 self.local_image_ids_by_path.remove(&ProjectPath {
646 path: old_file.path.clone(),
647 worktree_id: old_file.worktree_id(cx),
648 });
649 self.local_image_ids_by_path.insert(
650 ProjectPath {
651 worktree_id: new_file.worktree_id(cx),
652 path: new_file.path.clone(),
653 },
654 image_id,
655 );
656 }
657
658 if new_file.entry_id != old_file.entry_id {
659 if let Some(entry_id) = old_file.entry_id {
660 self.local_image_ids_by_entry_id.remove(&entry_id);
661 }
662 if let Some(entry_id) = new_file.entry_id {
663 self.local_image_ids_by_entry_id.insert(entry_id, image_id);
664 }
665 }
666
667 image.file_updated(Arc::new(new_file), cx);
668 });
669 None
670 }
671
672 fn image_changed_file(&mut self, image: Entity<ImageItem>, cx: &mut App) -> Option<()> {
673 let file = worktree::File::from_dyn(Some(&image.read(cx).file))?;
674
675 let image_id = image.read(cx).id;
676 if let Some(entry_id) = file.entry_id {
677 match self.local_image_ids_by_entry_id.get(&entry_id) {
678 Some(_) => {
679 return None;
680 }
681 None => {
682 self.local_image_ids_by_entry_id.insert(entry_id, image_id);
683 }
684 }
685 };
686 self.local_image_ids_by_path.insert(
687 ProjectPath {
688 worktree_id: file.worktree_id(cx),
689 path: file.path.clone(),
690 },
691 image_id,
692 );
693
694 Some(())
695 }
696}
697
698fn create_gpui_image(content: Vec<u8>) -> anyhow::Result<Arc<gpui::Image>> {
699 let format = image::guess_format(&content)?;
700
701 Ok(Arc::new(gpui::Image {
702 id: hash(&content),
703 format: match format {
704 image::ImageFormat::Png => gpui::ImageFormat::Png,
705 image::ImageFormat::Jpeg => gpui::ImageFormat::Jpeg,
706 image::ImageFormat::WebP => gpui::ImageFormat::Webp,
707 image::ImageFormat::Gif => gpui::ImageFormat::Gif,
708 image::ImageFormat::Bmp => gpui::ImageFormat::Bmp,
709 image::ImageFormat::Tiff => gpui::ImageFormat::Tiff,
710 _ => Err(anyhow::anyhow!("Image format not supported"))?,
711 },
712 bytes: content,
713 }))
714}
715
716impl ImageStoreImpl for Entity<RemoteImageStore> {
717 fn open_image(
718 &self,
719 _path: Arc<Path>,
720 _worktree: Entity<Worktree>,
721 _cx: &mut Context<ImageStore>,
722 ) -> Task<Result<Entity<ImageItem>>> {
723 Task::ready(Err(anyhow::anyhow!(
724 "Opening images from remote is not supported"
725 )))
726 }
727
728 fn reload_images(
729 &self,
730 _images: HashSet<Entity<ImageItem>>,
731 _cx: &mut Context<ImageStore>,
732 ) -> Task<Result<()>> {
733 Task::ready(Err(anyhow::anyhow!(
734 "Reloading images from remote is not supported"
735 )))
736 }
737
738 fn as_local(&self) -> Option<Entity<LocalImageStore>> {
739 None
740 }
741}
742
743#[cfg(test)]
744mod tests {
745 use super::*;
746 use fs::FakeFs;
747 use gpui::TestAppContext;
748 use serde_json::json;
749 use settings::SettingsStore;
750 use std::path::PathBuf;
751
752 pub fn init_test(cx: &mut TestAppContext) {
753 if std::env::var("RUST_LOG").is_ok() {
754 env_logger::try_init().ok();
755 }
756
757 cx.update(|cx| {
758 let settings_store = SettingsStore::test(cx);
759 cx.set_global(settings_store);
760 language::init(cx);
761 Project::init_settings(cx);
762 });
763 }
764
765 #[gpui::test]
766 async fn test_image_not_loaded_twice(cx: &mut TestAppContext) {
767 init_test(cx);
768 let fs = FakeFs::new(cx.executor());
769
770 fs.insert_tree("/root", json!({})).await;
771 // Create a png file that consists of a single white pixel
772 fs.insert_file(
773 "/root/image_1.png",
774 vec![
775 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48,
776 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00,
777 0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41, 0x54, 0x78,
778 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4, 0x00,
779 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
780 ],
781 )
782 .await;
783
784 let project = Project::test(fs, ["/root".as_ref()], cx).await;
785
786 let worktree_id =
787 cx.update(|cx| project.read(cx).worktrees(cx).next().unwrap().read(cx).id());
788
789 let project_path = ProjectPath {
790 worktree_id,
791 path: PathBuf::from("image_1.png").into(),
792 };
793
794 let (task1, task2) = project.update(cx, |project, cx| {
795 (
796 project.open_image(project_path.clone(), cx),
797 project.open_image(project_path.clone(), cx),
798 )
799 });
800
801 let image1 = task1.await.unwrap();
802 let image2 = task2.await.unwrap();
803
804 assert_eq!(image1, image2);
805 }
806}