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 pub 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}