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