diff --git a/crates/project/src/image_store.rs b/crates/project/src/image_store.rs index fc31d75e5288feb319956cc561ebd0b1d81800f1..b3425e7fade6e5f6ed1ef56725513763f333a08d 100644 --- a/crates/project/src/image_store.rs +++ b/crates/project/src/image_store.rs @@ -1,14 +1,16 @@ -use crate::worktree_store::{WorktreeStore, WorktreeStoreEvent}; -use crate::{Project, ProjectEntryId, ProjectPath}; +use crate::{ + worktree_store::{WorktreeStore, WorktreeStoreEvent}, + Project, ProjectEntryId, ProjectPath, +}; use anyhow::{Context as _, Result}; -use collections::{HashMap, HashSet}; -use futures::channel::oneshot; +use collections::{hash_map, HashMap, HashSet}; +use futures::{channel::oneshot, StreamExt}; use gpui::{ hash, prelude::*, AppContext, EventEmitter, Img, Model, ModelContext, Subscription, Task, WeakModel, }; use language::File; -use rpc::AnyProtoClient; +use rpc::{AnyProtoClient, ErrorExt as _}; use std::ffi::OsStr; use std::num::NonZeroU64; use std::path::Path; @@ -180,6 +182,11 @@ pub struct ImageStore { state: Box, opened_images: HashMap>, worktree_store: Model, + #[allow(clippy::type_complexity)] + loading_images_by_path: HashMap< + ProjectPath, + postage::watch::Receiver, Arc>>>, + >, } impl ImageStore { @@ -204,6 +211,7 @@ impl ImageStore { } })), opened_images: Default::default(), + loading_images_by_path: Default::default(), worktree_store, } } @@ -217,6 +225,7 @@ impl ImageStore { Self { state: Box::new(cx.new_model(|_| RemoteImageStore {})), opened_images: Default::default(), + loading_images_by_path: Default::default(), worktree_store, } } @@ -256,8 +265,57 @@ impl ImageStore { return Task::ready(Err(anyhow::anyhow!("no such worktree"))); }; - self.state - .open_image(project_path.path.clone(), worktree, cx) + let loading_watch = match self.loading_images_by_path.entry(project_path.clone()) { + // If the given path is already being loaded, then wait for that existing + // task to complete and return the same image. + hash_map::Entry::Occupied(e) => e.get().clone(), + + // Otherwise, record the fact that this path is now being loaded. + hash_map::Entry::Vacant(entry) => { + let (mut tx, rx) = postage::watch::channel(); + entry.insert(rx.clone()); + + let project_path = project_path.clone(); + let load_image = self + .state + .open_image(project_path.path.clone(), worktree, cx); + + cx.spawn(move |this, mut cx| async move { + let load_result = load_image.await; + *tx.borrow_mut() = Some(this.update(&mut cx, |this, _cx| { + // Record the fact that the image is no longer loading. + this.loading_images_by_path.remove(&project_path); + let image = load_result.map_err(Arc::new)?; + Ok(image) + })?); + anyhow::Ok(()) + }) + .detach(); + rx + } + }; + + cx.background_executor().spawn(async move { + Self::wait_for_loading_image(loading_watch) + .await + .map_err(|e| e.cloned()) + }) + } + + pub async fn wait_for_loading_image( + mut receiver: postage::watch::Receiver< + Option, Arc>>, + >, + ) -> Result, Arc> { + loop { + if let Some(result) = receiver.borrow().as_ref() { + match result { + Ok(image) => return Ok(image.to_owned()), + Err(e) => return Err(e.to_owned()), + } + } + receiver.next().await; + } } pub fn reload_images( @@ -582,3 +640,68 @@ impl ImageStoreImpl for Model { None } } + +#[cfg(test)] +mod tests { + use super::*; + use fs::FakeFs; + use gpui::TestAppContext; + use serde_json::json; + use settings::SettingsStore; + use std::path::PathBuf; + + pub fn init_test(cx: &mut TestAppContext) { + if std::env::var("RUST_LOG").is_ok() { + env_logger::try_init().ok(); + } + + cx.update(|cx| { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + language::init(cx); + Project::init_settings(cx); + }); + } + + #[gpui::test] + async fn test_image_not_loaded_twice(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.executor()); + + fs.insert_tree("/root", json!({})).await; + // Create a png file that consists of a single white pixel + fs.insert_file( + "/root/image_1.png", + vec![ + 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, + 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, + 0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41, 0x54, 0x78, + 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4, 0x00, + 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82, + ], + ) + .await; + + let project = Project::test(fs, ["/root".as_ref()], cx).await; + + let worktree_id = + cx.update(|cx| project.read(cx).worktrees(cx).next().unwrap().read(cx).id()); + + let project_path = ProjectPath { + worktree_id, + path: PathBuf::from("image_1.png").into(), + }; + + let (task1, task2) = project.update(cx, |project, cx| { + ( + project.open_image(project_path.clone(), cx), + project.open_image(project_path.clone(), cx), + ) + }); + + let image1 = task1.await.unwrap(); + let image2 = task2.await.unwrap(); + + assert_eq!(image1, image2); + } +}