Detailed changes
@@ -6031,6 +6031,20 @@ dependencies = [
[[package]]
name = "workspace"
version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "buffer",
+ "client",
+ "editor",
+ "gpui",
+ "log",
+ "postage",
+ "project",
+ "serde_json 1.0.64",
+ "theme",
+ "tree-sitter",
+ "tree-sitter-rust",
+]
[[package]]
name = "wyz"
@@ -6122,6 +6136,7 @@ dependencies = [
"unindent",
"url",
"util",
+ "workspace",
]
[[package]]
@@ -4,14 +4,13 @@ version = "0.1.0"
edition = "2018"
[features]
-test-support = []
+test-support = ["rpc/test-support"]
[dependencies]
gpui = { path = "../gpui" }
util = { path = "../util" }
rpc = { path = "../rpc" }
sum_tree = { path = "../sum_tree" }
-
anyhow = "1.0.38"
async-recursion = "0.3"
async-tungstenite = { version = "0.14", features = ["async-tls"] }
@@ -11,7 +11,7 @@ use async_tungstenite::tungstenite::{
error::Error as WebsocketError,
http::{Request, StatusCode},
};
-use gpui::{AsyncAppContext, Entity, ModelContext, Task};
+use gpui::{action, AsyncAppContext, Entity, ModelContext, MutableAppContext, Task};
use lazy_static::lazy_static;
use parking_lot::RwLock;
use postage::{prelude::Stream, watch};
@@ -28,7 +28,7 @@ use std::{
};
use surf::Url;
use thiserror::Error;
-use util::ResultExt;
+use util::{ResultExt, TryFutureExt};
pub use channel::*;
pub use rpc::*;
@@ -42,6 +42,16 @@ lazy_static! {
.and_then(|s| if s.is_empty() { None } else { Some(s) });
}
+action!(Authenticate);
+
+pub fn init(rpc: Arc<Client>, cx: &mut MutableAppContext) {
+ cx.add_global_action(move |_: &Authenticate, cx| {
+ let rpc = rpc.clone();
+ cx.spawn(|cx| async move { rpc.authenticate_and_connect(&cx).log_err().await })
+ .detach();
+ });
+}
+
pub struct Client {
peer: Arc<Peer>,
state: RwLock<ClientState>,
@@ -283,7 +283,7 @@ pub struct EditorSettings {
pub style: EditorStyle,
}
-#[derive(Clone, Deserialize)]
+#[derive(Clone, Deserialize, Default)]
pub struct EditorStyle {
pub text: TextStyle,
#[serde(default)]
@@ -18,7 +18,7 @@ pub struct Label {
highlight_indices: Vec<usize>,
}
-#[derive(Clone, Debug, Deserialize)]
+#[derive(Clone, Debug, Deserialize, Default)]
pub struct LabelStyle {
pub text: TextStyle,
pub highlight_text: Option<TextStyle>,
@@ -167,6 +167,32 @@ impl From<TextStyle> for HighlightStyle {
}
}
+impl Default for TextStyle {
+ fn default() -> Self {
+ FONT_CACHE.with(|font_cache| {
+ let font_cache = font_cache.borrow();
+ let font_cache = font_cache
+ .as_ref()
+ .expect("TextStyle::default can only be called within a call to with_font_cache");
+
+ let font_family_name = Arc::from("Courier");
+ let font_family_id = font_cache.load_family(&[&font_family_name]).unwrap();
+ let font_id = font_cache
+ .select_font(font_family_id, &Default::default())
+ .unwrap();
+ Self {
+ color: Default::default(),
+ font_family_name,
+ font_family_id,
+ font_id,
+ font_size: 14.,
+ font_properties: Default::default(),
+ underline: Default::default(),
+ }
+ })
+ }
+}
+
impl HighlightStyle {
fn from_json(json: HighlightStyleJson) -> Self {
let font_properties = properties_from_json(json.weight, json.italic);
@@ -985,7 +985,7 @@ mod tests {
fs::{FakeFs, Fs as _},
people_panel::JoinWorktree,
project::{ProjectPath, Worktree},
- workspace::Workspace,
+ workspace::{Workspace, WorkspaceParams},
};
#[gpui::test]
@@ -1102,13 +1102,9 @@ mod tests {
let mut server = TestServer::start().await;
let (client_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 app_state_b = zed::AppState {
- client: client_b,
- user_store: user_store_b,
- ..Arc::try_unwrap(cx_b.update(zed::test::test_app_state))
- .ok()
- .unwrap()
- };
+ let mut workspace_b_params = cx_b.update(WorkspaceParams::test);
+ workspace_b_params.client = client_b;
+ workspace_b_params.user_store = user_store_b;
cx_a.foreground().forbid_parking();
@@ -1141,7 +1137,7 @@ mod tests {
.await
.unwrap();
- let (window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(&app_state_b, cx));
+ let (window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(&workspace_b_params, cx));
cx_b.update(|cx| {
cx.dispatch_action(
window_b,
@@ -14,7 +14,7 @@ pub use theme_registry::*;
pub const DEFAULT_THEME_NAME: &'static str = "black";
-#[derive(Deserialize)]
+#[derive(Deserialize, Default)]
pub struct Theme {
#[serde(default)]
pub name: String,
@@ -26,7 +26,7 @@ pub struct Theme {
pub editor: EditorStyle,
}
-#[derive(Deserialize)]
+#[derive(Deserialize, Default)]
pub struct Workspace {
pub background: Color,
pub titlebar: Titlebar,
@@ -37,7 +37,7 @@ pub struct Workspace {
pub right_sidebar: Sidebar,
}
-#[derive(Clone, Deserialize)]
+#[derive(Clone, Deserialize, Default)]
pub struct Titlebar {
#[serde(flatten)]
pub container: ContainerStyle,
@@ -49,14 +49,14 @@ pub struct Titlebar {
pub outdated_warning: ContainedText,
}
-#[derive(Clone, Deserialize)]
+#[derive(Clone, Deserialize, Default)]
pub struct OfflineIcon {
#[serde(flatten)]
pub container: ContainerStyle,
pub width: f32,
}
-#[derive(Clone, Deserialize)]
+#[derive(Clone, Deserialize, Default)]
pub struct Tab {
pub height: f32,
#[serde(flatten)]
@@ -71,7 +71,7 @@ pub struct Tab {
pub icon_conflict: Color,
}
-#[derive(Deserialize)]
+#[derive(Deserialize, Default)]
pub struct Sidebar {
#[serde(flatten)]
pub container: ContainerStyle,
@@ -81,14 +81,14 @@ pub struct Sidebar {
pub resize_handle: ContainerStyle,
}
-#[derive(Deserialize)]
+#[derive(Deserialize, Default)]
pub struct SidebarItem {
pub icon_color: Color,
pub icon_size: f32,
pub height: f32,
}
-#[derive(Deserialize)]
+#[derive(Deserialize, Default)]
pub struct ChatPanel {
#[serde(flatten)]
pub container: ContainerStyle,
@@ -100,7 +100,7 @@ pub struct ChatPanel {
pub hovered_sign_in_prompt: TextStyle,
}
-#[derive(Debug, Deserialize)]
+#[derive(Debug, Deserialize, Default)]
pub struct ProjectPanel {
#[serde(flatten)]
pub container: ContainerStyle,
@@ -110,7 +110,7 @@ pub struct ProjectPanel {
pub hovered_selected_entry: ProjectPanelEntry,
}
-#[derive(Debug, Deserialize)]
+#[derive(Debug, Deserialize, Default)]
pub struct ProjectPanelEntry {
pub height: f32,
#[serde(flatten)]
@@ -121,7 +121,7 @@ pub struct ProjectPanelEntry {
pub icon_spacing: f32,
}
-#[derive(Deserialize)]
+#[derive(Deserialize, Default)]
pub struct PeoplePanel {
#[serde(flatten)]
pub container: ContainerStyle,
@@ -136,7 +136,7 @@ pub struct PeoplePanel {
pub hovered_unshared_worktree: WorktreeRow,
}
-#[derive(Deserialize)]
+#[derive(Deserialize, Default)]
pub struct WorktreeRow {
#[serde(flatten)]
pub container: ContainerStyle,
@@ -146,7 +146,7 @@ pub struct WorktreeRow {
pub guest_avatar_spacing: f32,
}
-#[derive(Deserialize)]
+#[derive(Deserialize, Default)]
pub struct ChatMessage {
#[serde(flatten)]
pub container: ContainerStyle,
@@ -155,7 +155,7 @@ pub struct ChatMessage {
pub timestamp: ContainedText,
}
-#[derive(Deserialize)]
+#[derive(Deserialize, Default)]
pub struct ChannelSelect {
#[serde(flatten)]
pub container: ContainerStyle,
@@ -167,7 +167,7 @@ pub struct ChannelSelect {
pub menu: ContainerStyle,
}
-#[derive(Deserialize)]
+#[derive(Deserialize, Default)]
pub struct ChannelName {
#[serde(flatten)]
pub container: ContainerStyle,
@@ -175,7 +175,7 @@ pub struct ChannelName {
pub name: TextStyle,
}
-#[derive(Deserialize)]
+#[derive(Deserialize, Default)]
pub struct Selector {
#[serde(flatten)]
pub container: ContainerStyle,
@@ -185,7 +185,7 @@ pub struct Selector {
pub active_item: ContainedLabel,
}
-#[derive(Clone, Debug, Deserialize)]
+#[derive(Clone, Debug, Deserialize, Default)]
pub struct ContainedText {
#[serde(flatten)]
pub container: ContainerStyle,
@@ -193,7 +193,7 @@ pub struct ContainedText {
pub text: TextStyle,
}
-#[derive(Deserialize)]
+#[derive(Deserialize, Default)]
pub struct ContainedLabel {
#[serde(flatten)]
pub container: ContainerStyle,
@@ -201,7 +201,7 @@ pub struct ContainedLabel {
pub label: LabelStyle,
}
-#[derive(Clone, Deserialize)]
+#[derive(Clone, Deserialize, Default)]
pub struct InputEditorStyle {
#[serde(flatten)]
pub container: ContainerStyle,
@@ -2,3 +2,31 @@
name = "workspace"
version = "0.1.0"
edition = "2018"
+
+[features]
+test-support = [
+ "client/test-support",
+ "project/test-support",
+ "tree-sitter",
+ "tree-sitter-rust"
+]
+
+[dependencies]
+buffer = { path = "../buffer" }
+client = { path = "../client" }
+editor = { path = "../editor" }
+gpui = { path = "../gpui" }
+project = { path = "../project" }
+theme = { path = "../theme" }
+anyhow = "1.0.38"
+log = "0.4"
+postage = { version = "0.4.1", features = ["futures-traits"] }
+tree-sitter = { version = "0.19.5", optional = true }
+tree-sitter-rust = { version = "0.19.0", optional = true }
+
+[dev-dependencies]
+client = { path = "../client", features = ["test-support"] }
+project = { path = "../project", features = ["test-support"] }
+serde_json = { version = "1.0.64", features = ["preserve_order"] }
+tree-sitter = "0.19.5"
+tree-sitter-rust = "0.19.0"
@@ -1,11 +1,11 @@
use super::{Item, ItemView};
-use crate::{project::ProjectPath, Settings};
+use crate::Settings;
use anyhow::Result;
use buffer::{Buffer, File as _};
use editor::{Editor, EditorSettings, Event};
use gpui::{fonts::TextStyle, AppContext, ModelHandle, Task, ViewContext};
use postage::watch;
-use project::Worktree;
+use project::{ProjectPath, Worktree};
use std::path::Path;
impl Item for Buffer {
@@ -0,0 +1,1517 @@
+mod items;
+pub mod pane;
+pub mod pane_group;
+pub mod settings;
+pub mod sidebar;
+
+use anyhow::Result;
+use buffer::{Buffer, LanguageRegistry};
+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,
+};
+use log::error;
+pub use pane::*;
+pub use pane_group::*;
+use postage::{prelude::Stream, watch};
+use project::{Fs, Project, ProjectPath, Worktree};
+pub use settings::Settings;
+use sidebar::{Side, Sidebar, SidebarItemId, ToggleSidebarItem, ToggleSidebarItemFocus};
+use std::{
+ collections::{hash_map::Entry, HashMap},
+ future::Future,
+ path::{Path, PathBuf},
+ sync::Arc,
+};
+
+action!(OpenNew, WorkspaceParams);
+action!(Save);
+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![
+ Binding::new("cmd-s", Save, None),
+ Binding::new("cmd-alt-i", DebugElements, None),
+ Binding::new(
+ "cmd-shift-!",
+ ToggleSidebarItem(SidebarItemId {
+ side: Side::Left,
+ item_index: 0,
+ }),
+ None,
+ ),
+ Binding::new(
+ "cmd-1",
+ ToggleSidebarItemFocus(SidebarItemId {
+ side: Side::Left,
+ item_index: 0,
+ }),
+ None,
+ ),
+ ]);
+ pane::init(cx);
+}
+
+pub trait Item: Entity + Sized {
+ type View: ItemView;
+
+ fn build_view(
+ handle: ModelHandle<Self>,
+ settings: watch::Receiver<Settings>,
+ cx: &mut ViewContext<Self::View>,
+ ) -> Self::View;
+
+ fn project_path(&self) -> Option<ProjectPath>;
+}
+
+pub trait ItemView: View {
+ fn title(&self, cx: &AppContext) -> String;
+ fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
+ fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
+ where
+ Self: Sized,
+ {
+ None
+ }
+ fn is_dirty(&self, _: &AppContext) -> bool {
+ false
+ }
+ fn has_conflict(&self, _: &AppContext) -> bool {
+ false
+ }
+ fn save(&mut self, cx: &mut ViewContext<Self>) -> Result<Task<Result<()>>>;
+ fn save_as(
+ &mut self,
+ worktree: ModelHandle<Worktree>,
+ path: &Path,
+ cx: &mut ViewContext<Self>,
+ ) -> Task<anyhow::Result<()>>;
+ fn should_activate_item_on_event(_: &Self::Event) -> bool {
+ false
+ }
+ fn should_close_item_on_event(_: &Self::Event) -> bool {
+ false
+ }
+ fn should_update_tab_on_event(_: &Self::Event) -> bool {
+ false
+ }
+}
+
+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;
+ fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
+}
+
+pub trait ItemViewHandle {
+ fn title(&self, cx: &AppContext) -> String;
+ fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
+ fn boxed_clone(&self) -> Box<dyn ItemViewHandle>;
+ fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemViewHandle>>;
+ fn set_parent_pane(&self, pane: &ViewHandle<Pane>, cx: &mut MutableAppContext);
+ fn id(&self) -> usize;
+ fn to_any(&self) -> AnyViewHandle;
+ fn is_dirty(&self, cx: &AppContext) -> bool;
+ fn has_conflict(&self, cx: &AppContext) -> bool;
+ fn save(&self, cx: &mut MutableAppContext) -> Result<Task<Result<()>>>;
+ fn save_as(
+ &self,
+ worktree: ModelHandle<Worktree>,
+ path: &Path,
+ cx: &mut MutableAppContext,
+ ) -> Task<anyhow::Result<()>>;
+}
+
+impl<T: Item> ItemHandle for ModelHandle<T> {
+ fn boxed_clone(&self) -> Box<dyn ItemHandle> {
+ Box::new(self.clone())
+ }
+
+ fn downgrade(&self) -> Box<dyn WeakItemHandle> {
+ Box::new(self.downgrade())
+ }
+}
+
+impl<T: Item> WeakItemHandle for WeakModelHandle<T> {
+ 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
+ }
+ }
+
+ fn alive(&self, cx: &AppContext) -> bool {
+ self.upgrade(cx).is_some()
+ }
+
+ fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
+ self.upgrade(cx).and_then(|h| h.read(cx).project_path())
+ }
+}
+
+impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
+ fn title(&self, cx: &AppContext) -> String {
+ self.read(cx).title(cx)
+ }
+
+ fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
+ self.read(cx).project_path(cx)
+ }
+
+ fn boxed_clone(&self) -> Box<dyn ItemViewHandle> {
+ Box::new(self.clone())
+ }
+
+ fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemViewHandle>> {
+ self.update(cx, |item, cx| {
+ cx.add_option_view(|cx| item.clone_on_split(cx))
+ })
+ .map(|handle| Box::new(handle) as Box<dyn ItemViewHandle>)
+ }
+
+ fn set_parent_pane(&self, pane: &ViewHandle<Pane>, cx: &mut MutableAppContext) {
+ pane.update(cx, |_, cx| {
+ cx.subscribe(self, |pane, item, event, cx| {
+ if T::should_close_item_on_event(event) {
+ pane.close_item(item.id(), cx);
+ return;
+ }
+ if T::should_activate_item_on_event(event) {
+ if let Some(ix) = pane.item_index(&item) {
+ pane.activate_item(ix, cx);
+ pane.activate(cx);
+ }
+ }
+ if T::should_update_tab_on_event(event) {
+ cx.notify()
+ }
+ })
+ .detach();
+ });
+ }
+
+ fn save(&self, cx: &mut MutableAppContext) -> Result<Task<Result<()>>> {
+ self.update(cx, |item, cx| item.save(cx))
+ }
+
+ fn save_as(
+ &self,
+ worktree: ModelHandle<Worktree>,
+ path: &Path,
+ cx: &mut MutableAppContext,
+ ) -> Task<anyhow::Result<()>> {
+ self.update(cx, |item, cx| item.save_as(worktree, path, cx))
+ }
+
+ fn is_dirty(&self, cx: &AppContext) -> bool {
+ self.read(cx).is_dirty(cx)
+ }
+
+ fn has_conflict(&self, cx: &AppContext) -> bool {
+ self.read(cx).has_conflict(cx)
+ }
+
+ fn id(&self) -> usize {
+ self.id()
+ }
+
+ fn to_any(&self) -> AnyViewHandle {
+ self.into()
+ }
+}
+
+impl Clone for Box<dyn ItemViewHandle> {
+ fn clone(&self) -> Box<dyn ItemViewHandle> {
+ self.boxed_clone()
+ }
+}
+
+impl Clone for Box<dyn ItemHandle> {
+ fn clone(&self) -> Box<dyn ItemHandle> {
+ self.boxed_clone()
+ }
+}
+
+#[derive(Clone)]
+pub struct WorkspaceParams {
+ pub client: Arc<Client>,
+ pub fs: Arc<dyn Fs>,
+ pub languages: Arc<LanguageRegistry>,
+ pub settings: watch::Receiver<Settings>,
+ pub user_store: ModelHandle<UserStore>,
+ pub channel_list: ModelHandle<ChannelList>,
+}
+
+impl WorkspaceParams {
+ #[cfg(any(test, feature = "test-support"))]
+ pub fn test(cx: &mut MutableAppContext) -> Self {
+ let grammar = tree_sitter_rust::language();
+ let language = Arc::new(buffer::Language {
+ config: buffer::LanguageConfig {
+ name: "Rust".to_string(),
+ path_suffixes: vec!["rs".to_string()],
+ },
+ brackets_query: tree_sitter::Query::new(grammar, "").unwrap(),
+ highlight_query: tree_sitter::Query::new(grammar, "").unwrap(),
+ highlight_map: Default::default(),
+ grammar,
+ });
+ let mut languages = LanguageRegistry::new();
+ languages.add(language);
+
+ let client = Client::new();
+ let http_client = client::test::FakeHttpClient::new(|_| async move {
+ Ok(client::http::ServerResponse::new(404))
+ });
+ let theme =
+ gpui::fonts::with_font_cache(cx.font_cache().clone(), || theme::Theme::default());
+ let settings = Settings::new("Courier", cx.font_cache(), Arc::new(theme)).unwrap();
+ let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
+ Self {
+ channel_list: cx
+ .add_model(|cx| ChannelList::new(user_store.clone(), client.clone(), cx)),
+ client,
+ fs: Arc::new(project::FakeFs::new()),
+ languages: Arc::new(languages),
+ settings: watch::channel_with(settings).1,
+ user_store,
+ }
+ }
+}
+
+pub struct Workspace {
+ pub settings: watch::Receiver<Settings>,
+ client: Arc<Client>,
+ user_store: ModelHandle<client::UserStore>,
+ fs: Arc<dyn Fs>,
+ modal: Option<AnyViewHandle>,
+ center: PaneGroup,
+ left_sidebar: Sidebar,
+ right_sidebar: Sidebar,
+ panes: Vec<ViewHandle<Pane>>,
+ active_pane: ViewHandle<Pane>,
+ project: ModelHandle<Project>,
+ items: Vec<Box<dyn WeakItemHandle>>,
+ loading_items: HashMap<
+ ProjectPath,
+ postage::watch::Receiver<Option<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>>>,
+ >,
+ _observe_current_user: Task<()>,
+}
+
+impl Workspace {
+ pub fn new(params: &WorkspaceParams, cx: &mut ViewContext<Self>) -> Self {
+ let project = cx.add_model(|_| {
+ Project::new(
+ params.languages.clone(),
+ params.client.clone(),
+ params.fs.clone(),
+ )
+ });
+ cx.observe(&project, |_, _, cx| cx.notify()).detach();
+
+ let pane = cx.add_view(|_| Pane::new(params.settings.clone()));
+ let pane_id = pane.id();
+ cx.observe(&pane, move |me, _, cx| {
+ let active_entry = me.active_project_path(cx);
+ me.project
+ .update(cx, |project, cx| project.set_active_path(active_entry, cx));
+ })
+ .detach();
+ cx.subscribe(&pane, move |me, _, event, cx| {
+ me.handle_pane_event(pane_id, event, cx)
+ })
+ .detach();
+ cx.focus(&pane);
+
+ 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 {
+ current_user.recv().await;
+ connection_status.recv().await;
+ let mut stream =
+ Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
+
+ while stream.recv().await.is_some() {
+ cx.update(|cx| {
+ if let Some(this) = this.upgrade(&cx) {
+ this.update(cx, |_, cx| cx.notify());
+ }
+ })
+ }
+ });
+
+ Workspace {
+ modal: None,
+ center: PaneGroup::new(pane.id()),
+ panes: vec![pane.clone()],
+ active_pane: pane.clone(),
+ settings: params.settings.clone(),
+ client: params.client.clone(),
+ user_store: params.user_store.clone(),
+ fs: params.fs.clone(),
+ left_sidebar: Sidebar::new(Side::Left),
+ right_sidebar: Sidebar::new(Side::Right),
+ project,
+ items: Default::default(),
+ loading_items: Default::default(),
+ _observe_current_user,
+ }
+ }
+
+ pub fn left_sidebar_mut(&mut self) -> &mut Sidebar {
+ &mut self.left_sidebar
+ }
+
+ pub fn right_sidebar_mut(&mut self) -> &mut Sidebar {
+ &mut self.right_sidebar
+ }
+
+ pub fn project(&self) -> &ModelHandle<Project> {
+ &self.project
+ }
+
+ pub fn worktrees<'a>(&self, cx: &'a AppContext) -> &'a [ModelHandle<Worktree>] {
+ &self.project.read(cx).worktrees()
+ }
+
+ pub fn contains_paths(&self, paths: &[PathBuf], cx: &AppContext) -> bool {
+ paths.iter().all(|path| self.contains_path(&path, cx))
+ }
+
+ pub fn contains_path(&self, path: &Path, cx: &AppContext) -> bool {
+ for worktree in self.worktrees(cx) {
+ let worktree = worktree.read(cx).as_local();
+ if worktree.map_or(false, |w| w.contains_abs_path(path)) {
+ return true;
+ }
+ }
+ false
+ }
+
+ pub fn worktree_scans_complete(&self, cx: &AppContext) -> impl Future<Output = ()> + 'static {
+ let futures = self
+ .worktrees(cx)
+ .iter()
+ .filter_map(|worktree| worktree.read(cx).as_local())
+ .map(|worktree| worktree.scan_complete())
+ .collect::<Vec<_>>();
+ async move {
+ for future in futures {
+ future.await;
+ }
+ }
+ }
+
+ pub fn open_paths(&mut self, abs_paths: &[PathBuf], cx: &mut ViewContext<Self>) -> Task<()> {
+ let entries = abs_paths
+ .iter()
+ .cloned()
+ .map(|path| self.project_path_for_path(&path, cx))
+ .collect::<Vec<_>>();
+
+ let fs = self.fs.clone();
+ let tasks = abs_paths
+ .iter()
+ .cloned()
+ .zip(entries.into_iter())
+ .map(|(abs_path, project_path)| {
+ cx.spawn(|this, mut cx| {
+ let fs = fs.clone();
+ async move {
+ let project_path = project_path.await?;
+ if fs.is_file(&abs_path).await {
+ if let Some(entry) =
+ this.update(&mut cx, |this, cx| this.open_entry(project_path, cx))
+ {
+ entry.await;
+ }
+ }
+ Ok(())
+ }
+ })
+ })
+ .collect::<Vec<Task<Result<()>>>>();
+
+ cx.foreground().spawn(async move {
+ for task in tasks {
+ if let Err(error) = task.await {
+ log::error!("error opening paths {}", error);
+ }
+ }
+ })
+ }
+
+ fn worktree_for_abs_path(
+ &self,
+ abs_path: &Path,
+ cx: &mut ViewContext<Self>,
+ ) -> Task<Result<(ModelHandle<Worktree>, PathBuf)>> {
+ let abs_path: Arc<Path> = Arc::from(abs_path);
+ cx.spawn(|this, mut cx| async move {
+ let mut entry_id = None;
+ this.read_with(&cx, |this, cx| {
+ for tree in this.worktrees(cx) {
+ if let Some(relative_path) = tree
+ .read(cx)
+ .as_local()
+ .and_then(|t| abs_path.strip_prefix(t.abs_path()).ok())
+ {
+ entry_id = Some((tree.clone(), relative_path.into()));
+ break;
+ }
+ }
+ });
+
+ if let Some(entry_id) = entry_id {
+ Ok(entry_id)
+ } else {
+ let worktree = this
+ .update(&mut cx, |this, cx| this.add_worktree(&abs_path, cx))
+ .await?;
+ Ok((worktree, PathBuf::new()))
+ }
+ })
+ }
+
+ fn project_path_for_path(
+ &self,
+ abs_path: &Path,
+ cx: &mut ViewContext<Self>,
+ ) -> Task<Result<ProjectPath>> {
+ let entry = self.worktree_for_abs_path(abs_path, cx);
+ cx.spawn(|_, _| async move {
+ let (worktree, path) = entry.await?;
+ Ok(ProjectPath {
+ worktree_id: worktree.id(),
+ path: path.into(),
+ })
+ })
+ }
+
+ pub fn add_worktree(
+ &self,
+ path: &Path,
+ cx: &mut ViewContext<Self>,
+ ) -> Task<Result<ModelHandle<Worktree>>> {
+ self.project
+ .update(cx, |project, cx| project.add_local_worktree(path, cx))
+ }
+
+ pub fn toggle_modal<V, F>(&mut self, cx: &mut ViewContext<Self>, add_view: F)
+ where
+ V: 'static + View,
+ F: FnOnce(&mut ViewContext<Self>, &mut Self) -> ViewHandle<V>,
+ {
+ if self.modal.as_ref().map_or(false, |modal| modal.is::<V>()) {
+ self.modal.take();
+ cx.focus_self();
+ } else {
+ let modal = add_view(cx, self);
+ cx.focus(&modal);
+ self.modal = Some(modal.into());
+ }
+ cx.notify();
+ }
+
+ pub fn modal(&self) -> Option<&AnyViewHandle> {
+ self.modal.as_ref()
+ }
+
+ pub fn dismiss_modal(&mut self, cx: &mut ViewContext<Self>) {
+ if self.modal.take().is_some() {
+ cx.focus(&self.active_pane);
+ cx.notify();
+ }
+ }
+
+ 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,
+ project_path: ProjectPath,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<Task<()>> {
+ let pane = self.active_pane().clone();
+ if self.activate_or_open_existing_entry(project_path.clone(), &pane, cx) {
+ return None;
+ }
+
+ // let (worktree_id, path) = project_path.clone();
+
+ let worktree = match self
+ .project
+ .read(cx)
+ .worktree_for_id(project_path.worktree_id)
+ {
+ Some(worktree) => worktree,
+ None => {
+ log::error!("worktree {} does not exist", project_path.worktree_id);
+ return None;
+ }
+ };
+
+ if let Entry::Vacant(entry) = self.loading_items.entry(project_path.clone()) {
+ let (mut tx, rx) = postage::watch::channel();
+ entry.insert(rx);
+
+ let project_path = project_path.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)
+ })
+ .await;
+ *tx.borrow_mut() = Some(
+ buffer
+ .map(|buffer| Box::new(buffer) as Box<dyn ItemHandle>)
+ .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 {
+ let load_result = loop {
+ if let Some(load_result) = watch.borrow().as_ref() {
+ break load_result.clone();
+ }
+ watch.recv().await;
+ };
+
+ this.update(&mut cx, |this, cx| {
+ this.loading_items.remove(&project_path);
+ if let Some(pane) = pane.upgrade(&cx) {
+ match load_result {
+ Ok(item) => {
+ // By the time loading finishes, the entry could have been already added
+ // 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());
+ }
+ }
+ Err(error) => {
+ log::error!("error opening item: {}", error);
+ }
+ }
+ }
+ })
+ }))
+ }
+
+ fn activate_or_open_existing_entry(
+ &mut self,
+ project_path: ProjectPath,
+ pane: &ViewHandle<Pane>,
+ cx: &mut ViewContext<Self>,
+ ) -> bool {
+ // If the pane contains a view for this file, then activate
+ // that item view.
+ if pane.update(cx, |pane, cx| pane.activate_entry(project_path.clone(), cx)) {
+ return true;
+ }
+
+ // Otherwise, if this file is already open somewhere in the workspace,
+ // then add another view for it.
+ let settings = self.settings.clone();
+ let mut view_for_existing_item = None;
+ self.items.retain(|item| {
+ if item.alive(cx.as_ref()) {
+ 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(),
+ );
+ }
+ true
+ } else {
+ false
+ }
+ });
+ if let Some(view) = view_for_existing_item {
+ pane.add_item_view(view, cx.as_mut());
+ true
+ } else {
+ false
+ }
+ }
+
+ pub fn active_item(&self, cx: &ViewContext<Self>) -> Option<Box<dyn ItemViewHandle>> {
+ self.active_pane().read(cx).active_item()
+ }
+
+ fn active_project_path(&self, cx: &ViewContext<Self>) -> Option<ProjectPath> {
+ self.active_item(cx).and_then(|item| item.project_path(cx))
+ }
+
+ pub fn save_active_item(&mut self, _: &Save, cx: &mut ViewContext<Self>) {
+ if let Some(item) = self.active_item(cx) {
+ let handle = cx.handle();
+ if item.project_path(cx.as_ref()).is_none() {
+ let worktree = self.worktrees(cx).first();
+ let start_abs_path = worktree
+ .and_then(|w| w.read(cx).as_local())
+ .map_or(Path::new(""), |w| w.abs_path())
+ .to_path_buf();
+ cx.prompt_for_new_path(&start_abs_path, move |abs_path, cx| {
+ if let Some(abs_path) = abs_path {
+ cx.spawn(|mut cx| async move {
+ let result = match handle
+ .update(&mut cx, |this, cx| {
+ this.worktree_for_abs_path(&abs_path, cx)
+ })
+ .await
+ {
+ Ok((worktree, path)) => {
+ handle
+ .update(&mut cx, |_, cx| {
+ item.save_as(worktree, &path, cx.as_mut())
+ })
+ .await
+ }
+ Err(error) => Err(error),
+ };
+
+ if let Err(error) = result {
+ error!("failed to save item: {:?}, ", error);
+ }
+ })
+ .detach()
+ }
+ });
+ return;
+ } else if item.has_conflict(cx.as_ref()) {
+ const CONFLICT_MESSAGE: &'static str = "This file has changed on disk since you started editing it. Do you want to overwrite it?";
+
+ cx.prompt(
+ PromptLevel::Warning,
+ CONFLICT_MESSAGE,
+ &["Overwrite", "Cancel"],
+ move |answer, cx| {
+ if answer == 0 {
+ cx.spawn(|mut cx| async move {
+ if let Err(error) = cx.update(|cx| item.save(cx)).unwrap().await {
+ error!("failed to save item: {:?}, ", error);
+ }
+ })
+ .detach();
+ }
+ },
+ );
+ } else {
+ cx.spawn(|_, mut cx| async move {
+ if let Err(error) = cx.update(|cx| item.save(cx)).unwrap().await {
+ error!("failed to save item: {:?}, ", error);
+ }
+ })
+ .detach();
+ }
+ }
+ }
+
+ pub fn toggle_sidebar_item(&mut self, action: &ToggleSidebarItem, cx: &mut ViewContext<Self>) {
+ let sidebar = match action.0.side {
+ Side::Left => &mut self.left_sidebar,
+ Side::Right => &mut self.right_sidebar,
+ };
+ sidebar.toggle_item(action.0.item_index);
+ if let Some(active_item) = sidebar.active_item() {
+ cx.focus(active_item);
+ } else {
+ cx.focus_self();
+ }
+ cx.notify();
+ }
+
+ pub fn toggle_sidebar_item_focus(
+ &mut self,
+ action: &ToggleSidebarItemFocus,
+ cx: &mut ViewContext<Self>,
+ ) {
+ let sidebar = match action.0.side {
+ Side::Left => &mut self.left_sidebar,
+ Side::Right => &mut self.right_sidebar,
+ };
+ sidebar.activate_item(action.0.item_index);
+ if let Some(active_item) = sidebar.active_item() {
+ if active_item.is_focused(cx) {
+ cx.focus_self();
+ } else {
+ cx.focus(active_item);
+ }
+ }
+ cx.notify();
+ }
+
+ pub fn debug_elements(&mut self, _: &DebugElements, cx: &mut ViewContext<Self>) {
+ match to_string_pretty(&cx.debug_elements()) {
+ Ok(json) => {
+ let kib = json.len() as f32 / 1024.;
+ cx.as_mut().write_to_clipboard(ClipboardItem::new(json));
+ log::info!(
+ "copied {:.1} KiB of element debug JSON to the clipboard",
+ kib
+ );
+ }
+ Err(error) => {
+ log::error!("error debugging elements: {}", error);
+ }
+ };
+ }
+
+ fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
+ let pane = cx.add_view(|_| Pane::new(self.settings.clone()));
+ let pane_id = pane.id();
+ cx.observe(&pane, move |me, _, cx| {
+ let active_entry = me.active_project_path(cx);
+ me.project
+ .update(cx, |project, cx| project.set_active_path(active_entry, cx));
+ })
+ .detach();
+ cx.subscribe(&pane, move |me, _, event, cx| {
+ me.handle_pane_event(pane_id, event, cx)
+ })
+ .detach();
+ self.panes.push(pane.clone());
+ self.activate_pane(pane.clone(), cx);
+ pane
+ }
+
+ fn activate_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
+ self.active_pane = pane;
+ cx.focus(&self.active_pane);
+ cx.notify();
+ }
+
+ fn handle_pane_event(
+ &mut self,
+ pane_id: usize,
+ event: &pane::Event,
+ cx: &mut ViewContext<Self>,
+ ) {
+ if let Some(pane) = self.pane(pane_id) {
+ match event {
+ pane::Event::Split(direction) => {
+ self.split_pane(pane, *direction, cx);
+ }
+ pane::Event::Remove => {
+ self.remove_pane(pane, cx);
+ }
+ pane::Event::Activate => {
+ self.activate_pane(pane, cx);
+ }
+ }
+ } else {
+ error!("pane {} not found", pane_id);
+ }
+ }
+
+ fn split_pane(
+ &mut self,
+ pane: ViewHandle<Pane>,
+ direction: SplitDirection,
+ cx: &mut ViewContext<Self>,
+ ) -> ViewHandle<Pane> {
+ let new_pane = self.add_pane(cx);
+ self.activate_pane(new_pane.clone(), cx);
+ if let Some(item) = pane.read(cx).active_item() {
+ if let Some(clone) = item.clone_on_split(cx.as_mut()) {
+ new_pane.add_item_view(clone, cx.as_mut());
+ }
+ }
+ self.center
+ .split(pane.id(), new_pane.id(), direction)
+ .unwrap();
+ cx.notify();
+ new_pane
+ }
+
+ fn remove_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
+ if self.center.remove(pane.id()).unwrap() {
+ self.panes.retain(|p| p != &pane);
+ self.activate_pane(self.panes.last().unwrap().clone(), cx);
+ }
+ }
+
+ fn pane(&self, pane_id: usize) -> Option<ViewHandle<Pane>> {
+ self.panes.iter().find(|pane| pane.id() == pane_id).cloned()
+ }
+
+ pub fn active_pane(&self) -> &ViewHandle<Pane> {
+ &self.active_pane
+ }
+
+ fn render_connection_status(&self) -> Option<ElementBox> {
+ let theme = &self.settings.borrow().theme;
+ match &*self.client.status().borrow() {
+ client::Status::ConnectionError
+ | client::Status::ConnectionLost
+ | client::Status::Reauthenticating
+ | client::Status::Reconnecting { .. }
+ | client::Status::ReconnectionError { .. } => Some(
+ Container::new(
+ Align::new(
+ ConstrainedBox::new(
+ Svg::new("icons/offline-14.svg")
+ .with_color(theme.workspace.titlebar.icon_color)
+ .boxed(),
+ )
+ .with_width(theme.workspace.titlebar.offline_icon.width)
+ .boxed(),
+ )
+ .boxed(),
+ )
+ .with_style(theme.workspace.titlebar.offline_icon.container)
+ .boxed(),
+ ),
+ client::Status::UpgradeRequired => Some(
+ Label::new(
+ "Please update Zed to collaborate".to_string(),
+ theme.workspace.titlebar.outdated_warning.text.clone(),
+ )
+ .contained()
+ .with_style(theme.workspace.titlebar.outdated_warning.container)
+ .aligned()
+ .boxed(),
+ ),
+ _ => None,
+ }
+ }
+
+ fn render_avatar(&self, cx: &mut RenderContext<Self>) -> ElementBox {
+ let theme = &self.settings.borrow().theme;
+ let avatar = if let Some(avatar) = self
+ .user_store
+ .read(cx)
+ .current_user()
+ .and_then(|user| user.avatar.clone())
+ {
+ Image::new(avatar)
+ .with_style(theme.workspace.titlebar.avatar)
+ .boxed()
+ } else {
+ MouseEventHandler::new::<Authenticate, _, _, _>(0, cx, |_, _| {
+ Svg::new("icons/signed-out-12.svg")
+ .with_color(theme.workspace.titlebar.icon_color)
+ .boxed()
+ })
+ .on_click(|cx| cx.dispatch_action(Authenticate))
+ .with_cursor_style(CursorStyle::PointingHand)
+ .boxed()
+ };
+
+ ConstrainedBox::new(
+ Align::new(
+ ConstrainedBox::new(avatar)
+ .with_width(theme.workspace.titlebar.avatar_width)
+ .boxed(),
+ )
+ .boxed(),
+ )
+ .with_width(theme.workspace.right_sidebar.width)
+ .boxed()
+ }
+}
+
+impl Entity for Workspace {
+ type Event = ();
+}
+
+impl View for Workspace {
+ fn ui_name() -> &'static str {
+ "Workspace"
+ }
+
+ fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
+ let settings = self.settings.borrow();
+ let theme = &settings.theme;
+ Container::new(
+ Flex::column()
+ .with_child(
+ ConstrainedBox::new(
+ Container::new(
+ Stack::new()
+ .with_child(
+ Align::new(
+ Label::new(
+ "zed".into(),
+ theme.workspace.titlebar.title.clone(),
+ )
+ .boxed(),
+ )
+ .boxed(),
+ )
+ .with_child(
+ Align::new(
+ Flex::row()
+ .with_children(self.render_connection_status())
+ .with_child(self.render_avatar(cx))
+ .boxed(),
+ )
+ .right()
+ .boxed(),
+ )
+ .boxed(),
+ )
+ .with_style(theme.workspace.titlebar.container)
+ .boxed(),
+ )
+ .with_height(32.)
+ .named("titlebar"),
+ )
+ .with_child(
+ Expanded::new(
+ 1.0,
+ Stack::new()
+ .with_child({
+ let mut content = Flex::row();
+ content.add_child(self.left_sidebar.render(&settings, cx));
+ if let Some(element) =
+ self.left_sidebar.render_active_item(&settings, cx)
+ {
+ content.add_child(Flexible::new(0.8, element).boxed());
+ }
+ content.add_child(
+ Expanded::new(1.0, self.center.render(&settings.theme)).boxed(),
+ );
+ if let Some(element) =
+ self.right_sidebar.render_active_item(&settings, cx)
+ {
+ content.add_child(Flexible::new(0.8, element).boxed());
+ }
+ content.add_child(self.right_sidebar.render(&settings, cx));
+ content.boxed()
+ })
+ .with_children(
+ self.modal.as_ref().map(|m| ChildView::new(m.id()).boxed()),
+ )
+ .boxed(),
+ )
+ .boxed(),
+ )
+ .boxed(),
+ )
+ .with_background_color(settings.theme.workspace.background)
+ .named("workspace")
+ }
+
+ fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
+ cx.focus(&self.active_pane);
+ }
+}
+
+#[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)
+ .worktrees(cx)
+ .iter()
+ .flat_map(|worktree| {
+ let worktree_id = worktree.id();
+ worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
+ worktree_id,
+ path: f.path.clone(),
+ })
+ })
+ .collect::<Vec<_>>()
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use editor::{Editor, Insert};
+ 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.insert(&Insert("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.insert(&Insert("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.insert(&Insert(" 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.insert(&Insert("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);
+ });
+ }
+}
@@ -1,5 +1,5 @@
use super::{ItemViewHandle, SplitDirection};
-use crate::{project::ProjectPath, settings::Settings};
+use crate::Settings;
use gpui::{
action,
elements::*,
@@ -9,6 +9,7 @@ use gpui::{
Entity, MutableAppContext, Quad, RenderContext, View, ViewContext, ViewHandle,
};
use postage::watch;
+use project::ProjectPath;
use std::cmp;
action!(Split, SplitDirection);
@@ -36,6 +36,7 @@ rpc = { path = "../rpc" }
sum_tree = { path = "../sum_tree" }
theme = { path = "../theme" }
util = { path = "../util" }
+workspace = { path = "../workspace" }
anyhow = "1.0.38"
async-recursion = "0.3"
async-trait = "0.1"
@@ -83,6 +84,7 @@ project = { path = "../project", features = ["test-support"] }
rpc = { path = "../rpc", features = ["test-support"] }
client = { path = "../client", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }
+workspace = { path = "../workspace", features = ["test-support"] }
cargo-bundle = "0.5.0"
env_logger = "0.8"
serde_json = { version = "1.0.64", features = ["preserve_order"] }
@@ -1,4 +1,3 @@
-use crate::{settings::Settings, workspace::Workspace};
use editor::{Editor, EditorSettings};
use fuzzy::PathMatch;
use gpui::{
@@ -23,6 +22,7 @@ use std::{
},
};
use util::post_inc;
+use workspace::{Settings, Workspace};
pub struct FileFinder {
handle: WeakViewHandle<Self>,
@@ -422,16 +422,15 @@ impl FileFinder {
#[cfg(test)]
mod tests {
use super::*;
- use crate::{test::test_app_state, workspace::Workspace};
use editor::Insert;
- use project::fs::FakeFs;
use serde_json::json;
use std::path::PathBuf;
+ use workspace::{Workspace, WorkspaceParams};
#[gpui::test]
async fn test_matching_paths(mut cx: gpui::TestAppContext) {
- let app_state = cx.update(test_app_state);
- app_state
+ let params = cx.update(WorkspaceParams::test);
+ params
.fs
.as_fake()
.insert_tree(
@@ -449,7 +448,7 @@ mod tests {
editor::init(cx);
});
- let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx));
+ 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)
@@ -493,7 +492,8 @@ mod tests {
#[gpui::test]
async fn test_matching_cancellation(mut cx: gpui::TestAppContext) {
- let fs = Arc::new(FakeFs::new());
+ let params = cx.update(WorkspaceParams::test);
+ let fs = params.fs.as_fake();
fs.insert_tree(
"/dir",
json!({
@@ -508,10 +508,7 @@ mod tests {
)
.await;
- let mut app_state = cx.update(test_app_state);
- Arc::get_mut(&mut app_state).unwrap().fs = fs;
-
- let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx));
+ let (_, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx));
workspace
.update(&mut cx, |workspace, cx| {
workspace.add_worktree("/dir".as_ref(), cx)
@@ -522,7 +519,7 @@ mod tests {
.await;
let (_, finder) = cx.add_window(|cx| {
FileFinder::new(
- app_state.settings.clone(),
+ params.settings.clone(),
workspace.read(cx).project().clone(),
cx,
)
@@ -569,14 +566,14 @@ mod tests {
#[gpui::test]
async fn test_single_file_worktrees(mut cx: gpui::TestAppContext) {
- let app_state = cx.update(test_app_state);
- app_state
+ let params = cx.update(WorkspaceParams::test);
+ params
.fs
.as_fake()
.insert_tree("/root", json!({ "the-parent-dir": { "the-file": "" } }))
.await;
- let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx));
+ let (_, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx));
workspace
.update(&mut cx, |workspace, cx| {
workspace.add_worktree(Path::new("/root/the-parent-dir/the-file"), cx)
@@ -587,7 +584,7 @@ mod tests {
.await;
let (_, finder) = cx.add_window(|cx| {
FileFinder::new(
- app_state.settings.clone(),
+ params.settings.clone(),
workspace.read(cx).project().clone(),
cx,
)
@@ -622,8 +619,8 @@ mod tests {
#[gpui::test(retries = 5)]
async fn test_multiple_matches_with_same_relative_path(mut cx: gpui::TestAppContext) {
- let app_state = cx.update(test_app_state);
- app_state
+ let params = cx.update(WorkspaceParams::test);
+ params
.fs
.as_fake()
.insert_tree(
@@ -635,7 +632,7 @@ mod tests {
)
.await;
- let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx));
+ let (_, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx));
workspace
.update(&mut cx, |workspace, cx| {
@@ -650,7 +647,7 @@ mod tests {
let (_, finder) = cx.add_window(|cx| {
FileFinder::new(
- app_state.settings.clone(),
+ params.settings.clone(),
workspace.read(cx).project().clone(),
cx,
)
@@ -5,11 +5,9 @@ pub mod language;
pub mod menus;
pub mod people_panel;
pub mod project_panel;
-pub mod settings;
#[cfg(any(test, feature = "test-support"))]
pub mod test;
pub mod theme_selector;
-pub mod workspace;
pub use buffer;
use buffer::LanguageRegistry;
@@ -28,18 +26,15 @@ use people_panel::PeoplePanel;
use postage::watch;
pub use project::{self, fs};
use project_panel::ProjectPanel;
-pub use settings::Settings;
use std::{path::PathBuf, sync::Arc};
use theme::ThemeRegistry;
-use util::TryFutureExt;
-
-use crate::workspace::Workspace;
+pub use workspace;
+use workspace::{Settings, Workspace, WorkspaceParams};
action!(About);
action!(Open, Arc<AppState>);
action!(OpenPaths, OpenParams);
action!(Quit);
-action!(Authenticate);
action!(AdjustBufferFontSize, f32);
const MIN_FONT_SIZE: f32 = 6.0;
@@ -69,15 +64,6 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
cx.add_global_action(open_new);
cx.add_global_action(quit);
- cx.add_global_action({
- let rpc = app_state.client.clone();
- move |_: &Authenticate, cx| {
- let rpc = rpc.clone();
- cx.spawn(|cx| async move { rpc.authenticate_and_connect(&cx).log_err().await })
- .detach();
- }
- });
-
cx.add_global_action({
let settings_tx = app_state.settings_tx.clone();
@@ -135,8 +121,9 @@ fn open_paths(action: &OpenPaths, cx: &mut MutableAppContext) -> Task<()> {
log::info!("open new workspace");
// Add a new workspace if necessary
+ let app_state = &action.0.app_state;
let (_, workspace) = cx.add_window(window_options(), |cx| {
- build_workspace(&action.0.app_state, cx)
+ build_workspace(&WorkspaceParams::from(app_state.as_ref()), cx)
});
workspace.update(cx, |workspace, cx| {
workspace.open_paths(&action.0.paths, cx)
@@ -145,33 +132,31 @@ 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.as_ref(), cx);
+ let mut workspace = build_workspace(&action.0, cx);
workspace.open_new_file(&action, cx);
workspace
});
}
-fn build_workspace(app_state: &AppState, cx: &mut ViewContext<Workspace>) -> Workspace {
- let mut workspace = Workspace::new(app_state, cx);
+fn build_workspace(params: &WorkspaceParams, cx: &mut ViewContext<Workspace>) -> Workspace {
+ let mut workspace = Workspace::new(params, cx);
let project = workspace.project().clone();
workspace.left_sidebar_mut().add_item(
"icons/folder-tree-16.svg",
- ProjectPanel::new(project, app_state.settings.clone(), cx).into(),
+ ProjectPanel::new(project, params.settings.clone(), cx).into(),
);
workspace.right_sidebar_mut().add_item(
"icons/user-16.svg",
- cx.add_view(|cx| {
- PeoplePanel::new(app_state.user_store.clone(), app_state.settings.clone(), cx)
- })
- .into(),
+ cx.add_view(|cx| PeoplePanel::new(params.user_store.clone(), params.settings.clone(), cx))
+ .into(),
);
workspace.right_sidebar_mut().add_item(
"icons/comment-16.svg",
cx.add_view(|cx| {
ChatPanel::new(
- app_state.client.clone(),
- app_state.channel_list.clone(),
- app_state.settings.clone(),
+ params.client.clone(),
+ params.channel_list.clone(),
+ params.settings.clone(),
cx,
)
})
@@ -193,13 +178,27 @@ fn quit(_: &Quit, cx: &mut gpui::MutableAppContext) {
cx.platform().quit();
}
+impl<'a> From<&'a AppState> for WorkspaceParams {
+ fn from(state: &'a AppState) -> Self {
+ Self {
+ client: state.client.clone(),
+ fs: state.fs.clone(),
+ languages: state.languages.clone(),
+ settings: state.settings.clone(),
+ user_store: state.user_store.clone(),
+ channel_list: state.channel_list.clone(),
+ }
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
- use crate::{test::test_app_state, workspace::ItemView};
use serde_json::json;
+ use test::test_app_state;
use theme::DEFAULT_THEME_NAME;
use util::test::temp_tree;
+ use workspace::ItemView;
#[gpui::test]
async fn test_open_paths_action(mut cx: gpui::TestAppContext) {
@@ -270,7 +269,7 @@ mod tests {
async fn test_new_empty_workspace(mut cx: gpui::TestAppContext) {
let app_state = cx.update(test_app_state);
cx.update(|cx| init(&app_state, cx));
- cx.dispatch_global_action(workspace::OpenNew(app_state.clone()));
+ cx.dispatch_global_action(workspace::OpenNew(app_state.as_ref().into()));
let window_id = *cx.window_ids().first().unwrap();
let workspace = cx.root_view::<Workspace>(window_id).unwrap();
let editor = workspace.update(&mut cx, |workspace, cx| {
@@ -8,6 +8,7 @@ use parking_lot::Mutex;
use simplelog::SimpleLogger;
use std::{fs, path::PathBuf, sync::Arc};
use theme::ThemeRegistry;
+use workspace::{self, settings, OpenNew};
use zed::{
self,
assets::Assets,
@@ -15,9 +16,7 @@ use zed::{
client::{http, ChannelList, UserStore},
editor, file_finder,
fs::RealFs,
- language, menus, people_panel, project_panel, settings, theme_selector,
- workspace::{self, OpenNew},
- AppState, OpenParams, OpenPaths,
+ language, menus, people_panel, project_panel, theme_selector, AppState, OpenParams, OpenPaths,
};
fn main() {
@@ -54,6 +53,7 @@ fn main() {
});
zed::init(&app_state, cx);
+ client::init(app_state.client.clone(), cx);
workspace::init(cx);
editor::init(cx);
file_finder::init(cx);
@@ -70,7 +70,7 @@ fn main() {
let paths = collect_path_args();
if paths.is_empty() {
- cx.dispatch_global_action(OpenNew(app_state));
+ cx.dispatch_global_action(OpenNew(app_state.as_ref().into()));
} else {
cx.dispatch_global_action(OpenPaths(OpenParams { paths, app_state }));
}
@@ -1,9 +1,18 @@
-use crate::{workspace, AppState};
+use crate::{AppState, WorkspaceParams};
use gpui::{Menu, MenuItem};
use std::sync::Arc;
#[cfg(target_os = "macos")]
pub fn menus(state: &Arc<AppState>) -> Vec<Menu<'static>> {
+ let workspace_params = WorkspaceParams {
+ client: state.client.clone(),
+ fs: state.fs.clone(),
+ languages: state.languages.clone(),
+ settings: state.settings.clone(),
+ user_store: state.user_store.clone(),
+ channel_list: state.channel_list.clone(),
+ };
+
vec![
Menu {
name: "Zed",
@@ -27,7 +36,7 @@ pub fn menus(state: &Arc<AppState>) -> Vec<Menu<'static>> {
MenuItem::Action {
name: "New",
keystroke: Some("cmd-n"),
- action: Box::new(workspace::OpenNew(state.clone())),
+ action: Box::new(workspace::OpenNew(workspace_params)),
},
MenuItem::Separator,
MenuItem::Action {
@@ -1,4 +1,3 @@
-use crate::{workspace::Workspace, Settings};
use client::{Collaborator, UserStore};
use gpui::{
action,
@@ -10,6 +9,7 @@ use gpui::{
};
use postage::watch;
use theme::Theme;
+use workspace::{Settings, Workspace};
action!(JoinWorktree, u64);
action!(LeaveWorktree, u64);
@@ -648,7 +648,7 @@ mod tests {
.read_with(&cx, |t, _| t.as_local().unwrap().scan_complete())
.await;
- let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx));
+ let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state.as_ref().into(), cx));
let panel = workspace.update(&mut cx, |_, cx| ProjectPanel::new(project, settings, cx));
assert_eq!(
visible_entry_details(&panel, 0..50, &mut cx),
@@ -1,4 +1,4 @@
-use crate::{assets::Assets, language, settings::Settings, AppState};
+use crate::{assets::Assets, language, AppState};
use buffer::LanguageRegistry;
use client::{http::ServerResponse, test::FakeHttpClient, ChannelList, Client, UserStore};
use gpui::{AssetSource, MutableAppContext};
@@ -7,6 +7,7 @@ use postage::watch;
use project::fs::FakeFs;
use std::sync::Arc;
use theme::{Theme, ThemeRegistry, DEFAULT_THEME_NAME};
+use workspace::Settings;
#[cfg(test)]
#[ctor::ctor]
@@ -1,1488 +0,0 @@
-mod items;
-pub mod pane;
-pub mod pane_group;
-pub mod sidebar;
-
-use crate::{
- fs::Fs,
- project::{Project, ProjectPath},
- settings::Settings,
- workspace::sidebar::{Side, Sidebar, SidebarItemId, ToggleSidebarItem, ToggleSidebarItemFocus},
- AppState, Authenticate,
-};
-use anyhow::Result;
-use buffer::Buffer;
-use client::Client;
-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,
-};
-use log::error;
-pub use pane::*;
-pub use pane_group::*;
-use postage::{prelude::Stream, watch};
-use project::Worktree;
-use std::{
- collections::{hash_map::Entry, HashMap},
- future::Future,
- path::{Path, PathBuf},
- sync::Arc,
-};
-
-action!(OpenNew, Arc<AppState>);
-action!(Save);
-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![
- Binding::new("cmd-s", Save, None),
- Binding::new("cmd-alt-i", DebugElements, None),
- Binding::new(
- "cmd-shift-!",
- ToggleSidebarItem(SidebarItemId {
- side: Side::Left,
- item_index: 0,
- }),
- None,
- ),
- Binding::new(
- "cmd-1",
- ToggleSidebarItemFocus(SidebarItemId {
- side: Side::Left,
- item_index: 0,
- }),
- None,
- ),
- ]);
- pane::init(cx);
-}
-
-pub trait Item: Entity + Sized {
- type View: ItemView;
-
- fn build_view(
- handle: ModelHandle<Self>,
- settings: watch::Receiver<Settings>,
- cx: &mut ViewContext<Self::View>,
- ) -> Self::View;
-
- fn project_path(&self) -> Option<ProjectPath>;
-}
-
-pub trait ItemView: View {
- fn title(&self, cx: &AppContext) -> String;
- fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
- fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
- where
- Self: Sized,
- {
- None
- }
- fn is_dirty(&self, _: &AppContext) -> bool {
- false
- }
- fn has_conflict(&self, _: &AppContext) -> bool {
- false
- }
- fn save(&mut self, cx: &mut ViewContext<Self>) -> Result<Task<Result<()>>>;
- fn save_as(
- &mut self,
- worktree: ModelHandle<Worktree>,
- path: &Path,
- cx: &mut ViewContext<Self>,
- ) -> Task<anyhow::Result<()>>;
- fn should_activate_item_on_event(_: &Self::Event) -> bool {
- false
- }
- fn should_close_item_on_event(_: &Self::Event) -> bool {
- false
- }
- fn should_update_tab_on_event(_: &Self::Event) -> bool {
- false
- }
-}
-
-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;
- fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
-}
-
-pub trait ItemViewHandle {
- fn title(&self, cx: &AppContext) -> String;
- fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
- fn boxed_clone(&self) -> Box<dyn ItemViewHandle>;
- fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemViewHandle>>;
- fn set_parent_pane(&self, pane: &ViewHandle<Pane>, cx: &mut MutableAppContext);
- fn id(&self) -> usize;
- fn to_any(&self) -> AnyViewHandle;
- fn is_dirty(&self, cx: &AppContext) -> bool;
- fn has_conflict(&self, cx: &AppContext) -> bool;
- fn save(&self, cx: &mut MutableAppContext) -> Result<Task<Result<()>>>;
- fn save_as(
- &self,
- worktree: ModelHandle<Worktree>,
- path: &Path,
- cx: &mut MutableAppContext,
- ) -> Task<anyhow::Result<()>>;
-}
-
-impl<T: Item> ItemHandle for ModelHandle<T> {
- fn boxed_clone(&self) -> Box<dyn ItemHandle> {
- Box::new(self.clone())
- }
-
- fn downgrade(&self) -> Box<dyn WeakItemHandle> {
- Box::new(self.downgrade())
- }
-}
-
-impl<T: Item> WeakItemHandle for WeakModelHandle<T> {
- 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
- }
- }
-
- fn alive(&self, cx: &AppContext) -> bool {
- self.upgrade(cx).is_some()
- }
-
- fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
- self.upgrade(cx).and_then(|h| h.read(cx).project_path())
- }
-}
-
-impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
- fn title(&self, cx: &AppContext) -> String {
- self.read(cx).title(cx)
- }
-
- fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
- self.read(cx).project_path(cx)
- }
-
- fn boxed_clone(&self) -> Box<dyn ItemViewHandle> {
- Box::new(self.clone())
- }
-
- fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemViewHandle>> {
- self.update(cx, |item, cx| {
- cx.add_option_view(|cx| item.clone_on_split(cx))
- })
- .map(|handle| Box::new(handle) as Box<dyn ItemViewHandle>)
- }
-
- fn set_parent_pane(&self, pane: &ViewHandle<Pane>, cx: &mut MutableAppContext) {
- pane.update(cx, |_, cx| {
- cx.subscribe(self, |pane, item, event, cx| {
- if T::should_close_item_on_event(event) {
- pane.close_item(item.id(), cx);
- return;
- }
- if T::should_activate_item_on_event(event) {
- if let Some(ix) = pane.item_index(&item) {
- pane.activate_item(ix, cx);
- pane.activate(cx);
- }
- }
- if T::should_update_tab_on_event(event) {
- cx.notify()
- }
- })
- .detach();
- });
- }
-
- fn save(&self, cx: &mut MutableAppContext) -> Result<Task<Result<()>>> {
- self.update(cx, |item, cx| item.save(cx))
- }
-
- fn save_as(
- &self,
- worktree: ModelHandle<Worktree>,
- path: &Path,
- cx: &mut MutableAppContext,
- ) -> Task<anyhow::Result<()>> {
- self.update(cx, |item, cx| item.save_as(worktree, path, cx))
- }
-
- fn is_dirty(&self, cx: &AppContext) -> bool {
- self.read(cx).is_dirty(cx)
- }
-
- fn has_conflict(&self, cx: &AppContext) -> bool {
- self.read(cx).has_conflict(cx)
- }
-
- fn id(&self) -> usize {
- self.id()
- }
-
- fn to_any(&self) -> AnyViewHandle {
- self.into()
- }
-}
-
-impl Clone for Box<dyn ItemViewHandle> {
- fn clone(&self) -> Box<dyn ItemViewHandle> {
- self.boxed_clone()
- }
-}
-
-impl Clone for Box<dyn ItemHandle> {
- fn clone(&self) -> Box<dyn ItemHandle> {
- self.boxed_clone()
- }
-}
-
-pub struct Workspace {
- pub settings: watch::Receiver<Settings>,
- client: Arc<Client>,
- user_store: ModelHandle<client::UserStore>,
- fs: Arc<dyn Fs>,
- modal: Option<AnyViewHandle>,
- center: PaneGroup,
- left_sidebar: Sidebar,
- right_sidebar: Sidebar,
- panes: Vec<ViewHandle<Pane>>,
- active_pane: ViewHandle<Pane>,
- project: ModelHandle<Project>,
- items: Vec<Box<dyn WeakItemHandle>>,
- loading_items: HashMap<
- ProjectPath,
- postage::watch::Receiver<Option<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>>>,
- >,
- _observe_current_user: Task<()>,
-}
-
-impl Workspace {
- pub fn new(app_state: &AppState, cx: &mut ViewContext<Self>) -> Self {
- let project = cx.add_model(|_| {
- Project::new(
- app_state.languages.clone(),
- app_state.client.clone(),
- app_state.fs.clone(),
- )
- });
- cx.observe(&project, |_, _, cx| cx.notify()).detach();
-
- let pane = cx.add_view(|_| Pane::new(app_state.settings.clone()));
- let pane_id = pane.id();
- cx.observe(&pane, move |me, _, cx| {
- let active_entry = me.active_project_path(cx);
- me.project
- .update(cx, |project, cx| project.set_active_path(active_entry, cx));
- })
- .detach();
- cx.subscribe(&pane, move |me, _, event, cx| {
- me.handle_pane_event(pane_id, event, cx)
- })
- .detach();
- cx.focus(&pane);
-
- let mut current_user = app_state.user_store.read(cx).watch_current_user().clone();
- let mut connection_status = app_state.client.status().clone();
- let _observe_current_user = cx.spawn_weak(|this, mut cx| async move {
- current_user.recv().await;
- connection_status.recv().await;
- let mut stream =
- Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
-
- while stream.recv().await.is_some() {
- cx.update(|cx| {
- if let Some(this) = this.upgrade(&cx) {
- this.update(cx, |_, cx| cx.notify());
- }
- })
- }
- });
-
- Workspace {
- modal: None,
- center: PaneGroup::new(pane.id()),
- panes: vec![pane.clone()],
- active_pane: pane.clone(),
- settings: app_state.settings.clone(),
- client: app_state.client.clone(),
- user_store: app_state.user_store.clone(),
- fs: app_state.fs.clone(),
- left_sidebar: Sidebar::new(Side::Left),
- right_sidebar: Sidebar::new(Side::Right),
- project,
- items: Default::default(),
- loading_items: Default::default(),
- _observe_current_user,
- }
- }
-
- pub fn left_sidebar_mut(&mut self) -> &mut Sidebar {
- &mut self.left_sidebar
- }
-
- pub fn right_sidebar_mut(&mut self) -> &mut Sidebar {
- &mut self.right_sidebar
- }
-
- pub fn project(&self) -> &ModelHandle<Project> {
- &self.project
- }
-
- pub fn worktrees<'a>(&self, cx: &'a AppContext) -> &'a [ModelHandle<Worktree>] {
- &self.project.read(cx).worktrees()
- }
-
- pub fn contains_paths(&self, paths: &[PathBuf], cx: &AppContext) -> bool {
- paths.iter().all(|path| self.contains_path(&path, cx))
- }
-
- pub fn contains_path(&self, path: &Path, cx: &AppContext) -> bool {
- for worktree in self.worktrees(cx) {
- let worktree = worktree.read(cx).as_local();
- if worktree.map_or(false, |w| w.contains_abs_path(path)) {
- return true;
- }
- }
- false
- }
-
- pub fn worktree_scans_complete(&self, cx: &AppContext) -> impl Future<Output = ()> + 'static {
- let futures = self
- .worktrees(cx)
- .iter()
- .filter_map(|worktree| worktree.read(cx).as_local())
- .map(|worktree| worktree.scan_complete())
- .collect::<Vec<_>>();
- async move {
- for future in futures {
- future.await;
- }
- }
- }
-
- pub fn open_paths(&mut self, abs_paths: &[PathBuf], cx: &mut ViewContext<Self>) -> Task<()> {
- let entries = abs_paths
- .iter()
- .cloned()
- .map(|path| self.project_path_for_path(&path, cx))
- .collect::<Vec<_>>();
-
- let fs = self.fs.clone();
- let tasks = abs_paths
- .iter()
- .cloned()
- .zip(entries.into_iter())
- .map(|(abs_path, project_path)| {
- cx.spawn(|this, mut cx| {
- let fs = fs.clone();
- async move {
- let project_path = project_path.await?;
- if fs.is_file(&abs_path).await {
- if let Some(entry) =
- this.update(&mut cx, |this, cx| this.open_entry(project_path, cx))
- {
- entry.await;
- }
- }
- Ok(())
- }
- })
- })
- .collect::<Vec<Task<Result<()>>>>();
-
- cx.foreground().spawn(async move {
- for task in tasks {
- if let Err(error) = task.await {
- log::error!("error opening paths {}", error);
- }
- }
- })
- }
-
- fn worktree_for_abs_path(
- &self,
- abs_path: &Path,
- cx: &mut ViewContext<Self>,
- ) -> Task<Result<(ModelHandle<Worktree>, PathBuf)>> {
- let abs_path: Arc<Path> = Arc::from(abs_path);
- cx.spawn(|this, mut cx| async move {
- let mut entry_id = None;
- this.read_with(&cx, |this, cx| {
- for tree in this.worktrees(cx) {
- if let Some(relative_path) = tree
- .read(cx)
- .as_local()
- .and_then(|t| abs_path.strip_prefix(t.abs_path()).ok())
- {
- entry_id = Some((tree.clone(), relative_path.into()));
- break;
- }
- }
- });
-
- if let Some(entry_id) = entry_id {
- Ok(entry_id)
- } else {
- let worktree = this
- .update(&mut cx, |this, cx| this.add_worktree(&abs_path, cx))
- .await?;
- Ok((worktree, PathBuf::new()))
- }
- })
- }
-
- fn project_path_for_path(
- &self,
- abs_path: &Path,
- cx: &mut ViewContext<Self>,
- ) -> Task<Result<ProjectPath>> {
- let entry = self.worktree_for_abs_path(abs_path, cx);
- cx.spawn(|_, _| async move {
- let (worktree, path) = entry.await?;
- Ok(ProjectPath {
- worktree_id: worktree.id(),
- path: path.into(),
- })
- })
- }
-
- pub fn add_worktree(
- &self,
- path: &Path,
- cx: &mut ViewContext<Self>,
- ) -> Task<Result<ModelHandle<Worktree>>> {
- self.project
- .update(cx, |project, cx| project.add_local_worktree(path, cx))
- }
-
- pub fn toggle_modal<V, F>(&mut self, cx: &mut ViewContext<Self>, add_view: F)
- where
- V: 'static + View,
- F: FnOnce(&mut ViewContext<Self>, &mut Self) -> ViewHandle<V>,
- {
- if self.modal.as_ref().map_or(false, |modal| modal.is::<V>()) {
- self.modal.take();
- cx.focus_self();
- } else {
- let modal = add_view(cx, self);
- cx.focus(&modal);
- self.modal = Some(modal.into());
- }
- cx.notify();
- }
-
- pub fn modal(&self) -> Option<&AnyViewHandle> {
- self.modal.as_ref()
- }
-
- pub fn dismiss_modal(&mut self, cx: &mut ViewContext<Self>) {
- if self.modal.take().is_some() {
- cx.focus(&self.active_pane);
- cx.notify();
- }
- }
-
- 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,
- project_path: ProjectPath,
- cx: &mut ViewContext<Self>,
- ) -> Option<Task<()>> {
- let pane = self.active_pane().clone();
- if self.activate_or_open_existing_entry(project_path.clone(), &pane, cx) {
- return None;
- }
-
- // let (worktree_id, path) = project_path.clone();
-
- let worktree = match self
- .project
- .read(cx)
- .worktree_for_id(project_path.worktree_id)
- {
- Some(worktree) => worktree,
- None => {
- log::error!("worktree {} does not exist", project_path.worktree_id);
- return None;
- }
- };
-
- if let Entry::Vacant(entry) = self.loading_items.entry(project_path.clone()) {
- let (mut tx, rx) = postage::watch::channel();
- entry.insert(rx);
-
- let project_path = project_path.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)
- })
- .await;
- *tx.borrow_mut() = Some(
- buffer
- .map(|buffer| Box::new(buffer) as Box<dyn ItemHandle>)
- .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 {
- let load_result = loop {
- if let Some(load_result) = watch.borrow().as_ref() {
- break load_result.clone();
- }
- watch.recv().await;
- };
-
- this.update(&mut cx, |this, cx| {
- this.loading_items.remove(&project_path);
- if let Some(pane) = pane.upgrade(&cx) {
- match load_result {
- Ok(item) => {
- // By the time loading finishes, the entry could have been already added
- // 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());
- }
- }
- Err(error) => {
- log::error!("error opening item: {}", error);
- }
- }
- }
- })
- }))
- }
-
- fn activate_or_open_existing_entry(
- &mut self,
- project_path: ProjectPath,
- pane: &ViewHandle<Pane>,
- cx: &mut ViewContext<Self>,
- ) -> bool {
- // If the pane contains a view for this file, then activate
- // that item view.
- if pane.update(cx, |pane, cx| pane.activate_entry(project_path.clone(), cx)) {
- return true;
- }
-
- // Otherwise, if this file is already open somewhere in the workspace,
- // then add another view for it.
- let settings = self.settings.clone();
- let mut view_for_existing_item = None;
- self.items.retain(|item| {
- if item.alive(cx.as_ref()) {
- 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(),
- );
- }
- true
- } else {
- false
- }
- });
- if let Some(view) = view_for_existing_item {
- pane.add_item_view(view, cx.as_mut());
- true
- } else {
- false
- }
- }
-
- pub fn active_item(&self, cx: &ViewContext<Self>) -> Option<Box<dyn ItemViewHandle>> {
- self.active_pane().read(cx).active_item()
- }
-
- fn active_project_path(&self, cx: &ViewContext<Self>) -> Option<ProjectPath> {
- self.active_item(cx).and_then(|item| item.project_path(cx))
- }
-
- pub fn save_active_item(&mut self, _: &Save, cx: &mut ViewContext<Self>) {
- if let Some(item) = self.active_item(cx) {
- let handle = cx.handle();
- if item.project_path(cx.as_ref()).is_none() {
- let worktree = self.worktrees(cx).first();
- let start_abs_path = worktree
- .and_then(|w| w.read(cx).as_local())
- .map_or(Path::new(""), |w| w.abs_path())
- .to_path_buf();
- cx.prompt_for_new_path(&start_abs_path, move |abs_path, cx| {
- if let Some(abs_path) = abs_path {
- cx.spawn(|mut cx| async move {
- let result = match handle
- .update(&mut cx, |this, cx| {
- this.worktree_for_abs_path(&abs_path, cx)
- })
- .await
- {
- Ok((worktree, path)) => {
- handle
- .update(&mut cx, |_, cx| {
- item.save_as(worktree, &path, cx.as_mut())
- })
- .await
- }
- Err(error) => Err(error),
- };
-
- if let Err(error) = result {
- error!("failed to save item: {:?}, ", error);
- }
- })
- .detach()
- }
- });
- return;
- } else if item.has_conflict(cx.as_ref()) {
- const CONFLICT_MESSAGE: &'static str = "This file has changed on disk since you started editing it. Do you want to overwrite it?";
-
- cx.prompt(
- PromptLevel::Warning,
- CONFLICT_MESSAGE,
- &["Overwrite", "Cancel"],
- move |answer, cx| {
- if answer == 0 {
- cx.spawn(|mut cx| async move {
- if let Err(error) = cx.update(|cx| item.save(cx)).unwrap().await {
- error!("failed to save item: {:?}, ", error);
- }
- })
- .detach();
- }
- },
- );
- } else {
- cx.spawn(|_, mut cx| async move {
- if let Err(error) = cx.update(|cx| item.save(cx)).unwrap().await {
- error!("failed to save item: {:?}, ", error);
- }
- })
- .detach();
- }
- }
- }
-
- pub fn toggle_sidebar_item(&mut self, action: &ToggleSidebarItem, cx: &mut ViewContext<Self>) {
- let sidebar = match action.0.side {
- Side::Left => &mut self.left_sidebar,
- Side::Right => &mut self.right_sidebar,
- };
- sidebar.toggle_item(action.0.item_index);
- if let Some(active_item) = sidebar.active_item() {
- cx.focus(active_item);
- } else {
- cx.focus_self();
- }
- cx.notify();
- }
-
- pub fn toggle_sidebar_item_focus(
- &mut self,
- action: &ToggleSidebarItemFocus,
- cx: &mut ViewContext<Self>,
- ) {
- let sidebar = match action.0.side {
- Side::Left => &mut self.left_sidebar,
- Side::Right => &mut self.right_sidebar,
- };
- sidebar.activate_item(action.0.item_index);
- if let Some(active_item) = sidebar.active_item() {
- if active_item.is_focused(cx) {
- cx.focus_self();
- } else {
- cx.focus(active_item);
- }
- }
- cx.notify();
- }
-
- pub fn debug_elements(&mut self, _: &DebugElements, cx: &mut ViewContext<Self>) {
- match to_string_pretty(&cx.debug_elements()) {
- Ok(json) => {
- let kib = json.len() as f32 / 1024.;
- cx.as_mut().write_to_clipboard(ClipboardItem::new(json));
- log::info!(
- "copied {:.1} KiB of element debug JSON to the clipboard",
- kib
- );
- }
- Err(error) => {
- log::error!("error debugging elements: {}", error);
- }
- };
- }
-
- fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
- let pane = cx.add_view(|_| Pane::new(self.settings.clone()));
- let pane_id = pane.id();
- cx.observe(&pane, move |me, _, cx| {
- let active_entry = me.active_project_path(cx);
- me.project
- .update(cx, |project, cx| project.set_active_path(active_entry, cx));
- })
- .detach();
- cx.subscribe(&pane, move |me, _, event, cx| {
- me.handle_pane_event(pane_id, event, cx)
- })
- .detach();
- self.panes.push(pane.clone());
- self.activate_pane(pane.clone(), cx);
- pane
- }
-
- fn activate_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
- self.active_pane = pane;
- cx.focus(&self.active_pane);
- cx.notify();
- }
-
- fn handle_pane_event(
- &mut self,
- pane_id: usize,
- event: &pane::Event,
- cx: &mut ViewContext<Self>,
- ) {
- if let Some(pane) = self.pane(pane_id) {
- match event {
- pane::Event::Split(direction) => {
- self.split_pane(pane, *direction, cx);
- }
- pane::Event::Remove => {
- self.remove_pane(pane, cx);
- }
- pane::Event::Activate => {
- self.activate_pane(pane, cx);
- }
- }
- } else {
- error!("pane {} not found", pane_id);
- }
- }
-
- fn split_pane(
- &mut self,
- pane: ViewHandle<Pane>,
- direction: SplitDirection,
- cx: &mut ViewContext<Self>,
- ) -> ViewHandle<Pane> {
- let new_pane = self.add_pane(cx);
- self.activate_pane(new_pane.clone(), cx);
- if let Some(item) = pane.read(cx).active_item() {
- if let Some(clone) = item.clone_on_split(cx.as_mut()) {
- new_pane.add_item_view(clone, cx.as_mut());
- }
- }
- self.center
- .split(pane.id(), new_pane.id(), direction)
- .unwrap();
- cx.notify();
- new_pane
- }
-
- fn remove_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
- if self.center.remove(pane.id()).unwrap() {
- self.panes.retain(|p| p != &pane);
- self.activate_pane(self.panes.last().unwrap().clone(), cx);
- }
- }
-
- fn pane(&self, pane_id: usize) -> Option<ViewHandle<Pane>> {
- self.panes.iter().find(|pane| pane.id() == pane_id).cloned()
- }
-
- pub fn active_pane(&self) -> &ViewHandle<Pane> {
- &self.active_pane
- }
-
- fn render_connection_status(&self) -> Option<ElementBox> {
- let theme = &self.settings.borrow().theme;
- match &*self.client.status().borrow() {
- client::Status::ConnectionError
- | client::Status::ConnectionLost
- | client::Status::Reauthenticating
- | client::Status::Reconnecting { .. }
- | client::Status::ReconnectionError { .. } => Some(
- Container::new(
- Align::new(
- ConstrainedBox::new(
- Svg::new("icons/offline-14.svg")
- .with_color(theme.workspace.titlebar.icon_color)
- .boxed(),
- )
- .with_width(theme.workspace.titlebar.offline_icon.width)
- .boxed(),
- )
- .boxed(),
- )
- .with_style(theme.workspace.titlebar.offline_icon.container)
- .boxed(),
- ),
- client::Status::UpgradeRequired => Some(
- Label::new(
- "Please update Zed to collaborate".to_string(),
- theme.workspace.titlebar.outdated_warning.text.clone(),
- )
- .contained()
- .with_style(theme.workspace.titlebar.outdated_warning.container)
- .aligned()
- .boxed(),
- ),
- _ => None,
- }
- }
-
- fn render_avatar(&self, cx: &mut RenderContext<Self>) -> ElementBox {
- let theme = &self.settings.borrow().theme;
- let avatar = if let Some(avatar) = self
- .user_store
- .read(cx)
- .current_user()
- .and_then(|user| user.avatar.clone())
- {
- Image::new(avatar)
- .with_style(theme.workspace.titlebar.avatar)
- .boxed()
- } else {
- MouseEventHandler::new::<Authenticate, _, _, _>(0, cx, |_, _| {
- Svg::new("icons/signed-out-12.svg")
- .with_color(theme.workspace.titlebar.icon_color)
- .boxed()
- })
- .on_click(|cx| cx.dispatch_action(Authenticate))
- .with_cursor_style(CursorStyle::PointingHand)
- .boxed()
- };
-
- ConstrainedBox::new(
- Align::new(
- ConstrainedBox::new(avatar)
- .with_width(theme.workspace.titlebar.avatar_width)
- .boxed(),
- )
- .boxed(),
- )
- .with_width(theme.workspace.right_sidebar.width)
- .boxed()
- }
-}
-
-impl Entity for Workspace {
- type Event = ();
-}
-
-impl View for Workspace {
- fn ui_name() -> &'static str {
- "Workspace"
- }
-
- fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
- let settings = self.settings.borrow();
- let theme = &settings.theme;
- Container::new(
- Flex::column()
- .with_child(
- ConstrainedBox::new(
- Container::new(
- Stack::new()
- .with_child(
- Align::new(
- Label::new(
- "zed".into(),
- theme.workspace.titlebar.title.clone(),
- )
- .boxed(),
- )
- .boxed(),
- )
- .with_child(
- Align::new(
- Flex::row()
- .with_children(self.render_connection_status())
- .with_child(self.render_avatar(cx))
- .boxed(),
- )
- .right()
- .boxed(),
- )
- .boxed(),
- )
- .with_style(theme.workspace.titlebar.container)
- .boxed(),
- )
- .with_height(32.)
- .named("titlebar"),
- )
- .with_child(
- Expanded::new(
- 1.0,
- Stack::new()
- .with_child({
- let mut content = Flex::row();
- content.add_child(self.left_sidebar.render(&settings, cx));
- if let Some(element) =
- self.left_sidebar.render_active_item(&settings, cx)
- {
- content.add_child(Flexible::new(0.8, element).boxed());
- }
- content.add_child(
- Expanded::new(1.0, self.center.render(&settings.theme)).boxed(),
- );
- if let Some(element) =
- self.right_sidebar.render_active_item(&settings, cx)
- {
- content.add_child(Flexible::new(0.8, element).boxed());
- }
- content.add_child(self.right_sidebar.render(&settings, cx));
- content.boxed()
- })
- .with_children(
- self.modal.as_ref().map(|m| ChildView::new(m.id()).boxed()),
- )
- .boxed(),
- )
- .boxed(),
- )
- .boxed(),
- )
- .with_background_color(settings.theme.workspace.background)
- .named("workspace")
- }
-
- fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
- cx.focus(&self.active_pane);
- }
-}
-
-#[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)
- .worktrees(cx)
- .iter()
- .flat_map(|worktree| {
- let worktree_id = worktree.id();
- worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
- worktree_id,
- path: f.path.clone(),
- })
- })
- .collect::<Vec<_>>()
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::{fs::FakeFs, test::test_app_state};
- use editor::{Editor, Insert};
- use serde_json::json;
- use std::collections::HashSet;
-
- #[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, 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 fs = FakeFs::new();
- 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 mut app_state = cx.update(test_app_state);
- Arc::get_mut(&mut app_state).unwrap().fs = Arc::new(fs);
-
- let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, 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);
- app_state
- .fs
- .as_fake()
- .insert_tree(
- "/root",
- json!({
- "a.txt": "",
- }),
- )
- .await;
-
- let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&app_state, 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.insert(&Insert("x".into()), cx)));
- app_state
- .fs
- .as_fake()
- .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 app_state = cx.update(test_app_state);
- app_state.fs.as_fake().insert_dir("/root").await.unwrap();
- let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, 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(app_state.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.insert(&Insert("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.insert(&Insert(" 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(app_state.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 app_state = cx.update(test_app_state);
- app_state.fs.as_fake().insert_dir("/root").await.unwrap();
- let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx));
-
- // Create a new untitled buffer
- let editor = workspace.update(&mut cx, |workspace, cx| {
- workspace.open_new_file(&OpenNew(app_state.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.insert(&Insert("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 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, 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);
- });
- }
-}