Merge pull request #249 from zed-industries/invert-workspace-editor-dependency
Invert dependency between workspace and editor
This commit is contained in:
Generated
+2
-2
@@ -1532,6 +1532,7 @@ dependencies = [
|
||||
"log",
|
||||
"parking_lot",
|
||||
"postage",
|
||||
"project",
|
||||
"rand 0.8.3",
|
||||
"serde",
|
||||
"smallvec",
|
||||
@@ -1542,6 +1543,7 @@ dependencies = [
|
||||
"tree-sitter-rust",
|
||||
"unindent",
|
||||
"util",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5611,9 +5613,7 @@ name = "workspace"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"buffer",
|
||||
"client",
|
||||
"editor",
|
||||
"gpui",
|
||||
"language",
|
||||
"log",
|
||||
|
||||
@@ -15,9 +15,11 @@ buffer = { path = "../buffer" }
|
||||
clock = { path = "../clock" }
|
||||
gpui = { path = "../gpui" }
|
||||
language = { path = "../language" }
|
||||
project = { path = "../project" }
|
||||
sum_tree = { path = "../sum_tree" }
|
||||
theme = { path = "../theme" }
|
||||
util = { path = "../util" }
|
||||
workspace = { path = "../workspace" }
|
||||
anyhow = "1.0"
|
||||
lazy_static = "1.4"
|
||||
log = "0.4"
|
||||
|
||||
@@ -1,66 +1,110 @@
|
||||
use super::{Item, ItemView};
|
||||
use crate::{status_bar::StatusItemView, Settings};
|
||||
use crate::{Editor, EditorSettings, Event};
|
||||
use anyhow::Result;
|
||||
use buffer::{Point, Selection, ToPoint};
|
||||
use editor::{Editor, EditorSettings, Event};
|
||||
use gpui::{
|
||||
elements::*, fonts::TextStyle, AppContext, Entity, ModelHandle, RenderContext, Subscription,
|
||||
Task, View, ViewContext, ViewHandle,
|
||||
elements::*, fonts::TextStyle, AppContext, Entity, ModelContext, ModelHandle,
|
||||
MutableAppContext, RenderContext, Subscription, Task, View, ViewContext, ViewHandle,
|
||||
WeakModelHandle,
|
||||
};
|
||||
use language::{Buffer, Diagnostic, File as _};
|
||||
use postage::watch;
|
||||
use project::{ProjectPath, Worktree};
|
||||
use std::fmt::Write;
|
||||
use std::path::Path;
|
||||
use workspace::{
|
||||
EntryOpener, ItemHandle, ItemView, ItemViewHandle, Settings, StatusItemView, WeakItemHandle,
|
||||
};
|
||||
|
||||
impl Item for Buffer {
|
||||
type View = Editor;
|
||||
pub struct BufferOpener;
|
||||
|
||||
fn build_view(
|
||||
handle: ModelHandle<Self>,
|
||||
#[derive(Clone)]
|
||||
pub struct BufferItemHandle(pub ModelHandle<Buffer>);
|
||||
|
||||
#[derive(Clone)]
|
||||
struct WeakBufferItemHandle(WeakModelHandle<Buffer>);
|
||||
|
||||
impl EntryOpener for BufferOpener {
|
||||
fn open(
|
||||
&self,
|
||||
worktree: &mut Worktree,
|
||||
project_path: ProjectPath,
|
||||
cx: &mut ModelContext<Worktree>,
|
||||
) -> Option<Task<Result<Box<dyn ItemHandle>>>> {
|
||||
let buffer = worktree.open_buffer(project_path.path, cx);
|
||||
let task = cx.spawn(|_, _| async move {
|
||||
buffer
|
||||
.await
|
||||
.map(|buffer| Box::new(BufferItemHandle(buffer)) as Box<dyn ItemHandle>)
|
||||
});
|
||||
Some(task)
|
||||
}
|
||||
}
|
||||
|
||||
impl ItemHandle for BufferItemHandle {
|
||||
fn add_view(
|
||||
&self,
|
||||
window_id: usize,
|
||||
settings: watch::Receiver<Settings>,
|
||||
cx: &mut ViewContext<Self::View>,
|
||||
) -> Self::View {
|
||||
Editor::for_buffer(
|
||||
handle,
|
||||
move |cx| {
|
||||
let settings = settings.borrow();
|
||||
let font_cache = cx.font_cache();
|
||||
let font_family_id = settings.buffer_font_family;
|
||||
let font_family_name = cx.font_cache().family_name(font_family_id).unwrap();
|
||||
let font_properties = Default::default();
|
||||
let font_id = font_cache
|
||||
.select_font(font_family_id, &font_properties)
|
||||
.unwrap();
|
||||
let font_size = settings.buffer_font_size;
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Box<dyn ItemViewHandle> {
|
||||
Box::new(cx.add_view(window_id, |cx| {
|
||||
Editor::for_buffer(
|
||||
self.0.clone(),
|
||||
move |cx| {
|
||||
let settings = settings.borrow();
|
||||
let font_cache = cx.font_cache();
|
||||
let font_family_id = settings.buffer_font_family;
|
||||
let font_family_name = cx.font_cache().family_name(font_family_id).unwrap();
|
||||
let font_properties = Default::default();
|
||||
let font_id = font_cache
|
||||
.select_font(font_family_id, &font_properties)
|
||||
.unwrap();
|
||||
let font_size = settings.buffer_font_size;
|
||||
|
||||
let mut theme = settings.theme.editor.clone();
|
||||
theme.text = TextStyle {
|
||||
color: theme.text.color,
|
||||
font_family_name,
|
||||
font_family_id,
|
||||
font_id,
|
||||
font_size,
|
||||
font_properties,
|
||||
underline: None,
|
||||
};
|
||||
EditorSettings {
|
||||
tab_size: settings.tab_size,
|
||||
style: theme,
|
||||
}
|
||||
},
|
||||
cx,
|
||||
)
|
||||
let mut theme = settings.theme.editor.clone();
|
||||
theme.text = TextStyle {
|
||||
color: theme.text.color,
|
||||
font_family_name,
|
||||
font_family_id,
|
||||
font_id,
|
||||
font_size,
|
||||
font_properties,
|
||||
underline: None,
|
||||
};
|
||||
EditorSettings {
|
||||
tab_size: settings.tab_size,
|
||||
style: theme,
|
||||
}
|
||||
},
|
||||
cx,
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
fn project_path(&self) -> Option<ProjectPath> {
|
||||
self.file().map(|f| ProjectPath {
|
||||
fn boxed_clone(&self) -> Box<dyn ItemHandle> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
|
||||
fn downgrade(&self) -> Box<dyn workspace::WeakItemHandle> {
|
||||
Box::new(WeakBufferItemHandle(self.0.downgrade()))
|
||||
}
|
||||
|
||||
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
|
||||
self.0.read(cx).file().map(|f| ProjectPath {
|
||||
worktree_id: f.worktree_id(),
|
||||
path: f.path().clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl WeakItemHandle for WeakBufferItemHandle {
|
||||
fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
|
||||
self.0
|
||||
.upgrade(cx)
|
||||
.map(|buffer| Box::new(BufferItemHandle(buffer)) as Box<dyn ItemHandle>)
|
||||
}
|
||||
}
|
||||
|
||||
impl ItemView for Editor {
|
||||
fn should_activate_item_on_event(event: &Event) -> bool {
|
||||
matches!(event, Event::Activate)
|
||||
@@ -226,7 +270,7 @@ impl View for CursorPosition {
|
||||
impl StatusItemView for CursorPosition {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
active_pane_item: Option<&dyn crate::ItemViewHandle>,
|
||||
active_pane_item: Option<&dyn ItemViewHandle>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if let Some(editor) = active_pane_item.and_then(|item| item.to_any().downcast::<Editor>()) {
|
||||
@@ -312,7 +356,7 @@ impl View for DiagnosticMessage {
|
||||
impl StatusItemView for DiagnosticMessage {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
active_pane_item: Option<&dyn crate::ItemViewHandle>,
|
||||
active_pane_item: Option<&dyn ItemViewHandle>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if let Some(editor) = active_pane_item.and_then(|item| item.to_any().downcast::<Editor>()) {
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod display_map;
|
||||
mod element;
|
||||
pub mod items;
|
||||
pub mod movement;
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -17,6 +18,7 @@ use gpui::{
|
||||
text_layout, AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle,
|
||||
MutableAppContext, RenderContext, View, ViewContext, WeakViewHandle,
|
||||
};
|
||||
use items::BufferItemHandle;
|
||||
use language::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smallvec::SmallVec;
|
||||
@@ -34,6 +36,7 @@ use std::{
|
||||
use sum_tree::Bias;
|
||||
use theme::{DiagnosticStyle, EditorStyle, SyntaxTheme};
|
||||
use util::post_inc;
|
||||
use workspace::{EntryOpener, Workspace};
|
||||
|
||||
const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
|
||||
const MAX_LINE_LEN: usize = 1024;
|
||||
@@ -97,7 +100,8 @@ action!(FoldSelectedRanges);
|
||||
action!(Scroll, Vector2F);
|
||||
action!(Select, SelectPhase);
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
pub fn init(cx: &mut MutableAppContext, entry_openers: &mut Vec<Box<dyn EntryOpener>>) {
|
||||
entry_openers.push(Box::new(items::BufferOpener));
|
||||
cx.add_bindings(vec![
|
||||
Binding::new("escape", Cancel, Some("Editor")),
|
||||
Binding::new("backspace", Backspace, Some("Editor")),
|
||||
@@ -201,6 +205,7 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||
Binding::new("alt-cmd-f", FoldSelectedRanges, Some("Editor")),
|
||||
]);
|
||||
|
||||
cx.add_action(Editor::open_new);
|
||||
cx.add_action(|this: &mut Editor, action: &Scroll, cx| this.set_scroll_position(action.0, cx));
|
||||
cx.add_action(Editor::select);
|
||||
cx.add_action(Editor::cancel);
|
||||
@@ -478,6 +483,15 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open_new(
|
||||
workspace: &mut Workspace,
|
||||
_: &workspace::OpenNew,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, "", cx));
|
||||
workspace.add_item(BufferItemHandle(buffer), cx);
|
||||
}
|
||||
|
||||
pub fn replica_id(&self, cx: &AppContext) -> ReplicaId {
|
||||
self.buffer.read(cx).replica_id()
|
||||
}
|
||||
|
||||
@@ -14,5 +14,6 @@ workspace = { path = "../workspace" }
|
||||
postage = { version = "0.4.1", features = ["futures-traits"] }
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
serde_json = { version = "1.0.64", features = ["preserve_order"] }
|
||||
workspace = { path = "../workspace", features = ["test-support"] }
|
||||
|
||||
@@ -429,7 +429,14 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_matching_paths(mut cx: gpui::TestAppContext) {
|
||||
let params = cx.update(WorkspaceParams::test);
|
||||
let mut entry_openers = Vec::new();
|
||||
cx.update(|cx| {
|
||||
super::init(cx);
|
||||
editor::init(cx, &mut entry_openers);
|
||||
});
|
||||
|
||||
let mut params = cx.update(WorkspaceParams::test);
|
||||
params.entry_openers = Arc::from(entry_openers);
|
||||
params
|
||||
.fs
|
||||
.as_fake()
|
||||
@@ -443,10 +450,6 @@ mod tests {
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
cx.update(|cx| {
|
||||
super::init(cx);
|
||||
editor::init(cx);
|
||||
});
|
||||
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx));
|
||||
workspace
|
||||
|
||||
@@ -11,4 +11,6 @@ workspace = { path = "../workspace" }
|
||||
postage = { version = "0.4.1", features = ["futures-traits"] }
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
workspace = { path = "../workspace", features = ["test-support"] }
|
||||
serde_json = { version = "1.0.64", features = ["preserve_order"] }
|
||||
|
||||
@@ -948,7 +948,8 @@ mod tests {
|
||||
lsp,
|
||||
people_panel::JoinWorktree,
|
||||
project::{ProjectPath, Worktree},
|
||||
workspace::{Workspace, WorkspaceParams},
|
||||
test::test_app_state,
|
||||
workspace::Workspace,
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
@@ -1059,15 +1060,17 @@ mod tests {
|
||||
#[gpui::test]
|
||||
async fn test_unshare_worktree(mut cx_a: TestAppContext, mut cx_b: TestAppContext) {
|
||||
cx_b.update(zed::people_panel::init);
|
||||
let lang_registry = Arc::new(LanguageRegistry::new());
|
||||
let mut app_state_a = cx_a.update(test_app_state);
|
||||
let mut app_state_b = cx_b.update(test_app_state);
|
||||
|
||||
// Connect to a server as 2 clients.
|
||||
let mut server = TestServer::start().await;
|
||||
let (client_a, _) = server.create_client(&mut cx_a, "user_a").await;
|
||||
let (client_a, user_store_a) = server.create_client(&mut cx_a, "user_a").await;
|
||||
let (client_b, user_store_b) = server.create_client(&mut cx_b, "user_b").await;
|
||||
let mut workspace_b_params = cx_b.update(WorkspaceParams::test);
|
||||
workspace_b_params.client = client_b;
|
||||
workspace_b_params.user_store = user_store_b;
|
||||
Arc::get_mut(&mut app_state_a).unwrap().client = client_a;
|
||||
Arc::get_mut(&mut app_state_a).unwrap().user_store = user_store_a;
|
||||
Arc::get_mut(&mut app_state_b).unwrap().client = client_b;
|
||||
Arc::get_mut(&mut app_state_b).unwrap().user_store = user_store_b;
|
||||
|
||||
cx_a.foreground().forbid_parking();
|
||||
|
||||
@@ -1083,10 +1086,10 @@ mod tests {
|
||||
)
|
||||
.await;
|
||||
let worktree_a = Worktree::open_local(
|
||||
client_a.clone(),
|
||||
app_state_a.client.clone(),
|
||||
"/a".as_ref(),
|
||||
fs,
|
||||
lang_registry.clone(),
|
||||
app_state_a.languages.clone(),
|
||||
&mut cx_a.to_async(),
|
||||
)
|
||||
.await
|
||||
@@ -1100,7 +1103,8 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(&workspace_b_params, cx));
|
||||
let (window_b, workspace_b) =
|
||||
cx_b.add_window(|cx| Workspace::new(&app_state_b.as_ref().into(), cx));
|
||||
cx_b.update(|cx| {
|
||||
cx.dispatch_action(
|
||||
window_b,
|
||||
|
||||
@@ -12,9 +12,7 @@ test-support = [
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
buffer = { path = "../buffer" }
|
||||
client = { path = "../client" }
|
||||
editor = { path = "../editor" }
|
||||
gpui = { path = "../gpui" }
|
||||
language = { path = "../language" }
|
||||
project = { path = "../project" }
|
||||
@@ -27,6 +25,7 @@ tree-sitter-rust = { version = "0.19.0", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
client = { path = "../client", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
project = { path = "../project", features = ["test-support"] }
|
||||
serde_json = { version = "1.0.64", features = ["preserve_order"] }
|
||||
tree-sitter = "0.19.5"
|
||||
|
||||
+92
-528
@@ -1,18 +1,17 @@
|
||||
mod items;
|
||||
pub mod pane;
|
||||
pub mod pane_group;
|
||||
pub mod settings;
|
||||
pub mod sidebar;
|
||||
mod status_bar;
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{anyhow, Result};
|
||||
use client::{Authenticate, ChannelList, Client, UserStore};
|
||||
use gpui::{
|
||||
action, elements::*, json::to_string_pretty, keymap::Binding, platform::CursorStyle,
|
||||
AnyViewHandle, AppContext, ClipboardItem, Entity, ModelHandle, MutableAppContext, PromptLevel,
|
||||
RenderContext, Task, View, ViewContext, ViewHandle, WeakModelHandle,
|
||||
AnyViewHandle, AppContext, ClipboardItem, Entity, ModelContext, ModelHandle, MutableAppContext,
|
||||
PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle, WeakModelHandle,
|
||||
};
|
||||
use language::{Buffer, LanguageRegistry};
|
||||
use language::LanguageRegistry;
|
||||
use log::error;
|
||||
pub use pane::*;
|
||||
pub use pane_group::*;
|
||||
@@ -20,6 +19,8 @@ use postage::{prelude::Stream, watch};
|
||||
use project::{Fs, Project, ProjectPath, Worktree};
|
||||
pub use settings::Settings;
|
||||
use sidebar::{Side, Sidebar, SidebarItemId, ToggleSidebarItem, ToggleSidebarItemFocus};
|
||||
use status_bar::StatusBar;
|
||||
pub use status_bar::StatusItemView;
|
||||
use std::{
|
||||
collections::{hash_map::Entry, HashMap},
|
||||
future::Future,
|
||||
@@ -27,8 +28,6 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use crate::status_bar::StatusBar;
|
||||
|
||||
action!(OpenNew, WorkspaceParams);
|
||||
action!(Save);
|
||||
action!(DebugElements);
|
||||
@@ -36,7 +35,6 @@ action!(DebugElements);
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(Workspace::save_active_item);
|
||||
cx.add_action(Workspace::debug_elements);
|
||||
cx.add_action(Workspace::open_new_file);
|
||||
cx.add_action(Workspace::toggle_sidebar_item);
|
||||
cx.add_action(Workspace::toggle_sidebar_item_focus);
|
||||
cx.add_bindings(vec![
|
||||
@@ -62,6 +60,15 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||
pane::init(cx);
|
||||
}
|
||||
|
||||
pub trait EntryOpener {
|
||||
fn open(
|
||||
&self,
|
||||
worktree: &mut Worktree,
|
||||
path: ProjectPath,
|
||||
cx: &mut ModelContext<Worktree>,
|
||||
) -> Option<Task<Result<Box<dyn ItemHandle>>>>;
|
||||
}
|
||||
|
||||
pub trait Item: Entity + Sized {
|
||||
type View: ItemView;
|
||||
|
||||
@@ -108,21 +115,21 @@ pub trait ItemView: View {
|
||||
}
|
||||
|
||||
pub trait ItemHandle: Send + Sync {
|
||||
fn boxed_clone(&self) -> Box<dyn ItemHandle>;
|
||||
fn downgrade(&self) -> Box<dyn WeakItemHandle>;
|
||||
}
|
||||
|
||||
pub trait WeakItemHandle {
|
||||
fn add_view(
|
||||
&self,
|
||||
window_id: usize,
|
||||
settings: watch::Receiver<Settings>,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Option<Box<dyn ItemViewHandle>>;
|
||||
fn alive(&self, cx: &AppContext) -> bool;
|
||||
) -> Box<dyn ItemViewHandle>;
|
||||
fn boxed_clone(&self) -> Box<dyn ItemHandle>;
|
||||
fn downgrade(&self) -> Box<dyn WeakItemHandle>;
|
||||
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
|
||||
}
|
||||
|
||||
pub trait WeakItemHandle {
|
||||
fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>>;
|
||||
}
|
||||
|
||||
pub trait ItemViewHandle {
|
||||
fn title(&self, cx: &AppContext) -> String;
|
||||
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
|
||||
@@ -143,6 +150,15 @@ pub trait ItemViewHandle {
|
||||
}
|
||||
|
||||
impl<T: Item> ItemHandle for ModelHandle<T> {
|
||||
fn add_view(
|
||||
&self,
|
||||
window_id: usize,
|
||||
settings: watch::Receiver<Settings>,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Box<dyn ItemViewHandle> {
|
||||
Box::new(cx.add_view(window_id, |cx| T::build_view(self.clone(), settings, cx)))
|
||||
}
|
||||
|
||||
fn boxed_clone(&self) -> Box<dyn ItemHandle> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
@@ -150,30 +166,38 @@ impl<T: Item> ItemHandle for ModelHandle<T> {
|
||||
fn downgrade(&self) -> Box<dyn WeakItemHandle> {
|
||||
Box::new(self.downgrade())
|
||||
}
|
||||
|
||||
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
|
||||
self.read(cx).project_path()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Item> WeakItemHandle for WeakModelHandle<T> {
|
||||
impl ItemHandle for Box<dyn ItemHandle> {
|
||||
fn add_view(
|
||||
&self,
|
||||
window_id: usize,
|
||||
settings: watch::Receiver<Settings>,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Option<Box<dyn ItemViewHandle>> {
|
||||
if let Some(handle) = self.upgrade(cx.as_ref()) {
|
||||
Some(Box::new(cx.add_view(window_id, |cx| {
|
||||
T::build_view(handle, settings, cx)
|
||||
})))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
) -> Box<dyn ItemViewHandle> {
|
||||
ItemHandle::add_view(self.as_ref(), window_id, settings, cx)
|
||||
}
|
||||
|
||||
fn alive(&self, cx: &AppContext) -> bool {
|
||||
self.upgrade(cx).is_some()
|
||||
fn boxed_clone(&self) -> Box<dyn ItemHandle> {
|
||||
self.as_ref().boxed_clone()
|
||||
}
|
||||
|
||||
fn downgrade(&self) -> Box<dyn WeakItemHandle> {
|
||||
self.as_ref().downgrade()
|
||||
}
|
||||
|
||||
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
|
||||
self.upgrade(cx).and_then(|h| h.read(cx).project_path())
|
||||
self.as_ref().project_path(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Item> WeakItemHandle for WeakModelHandle<T> {
|
||||
fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
|
||||
WeakModelHandle::<T>::upgrade(*self, cx).map(|i| Box::new(i) as Box<dyn ItemHandle>)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,21 +292,13 @@ pub struct WorkspaceParams {
|
||||
pub settings: watch::Receiver<Settings>,
|
||||
pub user_store: ModelHandle<UserStore>,
|
||||
pub channel_list: ModelHandle<ChannelList>,
|
||||
pub entry_openers: Arc<[Box<dyn EntryOpener>]>,
|
||||
}
|
||||
|
||||
impl WorkspaceParams {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn test(cx: &mut MutableAppContext) -> Self {
|
||||
let mut languages = LanguageRegistry::new();
|
||||
languages.add(Arc::new(language::Language::new(
|
||||
language::LanguageConfig {
|
||||
name: "Rust".to_string(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
tree_sitter_rust::language(),
|
||||
)));
|
||||
|
||||
let languages = LanguageRegistry::new();
|
||||
let client = Client::new();
|
||||
let http_client = client::test::FakeHttpClient::new(|_| async move {
|
||||
Ok(client::http::ServerResponse::new(404))
|
||||
@@ -299,6 +315,7 @@ impl WorkspaceParams {
|
||||
languages: Arc::new(languages),
|
||||
settings: watch::channel_with(settings).1,
|
||||
user_store,
|
||||
entry_openers: Arc::from([]),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -316,6 +333,7 @@ pub struct Workspace {
|
||||
active_pane: ViewHandle<Pane>,
|
||||
status_bar: ViewHandle<StatusBar>,
|
||||
project: ModelHandle<Project>,
|
||||
entry_openers: Arc<[Box<dyn EntryOpener>]>,
|
||||
items: Vec<Box<dyn WeakItemHandle>>,
|
||||
loading_items: HashMap<
|
||||
ProjectPath,
|
||||
@@ -349,15 +367,7 @@ impl Workspace {
|
||||
.detach();
|
||||
cx.focus(&pane);
|
||||
|
||||
let cursor_position = cx.add_view(|_| items::CursorPosition::new(params.settings.clone()));
|
||||
let diagnostic = cx.add_view(|_| items::DiagnosticMessage::new(params.settings.clone()));
|
||||
let status_bar = cx.add_view(|cx| {
|
||||
let mut status_bar = StatusBar::new(&pane, params.settings.clone(), cx);
|
||||
status_bar.add_left_item(diagnostic, cx);
|
||||
status_bar.add_right_item(cursor_position, cx);
|
||||
status_bar
|
||||
});
|
||||
|
||||
let status_bar = cx.add_view(|cx| StatusBar::new(&pane, params.settings.clone(), cx));
|
||||
let mut current_user = params.user_store.read(cx).watch_current_user().clone();
|
||||
let mut connection_status = params.client.status().clone();
|
||||
let _observe_current_user = cx.spawn_weak(|this, mut cx| async move {
|
||||
@@ -388,6 +398,7 @@ impl Workspace {
|
||||
left_sidebar: Sidebar::new(Side::Left),
|
||||
right_sidebar: Sidebar::new(Side::Right),
|
||||
project,
|
||||
entry_openers: params.entry_openers.clone(),
|
||||
items: Default::default(),
|
||||
loading_items: Default::default(),
|
||||
_observe_current_user,
|
||||
@@ -402,6 +413,10 @@ impl Workspace {
|
||||
&mut self.right_sidebar
|
||||
}
|
||||
|
||||
pub fn status_bar(&self) -> &ViewHandle<StatusBar> {
|
||||
&self.status_bar
|
||||
}
|
||||
|
||||
pub fn project(&self) -> &ModelHandle<Project> {
|
||||
&self.project
|
||||
}
|
||||
@@ -560,16 +575,6 @@ impl Workspace {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open_new_file(&mut self, _: &OpenNew, cx: &mut ViewContext<Self>) {
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, "", cx));
|
||||
let item_handle = ItemHandle::downgrade(&buffer);
|
||||
let view = item_handle
|
||||
.add_view(cx.window_id(), self.settings.clone(), cx)
|
||||
.unwrap();
|
||||
self.items.push(item_handle);
|
||||
self.active_pane().add_item_view(view, cx.as_mut());
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn open_entry(
|
||||
&mut self,
|
||||
@@ -581,8 +586,6 @@ impl Workspace {
|
||||
return None;
|
||||
}
|
||||
|
||||
// let (worktree_id, path) = project_path.clone();
|
||||
|
||||
let worktree = match self
|
||||
.project
|
||||
.read(cx)
|
||||
@@ -600,24 +603,26 @@ impl Workspace {
|
||||
entry.insert(rx);
|
||||
|
||||
let project_path = project_path.clone();
|
||||
let entry_openers = self.entry_openers.clone();
|
||||
cx.as_mut()
|
||||
.spawn(|mut cx| async move {
|
||||
let buffer = worktree
|
||||
.update(&mut cx, |worktree, cx| {
|
||||
worktree.open_buffer(project_path.path.as_ref(), cx)
|
||||
let item = worktree.update(&mut cx, move |worktree, cx| {
|
||||
for opener in entry_openers.iter() {
|
||||
if let Some(task) = opener.open(worktree, project_path.clone(), cx) {
|
||||
return task;
|
||||
}
|
||||
}
|
||||
|
||||
cx.spawn(|_, _| async move {
|
||||
Err(anyhow!("no opener for path {:?} found", project_path))
|
||||
})
|
||||
.await;
|
||||
*tx.borrow_mut() = Some(
|
||||
buffer
|
||||
.map(|buffer| Box::new(buffer) as Box<dyn ItemHandle>)
|
||||
.map_err(Arc::new),
|
||||
);
|
||||
});
|
||||
*tx.borrow_mut() = Some(item.await.map_err(Arc::new));
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
let pane = pane.downgrade();
|
||||
let settings = self.settings.clone();
|
||||
let mut watch = self.loading_items.get(&project_path).unwrap().clone();
|
||||
|
||||
Some(cx.spawn(|this, mut cx| async move {
|
||||
@@ -637,12 +642,7 @@ impl Workspace {
|
||||
// to the pane. If it was, we activate it, otherwise we'll store the
|
||||
// item and add a new view for it.
|
||||
if !this.activate_or_open_existing_entry(project_path, &pane, cx) {
|
||||
let weak_item = item.downgrade();
|
||||
let view = weak_item
|
||||
.add_view(cx.window_id(), settings, cx.as_mut())
|
||||
.unwrap();
|
||||
this.items.push(weak_item);
|
||||
pane.add_item_view(view, cx.as_mut());
|
||||
this.add_item(item, cx);
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
@@ -671,16 +671,14 @@ impl Workspace {
|
||||
let settings = self.settings.clone();
|
||||
let mut view_for_existing_item = None;
|
||||
self.items.retain(|item| {
|
||||
if item.alive(cx.as_ref()) {
|
||||
if let Some(item) = item.upgrade(cx) {
|
||||
if view_for_existing_item.is_none()
|
||||
&& item
|
||||
.project_path(cx)
|
||||
.map_or(false, |item_project_path| item_project_path == project_path)
|
||||
{
|
||||
view_for_existing_item = Some(
|
||||
item.add_view(cx.window_id(), settings.clone(), cx.as_mut())
|
||||
.unwrap(),
|
||||
);
|
||||
view_for_existing_item =
|
||||
Some(item.add_view(cx.window_id(), settings.clone(), cx.as_mut()));
|
||||
}
|
||||
true
|
||||
} else {
|
||||
@@ -695,7 +693,7 @@ impl Workspace {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn active_item(&self, cx: &ViewContext<Self>) -> Option<Box<dyn ItemViewHandle>> {
|
||||
pub fn active_item(&self, cx: &AppContext) -> Option<Box<dyn ItemViewHandle>> {
|
||||
self.active_pane().read(cx).active_item()
|
||||
}
|
||||
|
||||
@@ -836,6 +834,15 @@ impl Workspace {
|
||||
pane
|
||||
}
|
||||
|
||||
pub fn add_item<T>(&mut self, item_handle: T, cx: &mut ViewContext<Self>)
|
||||
where
|
||||
T: ItemHandle,
|
||||
{
|
||||
let view = item_handle.add_view(cx.window_id(), self.settings.clone(), cx);
|
||||
self.items.push(item_handle.downgrade());
|
||||
self.active_pane().add_item_view(view, cx.as_mut());
|
||||
}
|
||||
|
||||
fn activate_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
|
||||
self.active_pane = pane;
|
||||
self.status_bar.update(cx, |status_bar, cx| {
|
||||
@@ -868,7 +875,7 @@ impl Workspace {
|
||||
}
|
||||
}
|
||||
|
||||
fn split_pane(
|
||||
pub fn split_pane(
|
||||
&mut self,
|
||||
pane: ViewHandle<Pane>,
|
||||
direction: SplitDirection,
|
||||
@@ -895,6 +902,10 @@ impl Workspace {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn panes(&self) -> &[ViewHandle<Pane>] {
|
||||
&self.panes
|
||||
}
|
||||
|
||||
fn pane(&self, pane_id: usize) -> Option<ViewHandle<Pane>> {
|
||||
self.panes.iter().find(|pane| pane.id() == pane_id).cloned()
|
||||
}
|
||||
@@ -1069,12 +1080,10 @@ impl View for Workspace {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub trait WorkspaceHandle {
|
||||
fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl WorkspaceHandle for ViewHandle<Workspace> {
|
||||
fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
|
||||
self.read(cx)
|
||||
@@ -1090,448 +1099,3 @@ impl WorkspaceHandle for ViewHandle<Workspace> {
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use editor::{Editor, Input};
|
||||
use serde_json::json;
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_open_entry(mut cx: gpui::TestAppContext) {
|
||||
let params = cx.update(WorkspaceParams::test);
|
||||
params
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"a": {
|
||||
"file1": "contents 1",
|
||||
"file2": "contents 2",
|
||||
"file3": "contents 3",
|
||||
},
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx));
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
workspace.add_worktree(Path::new("/root"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
|
||||
.await;
|
||||
let entries = cx.read(|cx| workspace.file_project_paths(cx));
|
||||
let file1 = entries[0].clone();
|
||||
let file2 = entries[1].clone();
|
||||
let file3 = entries[2].clone();
|
||||
|
||||
// Open the first entry
|
||||
workspace
|
||||
.update(&mut cx, |w, cx| w.open_entry(file1.clone(), cx))
|
||||
.unwrap()
|
||||
.await;
|
||||
cx.read(|cx| {
|
||||
let pane = workspace.read(cx).active_pane().read(cx);
|
||||
assert_eq!(
|
||||
pane.active_item().unwrap().project_path(cx),
|
||||
Some(file1.clone())
|
||||
);
|
||||
assert_eq!(pane.items().len(), 1);
|
||||
});
|
||||
|
||||
// Open the second entry
|
||||
workspace
|
||||
.update(&mut cx, |w, cx| w.open_entry(file2.clone(), cx))
|
||||
.unwrap()
|
||||
.await;
|
||||
cx.read(|cx| {
|
||||
let pane = workspace.read(cx).active_pane().read(cx);
|
||||
assert_eq!(
|
||||
pane.active_item().unwrap().project_path(cx),
|
||||
Some(file2.clone())
|
||||
);
|
||||
assert_eq!(pane.items().len(), 2);
|
||||
});
|
||||
|
||||
// Open the first entry again. The existing pane item is activated.
|
||||
workspace.update(&mut cx, |w, cx| {
|
||||
assert!(w.open_entry(file1.clone(), cx).is_none())
|
||||
});
|
||||
cx.read(|cx| {
|
||||
let pane = workspace.read(cx).active_pane().read(cx);
|
||||
assert_eq!(
|
||||
pane.active_item().unwrap().project_path(cx),
|
||||
Some(file1.clone())
|
||||
);
|
||||
assert_eq!(pane.items().len(), 2);
|
||||
});
|
||||
|
||||
// Split the pane with the first entry, then open the second entry again.
|
||||
workspace.update(&mut cx, |w, cx| {
|
||||
w.split_pane(w.active_pane().clone(), SplitDirection::Right, cx);
|
||||
assert!(w.open_entry(file2.clone(), cx).is_none());
|
||||
assert_eq!(
|
||||
w.active_pane()
|
||||
.read(cx)
|
||||
.active_item()
|
||||
.unwrap()
|
||||
.project_path(cx.as_ref()),
|
||||
Some(file2.clone())
|
||||
);
|
||||
});
|
||||
|
||||
// Open the third entry twice concurrently. Only one pane item is added.
|
||||
let (t1, t2) = workspace.update(&mut cx, |w, cx| {
|
||||
(
|
||||
w.open_entry(file3.clone(), cx).unwrap(),
|
||||
w.open_entry(file3.clone(), cx).unwrap(),
|
||||
)
|
||||
});
|
||||
t1.await;
|
||||
t2.await;
|
||||
cx.read(|cx| {
|
||||
let pane = workspace.read(cx).active_pane().read(cx);
|
||||
assert_eq!(
|
||||
pane.active_item().unwrap().project_path(cx),
|
||||
Some(file3.clone())
|
||||
);
|
||||
let pane_entries = pane
|
||||
.items()
|
||||
.iter()
|
||||
.map(|i| i.project_path(cx).unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(pane_entries, &[file1, file2, file3]);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_open_paths(mut cx: gpui::TestAppContext) {
|
||||
let params = cx.update(WorkspaceParams::test);
|
||||
let fs = params.fs.as_fake();
|
||||
fs.insert_dir("/dir1").await.unwrap();
|
||||
fs.insert_dir("/dir2").await.unwrap();
|
||||
fs.insert_file("/dir1/a.txt", "".into()).await.unwrap();
|
||||
fs.insert_file("/dir2/b.txt", "".into()).await.unwrap();
|
||||
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx));
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
workspace.add_worktree("/dir1".as_ref(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
|
||||
.await;
|
||||
|
||||
// Open a file within an existing worktree.
|
||||
cx.update(|cx| {
|
||||
workspace.update(cx, |view, cx| view.open_paths(&["/dir1/a.txt".into()], cx))
|
||||
})
|
||||
.await;
|
||||
cx.read(|cx| {
|
||||
assert_eq!(
|
||||
workspace
|
||||
.read(cx)
|
||||
.active_pane()
|
||||
.read(cx)
|
||||
.active_item()
|
||||
.unwrap()
|
||||
.title(cx),
|
||||
"a.txt"
|
||||
);
|
||||
});
|
||||
|
||||
// Open a file outside of any existing worktree.
|
||||
cx.update(|cx| {
|
||||
workspace.update(cx, |view, cx| view.open_paths(&["/dir2/b.txt".into()], cx))
|
||||
})
|
||||
.await;
|
||||
cx.read(|cx| {
|
||||
let worktree_roots = workspace
|
||||
.read(cx)
|
||||
.worktrees(cx)
|
||||
.iter()
|
||||
.map(|w| w.read(cx).as_local().unwrap().abs_path())
|
||||
.collect::<HashSet<_>>();
|
||||
assert_eq!(
|
||||
worktree_roots,
|
||||
vec!["/dir1", "/dir2/b.txt"]
|
||||
.into_iter()
|
||||
.map(Path::new)
|
||||
.collect(),
|
||||
);
|
||||
assert_eq!(
|
||||
workspace
|
||||
.read(cx)
|
||||
.active_pane()
|
||||
.read(cx)
|
||||
.active_item()
|
||||
.unwrap()
|
||||
.title(cx),
|
||||
"b.txt"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_save_conflicting_item(mut cx: gpui::TestAppContext) {
|
||||
let params = cx.update(WorkspaceParams::test);
|
||||
let fs = params.fs.as_fake();
|
||||
fs.insert_tree("/root", json!({ "a.txt": "" })).await;
|
||||
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx));
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
workspace.add_worktree(Path::new("/root"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Open a file within an existing worktree.
|
||||
cx.update(|cx| {
|
||||
workspace.update(cx, |view, cx| {
|
||||
view.open_paths(&[PathBuf::from("/root/a.txt")], cx)
|
||||
})
|
||||
})
|
||||
.await;
|
||||
let editor = cx.read(|cx| {
|
||||
let pane = workspace.read(cx).active_pane().read(cx);
|
||||
let item = pane.active_item().unwrap();
|
||||
item.to_any().downcast::<Editor>().unwrap()
|
||||
});
|
||||
|
||||
cx.update(|cx| editor.update(cx, |editor, cx| editor.handle_input(&Input("x".into()), cx)));
|
||||
fs.insert_file("/root/a.txt", "changed".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
editor
|
||||
.condition(&cx, |editor, cx| editor.has_conflict(cx))
|
||||
.await;
|
||||
cx.read(|cx| assert!(editor.is_dirty(cx)));
|
||||
|
||||
cx.update(|cx| workspace.update(cx, |w, cx| w.save_active_item(&Save, cx)));
|
||||
cx.simulate_prompt_answer(window_id, 0);
|
||||
editor
|
||||
.condition(&cx, |editor, cx| !editor.is_dirty(cx))
|
||||
.await;
|
||||
cx.read(|cx| assert!(!editor.has_conflict(cx)));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_open_and_save_new_file(mut cx: gpui::TestAppContext) {
|
||||
let params = cx.update(WorkspaceParams::test);
|
||||
params.fs.as_fake().insert_dir("/root").await.unwrap();
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx));
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
workspace.add_worktree(Path::new("/root"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let worktree = cx.read(|cx| {
|
||||
workspace
|
||||
.read(cx)
|
||||
.worktrees(cx)
|
||||
.iter()
|
||||
.next()
|
||||
.unwrap()
|
||||
.clone()
|
||||
});
|
||||
|
||||
// Create a new untitled buffer
|
||||
let editor = workspace.update(&mut cx, |workspace, cx| {
|
||||
workspace.open_new_file(&OpenNew(params.clone()), cx);
|
||||
workspace
|
||||
.active_item(cx)
|
||||
.unwrap()
|
||||
.to_any()
|
||||
.downcast::<Editor>()
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert!(!editor.is_dirty(cx.as_ref()));
|
||||
assert_eq!(editor.title(cx.as_ref()), "untitled");
|
||||
assert!(editor.language(cx).is_none());
|
||||
editor.handle_input(&Input("hi".into()), cx);
|
||||
assert!(editor.is_dirty(cx.as_ref()));
|
||||
});
|
||||
|
||||
// Save the buffer. This prompts for a filename.
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
workspace.save_active_item(&Save, cx)
|
||||
});
|
||||
cx.simulate_new_path_selection(|parent_dir| {
|
||||
assert_eq!(parent_dir, Path::new("/root"));
|
||||
Some(parent_dir.join("the-new-name.rs"))
|
||||
});
|
||||
cx.read(|cx| {
|
||||
assert!(editor.is_dirty(cx));
|
||||
assert_eq!(editor.title(cx), "untitled");
|
||||
});
|
||||
|
||||
// When the save completes, the buffer's title is updated.
|
||||
editor
|
||||
.condition(&cx, |editor, cx| !editor.is_dirty(cx))
|
||||
.await;
|
||||
cx.read(|cx| {
|
||||
assert!(!editor.is_dirty(cx));
|
||||
assert_eq!(editor.title(cx), "the-new-name.rs");
|
||||
});
|
||||
// The language is assigned based on the path
|
||||
editor.read_with(&cx, |editor, cx| {
|
||||
assert_eq!(editor.language(cx).unwrap().name(), "Rust")
|
||||
});
|
||||
|
||||
// Edit the file and save it again. This time, there is no filename prompt.
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
editor.handle_input(&Input(" there".into()), cx);
|
||||
assert_eq!(editor.is_dirty(cx.as_ref()), true);
|
||||
});
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
workspace.save_active_item(&Save, cx)
|
||||
});
|
||||
assert!(!cx.did_prompt_for_new_path());
|
||||
editor
|
||||
.condition(&cx, |editor, cx| !editor.is_dirty(cx))
|
||||
.await;
|
||||
cx.read(|cx| assert_eq!(editor.title(cx), "the-new-name.rs"));
|
||||
|
||||
// Open the same newly-created file in another pane item. The new editor should reuse
|
||||
// the same buffer.
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
workspace.open_new_file(&OpenNew(params.clone()), cx);
|
||||
workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
|
||||
assert!(workspace
|
||||
.open_entry(
|
||||
ProjectPath {
|
||||
worktree_id: worktree.id(),
|
||||
path: Path::new("the-new-name.rs").into()
|
||||
},
|
||||
cx
|
||||
)
|
||||
.is_none());
|
||||
});
|
||||
let editor2 = workspace.update(&mut cx, |workspace, cx| {
|
||||
workspace
|
||||
.active_item(cx)
|
||||
.unwrap()
|
||||
.to_any()
|
||||
.downcast::<Editor>()
|
||||
.unwrap()
|
||||
});
|
||||
cx.read(|cx| {
|
||||
assert_eq!(editor2.read(cx).buffer(), editor.read(cx).buffer());
|
||||
})
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_setting_language_when_saving_as_single_file_worktree(
|
||||
mut cx: gpui::TestAppContext,
|
||||
) {
|
||||
let params = cx.update(WorkspaceParams::test);
|
||||
params.fs.as_fake().insert_dir("/root").await.unwrap();
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx));
|
||||
|
||||
// Create a new untitled buffer
|
||||
let editor = workspace.update(&mut cx, |workspace, cx| {
|
||||
workspace.open_new_file(&OpenNew(params.clone()), cx);
|
||||
workspace
|
||||
.active_item(cx)
|
||||
.unwrap()
|
||||
.to_any()
|
||||
.downcast::<Editor>()
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert!(editor.language(cx).is_none());
|
||||
editor.handle_input(&Input("hi".into()), cx);
|
||||
assert!(editor.is_dirty(cx.as_ref()));
|
||||
});
|
||||
|
||||
// Save the buffer. This prompts for a filename.
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
workspace.save_active_item(&Save, cx)
|
||||
});
|
||||
cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs")));
|
||||
|
||||
editor
|
||||
.condition(&cx, |editor, cx| !editor.is_dirty(cx))
|
||||
.await;
|
||||
|
||||
// The language is assigned based on the path
|
||||
editor.read_with(&cx, |editor, cx| {
|
||||
assert_eq!(editor.language(cx).unwrap().name(), "Rust")
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_pane_actions(mut cx: gpui::TestAppContext) {
|
||||
cx.update(|cx| pane::init(cx));
|
||||
let params = cx.update(WorkspaceParams::test);
|
||||
params
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"a": {
|
||||
"file1": "contents 1",
|
||||
"file2": "contents 2",
|
||||
"file3": "contents 3",
|
||||
},
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx));
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
workspace.add_worktree(Path::new("/root"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
|
||||
.await;
|
||||
let entries = cx.read(|cx| workspace.file_project_paths(cx));
|
||||
let file1 = entries[0].clone();
|
||||
|
||||
let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone());
|
||||
|
||||
workspace
|
||||
.update(&mut cx, |w, cx| w.open_entry(file1.clone(), cx))
|
||||
.unwrap()
|
||||
.await;
|
||||
cx.read(|cx| {
|
||||
assert_eq!(
|
||||
pane_1.read(cx).active_item().unwrap().project_path(cx),
|
||||
Some(file1.clone())
|
||||
);
|
||||
});
|
||||
|
||||
cx.dispatch_action(
|
||||
window_id,
|
||||
vec![pane_1.id()],
|
||||
pane::Split(SplitDirection::Right),
|
||||
);
|
||||
cx.update(|cx| {
|
||||
let pane_2 = workspace.read(cx).active_pane().clone();
|
||||
assert_ne!(pane_1, pane_2);
|
||||
|
||||
let pane2_item = pane_2.read(cx).active_item().unwrap();
|
||||
assert_eq!(pane2_item.project_path(cx.as_ref()), Some(file1.clone()));
|
||||
|
||||
cx.dispatch_action(window_id, vec![pane_2.id()], &CloseActiveItem);
|
||||
let workspace = workspace.read(cx);
|
||||
assert_eq!(workspace.panes.len(), 1);
|
||||
assert_eq!(workspace.active_pane(), &pane_1);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +95,6 @@ impl Pane {
|
||||
item_idx
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn items(&self) -> &[Box<dyn ItemViewHandle>] {
|
||||
&self.items
|
||||
}
|
||||
|
||||
+466
-8
@@ -26,7 +26,7 @@ use std::{path::PathBuf, sync::Arc};
|
||||
use theme::ThemeRegistry;
|
||||
use theme_selector::ThemeSelectorParams;
|
||||
pub use workspace;
|
||||
use workspace::{Settings, Workspace, WorkspaceParams};
|
||||
use workspace::{OpenNew, Settings, Workspace, WorkspaceParams};
|
||||
|
||||
action!(About);
|
||||
action!(Open, Arc<AppState>);
|
||||
@@ -45,6 +45,7 @@ pub struct AppState {
|
||||
pub user_store: ModelHandle<client::UserStore>,
|
||||
pub fs: Arc<dyn fs::Fs>,
|
||||
pub channel_list: ModelHandle<client::ChannelList>,
|
||||
pub entry_openers: Arc<[Box<dyn workspace::EntryOpener>]>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -128,12 +129,10 @@ fn open_paths(action: &OpenPaths, cx: &mut MutableAppContext) -> Task<()> {
|
||||
})
|
||||
}
|
||||
|
||||
fn open_new(action: &workspace::OpenNew, cx: &mut MutableAppContext) {
|
||||
cx.add_window(window_options(), |cx| {
|
||||
let mut workspace = build_workspace(&action.0, cx);
|
||||
workspace.open_new_file(&action, cx);
|
||||
workspace
|
||||
});
|
||||
fn open_new(action: &OpenNew, cx: &mut MutableAppContext) {
|
||||
let (window_id, workspace) =
|
||||
cx.add_window(window_options(), |cx| build_workspace(&action.0, cx));
|
||||
cx.dispatch_action(window_id, vec![workspace.id()], action);
|
||||
}
|
||||
|
||||
fn build_workspace(params: &WorkspaceParams, cx: &mut ViewContext<Workspace>) -> Workspace {
|
||||
@@ -160,6 +159,16 @@ fn build_workspace(params: &WorkspaceParams, cx: &mut ViewContext<Workspace>) ->
|
||||
})
|
||||
.into(),
|
||||
);
|
||||
|
||||
let diagnostic =
|
||||
cx.add_view(|_| editor::items::DiagnosticMessage::new(params.settings.clone()));
|
||||
let cursor_position =
|
||||
cx.add_view(|_| editor::items::CursorPosition::new(params.settings.clone()));
|
||||
workspace.status_bar().update(cx, |status_bar, cx| {
|
||||
status_bar.add_left_item(diagnostic, cx);
|
||||
status_bar.add_right_item(cursor_position, cx);
|
||||
});
|
||||
|
||||
workspace
|
||||
}
|
||||
|
||||
@@ -185,6 +194,7 @@ impl<'a> From<&'a AppState> for WorkspaceParams {
|
||||
settings: state.settings.clone(),
|
||||
user_store: state.user_store.clone(),
|
||||
channel_list: state.channel_list.clone(),
|
||||
entry_openers: state.entry_openers.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -202,11 +212,14 @@ impl<'a> From<&'a AppState> for ThemeSelectorParams {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use editor::Editor;
|
||||
use project::ProjectPath;
|
||||
use serde_json::json;
|
||||
use std::{collections::HashSet, path::Path};
|
||||
use test::test_app_state;
|
||||
use theme::DEFAULT_THEME_NAME;
|
||||
use util::test::temp_tree;
|
||||
use workspace::ItemView;
|
||||
use workspace::{pane, ItemView, ItemViewHandle, SplitDirection, WorkspaceHandle};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_open_paths_action(mut cx: gpui::TestAppContext) {
|
||||
@@ -308,6 +321,451 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_open_entry(mut cx: gpui::TestAppContext) {
|
||||
let app_state = cx.update(test_app_state);
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"a": {
|
||||
"file1": "contents 1",
|
||||
"file2": "contents 2",
|
||||
"file3": "contents 3",
|
||||
},
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state.as_ref().into(), cx));
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
workspace.add_worktree(Path::new("/root"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
|
||||
.await;
|
||||
let entries = cx.read(|cx| workspace.file_project_paths(cx));
|
||||
let file1 = entries[0].clone();
|
||||
let file2 = entries[1].clone();
|
||||
let file3 = entries[2].clone();
|
||||
|
||||
// Open the first entry
|
||||
workspace
|
||||
.update(&mut cx, |w, cx| w.open_entry(file1.clone(), cx))
|
||||
.unwrap()
|
||||
.await;
|
||||
cx.read(|cx| {
|
||||
let pane = workspace.read(cx).active_pane().read(cx);
|
||||
assert_eq!(
|
||||
pane.active_item().unwrap().project_path(cx),
|
||||
Some(file1.clone())
|
||||
);
|
||||
assert_eq!(pane.items().len(), 1);
|
||||
});
|
||||
|
||||
// Open the second entry
|
||||
workspace
|
||||
.update(&mut cx, |w, cx| w.open_entry(file2.clone(), cx))
|
||||
.unwrap()
|
||||
.await;
|
||||
cx.read(|cx| {
|
||||
let pane = workspace.read(cx).active_pane().read(cx);
|
||||
assert_eq!(
|
||||
pane.active_item().unwrap().project_path(cx),
|
||||
Some(file2.clone())
|
||||
);
|
||||
assert_eq!(pane.items().len(), 2);
|
||||
});
|
||||
|
||||
// Open the first entry again. The existing pane item is activated.
|
||||
workspace.update(&mut cx, |w, cx| {
|
||||
assert!(w.open_entry(file1.clone(), cx).is_none())
|
||||
});
|
||||
cx.read(|cx| {
|
||||
let pane = workspace.read(cx).active_pane().read(cx);
|
||||
assert_eq!(
|
||||
pane.active_item().unwrap().project_path(cx),
|
||||
Some(file1.clone())
|
||||
);
|
||||
assert_eq!(pane.items().len(), 2);
|
||||
});
|
||||
|
||||
// Split the pane with the first entry, then open the second entry again.
|
||||
workspace.update(&mut cx, |w, cx| {
|
||||
w.split_pane(w.active_pane().clone(), SplitDirection::Right, cx);
|
||||
assert!(w.open_entry(file2.clone(), cx).is_none());
|
||||
assert_eq!(
|
||||
w.active_pane()
|
||||
.read(cx)
|
||||
.active_item()
|
||||
.unwrap()
|
||||
.project_path(cx.as_ref()),
|
||||
Some(file2.clone())
|
||||
);
|
||||
});
|
||||
|
||||
// Open the third entry twice concurrently. Only one pane item is added.
|
||||
let (t1, t2) = workspace.update(&mut cx, |w, cx| {
|
||||
(
|
||||
w.open_entry(file3.clone(), cx).unwrap(),
|
||||
w.open_entry(file3.clone(), cx).unwrap(),
|
||||
)
|
||||
});
|
||||
t1.await;
|
||||
t2.await;
|
||||
cx.read(|cx| {
|
||||
let pane = workspace.read(cx).active_pane().read(cx);
|
||||
assert_eq!(
|
||||
pane.active_item().unwrap().project_path(cx),
|
||||
Some(file3.clone())
|
||||
);
|
||||
let pane_entries = pane
|
||||
.items()
|
||||
.iter()
|
||||
.map(|i| i.project_path(cx).unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(pane_entries, &[file1, file2, file3]);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_open_paths(mut cx: gpui::TestAppContext) {
|
||||
let app_state = cx.update(test_app_state);
|
||||
let fs = app_state.fs.as_fake();
|
||||
fs.insert_dir("/dir1").await.unwrap();
|
||||
fs.insert_dir("/dir2").await.unwrap();
|
||||
fs.insert_file("/dir1/a.txt", "".into()).await.unwrap();
|
||||
fs.insert_file("/dir2/b.txt", "".into()).await.unwrap();
|
||||
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state.as_ref().into(), cx));
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
workspace.add_worktree("/dir1".as_ref(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
|
||||
.await;
|
||||
|
||||
// Open a file within an existing worktree.
|
||||
cx.update(|cx| {
|
||||
workspace.update(cx, |view, cx| view.open_paths(&["/dir1/a.txt".into()], cx))
|
||||
})
|
||||
.await;
|
||||
cx.read(|cx| {
|
||||
assert_eq!(
|
||||
workspace
|
||||
.read(cx)
|
||||
.active_pane()
|
||||
.read(cx)
|
||||
.active_item()
|
||||
.unwrap()
|
||||
.title(cx),
|
||||
"a.txt"
|
||||
);
|
||||
});
|
||||
|
||||
// Open a file outside of any existing worktree.
|
||||
cx.update(|cx| {
|
||||
workspace.update(cx, |view, cx| view.open_paths(&["/dir2/b.txt".into()], cx))
|
||||
})
|
||||
.await;
|
||||
cx.read(|cx| {
|
||||
let worktree_roots = workspace
|
||||
.read(cx)
|
||||
.worktrees(cx)
|
||||
.iter()
|
||||
.map(|w| w.read(cx).as_local().unwrap().abs_path())
|
||||
.collect::<HashSet<_>>();
|
||||
assert_eq!(
|
||||
worktree_roots,
|
||||
vec!["/dir1", "/dir2/b.txt"]
|
||||
.into_iter()
|
||||
.map(Path::new)
|
||||
.collect(),
|
||||
);
|
||||
assert_eq!(
|
||||
workspace
|
||||
.read(cx)
|
||||
.active_pane()
|
||||
.read(cx)
|
||||
.active_item()
|
||||
.unwrap()
|
||||
.title(cx),
|
||||
"b.txt"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_save_conflicting_item(mut cx: gpui::TestAppContext) {
|
||||
let app_state = cx.update(test_app_state);
|
||||
let fs = app_state.fs.as_fake();
|
||||
fs.insert_tree("/root", json!({ "a.txt": "" })).await;
|
||||
|
||||
let (window_id, workspace) =
|
||||
cx.add_window(|cx| Workspace::new(&app_state.as_ref().into(), cx));
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
workspace.add_worktree(Path::new("/root"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Open a file within an existing worktree.
|
||||
cx.update(|cx| {
|
||||
workspace.update(cx, |view, cx| {
|
||||
view.open_paths(&[PathBuf::from("/root/a.txt")], cx)
|
||||
})
|
||||
})
|
||||
.await;
|
||||
let editor = cx.read(|cx| {
|
||||
let pane = workspace.read(cx).active_pane().read(cx);
|
||||
let item = pane.active_item().unwrap();
|
||||
item.to_any().downcast::<Editor>().unwrap()
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.handle_input(&editor::Input("x".into()), cx)
|
||||
})
|
||||
});
|
||||
fs.insert_file("/root/a.txt", "changed".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
editor
|
||||
.condition(&cx, |editor, cx| editor.has_conflict(cx))
|
||||
.await;
|
||||
cx.read(|cx| assert!(editor.is_dirty(cx)));
|
||||
|
||||
cx.update(|cx| workspace.update(cx, |w, cx| w.save_active_item(&workspace::Save, cx)));
|
||||
cx.simulate_prompt_answer(window_id, 0);
|
||||
editor
|
||||
.condition(&cx, |editor, cx| !editor.is_dirty(cx))
|
||||
.await;
|
||||
cx.read(|cx| assert!(!editor.has_conflict(cx)));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_open_and_save_new_file(mut cx: gpui::TestAppContext) {
|
||||
let app_state = cx.update(test_app_state);
|
||||
app_state.fs.as_fake().insert_dir("/root").await.unwrap();
|
||||
let params = app_state.as_ref().into();
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx));
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
workspace.add_worktree(Path::new("/root"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let worktree = cx.read(|cx| {
|
||||
workspace
|
||||
.read(cx)
|
||||
.worktrees(cx)
|
||||
.iter()
|
||||
.next()
|
||||
.unwrap()
|
||||
.clone()
|
||||
});
|
||||
|
||||
// Create a new untitled buffer
|
||||
cx.dispatch_action(window_id, vec![workspace.id()], OpenNew(params.clone()));
|
||||
let editor = workspace.read_with(&cx, |workspace, cx| {
|
||||
workspace
|
||||
.active_item(cx)
|
||||
.unwrap()
|
||||
.to_any()
|
||||
.downcast::<Editor>()
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert!(!editor.is_dirty(cx.as_ref()));
|
||||
assert_eq!(editor.title(cx.as_ref()), "untitled");
|
||||
assert!(editor.language(cx).is_none());
|
||||
editor.handle_input(&editor::Input("hi".into()), cx);
|
||||
assert!(editor.is_dirty(cx.as_ref()));
|
||||
});
|
||||
|
||||
// Save the buffer. This prompts for a filename.
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
workspace.save_active_item(&workspace::Save, cx)
|
||||
});
|
||||
cx.simulate_new_path_selection(|parent_dir| {
|
||||
assert_eq!(parent_dir, Path::new("/root"));
|
||||
Some(parent_dir.join("the-new-name.rs"))
|
||||
});
|
||||
cx.read(|cx| {
|
||||
assert!(editor.is_dirty(cx));
|
||||
assert_eq!(editor.title(cx), "untitled");
|
||||
});
|
||||
|
||||
// When the save completes, the buffer's title is updated.
|
||||
editor
|
||||
.condition(&cx, |editor, cx| !editor.is_dirty(cx))
|
||||
.await;
|
||||
cx.read(|cx| {
|
||||
assert!(!editor.is_dirty(cx));
|
||||
assert_eq!(editor.title(cx), "the-new-name.rs");
|
||||
});
|
||||
// The language is assigned based on the path
|
||||
editor.read_with(&cx, |editor, cx| {
|
||||
assert_eq!(editor.language(cx).unwrap().name(), "Rust")
|
||||
});
|
||||
|
||||
// Edit the file and save it again. This time, there is no filename prompt.
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
editor.handle_input(&editor::Input(" there".into()), cx);
|
||||
assert_eq!(editor.is_dirty(cx.as_ref()), true);
|
||||
});
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
workspace.save_active_item(&workspace::Save, cx)
|
||||
});
|
||||
assert!(!cx.did_prompt_for_new_path());
|
||||
editor
|
||||
.condition(&cx, |editor, cx| !editor.is_dirty(cx))
|
||||
.await;
|
||||
cx.read(|cx| assert_eq!(editor.title(cx), "the-new-name.rs"));
|
||||
|
||||
// Open the same newly-created file in another pane item. The new editor should reuse
|
||||
// the same buffer.
|
||||
cx.dispatch_action(window_id, vec![workspace.id()], OpenNew(params.clone()));
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
|
||||
assert!(workspace
|
||||
.open_entry(
|
||||
ProjectPath {
|
||||
worktree_id: worktree.id(),
|
||||
path: Path::new("the-new-name.rs").into()
|
||||
},
|
||||
cx
|
||||
)
|
||||
.is_none());
|
||||
});
|
||||
let editor2 = workspace.update(&mut cx, |workspace, cx| {
|
||||
workspace
|
||||
.active_item(cx)
|
||||
.unwrap()
|
||||
.to_any()
|
||||
.downcast::<Editor>()
|
||||
.unwrap()
|
||||
});
|
||||
cx.read(|cx| {
|
||||
assert_eq!(editor2.read(cx).buffer(), editor.read(cx).buffer());
|
||||
})
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_setting_language_when_saving_as_single_file_worktree(
|
||||
mut cx: gpui::TestAppContext,
|
||||
) {
|
||||
let app_state = cx.update(test_app_state);
|
||||
app_state.fs.as_fake().insert_dir("/root").await.unwrap();
|
||||
let params = app_state.as_ref().into();
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx));
|
||||
|
||||
// Create a new untitled buffer
|
||||
cx.dispatch_action(window_id, vec![workspace.id()], OpenNew(params.clone()));
|
||||
let editor = workspace.read_with(&cx, |workspace, cx| {
|
||||
workspace
|
||||
.active_item(cx)
|
||||
.unwrap()
|
||||
.to_any()
|
||||
.downcast::<Editor>()
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert!(editor.language(cx).is_none());
|
||||
editor.handle_input(&editor::Input("hi".into()), cx);
|
||||
assert!(editor.is_dirty(cx.as_ref()));
|
||||
});
|
||||
|
||||
// Save the buffer. This prompts for a filename.
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
workspace.save_active_item(&workspace::Save, cx)
|
||||
});
|
||||
cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs")));
|
||||
|
||||
editor
|
||||
.condition(&cx, |editor, cx| !editor.is_dirty(cx))
|
||||
.await;
|
||||
|
||||
// The language is assigned based on the path
|
||||
editor.read_with(&cx, |editor, cx| {
|
||||
assert_eq!(editor.language(cx).unwrap().name(), "Rust")
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_pane_actions(mut cx: gpui::TestAppContext) {
|
||||
cx.update(|cx| pane::init(cx));
|
||||
let app_state = cx.update(test_app_state);
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"a": {
|
||||
"file1": "contents 1",
|
||||
"file2": "contents 2",
|
||||
"file3": "contents 3",
|
||||
},
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let (window_id, workspace) =
|
||||
cx.add_window(|cx| Workspace::new(&app_state.as_ref().into(), cx));
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
workspace.add_worktree(Path::new("/root"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
|
||||
.await;
|
||||
let entries = cx.read(|cx| workspace.file_project_paths(cx));
|
||||
let file1 = entries[0].clone();
|
||||
|
||||
let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone());
|
||||
|
||||
workspace
|
||||
.update(&mut cx, |w, cx| w.open_entry(file1.clone(), cx))
|
||||
.unwrap()
|
||||
.await;
|
||||
cx.read(|cx| {
|
||||
assert_eq!(
|
||||
pane_1.read(cx).active_item().unwrap().project_path(cx),
|
||||
Some(file1.clone())
|
||||
);
|
||||
});
|
||||
|
||||
cx.dispatch_action(
|
||||
window_id,
|
||||
vec![pane_1.id()],
|
||||
pane::Split(SplitDirection::Right),
|
||||
);
|
||||
cx.update(|cx| {
|
||||
let pane_2 = workspace.read(cx).active_pane().clone();
|
||||
assert_ne!(pane_1, pane_2);
|
||||
|
||||
let pane2_item = pane_2.read(cx).active_item().unwrap();
|
||||
assert_eq!(pane2_item.project_path(cx.as_ref()), Some(file1.clone()));
|
||||
|
||||
cx.dispatch_action(window_id, vec![pane_2.id()], &workspace::CloseActiveItem);
|
||||
let workspace = workspace.read(cx);
|
||||
assert_eq!(workspace.panes().len(), 1);
|
||||
assert_eq!(workspace.active_pane(), &pane_1);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_bundled_themes(cx: &mut MutableAppContext) {
|
||||
let app_state = test_app_state(cx);
|
||||
|
||||
+11
-8
@@ -33,6 +33,16 @@ fn main() {
|
||||
let client = client::Client::new();
|
||||
let http = http::client();
|
||||
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
|
||||
let mut entry_openers = Vec::new();
|
||||
|
||||
client::init(client.clone(), cx);
|
||||
workspace::init(cx);
|
||||
editor::init(cx, &mut entry_openers);
|
||||
file_finder::init(cx);
|
||||
people_panel::init(cx);
|
||||
chat_panel::init(cx);
|
||||
project_panel::init(cx);
|
||||
|
||||
let app_state = Arc::new(AppState {
|
||||
languages: languages.clone(),
|
||||
settings_tx: Arc::new(Mutex::new(settings_tx)),
|
||||
@@ -43,16 +53,9 @@ fn main() {
|
||||
client,
|
||||
user_store,
|
||||
fs: Arc::new(RealFs),
|
||||
entry_openers: Arc::from(entry_openers),
|
||||
});
|
||||
|
||||
zed::init(&app_state, cx);
|
||||
client::init(app_state.client.clone(), cx);
|
||||
workspace::init(cx);
|
||||
editor::init(cx);
|
||||
file_finder::init(cx);
|
||||
people_panel::init(cx);
|
||||
chat_panel::init(cx);
|
||||
project_panel::init(cx);
|
||||
theme_selector::init(app_state.as_ref().into(), cx);
|
||||
|
||||
cx.set_menus(menus::menus(&app_state.clone()));
|
||||
|
||||
@@ -11,6 +11,7 @@ pub fn menus(state: &Arc<AppState>) -> Vec<Menu<'static>> {
|
||||
settings: state.settings.clone(),
|
||||
user_store: state.user_store.clone(),
|
||||
channel_list: state.channel_list.clone(),
|
||||
entry_openers: state.entry_openers.clone(),
|
||||
};
|
||||
|
||||
vec![
|
||||
|
||||
+13
-1
@@ -16,20 +16,32 @@ fn init_logger() {
|
||||
}
|
||||
|
||||
pub fn test_app_state(cx: &mut MutableAppContext) -> Arc<AppState> {
|
||||
let mut entry_openers = Vec::new();
|
||||
editor::init(cx, &mut entry_openers);
|
||||
let (settings_tx, settings) = watch::channel_with(build_settings(cx));
|
||||
let themes = ThemeRegistry::new(Assets, cx.font_cache().clone());
|
||||
let client = Client::new();
|
||||
let http = FakeHttpClient::new(|_| async move { Ok(ServerResponse::new(404)) });
|
||||
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
|
||||
let mut languages = LanguageRegistry::new();
|
||||
languages.add(Arc::new(language::Language::new(
|
||||
language::LanguageConfig {
|
||||
name: "Rust".to_string(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
tree_sitter_rust::language(),
|
||||
)));
|
||||
Arc::new(AppState {
|
||||
settings_tx: Arc::new(Mutex::new(settings_tx)),
|
||||
settings,
|
||||
themes,
|
||||
languages: Arc::new(LanguageRegistry::new()),
|
||||
languages: Arc::new(languages),
|
||||
channel_list: cx.add_model(|cx| ChannelList::new(user_store.clone(), client.clone(), cx)),
|
||||
client,
|
||||
user_store,
|
||||
fs: Arc::new(FakeFs::new()),
|
||||
entry_openers: Arc::from(entry_openers),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user