806 lines · 25.0 KB · Rust
RawDownload
use crate::{    worktree_store::{WorktreeStore, WorktreeStoreEvent},    Project, ProjectEntryId, ProjectItem, ProjectPath,};use anyhow::{anyhow, Context as _, Result};use collections::{hash_map, HashMap, HashSet};use futures::{channel::oneshot, StreamExt};use gpui::{    hash, prelude::*, App, AsyncApp, Context, Entity, EventEmitter, Img, Subscription, Task,    WeakEntity,};pub use image::ImageFormat;use image::{ExtendedColorType, GenericImageView, ImageReader};use language::{DiskState, File};use rpc::{AnyProtoClient, ErrorExt as _};use std::ffi::OsStr;use std::num::NonZeroU64;use std::path::Path;use std::sync::Arc;use util::ResultExt;use worktree::{LoadedBinaryFile, PathChange, Worktree}; #[derive(Clone, Copy, Debug, Hash, PartialEq, PartialOrd, Ord, Eq)]pub struct ImageId(NonZeroU64); impl std::fmt::Display for ImageId {    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {        write!(f, "{}", self.0)    }} impl From<NonZeroU64> for ImageId {    fn from(id: NonZeroU64) -> Self {        ImageId(id)    }} #[derive(Debug)]pub enum ImageItemEvent {    ReloadNeeded,    Reloaded,    FileHandleChanged,    MetadataUpdated,} impl EventEmitter<ImageItemEvent> for ImageItem {} pub enum ImageStoreEvent {    ImageAdded(Entity<ImageItem>),} impl EventEmitter<ImageStoreEvent> for ImageStore {} #[derive(Debug, Clone, Copy)]pub struct ImageMetadata {    pub width: u32,    pub height: u32,    pub file_size: u64,    pub colors: Option<ImageColorInfo>,    pub format: ImageFormat,} #[derive(Debug, Clone, Copy)]pub struct ImageColorInfo {    pub channels: u8,    pub bits_per_channel: u8,} impl ImageColorInfo {    pub fn from_color_type(color_type: impl Into<ExtendedColorType>) -> Option<Self> {        let (channels, bits_per_channel) = match color_type.into() {            ExtendedColorType::L8 => (1, 8),            ExtendedColorType::L16 => (1, 16),            ExtendedColorType::La8 => (2, 8),            ExtendedColorType::La16 => (2, 16),            ExtendedColorType::Rgb8 => (3, 8),            ExtendedColorType::Rgb16 => (3, 16),            ExtendedColorType::Rgba8 => (4, 8),            ExtendedColorType::Rgba16 => (4, 16),            ExtendedColorType::A8 => (1, 8),            ExtendedColorType::Bgr8 => (3, 8),            ExtendedColorType::Bgra8 => (4, 8),            ExtendedColorType::Cmyk8 => (4, 8),            _ => return None,        };         Some(Self {            channels,            bits_per_channel,        })    }     pub const fn bits_per_pixel(&self) -> u8 {        self.channels * self.bits_per_channel    }} pub struct ImageItem {    pub id: ImageId,    pub file: Arc<dyn File>,    pub image: Arc<gpui::Image>,    reload_task: Option<Task<()>>,    pub image_metadata: Option<ImageMetadata>,} impl ImageItem {    pub async fn load_image_metadata(        image: Entity<ImageItem>,        project: Entity<Project>,        cx: &mut AsyncApp,    ) -> Result<ImageMetadata> {        let (fs, image_path) = cx.update(|cx| {            let project_path = image.read(cx).project_path(cx);             let worktree = project                .read(cx)                .worktree_for_id(project_path.worktree_id, cx)                .ok_or_else(|| anyhow!("worktree not found"))?;            let worktree_root = worktree.read(cx).abs_path();            let image_path = image.read(cx).path();            let image_path = if image_path.is_absolute() {                image_path.to_path_buf()            } else {                worktree_root.join(image_path)            };             let fs = project.read(cx).fs().clone();             anyhow::Ok((fs, image_path))        })??;         let image_bytes = fs.load_bytes(&image_path).await?;        let image_format = image::guess_format(&image_bytes)?;         let mut image_reader = ImageReader::new(std::io::Cursor::new(image_bytes));        image_reader.set_format(image_format);        let image = image_reader.decode()?;         let (width, height) = image.dimensions();        let file_metadata = fs            .metadata(image_path.as_path())            .await?            .ok_or_else(|| anyhow!("failed to load image metadata"))?;         Ok(ImageMetadata {            width,            height,            file_size: file_metadata.len,            format: image_format,            colors: ImageColorInfo::from_color_type(image.color()),        })    }     pub fn project_path(&self, cx: &App) -> ProjectPath {        ProjectPath {            worktree_id: self.file.worktree_id(cx),            path: self.file.path().clone(),        }    }     pub fn path(&self) -> &Arc<Path> {        self.file.path()    }     fn file_updated(&mut self, new_file: Arc<dyn File>, cx: &mut Context<Self>) {        let mut file_changed = false;         let old_file = self.file.as_ref();        if new_file.path() != old_file.path() {            file_changed = true;        }         let old_state = old_file.disk_state();        let new_state = new_file.disk_state();        if old_state != new_state {            file_changed = true;            if matches!(new_state, DiskState::Present { .. }) {                cx.emit(ImageItemEvent::ReloadNeeded)            }        }         self.file = new_file;        if file_changed {            cx.emit(ImageItemEvent::FileHandleChanged);            cx.notify();        }    }     fn reload(&mut self, cx: &mut Context<Self>) -> Option<oneshot::Receiver<()>> {        let local_file = self.file.as_local()?;        let (tx, rx) = futures::channel::oneshot::channel();         let content = local_file.load_bytes(cx);        self.reload_task = Some(cx.spawn(|this, mut cx| async move {            if let Some(image) = content                .await                .context("Failed to load image content")                .and_then(create_gpui_image)                .log_err()            {                this.update(&mut cx, |this, cx| {                    this.image = image;                    cx.emit(ImageItemEvent::Reloaded);                })                .log_err();            }            _ = tx.send(());        }));        Some(rx)    }} impl ProjectItem for ImageItem {    fn try_open(        project: &Entity<Project>,        path: &ProjectPath,        cx: &mut App,    ) -> Option<Task<gpui::Result<Entity<Self>>>> {        let path = path.clone();        let project = project.clone();         let worktree_abs_path = project            .read(cx)            .worktree_for_id(path.worktree_id, cx)?            .read(cx)            .abs_path();         // Resolve the file extension from either the worktree path (if it's a single file)        // or from the project path's subpath.        let ext = worktree_abs_path            .extension()            .or_else(|| path.path.extension())            .and_then(OsStr::to_str)            .map(str::to_lowercase)            .unwrap_or_default();        let ext = ext.as_str();         // Only open the item if it's a binary image (no SVGs, etc.)        // Since we do not have a way to toggle to an editor        if Img::extensions().contains(&ext) && !ext.contains("svg") {            Some(cx.spawn(|mut cx| async move {                project                    .update(&mut cx, |project, cx| project.open_image(path, cx))?                    .await            }))        } else {            None        }    }     fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {        worktree::File::from_dyn(Some(&self.file))?.entry_id    }     fn project_path(&self, cx: &App) -> Option<ProjectPath> {        Some(self.project_path(cx).clone())    }     fn is_dirty(&self) -> bool {        false    }} trait ImageStoreImpl {    fn open_image(        &self,        path: Arc<Path>,        worktree: Entity<Worktree>,        cx: &mut Context<ImageStore>,    ) -> Task<Result<Entity<ImageItem>>>;     fn reload_images(        &self,        images: HashSet<Entity<ImageItem>>,        cx: &mut Context<ImageStore>,    ) -> Task<Result<()>>;     fn as_local(&self) -> Option<Entity<LocalImageStore>>;} struct RemoteImageStore {} struct LocalImageStore {    local_image_ids_by_path: HashMap<ProjectPath, ImageId>,    local_image_ids_by_entry_id: HashMap<ProjectEntryId, ImageId>,    image_store: WeakEntity<ImageStore>,    _subscription: Subscription,} pub struct ImageStore {    state: Box<dyn ImageStoreImpl>,    opened_images: HashMap<ImageId, WeakEntity<ImageItem>>,    worktree_store: Entity<WorktreeStore>,    #[allow(clippy::type_complexity)]    loading_images_by_path: HashMap<        ProjectPath,        postage::watch::Receiver<Option<Result<Entity<ImageItem>, Arc<anyhow::Error>>>>,    >,} impl ImageStore {    pub fn local(worktree_store: Entity<WorktreeStore>, cx: &mut Context<Self>) -> Self {        let this = cx.weak_entity();        Self {            state: Box::new(cx.new(|cx| {                let subscription = cx.subscribe(                    &worktree_store,                    |this: &mut LocalImageStore, _, event, cx| {                        if let WorktreeStoreEvent::WorktreeAdded(worktree) = event {                            this.subscribe_to_worktree(worktree, cx);                        }                    },                );                 LocalImageStore {                    local_image_ids_by_path: Default::default(),                    local_image_ids_by_entry_id: Default::default(),                    image_store: this,                    _subscription: subscription,                }            })),            opened_images: Default::default(),            loading_images_by_path: Default::default(),            worktree_store,        }    }     pub fn remote(        worktree_store: Entity<WorktreeStore>,        _upstream_client: AnyProtoClient,        _remote_id: u64,        cx: &mut Context<Self>,    ) -> Self {        Self {            state: Box::new(cx.new(|_| RemoteImageStore {})),            opened_images: Default::default(),            loading_images_by_path: Default::default(),            worktree_store,        }    }     pub fn images(&self) -> impl '_ + Iterator<Item = Entity<ImageItem>> {        self.opened_images            .values()            .filter_map(|image| image.upgrade())    }     pub fn get(&self, image_id: ImageId) -> Option<Entity<ImageItem>> {        self.opened_images            .get(&image_id)            .and_then(|image| image.upgrade())    }     pub fn get_by_path(&self, path: &ProjectPath, cx: &App) -> Option<Entity<ImageItem>> {        self.images()            .find(|image| &image.read(cx).project_path(cx) == path)    }     pub fn open_image(        &mut self,        project_path: ProjectPath,        cx: &mut Context<Self>,    ) -> Task<Result<Entity<ImageItem>>> {        let existing_image = self.get_by_path(&project_path, cx);        if let Some(existing_image) = existing_image {            return Task::ready(Ok(existing_image));        }         let Some(worktree) = self            .worktree_store            .read(cx)            .worktree_for_id(project_path.worktree_id, cx)        else {            return Task::ready(Err(anyhow::anyhow!("no such worktree")));        };         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_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<Result<Entity<ImageItem>, Arc<anyhow::Error>>>,        >,    ) -> Result<Entity<ImageItem>, Arc<anyhow::Error>> {        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(        &self,        images: HashSet<Entity<ImageItem>>,        cx: &mut Context<ImageStore>,    ) -> Task<Result<()>> {        if images.is_empty() {            return Task::ready(Ok(()));        }         self.state.reload_images(images, cx)    }     fn add_image(&mut self, image: Entity<ImageItem>, cx: &mut Context<ImageStore>) -> Result<()> {        let image_id = image.read(cx).id;         self.opened_images.insert(image_id, image.downgrade());         cx.subscribe(&image, Self::on_image_event).detach();        cx.emit(ImageStoreEvent::ImageAdded(image));        Ok(())    }     fn on_image_event(        &mut self,        image: Entity<ImageItem>,        event: &ImageItemEvent,        cx: &mut Context<Self>,    ) {        match event {            ImageItemEvent::FileHandleChanged => {                if let Some(local) = self.state.as_local() {                    local.update(cx, |local, cx| {                        local.image_changed_file(image, cx);                    })                }            }            _ => {}        }    }} impl ImageStoreImpl for Entity<LocalImageStore> {    fn open_image(        &self,        path: Arc<Path>,        worktree: Entity<Worktree>,        cx: &mut Context<ImageStore>,    ) -> Task<Result<Entity<ImageItem>>> {        let this = self.clone();         let load_file = worktree.update(cx, |worktree, cx| {            worktree.load_binary_file(path.as_ref(), cx)        });        cx.spawn(move |image_store, mut cx| async move {            let LoadedBinaryFile { file, content } = load_file.await?;            let image = create_gpui_image(content)?;             let entity = cx.new(|cx| ImageItem {                id: cx.entity_id().as_non_zero_u64().into(),                file: file.clone(),                image,                image_metadata: None,                reload_task: None,            })?;             let image_id = cx.read_entity(&entity, |model, _| model.id)?;             this.update(&mut cx, |this, cx| {                image_store.update(cx, |image_store, cx| {                    image_store.add_image(entity.clone(), cx)                })??;

Lines 1–500 of 806.