diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 3e520f4e2901ff378c64f405d082ad460349ec82..7a085d4f4fe0701b1dc9117144c819aeccd9005e 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -62,7 +62,7 @@ use util::{ use workspace::{ CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry, OpenOptions, ViewId, - invalid_buffer_view::InvalidBufferView, + invalid_item_view::InvalidItemView, item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions}, register_project_item, }; @@ -26251,7 +26251,7 @@ async fn test_non_utf_8_opens(cx: &mut TestAppContext) { assert_eq!( handle.to_any().entity_type(), - TypeId::of::() + TypeId::of::() ); } diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 3a037899f4273c2af8b1f54aa704d9850d76243e..6a5552f8c7a689e310c548e11c3a516fb9aedbe3 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -42,7 +42,7 @@ use ui::{IconDecorationKind, prelude::*}; use util::{ResultExt, TryFutureExt, paths::PathExt}; use workspace::{ CollaboratorId, ItemId, ItemNavHistory, ToolbarItemLocation, ViewId, Workspace, WorkspaceId, - invalid_buffer_view::InvalidBufferView, + invalid_item_view::InvalidItemView, item::{FollowableItem, Item, ItemBufferKind, ItemEvent, ProjectItem, SaveOptions}, searchable::{ Direction, FilteredSearchRange, SearchEvent, SearchableItem, SearchableItemHandle, @@ -1392,8 +1392,8 @@ impl ProjectItem for Editor { e: &anyhow::Error, window: &mut Window, cx: &mut App, - ) -> Option { - Some(InvalidBufferView::new(abs_path, is_local, e, window, cx)) + ) -> Option { + Some(InvalidItemView::new(abs_path, is_local, e, window, cx)) } } diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 047a005548ba0c7c34f6641a91333723883203cc..a5f4a368e377d0c43c43c101532feb3f34ae9fd6 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -1644,6 +1644,8 @@ pub enum ImageFormat { Bmp, /// .tif or .tiff Tiff, + /// .ico + Ico, } impl ImageFormat { @@ -1657,6 +1659,7 @@ impl ImageFormat { ImageFormat::Svg => "image/svg+xml", ImageFormat::Bmp => "image/bmp", ImageFormat::Tiff => "image/tiff", + ImageFormat::Ico => "image/ico", } } @@ -1670,6 +1673,7 @@ impl ImageFormat { "image/svg+xml" => Some(Self::Svg), "image/bmp" => Some(Self::Bmp), "image/tiff" | "image/tif" => Some(Self::Tiff), + "image/ico" => Some(Self::Ico), _ => None, } } @@ -1776,6 +1780,7 @@ impl Image { ImageFormat::Webp => frames_for_image(&self.bytes, image::ImageFormat::WebP)?, ImageFormat::Bmp => frames_for_image(&self.bytes, image::ImageFormat::Bmp)?, ImageFormat::Tiff => frames_for_image(&self.bytes, image::ImageFormat::Tiff)?, + ImageFormat::Ico => frames_for_image(&self.bytes, image::ImageFormat::Ico)?, ImageFormat::Svg => { let pixmap = svg_renderer.render_pixmap(&self.bytes, SvgSize::ScaleFactor(1.0))?; diff --git a/crates/gpui/src/platform/linux/x11/clipboard.rs b/crates/gpui/src/platform/linux/x11/clipboard.rs index 65ad16e82bf103c4ef08e79c692196d3fae58777..3be5008505446e8ca6c6fd93b559fec4779ae85c 100644 --- a/crates/gpui/src/platform/linux/x11/clipboard.rs +++ b/crates/gpui/src/platform/linux/x11/clipboard.rs @@ -86,6 +86,7 @@ x11rb::atom_manager! { SVG__MIME: ImageFormat::mime_type(ImageFormat::Svg ).as_bytes(), BMP__MIME: ImageFormat::mime_type(ImageFormat::Bmp ).as_bytes(), TIFF_MIME: ImageFormat::mime_type(ImageFormat::Tiff).as_bytes(), + ICO__MIME: ImageFormat::mime_type(ImageFormat::Ico ).as_bytes(), // This is just some random name for the property on our window, into which // the clipboard owner writes the data we requested. @@ -1003,6 +1004,7 @@ impl Clipboard { ImageFormat::Svg => self.inner.atoms.SVG__MIME, ImageFormat::Bmp => self.inner.atoms.BMP__MIME, ImageFormat::Tiff => self.inner.atoms.TIFF_MIME, + ImageFormat::Ico => self.inner.atoms.ICO__MIME, }; let data = vec![ClipboardData { bytes: image.bytes, diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index ee393fbced4924d0cd0556a8a4e6de21a012501a..244350169caffef10ea2740a30e36772506e6145 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -1607,6 +1607,7 @@ impl From for UTType { ImageFormat::Gif => Self::gif(), ImageFormat::Bmp => Self::bmp(), ImageFormat::Svg => Self::svg(), + ImageFormat::Ico => Self::ico(), } } } @@ -1645,6 +1646,11 @@ impl UTType { Self(unsafe { ns_string("public.svg-image") }) } + pub fn ico() -> Self { + // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/ico + Self(unsafe { ns_string("com.microsoft.ico") }) + } + pub fn tiff() -> Self { // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/tiff Self(unsafe { NSPasteboardTypeTIFF }) // This is a rare case where there's a built-in NSPasteboardType diff --git a/crates/image_viewer/src/image_viewer.rs b/crates/image_viewer/src/image_viewer.rs index fab0166efea5d4199973b66dd63f68b8bb0f2e1c..17259d15f1d81ac2f46e027fcf7889cdbbe9d011 100644 --- a/crates/image_viewer/src/image_viewer.rs +++ b/crates/image_viewer/src/image_viewer.rs @@ -1,6 +1,8 @@ mod image_info; mod image_viewer_settings; +use std::path::Path; + use anyhow::Context as _; use editor::{EditorSettings, items::entry_git_aware_label_color}; use file_icons::FileIcons; @@ -18,6 +20,7 @@ use ui::prelude::*; use util::paths::PathExt; use workspace::{ ItemId, ItemSettings, Pane, ToolbarItemLocation, Workspace, WorkspaceId, delete_unloaded_items, + invalid_item_view::InvalidItemView, item::{BreadcrumbText, Item, ProjectItem, SerializableItem, TabContentParams}, }; @@ -389,6 +392,19 @@ impl ProjectItem for ImageView { { Self::new(item, project, window, cx) } + + fn for_broken_project_item( + abs_path: &Path, + is_local: bool, + e: &anyhow::Error, + window: &mut Window, + cx: &mut App, + ) -> Option + where + Self: Sized, + { + Some(InvalidItemView::new(abs_path, is_local, e, window, cx)) + } } pub fn init(cx: &mut App) { diff --git a/crates/project/src/image_store.rs b/crates/project/src/image_store.rs index 71394ead2eb27067706023d4870c78c557c3747b..8fcf9c8a6172f866d819e34cbf3b0b4810a8fc8d 100644 --- a/crates/project/src/image_store.rs +++ b/crates/project/src/image_store.rs @@ -687,6 +687,7 @@ fn create_gpui_image(content: Vec) -> anyhow::Result> { image::ImageFormat::Gif => gpui::ImageFormat::Gif, image::ImageFormat::Bmp => gpui::ImageFormat::Bmp, image::ImageFormat::Tiff => gpui::ImageFormat::Tiff, + image::ImageFormat::Ico => gpui::ImageFormat::Ico, format => anyhow::bail!("Image format {format:?} not supported"), }, content, diff --git a/crates/workspace/src/invalid_buffer_view.rs b/crates/project/src/invalid_item_view.rs similarity index 94% rename from crates/workspace/src/invalid_buffer_view.rs rename to crates/project/src/invalid_item_view.rs index 05f409653b69e76654fa11d70b57d61fd6c0b73b..fdcdd16a69ce73d8471f8387d55cf91576f114af 100644 --- a/crates/workspace/src/invalid_buffer_view.rs +++ b/crates/project/src/invalid_item_view.rs @@ -11,7 +11,8 @@ use zed_actions::workspace::OpenWithSystem; use crate::Item; /// A view to display when a certain buffer fails to open. -pub struct InvalidBufferView { +#[derive(Debug)] +pub struct InvalidItemView { /// Which path was attempted to open. pub abs_path: Arc, /// An error message, happened when opening the buffer. @@ -20,7 +21,7 @@ pub struct InvalidBufferView { focus_handle: FocusHandle, } -impl InvalidBufferView { +impl InvalidItemView { pub fn new( abs_path: &Path, is_local: bool, @@ -37,7 +38,7 @@ impl InvalidBufferView { } } -impl Item for InvalidBufferView { +impl Item for InvalidItemView { type Event = (); fn tab_content_text(&self, mut detail: usize, _: &App) -> SharedString { @@ -66,15 +67,15 @@ impl Item for InvalidBufferView { } } -impl EventEmitter<()> for InvalidBufferView {} +impl EventEmitter<()> for InvalidItemView {} -impl Focusable for InvalidBufferView { +impl Focusable for InvalidItemView { fn focus_handle(&self, _: &App) -> FocusHandle { self.focus_handle.clone() } } -impl Render for InvalidBufferView { +impl Render for InvalidItemView { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl gpui::IntoElement { let abs_path = self.abs_path.clone(); v_flex() diff --git a/crates/repl/src/outputs/image.rs b/crates/repl/src/outputs/image.rs index 0cabbbbae4715181a76b3730bf492b481d0a6e1b..fefdbec2fa2770baa279a832bd55278bd502380d 100644 --- a/crates/repl/src/outputs/image.rs +++ b/crates/repl/src/outputs/image.rs @@ -51,6 +51,7 @@ impl ImageView { image::ImageFormat::WebP => ImageFormat::Webp, image::ImageFormat::Tiff => ImageFormat::Tiff, image::ImageFormat::Bmp => ImageFormat::Bmp, + image::ImageFormat::Ico => ImageFormat::Ico, format => { anyhow::bail!("unsupported image format {format:?}"); } diff --git a/crates/workspace/src/invalid_item_view.rs b/crates/workspace/src/invalid_item_view.rs new file mode 100644 index 0000000000000000000000000000000000000000..897190e9aecb97152434c695b823e0aee3148dcb --- /dev/null +++ b/crates/workspace/src/invalid_item_view.rs @@ -0,0 +1,117 @@ +use std::{path::Path, sync::Arc}; + +use gpui::{EventEmitter, FocusHandle, Focusable}; +use ui::{ + App, Button, ButtonCommon, ButtonStyle, Clickable, Context, FluentBuilder, InteractiveElement, + KeyBinding, Label, LabelCommon, LabelSize, ParentElement, Render, SharedString, Styled as _, + Window, h_flex, v_flex, +}; +use zed_actions::workspace::OpenWithSystem; + +use crate::Item; + +/// A view to display when a certain buffer/image/other item fails to open. +pub struct InvalidItemView { + /// Which path was attempted to open. + pub abs_path: Arc, + /// An error message, happened when opening the item. + pub error: SharedString, + is_local: bool, + focus_handle: FocusHandle, +} + +impl InvalidItemView { + pub fn new( + abs_path: &Path, + is_local: bool, + e: &anyhow::Error, + _: &mut Window, + cx: &mut App, + ) -> Self { + Self { + is_local, + abs_path: Arc::from(abs_path), + error: format!("{}", e.root_cause()).into(), + focus_handle: cx.focus_handle(), + } + } +} + +impl Item for InvalidItemView { + type Event = (); + + fn tab_content_text(&self, mut detail: usize, _: &App) -> SharedString { + // Ensure we always render at least the filename. + detail += 1; + + let path = self.abs_path.as_ref(); + + let mut prefix = path; + while detail > 0 { + if let Some(parent) = prefix.parent() { + prefix = parent; + detail -= 1; + } else { + break; + } + } + + let path = if detail > 0 { + path + } else { + path.strip_prefix(prefix).unwrap_or(path) + }; + + SharedString::new(path.to_string_lossy()) + } +} + +impl EventEmitter<()> for InvalidItemView {} + +impl Focusable for InvalidItemView { + fn focus_handle(&self, _: &App) -> FocusHandle { + self.focus_handle.clone() + } +} + +impl Render for InvalidItemView { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl gpui::IntoElement { + let abs_path = self.abs_path.clone(); + v_flex() + .size_full() + .track_focus(&self.focus_handle(cx)) + .flex_none() + .justify_center() + .overflow_hidden() + .key_context("InvalidItem") + .child( + h_flex().size_full().justify_center().child( + v_flex() + .justify_center() + .gap_2() + .child(h_flex().justify_center().child("Could not open file")) + .child( + h_flex() + .justify_center() + .child(Label::new(self.error.clone()).size(LabelSize::Small)), + ) + .when(self.is_local, |contents| { + contents.child( + h_flex().justify_center().child( + Button::new("open-with-system", "Open in Default App") + .on_click(move |_, _, cx| { + cx.open_with_system(&abs_path); + }) + .style(ButtonStyle::Outlined) + .key_binding(KeyBinding::for_action( + &OpenWithSystem, + window, + cx, + )), + ), + ) + }), + ), + ) + } +} diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 42d452a68ee72491a53e9da535d7713c735912f5..a328bb67924f9be7bcbdc457153699a672fea08b 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -1,7 +1,7 @@ use crate::{ CollaboratorId, DelayedDebouncedEditAction, FollowableViewRegistry, ItemNavHistory, SerializableItemRegistry, ToolbarItemLocation, ViewId, Workspace, WorkspaceId, - invalid_buffer_view::InvalidBufferView, + invalid_item_view::InvalidItemView, pane::{self, Pane}, persistence::model::ItemId, searchable::SearchableItemHandle, @@ -1062,7 +1062,7 @@ pub trait ProjectItem: Item { _e: &anyhow::Error, _window: &mut Window, _cx: &mut App, - ) -> Option + ) -> Option where Self: Sized, { diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 3e39a8d18faa367798db7a65689bcca59b07a5ae..68900e1156c56e03dcc1b335a93502da771bdc33 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -2,7 +2,7 @@ use crate::{ CloseWindow, NewFile, NewTerminal, OpenInTerminal, OpenOptions, OpenTerminal, OpenVisible, SplitDirection, ToggleFileFinder, ToggleProjectSymbols, ToggleZoom, Workspace, WorkspaceItemBuilder, - invalid_buffer_view::InvalidBufferView, + invalid_item_view::InvalidItemView, item::{ ActivateOnClose, ClosePosition, Item, ItemBufferKind, ItemHandle, ItemSettings, PreviewTabsSettings, ProjectItemKind, SaveOptions, ShowCloseButton, ShowDiagnostics, @@ -992,11 +992,11 @@ impl Pane { let new_item = build_item(self, window, cx); // A special case that won't ever get a `project_entry_id` but has to be deduplicated nonetheless. - if let Some(invalid_buffer_view) = new_item.downcast::() { + if let Some(invalid_buffer_view) = new_item.downcast::() { let mut already_open_view = None; let mut views_to_close = HashSet::default(); for existing_error_view in self - .items_of_type::() + .items_of_type::() .filter(|item| item.read(cx).abs_path == invalid_buffer_view.read(cx).abs_path) { if already_open_view.is_none() diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 2c0a024158733860b6adf956e617382b16e74b16..c0e59060bd9ca963343761b77f5b25b18dd8b302 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1,6 +1,6 @@ pub mod dock; pub mod history_manager; -pub mod invalid_buffer_view; +pub mod invalid_item_view; pub mod item; mod modal_layer; pub mod notifications;