1use crate::worktree_store::{WorktreeStore, WorktreeStoreEvent};
2use crate::{Project, ProjectEntryId, ProjectPath};
3use anyhow::{Context as _, Result};
4use collections::{HashMap, HashSet};
5use futures::channel::oneshot;
6use gpui::{
7 hash, prelude::*, AppContext, EventEmitter, Img, Model, ModelContext, Subscription, Task,
8 WeakModel,
9};
10use language::File;
11use rpc::AnyProtoClient;
12use std::ffi::OsStr;
13use std::num::NonZeroU64;
14use std::path::Path;
15use std::sync::Arc;
16use util::ResultExt;
17use worktree::{LoadedBinaryFile, PathChange, Worktree};
18
19#[derive(Clone, Copy, Debug, Hash, PartialEq, PartialOrd, Ord, Eq)]
20pub struct ImageId(NonZeroU64);
21
22impl std::fmt::Display for ImageId {
23 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24 write!(f, "{}", self.0)
25 }
26}
27
28impl From<NonZeroU64> for ImageId {
29 fn from(id: NonZeroU64) -> Self {
30 ImageId(id)
31 }
32}
33
34pub enum ImageItemEvent {
35 ReloadNeeded,
36 Reloaded,
37 FileHandleChanged,
38}
39
40impl EventEmitter<ImageItemEvent> for ImageItem {}
41
42pub enum ImageStoreEvent {
43 ImageAdded(Model<ImageItem>),
44}
45
46impl EventEmitter<ImageStoreEvent> for ImageStore {}
47
48pub struct ImageItem {
49 pub id: ImageId,
50 pub file: Arc<dyn File>,
51 pub image: Arc<gpui::Image>,
52 reload_task: Option<Task<()>>,
53}
54
55impl ImageItem {
56 pub fn project_path(&self, cx: &AppContext) -> ProjectPath {
57 ProjectPath {
58 worktree_id: self.file.worktree_id(cx),
59 path: self.file.path().clone(),
60 }
61 }
62
63 pub fn path(&self) -> &Arc<Path> {
64 self.file.path()
65 }
66
67 fn file_updated(&mut self, new_file: Arc<dyn File>, cx: &mut ModelContext<Self>) {
68 let mut file_changed = false;
69
70 let old_file = self.file.as_ref();
71 if new_file.path() != old_file.path() {
72 file_changed = true;
73 }
74
75 if !new_file.is_deleted() {
76 let new_mtime = new_file.mtime();
77 if new_mtime != old_file.mtime() {
78 file_changed = true;
79 cx.emit(ImageItemEvent::ReloadNeeded);
80 }
81 }
82
83 self.file = new_file;
84 if file_changed {
85 cx.emit(ImageItemEvent::FileHandleChanged);
86 cx.notify();
87 }
88 }
89
90 fn reload(&mut self, cx: &mut ModelContext<Self>) -> Option<oneshot::Receiver<()>> {
91 let local_file = self.file.as_local()?;
92 let (tx, rx) = futures::channel::oneshot::channel();
93
94 let content = local_file.load_bytes(cx);
95 self.reload_task = Some(cx.spawn(|this, mut cx| async move {
96 if let Some(image) = content
97 .await
98 .context("Failed to load image content")
99 .and_then(create_gpui_image)
100 .log_err()
101 {
102 this.update(&mut cx, |this, cx| {
103 this.image = image;
104 cx.emit(ImageItemEvent::Reloaded);
105 })
106 .log_err();
107 }
108 _ = tx.send(());
109 }));
110 Some(rx)
111 }
112}
113
114impl crate::Item for ImageItem {
115 fn try_open(
116 project: &Model<Project>,
117 path: &ProjectPath,
118 cx: &mut AppContext,
119 ) -> Option<Task<gpui::Result<Model<Self>>>> {
120 let path = path.clone();
121 let project = project.clone();
122
123 let ext = path
124 .path
125 .extension()
126 .and_then(OsStr::to_str)
127 .map(str::to_lowercase)
128 .unwrap_or_default();
129 let ext = ext.as_str();
130
131 // Only open the item if it's a binary image (no SVGs, etc.)
132 // Since we do not have a way to toggle to an editor
133 if Img::extensions().contains(&ext) && !ext.contains("svg") {
134 Some(cx.spawn(|mut cx| async move {
135 project
136 .update(&mut cx, |project, cx| project.open_image(path, cx))?
137 .await
138 }))
139 } else {
140 None
141 }
142 }
143
144 fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
145 worktree::File::from_dyn(Some(&self.file))?.entry_id
146 }
147
148 fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
149 Some(self.project_path(cx).clone())
150 }
151}
152
153trait ImageStoreImpl {
154 fn open_image(
155 &self,
156 path: Arc<Path>,
157 worktree: Model<Worktree>,
158 cx: &mut ModelContext<ImageStore>,
159 ) -> Task<Result<Model<ImageItem>>>;
160
161 fn reload_images(
162 &self,
163 images: HashSet<Model<ImageItem>>,
164 cx: &mut ModelContext<ImageStore>,
165 ) -> Task<Result<()>>;
166
167 fn as_local(&self) -> Option<Model<LocalImageStore>>;
168}
169
170struct RemoteImageStore {}
171
172struct LocalImageStore {
173 local_image_ids_by_path: HashMap<ProjectPath, ImageId>,
174 local_image_ids_by_entry_id: HashMap<ProjectEntryId, ImageId>,
175 image_store: WeakModel<ImageStore>,
176 _subscription: Subscription,
177}
178
179pub struct ImageStore {
180 state: Box<dyn ImageStoreImpl>,
181 opened_images: HashMap<ImageId, WeakModel<ImageItem>>,
182 worktree_store: Model<WorktreeStore>,
183}
184
185impl ImageStore {
186 pub fn local(worktree_store: Model<WorktreeStore>, cx: &mut ModelContext<Self>) -> Self {
187 let this = cx.weak_model();
188 Self {
189 state: Box::new(cx.new_model(|cx| {
190 let subscription = cx.subscribe(
191 &worktree_store,
192 |this: &mut LocalImageStore, _, event, cx| {
193 if let WorktreeStoreEvent::WorktreeAdded(worktree) = event {
194 this.subscribe_to_worktree(worktree, cx);
195 }
196 },
197 );
198
199 LocalImageStore {
200 local_image_ids_by_path: Default::default(),
201 local_image_ids_by_entry_id: Default::default(),
202 image_store: this,
203 _subscription: subscription,
204 }
205 })),
206 opened_images: Default::default(),
207 worktree_store,
208 }
209 }
210
211 pub fn remote(
212 worktree_store: Model<WorktreeStore>,
213 _upstream_client: AnyProtoClient,
214 _remote_id: u64,
215 cx: &mut ModelContext<Self>,
216 ) -> Self {
217 Self {
218 state: Box::new(cx.new_model(|_| RemoteImageStore {})),
219 opened_images: Default::default(),
220 worktree_store,
221 }
222 }
223
224 pub fn images(&self) -> impl '_ + Iterator<Item = Model<ImageItem>> {
225 self.opened_images
226 .values()
227 .filter_map(|image| image.upgrade())
228 }
229
230 pub fn get(&self, image_id: ImageId) -> Option<Model<ImageItem>> {
231 self.opened_images
232 .get(&image_id)
233 .and_then(|image| image.upgrade())
234 }
235
236 pub fn get_by_path(&self, path: &ProjectPath, cx: &AppContext) -> Option<Model<ImageItem>> {
237 self.images()
238 .find(|image| &image.read(cx).project_path(cx) == path)
239 }
240
241 pub fn open_image(
242 &mut self,
243 project_path: ProjectPath,
244 cx: &mut ModelContext<Self>,
245 ) -> Task<Result<Model<ImageItem>>> {
246 let existing_image = self.get_by_path(&project_path, cx);
247 if let Some(existing_image) = existing_image {
248 return Task::ready(Ok(existing_image));
249 }
250
251 let Some(worktree) = self
252 .worktree_store
253 .read(cx)
254 .worktree_for_id(project_path.worktree_id, cx)
255 else {
256 return Task::ready(Err(anyhow::anyhow!("no such worktree")));
257 };
258
259 self.state
260 .open_image(project_path.path.clone(), worktree, cx)
261 }
262
263 pub fn reload_images(
264 &self,
265 images: HashSet<Model<ImageItem>>,
266 cx: &mut ModelContext<ImageStore>,
267 ) -> Task<Result<()>> {
268 if images.is_empty() {
269 return Task::ready(Ok(()));
270 }
271
272 self.state.reload_images(images, cx)
273 }
274
275 fn add_image(
276 &mut self,
277 image: Model<ImageItem>,
278 cx: &mut ModelContext<ImageStore>,
279 ) -> Result<()> {
280 let image_id = image.read(cx).id;
281
282 self.opened_images.insert(image_id, image.downgrade());
283
284 cx.subscribe(&image, Self::on_image_event).detach();
285 cx.emit(ImageStoreEvent::ImageAdded(image));
286 Ok(())
287 }
288
289 fn on_image_event(
290 &mut self,
291 image: Model<ImageItem>,
292 event: &ImageItemEvent,
293 cx: &mut ModelContext<Self>,
294 ) {
295 match event {
296 ImageItemEvent::FileHandleChanged => {
297 if let Some(local) = self.state.as_local() {
298 local.update(cx, |local, cx| {
299 local.image_changed_file(image, cx);
300 })
301 }
302 }
303 _ => {}
304 }
305 }
306}
307
308impl ImageStoreImpl for Model<LocalImageStore> {
309 fn open_image(
310 &self,
311 path: Arc<Path>,
312 worktree: Model<Worktree>,
313 cx: &mut ModelContext<ImageStore>,
314 ) -> Task<Result<Model<ImageItem>>> {
315 let this = self.clone();
316
317 let load_file = worktree.update(cx, |worktree, cx| {
318 worktree.load_binary_file(path.as_ref(), cx)
319 });
320 cx.spawn(move |image_store, mut cx| async move {
321 let LoadedBinaryFile { file, content } = load_file.await?;
322 let image = create_gpui_image(content)?;
323
324 let model = cx.new_model(|cx| ImageItem {
325 id: cx.entity_id().as_non_zero_u64().into(),
326 file: file.clone(),
327 image,
328 reload_task: None,
329 })?;
330
331 let image_id = cx.read_model(&model, |model, _| model.id)?;
332
333 this.update(&mut cx, |this, cx| {
334 image_store.update(cx, |image_store, cx| {
335 image_store.add_image(model.clone(), cx)
336 })??;
337 this.local_image_ids_by_path.insert(
338 ProjectPath {
339 worktree_id: file.worktree_id(cx),
340 path: file.path.clone(),
341 },
342 image_id,
343 );
344
345 if let Some(entry_id) = file.entry_id {
346 this.local_image_ids_by_entry_id.insert(entry_id, image_id);
347 }
348
349 anyhow::Ok(())
350 })??;
351
352 Ok(model)
353 })
354 }
355
356 fn reload_images(
357 &self,
358 images: HashSet<Model<ImageItem>>,
359 cx: &mut ModelContext<ImageStore>,
360 ) -> Task<Result<()>> {
361 cx.spawn(move |_, mut cx| async move {
362 for image in images {
363 if let Some(rec) = image.update(&mut cx, |image, cx| image.reload(cx))? {
364 rec.await?
365 }
366 }
367 Ok(())
368 })
369 }
370
371 fn as_local(&self) -> Option<Model<LocalImageStore>> {
372 Some(self.clone())
373 }
374}
375
376impl LocalImageStore {
377 fn subscribe_to_worktree(&mut self, worktree: &Model<Worktree>, cx: &mut ModelContext<Self>) {
378 cx.subscribe(worktree, |this, worktree, event, cx| {
379 if worktree.read(cx).is_local() {
380 match event {
381 worktree::Event::UpdatedEntries(changes) => {
382 this.local_worktree_entries_changed(&worktree, changes, cx);
383 }
384 _ => {}
385 }
386 }
387 })
388 .detach();
389 }
390
391 fn local_worktree_entries_changed(
392 &mut self,
393 worktree_handle: &Model<Worktree>,
394 changes: &[(Arc<Path>, ProjectEntryId, PathChange)],
395 cx: &mut ModelContext<Self>,
396 ) {
397 let snapshot = worktree_handle.read(cx).snapshot();
398 for (path, entry_id, _) in changes {
399 self.local_worktree_entry_changed(*entry_id, path, worktree_handle, &snapshot, cx);
400 }
401 }
402
403 fn local_worktree_entry_changed(
404 &mut self,
405 entry_id: ProjectEntryId,
406 path: &Arc<Path>,
407 worktree: &Model<worktree::Worktree>,
408 snapshot: &worktree::Snapshot,
409 cx: &mut ModelContext<Self>,
410 ) -> Option<()> {
411 let project_path = ProjectPath {
412 worktree_id: snapshot.id(),
413 path: path.clone(),
414 };
415 let image_id = match self.local_image_ids_by_entry_id.get(&entry_id) {
416 Some(&image_id) => image_id,
417 None => self.local_image_ids_by_path.get(&project_path).copied()?,
418 };
419
420 let image = self
421 .image_store
422 .update(cx, |image_store, _| {
423 if let Some(image) = image_store.get(image_id) {
424 Some(image)
425 } else {
426 image_store.opened_images.remove(&image_id);
427 None
428 }
429 })
430 .ok()
431 .flatten();
432 let image = if let Some(image) = image {
433 image
434 } else {
435 self.local_image_ids_by_path.remove(&project_path);
436 self.local_image_ids_by_entry_id.remove(&entry_id);
437 return None;
438 };
439
440 image.update(cx, |image, cx| {
441 let Some(old_file) = worktree::File::from_dyn(Some(&image.file)) else {
442 return;
443 };
444 if old_file.worktree != *worktree {
445 return;
446 }
447
448 let new_file = if let Some(entry) = old_file
449 .entry_id
450 .and_then(|entry_id| snapshot.entry_for_id(entry_id))
451 {
452 worktree::File {
453 is_local: true,
454 entry_id: Some(entry.id),
455 mtime: entry.mtime,
456 path: entry.path.clone(),
457 worktree: worktree.clone(),
458 is_deleted: false,
459 is_private: entry.is_private,
460 }
461 } else if let Some(entry) = snapshot.entry_for_path(old_file.path.as_ref()) {
462 worktree::File {
463 is_local: true,
464 entry_id: Some(entry.id),
465 mtime: entry.mtime,
466 path: entry.path.clone(),
467 worktree: worktree.clone(),
468 is_deleted: false,
469 is_private: entry.is_private,
470 }
471 } else {
472 worktree::File {
473 is_local: true,
474 entry_id: old_file.entry_id,
475 path: old_file.path.clone(),
476 mtime: old_file.mtime,
477 worktree: worktree.clone(),
478 is_deleted: true,
479 is_private: old_file.is_private,
480 }
481 };
482
483 if new_file == *old_file {
484 return;
485 }
486
487 if new_file.path != old_file.path {
488 self.local_image_ids_by_path.remove(&ProjectPath {
489 path: old_file.path.clone(),
490 worktree_id: old_file.worktree_id(cx),
491 });
492 self.local_image_ids_by_path.insert(
493 ProjectPath {
494 worktree_id: new_file.worktree_id(cx),
495 path: new_file.path.clone(),
496 },
497 image_id,
498 );
499 }
500
501 if new_file.entry_id != old_file.entry_id {
502 if let Some(entry_id) = old_file.entry_id {
503 self.local_image_ids_by_entry_id.remove(&entry_id);
504 }
505 if let Some(entry_id) = new_file.entry_id {
506 self.local_image_ids_by_entry_id.insert(entry_id, image_id);
507 }
508 }
509
510 image.file_updated(Arc::new(new_file), cx);
511 });
512 None
513 }
514
515 fn image_changed_file(&mut self, image: Model<ImageItem>, cx: &mut AppContext) -> Option<()> {
516 let file = worktree::File::from_dyn(Some(&image.read(cx).file))?;
517
518 let image_id = image.read(cx).id;
519 if let Some(entry_id) = file.entry_id {
520 match self.local_image_ids_by_entry_id.get(&entry_id) {
521 Some(_) => {
522 return None;
523 }
524 None => {
525 self.local_image_ids_by_entry_id.insert(entry_id, image_id);
526 }
527 }
528 };
529 self.local_image_ids_by_path.insert(
530 ProjectPath {
531 worktree_id: file.worktree_id(cx),
532 path: file.path.clone(),
533 },
534 image_id,
535 );
536
537 Some(())
538 }
539}
540
541fn create_gpui_image(content: Vec<u8>) -> anyhow::Result<Arc<gpui::Image>> {
542 let format = image::guess_format(&content)?;
543
544 Ok(Arc::new(gpui::Image {
545 id: hash(&content),
546 format: match format {
547 image::ImageFormat::Png => gpui::ImageFormat::Png,
548 image::ImageFormat::Jpeg => gpui::ImageFormat::Jpeg,
549 image::ImageFormat::WebP => gpui::ImageFormat::Webp,
550 image::ImageFormat::Gif => gpui::ImageFormat::Gif,
551 image::ImageFormat::Bmp => gpui::ImageFormat::Bmp,
552 image::ImageFormat::Tiff => gpui::ImageFormat::Tiff,
553 _ => Err(anyhow::anyhow!("Image format not supported"))?,
554 },
555 bytes: content,
556 }))
557}
558
559impl ImageStoreImpl for Model<RemoteImageStore> {
560 fn open_image(
561 &self,
562 _path: Arc<Path>,
563 _worktree: Model<Worktree>,
564 _cx: &mut ModelContext<ImageStore>,
565 ) -> Task<Result<Model<ImageItem>>> {
566 Task::ready(Err(anyhow::anyhow!(
567 "Opening images from remote is not supported"
568 )))
569 }
570
571 fn reload_images(
572 &self,
573 _images: HashSet<Model<ImageItem>>,
574 _cx: &mut ModelContext<ImageStore>,
575 ) -> Task<Result<()>> {
576 Task::ready(Err(anyhow::anyhow!(
577 "Reloading images from remote is not supported"
578 )))
579 }
580
581 fn as_local(&self) -> Option<Model<LocalImageStore>> {
582 None
583 }
584}