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