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