image_store.rs

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