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