image_store.rs

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