image_store.rs

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