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