From 9bb195e17739825015c8e0c2eb98d89865e0f9a8 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 24 Nov 2021 15:18:15 +0100 Subject: [PATCH 1/5] Introduce "entry openers" but still register editors in `workspace` --- Cargo.lock | 1 + crates/editor/Cargo.toml | 1 + crates/workspace/src/lib.rs | 63 +++++++++++++++++++++++++++++-------- crates/zed/src/lib.rs | 2 ++ crates/zed/src/main.rs | 19 ++++++----- crates/zed/src/menus.rs | 1 + crates/zed/src/test.rs | 1 + 7 files changed, 67 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7b653eeed43a5cf723f37e229c549c3204105713..6ade8ba1a03351b12d818d7550f61101f9bf139a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1532,6 +1532,7 @@ dependencies = [ "log", "parking_lot", "postage", + "project", "rand 0.8.3", "serde", "smallvec", diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index 7f5ffec97ee79f7bb5761feb521ef80fefc7e26c..32bac7b7a01488757c0a75307796ddeda4020c52 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -15,6 +15,7 @@ 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" } diff --git a/crates/workspace/src/lib.rs b/crates/workspace/src/lib.rs index 0ca07d2246b19ac125284dba4403719fde05f10d..1073ea96a508e4fddb48aa4484b8e1679ca02a91 100644 --- a/crates/workspace/src/lib.rs +++ b/crates/workspace/src/lib.rs @@ -5,12 +5,12 @@ 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 log::error; @@ -33,7 +33,28 @@ action!(OpenNew, WorkspaceParams); action!(Save); action!(DebugElements); -pub fn init(cx: &mut MutableAppContext) { +struct BufferOpener; + +impl EntryOpener for BufferOpener { + fn open( + &self, + worktree: &mut Worktree, + project_path: ProjectPath, + cx: &mut ModelContext, + ) -> Option>>> { + let buffer = worktree.open_buffer(project_path.path, cx); + let task = cx.spawn(|_, _| async move { + buffer + .await + .map(|buffer| Box::new(buffer) as Box) + }); + Some(task) + } +} + +pub fn init(cx: &mut MutableAppContext, entry_openers: &mut Vec>) { + entry_openers.push(Box::new(BufferOpener)); + cx.add_action(Workspace::save_active_item); cx.add_action(Workspace::debug_elements); cx.add_action(Workspace::open_new_file); @@ -62,6 +83,15 @@ pub fn init(cx: &mut MutableAppContext) { pane::init(cx); } +pub trait EntryOpener { + fn open( + &self, + worktree: &mut Worktree, + path: ProjectPath, + cx: &mut ModelContext, + ) -> Option>>>; +} + pub trait Item: Entity + Sized { type View: ItemView; @@ -268,6 +298,7 @@ pub struct WorkspaceParams { pub settings: watch::Receiver, pub user_store: ModelHandle, pub channel_list: ModelHandle, + pub entry_openers: Arc<[Box]>, } impl WorkspaceParams { @@ -299,6 +330,7 @@ impl WorkspaceParams { languages: Arc::new(languages), settings: watch::channel_with(settings).1, user_store, + entry_openers: Arc::from([]), } } } @@ -316,6 +348,7 @@ pub struct Workspace { active_pane: ViewHandle, status_bar: ViewHandle, project: ModelHandle, + entry_openers: Arc<[Box]>, items: Vec>, loading_items: HashMap< ProjectPath, @@ -388,6 +421,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, @@ -600,18 +634,21 @@ 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) - .map_err(Arc::new), - ); + }); + *tx.borrow_mut() = Some(item.await.map_err(Arc::new)); }) .detach(); } diff --git a/crates/zed/src/lib.rs b/crates/zed/src/lib.rs index 4590230851da2bb43aff41f5fa69e0bdf5e95cac..cbc8ef803da04f090f779b9855f022a1904bc781 100644 --- a/crates/zed/src/lib.rs +++ b/crates/zed/src/lib.rs @@ -45,6 +45,7 @@ pub struct AppState { pub user_store: ModelHandle, pub fs: Arc, pub channel_list: ModelHandle, + pub entry_openers: Arc<[Box]>, } #[derive(Clone)] @@ -185,6 +186,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(), } } } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index d2357ea3c37ea2a0476957918eee27f001888972..8dd2dc53e684a963b793b8ece4ee827d21d9ad74 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -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, &mut entry_openers); + editor::init(cx); + 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())); diff --git a/crates/zed/src/menus.rs b/crates/zed/src/menus.rs index 52557e3904d90bc2090e59e0cdc5db6a5ca275f0..25a4b5e6f96d85683b05dbd93819866e683c4d86 100644 --- a/crates/zed/src/menus.rs +++ b/crates/zed/src/menus.rs @@ -11,6 +11,7 @@ pub fn menus(state: &Arc) -> Vec> { settings: state.settings.clone(), user_store: state.user_store.clone(), channel_list: state.channel_list.clone(), + entry_openers: state.entry_openers.clone(), }; vec![ diff --git a/crates/zed/src/test.rs b/crates/zed/src/test.rs index 8a7a3989100bbea4bc55b862f526f3749f1a8b73..f868ab704b47e1cd6eb9540ba2d2f44bd52fbf6b 100644 --- a/crates/zed/src/test.rs +++ b/crates/zed/src/test.rs @@ -30,6 +30,7 @@ pub fn test_app_state(cx: &mut MutableAppContext) -> Arc { client, user_store, fs: Arc::new(FakeFs::new()), + entry_openers: Arc::from([]), }) } From 03bd6d6c333c76d806f36075155eab866174525f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 24 Nov 2021 16:04:24 +0100 Subject: [PATCH 2/5] Pull up diagnostic and cursor position status bar items creation --- crates/workspace/Cargo.toml | 1 + crates/workspace/src/lib.rs | 19 +++++++------------ crates/zed/src/lib.rs | 10 ++++++++++ 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index a96eb23aafb9ce40687611e19d1c5f59576275af..9368f822aec939e5c9f9c06f10a8c010ca4cc558 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -27,6 +27,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" diff --git a/crates/workspace/src/lib.rs b/crates/workspace/src/lib.rs index 1073ea96a508e4fddb48aa4484b8e1679ca02a91..d98d07a75ef24a3e1f4578a311f64b1bcb5faaff 100644 --- a/crates/workspace/src/lib.rs +++ b/crates/workspace/src/lib.rs @@ -1,4 +1,4 @@ -mod items; +pub mod items; pub mod pane; pub mod pane_group; pub mod settings; @@ -20,6 +20,7 @@ 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; 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); @@ -382,15 +381,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 { @@ -436,6 +427,10 @@ impl Workspace { &mut self.right_sidebar } + pub fn status_bar(&self) -> &ViewHandle { + &self.status_bar + } + pub fn project(&self) -> &ModelHandle { &self.project } diff --git a/crates/zed/src/lib.rs b/crates/zed/src/lib.rs index cbc8ef803da04f090f779b9855f022a1904bc781..d3fed8c524573f0be7dac3e4014688f25013545f 100644 --- a/crates/zed/src/lib.rs +++ b/crates/zed/src/lib.rs @@ -161,6 +161,16 @@ fn build_workspace(params: &WorkspaceParams, cx: &mut ViewContext) -> }) .into(), ); + + let diagnostic = + cx.add_view(|_| workspace::items::DiagnosticMessage::new(params.settings.clone())); + let cursor_position = + cx.add_view(|_| workspace::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 } From 2cf44d30b7205e0d0cb52f3b2c03285853128d1c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 24 Nov 2021 16:17:41 +0100 Subject: [PATCH 3/5] :fire: Co-Authored-By: Nathan Sobo --- crates/workspace/src/lib.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/workspace/src/lib.rs b/crates/workspace/src/lib.rs index d98d07a75ef24a3e1f4578a311f64b1bcb5faaff..3eff1add2def2076008a934bf0d45fcb14ff54fd 100644 --- a/crates/workspace/src/lib.rs +++ b/crates/workspace/src/lib.rs @@ -610,8 +610,6 @@ impl Workspace { return None; } - // let (worktree_id, path) = project_path.clone(); - let worktree = match self .project .read(cx) From e88d3bb97e4320bbe38e063e0838f39f8b5c0c5a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 24 Nov 2021 17:07:16 +0100 Subject: [PATCH 4/5] Invert dependency between `editor` and `workspace` Co-Authored-By: Nathan Sobo --- Cargo.lock | 3 +- crates/editor/Cargo.toml | 1 + crates/{workspace => editor}/src/items.rs | 134 ++- crates/editor/src/lib.rs | 16 +- crates/file_finder/Cargo.toml | 1 + crates/file_finder/src/lib.rs | 13 +- crates/workspace/Cargo.toml | 2 - crates/workspace/src/lib.rs | 1006 ++++++++++----------- crates/zed/src/lib.rs | 12 +- crates/zed/src/main.rs | 4 +- 10 files changed, 618 insertions(+), 574 deletions(-) rename crates/{workspace => editor}/src/items.rs (72%) diff --git a/Cargo.lock b/Cargo.lock index 6ade8ba1a03351b12d818d7550f61101f9bf139a..c500ab6d9d6d48fb0421cc3a3bc66bb6871b1c8a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1543,6 +1543,7 @@ dependencies = [ "tree-sitter-rust", "unindent", "util", + "workspace", ] [[package]] @@ -5612,9 +5613,7 @@ name = "workspace" version = "0.1.0" dependencies = [ "anyhow", - "buffer", "client", - "editor", "gpui", "language", "log", diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index 32bac7b7a01488757c0a75307796ddeda4020c52..691eaa27d33011e64964d4ec47431c1ad0d4bd36 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -19,6 +19,7 @@ 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" diff --git a/crates/workspace/src/items.rs b/crates/editor/src/items.rs similarity index 72% rename from crates/workspace/src/items.rs rename to crates/editor/src/items.rs index 58358cdf4722e1a913dfede81b61e419e425dcdb..7812af7820e3d708a44ad3431d177e562166dc94 100644 --- a/crates/workspace/src/items.rs +++ b/crates/editor/src/items.rs @@ -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; + +#[derive(Clone)] +pub struct BufferItemHandle(pub ModelHandle); + +#[derive(Clone)] +struct WeakBufferItemHandle(WeakModelHandle); + +impl EntryOpener for BufferOpener { + fn open( + &self, + worktree: &mut Worktree, + project_path: ProjectPath, + cx: &mut ModelContext, + ) -> Option>>> { + 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) + }); + Some(task) + } +} - fn build_view( - handle: ModelHandle, +impl ItemHandle for BufferItemHandle { + fn add_view( + &self, + window_id: usize, settings: watch::Receiver, - cx: &mut ViewContext, - ) -> 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; - - 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, - ) + cx: &mut MutableAppContext, + ) -> Box { + 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, + ) + })) + } + + fn boxed_clone(&self) -> Box { + Box::new(self.clone()) } - fn project_path(&self) -> Option { - self.file().map(|f| ProjectPath { + fn downgrade(&self) -> Box { + Box::new(WeakBufferItemHandle(self.0.downgrade())) + } + + fn project_path(&self, cx: &AppContext) -> Option { + 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> { + self.0 + .upgrade(cx) + .map(|buffer| Box::new(BufferItemHandle(buffer)) as Box) + } +} + 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, ) { if let Some(editor) = active_pane_item.and_then(|item| item.to_any().downcast::()) { @@ -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, ) { if let Some(editor) = active_pane_item.and_then(|item| item.to_any().downcast::()) { diff --git a/crates/editor/src/lib.rs b/crates/editor/src/lib.rs index 056703f523aee0c3515de4aa83333f424bc73059..0eefe2e6b17d6bf5ccaedbed374be9c89e68a100 100644 --- a/crates/editor/src/lib.rs +++ b/crates/editor/src/lib.rs @@ -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>) { + 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, + ) { + 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() } diff --git a/crates/file_finder/Cargo.toml b/crates/file_finder/Cargo.toml index 70267b1d194eb7dc478571c838c35abfbd6c3a67..81995e2e7aedec1ac758a21857587c3854deb067 100644 --- a/crates/file_finder/Cargo.toml +++ b/crates/file_finder/Cargo.toml @@ -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"] } diff --git a/crates/file_finder/src/lib.rs b/crates/file_finder/src/lib.rs index bd52023de8a9c641551b3f16c918f03f23f45c28..7e000f957c595fbba41b1d5bf22882894e6315d3 100644 --- a/crates/file_finder/src/lib.rs +++ b/crates/file_finder/src/lib.rs @@ -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 diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 9368f822aec939e5c9f9c06f10a8c010ca4cc558..b20ab232f90a4b917756fda4b083f6f2592dbf03 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -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" } diff --git a/crates/workspace/src/lib.rs b/crates/workspace/src/lib.rs index 3eff1add2def2076008a934bf0d45fcb14ff54fd..0c3d3c0ba2de6edf236fd32454686b98ebbace70 100644 --- a/crates/workspace/src/lib.rs +++ b/crates/workspace/src/lib.rs @@ -1,4 +1,3 @@ -pub mod items; pub mod pane; pub mod pane_group; pub mod settings; @@ -12,7 +11,7 @@ use gpui::{ 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::*; @@ -21,6 +20,7 @@ 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, @@ -32,31 +32,9 @@ action!(OpenNew, WorkspaceParams); action!(Save); action!(DebugElements); -struct BufferOpener; - -impl EntryOpener for BufferOpener { - fn open( - &self, - worktree: &mut Worktree, - project_path: ProjectPath, - cx: &mut ModelContext, - ) -> Option>>> { - let buffer = worktree.open_buffer(project_path.path, cx); - let task = cx.spawn(|_, _| async move { - buffer - .await - .map(|buffer| Box::new(buffer) as Box) - }); - Some(task) - } -} - -pub fn init(cx: &mut MutableAppContext, entry_openers: &mut Vec>) { - entry_openers.push(Box::new(BufferOpener)); - +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![ @@ -137,21 +115,21 @@ pub trait ItemView: View { } pub trait ItemHandle: Send + Sync { - fn boxed_clone(&self) -> Box; - fn downgrade(&self) -> Box; -} - -pub trait WeakItemHandle { fn add_view( &self, window_id: usize, settings: watch::Receiver, cx: &mut MutableAppContext, - ) -> Option>; - fn alive(&self, cx: &AppContext) -> bool; + ) -> Box; + fn boxed_clone(&self) -> Box; + fn downgrade(&self) -> Box; fn project_path(&self, cx: &AppContext) -> Option; } +pub trait WeakItemHandle { + fn upgrade(&self, cx: &AppContext) -> Option>; +} + pub trait ItemViewHandle { fn title(&self, cx: &AppContext) -> String; fn project_path(&self, cx: &AppContext) -> Option; @@ -172,6 +150,15 @@ pub trait ItemViewHandle { } impl ItemHandle for ModelHandle { + fn add_view( + &self, + window_id: usize, + settings: watch::Receiver, + cx: &mut MutableAppContext, + ) -> Box { + Box::new(cx.add_view(window_id, |cx| T::build_view(self.clone(), settings, cx))) + } + fn boxed_clone(&self) -> Box { Box::new(self.clone()) } @@ -179,30 +166,38 @@ impl ItemHandle for ModelHandle { fn downgrade(&self) -> Box { Box::new(self.downgrade()) } + + fn project_path(&self, cx: &AppContext) -> Option { + self.read(cx).project_path() + } } -impl WeakItemHandle for WeakModelHandle { +impl ItemHandle for Box { fn add_view( &self, window_id: usize, settings: watch::Receiver, cx: &mut MutableAppContext, - ) -> Option> { - 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 { + 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 { + self.as_ref().boxed_clone() + } + + fn downgrade(&self) -> Box { + self.as_ref().downgrade() } fn project_path(&self, cx: &AppContext) -> Option { - self.upgrade(cx).and_then(|h| h.read(cx).project_path()) + self.as_ref().project_path(cx) + } +} + +impl WeakItemHandle for WeakModelHandle { + fn upgrade(&self, cx: &AppContext) -> Option> { + WeakModelHandle::::upgrade(*self, cx).map(|i| Box::new(i) as Box) } } @@ -589,16 +584,6 @@ impl Workspace { } } - pub fn open_new_file(&mut self, _: &OpenNew, cx: &mut ViewContext) { - 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, @@ -647,7 +632,6 @@ impl Workspace { } 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 { @@ -667,12 +651,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) => { @@ -701,16 +680,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 { @@ -866,6 +843,15 @@ impl Workspace { pane } + pub fn add_item(&mut self, item_handle: T, cx: &mut ViewContext) + 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, cx: &mut ViewContext) { self.active_pane = pane; self.status_bar.update(cx, |status_bar, cx| { @@ -1121,447 +1107,447 @@ impl WorkspaceHandle for ViewHandle { } } -#[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::>(); - 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::>(); - 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::().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::() - .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::() - .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::() - .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); - }); - } -} +// #[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::>(); +// 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::>(); +// 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::().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::() +// .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::() +// .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::() +// .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); +// }); +// } +// } diff --git a/crates/zed/src/lib.rs b/crates/zed/src/lib.rs index d3fed8c524573f0be7dac3e4014688f25013545f..0f0b240085eb482fb35c8066d0e603efd64654bc 100644 --- a/crates/zed/src/lib.rs +++ b/crates/zed/src/lib.rs @@ -130,11 +130,9 @@ 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 - }); + 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 { @@ -163,9 +161,9 @@ fn build_workspace(params: &WorkspaceParams, cx: &mut ViewContext) -> ); let diagnostic = - cx.add_view(|_| workspace::items::DiagnosticMessage::new(params.settings.clone())); + cx.add_view(|_| editor::items::DiagnosticMessage::new(params.settings.clone())); let cursor_position = - cx.add_view(|_| workspace::items::CursorPosition::new(params.settings.clone())); + 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); diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 8dd2dc53e684a963b793b8ece4ee827d21d9ad74..23ad33d8bd28702d55d4e6a17a08af02b61796e5 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -36,8 +36,8 @@ fn main() { let mut entry_openers = Vec::new(); client::init(client.clone(), cx); - workspace::init(cx, &mut entry_openers); - editor::init(cx); + workspace::init(cx); + editor::init(cx, &mut entry_openers); file_finder::init(cx); people_panel::init(cx); chat_panel::init(cx); From afdac15572909cbda73690d7f9f27db8909dbcc2 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 24 Nov 2021 17:39:15 +0100 Subject: [PATCH 5/5] Move integration test up into the `zed` crate --- crates/project_panel/Cargo.toml | 2 + crates/server/src/rpc.rs | 22 +- crates/workspace/src/lib.rs | 466 +------------------------------- crates/workspace/src/pane.rs | 1 - crates/zed/src/lib.rs | 454 ++++++++++++++++++++++++++++++- crates/zed/src/test.rs | 15 +- 6 files changed, 486 insertions(+), 474 deletions(-) diff --git a/crates/project_panel/Cargo.toml b/crates/project_panel/Cargo.toml index a682693a49f08d6626bfe2d4f7f02af926c3aa10..180ea078e1c090d664f1cda6fb95e5ba249b526d 100644 --- a/crates/project_panel/Cargo.toml +++ b/crates/project_panel/Cargo.toml @@ -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"] } diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 64463f27230de1f4a17be1d94aa069a2dbfe4795..f35bbb00e6185efdc0589d93be11ed3410c31f7a 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -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, diff --git a/crates/workspace/src/lib.rs b/crates/workspace/src/lib.rs index 0c3d3c0ba2de6edf236fd32454686b98ebbace70..4ec16f45454b4707ad42e9334ac136629387ed47 100644 --- a/crates/workspace/src/lib.rs +++ b/crates/workspace/src/lib.rs @@ -298,16 +298,7 @@ pub struct WorkspaceParams { 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)) @@ -702,7 +693,7 @@ impl Workspace { } } - pub fn active_item(&self, cx: &ViewContext) -> Option> { + pub fn active_item(&self, cx: &AppContext) -> Option> { self.active_pane().read(cx).active_item() } @@ -884,7 +875,7 @@ impl Workspace { } } - fn split_pane( + pub fn split_pane( &mut self, pane: ViewHandle, direction: SplitDirection, @@ -911,6 +902,10 @@ impl Workspace { } } + pub fn panes(&self) -> &[ViewHandle] { + &self.panes + } + fn pane(&self, pane_id: usize) -> Option> { self.panes.iter().find(|pane| pane.id() == pane_id).cloned() } @@ -1085,12 +1080,10 @@ impl View for Workspace { } } -#[cfg(test)] pub trait WorkspaceHandle { fn file_project_paths(&self, cx: &AppContext) -> Vec; } -#[cfg(test)] impl WorkspaceHandle for ViewHandle { fn file_project_paths(&self, cx: &AppContext) -> Vec { self.read(cx) @@ -1106,448 +1099,3 @@ impl WorkspaceHandle for ViewHandle { .collect::>() } } - -// #[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::>(); -// 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::>(); -// 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::().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::() -// .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::() -// .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::() -// .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); -// }); -// } -// } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index ebbc73c3b908d83b2e1bf5d2da058202e05902d6..a13602016abbede33230ee18d791f936606fc314 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -95,7 +95,6 @@ impl Pane { item_idx } - #[cfg(test)] pub fn items(&self) -> &[Box] { &self.items } diff --git a/crates/zed/src/lib.rs b/crates/zed/src/lib.rs index 0f0b240085eb482fb35c8066d0e603efd64654bc..cace44a81605ce23505846475fe44809dc9ce01c 100644 --- a/crates/zed/src/lib.rs +++ b/crates/zed/src/lib.rs @@ -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); @@ -129,7 +129,7 @@ fn open_paths(action: &OpenPaths, cx: &mut MutableAppContext) -> Task<()> { }) } -fn open_new(action: &workspace::OpenNew, cx: &mut MutableAppContext) { +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); @@ -212,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) { @@ -318,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::>(); + 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::>(); + 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::().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::() + .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::() + .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::() + .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); diff --git a/crates/zed/src/test.rs b/crates/zed/src/test.rs index f868ab704b47e1cd6eb9540ba2d2f44bd52fbf6b..5cb9b5f0e8944e0d1c9cdf0067b625729254eb25 100644 --- a/crates/zed/src/test.rs +++ b/crates/zed/src/test.rs @@ -16,21 +16,32 @@ fn init_logger() { } pub fn test_app_state(cx: &mut MutableAppContext) -> Arc { + 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: Arc::from(entry_openers), }) }