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