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}