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 _, TypedEnvelope, proto};
15use std::num::NonZeroU64;
16use std::path::PathBuf;
17use std::sync::Arc;
18use util::{ResultExt, rel_path::RelPath};
19use worktree::{LoadedBinaryFile, PathChange, Worktree, WorktreeId};
20
21#[derive(Clone, Copy, Debug, Hash, PartialEq, PartialOrd, Ord, Eq)]
22pub struct ImageId(NonZeroU64);
23
24impl ImageId {
25 pub fn to_proto(&self) -> u64 {
26 self.0.get()
27 }
28}
29
30impl std::fmt::Display for ImageId {
31 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32 write!(f, "{}", self.0)
33 }
34}
35
36impl From<NonZeroU64> for ImageId {
37 fn from(id: NonZeroU64) -> Self {
38 ImageId(id)
39 }
40}
41
42#[derive(Debug)]
43pub enum ImageItemEvent {
44 ReloadNeeded,
45 Reloaded,
46 FileHandleChanged,
47 MetadataUpdated,
48}
49
50impl EventEmitter<ImageItemEvent> for ImageItem {}
51
52pub enum ImageStoreEvent {
53 ImageAdded(Entity<ImageItem>),
54}
55
56impl EventEmitter<ImageStoreEvent> for ImageStore {}
57
58#[derive(Debug, Clone, Copy)]
59pub struct ImageMetadata {
60 pub width: u32,
61 pub height: u32,
62 pub file_size: u64,
63 pub colors: Option<ImageColorInfo>,
64 pub format: ImageFormat,
65}
66
67#[derive(Debug, Clone, Copy)]
68pub struct ImageColorInfo {
69 pub channels: u8,
70 pub bits_per_channel: u8,
71}
72
73impl ImageColorInfo {
74 pub fn from_color_type(color_type: impl Into<ExtendedColorType>) -> Option<Self> {
75 let (channels, bits_per_channel) = match color_type.into() {
76 ExtendedColorType::L8 => (1, 8),
77 ExtendedColorType::L16 => (1, 16),
78 ExtendedColorType::La8 => (2, 8),
79 ExtendedColorType::La16 => (2, 16),
80 ExtendedColorType::Rgb8 => (3, 8),
81 ExtendedColorType::Rgb16 => (3, 16),
82 ExtendedColorType::Rgba8 => (4, 8),
83 ExtendedColorType::Rgba16 => (4, 16),
84 ExtendedColorType::A8 => (1, 8),
85 ExtendedColorType::Bgr8 => (3, 8),
86 ExtendedColorType::Bgra8 => (4, 8),
87 ExtendedColorType::Cmyk8 => (4, 8),
88 _ => return None,
89 };
90
91 Some(Self {
92 channels,
93 bits_per_channel,
94 })
95 }
96
97 pub const fn bits_per_pixel(&self) -> u8 {
98 self.channels * self.bits_per_channel
99 }
100}
101
102pub struct ImageItem {
103 pub id: ImageId,
104 pub file: Arc<worktree::File>,
105 pub image: Arc<gpui::Image>,
106 reload_task: Option<Task<()>>,
107 pub image_metadata: Option<ImageMetadata>,
108}
109
110impl ImageItem {
111 fn compute_metadata_from_bytes(image_bytes: &[u8]) -> Result<ImageMetadata> {
112 let image_format = image::guess_format(image_bytes)?;
113
114 let mut image_reader = ImageReader::new(std::io::Cursor::new(image_bytes));
115 image_reader.set_format(image_format);
116 let image = image_reader.decode()?;
117
118 let (width, height) = image.dimensions();
119
120 Ok(ImageMetadata {
121 width,
122 height,
123 file_size: image_bytes.len() as u64,
124 format: image_format,
125 colors: ImageColorInfo::from_color_type(image.color()),
126 })
127 }
128
129 pub async fn load_image_metadata(
130 image: Entity<ImageItem>,
131 project: Entity<Project>,
132 cx: &mut AsyncApp,
133 ) -> Result<ImageMetadata> {
134 let (fs, image_path) = cx.update(|cx| {
135 let fs = project.read(cx).fs().clone();
136 let image_path = image
137 .read(cx)
138 .abs_path(cx)
139 .context("absolutizing image file path")?;
140 anyhow::Ok((fs, image_path))
141 })??;
142
143 let image_bytes = fs.load_bytes(&image_path).await?;
144 Self::compute_metadata_from_bytes(&image_bytes)
145 }
146
147 pub fn project_path(&self, cx: &App) -> ProjectPath {
148 ProjectPath {
149 worktree_id: self.file.worktree_id(cx),
150 path: self.file.path().clone(),
151 }
152 }
153
154 pub fn abs_path(&self, cx: &App) -> Option<PathBuf> {
155 Some(self.file.as_local()?.abs_path(cx))
156 }
157
158 fn file_updated(&mut self, new_file: Arc<worktree::File>, cx: &mut Context<Self>) {
159 let mut file_changed = false;
160
161 let old_file = &self.file;
162 if new_file.path() != old_file.path() {
163 file_changed = true;
164 }
165
166 let old_state = old_file.disk_state();
167 let new_state = new_file.disk_state();
168 if old_state != new_state {
169 file_changed = true;
170 if matches!(new_state, DiskState::Present { .. }) {
171 cx.emit(ImageItemEvent::ReloadNeeded)
172 }
173 }
174
175 self.file = new_file;
176 if file_changed {
177 cx.emit(ImageItemEvent::FileHandleChanged);
178 cx.notify();
179 }
180 }
181
182 fn reload(&mut self, cx: &mut Context<Self>) -> Option<oneshot::Receiver<()>> {
183 let local_file = self.file.as_local()?;
184 let (tx, rx) = futures::channel::oneshot::channel();
185
186 let content = local_file.load_bytes(cx);
187 self.reload_task = Some(cx.spawn(async move |this, cx| {
188 if let Some(image) = content
189 .await
190 .context("Failed to load image content")
191 .and_then(create_gpui_image)
192 .log_err()
193 {
194 this.update(cx, |this, cx| {
195 this.image = image;
196 cx.emit(ImageItemEvent::Reloaded);
197 })
198 .log_err();
199 }
200 _ = tx.send(());
201 }));
202 Some(rx)
203 }
204}
205
206pub fn is_image_file(project: &Entity<Project>, path: &ProjectPath, cx: &App) -> bool {
207 let ext = util::maybe!({
208 let worktree_abs_path = project
209 .read(cx)
210 .worktree_for_id(path.worktree_id, cx)?
211 .read(cx)
212 .abs_path();
213 path.path
214 .extension()
215 .or_else(|| worktree_abs_path.extension()?.to_str())
216 .map(str::to_lowercase)
217 });
218
219 match ext {
220 Some(ext) => Img::extensions().contains(&ext.as_str()) && !ext.contains("svg"),
221 None => false,
222 }
223}
224
225impl ProjectItem for ImageItem {
226 fn try_open(
227 project: &Entity<Project>,
228 path: &ProjectPath,
229 cx: &mut App,
230 ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
231 if is_image_file(project, path, cx) {
232 Some(cx.spawn({
233 let path = path.clone();
234 let project = project.clone();
235 async move |cx| {
236 project
237 .update(cx, |project, cx| project.open_image(path, cx))?
238 .await
239 }
240 }))
241 } else {
242 None
243 }
244 }
245
246 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
247 self.file.entry_id
248 }
249
250 fn project_path(&self, cx: &App) -> Option<ProjectPath> {
251 Some(self.project_path(cx))
252 }
253
254 fn is_dirty(&self) -> bool {
255 false
256 }
257}
258
259trait ImageStoreImpl {
260 fn open_image(
261 &self,
262 path: Arc<RelPath>,
263 worktree: Entity<Worktree>,
264 cx: &mut Context<ImageStore>,
265 ) -> Task<Result<Entity<ImageItem>>>;
266
267 fn reload_images(
268 &self,
269 images: HashSet<Entity<ImageItem>>,
270 cx: &mut Context<ImageStore>,
271 ) -> Task<Result<()>>;
272
273 fn as_local(&self) -> Option<Entity<LocalImageStore>>;
274 fn as_remote(&self) -> Option<Entity<RemoteImageStore>>;
275}
276
277struct RemoteImageStore {
278 upstream_client: AnyProtoClient,
279 project_id: u64,
280 loading_remote_images_by_id: HashMap<ImageId, LoadingRemoteImage>,
281 remote_image_listeners:
282 HashMap<ImageId, Vec<oneshot::Sender<anyhow::Result<Entity<ImageItem>>>>>,
283 loaded_images: HashMap<ImageId, Entity<ImageItem>>,
284}
285
286struct LoadingRemoteImage {
287 state: proto::ImageState,
288 chunks: Vec<Vec<u8>>,
289 received_size: u64,
290}
291
292struct LocalImageStore {
293 local_image_ids_by_path: HashMap<ProjectPath, ImageId>,
294 local_image_ids_by_entry_id: HashMap<ProjectEntryId, ImageId>,
295 image_store: WeakEntity<ImageStore>,
296 _subscription: Subscription,
297}
298
299pub struct ImageStore {
300 state: Box<dyn ImageStoreImpl>,
301 opened_images: HashMap<ImageId, WeakEntity<ImageItem>>,
302 worktree_store: Entity<WorktreeStore>,
303 #[allow(clippy::type_complexity)]
304 loading_images_by_path: HashMap<
305 ProjectPath,
306 postage::watch::Receiver<Option<Result<Entity<ImageItem>, Arc<anyhow::Error>>>>,
307 >,
308}
309
310impl ImageStore {
311 pub fn local(worktree_store: Entity<WorktreeStore>, cx: &mut Context<Self>) -> Self {
312 let this = cx.weak_entity();
313 Self {
314 state: Box::new(cx.new(|cx| {
315 let subscription = cx.subscribe(
316 &worktree_store,
317 |this: &mut LocalImageStore, _, event, cx| {
318 if let WorktreeStoreEvent::WorktreeAdded(worktree) = event {
319 this.subscribe_to_worktree(worktree, cx);
320 }
321 },
322 );
323
324 LocalImageStore {
325 local_image_ids_by_path: Default::default(),
326 local_image_ids_by_entry_id: Default::default(),
327 image_store: this,
328 _subscription: subscription,
329 }
330 })),
331 opened_images: Default::default(),
332 loading_images_by_path: Default::default(),
333 worktree_store,
334 }
335 }
336
337 pub fn remote(
338 worktree_store: Entity<WorktreeStore>,
339 upstream_client: AnyProtoClient,
340 project_id: u64,
341 cx: &mut Context<Self>,
342 ) -> Self {
343 Self {
344 state: Box::new(cx.new(|_| RemoteImageStore {
345 upstream_client,
346 project_id,
347 loading_remote_images_by_id: Default::default(),
348 remote_image_listeners: Default::default(),
349 loaded_images: Default::default(),
350 })),
351 opened_images: Default::default(),
352 loading_images_by_path: Default::default(),
353 worktree_store,
354 }
355 }
356
357 pub fn images(&self) -> impl '_ + Iterator<Item = Entity<ImageItem>> {
358 self.opened_images
359 .values()
360 .filter_map(|image| image.upgrade())
361 }
362
363 pub fn get(&self, image_id: ImageId) -> Option<Entity<ImageItem>> {
364 self.opened_images
365 .get(&image_id)
366 .and_then(|image| image.upgrade())
367 }
368
369 pub fn get_by_path(&self, path: &ProjectPath, cx: &App) -> Option<Entity<ImageItem>> {
370 self.images()
371 .find(|image| &image.read(cx).project_path(cx) == path)
372 }
373
374 pub fn open_image(
375 &mut self,
376 project_path: ProjectPath,
377 cx: &mut Context<Self>,
378 ) -> Task<Result<Entity<ImageItem>>> {
379 let existing_image = self.get_by_path(&project_path, cx);
380 if let Some(existing_image) = existing_image {
381 return Task::ready(Ok(existing_image));
382 }
383
384 let Some(worktree) = self
385 .worktree_store
386 .read(cx)
387 .worktree_for_id(project_path.worktree_id, cx)
388 else {
389 return Task::ready(Err(anyhow::anyhow!("no such worktree")));
390 };
391
392 let loading_watch = match self.loading_images_by_path.entry(project_path.clone()) {
393 // If the given path is already being loaded, then wait for that existing
394 // task to complete and return the same image.
395 hash_map::Entry::Occupied(e) => e.get().clone(),
396
397 // Otherwise, record the fact that this path is now being loaded.
398 hash_map::Entry::Vacant(entry) => {
399 let (mut tx, rx) = postage::watch::channel();
400 entry.insert(rx.clone());
401
402 let load_image = self
403 .state
404 .open_image(project_path.path.clone(), worktree, cx);
405
406 cx.spawn(async move |this, cx| {
407 let load_result = load_image.await;
408 *tx.borrow_mut() = Some(this.update(cx, |this, _cx| {
409 // Record the fact that the image is no longer loading.
410 this.loading_images_by_path.remove(&project_path);
411 let image = load_result.map_err(Arc::new)?;
412 Ok(image)
413 })?);
414 anyhow::Ok(())
415 })
416 .detach();
417 rx
418 }
419 };
420
421 cx.background_spawn(async move {
422 Self::wait_for_loading_image(loading_watch)
423 .await
424 .map_err(|e| e.cloned())
425 })
426 }
427
428 pub async fn wait_for_loading_image(
429 mut receiver: postage::watch::Receiver<
430 Option<Result<Entity<ImageItem>, Arc<anyhow::Error>>>,
431 >,
432 ) -> Result<Entity<ImageItem>, Arc<anyhow::Error>> {
433 loop {
434 if let Some(result) = receiver.borrow().as_ref() {
435 match result {
436 Ok(image) => return Ok(image.to_owned()),
437 Err(e) => return Err(e.to_owned()),
438 }
439 }
440 receiver.next().await;
441 }
442 }
443
444 pub fn reload_images(
445 &self,
446 images: HashSet<Entity<ImageItem>>,
447 cx: &mut Context<ImageStore>,
448 ) -> Task<Result<()>> {
449 if images.is_empty() {
450 return Task::ready(Ok(()));
451 }
452
453 self.state.reload_images(images, cx)
454 }
455
456 fn add_image(&mut self, image: Entity<ImageItem>, cx: &mut Context<ImageStore>) -> Result<()> {
457 let image_id = image.read(cx).id;
458 self.opened_images.insert(image_id, image.downgrade());
459 cx.subscribe(&image, Self::on_image_event).detach();
460 cx.emit(ImageStoreEvent::ImageAdded(image));
461 Ok(())
462 }
463
464 fn on_image_event(
465 &mut self,
466 image: Entity<ImageItem>,
467 event: &ImageItemEvent,
468 cx: &mut Context<Self>,
469 ) {
470 if let ImageItemEvent::FileHandleChanged = event
471 && let Some(local) = self.state.as_local()
472 {
473 local.update(cx, |local, cx| {
474 local.image_changed_file(image, cx);
475 })
476 }
477 }
478
479 pub fn handle_create_image_for_peer(
480 &mut self,
481 envelope: TypedEnvelope<proto::CreateImageForPeer>,
482 cx: &mut Context<Self>,
483 ) -> Result<()> {
484 if let Some(remote) = self.state.as_remote() {
485 let worktree_store = self.worktree_store.clone();
486 let image = remote.update(cx, |remote, cx| {
487 remote.handle_create_image_for_peer(envelope, &worktree_store, cx)
488 })?;
489 if let Some(image) = image {
490 remote.update(cx, |this, cx| {
491 let image = image.clone();
492 let image_id = image.read(cx).id;
493 this.loaded_images.insert(image_id, image)
494 });
495
496 self.add_image(image, cx)?;
497 }
498 }
499
500 Ok(())
501 }
502}
503
504impl RemoteImageStore {
505 pub fn wait_for_remote_image(
506 &mut self,
507 id: ImageId,
508 cx: &mut Context<Self>,
509 ) -> Task<Result<Entity<ImageItem>>> {
510 if let Some(image) = self.loaded_images.remove(&id) {
511 return Task::ready(Ok(image));
512 }
513
514 let (tx, rx) = oneshot::channel();
515 self.remote_image_listeners.entry(id).or_default().push(tx);
516
517 cx.spawn(async move |_this, cx| {
518 let result = cx.background_spawn(async move { rx.await? }).await;
519 result
520 })
521 }
522
523 pub fn handle_create_image_for_peer(
524 &mut self,
525 envelope: TypedEnvelope<proto::CreateImageForPeer>,
526 worktree_store: &Entity<WorktreeStore>,
527 cx: &mut Context<Self>,
528 ) -> Result<Option<Entity<ImageItem>>> {
529 use proto::create_image_for_peer::Variant;
530 match envelope.payload.variant {
531 Some(Variant::State(state)) => {
532 let image_id =
533 ImageId::from(NonZeroU64::new(state.id).context("invalid image id")?);
534
535 self.loading_remote_images_by_id.insert(
536 image_id,
537 LoadingRemoteImage {
538 state,
539 chunks: Vec::new(),
540 received_size: 0,
541 },
542 );
543 Ok(None)
544 }
545 Some(Variant::Chunk(chunk)) => {
546 let image_id =
547 ImageId::from(NonZeroU64::new(chunk.image_id).context("invalid image id")?);
548
549 let loading = self
550 .loading_remote_images_by_id
551 .get_mut(&image_id)
552 .context("received chunk for unknown image")?;
553
554 loading.received_size += chunk.data.len() as u64;
555 loading.chunks.push(chunk.data);
556
557 if loading.received_size == loading.state.content_size {
558 let loading = self.loading_remote_images_by_id.remove(&image_id).unwrap();
559
560 let mut content = Vec::with_capacity(loading.received_size as usize);
561 for chunk_data in loading.chunks {
562 content.extend_from_slice(&chunk_data);
563 }
564
565 let image_metadata = ImageItem::compute_metadata_from_bytes(&content).log_err();
566 let image = create_gpui_image(content)?;
567
568 let proto_file = loading.state.file.context("missing file in image state")?;
569 let worktree_id = WorktreeId::from_proto(proto_file.worktree_id);
570 let worktree = worktree_store
571 .read(cx)
572 .worktree_for_id(worktree_id, cx)
573 .context("worktree not found")?;
574
575 let file = Arc::new(
576 worktree::File::from_proto(proto_file, worktree, cx)
577 .context("invalid file in image state")?,
578 );
579
580 let entity = cx.new(|_cx| ImageItem {
581 id: image_id,
582 file,
583 image,
584 image_metadata,
585 reload_task: None,
586 });
587
588 if let Some(listeners) = self.remote_image_listeners.remove(&image_id) {
589 for listener in listeners {
590 listener.send(Ok(entity.clone())).ok();
591 }
592 }
593
594 Ok(Some(entity))
595 } else {
596 Ok(None)
597 }
598 }
599 None => {
600 log::warn!("Received CreateImageForPeer with no variant");
601 Ok(None)
602 }
603 }
604 }
605
606 // TODO: subscribe to worktree and update image contents or at least mark as dirty on file changes
607}
608
609impl ImageStoreImpl for Entity<LocalImageStore> {
610 fn open_image(
611 &self,
612 path: Arc<RelPath>,
613 worktree: Entity<Worktree>,
614 cx: &mut Context<ImageStore>,
615 ) -> Task<Result<Entity<ImageItem>>> {
616 let this = self.clone();
617
618 let load_file = worktree.update(cx, |worktree, cx| {
619 worktree.load_binary_file(path.as_ref(), cx)
620 });
621 cx.spawn(async move |image_store, cx| {
622 let LoadedBinaryFile { file, content } = load_file.await?;
623 let image = create_gpui_image(content)?;
624
625 let entity = cx.new(|cx| ImageItem {
626 id: cx.entity_id().as_non_zero_u64().into(),
627 file: file.clone(),
628 image,
629 image_metadata: None,
630 reload_task: None,
631 })?;
632
633 let image_id = cx.read_entity(&entity, |model, _| model.id)?;
634
635 this.update(cx, |this, cx| {
636 image_store.update(cx, |image_store, cx| {
637 image_store.add_image(entity.clone(), cx)
638 })??;
639 this.local_image_ids_by_path.insert(
640 ProjectPath {
641 worktree_id: file.worktree_id(cx),
642 path: file.path.clone(),
643 },
644 image_id,
645 );
646
647 if let Some(entry_id) = file.entry_id {
648 this.local_image_ids_by_entry_id.insert(entry_id, image_id);
649 }
650
651 anyhow::Ok(())
652 })??;
653
654 Ok(entity)
655 })
656 }
657
658 fn reload_images(
659 &self,
660 images: HashSet<Entity<ImageItem>>,
661 cx: &mut Context<ImageStore>,
662 ) -> Task<Result<()>> {
663 cx.spawn(async move |_, cx| {
664 for image in images {
665 if let Some(rec) = image.update(cx, |image, cx| image.reload(cx))? {
666 rec.await?
667 }
668 }
669 Ok(())
670 })
671 }
672
673 fn as_local(&self) -> Option<Entity<LocalImageStore>> {
674 Some(self.clone())
675 }
676
677 fn as_remote(&self) -> Option<Entity<RemoteImageStore>> {
678 None
679 }
680}
681
682impl ImageStoreImpl for Entity<RemoteImageStore> {
683 fn open_image(
684 &self,
685 path: Arc<RelPath>,
686 worktree: Entity<Worktree>,
687 cx: &mut Context<ImageStore>,
688 ) -> Task<Result<Entity<ImageItem>>> {
689 let worktree_id = worktree.read(cx).id().to_proto();
690 let (project_id, client) = {
691 let store = self.read(cx);
692 (store.project_id, store.upstream_client.clone())
693 };
694 let remote_store = self.clone();
695
696 cx.spawn(async move |_image_store, cx| {
697 let response = client
698 .request(rpc::proto::OpenImageByPath {
699 project_id,
700 worktree_id,
701 path: path.to_proto(),
702 })
703 .await?;
704
705 let image_id = ImageId::from(
706 NonZeroU64::new(response.image_id).context("invalid image_id in response")?,
707 );
708
709 remote_store
710 .update(cx, |remote_store, cx| {
711 remote_store.wait_for_remote_image(image_id, cx)
712 })?
713 .await
714 })
715 }
716
717 fn reload_images(
718 &self,
719 _images: HashSet<Entity<ImageItem>>,
720 _cx: &mut Context<ImageStore>,
721 ) -> Task<Result<()>> {
722 Task::ready(Err(anyhow::anyhow!(
723 "Reloading images from remote is not supported"
724 )))
725 }
726
727 fn as_local(&self) -> Option<Entity<LocalImageStore>> {
728 None
729 }
730
731 fn as_remote(&self) -> Option<Entity<RemoteImageStore>> {
732 Some(self.clone())
733 }
734}
735
736impl LocalImageStore {
737 fn subscribe_to_worktree(&mut self, worktree: &Entity<Worktree>, cx: &mut Context<Self>) {
738 cx.subscribe(worktree, |this, worktree, event, cx| {
739 if worktree.read(cx).is_local()
740 && let worktree::Event::UpdatedEntries(changes) = event
741 {
742 this.local_worktree_entries_changed(&worktree, changes, cx);
743 }
744 })
745 .detach();
746 }
747
748 fn local_worktree_entries_changed(
749 &mut self,
750 worktree_handle: &Entity<Worktree>,
751 changes: &[(Arc<RelPath>, ProjectEntryId, PathChange)],
752 cx: &mut Context<Self>,
753 ) {
754 let snapshot = worktree_handle.read(cx).snapshot();
755 for (path, entry_id, _) in changes {
756 self.local_worktree_entry_changed(*entry_id, path, worktree_handle, &snapshot, cx);
757 }
758 }
759
760 fn local_worktree_entry_changed(
761 &mut self,
762 entry_id: ProjectEntryId,
763 path: &Arc<RelPath>,
764 worktree: &Entity<worktree::Worktree>,
765 snapshot: &worktree::Snapshot,
766 cx: &mut Context<Self>,
767 ) -> Option<()> {
768 let project_path = ProjectPath {
769 worktree_id: snapshot.id(),
770 path: path.clone(),
771 };
772 let image_id = match self.local_image_ids_by_entry_id.get(&entry_id) {
773 Some(&image_id) => image_id,
774 None => self.local_image_ids_by_path.get(&project_path).copied()?,
775 };
776
777 let image = self
778 .image_store
779 .update(cx, |image_store, _| {
780 if let Some(image) = image_store.get(image_id) {
781 Some(image)
782 } else {
783 image_store.opened_images.remove(&image_id);
784 None
785 }
786 })
787 .ok()
788 .flatten();
789 let image = if let Some(image) = image {
790 image
791 } else {
792 self.local_image_ids_by_path.remove(&project_path);
793 self.local_image_ids_by_entry_id.remove(&entry_id);
794 return None;
795 };
796
797 image.update(cx, |image, cx| {
798 let old_file = &image.file;
799 if old_file.worktree != *worktree {
800 return;
801 }
802
803 let snapshot_entry = old_file
804 .entry_id
805 .and_then(|entry_id| snapshot.entry_for_id(entry_id))
806 .or_else(|| snapshot.entry_for_path(old_file.path.as_ref()));
807
808 let new_file = if let Some(entry) = snapshot_entry {
809 worktree::File {
810 disk_state: match entry.mtime {
811 Some(mtime) => DiskState::Present { mtime },
812 None => old_file.disk_state,
813 },
814 is_local: true,
815 entry_id: Some(entry.id),
816 path: entry.path.clone(),
817 worktree: worktree.clone(),
818 is_private: entry.is_private,
819 }
820 } else {
821 worktree::File {
822 disk_state: DiskState::Deleted,
823 is_local: true,
824 entry_id: old_file.entry_id,
825 path: old_file.path.clone(),
826 worktree: worktree.clone(),
827 is_private: old_file.is_private,
828 }
829 };
830
831 if new_file == **old_file {
832 return;
833 }
834
835 if new_file.path != old_file.path {
836 self.local_image_ids_by_path.remove(&ProjectPath {
837 path: old_file.path.clone(),
838 worktree_id: old_file.worktree_id(cx),
839 });
840 self.local_image_ids_by_path.insert(
841 ProjectPath {
842 worktree_id: new_file.worktree_id(cx),
843 path: new_file.path.clone(),
844 },
845 image_id,
846 );
847 }
848
849 if new_file.entry_id != old_file.entry_id {
850 if let Some(entry_id) = old_file.entry_id {
851 self.local_image_ids_by_entry_id.remove(&entry_id);
852 }
853 if let Some(entry_id) = new_file.entry_id {
854 self.local_image_ids_by_entry_id.insert(entry_id, image_id);
855 }
856 }
857
858 image.file_updated(Arc::new(new_file), cx);
859 });
860 None
861 }
862
863 fn image_changed_file(&mut self, image: Entity<ImageItem>, cx: &mut App) -> Option<()> {
864 let image = image.read(cx);
865 let file = &image.file;
866
867 let image_id = image.id;
868 if let Some(entry_id) = file.entry_id {
869 match self.local_image_ids_by_entry_id.get(&entry_id) {
870 Some(_) => {
871 return None;
872 }
873 None => {
874 self.local_image_ids_by_entry_id.insert(entry_id, image_id);
875 }
876 }
877 };
878 self.local_image_ids_by_path.insert(
879 ProjectPath {
880 worktree_id: file.worktree_id(cx),
881 path: file.path.clone(),
882 },
883 image_id,
884 );
885
886 Some(())
887 }
888}
889
890fn create_gpui_image(content: Vec<u8>) -> anyhow::Result<Arc<gpui::Image>> {
891 let format = image::guess_format(&content)?;
892
893 Ok(Arc::new(gpui::Image::from_bytes(
894 match format {
895 image::ImageFormat::Png => gpui::ImageFormat::Png,
896 image::ImageFormat::Jpeg => gpui::ImageFormat::Jpeg,
897 image::ImageFormat::WebP => gpui::ImageFormat::Webp,
898 image::ImageFormat::Gif => gpui::ImageFormat::Gif,
899 image::ImageFormat::Bmp => gpui::ImageFormat::Bmp,
900 image::ImageFormat::Tiff => gpui::ImageFormat::Tiff,
901 image::ImageFormat::Ico => gpui::ImageFormat::Ico,
902 format => anyhow::bail!("Image format {format:?} not supported"),
903 },
904 content,
905 )))
906}
907
908#[cfg(test)]
909mod tests {
910 use super::*;
911 use fs::FakeFs;
912 use gpui::TestAppContext;
913 use serde_json::json;
914 use settings::SettingsStore;
915 use util::rel_path::rel_path;
916
917 pub fn init_test(cx: &mut TestAppContext) {
918 zlog::init_test();
919
920 cx.update(|cx| {
921 let settings_store = SettingsStore::test(cx);
922 cx.set_global(settings_store);
923 });
924 }
925
926 #[gpui::test]
927 async fn test_image_not_loaded_twice(cx: &mut TestAppContext) {
928 init_test(cx);
929 let fs = FakeFs::new(cx.executor());
930
931 fs.insert_tree("/root", json!({})).await;
932 // Create a png file that consists of a single white pixel
933 fs.insert_file(
934 "/root/image_1.png",
935 vec![
936 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48,
937 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00,
938 0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41, 0x54, 0x78,
939 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4, 0x00,
940 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
941 ],
942 )
943 .await;
944
945 let project = Project::test(fs, ["/root".as_ref()], cx).await;
946
947 let worktree_id =
948 cx.update(|cx| project.read(cx).worktrees(cx).next().unwrap().read(cx).id());
949
950 let project_path = ProjectPath {
951 worktree_id,
952 path: rel_path("image_1.png").into(),
953 };
954
955 let (task1, task2) = project.update(cx, |project, cx| {
956 (
957 project.open_image(project_path.clone(), cx),
958 project.open_image(project_path.clone(), cx),
959 )
960 });
961
962 let image1 = task1.await.unwrap();
963 let image2 = task2.await.unwrap();
964
965 assert_eq!(image1, image2);
966 }
967
968 #[gpui::test]
969 fn test_compute_metadata_from_bytes() {
970 // Single white pixel PNG
971 let png_bytes = vec![
972 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48,
973 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00,
974 0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41, 0x54, 0x78,
975 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4, 0x00,
976 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
977 ];
978
979 let metadata = ImageItem::compute_metadata_from_bytes(&png_bytes).unwrap();
980
981 assert_eq!(metadata.width, 1);
982 assert_eq!(metadata.height, 1);
983 assert_eq!(metadata.file_size, png_bytes.len() as u64);
984 assert_eq!(metadata.format, image::ImageFormat::Png);
985 assert!(metadata.colors.is_some());
986 }
987}