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