1pub mod lsp_status;
2pub mod menu;
3pub mod pane;
4pub mod pane_group;
5pub mod settings;
6pub mod sidebar;
7mod status_bar;
8
9use anyhow::{anyhow, Result};
10use client::{Authenticate, ChannelList, Client, User, UserStore};
11use clock::ReplicaId;
12use gpui::{
13 action,
14 color::Color,
15 elements::*,
16 geometry::{vector::vec2f, PathBuilder},
17 json::{self, to_string_pretty, ToJson},
18 keymap::Binding,
19 platform::{CursorStyle, WindowOptions},
20 AnyViewHandle, AppContext, ClipboardItem, Entity, ImageData, ModelHandle, MutableAppContext,
21 PathPromptOptions, PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle,
22 WeakViewHandle,
23};
24use language::{Buffer, LanguageRegistry};
25use log::error;
26pub use pane::*;
27pub use pane_group::*;
28use postage::prelude::Stream;
29use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, Worktree};
30pub use settings::Settings;
31use sidebar::{Side, Sidebar, SidebarItemId, ToggleSidebarItem, ToggleSidebarItemFocus};
32use status_bar::StatusBar;
33pub use status_bar::StatusItemView;
34use std::{
35 any::{Any, TypeId},
36 cell::RefCell,
37 future::Future,
38 path::{Path, PathBuf},
39 rc::Rc,
40 sync::Arc,
41};
42use theme::{Theme, ThemeRegistry};
43
44pub type BuildEditor = Arc<
45 dyn Fn(
46 usize,
47 ModelHandle<Project>,
48 ModelHandle<Buffer>,
49 &mut MutableAppContext,
50 ) -> Box<dyn ItemViewHandle>,
51>;
52
53action!(Open, Arc<AppState>);
54action!(OpenNew, Arc<AppState>);
55action!(OpenPaths, OpenParams);
56action!(ToggleShare);
57action!(JoinProject, JoinProjectParams);
58action!(Save);
59action!(DebugElements);
60
61pub fn init(cx: &mut MutableAppContext) {
62 pane::init(cx);
63 menu::init(cx);
64
65 cx.add_global_action(open);
66 cx.add_global_action(move |action: &OpenPaths, cx: &mut MutableAppContext| {
67 open_paths(&action.0.paths, &action.0.app_state, cx).detach();
68 });
69 cx.add_global_action(move |action: &OpenNew, cx: &mut MutableAppContext| {
70 open_new(&action.0, cx)
71 });
72 cx.add_global_action(move |action: &JoinProject, cx: &mut MutableAppContext| {
73 join_project(action.0.project_id, &action.0.app_state, cx).detach();
74 });
75
76 cx.add_action(Workspace::toggle_share);
77 cx.add_action(
78 |workspace: &mut Workspace, _: &Save, cx: &mut ViewContext<Workspace>| {
79 workspace.save_active_item(cx).detach_and_log_err(cx);
80 },
81 );
82 cx.add_action(Workspace::debug_elements);
83 cx.add_action(Workspace::toggle_sidebar_item);
84 cx.add_action(Workspace::toggle_sidebar_item_focus);
85 cx.add_bindings(vec![
86 Binding::new("cmd-s", Save, None),
87 Binding::new("cmd-alt-i", DebugElements, None),
88 Binding::new(
89 "cmd-shift-!",
90 ToggleSidebarItem(SidebarItemId {
91 side: Side::Left,
92 item_index: 0,
93 }),
94 None,
95 ),
96 Binding::new(
97 "cmd-1",
98 ToggleSidebarItemFocus(SidebarItemId {
99 side: Side::Left,
100 item_index: 0,
101 }),
102 None,
103 ),
104 ]);
105}
106
107pub fn register_editor_builder<F, V>(cx: &mut MutableAppContext, build_editor: F)
108where
109 V: ItemView,
110 F: 'static + Fn(ModelHandle<Project>, ModelHandle<Buffer>, &mut ViewContext<V>) -> V,
111{
112 cx.add_app_state::<BuildEditor>(Arc::new(move |window_id, project, model, cx| {
113 Box::new(cx.add_view(window_id, |cx| build_editor(project, model, cx)))
114 }));
115}
116
117pub struct AppState {
118 pub languages: Arc<LanguageRegistry>,
119 pub themes: Arc<ThemeRegistry>,
120 pub client: Arc<client::Client>,
121 pub user_store: ModelHandle<client::UserStore>,
122 pub fs: Arc<dyn fs::Fs>,
123 pub channel_list: ModelHandle<client::ChannelList>,
124 pub build_window_options: &'static dyn Fn() -> WindowOptions<'static>,
125 pub build_workspace: &'static dyn Fn(
126 ModelHandle<Project>,
127 &Arc<AppState>,
128 &mut ViewContext<Workspace>,
129 ) -> Workspace,
130}
131
132#[derive(Clone)]
133pub struct OpenParams {
134 pub paths: Vec<PathBuf>,
135 pub app_state: Arc<AppState>,
136}
137
138#[derive(Clone)]
139pub struct JoinProjectParams {
140 pub project_id: u64,
141 pub app_state: Arc<AppState>,
142}
143
144pub trait ItemView: View {
145 fn deactivated(&mut self, _: &mut ViewContext<Self>) {}
146 fn navigate(&mut self, _: Box<dyn Any>, _: &mut ViewContext<Self>) {}
147 fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox;
148 fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
149 fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext<Self>);
150 fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
151 where
152 Self: Sized,
153 {
154 None
155 }
156 fn is_dirty(&self, _: &AppContext) -> bool {
157 false
158 }
159 fn has_conflict(&self, _: &AppContext) -> bool {
160 false
161 }
162 fn can_save(&self, cx: &AppContext) -> bool;
163 fn save(
164 &mut self,
165 project: ModelHandle<Project>,
166 cx: &mut ViewContext<Self>,
167 ) -> Task<Result<()>>;
168 fn can_save_as(&self, cx: &AppContext) -> bool;
169 fn save_as(
170 &mut self,
171 project: ModelHandle<Project>,
172 abs_path: PathBuf,
173 cx: &mut ViewContext<Self>,
174 ) -> Task<Result<()>>;
175 fn should_activate_item_on_event(_: &Self::Event) -> bool {
176 false
177 }
178 fn should_close_item_on_event(_: &Self::Event) -> bool {
179 false
180 }
181 fn should_update_tab_on_event(_: &Self::Event) -> bool {
182 false
183 }
184 fn act_as_type(
185 &self,
186 type_id: TypeId,
187 self_handle: &ViewHandle<Self>,
188 _: &AppContext,
189 ) -> Option<AnyViewHandle> {
190 if TypeId::of::<Self>() == type_id {
191 Some(self_handle.into())
192 } else {
193 None
194 }
195 }
196}
197
198pub trait ItemViewHandle: 'static {
199 fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox;
200 fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
201 fn boxed_clone(&self) -> Box<dyn ItemViewHandle>;
202 fn set_nav_history(&self, nav_history: Rc<RefCell<NavHistory>>, cx: &mut MutableAppContext);
203 fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemViewHandle>>;
204 fn added_to_pane(&mut self, cx: &mut ViewContext<Pane>);
205 fn deactivated(&self, cx: &mut MutableAppContext);
206 fn navigate(&self, data: Box<dyn Any>, cx: &mut MutableAppContext);
207 fn id(&self) -> usize;
208 fn to_any(&self) -> AnyViewHandle;
209 fn is_dirty(&self, cx: &AppContext) -> bool;
210 fn has_conflict(&self, cx: &AppContext) -> bool;
211 fn can_save(&self, cx: &AppContext) -> bool;
212 fn can_save_as(&self, cx: &AppContext) -> bool;
213 fn save(&self, project: ModelHandle<Project>, cx: &mut MutableAppContext) -> Task<Result<()>>;
214 fn save_as(
215 &self,
216 project: ModelHandle<Project>,
217 abs_path: PathBuf,
218 cx: &mut MutableAppContext,
219 ) -> Task<Result<()>>;
220 fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyViewHandle>;
221}
222
223pub trait WeakItemViewHandle {
224 fn id(&self) -> usize;
225 fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemViewHandle>>;
226}
227
228impl dyn ItemViewHandle {
229 pub fn downcast<T: View>(&self) -> Option<ViewHandle<T>> {
230 self.to_any().downcast()
231 }
232
233 pub fn act_as<T: View>(&self, cx: &AppContext) -> Option<ViewHandle<T>> {
234 self.act_as_type(TypeId::of::<T>(), cx)
235 .and_then(|t| t.downcast())
236 }
237}
238
239impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
240 fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox {
241 self.read(cx).tab_content(style, cx)
242 }
243
244 fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
245 self.read(cx).project_path(cx)
246 }
247
248 fn boxed_clone(&self) -> Box<dyn ItemViewHandle> {
249 Box::new(self.clone())
250 }
251
252 fn clone_on_split(
253 &self,
254 // nav_history: Rc<RefCell<NavHistory>>,
255 cx: &mut MutableAppContext,
256 ) -> Option<Box<dyn ItemViewHandle>> {
257 self.update(cx, |item, cx| {
258 cx.add_option_view(|cx| item.clone_on_split(cx))
259 })
260 .map(|handle| Box::new(handle) as Box<dyn ItemViewHandle>)
261 }
262
263 fn set_nav_history(&self, nav_history: Rc<RefCell<NavHistory>>, cx: &mut MutableAppContext) {
264 self.update(cx, |item, cx| {
265 item.set_nav_history(ItemNavHistory::new(nav_history, &cx.handle()), cx);
266 })
267 }
268
269 fn added_to_pane(&mut self, cx: &mut ViewContext<Pane>) {
270 cx.subscribe(self, |pane, item, event, cx| {
271 if T::should_close_item_on_event(event) {
272 pane.close_item(item.id(), cx);
273 return;
274 }
275 if T::should_activate_item_on_event(event) {
276 if let Some(ix) = pane.index_for_item_view(&item) {
277 pane.activate_item(ix, cx);
278 pane.activate(cx);
279 }
280 }
281 if T::should_update_tab_on_event(event) {
282 cx.notify()
283 }
284 })
285 .detach();
286 }
287
288 fn deactivated(&self, cx: &mut MutableAppContext) {
289 self.update(cx, |this, cx| this.deactivated(cx));
290 }
291
292 fn navigate(&self, data: Box<dyn Any>, cx: &mut MutableAppContext) {
293 self.update(cx, |this, cx| this.navigate(data, cx));
294 }
295
296 fn save(&self, project: ModelHandle<Project>, cx: &mut MutableAppContext) -> Task<Result<()>> {
297 self.update(cx, |item, cx| item.save(project, cx))
298 }
299
300 fn save_as(
301 &self,
302 project: ModelHandle<Project>,
303 abs_path: PathBuf,
304 cx: &mut MutableAppContext,
305 ) -> Task<anyhow::Result<()>> {
306 self.update(cx, |item, cx| item.save_as(project, abs_path, cx))
307 }
308
309 fn is_dirty(&self, cx: &AppContext) -> bool {
310 self.read(cx).is_dirty(cx)
311 }
312
313 fn has_conflict(&self, cx: &AppContext) -> bool {
314 self.read(cx).has_conflict(cx)
315 }
316
317 fn id(&self) -> usize {
318 self.id()
319 }
320
321 fn to_any(&self) -> AnyViewHandle {
322 self.into()
323 }
324
325 fn can_save(&self, cx: &AppContext) -> bool {
326 self.read(cx).can_save(cx)
327 }
328
329 fn can_save_as(&self, cx: &AppContext) -> bool {
330 self.read(cx).can_save_as(cx)
331 }
332
333 fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyViewHandle> {
334 self.read(cx).act_as_type(type_id, self, cx)
335 }
336}
337
338impl Into<AnyViewHandle> for Box<dyn ItemViewHandle> {
339 fn into(self) -> AnyViewHandle {
340 self.to_any()
341 }
342}
343
344impl Clone for Box<dyn ItemViewHandle> {
345 fn clone(&self) -> Box<dyn ItemViewHandle> {
346 self.boxed_clone()
347 }
348}
349
350impl<T: ItemView> WeakItemViewHandle for WeakViewHandle<T> {
351 fn id(&self) -> usize {
352 self.id()
353 }
354
355 fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemViewHandle>> {
356 self.upgrade(cx)
357 .map(|v| Box::new(v) as Box<dyn ItemViewHandle>)
358 }
359}
360
361#[derive(Clone)]
362pub struct WorkspaceParams {
363 pub project: ModelHandle<Project>,
364 pub client: Arc<Client>,
365 pub fs: Arc<dyn Fs>,
366 pub languages: Arc<LanguageRegistry>,
367 pub user_store: ModelHandle<UserStore>,
368 pub channel_list: ModelHandle<ChannelList>,
369}
370
371impl WorkspaceParams {
372 #[cfg(any(test, feature = "test-support"))]
373 pub fn test(cx: &mut MutableAppContext) -> Self {
374 let settings = Settings::test(cx);
375 cx.add_app_state(settings);
376
377 let fs = project::FakeFs::new(cx.background().clone());
378 let languages = Arc::new(LanguageRegistry::test());
379 let http_client = client::test::FakeHttpClient::new(|_| async move {
380 Ok(client::http::ServerResponse::new(404))
381 });
382 let client = Client::new(http_client.clone());
383 let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
384 let project = Project::local(
385 client.clone(),
386 user_store.clone(),
387 languages.clone(),
388 fs.clone(),
389 cx,
390 );
391 Self {
392 project,
393 channel_list: cx
394 .add_model(|cx| ChannelList::new(user_store.clone(), client.clone(), cx)),
395 client,
396 fs,
397 languages,
398 user_store,
399 }
400 }
401
402 #[cfg(any(test, feature = "test-support"))]
403 pub fn local(app_state: &Arc<AppState>, cx: &mut MutableAppContext) -> Self {
404 Self {
405 project: Project::local(
406 app_state.client.clone(),
407 app_state.user_store.clone(),
408 app_state.languages.clone(),
409 app_state.fs.clone(),
410 cx,
411 ),
412 client: app_state.client.clone(),
413 fs: app_state.fs.clone(),
414 languages: app_state.languages.clone(),
415 user_store: app_state.user_store.clone(),
416 channel_list: app_state.channel_list.clone(),
417 }
418 }
419}
420
421pub struct Workspace {
422 weak_self: WeakViewHandle<Self>,
423 client: Arc<Client>,
424 user_store: ModelHandle<client::UserStore>,
425 fs: Arc<dyn Fs>,
426 modal: Option<AnyViewHandle>,
427 center: PaneGroup,
428 left_sidebar: Sidebar,
429 right_sidebar: Sidebar,
430 panes: Vec<ViewHandle<Pane>>,
431 active_pane: ViewHandle<Pane>,
432 status_bar: ViewHandle<StatusBar>,
433 project: ModelHandle<Project>,
434 // items: BTreeMap<Reverse<usize>, Box<dyn WeakItemHandle>>,
435 _observe_current_user: Task<()>,
436}
437
438impl Workspace {
439 pub fn new(params: &WorkspaceParams, cx: &mut ViewContext<Self>) -> Self {
440 cx.observe(¶ms.project, |_, project, cx| {
441 if project.read(cx).is_read_only() {
442 cx.blur();
443 }
444 cx.notify()
445 })
446 .detach();
447
448 let pane = cx.add_view(|_| Pane::new());
449 let pane_id = pane.id();
450 cx.observe(&pane, move |me, _, cx| {
451 let active_entry = me.active_project_path(cx);
452 me.project
453 .update(cx, |project, cx| project.set_active_path(active_entry, cx));
454 })
455 .detach();
456 cx.subscribe(&pane, move |me, _, event, cx| {
457 me.handle_pane_event(pane_id, event, cx)
458 })
459 .detach();
460 cx.focus(&pane);
461
462 let status_bar = cx.add_view(|cx| StatusBar::new(&pane, cx));
463 let mut current_user = params.user_store.read(cx).watch_current_user().clone();
464 let mut connection_status = params.client.status().clone();
465 let _observe_current_user = cx.spawn_weak(|this, mut cx| async move {
466 current_user.recv().await;
467 connection_status.recv().await;
468 let mut stream =
469 Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
470
471 while stream.recv().await.is_some() {
472 cx.update(|cx| {
473 if let Some(this) = this.upgrade(cx) {
474 this.update(cx, |_, cx| cx.notify());
475 }
476 })
477 }
478 });
479
480 let weak_self = cx.weak_handle();
481
482 cx.emit_global(WorkspaceCreated(weak_self.clone()));
483
484 Workspace {
485 modal: None,
486 weak_self,
487 center: PaneGroup::new(pane.clone()),
488 panes: vec![pane.clone()],
489 active_pane: pane.clone(),
490 status_bar,
491 client: params.client.clone(),
492 user_store: params.user_store.clone(),
493 fs: params.fs.clone(),
494 left_sidebar: Sidebar::new(Side::Left),
495 right_sidebar: Sidebar::new(Side::Right),
496 project: params.project.clone(),
497 _observe_current_user,
498 }
499 }
500
501 pub fn weak_handle(&self) -> WeakViewHandle<Self> {
502 self.weak_self.clone()
503 }
504
505 pub fn left_sidebar_mut(&mut self) -> &mut Sidebar {
506 &mut self.left_sidebar
507 }
508
509 pub fn right_sidebar_mut(&mut self) -> &mut Sidebar {
510 &mut self.right_sidebar
511 }
512
513 pub fn status_bar(&self) -> &ViewHandle<StatusBar> {
514 &self.status_bar
515 }
516
517 pub fn project(&self) -> &ModelHandle<Project> {
518 &self.project
519 }
520
521 pub fn worktrees<'a>(
522 &self,
523 cx: &'a AppContext,
524 ) -> impl 'a + Iterator<Item = ModelHandle<Worktree>> {
525 self.project.read(cx).worktrees(cx)
526 }
527
528 pub fn contains_paths(&self, paths: &[PathBuf], cx: &AppContext) -> bool {
529 paths.iter().all(|path| self.contains_path(&path, cx))
530 }
531
532 pub fn contains_path(&self, path: &Path, cx: &AppContext) -> bool {
533 for worktree in self.worktrees(cx) {
534 let worktree = worktree.read(cx).as_local();
535 if worktree.map_or(false, |w| w.contains_abs_path(path)) {
536 return true;
537 }
538 }
539 false
540 }
541
542 pub fn worktree_scans_complete(&self, cx: &AppContext) -> impl Future<Output = ()> + 'static {
543 let futures = self
544 .worktrees(cx)
545 .filter_map(|worktree| worktree.read(cx).as_local())
546 .map(|worktree| worktree.scan_complete())
547 .collect::<Vec<_>>();
548 async move {
549 for future in futures {
550 future.await;
551 }
552 }
553 }
554
555 pub fn open_paths(
556 &mut self,
557 abs_paths: &[PathBuf],
558 cx: &mut ViewContext<Self>,
559 ) -> Task<Vec<Option<Result<Box<dyn ItemViewHandle>, Arc<anyhow::Error>>>>> {
560 let entries = abs_paths
561 .iter()
562 .cloned()
563 .map(|path| self.project_path_for_path(&path, cx))
564 .collect::<Vec<_>>();
565
566 let fs = self.fs.clone();
567 let tasks = abs_paths
568 .iter()
569 .cloned()
570 .zip(entries.into_iter())
571 .map(|(abs_path, project_path)| {
572 cx.spawn(|this, mut cx| {
573 let fs = fs.clone();
574 async move {
575 let project_path = project_path.await.ok()?;
576 if fs.is_file(&abs_path).await {
577 Some(
578 this.update(&mut cx, |this, cx| this.open_path(project_path, cx))
579 .await,
580 )
581 } else {
582 None
583 }
584 }
585 })
586 })
587 .collect::<Vec<_>>();
588
589 cx.foreground().spawn(async move {
590 let mut items = Vec::new();
591 for task in tasks {
592 items.push(task.await);
593 }
594 items
595 })
596 }
597
598 fn project_path_for_path(
599 &self,
600 abs_path: &Path,
601 cx: &mut ViewContext<Self>,
602 ) -> Task<Result<ProjectPath>> {
603 let entry = self.project().update(cx, |project, cx| {
604 project.find_or_create_local_worktree(abs_path, true, cx)
605 });
606 cx.spawn(|_, cx| async move {
607 let (worktree, path) = entry.await?;
608 Ok(ProjectPath {
609 worktree_id: worktree.read_with(&cx, |t, _| t.id()),
610 path: path.into(),
611 })
612 })
613 }
614
615 // Returns the model that was toggled closed if it was open
616 pub fn toggle_modal<V, F>(
617 &mut self,
618 cx: &mut ViewContext<Self>,
619 add_view: F,
620 ) -> Option<ViewHandle<V>>
621 where
622 V: 'static + View,
623 F: FnOnce(&mut ViewContext<Self>, &mut Self) -> ViewHandle<V>,
624 {
625 cx.notify();
626 // Whatever modal was visible is getting clobbered. If its the same type as V, then return
627 // it. Otherwise, create a new modal and set it as active.
628 let already_open_modal = self.modal.take().and_then(|modal| modal.downcast::<V>());
629 if let Some(already_open_modal) = already_open_modal {
630 cx.focus_self();
631 Some(already_open_modal)
632 } else {
633 let modal = add_view(cx, self);
634 cx.focus(&modal);
635 self.modal = Some(modal.into());
636 None
637 }
638 }
639
640 pub fn modal(&self) -> Option<&AnyViewHandle> {
641 self.modal.as_ref()
642 }
643
644 pub fn dismiss_modal(&mut self, cx: &mut ViewContext<Self>) {
645 if self.modal.take().is_some() {
646 cx.focus(&self.active_pane);
647 cx.notify();
648 }
649 }
650
651 pub fn item_for_entry(
652 &self,
653 entry_id: ProjectEntryId,
654 cx: &AppContext,
655 ) -> Option<Box<dyn ItemViewHandle>> {
656 self.panes()
657 .iter()
658 .find_map(|pane| pane.read(cx).item_for_entry(entry_id))
659 }
660
661 pub fn item_of_type<T: ItemView>(&self, cx: &AppContext) -> Option<ViewHandle<T>> {
662 self.items_of_type(cx).max_by_key(|item| item.id())
663 }
664
665 pub fn items_of_type<'a, T: ItemView>(
666 &'a self,
667 cx: &'a AppContext,
668 ) -> impl 'a + Iterator<Item = ViewHandle<T>> {
669 self.panes.iter().flat_map(|pane| {
670 pane.read(cx)
671 .item_views()
672 .filter_map(|item| item.to_any().downcast())
673 })
674 }
675
676 pub fn active_item(&self, cx: &AppContext) -> Option<Box<dyn ItemViewHandle>> {
677 self.active_pane().read(cx).active_item()
678 }
679
680 fn active_project_path(&self, cx: &ViewContext<Self>) -> Option<ProjectPath> {
681 self.active_item(cx).and_then(|item| item.project_path(cx))
682 }
683
684 pub fn save_active_item(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
685 let project = self.project.clone();
686 if let Some(item) = self.active_item(cx) {
687 if item.can_save(cx) {
688 if item.has_conflict(cx.as_ref()) {
689 const CONFLICT_MESSAGE: &'static str = "This file has changed on disk since you started editing it. Do you want to overwrite it?";
690
691 let mut answer = cx.prompt(
692 PromptLevel::Warning,
693 CONFLICT_MESSAGE,
694 &["Overwrite", "Cancel"],
695 );
696 cx.spawn(|_, mut cx| async move {
697 let answer = answer.recv().await;
698 if answer == Some(0) {
699 cx.update(|cx| item.save(project, cx)).await?;
700 }
701 Ok(())
702 })
703 } else {
704 item.save(project, cx)
705 }
706 } else if item.can_save_as(cx) {
707 let worktree = self.worktrees(cx).next();
708 let start_abs_path = worktree
709 .and_then(|w| w.read(cx).as_local())
710 .map_or(Path::new(""), |w| w.abs_path())
711 .to_path_buf();
712 let mut abs_path = cx.prompt_for_new_path(&start_abs_path);
713 cx.spawn(|_, mut cx| async move {
714 if let Some(abs_path) = abs_path.recv().await.flatten() {
715 cx.update(|cx| item.save_as(project, abs_path, cx)).await?;
716 }
717 Ok(())
718 })
719 } else {
720 Task::ready(Ok(()))
721 }
722 } else {
723 Task::ready(Ok(()))
724 }
725 }
726
727 pub fn toggle_sidebar_item(&mut self, action: &ToggleSidebarItem, cx: &mut ViewContext<Self>) {
728 let sidebar = match action.0.side {
729 Side::Left => &mut self.left_sidebar,
730 Side::Right => &mut self.right_sidebar,
731 };
732 sidebar.toggle_item(action.0.item_index);
733 if let Some(active_item) = sidebar.active_item() {
734 cx.focus(active_item);
735 } else {
736 cx.focus_self();
737 }
738 cx.notify();
739 }
740
741 pub fn toggle_sidebar_item_focus(
742 &mut self,
743 action: &ToggleSidebarItemFocus,
744 cx: &mut ViewContext<Self>,
745 ) {
746 let sidebar = match action.0.side {
747 Side::Left => &mut self.left_sidebar,
748 Side::Right => &mut self.right_sidebar,
749 };
750 sidebar.activate_item(action.0.item_index);
751 if let Some(active_item) = sidebar.active_item() {
752 if active_item.is_focused(cx) {
753 cx.focus_self();
754 } else {
755 cx.focus(active_item);
756 }
757 }
758 cx.notify();
759 }
760
761 pub fn debug_elements(&mut self, _: &DebugElements, cx: &mut ViewContext<Self>) {
762 match to_string_pretty(&cx.debug_elements()) {
763 Ok(json) => {
764 let kib = json.len() as f32 / 1024.;
765 cx.as_mut().write_to_clipboard(ClipboardItem::new(json));
766 log::info!(
767 "copied {:.1} KiB of element debug JSON to the clipboard",
768 kib
769 );
770 }
771 Err(error) => {
772 log::error!("error debugging elements: {}", error);
773 }
774 };
775 }
776
777 fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
778 let pane = cx.add_view(|_| Pane::new());
779 let pane_id = pane.id();
780 cx.observe(&pane, move |me, _, cx| {
781 let active_entry = me.active_project_path(cx);
782 me.project
783 .update(cx, |project, cx| project.set_active_path(active_entry, cx));
784 })
785 .detach();
786 cx.subscribe(&pane, move |me, _, event, cx| {
787 me.handle_pane_event(pane_id, event, cx)
788 })
789 .detach();
790 self.panes.push(pane.clone());
791 self.activate_pane(pane.clone(), cx);
792 pane
793 }
794
795 pub fn add_item(&mut self, item_view: Box<dyn ItemViewHandle>, cx: &mut ViewContext<Self>) {
796 self.active_pane()
797 .update(cx, |pane, cx| pane.add_item(None, item_view, cx))
798 }
799
800 pub fn open_path(
801 &mut self,
802 path: ProjectPath,
803 cx: &mut ViewContext<Self>,
804 ) -> Task<Result<Box<dyn ItemViewHandle>, Arc<anyhow::Error>>> {
805 let pane = self.active_pane().downgrade();
806 let task = self.load_path(path, cx);
807 cx.spawn(|this, mut cx| async move {
808 let (project_entry_id, build_editor) = task.await?;
809 let pane = pane
810 .upgrade(&cx)
811 .ok_or_else(|| anyhow!("pane was closed"))?;
812 this.update(&mut cx, |_, cx| {
813 pane.update(cx, |pane, cx| {
814 Ok(pane.open_item(project_entry_id, cx, build_editor))
815 })
816 })
817 })
818 }
819
820 pub(crate) fn load_path(
821 &mut self,
822 path: ProjectPath,
823 cx: &mut ViewContext<Self>,
824 ) -> Task<
825 Result<(
826 ProjectEntryId,
827 impl 'static + FnOnce(&mut MutableAppContext) -> Box<dyn ItemViewHandle>,
828 )>,
829 > {
830 let project = self.project().clone();
831 let buffer = project.update(cx, |project, cx| project.open_buffer_for_path(path, cx));
832 cx.spawn(|this, mut cx| async move {
833 let buffer = buffer.await?;
834 let project_entry_id = buffer.read_with(&cx, |buffer, cx| {
835 project::File::from_dyn(buffer.file())
836 .and_then(|file| file.project_entry_id(cx))
837 .ok_or_else(|| anyhow!("buffer has no entry"))
838 })?;
839 let (window_id, build_editor) = this.update(&mut cx, |_, cx| {
840 (cx.window_id(), cx.app_state::<BuildEditor>().clone())
841 });
842 let build_editor =
843 move |cx: &mut MutableAppContext| build_editor(window_id, project, buffer, cx);
844 Ok((project_entry_id, build_editor))
845 })
846 }
847
848 pub fn activate_item(&mut self, item: &dyn ItemViewHandle, cx: &mut ViewContext<Self>) -> bool {
849 let result = self.panes.iter().find_map(|pane| {
850 if let Some(ix) = pane.read(cx).index_for_item(item) {
851 Some((pane.clone(), ix))
852 } else {
853 None
854 }
855 });
856 if let Some((pane, ix)) = result {
857 self.activate_pane(pane.clone(), cx);
858 pane.update(cx, |pane, cx| pane.activate_item(ix, cx));
859 true
860 } else {
861 false
862 }
863 }
864
865 pub fn activate_next_pane(&mut self, cx: &mut ViewContext<Self>) {
866 let ix = self
867 .panes
868 .iter()
869 .position(|pane| pane == &self.active_pane)
870 .unwrap();
871 let next_ix = (ix + 1) % self.panes.len();
872 self.activate_pane(self.panes[next_ix].clone(), cx);
873 }
874
875 fn activate_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
876 if self.active_pane != pane {
877 self.active_pane = pane;
878 self.status_bar.update(cx, |status_bar, cx| {
879 status_bar.set_active_pane(&self.active_pane, cx);
880 });
881 cx.focus(&self.active_pane);
882 cx.notify();
883 }
884 }
885
886 fn handle_pane_event(
887 &mut self,
888 pane_id: usize,
889 event: &pane::Event,
890 cx: &mut ViewContext<Self>,
891 ) {
892 if let Some(pane) = self.pane(pane_id) {
893 match event {
894 pane::Event::Split(direction) => {
895 self.split_pane(pane, *direction, cx);
896 }
897 pane::Event::Remove => {
898 self.remove_pane(pane, cx);
899 }
900 pane::Event::Activate => {
901 self.activate_pane(pane, cx);
902 }
903 }
904 } else {
905 error!("pane {} not found", pane_id);
906 }
907 }
908
909 pub fn split_pane(
910 &mut self,
911 pane: ViewHandle<Pane>,
912 direction: SplitDirection,
913 cx: &mut ViewContext<Self>,
914 ) -> ViewHandle<Pane> {
915 let new_pane = self.add_pane(cx);
916 self.activate_pane(new_pane.clone(), cx);
917 if let Some(item) = pane.read(cx).active_item() {
918 let project_entry_id = pane.read(cx).project_entry_id_for_item(item.as_ref());
919 if let Some(clone) = item.clone_on_split(cx.as_mut()) {
920 new_pane.update(cx, |new_pane, cx| {
921 new_pane.add_item(project_entry_id, clone, cx);
922 });
923 }
924 }
925 self.center.split(&pane, &new_pane, direction).unwrap();
926 cx.notify();
927 new_pane
928 }
929
930 fn remove_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
931 if self.center.remove(&pane).unwrap() {
932 self.panes.retain(|p| p != &pane);
933 self.activate_pane(self.panes.last().unwrap().clone(), cx);
934 cx.notify();
935 }
936 }
937
938 pub fn panes(&self) -> &[ViewHandle<Pane>] {
939 &self.panes
940 }
941
942 fn pane(&self, pane_id: usize) -> Option<ViewHandle<Pane>> {
943 self.panes.iter().find(|pane| pane.id() == pane_id).cloned()
944 }
945
946 pub fn active_pane(&self) -> &ViewHandle<Pane> {
947 &self.active_pane
948 }
949
950 fn toggle_share(&mut self, _: &ToggleShare, cx: &mut ViewContext<Self>) {
951 self.project.update(cx, |project, cx| {
952 if project.is_local() {
953 if project.is_shared() {
954 project.unshare(cx).detach();
955 } else {
956 project.share(cx).detach();
957 }
958 }
959 });
960 }
961
962 fn render_connection_status(&self, cx: &mut RenderContext<Self>) -> Option<ElementBox> {
963 let theme = &cx.app_state::<Settings>().theme;
964 match &*self.client.status().borrow() {
965 client::Status::ConnectionError
966 | client::Status::ConnectionLost
967 | client::Status::Reauthenticating
968 | client::Status::Reconnecting { .. }
969 | client::Status::ReconnectionError { .. } => Some(
970 Container::new(
971 Align::new(
972 ConstrainedBox::new(
973 Svg::new("icons/offline-14.svg")
974 .with_color(theme.workspace.titlebar.offline_icon.color)
975 .boxed(),
976 )
977 .with_width(theme.workspace.titlebar.offline_icon.width)
978 .boxed(),
979 )
980 .boxed(),
981 )
982 .with_style(theme.workspace.titlebar.offline_icon.container)
983 .boxed(),
984 ),
985 client::Status::UpgradeRequired => Some(
986 Label::new(
987 "Please update Zed to collaborate".to_string(),
988 theme.workspace.titlebar.outdated_warning.text.clone(),
989 )
990 .contained()
991 .with_style(theme.workspace.titlebar.outdated_warning.container)
992 .aligned()
993 .boxed(),
994 ),
995 _ => None,
996 }
997 }
998
999 fn render_titlebar(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
1000 ConstrainedBox::new(
1001 Container::new(
1002 Stack::new()
1003 .with_child(
1004 Align::new(
1005 Label::new("zed".into(), theme.workspace.titlebar.title.clone())
1006 .boxed(),
1007 )
1008 .boxed(),
1009 )
1010 .with_child(
1011 Align::new(
1012 Flex::row()
1013 .with_children(self.render_share_icon(theme, cx))
1014 .with_children(self.render_collaborators(theme, cx))
1015 .with_child(self.render_current_user(
1016 self.user_store.read(cx).current_user().as_ref(),
1017 self.project.read(cx).replica_id(),
1018 theme,
1019 cx,
1020 ))
1021 .with_children(self.render_connection_status(cx))
1022 .boxed(),
1023 )
1024 .right()
1025 .boxed(),
1026 )
1027 .boxed(),
1028 )
1029 .with_style(theme.workspace.titlebar.container)
1030 .boxed(),
1031 )
1032 .with_height(theme.workspace.titlebar.height)
1033 .named("titlebar")
1034 }
1035
1036 fn render_collaborators(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> Vec<ElementBox> {
1037 let mut collaborators = self
1038 .project
1039 .read(cx)
1040 .collaborators()
1041 .values()
1042 .cloned()
1043 .collect::<Vec<_>>();
1044 collaborators.sort_unstable_by_key(|collaborator| collaborator.replica_id);
1045 collaborators
1046 .into_iter()
1047 .filter_map(|collaborator| {
1048 Some(self.render_avatar(
1049 collaborator.user.avatar.clone()?,
1050 collaborator.replica_id,
1051 theme,
1052 ))
1053 })
1054 .collect()
1055 }
1056
1057 fn render_current_user(
1058 &self,
1059 user: Option<&Arc<User>>,
1060 replica_id: ReplicaId,
1061 theme: &Theme,
1062 cx: &mut RenderContext<Self>,
1063 ) -> ElementBox {
1064 if let Some(avatar) = user.and_then(|user| user.avatar.clone()) {
1065 self.render_avatar(avatar, replica_id, theme)
1066 } else {
1067 MouseEventHandler::new::<Authenticate, _, _>(0, cx, |state, _| {
1068 let style = if state.hovered {
1069 &theme.workspace.titlebar.hovered_sign_in_prompt
1070 } else {
1071 &theme.workspace.titlebar.sign_in_prompt
1072 };
1073 Label::new("Sign in".to_string(), style.text.clone())
1074 .contained()
1075 .with_style(style.container)
1076 .boxed()
1077 })
1078 .on_click(|cx| cx.dispatch_action(Authenticate))
1079 .with_cursor_style(CursorStyle::PointingHand)
1080 .aligned()
1081 .boxed()
1082 }
1083 }
1084
1085 fn render_avatar(
1086 &self,
1087 avatar: Arc<ImageData>,
1088 replica_id: ReplicaId,
1089 theme: &Theme,
1090 ) -> ElementBox {
1091 ConstrainedBox::new(
1092 Stack::new()
1093 .with_child(
1094 ConstrainedBox::new(
1095 Image::new(avatar)
1096 .with_style(theme.workspace.titlebar.avatar)
1097 .boxed(),
1098 )
1099 .with_width(theme.workspace.titlebar.avatar_width)
1100 .aligned()
1101 .boxed(),
1102 )
1103 .with_child(
1104 AvatarRibbon::new(theme.editor.replica_selection_style(replica_id).cursor)
1105 .constrained()
1106 .with_width(theme.workspace.titlebar.avatar_ribbon.width)
1107 .with_height(theme.workspace.titlebar.avatar_ribbon.height)
1108 .aligned()
1109 .bottom()
1110 .boxed(),
1111 )
1112 .boxed(),
1113 )
1114 .with_width(theme.workspace.right_sidebar.width)
1115 .boxed()
1116 }
1117
1118 fn render_share_icon(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> Option<ElementBox> {
1119 if self.project().read(cx).is_local() && self.client.user_id().is_some() {
1120 enum Share {}
1121
1122 let color = if self.project().read(cx).is_shared() {
1123 theme.workspace.titlebar.share_icon_active_color
1124 } else {
1125 theme.workspace.titlebar.share_icon_color
1126 };
1127 Some(
1128 MouseEventHandler::new::<Share, _, _>(0, cx, |_, _| {
1129 Align::new(
1130 ConstrainedBox::new(
1131 Svg::new("icons/broadcast-24.svg").with_color(color).boxed(),
1132 )
1133 .with_width(24.)
1134 .boxed(),
1135 )
1136 .boxed()
1137 })
1138 .with_cursor_style(CursorStyle::PointingHand)
1139 .on_click(|cx| cx.dispatch_action(ToggleShare))
1140 .boxed(),
1141 )
1142 } else {
1143 None
1144 }
1145 }
1146
1147 fn render_disconnected_overlay(&self, cx: &AppContext) -> Option<ElementBox> {
1148 if self.project.read(cx).is_read_only() {
1149 let theme = &cx.app_state::<Settings>().theme;
1150 Some(
1151 EventHandler::new(
1152 Label::new(
1153 "Your connection to the remote project has been lost.".to_string(),
1154 theme.workspace.disconnected_overlay.text.clone(),
1155 )
1156 .aligned()
1157 .contained()
1158 .with_style(theme.workspace.disconnected_overlay.container)
1159 .boxed(),
1160 )
1161 .capture(|_, _, _| true)
1162 .boxed(),
1163 )
1164 } else {
1165 None
1166 }
1167 }
1168}
1169
1170impl Entity for Workspace {
1171 type Event = ();
1172}
1173
1174impl View for Workspace {
1175 fn ui_name() -> &'static str {
1176 "Workspace"
1177 }
1178
1179 fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
1180 let theme = cx.app_state::<Settings>().theme.clone();
1181 Stack::new()
1182 .with_child(
1183 Flex::column()
1184 .with_child(self.render_titlebar(&theme, cx))
1185 .with_child(
1186 Stack::new()
1187 .with_child({
1188 let mut content = Flex::row();
1189 content.add_child(self.left_sidebar.render(&theme, cx));
1190 if let Some(element) =
1191 self.left_sidebar.render_active_item(&theme, cx)
1192 {
1193 content.add_child(Flexible::new(0.8, false, element).boxed());
1194 }
1195 content.add_child(
1196 Flex::column()
1197 .with_child(
1198 Flexible::new(1., true, self.center.render(&theme))
1199 .boxed(),
1200 )
1201 .with_child(ChildView::new(&self.status_bar).boxed())
1202 .flexible(1., true)
1203 .boxed(),
1204 );
1205 if let Some(element) =
1206 self.right_sidebar.render_active_item(&theme, cx)
1207 {
1208 content.add_child(Flexible::new(0.8, false, element).boxed());
1209 }
1210 content.add_child(self.right_sidebar.render(&theme, cx));
1211 content.boxed()
1212 })
1213 .with_children(self.modal.as_ref().map(|m| ChildView::new(m).boxed()))
1214 .flexible(1.0, true)
1215 .boxed(),
1216 )
1217 .contained()
1218 .with_background_color(theme.workspace.background)
1219 .boxed(),
1220 )
1221 .with_children(self.render_disconnected_overlay(cx))
1222 .named("workspace")
1223 }
1224
1225 fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
1226 cx.focus(&self.active_pane);
1227 }
1228}
1229
1230pub trait WorkspaceHandle {
1231 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
1232}
1233
1234impl WorkspaceHandle for ViewHandle<Workspace> {
1235 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
1236 self.read(cx)
1237 .worktrees(cx)
1238 .flat_map(|worktree| {
1239 let worktree_id = worktree.read(cx).id();
1240 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
1241 worktree_id,
1242 path: f.path.clone(),
1243 })
1244 })
1245 .collect::<Vec<_>>()
1246 }
1247}
1248
1249pub struct AvatarRibbon {
1250 color: Color,
1251}
1252
1253impl AvatarRibbon {
1254 pub fn new(color: Color) -> AvatarRibbon {
1255 AvatarRibbon { color }
1256 }
1257}
1258
1259impl Element for AvatarRibbon {
1260 type LayoutState = ();
1261
1262 type PaintState = ();
1263
1264 fn layout(
1265 &mut self,
1266 constraint: gpui::SizeConstraint,
1267 _: &mut gpui::LayoutContext,
1268 ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
1269 (constraint.max, ())
1270 }
1271
1272 fn paint(
1273 &mut self,
1274 bounds: gpui::geometry::rect::RectF,
1275 _: gpui::geometry::rect::RectF,
1276 _: &mut Self::LayoutState,
1277 cx: &mut gpui::PaintContext,
1278 ) -> Self::PaintState {
1279 let mut path = PathBuilder::new();
1280 path.reset(bounds.lower_left());
1281 path.curve_to(
1282 bounds.origin() + vec2f(bounds.height(), 0.),
1283 bounds.origin(),
1284 );
1285 path.line_to(bounds.upper_right() - vec2f(bounds.height(), 0.));
1286 path.curve_to(bounds.lower_right(), bounds.upper_right());
1287 path.line_to(bounds.lower_left());
1288 cx.scene.push_path(path.build(self.color, None));
1289 }
1290
1291 fn dispatch_event(
1292 &mut self,
1293 _: &gpui::Event,
1294 _: gpui::geometry::rect::RectF,
1295 _: &mut Self::LayoutState,
1296 _: &mut Self::PaintState,
1297 _: &mut gpui::EventContext,
1298 ) -> bool {
1299 false
1300 }
1301
1302 fn debug(
1303 &self,
1304 bounds: gpui::geometry::rect::RectF,
1305 _: &Self::LayoutState,
1306 _: &Self::PaintState,
1307 _: &gpui::DebugContext,
1308 ) -> gpui::json::Value {
1309 json::json!({
1310 "type": "AvatarRibbon",
1311 "bounds": bounds.to_json(),
1312 "color": self.color.to_json(),
1313 })
1314 }
1315}
1316
1317impl std::fmt::Debug for OpenParams {
1318 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1319 f.debug_struct("OpenParams")
1320 .field("paths", &self.paths)
1321 .finish()
1322 }
1323}
1324
1325fn open(action: &Open, cx: &mut MutableAppContext) {
1326 let app_state = action.0.clone();
1327 let mut paths = cx.prompt_for_paths(PathPromptOptions {
1328 files: true,
1329 directories: true,
1330 multiple: true,
1331 });
1332 cx.spawn(|mut cx| async move {
1333 if let Some(paths) = paths.recv().await.flatten() {
1334 cx.update(|cx| cx.dispatch_global_action(OpenPaths(OpenParams { paths, app_state })));
1335 }
1336 })
1337 .detach();
1338}
1339
1340pub struct WorkspaceCreated(WeakViewHandle<Workspace>);
1341
1342pub fn open_paths(
1343 abs_paths: &[PathBuf],
1344 app_state: &Arc<AppState>,
1345 cx: &mut MutableAppContext,
1346) -> Task<ViewHandle<Workspace>> {
1347 log::info!("open paths {:?}", abs_paths);
1348
1349 // Open paths in existing workspace if possible
1350 let mut existing = None;
1351 for window_id in cx.window_ids().collect::<Vec<_>>() {
1352 if let Some(workspace_handle) = cx.root_view::<Workspace>(window_id) {
1353 if workspace_handle.update(cx, |workspace, cx| {
1354 if workspace.contains_paths(abs_paths, cx.as_ref()) {
1355 cx.activate_window(window_id);
1356 existing = Some(workspace_handle.clone());
1357 true
1358 } else {
1359 false
1360 }
1361 }) {
1362 break;
1363 }
1364 }
1365 }
1366
1367 let workspace = existing.unwrap_or_else(|| {
1368 cx.add_window((app_state.build_window_options)(), |cx| {
1369 let project = Project::local(
1370 app_state.client.clone(),
1371 app_state.user_store.clone(),
1372 app_state.languages.clone(),
1373 app_state.fs.clone(),
1374 cx,
1375 );
1376 (app_state.build_workspace)(project, &app_state, cx)
1377 })
1378 .1
1379 });
1380
1381 let task = workspace.update(cx, |workspace, cx| workspace.open_paths(abs_paths, cx));
1382 cx.spawn(|_| async move {
1383 task.await;
1384 workspace
1385 })
1386}
1387
1388pub fn join_project(
1389 project_id: u64,
1390 app_state: &Arc<AppState>,
1391 cx: &mut MutableAppContext,
1392) -> Task<Result<ViewHandle<Workspace>>> {
1393 for window_id in cx.window_ids().collect::<Vec<_>>() {
1394 if let Some(workspace) = cx.root_view::<Workspace>(window_id) {
1395 if workspace.read(cx).project().read(cx).remote_id() == Some(project_id) {
1396 return Task::ready(Ok(workspace));
1397 }
1398 }
1399 }
1400
1401 let app_state = app_state.clone();
1402 cx.spawn(|mut cx| async move {
1403 let project = Project::remote(
1404 project_id,
1405 app_state.client.clone(),
1406 app_state.user_store.clone(),
1407 app_state.languages.clone(),
1408 app_state.fs.clone(),
1409 &mut cx,
1410 )
1411 .await?;
1412 Ok(cx.update(|cx| {
1413 cx.add_window((app_state.build_window_options)(), |cx| {
1414 (app_state.build_workspace)(project, &app_state, cx)
1415 })
1416 .1
1417 }))
1418 })
1419}
1420
1421fn open_new(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
1422 let (window_id, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
1423 let project = Project::local(
1424 app_state.client.clone(),
1425 app_state.user_store.clone(),
1426 app_state.languages.clone(),
1427 app_state.fs.clone(),
1428 cx,
1429 );
1430 (app_state.build_workspace)(project, &app_state, cx)
1431 });
1432 cx.dispatch_action(window_id, vec![workspace.id()], &OpenNew(app_state.clone()));
1433}