Enable reload of images in image viewer (#20374)
Closes #11529 Release Notes: - Fixed an issue where the image preview would not update when the underlying file changed --------- Co-authored-by: Bennet <bennet@zed.dev>
This commit is contained in:
parent
f3320998a8
commit
0dbda71423
10 changed files with 840 additions and 109 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -5978,6 +5978,7 @@ dependencies = [
|
|||
"settings",
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
|
@ -9080,6 +9081,7 @@ dependencies = [
|
|||
"globset",
|
||||
"gpui",
|
||||
"http_client",
|
||||
"image",
|
||||
"itertools 0.13.0",
|
||||
"language",
|
||||
"log",
|
||||
|
|
|
@ -1272,5 +1272,9 @@ mod tests {
|
|||
fn load(&self, _: &AppContext) -> Task<Result<String>> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn load_bytes(&self, _cx: &AppContext) -> Task<Result<Vec<u8>>> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -92,6 +92,12 @@ impl From<Arc<RenderImage>> for ImageSource {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<Arc<Image>> for ImageSource {
|
||||
fn from(value: Arc<Image>) -> Self {
|
||||
Self::Image(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// An image element.
|
||||
pub struct Img {
|
||||
interactivity: Interactivity,
|
||||
|
|
|
@ -21,4 +21,5 @@ project.workspace = true
|
|||
settings.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use gpui::{
|
||||
canvas, div, fill, img, opaque_grey, point, size, AnyElement, AppContext, Bounds, Context,
|
||||
EventEmitter, FocusHandle, FocusableView, Img, InteractiveElement, IntoElement, Model,
|
||||
ObjectFit, ParentElement, Render, Styled, Task, View, ViewContext, VisualContext, WeakView,
|
||||
WindowContext,
|
||||
canvas, div, fill, img, opaque_grey, point, size, AnyElement, AppContext, Bounds, EventEmitter,
|
||||
FocusHandle, FocusableView, InteractiveElement, IntoElement, Model, ObjectFit, ParentElement,
|
||||
Render, Styled, Task, View, ViewContext, VisualContext, WeakView, WindowContext,
|
||||
};
|
||||
use persistence::IMAGE_VIEWER;
|
||||
use theme::Theme;
|
||||
use ui::prelude::*;
|
||||
|
||||
use file_icons::FileIcons;
|
||||
use project::{Project, ProjectEntryId, ProjectPath};
|
||||
use project::{image_store::ImageItemEvent, ImageItem, Project, ProjectPath};
|
||||
use settings::Settings;
|
||||
use std::{ffi::OsStr, path::PathBuf};
|
||||
use util::paths::PathExt;
|
||||
use workspace::{
|
||||
item::{BreadcrumbText, Item, ProjectItem, SerializableItem, TabContentParams},
|
||||
ItemId, ItemSettings, Pane, ToolbarItemLocation, Workspace, WorkspaceId,
|
||||
|
@ -20,86 +21,80 @@ use workspace::{
|
|||
|
||||
const IMAGE_VIEWER_KIND: &str = "ImageView";
|
||||
|
||||
pub struct ImageItem {
|
||||
id: ProjectEntryId,
|
||||
path: PathBuf,
|
||||
project_path: ProjectPath,
|
||||
project: Model<Project>,
|
||||
}
|
||||
|
||||
impl project::Item for ImageItem {
|
||||
fn try_open(
|
||||
project: &Model<Project>,
|
||||
path: &ProjectPath,
|
||||
cx: &mut AppContext,
|
||||
) -> Option<Task<gpui::Result<Model<Self>>>> {
|
||||
let path = path.clone();
|
||||
let project = project.clone();
|
||||
|
||||
let ext = 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 {
|
||||
let abs_path = project
|
||||
.read_with(&cx, |project, cx| project.absolute_path(&path, cx))?
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to find the absolute path"))?;
|
||||
|
||||
let id = project
|
||||
.update(&mut cx, |project, cx| project.entry_for_path(&path, cx))?
|
||||
.context("Entry not found")?
|
||||
.id;
|
||||
|
||||
cx.new_model(|_| ImageItem {
|
||||
project,
|
||||
path: abs_path,
|
||||
project_path: path,
|
||||
id,
|
||||
})
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
|
||||
Some(self.id)
|
||||
}
|
||||
|
||||
fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
|
||||
Some(self.project_path.clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ImageView {
|
||||
image: Model<ImageItem>,
|
||||
image_item: Model<ImageItem>,
|
||||
project: Model<Project>,
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
impl ImageView {
|
||||
pub fn new(
|
||||
image_item: Model<ImageItem>,
|
||||
project: Model<Project>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
cx.subscribe(&image_item, Self::on_image_event).detach();
|
||||
Self {
|
||||
image_item,
|
||||
project,
|
||||
focus_handle: cx.focus_handle(),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_image_event(
|
||||
&mut self,
|
||||
_: Model<ImageItem>,
|
||||
event: &ImageItemEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
ImageItemEvent::FileHandleChanged | ImageItemEvent::Reloaded => {
|
||||
cx.emit(ImageViewEvent::TitleChanged);
|
||||
cx.notify();
|
||||
}
|
||||
ImageItemEvent::ReloadNeeded => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ImageViewEvent {
|
||||
TitleChanged,
|
||||
}
|
||||
|
||||
impl EventEmitter<ImageViewEvent> for ImageView {}
|
||||
|
||||
impl Item for ImageView {
|
||||
type Event = ();
|
||||
type Event = ImageViewEvent;
|
||||
|
||||
fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
|
||||
match event {
|
||||
ImageViewEvent::TitleChanged => {
|
||||
f(workspace::item::ItemEvent::UpdateTab);
|
||||
f(workspace::item::ItemEvent::UpdateBreadcrumbs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn for_each_project_item(
|
||||
&self,
|
||||
cx: &AppContext,
|
||||
f: &mut dyn FnMut(gpui::EntityId, &dyn project::Item),
|
||||
) {
|
||||
f(self.image.entity_id(), self.image.read(cx))
|
||||
f(self.image_item.entity_id(), self.image_item.read(cx))
|
||||
}
|
||||
|
||||
fn is_singleton(&self, _cx: &AppContext) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString> {
|
||||
let abs_path = self.image_item.read(cx).file.as_local()?.abs_path(cx);
|
||||
let file_path = abs_path.compact().to_string_lossy().to_string();
|
||||
Some(file_path.into())
|
||||
}
|
||||
|
||||
fn tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement {
|
||||
let path = &self.image.read(cx).path;
|
||||
let path = self.image_item.read(cx).file.path();
|
||||
let title = path
|
||||
.file_name()
|
||||
.unwrap_or_else(|| path.as_os_str())
|
||||
|
@ -113,10 +108,10 @@ impl Item for ImageView {
|
|||
}
|
||||
|
||||
fn tab_icon(&self, cx: &WindowContext) -> Option<Icon> {
|
||||
let path = &self.image.read(cx).path;
|
||||
let path = self.image_item.read(cx).path();
|
||||
ItemSettings::get_global(cx)
|
||||
.file_icons
|
||||
.then(|| FileIcons::get_icon(path.as_path(), cx))
|
||||
.then(|| FileIcons::get_icon(path, cx))
|
||||
.flatten()
|
||||
.map(Icon::from_path)
|
||||
}
|
||||
|
@ -126,7 +121,7 @@ impl Item for ImageView {
|
|||
}
|
||||
|
||||
fn breadcrumbs(&self, _theme: &Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
|
||||
let text = breadcrumbs_text_for_image(self.image.read(cx), cx);
|
||||
let text = breadcrumbs_text_for_image(self.project.read(cx), self.image_item.read(cx), cx);
|
||||
Some(vec![BreadcrumbText {
|
||||
text,
|
||||
highlights: None,
|
||||
|
@ -143,22 +138,21 @@ impl Item for ImageView {
|
|||
Self: Sized,
|
||||
{
|
||||
Some(cx.new_view(|cx| Self {
|
||||
image: self.image.clone(),
|
||||
image_item: self.image_item.clone(),
|
||||
project: self.project.clone(),
|
||||
focus_handle: cx.focus_handle(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
fn breadcrumbs_text_for_image(image: &ImageItem, cx: &AppContext) -> String {
|
||||
let path = &image.project_path.path;
|
||||
let project = image.project.read(cx);
|
||||
|
||||
fn breadcrumbs_text_for_image(project: &Project, image: &ImageItem, cx: &AppContext) -> String {
|
||||
let path = image.path();
|
||||
if project.visible_worktrees(cx).count() <= 1 {
|
||||
return path.to_string_lossy().to_string();
|
||||
}
|
||||
|
||||
project
|
||||
.worktree_for_entry(image.id, cx)
|
||||
.worktree_for_id(image.project_path(cx).worktree_id, cx)
|
||||
.map(|worktree| {
|
||||
PathBuf::from(worktree.read(cx).root_name())
|
||||
.join(path)
|
||||
|
@ -198,26 +192,11 @@ impl SerializableItem for ImageView {
|
|||
path: relative_path.into(),
|
||||
};
|
||||
|
||||
let id = project
|
||||
.update(&mut cx, |project, cx| {
|
||||
project.entry_for_path(&project_path, cx)
|
||||
})?
|
||||
.context("No entry found")?
|
||||
.id;
|
||||
let image_item = project
|
||||
.update(&mut cx, |project, cx| project.open_image(project_path, cx))?
|
||||
.await?;
|
||||
|
||||
cx.update(|cx| {
|
||||
let image = cx.new_model(|_| ImageItem {
|
||||
id,
|
||||
path: image_path,
|
||||
project_path,
|
||||
project,
|
||||
});
|
||||
|
||||
Ok(cx.new_view(|cx| ImageView {
|
||||
image,
|
||||
focus_handle: cx.focus_handle(),
|
||||
}))
|
||||
})?
|
||||
cx.update(|cx| Ok(cx.new_view(|cx| ImageView::new(image_item, project, cx))))?
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -237,9 +216,9 @@ impl SerializableItem for ImageView {
|
|||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<Task<gpui::Result<()>>> {
|
||||
let workspace_id = workspace.database_id()?;
|
||||
let image_path = self.image_item.read(cx).file.as_local()?.abs_path(cx);
|
||||
|
||||
Some(cx.background_executor().spawn({
|
||||
let image_path = self.image.read(cx).path.clone();
|
||||
async move {
|
||||
IMAGE_VIEWER
|
||||
.save_image_path(item_id, workspace_id, image_path)
|
||||
|
@ -262,7 +241,7 @@ impl FocusableView for ImageView {
|
|||
|
||||
impl Render for ImageView {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let image_path = self.image.read(cx).path.clone();
|
||||
let image = self.image_item.read(cx).image.clone();
|
||||
let checkered_background = |bounds: Bounds<Pixels>, _, cx: &mut WindowContext| {
|
||||
let square_size = 32.0;
|
||||
|
||||
|
@ -319,7 +298,7 @@ impl Render for ImageView {
|
|||
// TODO: In browser based Tailwind & Flex this would be h-screen and we'd use w-full
|
||||
.h_full()
|
||||
.child(
|
||||
img(image_path)
|
||||
img(image)
|
||||
.object_fit(ObjectFit::ScaleDown)
|
||||
.max_w_full()
|
||||
.max_h_full(),
|
||||
|
@ -332,17 +311,14 @@ impl ProjectItem for ImageView {
|
|||
type Item = ImageItem;
|
||||
|
||||
fn for_project_item(
|
||||
_project: Model<Project>,
|
||||
project: Model<Project>,
|
||||
item: Model<Self::Item>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Self {
|
||||
image: item,
|
||||
focus_handle: cx.focus_handle(),
|
||||
}
|
||||
Self::new(item, project, cx)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -413,9 +413,12 @@ pub trait LocalFile: File {
|
|||
/// Returns the absolute path of this file
|
||||
fn abs_path(&self, cx: &AppContext) -> PathBuf;
|
||||
|
||||
/// Loads the file's contents from disk.
|
||||
/// Loads the file contents from disk and returns them as a UTF-8 encoded string.
|
||||
fn load(&self, cx: &AppContext) -> Task<Result<String>>;
|
||||
|
||||
/// Loads the file's contents from disk.
|
||||
fn load_bytes(&self, cx: &AppContext) -> Task<Result<Vec<u8>>>;
|
||||
|
||||
/// Returns true if the file should not be shared with collaborators.
|
||||
fn is_private(&self, _: &AppContext) -> bool {
|
||||
false
|
||||
|
|
|
@ -42,6 +42,7 @@ language.workspace = true
|
|||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
node_runtime.workspace = true
|
||||
image.workspace = true
|
||||
parking_lot.workspace = true
|
||||
pathdiff.workspace = true
|
||||
paths.workspace = true
|
||||
|
|
584
crates/project/src/image_store.rs
Normal file
584
crates/project/src/image_store.rs
Normal file
|
@ -0,0 +1,584 @@
|
|||
use crate::worktree_store::{WorktreeStore, WorktreeStoreEvent};
|
||||
use crate::{Project, ProjectEntryId, ProjectPath};
|
||||
use anyhow::{Context as _, Result};
|
||||
use collections::{HashMap, HashSet};
|
||||
use futures::channel::oneshot;
|
||||
use gpui::{
|
||||
hash, prelude::*, AppContext, EventEmitter, Img, Model, ModelContext, Subscription, Task,
|
||||
WeakModel,
|
||||
};
|
||||
use language::File;
|
||||
use rpc::AnyProtoClient;
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ImageItemEvent {
|
||||
ReloadNeeded,
|
||||
Reloaded,
|
||||
FileHandleChanged,
|
||||
}
|
||||
|
||||
impl EventEmitter<ImageItemEvent> for ImageItem {}
|
||||
|
||||
pub enum ImageStoreEvent {
|
||||
ImageAdded(Model<ImageItem>),
|
||||
}
|
||||
|
||||
impl EventEmitter<ImageStoreEvent> for ImageStore {}
|
||||
|
||||
pub struct ImageItem {
|
||||
pub id: ImageId,
|
||||
pub file: Arc<dyn File>,
|
||||
pub image: Arc<gpui::Image>,
|
||||
reload_task: Option<Task<()>>,
|
||||
}
|
||||
|
||||
impl ImageItem {
|
||||
pub fn project_path(&self, cx: &AppContext) -> 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 ModelContext<Self>) {
|
||||
let mut file_changed = false;
|
||||
|
||||
let old_file = self.file.as_ref();
|
||||
if new_file.path() != old_file.path() {
|
||||
file_changed = true;
|
||||
}
|
||||
|
||||
if !new_file.is_deleted() {
|
||||
let new_mtime = new_file.mtime();
|
||||
if new_mtime != old_file.mtime() {
|
||||
file_changed = true;
|
||||
cx.emit(ImageItemEvent::ReloadNeeded);
|
||||
}
|
||||
}
|
||||
|
||||
self.file = new_file;
|
||||
if file_changed {
|
||||
cx.emit(ImageItemEvent::FileHandleChanged);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn reload(&mut self, cx: &mut ModelContext<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 crate::Item for ImageItem {
|
||||
fn try_open(
|
||||
project: &Model<Project>,
|
||||
path: &ProjectPath,
|
||||
cx: &mut AppContext,
|
||||
) -> Option<Task<gpui::Result<Model<Self>>>> {
|
||||
let path = path.clone();
|
||||
let project = project.clone();
|
||||
|
||||
let ext = 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, _: &AppContext) -> Option<ProjectEntryId> {
|
||||
worktree::File::from_dyn(Some(&self.file))?.entry_id
|
||||
}
|
||||
|
||||
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
|
||||
Some(self.project_path(cx).clone())
|
||||
}
|
||||
}
|
||||
|
||||
trait ImageStoreImpl {
|
||||
fn open_image(
|
||||
&self,
|
||||
path: Arc<Path>,
|
||||
worktree: Model<Worktree>,
|
||||
cx: &mut ModelContext<ImageStore>,
|
||||
) -> Task<Result<Model<ImageItem>>>;
|
||||
|
||||
fn reload_images(
|
||||
&self,
|
||||
images: HashSet<Model<ImageItem>>,
|
||||
cx: &mut ModelContext<ImageStore>,
|
||||
) -> Task<Result<()>>;
|
||||
|
||||
fn as_local(&self) -> Option<Model<LocalImageStore>>;
|
||||
}
|
||||
|
||||
struct RemoteImageStore {}
|
||||
|
||||
struct LocalImageStore {
|
||||
local_image_ids_by_path: HashMap<ProjectPath, ImageId>,
|
||||
local_image_ids_by_entry_id: HashMap<ProjectEntryId, ImageId>,
|
||||
image_store: WeakModel<ImageStore>,
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
pub struct ImageStore {
|
||||
state: Box<dyn ImageStoreImpl>,
|
||||
opened_images: HashMap<ImageId, WeakModel<ImageItem>>,
|
||||
worktree_store: Model<WorktreeStore>,
|
||||
}
|
||||
|
||||
impl ImageStore {
|
||||
pub fn local(worktree_store: Model<WorktreeStore>, cx: &mut ModelContext<Self>) -> Self {
|
||||
let this = cx.weak_model();
|
||||
Self {
|
||||
state: Box::new(cx.new_model(|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(),
|
||||
worktree_store,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remote(
|
||||
worktree_store: Model<WorktreeStore>,
|
||||
_upstream_client: AnyProtoClient,
|
||||
_remote_id: u64,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
Self {
|
||||
state: Box::new(cx.new_model(|_| RemoteImageStore {})),
|
||||
opened_images: Default::default(),
|
||||
worktree_store,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn images(&self) -> impl '_ + Iterator<Item = Model<ImageItem>> {
|
||||
self.opened_images
|
||||
.values()
|
||||
.filter_map(|image| image.upgrade())
|
||||
}
|
||||
|
||||
pub fn get(&self, image_id: ImageId) -> Option<Model<ImageItem>> {
|
||||
self.opened_images
|
||||
.get(&image_id)
|
||||
.and_then(|image| image.upgrade())
|
||||
}
|
||||
|
||||
pub fn get_by_path(&self, path: &ProjectPath, cx: &AppContext) -> Option<Model<ImageItem>> {
|
||||
self.images()
|
||||
.find(|image| &image.read(cx).project_path(cx) == path)
|
||||
}
|
||||
|
||||
pub fn open_image(
|
||||
&mut self,
|
||||
project_path: ProjectPath,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Model<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")));
|
||||
};
|
||||
|
||||
self.state
|
||||
.open_image(project_path.path.clone(), worktree, cx)
|
||||
}
|
||||
|
||||
pub fn reload_images(
|
||||
&self,
|
||||
images: HashSet<Model<ImageItem>>,
|
||||
cx: &mut ModelContext<ImageStore>,
|
||||
) -> Task<Result<()>> {
|
||||
if images.is_empty() {
|
||||
return Task::ready(Ok(()));
|
||||
}
|
||||
|
||||
self.state.reload_images(images, cx)
|
||||
}
|
||||
|
||||
fn add_image(
|
||||
&mut self,
|
||||
image: Model<ImageItem>,
|
||||
cx: &mut ModelContext<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: Model<ImageItem>,
|
||||
event: &ImageItemEvent,
|
||||
cx: &mut ModelContext<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 Model<LocalImageStore> {
|
||||
fn open_image(
|
||||
&self,
|
||||
path: Arc<Path>,
|
||||
worktree: Model<Worktree>,
|
||||
cx: &mut ModelContext<ImageStore>,
|
||||
) -> Task<Result<Model<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 model = cx.new_model(|cx| ImageItem {
|
||||
id: cx.entity_id().as_non_zero_u64().into(),
|
||||
file: file.clone(),
|
||||
image,
|
||||
reload_task: None,
|
||||
})?;
|
||||
|
||||
let image_id = cx.read_model(&model, |model, _| model.id)?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
image_store.update(cx, |image_store, cx| {
|
||||
image_store.add_image(model.clone(), cx)
|
||||
})??;
|
||||
this.local_image_ids_by_path.insert(
|
||||
ProjectPath {
|
||||
worktree_id: file.worktree_id(cx),
|
||||
path: file.path.clone(),
|
||||
},
|
||||
image_id,
|
||||
);
|
||||
|
||||
if let Some(entry_id) = file.entry_id {
|
||||
this.local_image_ids_by_entry_id.insert(entry_id, image_id);
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})??;
|
||||
|
||||
Ok(model)
|
||||
})
|
||||
}
|
||||
|
||||
fn reload_images(
|
||||
&self,
|
||||
images: HashSet<Model<ImageItem>>,
|
||||
cx: &mut ModelContext<ImageStore>,
|
||||
) -> Task<Result<()>> {
|
||||
cx.spawn(move |_, mut cx| async move {
|
||||
for image in images {
|
||||
if let Some(rec) = image.update(&mut cx, |image, cx| image.reload(cx))? {
|
||||
rec.await?
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn as_local(&self) -> Option<Model<LocalImageStore>> {
|
||||
Some(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl LocalImageStore {
|
||||
fn subscribe_to_worktree(&mut self, worktree: &Model<Worktree>, cx: &mut ModelContext<Self>) {
|
||||
cx.subscribe(worktree, |this, worktree, event, cx| {
|
||||
if worktree.read(cx).is_local() {
|
||||
match event {
|
||||
worktree::Event::UpdatedEntries(changes) => {
|
||||
this.local_worktree_entries_changed(&worktree, changes, cx);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn local_worktree_entries_changed(
|
||||
&mut self,
|
||||
worktree_handle: &Model<Worktree>,
|
||||
changes: &[(Arc<Path>, ProjectEntryId, PathChange)],
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let snapshot = worktree_handle.read(cx).snapshot();
|
||||
for (path, entry_id, _) in changes {
|
||||
self.local_worktree_entry_changed(*entry_id, path, worktree_handle, &snapshot, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn local_worktree_entry_changed(
|
||||
&mut self,
|
||||
entry_id: ProjectEntryId,
|
||||
path: &Arc<Path>,
|
||||
worktree: &Model<worktree::Worktree>,
|
||||
snapshot: &worktree::Snapshot,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Option<()> {
|
||||
let project_path = ProjectPath {
|
||||
worktree_id: snapshot.id(),
|
||||
path: path.clone(),
|
||||
};
|
||||
let image_id = match self.local_image_ids_by_entry_id.get(&entry_id) {
|
||||
Some(&image_id) => image_id,
|
||||
None => self.local_image_ids_by_path.get(&project_path).copied()?,
|
||||
};
|
||||
|
||||
let image = self
|
||||
.image_store
|
||||
.update(cx, |image_store, _| {
|
||||
if let Some(image) = image_store.get(image_id) {
|
||||
Some(image)
|
||||
} else {
|
||||
image_store.opened_images.remove(&image_id);
|
||||
None
|
||||
}
|
||||
})
|
||||
.ok()
|
||||
.flatten();
|
||||
let image = if let Some(image) = image {
|
||||
image
|
||||
} else {
|
||||
self.local_image_ids_by_path.remove(&project_path);
|
||||
self.local_image_ids_by_entry_id.remove(&entry_id);
|
||||
return None;
|
||||
};
|
||||
|
||||
image.update(cx, |image, cx| {
|
||||
let Some(old_file) = worktree::File::from_dyn(Some(&image.file)) else {
|
||||
return;
|
||||
};
|
||||
if old_file.worktree != *worktree {
|
||||
return;
|
||||
}
|
||||
|
||||
let new_file = if let Some(entry) = old_file
|
||||
.entry_id
|
||||
.and_then(|entry_id| snapshot.entry_for_id(entry_id))
|
||||
{
|
||||
worktree::File {
|
||||
is_local: true,
|
||||
entry_id: Some(entry.id),
|
||||
mtime: entry.mtime,
|
||||
path: entry.path.clone(),
|
||||
worktree: worktree.clone(),
|
||||
is_deleted: false,
|
||||
is_private: entry.is_private,
|
||||
}
|
||||
} else if let Some(entry) = snapshot.entry_for_path(old_file.path.as_ref()) {
|
||||
worktree::File {
|
||||
is_local: true,
|
||||
entry_id: Some(entry.id),
|
||||
mtime: entry.mtime,
|
||||
path: entry.path.clone(),
|
||||
worktree: worktree.clone(),
|
||||
is_deleted: false,
|
||||
is_private: entry.is_private,
|
||||
}
|
||||
} else {
|
||||
worktree::File {
|
||||
is_local: true,
|
||||
entry_id: old_file.entry_id,
|
||||
path: old_file.path.clone(),
|
||||
mtime: old_file.mtime,
|
||||
worktree: worktree.clone(),
|
||||
is_deleted: true,
|
||||
is_private: old_file.is_private,
|
||||
}
|
||||
};
|
||||
|
||||
if new_file == *old_file {
|
||||
return;
|
||||
}
|
||||
|
||||
if new_file.path != old_file.path {
|
||||
self.local_image_ids_by_path.remove(&ProjectPath {
|
||||
path: old_file.path.clone(),
|
||||
worktree_id: old_file.worktree_id(cx),
|
||||
});
|
||||
self.local_image_ids_by_path.insert(
|
||||
ProjectPath {
|
||||
worktree_id: new_file.worktree_id(cx),
|
||||
path: new_file.path.clone(),
|
||||
},
|
||||
image_id,
|
||||
);
|
||||
}
|
||||
|
||||
if new_file.entry_id != old_file.entry_id {
|
||||
if let Some(entry_id) = old_file.entry_id {
|
||||
self.local_image_ids_by_entry_id.remove(&entry_id);
|
||||
}
|
||||
if let Some(entry_id) = new_file.entry_id {
|
||||
self.local_image_ids_by_entry_id.insert(entry_id, image_id);
|
||||
}
|
||||
}
|
||||
|
||||
image.file_updated(Arc::new(new_file), cx);
|
||||
});
|
||||
None
|
||||
}
|
||||
|
||||
fn image_changed_file(&mut self, image: Model<ImageItem>, cx: &mut AppContext) -> Option<()> {
|
||||
let file = worktree::File::from_dyn(Some(&image.read(cx).file))?;
|
||||
|
||||
let image_id = image.read(cx).id;
|
||||
if let Some(entry_id) = file.entry_id {
|
||||
match self.local_image_ids_by_entry_id.get(&entry_id) {
|
||||
Some(_) => {
|
||||
return None;
|
||||
}
|
||||
None => {
|
||||
self.local_image_ids_by_entry_id.insert(entry_id, image_id);
|
||||
}
|
||||
}
|
||||
};
|
||||
self.local_image_ids_by_path.insert(
|
||||
ProjectPath {
|
||||
worktree_id: file.worktree_id(cx),
|
||||
path: file.path.clone(),
|
||||
},
|
||||
image_id,
|
||||
);
|
||||
|
||||
Some(())
|
||||
}
|
||||
}
|
||||
|
||||
fn create_gpui_image(content: Vec<u8>) -> anyhow::Result<Arc<gpui::Image>> {
|
||||
let format = image::guess_format(&content)?;
|
||||
|
||||
Ok(Arc::new(gpui::Image {
|
||||
id: hash(&content),
|
||||
format: match format {
|
||||
image::ImageFormat::Png => gpui::ImageFormat::Png,
|
||||
image::ImageFormat::Jpeg => gpui::ImageFormat::Jpeg,
|
||||
image::ImageFormat::WebP => gpui::ImageFormat::Webp,
|
||||
image::ImageFormat::Gif => gpui::ImageFormat::Gif,
|
||||
image::ImageFormat::Bmp => gpui::ImageFormat::Bmp,
|
||||
image::ImageFormat::Tiff => gpui::ImageFormat::Tiff,
|
||||
_ => Err(anyhow::anyhow!("Image format not supported"))?,
|
||||
},
|
||||
bytes: content,
|
||||
}))
|
||||
}
|
||||
|
||||
impl ImageStoreImpl for Model<RemoteImageStore> {
|
||||
fn open_image(
|
||||
&self,
|
||||
_path: Arc<Path>,
|
||||
_worktree: Model<Worktree>,
|
||||
_cx: &mut ModelContext<ImageStore>,
|
||||
) -> Task<Result<Model<ImageItem>>> {
|
||||
Task::ready(Err(anyhow::anyhow!(
|
||||
"Opening images from remote is not supported"
|
||||
)))
|
||||
}
|
||||
|
||||
fn reload_images(
|
||||
&self,
|
||||
_images: HashSet<Model<ImageItem>>,
|
||||
_cx: &mut ModelContext<ImageStore>,
|
||||
) -> Task<Result<()>> {
|
||||
Task::ready(Err(anyhow::anyhow!(
|
||||
"Reloading images from remote is not supported"
|
||||
)))
|
||||
}
|
||||
|
||||
fn as_local(&self) -> Option<Model<LocalImageStore>> {
|
||||
None
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ pub mod buffer_store;
|
|||
mod color_extractor;
|
||||
pub mod connection_manager;
|
||||
pub mod debounced_delay;
|
||||
pub mod image_store;
|
||||
pub mod lsp_command;
|
||||
pub mod lsp_ext_command;
|
||||
pub mod lsp_store;
|
||||
|
@ -35,6 +36,8 @@ use futures::{
|
|||
future::try_join_all,
|
||||
StreamExt,
|
||||
};
|
||||
pub use image_store::{ImageItem, ImageStore};
|
||||
use image_store::{ImageItemEvent, ImageStoreEvent};
|
||||
|
||||
use git::{blame::Blame, repository::GitRepository};
|
||||
use gpui::{
|
||||
|
@ -146,6 +149,7 @@ pub struct Project {
|
|||
client_subscriptions: Vec<client::Subscription>,
|
||||
worktree_store: Model<WorktreeStore>,
|
||||
buffer_store: Model<BufferStore>,
|
||||
image_store: Model<ImageStore>,
|
||||
lsp_store: Model<LspStore>,
|
||||
_subscriptions: Vec<gpui::Subscription>,
|
||||
buffers_needing_diff: HashSet<WeakModel<Buffer>>,
|
||||
|
@ -205,10 +209,11 @@ enum BufferOrderedMessage {
|
|||
|
||||
#[derive(Debug)]
|
||||
enum ProjectClientState {
|
||||
/// Single-player mode.
|
||||
Local,
|
||||
Shared {
|
||||
remote_id: u64,
|
||||
},
|
||||
/// Multi-player mode but still a local project.
|
||||
Shared { remote_id: u64 },
|
||||
/// Multi-player mode but working on a remote project.
|
||||
Remote {
|
||||
sharing_has_stopped: bool,
|
||||
capability: Capability,
|
||||
|
@ -606,6 +611,10 @@ impl Project {
|
|||
cx.subscribe(&buffer_store, Self::on_buffer_store_event)
|
||||
.detach();
|
||||
|
||||
let image_store = cx.new_model(|cx| ImageStore::local(worktree_store.clone(), cx));
|
||||
cx.subscribe(&image_store, Self::on_image_store_event)
|
||||
.detach();
|
||||
|
||||
let prettier_store = cx.new_model(|cx| {
|
||||
PrettierStore::new(
|
||||
node.clone(),
|
||||
|
@ -666,6 +675,7 @@ impl Project {
|
|||
collaborators: Default::default(),
|
||||
worktree_store,
|
||||
buffer_store,
|
||||
image_store,
|
||||
lsp_store,
|
||||
join_project_response_message_id: 0,
|
||||
client_state: ProjectClientState::Local,
|
||||
|
@ -729,6 +739,14 @@ impl Project {
|
|||
cx,
|
||||
)
|
||||
});
|
||||
let image_store = cx.new_model(|cx| {
|
||||
ImageStore::remote(
|
||||
worktree_store.clone(),
|
||||
ssh.read(cx).proto_client(),
|
||||
SSH_PROJECT_ID,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.subscribe(&buffer_store, Self::on_buffer_store_event)
|
||||
.detach();
|
||||
|
||||
|
@ -774,6 +792,7 @@ impl Project {
|
|||
collaborators: Default::default(),
|
||||
worktree_store,
|
||||
buffer_store,
|
||||
image_store,
|
||||
lsp_store,
|
||||
join_project_response_message_id: 0,
|
||||
client_state: ProjectClientState::Local,
|
||||
|
@ -920,6 +939,9 @@ impl Project {
|
|||
let buffer_store = cx.new_model(|cx| {
|
||||
BufferStore::remote(worktree_store.clone(), client.clone().into(), remote_id, cx)
|
||||
})?;
|
||||
let image_store = cx.new_model(|cx| {
|
||||
ImageStore::remote(worktree_store.clone(), client.clone().into(), remote_id, cx)
|
||||
})?;
|
||||
|
||||
let lsp_store = cx.new_model(|cx| {
|
||||
let mut lsp_store = LspStore::new_remote(
|
||||
|
@ -982,6 +1004,7 @@ impl Project {
|
|||
let mut this = Self {
|
||||
buffer_ordered_messages_tx: tx,
|
||||
buffer_store: buffer_store.clone(),
|
||||
image_store,
|
||||
worktree_store: worktree_store.clone(),
|
||||
lsp_store: lsp_store.clone(),
|
||||
active_entry: None,
|
||||
|
@ -1783,7 +1806,7 @@ impl Project {
|
|||
path: impl Into<ProjectPath>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Model<Buffer>>> {
|
||||
if (self.is_via_collab() || self.is_via_ssh()) && self.is_disconnected(cx) {
|
||||
if self.is_disconnected(cx) {
|
||||
return Task::ready(Err(anyhow!(ErrorCode::Disconnected)));
|
||||
}
|
||||
|
||||
|
@ -1879,6 +1902,20 @@ impl Project {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn open_image(
|
||||
&mut self,
|
||||
path: impl Into<ProjectPath>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Model<ImageItem>>> {
|
||||
if self.is_disconnected(cx) {
|
||||
return Task::ready(Err(anyhow!(ErrorCode::Disconnected)));
|
||||
}
|
||||
|
||||
self.image_store.update(cx, |image_store, cx| {
|
||||
image_store.open_image(path.into(), cx)
|
||||
})
|
||||
}
|
||||
|
||||
async fn send_buffer_ordered_messages(
|
||||
this: WeakModel<Self>,
|
||||
rx: UnboundedReceiver<BufferOrderedMessage>,
|
||||
|
@ -2013,6 +2050,22 @@ impl Project {
|
|||
}
|
||||
}
|
||||
|
||||
fn on_image_store_event(
|
||||
&mut self,
|
||||
_: Model<ImageStore>,
|
||||
event: &ImageStoreEvent,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
ImageStoreEvent::ImageAdded(image) => {
|
||||
cx.subscribe(image, |this, image, event, cx| {
|
||||
this.on_image_event(image, event, cx);
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_lsp_store_event(
|
||||
&mut self,
|
||||
_: Model<LspStore>,
|
||||
|
@ -2253,6 +2306,25 @@ impl Project {
|
|||
None
|
||||
}
|
||||
|
||||
fn on_image_event(
|
||||
&mut self,
|
||||
image: Model<ImageItem>,
|
||||
event: &ImageItemEvent,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Option<()> {
|
||||
match event {
|
||||
ImageItemEvent::ReloadNeeded => {
|
||||
if !self.is_via_collab() {
|
||||
self.reload_images([image.clone()].into_iter().collect(), cx)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn request_buffer_diff_recalculation(
|
||||
&mut self,
|
||||
buffer: &Model<Buffer>,
|
||||
|
@ -2466,6 +2538,15 @@ impl Project {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn reload_images(
|
||||
&self,
|
||||
images: HashSet<Model<ImageItem>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
self.image_store
|
||||
.update(cx, |image_store, cx| image_store.reload_images(images, cx))
|
||||
}
|
||||
|
||||
pub fn format(
|
||||
&mut self,
|
||||
buffers: HashSet<Model<Buffer>>,
|
||||
|
|
|
@ -103,6 +103,11 @@ pub struct LoadedFile {
|
|||
pub diff_base: Option<String>,
|
||||
}
|
||||
|
||||
pub struct LoadedBinaryFile {
|
||||
pub file: Arc<File>,
|
||||
pub content: Vec<u8>,
|
||||
}
|
||||
|
||||
pub struct LocalWorktree {
|
||||
snapshot: LocalSnapshot,
|
||||
scan_requests_tx: channel::Sender<ScanRequest>,
|
||||
|
@ -685,6 +690,19 @@ impl Worktree {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn load_binary_file(
|
||||
&self,
|
||||
path: &Path,
|
||||
cx: &ModelContext<Worktree>,
|
||||
) -> Task<Result<LoadedBinaryFile>> {
|
||||
match self {
|
||||
Worktree::Local(this) => this.load_binary_file(path, cx),
|
||||
Worktree::Remote(_) => {
|
||||
Task::ready(Err(anyhow!("remote worktrees can't yet load binary files")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_file(
|
||||
&self,
|
||||
path: &Path,
|
||||
|
@ -1260,6 +1278,53 @@ impl LocalWorktree {
|
|||
self.git_repositories.get(&repo.work_directory.0)
|
||||
}
|
||||
|
||||
fn load_binary_file(
|
||||
&self,
|
||||
path: &Path,
|
||||
cx: &ModelContext<Worktree>,
|
||||
) -> Task<Result<LoadedBinaryFile>> {
|
||||
let path = Arc::from(path);
|
||||
let abs_path = self.absolutize(&path);
|
||||
let fs = self.fs.clone();
|
||||
let entry = self.refresh_entry(path.clone(), None, cx);
|
||||
let is_private = self.is_path_private(path.as_ref());
|
||||
|
||||
let worktree = cx.weak_model();
|
||||
cx.background_executor().spawn(async move {
|
||||
let abs_path = abs_path?;
|
||||
let content = fs.load_bytes(&abs_path).await?;
|
||||
|
||||
let worktree = worktree
|
||||
.upgrade()
|
||||
.ok_or_else(|| anyhow!("worktree was dropped"))?;
|
||||
let file = match entry.await? {
|
||||
Some(entry) => File::for_entry(entry, worktree),
|
||||
None => {
|
||||
let metadata = fs
|
||||
.metadata(&abs_path)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("Loading metadata for excluded file {abs_path:?}")
|
||||
})?
|
||||
.with_context(|| {
|
||||
format!("Excluded file {abs_path:?} got removed during loading")
|
||||
})?;
|
||||
Arc::new(File {
|
||||
entry_id: None,
|
||||
worktree,
|
||||
path,
|
||||
mtime: Some(metadata.mtime),
|
||||
is_local: true,
|
||||
is_deleted: false,
|
||||
is_private,
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
Ok(LoadedBinaryFile { file, content })
|
||||
})
|
||||
}
|
||||
|
||||
fn load_file(&self, path: &Path, cx: &ModelContext<Worktree>) -> Task<Result<LoadedFile>> {
|
||||
let path = Arc::from(path);
|
||||
let abs_path = self.absolutize(&path);
|
||||
|
@ -3213,6 +3278,14 @@ impl language::LocalFile for File {
|
|||
cx.background_executor()
|
||||
.spawn(async move { fs.load(&abs_path?).await })
|
||||
}
|
||||
|
||||
fn load_bytes(&self, cx: &AppContext) -> Task<Result<Vec<u8>>> {
|
||||
let worktree = self.worktree.read(cx).as_local().unwrap();
|
||||
let abs_path = worktree.absolutize(&self.path);
|
||||
let fs = worktree.fs.clone();
|
||||
cx.background_executor()
|
||||
.spawn(async move { fs.load_bytes(&abs_path?).await })
|
||||
}
|
||||
}
|
||||
|
||||
impl File {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue