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