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    pub 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}