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                // TODO kb this is causing another error, and we also pass a worktree nearby — seems ok to pass "" here?
380                // let image_path = worktree
381                //     .read(cx)
382                //     .absolutize(&project_path.path)
383                //     .map(Arc::from)
384                //     .unwrap_or_else(|_| project_path.path.clone());
385                let load_image = self
386                    .state
387                    .open_image(project_path.path.clone(), worktree, cx);
388
389                cx.spawn(async move |this, cx| {
390                    let load_result = load_image.await;
391                    *tx.borrow_mut() = Some(this.update(cx, |this, _cx| {
392                        // Record the fact that the image is no longer loading.
393                        this.loading_images_by_path.remove(&project_path);
394                        let image = load_result.map_err(Arc::new)?;
395                        Ok(image)
396                    })?);
397                    anyhow::Ok(())
398                })
399                .detach();
400                rx
401            }
402        };
403
404        cx.background_spawn(async move {
405            Self::wait_for_loading_image(loading_watch)
406                .await
407                .map_err(|e| e.cloned())
408        })
409    }
410
411    pub async fn wait_for_loading_image(
412        mut receiver: postage::watch::Receiver<
413            Option<Result<Entity<ImageItem>, Arc<anyhow::Error>>>,
414        >,
415    ) -> Result<Entity<ImageItem>, Arc<anyhow::Error>> {
416        loop {
417            if let Some(result) = receiver.borrow().as_ref() {
418                match result {
419                    Ok(image) => return Ok(image.to_owned()),
420                    Err(e) => return Err(e.to_owned()),
421                }
422            }
423            receiver.next().await;
424        }
425    }
426
427    pub fn reload_images(
428        &self,
429        images: HashSet<Entity<ImageItem>>,
430        cx: &mut Context<ImageStore>,
431    ) -> Task<Result<()>> {
432        if images.is_empty() {
433            return Task::ready(Ok(()));
434        }
435
436        self.state.reload_images(images, cx)
437    }
438
439    fn add_image(&mut self, image: Entity<ImageItem>, cx: &mut Context<ImageStore>) -> Result<()> {
440        let image_id = image.read(cx).id;
441
442        self.opened_images.insert(image_id, image.downgrade());
443
444        cx.subscribe(&image, Self::on_image_event).detach();
445        cx.emit(ImageStoreEvent::ImageAdded(image));
446        Ok(())
447    }
448
449    fn on_image_event(
450        &mut self,
451        image: Entity<ImageItem>,
452        event: &ImageItemEvent,
453        cx: &mut Context<Self>,
454    ) {
455        match event {
456            ImageItemEvent::FileHandleChanged => {
457                if let Some(local) = self.state.as_local() {
458                    local.update(cx, |local, cx| {
459                        local.image_changed_file(image, cx);
460                    })
461                }
462            }
463            _ => {}
464        }
465    }
466}
467
468impl ImageStoreImpl for Entity<LocalImageStore> {
469    fn open_image(
470        &self,
471        path: Arc<Path>,
472        worktree: Entity<Worktree>,
473        cx: &mut Context<ImageStore>,
474    ) -> Task<Result<Entity<ImageItem>>> {
475        let this = self.clone();
476
477        let load_file = worktree.update(cx, |worktree, cx| {
478            worktree.load_binary_file(path.as_ref(), cx)
479        });
480        cx.spawn(async move |image_store, cx| {
481            let LoadedBinaryFile { file, content } = load_file.await?;
482            let image = create_gpui_image(content)?;
483
484            let entity = cx.new(|cx| ImageItem {
485                id: cx.entity_id().as_non_zero_u64().into(),
486                file: file.clone(),
487                image,
488                image_metadata: None,
489                reload_task: None,
490            })?;
491
492            let image_id = cx.read_entity(&entity, |model, _| model.id)?;
493
494            this.update(cx, |this, cx| {
495                image_store.update(cx, |image_store, cx| {
496                    image_store.add_image(entity.clone(), cx)
497                })??;
498                this.local_image_ids_by_path.insert(
499                    ProjectPath {
500                        worktree_id: file.worktree_id(cx),
501                        path: file.path.clone(),
502                    },
503                    image_id,
504                );
505
506                if let Some(entry_id) = file.entry_id {
507                    this.local_image_ids_by_entry_id.insert(entry_id, image_id);
508                }
509
510                anyhow::Ok(())
511            })??;
512
513            Ok(entity)
514        })
515    }
516
517    fn reload_images(
518        &self,
519        images: HashSet<Entity<ImageItem>>,
520        cx: &mut Context<ImageStore>,
521    ) -> Task<Result<()>> {
522        cx.spawn(async move |_, cx| {
523            for image in images {
524                if let Some(rec) = image.update(cx, |image, cx| image.reload(cx))? {
525                    rec.await?
526                }
527            }
528            Ok(())
529        })
530    }
531
532    fn as_local(&self) -> Option<Entity<LocalImageStore>> {
533        Some(self.clone())
534    }
535}
536
537impl LocalImageStore {
538    fn subscribe_to_worktree(&mut self, worktree: &Entity<Worktree>, cx: &mut Context<Self>) {
539        cx.subscribe(worktree, |this, worktree, event, cx| {
540            if worktree.read(cx).is_local() {
541                match event {
542                    worktree::Event::UpdatedEntries(changes) => {
543                        this.local_worktree_entries_changed(&worktree, changes, cx);
544                    }
545                    _ => {}
546                }
547            }
548        })
549        .detach();
550    }
551
552    fn local_worktree_entries_changed(
553        &mut self,
554        worktree_handle: &Entity<Worktree>,
555        changes: &[(Arc<Path>, ProjectEntryId, PathChange)],
556        cx: &mut Context<Self>,
557    ) {
558        let snapshot = worktree_handle.read(cx).snapshot();
559        for (path, entry_id, _) in changes {
560            self.local_worktree_entry_changed(*entry_id, path, worktree_handle, &snapshot, cx);
561        }
562    }
563
564    fn local_worktree_entry_changed(
565        &mut self,
566        entry_id: ProjectEntryId,
567        path: &Arc<Path>,
568        worktree: &Entity<worktree::Worktree>,
569        snapshot: &worktree::Snapshot,
570        cx: &mut Context<Self>,
571    ) -> Option<()> {
572        let project_path = ProjectPath {
573            worktree_id: snapshot.id(),
574            path: path.clone(),
575        };
576        let image_id = match self.local_image_ids_by_entry_id.get(&entry_id) {
577            Some(&image_id) => image_id,
578            None => self.local_image_ids_by_path.get(&project_path).copied()?,
579        };
580
581        let image = self
582            .image_store
583            .update(cx, |image_store, _| {
584                if let Some(image) = image_store.get(image_id) {
585                    Some(image)
586                } else {
587                    image_store.opened_images.remove(&image_id);
588                    None
589                }
590            })
591            .ok()
592            .flatten();
593        let image = if let Some(image) = image {
594            image
595        } else {
596            self.local_image_ids_by_path.remove(&project_path);
597            self.local_image_ids_by_entry_id.remove(&entry_id);
598            return None;
599        };
600
601        image.update(cx, |image, cx| {
602            let old_file = &image.file;
603            if old_file.worktree != *worktree {
604                return;
605            }
606
607            let snapshot_entry = old_file
608                .entry_id
609                .and_then(|entry_id| snapshot.entry_for_id(entry_id))
610                .or_else(|| snapshot.entry_for_path(old_file.path.as_ref()));
611
612            let new_file = if let Some(entry) = snapshot_entry {
613                worktree::File {
614                    disk_state: match entry.mtime {
615                        Some(mtime) => DiskState::Present { mtime },
616                        None => old_file.disk_state,
617                    },
618                    is_local: true,
619                    entry_id: Some(entry.id),
620                    path: entry.path.clone(),
621                    worktree: worktree.clone(),
622                    is_private: entry.is_private,
623                }
624            } else {
625                worktree::File {
626                    disk_state: DiskState::Deleted,
627                    is_local: true,
628                    entry_id: old_file.entry_id,
629                    path: old_file.path.clone(),
630                    worktree: worktree.clone(),
631                    is_private: old_file.is_private,
632                }
633            };
634
635            if new_file == **old_file {
636                return;
637            }
638
639            if new_file.path != old_file.path {
640                self.local_image_ids_by_path.remove(&ProjectPath {
641                    path: old_file.path.clone(),
642                    worktree_id: old_file.worktree_id(cx),
643                });
644                self.local_image_ids_by_path.insert(
645                    ProjectPath {
646                        worktree_id: new_file.worktree_id(cx),
647                        path: new_file.path.clone(),
648                    },
649                    image_id,
650                );
651            }
652
653            if new_file.entry_id != old_file.entry_id {
654                if let Some(entry_id) = old_file.entry_id {
655                    self.local_image_ids_by_entry_id.remove(&entry_id);
656                }
657                if let Some(entry_id) = new_file.entry_id {
658                    self.local_image_ids_by_entry_id.insert(entry_id, image_id);
659                }
660            }
661
662            image.file_updated(Arc::new(new_file), cx);
663        });
664        None
665    }
666
667    fn image_changed_file(&mut self, image: Entity<ImageItem>, cx: &mut App) -> Option<()> {
668        let image = image.read(cx);
669        let file = &image.file;
670
671        let image_id = image.id;
672        if let Some(entry_id) = file.entry_id {
673            match self.local_image_ids_by_entry_id.get(&entry_id) {
674                Some(_) => {
675                    return None;
676                }
677                None => {
678                    self.local_image_ids_by_entry_id.insert(entry_id, image_id);
679                }
680            }
681        };
682        self.local_image_ids_by_path.insert(
683            ProjectPath {
684                worktree_id: file.worktree_id(cx),
685                path: file.path.clone(),
686            },
687            image_id,
688        );
689
690        Some(())
691    }
692}
693
694fn create_gpui_image(content: Vec<u8>) -> anyhow::Result<Arc<gpui::Image>> {
695    let format = image::guess_format(&content)?;
696
697    Ok(Arc::new(gpui::Image::from_bytes(
698        match format {
699            image::ImageFormat::Png => gpui::ImageFormat::Png,
700            image::ImageFormat::Jpeg => gpui::ImageFormat::Jpeg,
701            image::ImageFormat::WebP => gpui::ImageFormat::Webp,
702            image::ImageFormat::Gif => gpui::ImageFormat::Gif,
703            image::ImageFormat::Bmp => gpui::ImageFormat::Bmp,
704            image::ImageFormat::Tiff => gpui::ImageFormat::Tiff,
705            format => anyhow::bail!("Image format {format:?} not supported"),
706        },
707        content,
708    )))
709}
710
711impl ImageStoreImpl for Entity<RemoteImageStore> {
712    fn open_image(
713        &self,
714        _path: Arc<Path>,
715        _worktree: Entity<Worktree>,
716        _cx: &mut Context<ImageStore>,
717    ) -> Task<Result<Entity<ImageItem>>> {
718        Task::ready(Err(anyhow::anyhow!(
719            "Opening images from remote is not supported"
720        )))
721    }
722
723    fn reload_images(
724        &self,
725        _images: HashSet<Entity<ImageItem>>,
726        _cx: &mut Context<ImageStore>,
727    ) -> Task<Result<()>> {
728        Task::ready(Err(anyhow::anyhow!(
729            "Reloading images from remote is not supported"
730        )))
731    }
732
733    fn as_local(&self) -> Option<Entity<LocalImageStore>> {
734        None
735    }
736}
737
738#[cfg(test)]
739mod tests {
740    use super::*;
741    use fs::FakeFs;
742    use gpui::TestAppContext;
743    use serde_json::json;
744    use settings::SettingsStore;
745    use std::path::PathBuf;
746
747    pub fn init_test(cx: &mut TestAppContext) {
748        if std::env::var("RUST_LOG").is_ok() {
749            env_logger::try_init().ok();
750        }
751
752        cx.update(|cx| {
753            let settings_store = SettingsStore::test(cx);
754            cx.set_global(settings_store);
755            language::init(cx);
756            Project::init_settings(cx);
757        });
758    }
759
760    #[gpui::test]
761    async fn test_image_not_loaded_twice(cx: &mut TestAppContext) {
762        init_test(cx);
763        let fs = FakeFs::new(cx.executor());
764
765        fs.insert_tree("/root", json!({})).await;
766        // Create a png file that consists of a single white pixel
767        fs.insert_file(
768            "/root/image_1.png",
769            vec![
770                0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48,
771                0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00,
772                0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41, 0x54, 0x78,
773                0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4, 0x00,
774                0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
775            ],
776        )
777        .await;
778
779        let project = Project::test(fs, ["/root".as_ref()], cx).await;
780
781        let worktree_id =
782            cx.update(|cx| project.read(cx).worktrees(cx).next().unwrap().read(cx).id());
783
784        let project_path = ProjectPath {
785            worktree_id,
786            path: PathBuf::from("image_1.png").into(),
787        };
788
789        let (task1, task2) = project.update(cx, |project, cx| {
790            (
791                project.open_image(project_path.clone(), cx),
792                project.open_image(project_path.clone(), cx),
793            )
794        });
795
796        let image1 = task1.await.unwrap();
797        let image2 = task2.await.unwrap();
798
799        assert_eq!(image1, image2);
800    }
801}