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