Detailed changes
@@ -855,7 +855,7 @@
"ctrl-backspace": ["project_panel::Delete", { "skip_prompt": false }],
"ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }],
"alt-ctrl-r": "project_panel::RevealInFileManager",
- "ctrl-shift-enter": "project_panel::OpenWithSystem",
+ "ctrl-shift-enter": "workspace::OpenWithSystem",
"alt-d": "project_panel::CompareMarkedFiles",
"shift-find": "project_panel::NewSearchInDirectory",
"ctrl-alt-shift-f": "project_panel::NewSearchInDirectory",
@@ -1198,5 +1198,12 @@
"alt-shift-l": "onboarding::SignIn",
"alt-shift-a": "onboarding::OpenAccount"
}
+ },
+ {
+ "context": "InvalidBuffer",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-shift-enter": "workspace::OpenWithSystem"
+ }
}
]
@@ -915,7 +915,7 @@
"cmd-backspace": ["project_panel::Trash", { "skip_prompt": true }],
"cmd-delete": ["project_panel::Delete", { "skip_prompt": false }],
"alt-cmd-r": "project_panel::RevealInFileManager",
- "ctrl-shift-enter": "project_panel::OpenWithSystem",
+ "ctrl-shift-enter": "workspace::OpenWithSystem",
"alt-d": "project_panel::CompareMarkedFiles",
"cmd-alt-backspace": ["project_panel::Delete", { "skip_prompt": false }],
"cmd-alt-shift-f": "project_panel::NewSearchInDirectory",
@@ -1301,5 +1301,12 @@
"alt-tab": "onboarding::SignIn",
"alt-shift-a": "onboarding::OpenAccount"
}
+ },
+ {
+ "context": "InvalidBuffer",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-shift-enter": "workspace::OpenWithSystem"
+ }
}
]
@@ -819,7 +819,7 @@
"v": "project_panel::OpenPermanent",
"p": "project_panel::Open",
"x": "project_panel::RevealInFileManager",
- "s": "project_panel::OpenWithSystem",
+ "s": "workspace::OpenWithSystem",
"z d": "project_panel::CompareMarkedFiles",
"] c": "project_panel::SelectNextGitEntry",
"[ c": "project_panel::SelectPrevGitEntry",
@@ -57,7 +57,9 @@ use util::{
use workspace::{
CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
OpenOptions, ViewId,
+ invalid_buffer_view::InvalidBufferView,
item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
+ register_project_item,
};
#[gpui::test]
@@ -24348,6 +24350,41 @@ async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
});
}
+#[gpui::test]
+async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ cx.update(|cx| {
+ register_project_item::<Editor>(cx);
+ });
+
+ let fs = FakeFs::new(cx.executor());
+ fs.insert_tree("/root1", json!({})).await;
+ fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
+ .await;
+
+ let project = Project::test(fs, ["/root1".as_ref()], cx).await;
+ let (workspace, cx) =
+ cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
+
+ let worktree_id = project.update(cx, |project, cx| {
+ project.worktrees(cx).next().unwrap().read(cx).id()
+ });
+
+ let handle = workspace
+ .update_in(cx, |workspace, window, cx| {
+ let project_path = (worktree_id, "one.pdf");
+ workspace.open_path(project_path, None, true, window, cx)
+ })
+ .await
+ .unwrap();
+
+ assert_eq!(
+ handle.to_any().entity_type(),
+ TypeId::of::<InvalidBufferView>()
+ );
+}
+
#[track_caller]
fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
editor
@@ -42,6 +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,
item::{FollowableItem, Item, ItemEvent, ProjectItem, SaveOptions},
searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
};
@@ -1401,6 +1402,16 @@ impl ProjectItem for Editor {
editor
}
+
+ fn for_broken_project_item(
+ abs_path: PathBuf,
+ is_local: bool,
+ e: &anyhow::Error,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> Option<InvalidBufferView> {
+ Some(InvalidBufferView::new(abs_path, is_local, e, window, cx))
+ }
}
fn clip_ranges<'a>(
@@ -69,6 +69,7 @@ use workspace::{
notifications::{DetachAndPromptErr, NotifyTaskExt},
};
use worktree::CreatedEntry;
+use zed_actions::workspace::OpenWithSystem;
const PROJECT_PANEL_KEY: &str = "ProjectPanel";
const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX;
@@ -255,8 +256,6 @@ actions!(
RevealInFileManager,
/// Removes the selected folder from the project.
RemoveFromProject,
- /// Opens the selected file with the system's default application.
- OpenWithSystem,
/// Cuts the selected file or directory.
Cut,
/// Pastes the previously cut or copied item.
@@ -0,0 +1,111 @@
+use std::{path::PathBuf, sync::Arc};
+
+use gpui::{EventEmitter, FocusHandle, Focusable};
+use ui::{
+ App, Button, ButtonCommon, ButtonStyle, Clickable, Context, FluentBuilder, InteractiveElement,
+ KeyBinding, 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 fails to open.
+pub struct InvalidBufferView {
+ /// Which path was attempted to open.
+ pub abs_path: Arc<PathBuf>,
+ /// An error message, happened when opening the buffer.
+ pub error: SharedString,
+ is_local: bool,
+ focus_handle: FocusHandle,
+}
+
+impl InvalidBufferView {
+ pub fn new(
+ abs_path: PathBuf,
+ is_local: bool,
+ e: &anyhow::Error,
+ _: &mut Window,
+ cx: &mut App,
+ ) -> Self {
+ Self {
+ is_local,
+ abs_path: Arc::new(abs_path),
+ error: format!("{e}").into(),
+ focus_handle: cx.focus_handle(),
+ }
+ }
+}
+
+impl Item for InvalidBufferView {
+ 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_path();
+
+ 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 InvalidBufferView {}
+
+impl Focusable for InvalidBufferView {
+ fn focus_handle(&self, _: &App) -> FocusHandle {
+ self.focus_handle.clone()
+ }
+}
+
+impl Render for InvalidBufferView {
+ fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> 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("InvalidBuffer")
+ .child(
+ h_flex().size_full().justify_center().child(
+ v_flex()
+ .justify_center()
+ .gap_2()
+ .child("Cannot display the file contents in Zed")
+ .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,
+ )),
+ ),
+ )
+ }),
+ ),
+ )
+ }
+}
@@ -1,6 +1,7 @@
use crate::{
CollaboratorId, DelayedDebouncedEditAction, FollowableViewRegistry, ItemNavHistory,
SerializableItemRegistry, ToolbarItemLocation, ViewId, Workspace, WorkspaceId,
+ invalid_buffer_view::InvalidBufferView,
pane::{self, Pane},
persistence::model::ItemId,
searchable::SearchableItemHandle,
@@ -22,6 +23,7 @@ use std::{
any::{Any, TypeId},
cell::RefCell,
ops::Range,
+ path::PathBuf,
rc::Rc,
sync::Arc,
time::Duration,
@@ -1161,6 +1163,22 @@ pub trait ProjectItem: Item {
) -> Self
where
Self: Sized;
+
+ /// A fallback handler, which will be called after [`project::ProjectItem::try_open`] fails,
+ /// with the error from that failure as an argument.
+ /// Allows to open an item that can gracefully display and handle errors.
+ fn for_broken_project_item(
+ _abs_path: PathBuf,
+ _is_local: bool,
+ _e: &anyhow::Error,
+ _window: &mut Window,
+ _cx: &mut App,
+ ) -> Option<InvalidBufferView>
+ where
+ Self: Sized,
+ {
+ None
+ }
}
#[derive(Debug)]
@@ -2,6 +2,7 @@ use crate::{
CloseWindow, NewFile, NewTerminal, OpenInTerminal, OpenOptions, OpenTerminal, OpenVisible,
SplitDirection, ToggleFileFinder, ToggleProjectSymbols, ToggleZoom, Workspace,
WorkspaceItemBuilder,
+ invalid_buffer_view::InvalidBufferView,
item::{
ActivateOnClose, ClosePosition, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
ProjectItemKind, SaveOptions, ShowCloseButton, ShowDiagnostics, TabContentParams,
@@ -897,19 +898,43 @@ impl Pane {
}
}
}
- if let Some((index, existing_item)) = existing_item {
- // If the item is already open, and the item is a preview item
- // and we are not allowing items to open as preview, mark the item as persistent.
- if let Some(preview_item_id) = self.preview_item_id
- && let Some(tab) = self.items.get(index)
- && tab.item_id() == preview_item_id
- && !allow_preview
- {
- self.set_preview_item_id(None, cx);
- }
- if activate {
- self.activate_item(index, focus_item, focus_item, window, cx);
+
+ let set_up_existing_item =
+ |index: usize, pane: &mut Self, window: &mut Window, cx: &mut Context<Self>| {
+ // If the item is already open, and the item is a preview item
+ // and we are not allowing items to open as preview, mark the item as persistent.
+ if let Some(preview_item_id) = pane.preview_item_id
+ && let Some(tab) = pane.items.get(index)
+ && tab.item_id() == preview_item_id
+ && !allow_preview
+ {
+ pane.set_preview_item_id(None, cx);
+ }
+ if activate {
+ pane.activate_item(index, focus_item, focus_item, window, cx);
+ }
+ };
+ let set_up_new_item = |new_item: Box<dyn ItemHandle>,
+ destination_index: Option<usize>,
+ pane: &mut Self,
+ window: &mut Window,
+ cx: &mut Context<Self>| {
+ if allow_preview {
+ pane.set_preview_item_id(Some(new_item.item_id()), cx);
}
+ pane.add_item_inner(
+ new_item,
+ true,
+ focus_item,
+ activate,
+ destination_index,
+ window,
+ cx,
+ );
+ };
+
+ if let Some((index, existing_item)) = existing_item {
+ set_up_existing_item(index, self, window, cx);
existing_item
} else {
// If the item is being opened as preview and we have an existing preview tab,
@@ -921,21 +946,46 @@ 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::<InvalidBufferView>() {
+ let mut already_open_view = None;
+ let mut views_to_close = HashSet::default();
+ for existing_error_view in self
+ .items_of_type::<InvalidBufferView>()
+ .filter(|item| item.read(cx).abs_path == invalid_buffer_view.read(cx).abs_path)
+ {
+ if already_open_view.is_none()
+ && existing_error_view.read(cx).error == invalid_buffer_view.read(cx).error
+ {
+ already_open_view = Some(existing_error_view);
+ } else {
+ views_to_close.insert(existing_error_view.item_id());
+ }
+ }
- if allow_preview {
- self.set_preview_item_id(Some(new_item.item_id()), cx);
- }
- self.add_item_inner(
- new_item.clone(),
- true,
- focus_item,
- activate,
- destination_index,
- window,
- cx,
- );
+ let resulting_item = match already_open_view {
+ Some(already_open_view) => {
+ if let Some(index) = self.index_for_item_id(already_open_view.item_id()) {
+ set_up_existing_item(index, self, window, cx);
+ }
+ Box::new(already_open_view) as Box<_>
+ }
+ None => {
+ set_up_new_item(new_item.clone(), destination_index, self, window, cx);
+ new_item
+ }
+ };
- new_item
+ self.close_items(window, cx, SaveIntent::Skip, |existing_item| {
+ views_to_close.contains(&existing_item)
+ })
+ .detach();
+
+ resulting_item
+ } else {
+ set_up_new_item(new_item.clone(), destination_index, self, window, cx);
+ new_item
+ }
}
}
@@ -1,5 +1,6 @@
pub mod dock;
pub mod history_manager;
+pub mod invalid_buffer_view;
pub mod item;
mod modal_layer;
pub mod notifications;
@@ -612,21 +613,49 @@ impl ProjectItemRegistry {
);
self.build_project_item_for_path_fns
.push(|project, project_path, window, cx| {
+ let project_path = project_path.clone();
+ let abs_path = project.read(cx).absolute_path(&project_path, cx);
+ let is_local = project.read(cx).is_local();
let project_item =
- <T::Item as project::ProjectItem>::try_open(project, project_path, cx)?;
+ <T::Item as project::ProjectItem>::try_open(project, &project_path, cx)?;
let project = project.clone();
- Some(window.spawn(cx, async move |cx| {
- let project_item = project_item.await?;
- let project_entry_id: Option<ProjectEntryId> =
- project_item.read_with(cx, project::ProjectItem::entry_id)?;
- let build_workspace_item = Box::new(
- |pane: &mut Pane, window: &mut Window, cx: &mut Context<Pane>| {
- Box::new(cx.new(|cx| {
- T::for_project_item(project, Some(pane), project_item, window, cx)
- })) as Box<dyn ItemHandle>
+ Some(window.spawn(cx, async move |cx| match project_item.await {
+ Ok(project_item) => {
+ let project_item = project_item;
+ let project_entry_id: Option<ProjectEntryId> =
+ project_item.read_with(cx, project::ProjectItem::entry_id)?;
+ let build_workspace_item = Box::new(
+ |pane: &mut Pane, window: &mut Window, cx: &mut Context<Pane>| {
+ Box::new(cx.new(|cx| {
+ T::for_project_item(
+ project,
+ Some(pane),
+ project_item,
+ window,
+ cx,
+ )
+ })) as Box<dyn ItemHandle>
+ },
+ ) as Box<_>;
+ Ok((project_entry_id, build_workspace_item))
+ }
+ Err(e) => match abs_path {
+ Some(abs_path) => match cx.update(|window, cx| {
+ T::for_broken_project_item(abs_path, is_local, &e, window, cx)
+ })? {
+ Some(broken_project_item_view) => {
+ let build_workspace_item = Box::new(
+ move |_: &mut Pane, _: &mut Window, cx: &mut Context<Pane>| {
+ cx.new(|_| broken_project_item_view).boxed_clone()
+ },
+ )
+ as Box<_>;
+ Ok((None, build_workspace_item))
+ }
+ None => Err(e)?,
},
- ) as Box<_>;
- Ok((project_entry_id, build_workspace_item))
+ None => Err(e)?,
+ },
}))
});
}
@@ -3379,9 +3408,8 @@ impl Workspace {
window: &mut Window,
cx: &mut App,
) -> Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>> {
- let project = self.project().clone();
let registry = cx.default_global::<ProjectItemRegistry>().clone();
- registry.open_path(&project, &path, window, cx)
+ registry.open_path(self.project(), &path, window, cx)
}
pub fn find_project_item<T>(
@@ -156,7 +156,10 @@ pub mod workspace {
#[action(deprecated_aliases = ["editor::CopyPath", "outline_panel::CopyPath", "project_panel::CopyPath"])]
CopyPath,
#[action(deprecated_aliases = ["editor::CopyRelativePath", "outline_panel::CopyRelativePath", "project_panel::CopyRelativePath"])]
- CopyRelativePath
+ CopyRelativePath,
+ /// Opens the selected file with the system's default application.
+ #[action(deprecated_aliases = ["project_panel::OpenWithSystem"])]
+ OpenWithSystem,
]
);
}