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