Foundations for Open All the Things (#9353)
This is the beginning of setting up a flexible way to open items beyond the text buffer -- think notebooks, images, GeoJSON, etc. The primary requirement to allow opening an arbitrary file is `try_open` on the `project::Item` trait. Now we can make new `Item`s for other types with their own ways to render. Under the hood, `register_project_item` uses this new opening scheme. It supports a dynamic array of opener functions, that will handle specific item types. By default, a `Buffer` should be able to be able to open any file that another opener did not. A key detail here is that the order of registration matters. The last item has primacy. Here's an example: ```rust workspace::register_project_item::<Editor>(cx); workspace::register_project_item::<Notebook>(cx); workspace::register_project_item::<ImageViewer>(cx); ``` When a project item (file) is attempted to be opened, it's first tried with the `ImageViewer`, followed by the `Notebook`, then the `Editor`. The tests are set up in a way that should make it _hopefully_ easy to learn how to write a new opener. First to go after should probably be image files. Release Notes: N/A --------- Co-authored-by: Antonio Scandurra <me@as-cii.com> Co-authored-by: Mikayla Maki <mikayla@zed.dev>
This commit is contained in:
parent
f9b9123606
commit
72d36d0213
3 changed files with 369 additions and 39 deletions
|
@ -118,6 +118,13 @@ const SERVER_LAUNCHING_BEFORE_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5
|
||||||
pub const SERVER_PROGRESS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(100);
|
pub const SERVER_PROGRESS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(100);
|
||||||
|
|
||||||
pub trait Item {
|
pub trait Item {
|
||||||
|
fn try_open(
|
||||||
|
project: &Model<Project>,
|
||||||
|
path: &ProjectPath,
|
||||||
|
cx: &mut AppContext,
|
||||||
|
) -> Option<Task<Result<Model<Self>>>>
|
||||||
|
where
|
||||||
|
Self: Sized;
|
||||||
fn entry_id(&self, cx: &AppContext) -> Option<ProjectEntryId>;
|
fn entry_id(&self, cx: &AppContext) -> Option<ProjectEntryId>;
|
||||||
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
|
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
|
||||||
}
|
}
|
||||||
|
@ -9616,6 +9623,14 @@ fn resolve_path(base: &Path, path: &Path) -> PathBuf {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Item for Buffer {
|
impl Item for Buffer {
|
||||||
|
fn try_open(
|
||||||
|
project: &Model<Project>,
|
||||||
|
path: &ProjectPath,
|
||||||
|
cx: &mut AppContext,
|
||||||
|
) -> Option<Task<Result<Model<Self>>>> {
|
||||||
|
Some(project.update(cx, |project, cx| project.open_buffer(path.clone(), cx)))
|
||||||
|
}
|
||||||
|
|
||||||
fn entry_id(&self, cx: &AppContext) -> Option<ProjectEntryId> {
|
fn entry_id(&self, cx: &AppContext) -> Option<ProjectEntryId> {
|
||||||
File::from_dyn(self.file()).and_then(|file| file.project_entry_id(cx))
|
File::from_dyn(self.file()).and_then(|file| file.project_entry_id(cx))
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,7 @@ use std::{
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
use theme::Theme;
|
use theme::Theme;
|
||||||
|
use ui::Element as _;
|
||||||
|
|
||||||
pub const LEADER_UPDATE_THROTTLE: Duration = Duration::from_millis(200);
|
pub const LEADER_UPDATE_THROTTLE: Duration = Duration::from_millis(200);
|
||||||
|
|
||||||
|
@ -100,6 +101,15 @@ pub struct BreadcrumbText {
|
||||||
|
|
||||||
pub trait Item: FocusableView + EventEmitter<Self::Event> {
|
pub trait Item: FocusableView + EventEmitter<Self::Event> {
|
||||||
type Event;
|
type Event;
|
||||||
|
fn tab_content(
|
||||||
|
&self,
|
||||||
|
_detail: Option<usize>,
|
||||||
|
_selected: bool,
|
||||||
|
_cx: &WindowContext,
|
||||||
|
) -> AnyElement {
|
||||||
|
gpui::Empty.into_any()
|
||||||
|
}
|
||||||
|
fn to_item_events(_event: &Self::Event, _f: impl FnMut(ItemEvent)) {}
|
||||||
|
|
||||||
fn deactivated(&mut self, _: &mut ViewContext<Self>) {}
|
fn deactivated(&mut self, _: &mut ViewContext<Self>) {}
|
||||||
fn workspace_deactivated(&mut self, _: &mut ViewContext<Self>) {}
|
fn workspace_deactivated(&mut self, _: &mut ViewContext<Self>) {}
|
||||||
|
@ -112,9 +122,10 @@ pub trait Item: FocusableView + EventEmitter<Self::Event> {
|
||||||
fn tab_description(&self, _: usize, _: &AppContext) -> Option<SharedString> {
|
fn tab_description(&self, _: usize, _: &AppContext) -> Option<SharedString> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
fn tab_content(&self, detail: Option<usize>, selected: bool, cx: &WindowContext) -> AnyElement;
|
|
||||||
|
|
||||||
fn telemetry_event_text(&self) -> Option<&'static str>;
|
fn telemetry_event_text(&self) -> Option<&'static str> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
/// (model id, Item)
|
/// (model id, Item)
|
||||||
fn for_each_project_item(
|
fn for_each_project_item(
|
||||||
|
@ -170,8 +181,6 @@ pub trait Item: FocusableView + EventEmitter<Self::Event> {
|
||||||
unimplemented!("reload() must be implemented if can_save() returns true")
|
unimplemented!("reload() must be implemented if can_save() returns true")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_item_events(event: &Self::Event, f: impl FnMut(ItemEvent));
|
|
||||||
|
|
||||||
fn act_as_type<'a>(
|
fn act_as_type<'a>(
|
||||||
&'a self,
|
&'a self,
|
||||||
type_id: TypeId,
|
type_id: TypeId,
|
||||||
|
@ -847,6 +856,14 @@ pub mod test {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl project::Item for TestProjectItem {
|
impl project::Item for TestProjectItem {
|
||||||
|
fn try_open(
|
||||||
|
_project: &Model<Project>,
|
||||||
|
_path: &ProjectPath,
|
||||||
|
_cx: &mut AppContext,
|
||||||
|
) -> Option<Task<gpui::Result<Model<Self>>>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
|
fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
|
||||||
self.entry_id
|
self.entry_id
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,13 +26,13 @@ use futures::{
|
||||||
Future, FutureExt, StreamExt,
|
Future, FutureExt, StreamExt,
|
||||||
};
|
};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, canvas, div, impl_actions, point, size, Action, AnyElement, AnyModel, AnyView,
|
actions, canvas, div, impl_actions, point, size, Action, AnyElement, AnyView, AnyWeakView,
|
||||||
AnyWeakView, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Context, Div,
|
AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Context, Div, DragMoveEvent, Element,
|
||||||
DragMoveEvent, Element, ElementContext, Entity, EntityId, EventEmitter, FocusHandle,
|
ElementContext, Entity, EntityId, EventEmitter, FocusHandle, FocusableView, Global,
|
||||||
FocusableView, Global, GlobalPixels, InteractiveElement, IntoElement, KeyContext, Keystroke,
|
GlobalPixels, InteractiveElement, IntoElement, KeyContext, Keystroke, LayoutId, ManagedView,
|
||||||
LayoutId, ManagedView, Model, ModelContext, ParentElement, PathPromptOptions, Pixels, Point,
|
Model, ModelContext, ParentElement, PathPromptOptions, Pixels, Point, PromptLevel, Render,
|
||||||
PromptLevel, Render, SharedString, Size, Styled, Subscription, Task, View, ViewContext,
|
SharedString, Size, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView,
|
||||||
VisualContext, WeakView, WindowContext, WindowHandle, WindowOptions,
|
WindowContext, WindowHandle, WindowOptions,
|
||||||
};
|
};
|
||||||
use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
|
use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
@ -275,17 +275,37 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Default, Deref, DerefMut)]
|
#[derive(Clone, Default, Deref, DerefMut)]
|
||||||
struct ProjectItemBuilders(
|
struct ProjectItemOpeners(Vec<ProjectItemOpener>);
|
||||||
HashMap<TypeId, fn(Model<Project>, AnyModel, &mut ViewContext<Pane>) -> Box<dyn ItemHandle>>,
|
|
||||||
);
|
|
||||||
|
|
||||||
impl Global for ProjectItemBuilders {}
|
type ProjectItemOpener = fn(
|
||||||
|
&Model<Project>,
|
||||||
|
&ProjectPath,
|
||||||
|
&mut WindowContext,
|
||||||
|
)
|
||||||
|
-> Option<Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>>>;
|
||||||
|
|
||||||
|
type WorkspaceItemBuilder = Box<dyn FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>>;
|
||||||
|
|
||||||
|
impl Global for ProjectItemOpeners {}
|
||||||
|
|
||||||
|
/// Registers a [ProjectItem] for the app. When opening a file, all the registered
|
||||||
|
/// items will get a chance to open the file, starting from the project item that
|
||||||
|
/// was added last.
|
||||||
pub fn register_project_item<I: ProjectItem>(cx: &mut AppContext) {
|
pub fn register_project_item<I: ProjectItem>(cx: &mut AppContext) {
|
||||||
let builders = cx.default_global::<ProjectItemBuilders>();
|
let builders = cx.default_global::<ProjectItemOpeners>();
|
||||||
builders.insert(TypeId::of::<I::Item>(), |project, model, cx| {
|
builders.push(|project, project_path, cx| {
|
||||||
let item = model.downcast::<I::Item>().unwrap();
|
let project_item = <I::Item as project::Item>::try_open(&project, project_path, cx)?;
|
||||||
Box::new(cx.new_view(|cx| I::for_project_item(project, item, cx)))
|
let project = project.clone();
|
||||||
|
Some(cx.spawn(|cx| async move {
|
||||||
|
let project_item = project_item.await?;
|
||||||
|
let project_entry_id: Option<ProjectEntryId> =
|
||||||
|
project_item.read_with(&cx, |item, cx| project::Item::entry_id(item, cx))?;
|
||||||
|
let build_workspace_item = Box::new(|cx: &mut ViewContext<Pane>| {
|
||||||
|
Box::new(cx.new_view(|cx| I::for_project_item(project, project_item, cx)))
|
||||||
|
as Box<dyn ItemHandle>
|
||||||
|
}) as Box<_>;
|
||||||
|
Ok((project_entry_id, build_workspace_item))
|
||||||
|
}))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2051,26 +2071,17 @@ impl Workspace {
|
||||||
&mut self,
|
&mut self,
|
||||||
path: ProjectPath,
|
path: ProjectPath,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) -> Task<
|
) -> Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>> {
|
||||||
Result<(
|
|
||||||
Option<ProjectEntryId>,
|
|
||||||
impl 'static + Send + FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
|
|
||||||
)>,
|
|
||||||
> {
|
|
||||||
let project = self.project().clone();
|
let project = self.project().clone();
|
||||||
let project_item = project.update(cx, |project, cx| project.open_path(path, cx));
|
let project_item_builders = cx.default_global::<ProjectItemOpeners>().clone();
|
||||||
cx.spawn(|mut cx| async move {
|
let Some(open_project_item) = project_item_builders
|
||||||
let (project_entry_id, project_item) = project_item.await?;
|
.iter()
|
||||||
let build_item = cx.update(|cx| {
|
.rev()
|
||||||
cx.default_global::<ProjectItemBuilders>()
|
.find_map(|open_project_item| open_project_item(&project, &path, cx))
|
||||||
.get(&project_item.entity_type())
|
else {
|
||||||
.ok_or_else(|| anyhow!("no item builder for project item"))
|
return Task::ready(Err(anyhow!("cannot open file {:?}", path.path)));
|
||||||
.cloned()
|
};
|
||||||
})??;
|
open_project_item
|
||||||
let build_item =
|
|
||||||
move |cx: &mut ViewContext<Pane>| build_item(project, project_item, cx);
|
|
||||||
Ok((project_entry_id, build_item))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn open_project_item<T>(
|
pub fn open_project_item<T>(
|
||||||
|
@ -4811,7 +4822,7 @@ mod tests {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use fs::FakeFs;
|
use fs::FakeFs;
|
||||||
use gpui::{px, DismissEvent, TestAppContext, VisualTestContext};
|
use gpui::{px, DismissEvent, Empty, TestAppContext, VisualTestContext};
|
||||||
use project::{Project, ProjectEntryId};
|
use project::{Project, ProjectEntryId};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
|
@ -5787,6 +5798,293 @@ mod tests {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mod register_project_item_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
const TEST_PNG_KIND: &str = "TestPngItemView";
|
||||||
|
// View
|
||||||
|
struct TestPngItemView {
|
||||||
|
focus_handle: FocusHandle,
|
||||||
|
}
|
||||||
|
// Model
|
||||||
|
struct TestPngItem {}
|
||||||
|
|
||||||
|
impl project::Item for TestPngItem {
|
||||||
|
fn try_open(
|
||||||
|
_project: &Model<Project>,
|
||||||
|
path: &ProjectPath,
|
||||||
|
cx: &mut AppContext,
|
||||||
|
) -> Option<Task<gpui::Result<Model<Self>>>> {
|
||||||
|
if path.path.extension().unwrap() == "png" {
|
||||||
|
Some(cx.spawn(|mut cx| async move { cx.new_model(|_| TestPngItem {}) }))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Item for TestPngItemView {
|
||||||
|
type Event = ();
|
||||||
|
|
||||||
|
fn serialized_item_kind() -> Option<&'static str> {
|
||||||
|
Some(TEST_PNG_KIND)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl EventEmitter<()> for TestPngItemView {}
|
||||||
|
impl FocusableView for TestPngItemView {
|
||||||
|
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
|
||||||
|
self.focus_handle.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for TestPngItemView {
|
||||||
|
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
|
Empty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProjectItem for TestPngItemView {
|
||||||
|
type Item = TestPngItem;
|
||||||
|
|
||||||
|
fn for_project_item(
|
||||||
|
_project: Model<Project>,
|
||||||
|
_item: Model<Self::Item>,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> Self
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
Self {
|
||||||
|
focus_handle: cx.focus_handle(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const TEST_IPYNB_KIND: &str = "TestIpynbItemView";
|
||||||
|
// View
|
||||||
|
struct TestIpynbItemView {
|
||||||
|
focus_handle: FocusHandle,
|
||||||
|
}
|
||||||
|
// Model
|
||||||
|
struct TestIpynbItem {}
|
||||||
|
|
||||||
|
impl project::Item for TestIpynbItem {
|
||||||
|
fn try_open(
|
||||||
|
_project: &Model<Project>,
|
||||||
|
path: &ProjectPath,
|
||||||
|
cx: &mut AppContext,
|
||||||
|
) -> Option<Task<gpui::Result<Model<Self>>>> {
|
||||||
|
if path.path.extension().unwrap() == "ipynb" {
|
||||||
|
Some(cx.spawn(|mut cx| async move { cx.new_model(|_| TestIpynbItem {}) }))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Item for TestIpynbItemView {
|
||||||
|
type Event = ();
|
||||||
|
|
||||||
|
fn serialized_item_kind() -> Option<&'static str> {
|
||||||
|
Some(TEST_IPYNB_KIND)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl EventEmitter<()> for TestIpynbItemView {}
|
||||||
|
impl FocusableView for TestIpynbItemView {
|
||||||
|
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
|
||||||
|
self.focus_handle.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for TestIpynbItemView {
|
||||||
|
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
|
Empty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProjectItem for TestIpynbItemView {
|
||||||
|
type Item = TestIpynbItem;
|
||||||
|
|
||||||
|
fn for_project_item(
|
||||||
|
_project: Model<Project>,
|
||||||
|
_item: Model<Self::Item>,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> Self
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
Self {
|
||||||
|
focus_handle: cx.focus_handle(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TestAlternatePngItemView {
|
||||||
|
focus_handle: FocusHandle,
|
||||||
|
}
|
||||||
|
|
||||||
|
const TEST_ALTERNATE_PNG_KIND: &str = "TestAlternatePngItemView";
|
||||||
|
impl Item for TestAlternatePngItemView {
|
||||||
|
type Event = ();
|
||||||
|
|
||||||
|
fn serialized_item_kind() -> Option<&'static str> {
|
||||||
|
Some(TEST_ALTERNATE_PNG_KIND)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl EventEmitter<()> for TestAlternatePngItemView {}
|
||||||
|
impl FocusableView for TestAlternatePngItemView {
|
||||||
|
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
|
||||||
|
self.focus_handle.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for TestAlternatePngItemView {
|
||||||
|
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
|
Empty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProjectItem for TestAlternatePngItemView {
|
||||||
|
type Item = TestPngItem;
|
||||||
|
|
||||||
|
fn for_project_item(
|
||||||
|
_project: Model<Project>,
|
||||||
|
_item: Model<Self::Item>,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> Self
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
Self {
|
||||||
|
focus_handle: cx.focus_handle(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_register_project_item(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
|
||||||
|
cx.update(|cx| {
|
||||||
|
register_project_item::<TestPngItemView>(cx);
|
||||||
|
register_project_item::<TestIpynbItemView>(cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
fs.insert_tree(
|
||||||
|
"/root1",
|
||||||
|
json!({
|
||||||
|
"one.png": "BINARYDATAHERE",
|
||||||
|
"two.ipynb": "{ totally a notebook }",
|
||||||
|
"three.txt": "editing text, sure why not?"
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let project = Project::test(fs, ["root1".as_ref()], cx).await;
|
||||||
|
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
|
||||||
|
|
||||||
|
let worktree_id = project.update(cx, |project, cx| {
|
||||||
|
project.worktrees().next().unwrap().read(cx).id()
|
||||||
|
});
|
||||||
|
|
||||||
|
let handle = workspace
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
let project_path = (worktree_id, "one.png");
|
||||||
|
workspace.open_path(project_path, None, true, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Now we can check if the handle we got back errored or not
|
||||||
|
assert_eq!(handle.serialized_item_kind().unwrap(), TEST_PNG_KIND);
|
||||||
|
|
||||||
|
let handle = workspace
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
let project_path = (worktree_id, "two.ipynb");
|
||||||
|
workspace.open_path(project_path, None, true, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(handle.serialized_item_kind().unwrap(), TEST_IPYNB_KIND);
|
||||||
|
|
||||||
|
let handle = workspace
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
let project_path = (worktree_id, "three.txt");
|
||||||
|
workspace.open_path(project_path, None, true, cx)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
assert!(handle.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
|
||||||
|
cx.update(|cx| {
|
||||||
|
register_project_item::<TestPngItemView>(cx);
|
||||||
|
register_project_item::<TestAlternatePngItemView>(cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
fs.insert_tree(
|
||||||
|
"/root1",
|
||||||
|
json!({
|
||||||
|
"one.png": "BINARYDATAHERE",
|
||||||
|
"two.ipynb": "{ totally a notebook }",
|
||||||
|
"three.txt": "editing text, sure why not?"
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let project = Project::test(fs, ["root1".as_ref()], cx).await;
|
||||||
|
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
|
||||||
|
|
||||||
|
let worktree_id = project.update(cx, |project, cx| {
|
||||||
|
project.worktrees().next().unwrap().read(cx).id()
|
||||||
|
});
|
||||||
|
|
||||||
|
let handle = workspace
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
let project_path = (worktree_id, "one.png");
|
||||||
|
workspace.open_path(project_path, None, true, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// This _must_ be the second item registered
|
||||||
|
assert_eq!(
|
||||||
|
handle.serialized_item_kind().unwrap(),
|
||||||
|
TEST_ALTERNATE_PNG_KIND
|
||||||
|
);
|
||||||
|
|
||||||
|
let handle = workspace
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
let project_path = (worktree_id, "three.txt");
|
||||||
|
workspace.open_path(project_path, None, true, cx)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
assert!(handle.is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn init_test(cx: &mut TestAppContext) {
|
pub fn init_test(cx: &mut TestAppContext) {
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
let settings_store = SettingsStore::test(cx);
|
let settings_store = SettingsStore::test(cx);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue