1pub mod pane;
2pub mod pane_group;
3pub mod settings;
4pub mod sidebar;
5mod status_bar;
6
7use anyhow::{anyhow, Result};
8use client::{Authenticate, ChannelList, Client, UserStore};
9use gpui::{
10 action, elements::*, json::to_string_pretty, keymap::Binding, platform::CursorStyle,
11 AnyViewHandle, AppContext, ClipboardItem, Entity, ModelContext, ModelHandle, MutableAppContext,
12 PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle, WeakModelHandle,
13};
14use language::LanguageRegistry;
15use log::error;
16pub use pane::*;
17pub use pane_group::*;
18use postage::{prelude::Stream, watch};
19use project::{Fs, Project, ProjectPath, Worktree};
20pub use settings::Settings;
21use sidebar::{Side, Sidebar, SidebarItemId, ToggleSidebarItem, ToggleSidebarItemFocus};
22use status_bar::StatusBar;
23pub use status_bar::StatusItemView;
24use std::{
25 collections::{hash_map::Entry, HashMap},
26 future::Future,
27 path::{Path, PathBuf},
28 sync::Arc,
29};
30
31action!(OpenNew, WorkspaceParams);
32action!(Save);
33action!(DebugElements);
34
35pub fn init(cx: &mut MutableAppContext) {
36 cx.add_action(Workspace::save_active_item);
37 cx.add_action(Workspace::debug_elements);
38 cx.add_action(Workspace::toggle_sidebar_item);
39 cx.add_action(Workspace::toggle_sidebar_item_focus);
40 cx.add_bindings(vec![
41 Binding::new("cmd-s", Save, None),
42 Binding::new("cmd-alt-i", DebugElements, None),
43 Binding::new(
44 "cmd-shift-!",
45 ToggleSidebarItem(SidebarItemId {
46 side: Side::Left,
47 item_index: 0,
48 }),
49 None,
50 ),
51 Binding::new(
52 "cmd-1",
53 ToggleSidebarItemFocus(SidebarItemId {
54 side: Side::Left,
55 item_index: 0,
56 }),
57 None,
58 ),
59 ]);
60 pane::init(cx);
61}
62
63pub trait EntryOpener {
64 fn open(
65 &self,
66 worktree: &mut Worktree,
67 path: ProjectPath,
68 cx: &mut ModelContext<Worktree>,
69 ) -> Option<Task<Result<Box<dyn ItemHandle>>>>;
70}
71
72pub trait Item: Entity + Sized {
73 type View: ItemView;
74
75 fn build_view(
76 handle: ModelHandle<Self>,
77 settings: watch::Receiver<Settings>,
78 cx: &mut ViewContext<Self::View>,
79 ) -> Self::View;
80
81 fn project_path(&self) -> Option<ProjectPath>;
82}
83
84pub trait ItemView: View {
85 fn title(&self, cx: &AppContext) -> String;
86 fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
87 fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
88 where
89 Self: Sized,
90 {
91 None
92 }
93 fn is_dirty(&self, _: &AppContext) -> bool {
94 false
95 }
96 fn has_conflict(&self, _: &AppContext) -> bool {
97 false
98 }
99 fn save(&mut self, cx: &mut ViewContext<Self>) -> Result<Task<Result<()>>>;
100 fn save_as(
101 &mut self,
102 worktree: ModelHandle<Worktree>,
103 path: &Path,
104 cx: &mut ViewContext<Self>,
105 ) -> Task<anyhow::Result<()>>;
106 fn should_activate_item_on_event(_: &Self::Event) -> bool {
107 false
108 }
109 fn should_close_item_on_event(_: &Self::Event) -> bool {
110 false
111 }
112 fn should_update_tab_on_event(_: &Self::Event) -> bool {
113 false
114 }
115}
116
117pub trait ItemHandle: Send + Sync {
118 fn add_view(
119 &self,
120 window_id: usize,
121 settings: watch::Receiver<Settings>,
122 cx: &mut MutableAppContext,
123 ) -> Box<dyn ItemViewHandle>;
124 fn boxed_clone(&self) -> Box<dyn ItemHandle>;
125 fn downgrade(&self) -> Box<dyn WeakItemHandle>;
126 fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
127}
128
129pub trait WeakItemHandle {
130 fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>>;
131}
132
133pub trait ItemViewHandle {
134 fn title(&self, cx: &AppContext) -> String;
135 fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
136 fn boxed_clone(&self) -> Box<dyn ItemViewHandle>;
137 fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemViewHandle>>;
138 fn set_parent_pane(&self, pane: &ViewHandle<Pane>, cx: &mut MutableAppContext);
139 fn id(&self) -> usize;
140 fn to_any(&self) -> AnyViewHandle;
141 fn is_dirty(&self, cx: &AppContext) -> bool;
142 fn has_conflict(&self, cx: &AppContext) -> bool;
143 fn save(&self, cx: &mut MutableAppContext) -> Result<Task<Result<()>>>;
144 fn save_as(
145 &self,
146 worktree: ModelHandle<Worktree>,
147 path: &Path,
148 cx: &mut MutableAppContext,
149 ) -> Task<anyhow::Result<()>>;
150}
151
152impl<T: Item> ItemHandle for ModelHandle<T> {
153 fn add_view(
154 &self,
155 window_id: usize,
156 settings: watch::Receiver<Settings>,
157 cx: &mut MutableAppContext,
158 ) -> Box<dyn ItemViewHandle> {
159 Box::new(cx.add_view(window_id, |cx| T::build_view(self.clone(), settings, cx)))
160 }
161
162 fn boxed_clone(&self) -> Box<dyn ItemHandle> {
163 Box::new(self.clone())
164 }
165
166 fn downgrade(&self) -> Box<dyn WeakItemHandle> {
167 Box::new(self.downgrade())
168 }
169
170 fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
171 self.read(cx).project_path()
172 }
173}
174
175impl ItemHandle for Box<dyn ItemHandle> {
176 fn add_view(
177 &self,
178 window_id: usize,
179 settings: watch::Receiver<Settings>,
180 cx: &mut MutableAppContext,
181 ) -> Box<dyn ItemViewHandle> {
182 ItemHandle::add_view(self.as_ref(), window_id, settings, cx)
183 }
184
185 fn boxed_clone(&self) -> Box<dyn ItemHandle> {
186 self.as_ref().boxed_clone()
187 }
188
189 fn downgrade(&self) -> Box<dyn WeakItemHandle> {
190 self.as_ref().downgrade()
191 }
192
193 fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
194 self.as_ref().project_path(cx)
195 }
196}
197
198impl<T: Item> WeakItemHandle for WeakModelHandle<T> {
199 fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
200 WeakModelHandle::<T>::upgrade(*self, cx).map(|i| Box::new(i) as Box<dyn ItemHandle>)
201 }
202}
203
204impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
205 fn title(&self, cx: &AppContext) -> String {
206 self.read(cx).title(cx)
207 }
208
209 fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
210 self.read(cx).project_path(cx)
211 }
212
213 fn boxed_clone(&self) -> Box<dyn ItemViewHandle> {
214 Box::new(self.clone())
215 }
216
217 fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemViewHandle>> {
218 self.update(cx, |item, cx| {
219 cx.add_option_view(|cx| item.clone_on_split(cx))
220 })
221 .map(|handle| Box::new(handle) as Box<dyn ItemViewHandle>)
222 }
223
224 fn set_parent_pane(&self, pane: &ViewHandle<Pane>, cx: &mut MutableAppContext) {
225 pane.update(cx, |_, cx| {
226 cx.subscribe(self, |pane, item, event, cx| {
227 if T::should_close_item_on_event(event) {
228 pane.close_item(item.id(), cx);
229 return;
230 }
231 if T::should_activate_item_on_event(event) {
232 if let Some(ix) = pane.item_index(&item) {
233 pane.activate_item(ix, cx);
234 pane.activate(cx);
235 }
236 }
237 if T::should_update_tab_on_event(event) {
238 cx.notify()
239 }
240 })
241 .detach();
242 });
243 }
244
245 fn save(&self, cx: &mut MutableAppContext) -> Result<Task<Result<()>>> {
246 self.update(cx, |item, cx| item.save(cx))
247 }
248
249 fn save_as(
250 &self,
251 worktree: ModelHandle<Worktree>,
252 path: &Path,
253 cx: &mut MutableAppContext,
254 ) -> Task<anyhow::Result<()>> {
255 self.update(cx, |item, cx| item.save_as(worktree, path, cx))
256 }
257
258 fn is_dirty(&self, cx: &AppContext) -> bool {
259 self.read(cx).is_dirty(cx)
260 }
261
262 fn has_conflict(&self, cx: &AppContext) -> bool {
263 self.read(cx).has_conflict(cx)
264 }
265
266 fn id(&self) -> usize {
267 self.id()
268 }
269
270 fn to_any(&self) -> AnyViewHandle {
271 self.into()
272 }
273}
274
275impl Clone for Box<dyn ItemViewHandle> {
276 fn clone(&self) -> Box<dyn ItemViewHandle> {
277 self.boxed_clone()
278 }
279}
280
281impl Clone for Box<dyn ItemHandle> {
282 fn clone(&self) -> Box<dyn ItemHandle> {
283 self.boxed_clone()
284 }
285}
286
287#[derive(Clone)]
288pub struct WorkspaceParams {
289 pub client: Arc<Client>,
290 pub fs: Arc<dyn Fs>,
291 pub languages: Arc<LanguageRegistry>,
292 pub settings: watch::Receiver<Settings>,
293 pub user_store: ModelHandle<UserStore>,
294 pub channel_list: ModelHandle<ChannelList>,
295 pub entry_openers: Arc<[Box<dyn EntryOpener>]>,
296}
297
298impl WorkspaceParams {
299 #[cfg(any(test, feature = "test-support"))]
300 pub fn test(cx: &mut MutableAppContext) -> Self {
301 let languages = LanguageRegistry::new();
302 let client = Client::new();
303 let http_client = client::test::FakeHttpClient::new(|_| async move {
304 Ok(client::http::ServerResponse::new(404))
305 });
306 let theme =
307 gpui::fonts::with_font_cache(cx.font_cache().clone(), || theme::Theme::default());
308 let settings = Settings::new("Courier", cx.font_cache(), Arc::new(theme)).unwrap();
309 let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
310 Self {
311 channel_list: cx
312 .add_model(|cx| ChannelList::new(user_store.clone(), client.clone(), cx)),
313 client,
314 fs: Arc::new(project::FakeFs::new()),
315 languages: Arc::new(languages),
316 settings: watch::channel_with(settings).1,
317 user_store,
318 entry_openers: Arc::from([]),
319 }
320 }
321}
322
323pub struct Workspace {
324 pub settings: watch::Receiver<Settings>,
325 client: Arc<Client>,
326 user_store: ModelHandle<client::UserStore>,
327 fs: Arc<dyn Fs>,
328 modal: Option<AnyViewHandle>,
329 center: PaneGroup,
330 left_sidebar: Sidebar,
331 right_sidebar: Sidebar,
332 panes: Vec<ViewHandle<Pane>>,
333 active_pane: ViewHandle<Pane>,
334 status_bar: ViewHandle<StatusBar>,
335 project: ModelHandle<Project>,
336 entry_openers: Arc<[Box<dyn EntryOpener>]>,
337 items: Vec<Box<dyn WeakItemHandle>>,
338 loading_items: HashMap<
339 ProjectPath,
340 postage::watch::Receiver<Option<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>>>,
341 >,
342 _observe_current_user: Task<()>,
343}
344
345impl Workspace {
346 pub fn new(params: &WorkspaceParams, cx: &mut ViewContext<Self>) -> Self {
347 let project = cx.add_model(|_| {
348 Project::new(
349 params.languages.clone(),
350 params.client.clone(),
351 params.fs.clone(),
352 )
353 });
354 cx.observe(&project, |_, _, cx| cx.notify()).detach();
355
356 let pane = cx.add_view(|_| Pane::new(params.settings.clone()));
357 let pane_id = pane.id();
358 cx.observe(&pane, move |me, _, cx| {
359 let active_entry = me.active_project_path(cx);
360 me.project
361 .update(cx, |project, cx| project.set_active_path(active_entry, cx));
362 })
363 .detach();
364 cx.subscribe(&pane, move |me, _, event, cx| {
365 me.handle_pane_event(pane_id, event, cx)
366 })
367 .detach();
368 cx.focus(&pane);
369
370 let status_bar = cx.add_view(|cx| StatusBar::new(&pane, params.settings.clone(), cx));
371 let mut current_user = params.user_store.read(cx).watch_current_user().clone();
372 let mut connection_status = params.client.status().clone();
373 let _observe_current_user = cx.spawn_weak(|this, mut cx| async move {
374 current_user.recv().await;
375 connection_status.recv().await;
376 let mut stream =
377 Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
378
379 while stream.recv().await.is_some() {
380 cx.update(|cx| {
381 if let Some(this) = this.upgrade(&cx) {
382 this.update(cx, |_, cx| cx.notify());
383 }
384 })
385 }
386 });
387
388 Workspace {
389 modal: None,
390 center: PaneGroup::new(pane.id()),
391 panes: vec![pane.clone()],
392 active_pane: pane.clone(),
393 status_bar,
394 settings: params.settings.clone(),
395 client: params.client.clone(),
396 user_store: params.user_store.clone(),
397 fs: params.fs.clone(),
398 left_sidebar: Sidebar::new(Side::Left),
399 right_sidebar: Sidebar::new(Side::Right),
400 project,
401 entry_openers: params.entry_openers.clone(),
402 items: Default::default(),
403 loading_items: Default::default(),
404 _observe_current_user,
405 }
406 }
407
408 pub fn left_sidebar_mut(&mut self) -> &mut Sidebar {
409 &mut self.left_sidebar
410 }
411
412 pub fn right_sidebar_mut(&mut self) -> &mut Sidebar {
413 &mut self.right_sidebar
414 }
415
416 pub fn status_bar(&self) -> &ViewHandle<StatusBar> {
417 &self.status_bar
418 }
419
420 pub fn project(&self) -> &ModelHandle<Project> {
421 &self.project
422 }
423
424 pub fn worktrees<'a>(&self, cx: &'a AppContext) -> &'a [ModelHandle<Worktree>] {
425 &self.project.read(cx).worktrees()
426 }
427
428 pub fn contains_paths(&self, paths: &[PathBuf], cx: &AppContext) -> bool {
429 paths.iter().all(|path| self.contains_path(&path, cx))
430 }
431
432 pub fn contains_path(&self, path: &Path, cx: &AppContext) -> bool {
433 for worktree in self.worktrees(cx) {
434 let worktree = worktree.read(cx).as_local();
435 if worktree.map_or(false, |w| w.contains_abs_path(path)) {
436 return true;
437 }
438 }
439 false
440 }
441
442 pub fn worktree_scans_complete(&self, cx: &AppContext) -> impl Future<Output = ()> + 'static {
443 let futures = self
444 .worktrees(cx)
445 .iter()
446 .filter_map(|worktree| worktree.read(cx).as_local())
447 .map(|worktree| worktree.scan_complete())
448 .collect::<Vec<_>>();
449 async move {
450 for future in futures {
451 future.await;
452 }
453 }
454 }
455
456 pub fn open_paths(&mut self, abs_paths: &[PathBuf], cx: &mut ViewContext<Self>) -> Task<()> {
457 let entries = abs_paths
458 .iter()
459 .cloned()
460 .map(|path| self.project_path_for_path(&path, cx))
461 .collect::<Vec<_>>();
462
463 let fs = self.fs.clone();
464 let tasks = abs_paths
465 .iter()
466 .cloned()
467 .zip(entries.into_iter())
468 .map(|(abs_path, project_path)| {
469 cx.spawn(|this, mut cx| {
470 let fs = fs.clone();
471 async move {
472 let project_path = project_path.await?;
473 if fs.is_file(&abs_path).await {
474 if let Some(entry) =
475 this.update(&mut cx, |this, cx| this.open_entry(project_path, cx))
476 {
477 entry.await;
478 }
479 }
480 Ok(())
481 }
482 })
483 })
484 .collect::<Vec<Task<Result<()>>>>();
485
486 cx.foreground().spawn(async move {
487 for task in tasks {
488 if let Err(error) = task.await {
489 log::error!("error opening paths {}", error);
490 }
491 }
492 })
493 }
494
495 fn worktree_for_abs_path(
496 &self,
497 abs_path: &Path,
498 cx: &mut ViewContext<Self>,
499 ) -> Task<Result<(ModelHandle<Worktree>, PathBuf)>> {
500 let abs_path: Arc<Path> = Arc::from(abs_path);
501 cx.spawn(|this, mut cx| async move {
502 let mut entry_id = None;
503 this.read_with(&cx, |this, cx| {
504 for tree in this.worktrees(cx) {
505 if let Some(relative_path) = tree
506 .read(cx)
507 .as_local()
508 .and_then(|t| abs_path.strip_prefix(t.abs_path()).ok())
509 {
510 entry_id = Some((tree.clone(), relative_path.into()));
511 break;
512 }
513 }
514 });
515
516 if let Some(entry_id) = entry_id {
517 Ok(entry_id)
518 } else {
519 let worktree = this
520 .update(&mut cx, |this, cx| this.add_worktree(&abs_path, cx))
521 .await?;
522 Ok((worktree, PathBuf::new()))
523 }
524 })
525 }
526
527 fn project_path_for_path(
528 &self,
529 abs_path: &Path,
530 cx: &mut ViewContext<Self>,
531 ) -> Task<Result<ProjectPath>> {
532 let entry = self.worktree_for_abs_path(abs_path, cx);
533 cx.spawn(|_, _| async move {
534 let (worktree, path) = entry.await?;
535 Ok(ProjectPath {
536 worktree_id: worktree.id(),
537 path: path.into(),
538 })
539 })
540 }
541
542 pub fn add_worktree(
543 &self,
544 path: &Path,
545 cx: &mut ViewContext<Self>,
546 ) -> Task<Result<ModelHandle<Worktree>>> {
547 self.project
548 .update(cx, |project, cx| project.add_local_worktree(path, cx))
549 }
550
551 pub fn toggle_modal<V, F>(&mut self, cx: &mut ViewContext<Self>, add_view: F)
552 where
553 V: 'static + View,
554 F: FnOnce(&mut ViewContext<Self>, &mut Self) -> ViewHandle<V>,
555 {
556 if self.modal.as_ref().map_or(false, |modal| modal.is::<V>()) {
557 self.modal.take();
558 cx.focus_self();
559 } else {
560 let modal = add_view(cx, self);
561 cx.focus(&modal);
562 self.modal = Some(modal.into());
563 }
564 cx.notify();
565 }
566
567 pub fn modal(&self) -> Option<&AnyViewHandle> {
568 self.modal.as_ref()
569 }
570
571 pub fn dismiss_modal(&mut self, cx: &mut ViewContext<Self>) {
572 if self.modal.take().is_some() {
573 cx.focus(&self.active_pane);
574 cx.notify();
575 }
576 }
577
578 #[must_use]
579 pub fn open_entry(
580 &mut self,
581 project_path: ProjectPath,
582 cx: &mut ViewContext<Self>,
583 ) -> Option<Task<()>> {
584 let pane = self.active_pane().clone();
585 if self.activate_or_open_existing_entry(project_path.clone(), &pane, cx) {
586 return None;
587 }
588
589 let worktree = match self
590 .project
591 .read(cx)
592 .worktree_for_id(project_path.worktree_id)
593 {
594 Some(worktree) => worktree,
595 None => {
596 log::error!("worktree {} does not exist", project_path.worktree_id);
597 return None;
598 }
599 };
600
601 if let Entry::Vacant(entry) = self.loading_items.entry(project_path.clone()) {
602 let (mut tx, rx) = postage::watch::channel();
603 entry.insert(rx);
604
605 let project_path = project_path.clone();
606 let entry_openers = self.entry_openers.clone();
607 cx.as_mut()
608 .spawn(|mut cx| async move {
609 let item = worktree.update(&mut cx, move |worktree, cx| {
610 for opener in entry_openers.iter() {
611 if let Some(task) = opener.open(worktree, project_path.clone(), cx) {
612 return task;
613 }
614 }
615
616 cx.spawn(|_, _| async move {
617 Err(anyhow!("no opener for path {:?} found", project_path))
618 })
619 });
620 *tx.borrow_mut() = Some(item.await.map_err(Arc::new));
621 })
622 .detach();
623 }
624
625 let pane = pane.downgrade();
626 let mut watch = self.loading_items.get(&project_path).unwrap().clone();
627
628 Some(cx.spawn(|this, mut cx| async move {
629 let load_result = loop {
630 if let Some(load_result) = watch.borrow().as_ref() {
631 break load_result.clone();
632 }
633 watch.recv().await;
634 };
635
636 this.update(&mut cx, |this, cx| {
637 this.loading_items.remove(&project_path);
638 if let Some(pane) = pane.upgrade(&cx) {
639 match load_result {
640 Ok(item) => {
641 // By the time loading finishes, the entry could have been already added
642 // to the pane. If it was, we activate it, otherwise we'll store the
643 // item and add a new view for it.
644 if !this.activate_or_open_existing_entry(project_path, &pane, cx) {
645 this.add_item(item, cx);
646 }
647 }
648 Err(error) => {
649 log::error!("error opening item: {}", error);
650 }
651 }
652 }
653 })
654 }))
655 }
656
657 fn activate_or_open_existing_entry(
658 &mut self,
659 project_path: ProjectPath,
660 pane: &ViewHandle<Pane>,
661 cx: &mut ViewContext<Self>,
662 ) -> bool {
663 // If the pane contains a view for this file, then activate
664 // that item view.
665 if pane.update(cx, |pane, cx| pane.activate_entry(project_path.clone(), cx)) {
666 return true;
667 }
668
669 // Otherwise, if this file is already open somewhere in the workspace,
670 // then add another view for it.
671 let settings = self.settings.clone();
672 let mut view_for_existing_item = None;
673 self.items.retain(|item| {
674 if let Some(item) = item.upgrade(cx) {
675 if view_for_existing_item.is_none()
676 && item
677 .project_path(cx)
678 .map_or(false, |item_project_path| item_project_path == project_path)
679 {
680 view_for_existing_item =
681 Some(item.add_view(cx.window_id(), settings.clone(), cx.as_mut()));
682 }
683 true
684 } else {
685 false
686 }
687 });
688 if let Some(view) = view_for_existing_item {
689 pane.add_item_view(view, cx.as_mut());
690 true
691 } else {
692 false
693 }
694 }
695
696 pub fn active_item(&self, cx: &AppContext) -> Option<Box<dyn ItemViewHandle>> {
697 self.active_pane().read(cx).active_item()
698 }
699
700 fn active_project_path(&self, cx: &ViewContext<Self>) -> Option<ProjectPath> {
701 self.active_item(cx).and_then(|item| item.project_path(cx))
702 }
703
704 pub fn save_active_item(&mut self, _: &Save, cx: &mut ViewContext<Self>) {
705 if let Some(item) = self.active_item(cx) {
706 let handle = cx.handle();
707 if item.project_path(cx.as_ref()).is_none() {
708 let worktree = self.worktrees(cx).first();
709 let start_abs_path = worktree
710 .and_then(|w| w.read(cx).as_local())
711 .map_or(Path::new(""), |w| w.abs_path())
712 .to_path_buf();
713 cx.prompt_for_new_path(&start_abs_path, move |abs_path, cx| {
714 if let Some(abs_path) = abs_path {
715 cx.spawn(|mut cx| async move {
716 let result = match handle
717 .update(&mut cx, |this, cx| {
718 this.worktree_for_abs_path(&abs_path, cx)
719 })
720 .await
721 {
722 Ok((worktree, path)) => {
723 handle
724 .update(&mut cx, |_, cx| {
725 item.save_as(worktree, &path, cx.as_mut())
726 })
727 .await
728 }
729 Err(error) => Err(error),
730 };
731
732 if let Err(error) = result {
733 error!("failed to save item: {:?}, ", error);
734 }
735 })
736 .detach()
737 }
738 });
739 return;
740 } else if item.has_conflict(cx.as_ref()) {
741 const CONFLICT_MESSAGE: &'static str = "This file has changed on disk since you started editing it. Do you want to overwrite it?";
742
743 cx.prompt(
744 PromptLevel::Warning,
745 CONFLICT_MESSAGE,
746 &["Overwrite", "Cancel"],
747 move |answer, cx| {
748 if answer == 0 {
749 cx.spawn(|mut cx| async move {
750 if let Err(error) = cx.update(|cx| item.save(cx)).unwrap().await {
751 error!("failed to save item: {:?}, ", error);
752 }
753 })
754 .detach();
755 }
756 },
757 );
758 } else {
759 cx.spawn(|_, mut cx| async move {
760 if let Err(error) = cx.update(|cx| item.save(cx)).unwrap().await {
761 error!("failed to save item: {:?}, ", error);
762 }
763 })
764 .detach();
765 }
766 }
767 }
768
769 pub fn toggle_sidebar_item(&mut self, action: &ToggleSidebarItem, cx: &mut ViewContext<Self>) {
770 let sidebar = match action.0.side {
771 Side::Left => &mut self.left_sidebar,
772 Side::Right => &mut self.right_sidebar,
773 };
774 sidebar.toggle_item(action.0.item_index);
775 if let Some(active_item) = sidebar.active_item() {
776 cx.focus(active_item);
777 } else {
778 cx.focus_self();
779 }
780 cx.notify();
781 }
782
783 pub fn toggle_sidebar_item_focus(
784 &mut self,
785 action: &ToggleSidebarItemFocus,
786 cx: &mut ViewContext<Self>,
787 ) {
788 let sidebar = match action.0.side {
789 Side::Left => &mut self.left_sidebar,
790 Side::Right => &mut self.right_sidebar,
791 };
792 sidebar.activate_item(action.0.item_index);
793 if let Some(active_item) = sidebar.active_item() {
794 if active_item.is_focused(cx) {
795 cx.focus_self();
796 } else {
797 cx.focus(active_item);
798 }
799 }
800 cx.notify();
801 }
802
803 pub fn debug_elements(&mut self, _: &DebugElements, cx: &mut ViewContext<Self>) {
804 match to_string_pretty(&cx.debug_elements()) {
805 Ok(json) => {
806 let kib = json.len() as f32 / 1024.;
807 cx.as_mut().write_to_clipboard(ClipboardItem::new(json));
808 log::info!(
809 "copied {:.1} KiB of element debug JSON to the clipboard",
810 kib
811 );
812 }
813 Err(error) => {
814 log::error!("error debugging elements: {}", error);
815 }
816 };
817 }
818
819 fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
820 let pane = cx.add_view(|_| Pane::new(self.settings.clone()));
821 let pane_id = pane.id();
822 cx.observe(&pane, move |me, _, cx| {
823 let active_entry = me.active_project_path(cx);
824 me.project
825 .update(cx, |project, cx| project.set_active_path(active_entry, cx));
826 })
827 .detach();
828 cx.subscribe(&pane, move |me, _, event, cx| {
829 me.handle_pane_event(pane_id, event, cx)
830 })
831 .detach();
832 self.panes.push(pane.clone());
833 self.activate_pane(pane.clone(), cx);
834 pane
835 }
836
837 pub fn add_item<T>(&mut self, item_handle: T, cx: &mut ViewContext<Self>)
838 where
839 T: ItemHandle,
840 {
841 let view = item_handle.add_view(cx.window_id(), self.settings.clone(), cx);
842 self.items.push(item_handle.downgrade());
843 self.active_pane().add_item_view(view, cx.as_mut());
844 }
845
846 fn activate_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
847 self.active_pane = pane;
848 self.status_bar.update(cx, |status_bar, cx| {
849 status_bar.set_active_pane(&self.active_pane, cx);
850 });
851 cx.focus(&self.active_pane);
852 cx.notify();
853 }
854
855 fn handle_pane_event(
856 &mut self,
857 pane_id: usize,
858 event: &pane::Event,
859 cx: &mut ViewContext<Self>,
860 ) {
861 if let Some(pane) = self.pane(pane_id) {
862 match event {
863 pane::Event::Split(direction) => {
864 self.split_pane(pane, *direction, cx);
865 }
866 pane::Event::Remove => {
867 self.remove_pane(pane, cx);
868 }
869 pane::Event::Activate => {
870 self.activate_pane(pane, cx);
871 }
872 }
873 } else {
874 error!("pane {} not found", pane_id);
875 }
876 }
877
878 pub fn split_pane(
879 &mut self,
880 pane: ViewHandle<Pane>,
881 direction: SplitDirection,
882 cx: &mut ViewContext<Self>,
883 ) -> ViewHandle<Pane> {
884 let new_pane = self.add_pane(cx);
885 self.activate_pane(new_pane.clone(), cx);
886 if let Some(item) = pane.read(cx).active_item() {
887 if let Some(clone) = item.clone_on_split(cx.as_mut()) {
888 new_pane.add_item_view(clone, cx.as_mut());
889 }
890 }
891 self.center
892 .split(pane.id(), new_pane.id(), direction)
893 .unwrap();
894 cx.notify();
895 new_pane
896 }
897
898 fn remove_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
899 if self.center.remove(pane.id()).unwrap() {
900 self.panes.retain(|p| p != &pane);
901 self.activate_pane(self.panes.last().unwrap().clone(), cx);
902 }
903 }
904
905 pub fn panes(&self) -> &[ViewHandle<Pane>] {
906 &self.panes
907 }
908
909 fn pane(&self, pane_id: usize) -> Option<ViewHandle<Pane>> {
910 self.panes.iter().find(|pane| pane.id() == pane_id).cloned()
911 }
912
913 pub fn active_pane(&self) -> &ViewHandle<Pane> {
914 &self.active_pane
915 }
916
917 fn render_connection_status(&self) -> Option<ElementBox> {
918 let theme = &self.settings.borrow().theme;
919 match &*self.client.status().borrow() {
920 client::Status::ConnectionError
921 | client::Status::ConnectionLost
922 | client::Status::Reauthenticating
923 | client::Status::Reconnecting { .. }
924 | client::Status::ReconnectionError { .. } => Some(
925 Container::new(
926 Align::new(
927 ConstrainedBox::new(
928 Svg::new("icons/offline-14.svg")
929 .with_color(theme.workspace.titlebar.icon_color)
930 .boxed(),
931 )
932 .with_width(theme.workspace.titlebar.offline_icon.width)
933 .boxed(),
934 )
935 .boxed(),
936 )
937 .with_style(theme.workspace.titlebar.offline_icon.container)
938 .boxed(),
939 ),
940 client::Status::UpgradeRequired => Some(
941 Label::new(
942 "Please update Zed to collaborate".to_string(),
943 theme.workspace.titlebar.outdated_warning.text.clone(),
944 )
945 .contained()
946 .with_style(theme.workspace.titlebar.outdated_warning.container)
947 .aligned()
948 .boxed(),
949 ),
950 _ => None,
951 }
952 }
953
954 fn render_avatar(&self, cx: &mut RenderContext<Self>) -> ElementBox {
955 let theme = &self.settings.borrow().theme;
956 if let Some(avatar) = self
957 .user_store
958 .read(cx)
959 .current_user()
960 .and_then(|user| user.avatar.clone())
961 {
962 ConstrainedBox::new(
963 Align::new(
964 ConstrainedBox::new(
965 Image::new(avatar)
966 .with_style(theme.workspace.titlebar.avatar)
967 .boxed(),
968 )
969 .with_width(theme.workspace.titlebar.avatar_width)
970 .boxed(),
971 )
972 .boxed(),
973 )
974 .with_width(theme.workspace.right_sidebar.width)
975 .boxed()
976 } else {
977 MouseEventHandler::new::<Authenticate, _, _, _>(0, cx, |state, _| {
978 let style = if state.hovered {
979 &theme.workspace.titlebar.hovered_sign_in_prompt
980 } else {
981 &theme.workspace.titlebar.sign_in_prompt
982 };
983 Label::new("Sign in".to_string(), style.text.clone())
984 .contained()
985 .with_style(style.container)
986 .boxed()
987 })
988 .on_click(|cx| cx.dispatch_action(Authenticate))
989 .with_cursor_style(CursorStyle::PointingHand)
990 .aligned()
991 .boxed()
992 }
993 }
994}
995
996impl Entity for Workspace {
997 type Event = ();
998}
999
1000impl View for Workspace {
1001 fn ui_name() -> &'static str {
1002 "Workspace"
1003 }
1004
1005 fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
1006 let settings = self.settings.borrow();
1007 let theme = &settings.theme;
1008 Container::new(
1009 Flex::column()
1010 .with_child(
1011 ConstrainedBox::new(
1012 Container::new(
1013 Stack::new()
1014 .with_child(
1015 Align::new(
1016 Label::new(
1017 "zed".into(),
1018 theme.workspace.titlebar.title.clone(),
1019 )
1020 .boxed(),
1021 )
1022 .boxed(),
1023 )
1024 .with_child(
1025 Align::new(
1026 Flex::row()
1027 .with_children(self.render_connection_status())
1028 .with_child(self.render_avatar(cx))
1029 .boxed(),
1030 )
1031 .right()
1032 .boxed(),
1033 )
1034 .boxed(),
1035 )
1036 .with_style(theme.workspace.titlebar.container)
1037 .boxed(),
1038 )
1039 .with_height(32.)
1040 .named("titlebar"),
1041 )
1042 .with_child(
1043 Expanded::new(
1044 1.0,
1045 Stack::new()
1046 .with_child({
1047 let mut content = Flex::row();
1048 content.add_child(self.left_sidebar.render(&settings, cx));
1049 if let Some(element) =
1050 self.left_sidebar.render_active_item(&settings, cx)
1051 {
1052 content.add_child(Flexible::new(0.8, element).boxed());
1053 }
1054 content.add_child(
1055 Flex::column()
1056 .with_child(
1057 Expanded::new(1.0, self.center.render(&settings.theme))
1058 .boxed(),
1059 )
1060 .with_child(ChildView::new(self.status_bar.id()).boxed())
1061 .expanded(1.)
1062 .boxed(),
1063 );
1064 if let Some(element) =
1065 self.right_sidebar.render_active_item(&settings, cx)
1066 {
1067 content.add_child(Flexible::new(0.8, element).boxed());
1068 }
1069 content.add_child(self.right_sidebar.render(&settings, cx));
1070 content.boxed()
1071 })
1072 .with_children(
1073 self.modal.as_ref().map(|m| ChildView::new(m.id()).boxed()),
1074 )
1075 .boxed(),
1076 )
1077 .boxed(),
1078 )
1079 .boxed(),
1080 )
1081 .with_background_color(settings.theme.workspace.background)
1082 .named("workspace")
1083 }
1084
1085 fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
1086 cx.focus(&self.active_pane);
1087 }
1088}
1089
1090pub trait WorkspaceHandle {
1091 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
1092}
1093
1094impl WorkspaceHandle for ViewHandle<Workspace> {
1095 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
1096 self.read(cx)
1097 .worktrees(cx)
1098 .iter()
1099 .flat_map(|worktree| {
1100 let worktree_id = worktree.id();
1101 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
1102 worktree_id,
1103 path: f.path.clone(),
1104 })
1105 })
1106 .collect::<Vec<_>>()
1107 }
1108}