image_store.rs

  1use crate::worktree_store::{WorktreeStore, WorktreeStoreEvent};
  2use crate::{Project, ProjectEntryId, ProjectPath};
  3use anyhow::{Context as _, Result};
  4use collections::{HashMap, HashSet};
  5use futures::channel::oneshot;
  6use gpui::{
  7    hash, prelude::*, AppContext, EventEmitter, Img, Model, ModelContext, Subscription, Task,
  8    WeakModel,
  9};
 10use language::File;
 11use rpc::AnyProtoClient;
 12use std::ffi::OsStr;
 13use std::num::NonZeroU64;
 14use std::path::Path;
 15use std::sync::Arc;
 16use util::ResultExt;
 17use worktree::{LoadedBinaryFile, PathChange, Worktree};
 18
 19#[derive(Clone, Copy, Debug, Hash, PartialEq, PartialOrd, Ord, Eq)]
 20pub struct ImageId(NonZeroU64);
 21
 22impl std::fmt::Display for ImageId {
 23    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 24        write!(f, "{}", self.0)
 25    }
 26}
 27
 28impl From<NonZeroU64> for ImageId {
 29    fn from(id: NonZeroU64) -> Self {
 30        ImageId(id)
 31    }
 32}
 33
 34pub enum ImageItemEvent {
 35    ReloadNeeded,
 36    Reloaded,
 37    FileHandleChanged,
 38}
 39
 40impl EventEmitter<ImageItemEvent> for ImageItem {}
 41
 42pub enum ImageStoreEvent {
 43    ImageAdded(Model<ImageItem>),
 44}
 45
 46impl EventEmitter<ImageStoreEvent> for ImageStore {}
 47
 48pub struct ImageItem {
 49    pub id: ImageId,
 50    pub file: Arc<dyn File>,
 51    pub image: Arc<gpui::Image>,
 52    reload_task: Option<Task<()>>,
 53}
 54
 55impl ImageItem {
 56    pub fn project_path(&self, cx: &AppContext) -> ProjectPath {
 57        ProjectPath {
 58            worktree_id: self.file.worktree_id(cx),
 59            path: self.file.path().clone(),
 60        }
 61    }
 62
 63    pub fn path(&self) -> &Arc<Path> {
 64        self.file.path()
 65    }
 66
 67    fn file_updated(&mut self, new_file: Arc<dyn File>, cx: &mut ModelContext<Self>) {
 68        let mut file_changed = false;
 69
 70        let old_file = self.file.as_ref();
 71        if new_file.path() != old_file.path() {
 72            file_changed = true;
 73        }
 74
 75        if !new_file.is_deleted() {
 76            let new_mtime = new_file.mtime();
 77            if new_mtime != old_file.mtime() {
 78                file_changed = true;
 79                cx.emit(ImageItemEvent::ReloadNeeded);
 80            }
 81        }
 82
 83        self.file = new_file;
 84        if file_changed {
 85            cx.emit(ImageItemEvent::FileHandleChanged);
 86            cx.notify();
 87        }
 88    }
 89
 90    fn reload(&mut self, cx: &mut ModelContext<Self>) -> Option<oneshot::Receiver<()>> {
 91        let local_file = self.file.as_local()?;
 92        let (tx, rx) = futures::channel::oneshot::channel();
 93
 94        let content = local_file.load_bytes(cx);
 95        self.reload_task = Some(cx.spawn(|this, mut cx| async move {
 96            if let Some(image) = content
 97                .await
 98                .context("Failed to load image content")
 99                .and_then(create_gpui_image)
100                .log_err()
101            {
102                this.update(&mut cx, |this, cx| {
103                    this.image = image;
104                    cx.emit(ImageItemEvent::Reloaded);
105                })
106                .log_err();
107            }
108            _ = tx.send(());
109        }));
110        Some(rx)
111    }
112}
113
114impl crate::Item for ImageItem {
115    fn try_open(
116        project: &Model<Project>,
117        path: &ProjectPath,
118        cx: &mut AppContext,
119    ) -> Option<Task<gpui::Result<Model<Self>>>> {
120        let path = path.clone();
121        let project = project.clone();
122
123        let ext = path
124            .path
125            .extension()
126            .and_then(OsStr::to_str)
127            .map(str::to_lowercase)
128            .unwrap_or_default();
129        let ext = ext.as_str();
130
131        // Only open the item if it's a binary image (no SVGs, etc.)
132        // Since we do not have a way to toggle to an editor
133        if Img::extensions().contains(&ext) && !ext.contains("svg") {
134            Some(cx.spawn(|mut cx| async move {
135                project
136                    .update(&mut cx, |project, cx| project.open_image(path, cx))?
137                    .await
138            }))
139        } else {
140            None
141        }
142    }
143
144    fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
145        worktree::File::from_dyn(Some(&self.file))?.entry_id
146    }
147
148    fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
149        Some(self.project_path(cx).clone())
150    }
151}
152
153trait ImageStoreImpl {
154    fn open_image(
155        &self,
156        path: Arc<Path>,
157        worktree: Model<Worktree>,
158        cx: &mut ModelContext<ImageStore>,
159    ) -> Task<Result<Model<ImageItem>>>;
160
161    fn reload_images(
162        &self,
163        images: HashSet<Model<ImageItem>>,
164        cx: &mut ModelContext<ImageStore>,
165    ) -> Task<Result<()>>;
166
167    fn as_local(&self) -> Option<Model<LocalImageStore>>;
168}
169
170struct RemoteImageStore {}
171
172struct LocalImageStore {
173    local_image_ids_by_path: HashMap<ProjectPath, ImageId>,
174    local_image_ids_by_entry_id: HashMap<ProjectEntryId, ImageId>,
175    image_store: WeakModel<ImageStore>,
176    _subscription: Subscription,
177}
178
179pub struct ImageStore {
180    state: Box<dyn ImageStoreImpl>,
181    opened_images: HashMap<ImageId, WeakModel<ImageItem>>,
182    worktree_store: Model<WorktreeStore>,
183}
184
185impl ImageStore {
186    pub fn local(worktree_store: Model<WorktreeStore>, cx: &mut ModelContext<Self>) -> Self {
187        let this = cx.weak_model();
188        Self {
189            state: Box::new(cx.new_model(|cx| {
190                let subscription = cx.subscribe(
191                    &worktree_store,
192                    |this: &mut LocalImageStore, _, event, cx| {
193                        if let WorktreeStoreEvent::WorktreeAdded(worktree) = event {
194                            this.subscribe_to_worktree(worktree, cx);
195                        }
196                    },
197                );
198
199                LocalImageStore {
200                    local_image_ids_by_path: Default::default(),
201                    local_image_ids_by_entry_id: Default::default(),
202                    image_store: this,
203                    _subscription: subscription,
204                }
205            })),
206            opened_images: Default::default(),
207            worktree_store,
208        }
209    }
210
211    pub fn remote(
212        worktree_store: Model<WorktreeStore>,
213        _upstream_client: AnyProtoClient,
214        _remote_id: u64,
215        cx: &mut ModelContext<Self>,
216    ) -> Self {
217        Self {
218            state: Box::new(cx.new_model(|_| RemoteImageStore {})),
219            opened_images: Default::default(),
220            worktree_store,
221        }
222    }
223
224    pub fn images(&self) -> impl '_ + Iterator<Item = Model<ImageItem>> {
225        self.opened_images
226            .values()
227            .filter_map(|image| image.upgrade())
228    }
229
230    pub fn get(&self, image_id: ImageId) -> Option<Model<ImageItem>> {
231        self.opened_images
232            .get(&image_id)
233            .and_then(|image| image.upgrade())
234    }
235
236    pub fn get_by_path(&self, path: &ProjectPath, cx: &AppContext) -> Option<Model<ImageItem>> {
237        self.images()
238            .find(|image| &image.read(cx).project_path(cx) == path)
239    }
240
241    pub fn open_image(
242        &mut self,
243        project_path: ProjectPath,
244        cx: &mut ModelContext<Self>,
245    ) -> Task<Result<Model<ImageItem>>> {
246        let existing_image = self.get_by_path(&project_path, cx);
247        if let Some(existing_image) = existing_image {
248            return Task::ready(Ok(existing_image));
249        }
250
251        let Some(worktree) = self
252            .worktree_store
253            .read(cx)
254            .worktree_for_id(project_path.worktree_id, cx)
255        else {
256            return Task::ready(Err(anyhow::anyhow!("no such worktree")));
257        };
258
259        self.state
260            .open_image(project_path.path.clone(), worktree, cx)
261    }
262
263    pub fn reload_images(
264        &self,
265        images: HashSet<Model<ImageItem>>,
266        cx: &mut ModelContext<ImageStore>,
267    ) -> Task<Result<()>> {
268        if images.is_empty() {
269            return Task::ready(Ok(()));
270        }
271
272        self.state.reload_images(images, cx)
273    }
274
275    fn add_image(
276        &mut self,
277        image: Model<ImageItem>,
278        cx: &mut ModelContext<ImageStore>,
279    ) -> Result<()> {
280        let image_id = image.read(cx).id;
281
282        self.opened_images.insert(image_id, image.downgrade());
283
284        cx.subscribe(&image, Self::on_image_event).detach();
285        cx.emit(ImageStoreEvent::ImageAdded(image));
286        Ok(())
287    }
288
289    fn on_image_event(
290        &mut self,
291        image: Model<ImageItem>,
292        event: &ImageItemEvent,
293        cx: &mut ModelContext<Self>,
294    ) {
295        match event {
296            ImageItemEvent::FileHandleChanged => {
297                if let Some(local) = self.state.as_local() {
298                    local.update(cx, |local, cx| {
299                        local.image_changed_file(image, cx);
300                    })
301                }
302            }
303            _ => {}
304        }
305    }
306}
307
308impl ImageStoreImpl for Model<LocalImageStore> {
309    fn open_image(
310        &self,
311        path: Arc<Path>,
312        worktree: Model<Worktree>,
313        cx: &mut ModelContext<ImageStore>,
314    ) -> Task<Result<Model<ImageItem>>> {
315        let this = self.clone();
316
317        let load_file = worktree.update(cx, |worktree, cx| {
318            worktree.load_binary_file(path.as_ref(), cx)
319        });
320        cx.spawn(move |image_store, mut cx| async move {
321            let LoadedBinaryFile { file, content } = load_file.await?;
322            let image = create_gpui_image(content)?;
323
324            let model = cx.new_model(|cx| ImageItem {
325                id: cx.entity_id().as_non_zero_u64().into(),
326                file: file.clone(),
327                image,
328                reload_task: None,
329            })?;
330
331            let image_id = cx.read_model(&model, |model, _| model.id)?;
332
333            this.update(&mut cx, |this, cx| {
334                image_store.update(cx, |image_store, cx| {
335                    image_store.add_image(model.clone(), cx)
336                })??;
337                this.local_image_ids_by_path.insert(
338                    ProjectPath {
339                        worktree_id: file.worktree_id(cx),
340                        path: file.path.clone(),
341                    },
342                    image_id,
343                );
344
345                if let Some(entry_id) = file.entry_id {
346                    this.local_image_ids_by_entry_id.insert(entry_id, image_id);
347                }
348
349                anyhow::Ok(())
350            })??;
351
352            Ok(model)
353        })
354    }
355
356    fn reload_images(
357        &self,
358        images: HashSet<Model<ImageItem>>,
359        cx: &mut ModelContext<ImageStore>,
360    ) -> Task<Result<()>> {
361        cx.spawn(move |_, mut cx| async move {
362            for image in images {
363                if let Some(rec) = image.update(&mut cx, |image, cx| image.reload(cx))? {
364                    rec.await?
365                }
366            }
367            Ok(())
368        })
369    }
370
371    fn as_local(&self) -> Option<Model<LocalImageStore>> {
372        Some(self.clone())
373    }
374}
375
376impl LocalImageStore {
377    fn subscribe_to_worktree(&mut self, worktree: &Model<Worktree>, cx: &mut ModelContext<Self>) {
378        cx.subscribe(worktree, |this, worktree, event, cx| {
379            if worktree.read(cx).is_local() {
380                match event {
381                    worktree::Event::UpdatedEntries(changes) => {
382                        this.local_worktree_entries_changed(&worktree, changes, cx);
383                    }
384                    _ => {}
385                }
386            }
387        })
388        .detach();
389    }
390
391    fn local_worktree_entries_changed(
392        &mut self,
393        worktree_handle: &Model<Worktree>,
394        changes: &[(Arc<Path>, ProjectEntryId, PathChange)],
395        cx: &mut ModelContext<Self>,
396    ) {
397        let snapshot = worktree_handle.read(cx).snapshot();
398        for (path, entry_id, _) in changes {
399            self.local_worktree_entry_changed(*entry_id, path, worktree_handle, &snapshot, cx);
400        }
401    }
402
403    fn local_worktree_entry_changed(
404        &mut self,
405        entry_id: ProjectEntryId,
406        path: &Arc<Path>,
407        worktree: &Model<worktree::Worktree>,
408        snapshot: &worktree::Snapshot,
409        cx: &mut ModelContext<Self>,
410    ) -> Option<()> {
411        let project_path = ProjectPath {
412            worktree_id: snapshot.id(),
413            path: path.clone(),
414        };
415        let image_id = match self.local_image_ids_by_entry_id.get(&entry_id) {
416            Some(&image_id) => image_id,
417            None => self.local_image_ids_by_path.get(&project_path).copied()?,
418        };
419
420        let image = self
421            .image_store
422            .update(cx, |image_store, _| {
423                if let Some(image) = image_store.get(image_id) {
424                    Some(image)
425                } else {
426                    image_store.opened_images.remove(&image_id);
427                    None
428                }
429            })
430            .ok()
431            .flatten();
432        let image = if let Some(image) = image {
433            image
434        } else {
435            self.local_image_ids_by_path.remove(&project_path);
436            self.local_image_ids_by_entry_id.remove(&entry_id);
437            return None;
438        };
439
440        image.update(cx, |image, cx| {
441            let Some(old_file) = worktree::File::from_dyn(Some(&image.file)) else {
442                return;
443            };
444            if old_file.worktree != *worktree {
445                return;
446            }
447
448            let new_file = if let Some(entry) = old_file
449                .entry_id
450                .and_then(|entry_id| snapshot.entry_for_id(entry_id))
451            {
452                worktree::File {
453                    is_local: true,
454                    entry_id: Some(entry.id),
455                    mtime: entry.mtime,
456                    path: entry.path.clone(),
457                    worktree: worktree.clone(),
458                    is_deleted: false,
459                    is_private: entry.is_private,
460                }
461            } else if let Some(entry) = snapshot.entry_for_path(old_file.path.as_ref()) {
462                worktree::File {
463                    is_local: true,
464                    entry_id: Some(entry.id),
465                    mtime: entry.mtime,
466                    path: entry.path.clone(),
467                    worktree: worktree.clone(),
468                    is_deleted: false,
469                    is_private: entry.is_private,
470                }
471            } else {
472                worktree::File {
473                    is_local: true,
474                    entry_id: old_file.entry_id,
475                    path: old_file.path.clone(),
476                    mtime: old_file.mtime,
477                    worktree: worktree.clone(),
478                    is_deleted: true,
479                    is_private: old_file.is_private,
480                }
481            };
482
483            if new_file == *old_file {
484                return;
485            }
486
487            if new_file.path != old_file.path {
488                self.local_image_ids_by_path.remove(&ProjectPath {
489                    path: old_file.path.clone(),
490                    worktree_id: old_file.worktree_id(cx),
491                });
492                self.local_image_ids_by_path.insert(
493                    ProjectPath {
494                        worktree_id: new_file.worktree_id(cx),
495                        path: new_file.path.clone(),
496                    },
497                    image_id,
498                );
499            }
500
501            if new_file.entry_id != old_file.entry_id {
502                if let Some(entry_id) = old_file.entry_id {
503                    self.local_image_ids_by_entry_id.remove(&entry_id);
504                }
505                if let Some(entry_id) = new_file.entry_id {
506                    self.local_image_ids_by_entry_id.insert(entry_id, image_id);
507                }
508            }
509
510            image.file_updated(Arc::new(new_file), cx);
511        });
512        None
513    }
514
515    fn image_changed_file(&mut self, image: Model<ImageItem>, cx: &mut AppContext) -> Option<()> {
516        let file = worktree::File::from_dyn(Some(&image.read(cx).file))?;
517
518        let image_id = image.read(cx).id;
519        if let Some(entry_id) = file.entry_id {
520            match self.local_image_ids_by_entry_id.get(&entry_id) {
521                Some(_) => {
522                    return None;
523                }
524                None => {
525                    self.local_image_ids_by_entry_id.insert(entry_id, image_id);
526                }
527            }
528        };
529        self.local_image_ids_by_path.insert(
530            ProjectPath {
531                worktree_id: file.worktree_id(cx),
532                path: file.path.clone(),
533            },
534            image_id,
535        );
536
537        Some(())
538    }
539}
540
541fn create_gpui_image(content: Vec<u8>) -> anyhow::Result<Arc<gpui::Image>> {
542    let format = image::guess_format(&content)?;
543
544    Ok(Arc::new(gpui::Image {
545        id: hash(&content),
546        format: match format {
547            image::ImageFormat::Png => gpui::ImageFormat::Png,
548            image::ImageFormat::Jpeg => gpui::ImageFormat::Jpeg,
549            image::ImageFormat::WebP => gpui::ImageFormat::Webp,
550            image::ImageFormat::Gif => gpui::ImageFormat::Gif,
551            image::ImageFormat::Bmp => gpui::ImageFormat::Bmp,
552            image::ImageFormat::Tiff => gpui::ImageFormat::Tiff,
553            _ => Err(anyhow::anyhow!("Image format not supported"))?,
554        },
555        bytes: content,
556    }))
557}
558
559impl ImageStoreImpl for Model<RemoteImageStore> {
560    fn open_image(
561        &self,
562        _path: Arc<Path>,
563        _worktree: Model<Worktree>,
564        _cx: &mut ModelContext<ImageStore>,
565    ) -> Task<Result<Model<ImageItem>>> {
566        Task::ready(Err(anyhow::anyhow!(
567            "Opening images from remote is not supported"
568        )))
569    }
570
571    fn reload_images(
572        &self,
573        _images: HashSet<Model<ImageItem>>,
574        _cx: &mut ModelContext<ImageStore>,
575    ) -> Task<Result<()>> {
576        Task::ready(Err(anyhow::anyhow!(
577            "Reloading images from remote is not supported"
578        )))
579    }
580
581    fn as_local(&self) -> Option<Model<LocalImageStore>> {
582        None
583    }
584}