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