1/// NOTE: Focus only 'takes' after an update has flushed_effects.
2///
3/// This may cause issues when you're trying to write tests that use workspace focus to add items at
4/// specific locations.
5pub mod dock;
6pub mod item;
7pub mod notifications;
8pub mod pane;
9pub mod pane_group;
10mod persistence;
11pub mod searchable;
12pub mod shared_screen;
13pub mod sidebar;
14mod status_bar;
15mod toolbar;
16
17use anyhow::{anyhow, Result};
18use call::ActiveCall;
19use client::{
20 proto::{self, PeerId},
21 Client, TypedEnvelope, UserStore,
22};
23use collections::{hash_map, HashMap, HashSet};
24use dock::{Dock, DockDefaultItemFactory, ToggleDockButton};
25use drag_and_drop::DragAndDrop;
26use fs::{self, Fs};
27use futures::{
28 channel::{mpsc, oneshot},
29 future::try_join_all,
30 FutureExt, StreamExt,
31};
32use gpui::{
33 actions,
34 elements::*,
35 geometry::vector::Vector2F,
36 impl_actions, impl_internal_actions,
37 keymap_matcher::KeymapContext,
38 platform::{CursorStyle, WindowOptions},
39 AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
40 MouseButton, MutableAppContext, PathPromptOptions, Platform, PromptLevel, RenderContext,
41 SizeConstraint, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowBounds,
42};
43use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem};
44use language::LanguageRegistry;
45use std::{
46 any::TypeId,
47 borrow::Cow,
48 cmp,
49 future::Future,
50 path::{Path, PathBuf},
51 sync::Arc,
52 time::Duration,
53};
54
55use crate::{
56 notifications::simple_message_notification::{MessageNotification, OsOpen},
57 persistence::model::{SerializedPane, SerializedPaneGroup, SerializedWorkspace},
58};
59use log::{error, warn};
60use notifications::NotificationHandle;
61pub use pane::*;
62pub use pane_group::*;
63use persistence::{model::SerializedItem, DB};
64pub use persistence::{
65 model::{ItemId, WorkspaceLocation},
66 WorkspaceDb, DB as WORKSPACE_DB,
67};
68use postage::prelude::Stream;
69use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
70use serde::Deserialize;
71use settings::{Autosave, DockAnchor, Settings};
72use shared_screen::SharedScreen;
73use sidebar::{Sidebar, SidebarButtons, SidebarSide, ToggleSidebarItem};
74use status_bar::StatusBar;
75pub use status_bar::StatusItemView;
76use theme::{Theme, ThemeRegistry};
77pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
78use util::ResultExt;
79
80#[derive(Clone, PartialEq)]
81pub struct RemoveWorktreeFromProject(pub WorktreeId);
82
83actions!(
84 workspace,
85 [
86 Open,
87 NewFile,
88 NewWindow,
89 CloseWindow,
90 AddFolderToProject,
91 Unfollow,
92 Save,
93 SaveAs,
94 SaveAll,
95 ActivatePreviousPane,
96 ActivateNextPane,
97 FollowNextCollaborator,
98 ToggleLeftSidebar,
99 ToggleRightSidebar,
100 NewTerminal,
101 NewSearch,
102 Feedback,
103 ]
104);
105
106#[derive(Clone, PartialEq)]
107pub struct OpenPaths {
108 pub paths: Vec<PathBuf>,
109}
110
111#[derive(Clone, Deserialize, PartialEq)]
112pub struct ActivatePane(pub usize);
113
114#[derive(Clone, PartialEq)]
115pub struct ToggleFollow(pub PeerId);
116
117#[derive(Clone, PartialEq)]
118pub struct JoinProject {
119 pub project_id: u64,
120 pub follow_user_id: u64,
121}
122
123#[derive(Clone, PartialEq)]
124pub struct OpenSharedScreen {
125 pub peer_id: PeerId,
126}
127
128#[derive(Clone, PartialEq)]
129pub struct SplitWithItem {
130 pane_to_split: WeakViewHandle<Pane>,
131 split_direction: SplitDirection,
132 from: WeakViewHandle<Pane>,
133 item_id_to_move: usize,
134}
135
136#[derive(Clone, PartialEq)]
137pub struct SplitWithProjectEntry {
138 pane_to_split: WeakViewHandle<Pane>,
139 split_direction: SplitDirection,
140 project_entry: ProjectEntryId,
141}
142
143#[derive(Clone, PartialEq)]
144pub struct OpenProjectEntryInPane {
145 pane: WeakViewHandle<Pane>,
146 project_entry: ProjectEntryId,
147}
148
149pub type WorkspaceId = i64;
150
151impl_internal_actions!(
152 workspace,
153 [
154 OpenPaths,
155 ToggleFollow,
156 JoinProject,
157 OpenSharedScreen,
158 RemoveWorktreeFromProject,
159 SplitWithItem,
160 SplitWithProjectEntry,
161 OpenProjectEntryInPane,
162 ]
163);
164impl_actions!(workspace, [ActivatePane]);
165
166pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
167 pane::init(cx);
168 dock::init(cx);
169 notifications::init(cx);
170
171 cx.add_global_action(open);
172 cx.add_global_action({
173 let app_state = Arc::downgrade(&app_state);
174 move |action: &OpenPaths, cx: &mut MutableAppContext| {
175 if let Some(app_state) = app_state.upgrade() {
176 open_paths(&action.paths, &app_state, cx).detach();
177 }
178 }
179 });
180 cx.add_global_action({
181 let app_state = Arc::downgrade(&app_state);
182 move |_: &NewFile, cx: &mut MutableAppContext| {
183 if let Some(app_state) = app_state.upgrade() {
184 open_new(&app_state, cx).detach();
185 }
186 }
187 });
188
189 cx.add_global_action({
190 let app_state = Arc::downgrade(&app_state);
191 move |_: &NewWindow, cx: &mut MutableAppContext| {
192 if let Some(app_state) = app_state.upgrade() {
193 open_new(&app_state, cx).detach();
194 }
195 }
196 });
197
198 cx.add_async_action(Workspace::toggle_follow);
199 cx.add_async_action(Workspace::follow_next_collaborator);
200 cx.add_async_action(Workspace::close);
201 cx.add_global_action(Workspace::close_global);
202 cx.add_async_action(Workspace::save_all);
203 cx.add_action(Workspace::open_shared_screen);
204 cx.add_action(Workspace::add_folder_to_project);
205 cx.add_action(Workspace::remove_folder_from_project);
206 cx.add_action(
207 |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext<Workspace>| {
208 let pane = workspace.active_pane().clone();
209 workspace.unfollow(&pane, cx);
210 },
211 );
212 cx.add_action(
213 |workspace: &mut Workspace, _: &Save, cx: &mut ViewContext<Workspace>| {
214 workspace.save_active_item(false, cx).detach_and_log_err(cx);
215 },
216 );
217 cx.add_action(
218 |workspace: &mut Workspace, _: &SaveAs, cx: &mut ViewContext<Workspace>| {
219 workspace.save_active_item(true, cx).detach_and_log_err(cx);
220 },
221 );
222 cx.add_action(Workspace::toggle_sidebar_item);
223 cx.add_action(Workspace::focus_center);
224 cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| {
225 workspace.activate_previous_pane(cx)
226 });
227 cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| {
228 workspace.activate_next_pane(cx)
229 });
230 cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftSidebar, cx| {
231 workspace.toggle_sidebar(SidebarSide::Left, cx);
232 });
233 cx.add_action(|workspace: &mut Workspace, _: &ToggleRightSidebar, cx| {
234 workspace.toggle_sidebar(SidebarSide::Right, cx);
235 });
236 cx.add_action(Workspace::activate_pane_at_index);
237
238 cx.add_action(Workspace::split_pane_with_item);
239 cx.add_action(Workspace::split_pane_with_project_entry);
240
241 cx.add_async_action(
242 |workspace: &mut Workspace,
243 OpenProjectEntryInPane {
244 pane,
245 project_entry,
246 }: &_,
247 cx| {
248 workspace
249 .project
250 .read(cx)
251 .path_for_entry(*project_entry, cx)
252 .map(|path| {
253 let task = workspace.open_path(path, Some(pane.clone()), true, cx);
254 cx.foreground().spawn(async move {
255 task.await?;
256 Ok(())
257 })
258 })
259 },
260 );
261
262 let client = &app_state.client;
263 client.add_view_request_handler(Workspace::handle_follow);
264 client.add_view_message_handler(Workspace::handle_unfollow);
265 client.add_view_message_handler(Workspace::handle_update_followers);
266}
267
268type ProjectItemBuilders = HashMap<
269 TypeId,
270 fn(ModelHandle<Project>, AnyModelHandle, &mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
271>;
272pub fn register_project_item<I: ProjectItem>(cx: &mut MutableAppContext) {
273 cx.update_default_global(|builders: &mut ProjectItemBuilders, _| {
274 builders.insert(TypeId::of::<I::Item>(), |project, model, cx| {
275 let item = model.downcast::<I::Item>().unwrap();
276 Box::new(cx.add_view(|cx| I::for_project_item(project, item, cx)))
277 });
278 });
279}
280
281type FollowableItemBuilder = fn(
282 ViewHandle<Pane>,
283 ModelHandle<Project>,
284 ViewId,
285 &mut Option<proto::view::Variant>,
286 &mut MutableAppContext,
287) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>;
288type FollowableItemBuilders = HashMap<
289 TypeId,
290 (
291 FollowableItemBuilder,
292 fn(AnyViewHandle) -> Box<dyn FollowableItemHandle>,
293 ),
294>;
295pub fn register_followable_item<I: FollowableItem>(cx: &mut MutableAppContext) {
296 cx.update_default_global(|builders: &mut FollowableItemBuilders, _| {
297 builders.insert(
298 TypeId::of::<I>(),
299 (
300 |pane, project, id, state, cx| {
301 I::from_state_proto(pane, project, id, state, cx).map(|task| {
302 cx.foreground()
303 .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
304 })
305 },
306 |this| Box::new(this.downcast::<I>().unwrap()),
307 ),
308 );
309 });
310}
311
312type ItemDeserializers = HashMap<
313 Arc<str>,
314 fn(
315 ModelHandle<Project>,
316 WeakViewHandle<Workspace>,
317 WorkspaceId,
318 ItemId,
319 &mut ViewContext<Pane>,
320 ) -> Task<Result<Box<dyn ItemHandle>>>,
321>;
322pub fn register_deserializable_item<I: Item>(cx: &mut MutableAppContext) {
323 cx.update_default_global(|deserializers: &mut ItemDeserializers, _cx| {
324 if let Some(serialized_item_kind) = I::serialized_item_kind() {
325 deserializers.insert(
326 Arc::from(serialized_item_kind),
327 |project, workspace, workspace_id, item_id, cx| {
328 let task = I::deserialize(project, workspace, workspace_id, item_id, cx);
329 cx.foreground()
330 .spawn(async { Ok(Box::new(task.await?) as Box<_>) })
331 },
332 );
333 }
334 });
335}
336
337pub struct AppState {
338 pub languages: Arc<LanguageRegistry>,
339 pub themes: Arc<ThemeRegistry>,
340 pub client: Arc<client::Client>,
341 pub user_store: ModelHandle<client::UserStore>,
342 pub fs: Arc<dyn fs::Fs>,
343 pub build_window_options:
344 fn(Option<WindowBounds>, Option<uuid::Uuid>, &dyn Platform) -> WindowOptions<'static>,
345 pub initialize_workspace: fn(&mut Workspace, &Arc<AppState>, &mut ViewContext<Workspace>),
346 pub dock_default_item_factory: DockDefaultItemFactory,
347}
348
349impl AppState {
350 #[cfg(any(test, feature = "test-support"))]
351 pub fn test(cx: &mut MutableAppContext) -> Arc<Self> {
352 use fs::HomeDir;
353
354 cx.set_global(HomeDir(Path::new("/tmp/").to_path_buf()));
355 let settings = Settings::test(cx);
356 cx.set_global(settings);
357
358 let fs = fs::FakeFs::new(cx.background().clone());
359 let languages = Arc::new(LanguageRegistry::test());
360 let http_client = client::test::FakeHttpClient::with_404_response();
361 let client = Client::new(http_client.clone(), cx);
362 let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
363 let themes = ThemeRegistry::new((), cx.font_cache().clone());
364 Arc::new(Self {
365 client,
366 themes,
367 fs,
368 languages,
369 user_store,
370 initialize_workspace: |_, _, _| {},
371 build_window_options: |_, _, _| Default::default(),
372 dock_default_item_factory: |_, _| unimplemented!(),
373 })
374 }
375}
376
377struct DelayedDebouncedEditAction {
378 task: Option<Task<()>>,
379 cancel_channel: Option<oneshot::Sender<()>>,
380}
381
382impl DelayedDebouncedEditAction {
383 fn new() -> DelayedDebouncedEditAction {
384 DelayedDebouncedEditAction {
385 task: None,
386 cancel_channel: None,
387 }
388 }
389
390 fn fire_new<F, Fut>(
391 &mut self,
392 delay: Duration,
393 workspace: &Workspace,
394 cx: &mut ViewContext<Workspace>,
395 f: F,
396 ) where
397 F: FnOnce(ModelHandle<Project>, AsyncAppContext) -> Fut + 'static,
398 Fut: 'static + Future<Output = ()>,
399 {
400 if let Some(channel) = self.cancel_channel.take() {
401 _ = channel.send(());
402 }
403
404 let project = workspace.project().downgrade();
405
406 let (sender, mut receiver) = oneshot::channel::<()>();
407 self.cancel_channel = Some(sender);
408
409 let previous_task = self.task.take();
410 self.task = Some(cx.spawn_weak(|_, cx| async move {
411 let mut timer = cx.background().timer(delay).fuse();
412 if let Some(previous_task) = previous_task {
413 previous_task.await;
414 }
415
416 futures::select_biased! {
417 _ = receiver => return,
418 _ = timer => {}
419 }
420
421 if let Some(project) = project.upgrade(&cx) {
422 (f)(project, cx).await;
423 }
424 }));
425 }
426}
427
428pub enum Event {
429 DockAnchorChanged,
430 PaneAdded(ViewHandle<Pane>),
431 ContactRequestedJoin(u64),
432}
433
434pub struct Workspace {
435 weak_self: WeakViewHandle<Self>,
436 client: Arc<Client>,
437 user_store: ModelHandle<client::UserStore>,
438 remote_entity_subscription: Option<client::Subscription>,
439 fs: Arc<dyn Fs>,
440 modal: Option<AnyViewHandle>,
441 center: PaneGroup,
442 left_sidebar: ViewHandle<Sidebar>,
443 right_sidebar: ViewHandle<Sidebar>,
444 panes: Vec<ViewHandle<Pane>>,
445 panes_by_item: HashMap<usize, WeakViewHandle<Pane>>,
446 active_pane: ViewHandle<Pane>,
447 last_active_center_pane: Option<WeakViewHandle<Pane>>,
448 status_bar: ViewHandle<StatusBar>,
449 titlebar_item: Option<AnyViewHandle>,
450 dock: Dock,
451 notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
452 project: ModelHandle<Project>,
453 leader_state: LeaderState,
454 follower_states_by_leader: FollowerStatesByLeader,
455 last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
456 window_edited: bool,
457 active_call: Option<(ModelHandle<ActiveCall>, Vec<gpui::Subscription>)>,
458 leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
459 database_id: WorkspaceId,
460 _apply_leader_updates: Task<Result<()>>,
461 _observe_current_user: Task<()>,
462}
463
464#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
465pub struct ViewId {
466 pub creator: PeerId,
467 pub id: u64,
468}
469
470#[derive(Default)]
471struct LeaderState {
472 followers: HashSet<PeerId>,
473}
474
475type FollowerStatesByLeader = HashMap<PeerId, HashMap<ViewHandle<Pane>, FollowerState>>;
476
477#[derive(Default)]
478struct FollowerState {
479 active_view_id: Option<ViewId>,
480 items_by_leader_view_id: HashMap<ViewId, Box<dyn FollowableItemHandle>>,
481}
482
483impl Workspace {
484 pub fn new(
485 serialized_workspace: Option<SerializedWorkspace>,
486 workspace_id: WorkspaceId,
487 project: ModelHandle<Project>,
488 dock_default_factory: DockDefaultItemFactory,
489 cx: &mut ViewContext<Self>,
490 ) -> Self {
491 cx.observe_fullscreen(|_, _, cx| cx.notify()).detach();
492
493 cx.observe_window_activation(Self::on_window_activation_changed)
494 .detach();
495 cx.observe(&project, |_, _, cx| cx.notify()).detach();
496 cx.subscribe(&project, move |this, _, event, cx| {
497 match event {
498 project::Event::RemoteIdChanged(remote_id) => {
499 this.project_remote_id_changed(*remote_id, cx);
500 }
501 project::Event::CollaboratorLeft(peer_id) => {
502 this.collaborator_left(*peer_id, cx);
503 }
504 project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded => {
505 this.update_window_title(cx);
506 this.serialize_workspace(cx);
507 }
508 project::Event::DisconnectedFromHost => {
509 this.update_window_edited(cx);
510 cx.blur();
511 }
512 _ => {}
513 }
514 cx.notify()
515 })
516 .detach();
517
518 let center_pane = cx.add_view(|cx| Pane::new(None, cx));
519 let pane_id = center_pane.id();
520 cx.subscribe(¢er_pane, move |this, _, event, cx| {
521 this.handle_pane_event(pane_id, event, cx)
522 })
523 .detach();
524 cx.focus(¢er_pane);
525 cx.emit(Event::PaneAdded(center_pane.clone()));
526 let dock = Dock::new(dock_default_factory, cx);
527 let dock_pane = dock.pane().clone();
528
529 let fs = project.read(cx).fs().clone();
530 let user_store = project.read(cx).user_store();
531 let client = project.read(cx).client();
532 let mut current_user = user_store.read(cx).watch_current_user();
533 let mut connection_status = client.status();
534 let _observe_current_user = cx.spawn_weak(|this, mut cx| async move {
535 current_user.recv().await;
536 connection_status.recv().await;
537 let mut stream =
538 Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
539
540 while stream.recv().await.is_some() {
541 cx.update(|cx| {
542 if let Some(this) = this.upgrade(cx) {
543 this.update(cx, |_, cx| cx.notify());
544 }
545 })
546 }
547 });
548 let handle = cx.handle();
549 let weak_handle = cx.weak_handle();
550
551 // All leader updates are enqueued and then processed in a single task, so
552 // that each asynchronous operation can be run in order.
553 let (leader_updates_tx, mut leader_updates_rx) =
554 mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>();
555 let _apply_leader_updates = cx.spawn_weak(|this, mut cx| async move {
556 while let Some((leader_id, update)) = leader_updates_rx.next().await {
557 let Some(this) = this.upgrade(&cx) else { break };
558 Self::process_leader_update(this, leader_id, update, &mut cx)
559 .await
560 .log_err();
561 }
562
563 Ok(())
564 });
565
566 cx.emit_global(WorkspaceCreated(weak_handle.clone()));
567
568 let left_sidebar = cx.add_view(|_| Sidebar::new(SidebarSide::Left));
569 let right_sidebar = cx.add_view(|_| Sidebar::new(SidebarSide::Right));
570 let left_sidebar_buttons = cx.add_view(|cx| SidebarButtons::new(left_sidebar.clone(), cx));
571 let toggle_dock = cx.add_view(|cx| ToggleDockButton::new(handle, cx));
572 let right_sidebar_buttons =
573 cx.add_view(|cx| SidebarButtons::new(right_sidebar.clone(), cx));
574 let status_bar = cx.add_view(|cx| {
575 let mut status_bar = StatusBar::new(¢er_pane.clone(), cx);
576 status_bar.add_left_item(left_sidebar_buttons, cx);
577 status_bar.add_right_item(right_sidebar_buttons, cx);
578 status_bar.add_right_item(toggle_dock, cx);
579 status_bar
580 });
581
582 cx.update_default_global::<DragAndDrop<Workspace>, _, _>(|drag_and_drop, _| {
583 drag_and_drop.register_container(weak_handle.clone());
584 });
585
586 let mut active_call = None;
587 if cx.has_global::<ModelHandle<ActiveCall>>() {
588 let call = cx.global::<ModelHandle<ActiveCall>>().clone();
589 let mut subscriptions = Vec::new();
590 subscriptions.push(cx.subscribe(&call, Self::on_active_call_event));
591 active_call = Some((call, subscriptions));
592 }
593
594 let mut this = Workspace {
595 modal: None,
596 weak_self: weak_handle.clone(),
597 center: PaneGroup::new(center_pane.clone()),
598 dock,
599 // When removing an item, the last element remaining in this array
600 // is used to find where focus should fallback to. As such, the order
601 // of these two variables is important.
602 panes: vec![dock_pane.clone(), center_pane.clone()],
603 panes_by_item: Default::default(),
604 active_pane: center_pane.clone(),
605 last_active_center_pane: Some(center_pane.downgrade()),
606 status_bar,
607 titlebar_item: None,
608 notifications: Default::default(),
609 client,
610 remote_entity_subscription: None,
611 user_store,
612 fs,
613 left_sidebar,
614 right_sidebar,
615 project: project.clone(),
616 leader_state: Default::default(),
617 follower_states_by_leader: Default::default(),
618 last_leaders_by_pane: Default::default(),
619 window_edited: false,
620 active_call,
621 database_id: workspace_id,
622 _observe_current_user,
623 _apply_leader_updates,
624 leader_updates_tx,
625 };
626 this.project_remote_id_changed(project.read(cx).remote_id(), cx);
627 cx.defer(|this, cx| this.update_window_title(cx));
628
629 if let Some(serialized_workspace) = serialized_workspace {
630 cx.defer(move |_, cx| {
631 Self::load_from_serialized_workspace(weak_handle, serialized_workspace, cx)
632 });
633 }
634
635 this
636 }
637
638 fn new_local(
639 abs_paths: Vec<PathBuf>,
640 app_state: Arc<AppState>,
641 cx: &mut MutableAppContext,
642 ) -> Task<(
643 ViewHandle<Workspace>,
644 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
645 )> {
646 let project_handle = Project::local(
647 app_state.client.clone(),
648 app_state.user_store.clone(),
649 app_state.languages.clone(),
650 app_state.fs.clone(),
651 cx,
652 );
653
654 cx.spawn(|mut cx| async move {
655 let serialized_workspace = persistence::DB.workspace_for_roots(&abs_paths.as_slice());
656
657 let paths_to_open = serialized_workspace
658 .as_ref()
659 .map(|workspace| workspace.location.paths())
660 .unwrap_or(Arc::new(abs_paths));
661
662 // Get project paths for all of the abs_paths
663 let mut worktree_roots: HashSet<Arc<Path>> = Default::default();
664 let mut project_paths = Vec::new();
665 for path in paths_to_open.iter() {
666 if let Some((worktree, project_entry)) = cx
667 .update(|cx| {
668 Workspace::project_path_for_path(project_handle.clone(), &path, true, cx)
669 })
670 .await
671 .log_err()
672 {
673 worktree_roots.insert(worktree.read_with(&mut cx, |tree, _| tree.abs_path()));
674 project_paths.push(Some(project_entry));
675 } else {
676 project_paths.push(None);
677 }
678 }
679
680 let workspace_id = if let Some(serialized_workspace) = serialized_workspace.as_ref() {
681 serialized_workspace.id
682 } else {
683 DB.next_id().await.unwrap_or(0)
684 };
685
686 let (bounds, display) = serialized_workspace
687 .as_ref()
688 .and_then(|sw| sw.bounds.zip(sw.display))
689 .and_then(|(mut bounds, display)| {
690 // Stored bounds are relative to the containing display. So convert back to global coordinates if that screen still exists
691 if let WindowBounds::Fixed(mut window_bounds) = bounds {
692 if let Some(screen) = cx.platform().screen_by_id(display) {
693 let screen_bounds = screen.bounds();
694 window_bounds
695 .set_origin_x(window_bounds.origin_x() + screen_bounds.origin_x());
696 window_bounds
697 .set_origin_y(window_bounds.origin_y() + screen_bounds.origin_y());
698 bounds = WindowBounds::Fixed(window_bounds);
699 } else {
700 // Screen no longer exists. Return none here.
701 return None;
702 }
703 }
704
705 Some((bounds, display))
706 })
707 .unzip();
708
709 // Use the serialized workspace to construct the new window
710 let (_, workspace) = cx.add_window(
711 (app_state.build_window_options)(bounds, display, cx.platform().as_ref()),
712 |cx| {
713 let mut workspace = Workspace::new(
714 serialized_workspace,
715 workspace_id,
716 project_handle,
717 app_state.dock_default_item_factory,
718 cx,
719 );
720 (app_state.initialize_workspace)(&mut workspace, &app_state, cx);
721 cx.observe_window_bounds(move |_, mut bounds, display, cx| {
722 // Transform fixed bounds to be stored in terms of the containing display
723 if let WindowBounds::Fixed(mut window_bounds) = bounds {
724 if let Some(screen) = cx.platform().screen_by_id(display) {
725 let screen_bounds = screen.bounds();
726 window_bounds.set_origin_x(
727 window_bounds.origin_x() - screen_bounds.origin_x(),
728 );
729 window_bounds.set_origin_y(
730 window_bounds.origin_y() - screen_bounds.origin_y(),
731 );
732 bounds = WindowBounds::Fixed(window_bounds);
733 }
734 }
735
736 cx.background()
737 .spawn(DB.set_window_bounds(workspace_id, bounds, display))
738 .detach_and_log_err(cx);
739 })
740 .detach();
741 workspace
742 },
743 );
744
745 notify_if_database_failed(&workspace, &mut cx);
746
747 // Call open path for each of the project paths
748 // (this will bring them to the front if they were in the serialized workspace)
749 debug_assert!(paths_to_open.len() == project_paths.len());
750 let tasks = paths_to_open
751 .iter()
752 .cloned()
753 .zip(project_paths.into_iter())
754 .map(|(abs_path, project_path)| {
755 let workspace = workspace.clone();
756 cx.spawn(|mut cx| {
757 let fs = app_state.fs.clone();
758 async move {
759 let project_path = project_path?;
760 if fs.is_file(&abs_path).await {
761 Some(
762 workspace
763 .update(&mut cx, |workspace, cx| {
764 workspace.open_path(project_path, None, true, cx)
765 })
766 .await,
767 )
768 } else {
769 None
770 }
771 }
772 })
773 });
774
775 let opened_items = futures::future::join_all(tasks.into_iter()).await;
776
777 (workspace, opened_items)
778 })
779 }
780
781 pub fn weak_handle(&self) -> WeakViewHandle<Self> {
782 self.weak_self.clone()
783 }
784
785 pub fn left_sidebar(&self) -> &ViewHandle<Sidebar> {
786 &self.left_sidebar
787 }
788
789 pub fn right_sidebar(&self) -> &ViewHandle<Sidebar> {
790 &self.right_sidebar
791 }
792
793 pub fn status_bar(&self) -> &ViewHandle<StatusBar> {
794 &self.status_bar
795 }
796
797 pub fn user_store(&self) -> &ModelHandle<UserStore> {
798 &self.user_store
799 }
800
801 pub fn project(&self) -> &ModelHandle<Project> {
802 &self.project
803 }
804
805 pub fn client(&self) -> &Arc<Client> {
806 &self.client
807 }
808
809 pub fn set_titlebar_item(
810 &mut self,
811 item: impl Into<AnyViewHandle>,
812 cx: &mut ViewContext<Self>,
813 ) {
814 self.titlebar_item = Some(item.into());
815 cx.notify();
816 }
817
818 pub fn titlebar_item(&self) -> Option<AnyViewHandle> {
819 self.titlebar_item.clone()
820 }
821
822 /// Call the given callback with a workspace whose project is local.
823 ///
824 /// If the given workspace has a local project, then it will be passed
825 /// to the callback. Otherwise, a new empty window will be created.
826 pub fn with_local_workspace<T, F>(
827 &mut self,
828 app_state: &Arc<AppState>,
829 cx: &mut ViewContext<Self>,
830 callback: F,
831 ) -> Task<T>
832 where
833 T: 'static,
834 F: 'static + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
835 {
836 if self.project.read(cx).is_local() {
837 Task::Ready(Some(callback(self, cx)))
838 } else {
839 let task = Self::new_local(Vec::new(), app_state.clone(), cx);
840 cx.spawn(|_vh, mut cx| async move {
841 let (workspace, _) = task.await;
842 workspace.update(&mut cx, callback)
843 })
844 }
845 }
846
847 pub fn worktrees<'a>(
848 &self,
849 cx: &'a AppContext,
850 ) -> impl 'a + Iterator<Item = ModelHandle<Worktree>> {
851 self.project.read(cx).worktrees(cx)
852 }
853
854 pub fn visible_worktrees<'a>(
855 &self,
856 cx: &'a AppContext,
857 ) -> impl 'a + Iterator<Item = ModelHandle<Worktree>> {
858 self.project.read(cx).visible_worktrees(cx)
859 }
860
861 pub fn worktree_scans_complete(&self, cx: &AppContext) -> impl Future<Output = ()> + 'static {
862 let futures = self
863 .worktrees(cx)
864 .filter_map(|worktree| worktree.read(cx).as_local())
865 .map(|worktree| worktree.scan_complete())
866 .collect::<Vec<_>>();
867 async move {
868 for future in futures {
869 future.await;
870 }
871 }
872 }
873
874 pub fn close_global(_: &CloseWindow, cx: &mut MutableAppContext) {
875 let id = cx.window_ids().find(|&id| cx.window_is_active(id));
876 if let Some(id) = id {
877 //This can only get called when the window's project connection has been lost
878 //so we don't need to prompt the user for anything and instead just close the window
879 cx.remove_window(id);
880 }
881 }
882
883 pub fn close(
884 &mut self,
885 _: &CloseWindow,
886 cx: &mut ViewContext<Self>,
887 ) -> Option<Task<Result<()>>> {
888 let prepare = self.prepare_to_close(false, cx);
889 Some(cx.spawn(|this, mut cx| async move {
890 if prepare.await? {
891 this.update(&mut cx, |_, cx| {
892 let window_id = cx.window_id();
893 cx.remove_window(window_id);
894 });
895 }
896 Ok(())
897 }))
898 }
899
900 pub fn prepare_to_close(
901 &mut self,
902 quitting: bool,
903 cx: &mut ViewContext<Self>,
904 ) -> Task<Result<bool>> {
905 let active_call = self.active_call().cloned();
906 let window_id = cx.window_id();
907 let workspace_count = cx
908 .window_ids()
909 .flat_map(|window_id| cx.root_view::<Workspace>(window_id))
910 .count();
911
912 cx.spawn(|this, mut cx| async move {
913 if let Some(active_call) = active_call {
914 if !quitting
915 && workspace_count == 1
916 && active_call.read_with(&cx, |call, _| call.room().is_some())
917 {
918 let answer = cx
919 .prompt(
920 window_id,
921 PromptLevel::Warning,
922 "Do you want to leave the current call?",
923 &["Close window and hang up", "Cancel"],
924 )
925 .next()
926 .await;
927
928 if answer == Some(1) {
929 return anyhow::Ok(false);
930 } else {
931 active_call.update(&mut cx, |call, cx| call.hang_up(cx))?;
932 }
933 }
934 }
935
936 Ok(this
937 .update(&mut cx, |this, cx| this.save_all_internal(true, cx))
938 .await?)
939 })
940 }
941
942 fn save_all(&mut self, _: &SaveAll, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
943 let save_all = self.save_all_internal(false, cx);
944 Some(cx.foreground().spawn(async move {
945 save_all.await?;
946 Ok(())
947 }))
948 }
949
950 fn save_all_internal(
951 &mut self,
952 should_prompt_to_save: bool,
953 cx: &mut ViewContext<Self>,
954 ) -> Task<Result<bool>> {
955 if self.project.read(cx).is_read_only() {
956 return Task::ready(Ok(true));
957 }
958
959 let dirty_items = self
960 .panes
961 .iter()
962 .flat_map(|pane| {
963 pane.read(cx).items().filter_map(|item| {
964 if item.is_dirty(cx) {
965 Some((pane.clone(), item.boxed_clone()))
966 } else {
967 None
968 }
969 })
970 })
971 .collect::<Vec<_>>();
972
973 let project = self.project.clone();
974 cx.spawn_weak(|_, mut cx| async move {
975 for (pane, item) in dirty_items {
976 let (singleton, project_entry_ids) =
977 cx.read(|cx| (item.is_singleton(cx), item.project_entry_ids(cx)));
978 if singleton || !project_entry_ids.is_empty() {
979 if let Some(ix) =
980 pane.read_with(&cx, |pane, _| pane.index_for_item(item.as_ref()))
981 {
982 if !Pane::save_item(
983 project.clone(),
984 &pane,
985 ix,
986 &*item,
987 should_prompt_to_save,
988 &mut cx,
989 )
990 .await?
991 {
992 return Ok(false);
993 }
994 }
995 }
996 }
997 Ok(true)
998 })
999 }
1000
1001 #[allow(clippy::type_complexity)]
1002 pub fn open_paths(
1003 &mut self,
1004 mut abs_paths: Vec<PathBuf>,
1005 visible: bool,
1006 cx: &mut ViewContext<Self>,
1007 ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
1008 let fs = self.fs.clone();
1009
1010 // Sort the paths to ensure we add worktrees for parents before their children.
1011 abs_paths.sort_unstable();
1012 cx.spawn(|this, mut cx| async move {
1013 let mut project_paths = Vec::new();
1014 for path in &abs_paths {
1015 project_paths.push(
1016 this.update(&mut cx, |this, cx| {
1017 Workspace::project_path_for_path(this.project.clone(), path, visible, cx)
1018 })
1019 .await
1020 .log_err(),
1021 );
1022 }
1023
1024 let tasks = abs_paths
1025 .iter()
1026 .cloned()
1027 .zip(project_paths.into_iter())
1028 .map(|(abs_path, project_path)| {
1029 let this = this.clone();
1030 cx.spawn(|mut cx| {
1031 let fs = fs.clone();
1032 async move {
1033 let (_worktree, project_path) = project_path?;
1034 if fs.is_file(&abs_path).await {
1035 Some(
1036 this.update(&mut cx, |this, cx| {
1037 this.open_path(project_path, None, true, cx)
1038 })
1039 .await,
1040 )
1041 } else {
1042 None
1043 }
1044 }
1045 })
1046 })
1047 .collect::<Vec<_>>();
1048
1049 futures::future::join_all(tasks).await
1050 })
1051 }
1052
1053 fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext<Self>) {
1054 let mut paths = cx.prompt_for_paths(PathPromptOptions {
1055 files: false,
1056 directories: true,
1057 multiple: true,
1058 });
1059 cx.spawn(|this, mut cx| async move {
1060 if let Some(paths) = paths.recv().await.flatten() {
1061 let results = this
1062 .update(&mut cx, |this, cx| this.open_paths(paths, true, cx))
1063 .await;
1064 for result in results.into_iter().flatten() {
1065 result.log_err();
1066 }
1067 }
1068 })
1069 .detach();
1070 }
1071
1072 fn remove_folder_from_project(
1073 &mut self,
1074 RemoveWorktreeFromProject(worktree_id): &RemoveWorktreeFromProject,
1075 cx: &mut ViewContext<Self>,
1076 ) {
1077 let future = self
1078 .project
1079 .update(cx, |project, cx| project.remove_worktree(*worktree_id, cx));
1080 cx.foreground().spawn(future).detach();
1081 }
1082
1083 fn project_path_for_path(
1084 project: ModelHandle<Project>,
1085 abs_path: &Path,
1086 visible: bool,
1087 cx: &mut MutableAppContext,
1088 ) -> Task<Result<(ModelHandle<Worktree>, ProjectPath)>> {
1089 let entry = project.update(cx, |project, cx| {
1090 project.find_or_create_local_worktree(abs_path, visible, cx)
1091 });
1092 cx.spawn(|cx| async move {
1093 let (worktree, path) = entry.await?;
1094 let worktree_id = worktree.read_with(&cx, |t, _| t.id());
1095 Ok((
1096 worktree,
1097 ProjectPath {
1098 worktree_id,
1099 path: path.into(),
1100 },
1101 ))
1102 })
1103 }
1104
1105 /// Returns the modal that was toggled closed if it was open.
1106 pub fn toggle_modal<V, F>(
1107 &mut self,
1108 cx: &mut ViewContext<Self>,
1109 add_view: F,
1110 ) -> Option<ViewHandle<V>>
1111 where
1112 V: 'static + View,
1113 F: FnOnce(&mut Self, &mut ViewContext<Self>) -> ViewHandle<V>,
1114 {
1115 cx.notify();
1116 // Whatever modal was visible is getting clobbered. If its the same type as V, then return
1117 // it. Otherwise, create a new modal and set it as active.
1118 let already_open_modal = self.modal.take().and_then(|modal| modal.downcast::<V>());
1119 if let Some(already_open_modal) = already_open_modal {
1120 cx.focus_self();
1121 Some(already_open_modal)
1122 } else {
1123 let modal = add_view(self, cx);
1124 cx.focus(&modal);
1125 self.modal = Some(modal.into());
1126 None
1127 }
1128 }
1129
1130 pub fn modal<V: 'static + View>(&self) -> Option<ViewHandle<V>> {
1131 self.modal
1132 .as_ref()
1133 .and_then(|modal| modal.clone().downcast::<V>())
1134 }
1135
1136 pub fn dismiss_modal(&mut self, cx: &mut ViewContext<Self>) {
1137 if self.modal.take().is_some() {
1138 cx.focus(&self.active_pane);
1139 cx.notify();
1140 }
1141 }
1142
1143 pub fn items<'a>(
1144 &'a self,
1145 cx: &'a AppContext,
1146 ) -> impl 'a + Iterator<Item = &Box<dyn ItemHandle>> {
1147 self.panes.iter().flat_map(|pane| pane.read(cx).items())
1148 }
1149
1150 pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<ViewHandle<T>> {
1151 self.items_of_type(cx).max_by_key(|item| item.id())
1152 }
1153
1154 pub fn items_of_type<'a, T: Item>(
1155 &'a self,
1156 cx: &'a AppContext,
1157 ) -> impl 'a + Iterator<Item = ViewHandle<T>> {
1158 self.panes
1159 .iter()
1160 .flat_map(|pane| pane.read(cx).items_of_type())
1161 }
1162
1163 pub fn active_item(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
1164 self.active_pane().read(cx).active_item()
1165 }
1166
1167 fn active_project_path(&self, cx: &ViewContext<Self>) -> Option<ProjectPath> {
1168 self.active_item(cx).and_then(|item| item.project_path(cx))
1169 }
1170
1171 pub fn save_active_item(
1172 &mut self,
1173 force_name_change: bool,
1174 cx: &mut ViewContext<Self>,
1175 ) -> Task<Result<()>> {
1176 let project = self.project.clone();
1177 if let Some(item) = self.active_item(cx) {
1178 if !force_name_change && item.can_save(cx) {
1179 if item.has_conflict(cx.as_ref()) {
1180 const CONFLICT_MESSAGE: &str = "This file has changed on disk since you started editing it. Do you want to overwrite it?";
1181
1182 let mut answer = cx.prompt(
1183 PromptLevel::Warning,
1184 CONFLICT_MESSAGE,
1185 &["Overwrite", "Cancel"],
1186 );
1187 cx.spawn(|_, mut cx| async move {
1188 let answer = answer.recv().await;
1189 if answer == Some(0) {
1190 cx.update(|cx| item.save(project, cx)).await?;
1191 }
1192 Ok(())
1193 })
1194 } else {
1195 item.save(project, cx)
1196 }
1197 } else if item.is_singleton(cx) {
1198 let worktree = self.worktrees(cx).next();
1199 let start_abs_path = worktree
1200 .and_then(|w| w.read(cx).as_local())
1201 .map_or(Path::new(""), |w| w.abs_path())
1202 .to_path_buf();
1203 let mut abs_path = cx.prompt_for_new_path(&start_abs_path);
1204 cx.spawn(|_, mut cx| async move {
1205 if let Some(abs_path) = abs_path.recv().await.flatten() {
1206 cx.update(|cx| item.save_as(project, abs_path, cx)).await?;
1207 }
1208 Ok(())
1209 })
1210 } else {
1211 Task::ready(Ok(()))
1212 }
1213 } else {
1214 Task::ready(Ok(()))
1215 }
1216 }
1217
1218 pub fn toggle_sidebar(&mut self, sidebar_side: SidebarSide, cx: &mut ViewContext<Self>) {
1219 let sidebar = match sidebar_side {
1220 SidebarSide::Left => &mut self.left_sidebar,
1221 SidebarSide::Right => &mut self.right_sidebar,
1222 };
1223 let open = sidebar.update(cx, |sidebar, cx| {
1224 let open = !sidebar.is_open();
1225 sidebar.set_open(open, cx);
1226 open
1227 });
1228
1229 if open {
1230 Dock::hide_on_sidebar_shown(self, sidebar_side, cx);
1231 }
1232
1233 self.serialize_workspace(cx);
1234
1235 cx.focus_self();
1236 cx.notify();
1237 }
1238
1239 pub fn toggle_sidebar_item(&mut self, action: &ToggleSidebarItem, cx: &mut ViewContext<Self>) {
1240 let sidebar = match action.sidebar_side {
1241 SidebarSide::Left => &mut self.left_sidebar,
1242 SidebarSide::Right => &mut self.right_sidebar,
1243 };
1244 let active_item = sidebar.update(cx, move |sidebar, cx| {
1245 if sidebar.is_open() && sidebar.active_item_ix() == action.item_index {
1246 sidebar.set_open(false, cx);
1247 None
1248 } else {
1249 sidebar.set_open(true, cx);
1250 sidebar.activate_item(action.item_index, cx);
1251 sidebar.active_item().cloned()
1252 }
1253 });
1254
1255 if let Some(active_item) = active_item {
1256 Dock::hide_on_sidebar_shown(self, action.sidebar_side, cx);
1257
1258 if active_item.is_focused(cx) {
1259 cx.focus_self();
1260 } else {
1261 cx.focus(active_item.to_any());
1262 }
1263 } else {
1264 cx.focus_self();
1265 }
1266
1267 self.serialize_workspace(cx);
1268
1269 cx.notify();
1270 }
1271
1272 pub fn toggle_sidebar_item_focus(
1273 &mut self,
1274 sidebar_side: SidebarSide,
1275 item_index: usize,
1276 cx: &mut ViewContext<Self>,
1277 ) {
1278 let sidebar = match sidebar_side {
1279 SidebarSide::Left => &mut self.left_sidebar,
1280 SidebarSide::Right => &mut self.right_sidebar,
1281 };
1282 let active_item = sidebar.update(cx, |sidebar, cx| {
1283 sidebar.set_open(true, cx);
1284 sidebar.activate_item(item_index, cx);
1285 sidebar.active_item().cloned()
1286 });
1287 if let Some(active_item) = active_item {
1288 Dock::hide_on_sidebar_shown(self, sidebar_side, cx);
1289
1290 if active_item.is_focused(cx) {
1291 cx.focus_self();
1292 } else {
1293 cx.focus(active_item.to_any());
1294 }
1295 }
1296
1297 self.serialize_workspace(cx);
1298
1299 cx.notify();
1300 }
1301
1302 pub fn focus_center(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
1303 cx.focus_self();
1304 cx.notify();
1305 }
1306
1307 fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
1308 let pane = cx.add_view(|cx| Pane::new(None, cx));
1309 let pane_id = pane.id();
1310 cx.subscribe(&pane, move |this, _, event, cx| {
1311 this.handle_pane_event(pane_id, event, cx)
1312 })
1313 .detach();
1314 self.panes.push(pane.clone());
1315 cx.focus(pane.clone());
1316 cx.emit(Event::PaneAdded(pane.clone()));
1317 pane
1318 }
1319
1320 pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
1321 let active_pane = self.active_pane().clone();
1322 Pane::add_item(self, &active_pane, item, true, true, None, cx);
1323 }
1324
1325 pub fn open_path(
1326 &mut self,
1327 path: impl Into<ProjectPath>,
1328 pane: Option<WeakViewHandle<Pane>>,
1329 focus_item: bool,
1330 cx: &mut ViewContext<Self>,
1331 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
1332 let pane = pane.unwrap_or_else(|| self.active_pane().downgrade());
1333 let task = self.load_path(path.into(), cx);
1334 cx.spawn(|this, mut cx| async move {
1335 let (project_entry_id, build_item) = task.await?;
1336 let pane = pane
1337 .upgrade(&cx)
1338 .ok_or_else(|| anyhow!("pane was closed"))?;
1339 this.update(&mut cx, |this, cx| {
1340 Ok(Pane::open_item(
1341 this,
1342 pane,
1343 project_entry_id,
1344 focus_item,
1345 cx,
1346 build_item,
1347 ))
1348 })
1349 })
1350 }
1351
1352 pub(crate) fn load_path(
1353 &mut self,
1354 path: ProjectPath,
1355 cx: &mut ViewContext<Self>,
1356 ) -> Task<
1357 Result<(
1358 ProjectEntryId,
1359 impl 'static + FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
1360 )>,
1361 > {
1362 let project = self.project().clone();
1363 let project_item = project.update(cx, |project, cx| project.open_path(path, cx));
1364 cx.as_mut().spawn(|mut cx| async move {
1365 let (project_entry_id, project_item) = project_item.await?;
1366 let build_item = cx.update(|cx| {
1367 cx.default_global::<ProjectItemBuilders>()
1368 .get(&project_item.model_type())
1369 .ok_or_else(|| anyhow!("no item builder for project item"))
1370 .cloned()
1371 })?;
1372 let build_item =
1373 move |cx: &mut ViewContext<Pane>| build_item(project, project_item, cx);
1374 Ok((project_entry_id, build_item))
1375 })
1376 }
1377
1378 pub fn open_project_item<T>(
1379 &mut self,
1380 project_item: ModelHandle<T::Item>,
1381 cx: &mut ViewContext<Self>,
1382 ) -> ViewHandle<T>
1383 where
1384 T: ProjectItem,
1385 {
1386 use project::Item as _;
1387
1388 let entry_id = project_item.read(cx).entry_id(cx);
1389 if let Some(item) = entry_id
1390 .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx))
1391 .and_then(|item| item.downcast())
1392 {
1393 self.activate_item(&item, cx);
1394 return item;
1395 }
1396
1397 let item = cx.add_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
1398 self.add_item(Box::new(item.clone()), cx);
1399 item
1400 }
1401
1402 pub fn open_shared_screen(&mut self, action: &OpenSharedScreen, cx: &mut ViewContext<Self>) {
1403 if let Some(shared_screen) =
1404 self.shared_screen_for_peer(action.peer_id, &self.active_pane, cx)
1405 {
1406 let pane = self.active_pane.clone();
1407 Pane::add_item(self, &pane, Box::new(shared_screen), false, true, None, cx);
1408 }
1409 }
1410
1411 pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut ViewContext<Self>) -> bool {
1412 let result = self.panes.iter().find_map(|pane| {
1413 pane.read(cx)
1414 .index_for_item(item)
1415 .map(|ix| (pane.clone(), ix))
1416 });
1417 if let Some((pane, ix)) = result {
1418 pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx));
1419 true
1420 } else {
1421 false
1422 }
1423 }
1424
1425 fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext<Self>) {
1426 let panes = self.center.panes();
1427 if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
1428 cx.focus(pane);
1429 } else {
1430 self.split_pane(self.active_pane.clone(), SplitDirection::Right, cx);
1431 }
1432 }
1433
1434 pub fn activate_next_pane(&mut self, cx: &mut ViewContext<Self>) {
1435 let panes = self.center.panes();
1436 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
1437 let next_ix = (ix + 1) % panes.len();
1438 let next_pane = panes[next_ix].clone();
1439 cx.focus(next_pane);
1440 }
1441 }
1442
1443 pub fn activate_previous_pane(&mut self, cx: &mut ViewContext<Self>) {
1444 let panes = self.center.panes();
1445 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
1446 let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
1447 let prev_pane = panes[prev_ix].clone();
1448 cx.focus(prev_pane);
1449 }
1450 }
1451
1452 fn handle_pane_focused(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
1453 if self.active_pane != pane {
1454 self.active_pane
1455 .update(cx, |pane, cx| pane.set_active(false, cx));
1456 self.active_pane = pane.clone();
1457 self.active_pane
1458 .update(cx, |pane, cx| pane.set_active(true, cx));
1459 self.status_bar.update(cx, |status_bar, cx| {
1460 status_bar.set_active_pane(&self.active_pane, cx);
1461 });
1462 self.active_item_path_changed(cx);
1463
1464 if &pane == self.dock_pane() {
1465 Dock::show(self, cx);
1466 } else {
1467 self.last_active_center_pane = Some(pane.downgrade());
1468 if self.dock.is_anchored_at(DockAnchor::Expanded) {
1469 Dock::hide(self, cx);
1470 }
1471 }
1472 cx.notify();
1473 }
1474
1475 self.update_followers(
1476 proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
1477 id: self.active_item(cx).and_then(|item| {
1478 item.to_followable_item_handle(cx)?
1479 .remote_id(&self.client, cx)
1480 .map(|id| id.to_proto())
1481 }),
1482 leader_id: self.leader_for_pane(&pane),
1483 }),
1484 cx,
1485 );
1486 }
1487
1488 fn handle_pane_event(
1489 &mut self,
1490 pane_id: usize,
1491 event: &pane::Event,
1492 cx: &mut ViewContext<Self>,
1493 ) {
1494 if let Some(pane) = self.pane(pane_id) {
1495 let is_dock = &pane == self.dock.pane();
1496 match event {
1497 pane::Event::Split(direction) if !is_dock => {
1498 self.split_pane(pane, *direction, cx);
1499 }
1500 pane::Event::Remove if !is_dock => self.remove_pane(pane, cx),
1501 pane::Event::Remove if is_dock => Dock::hide(self, cx),
1502 pane::Event::ActivateItem { local } => {
1503 if *local {
1504 self.unfollow(&pane, cx);
1505 }
1506 if &pane == self.active_pane() {
1507 self.active_item_path_changed(cx);
1508 }
1509 }
1510 pane::Event::ChangeItemTitle => {
1511 if pane == self.active_pane {
1512 self.active_item_path_changed(cx);
1513 }
1514 self.update_window_edited(cx);
1515 }
1516 pane::Event::RemoveItem { item_id } => {
1517 self.update_window_edited(cx);
1518 if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) {
1519 if entry.get().id() == pane.id() {
1520 entry.remove();
1521 }
1522 }
1523 }
1524 _ => {}
1525 }
1526
1527 self.serialize_workspace(cx);
1528 } else if self.dock.visible_pane().is_none() {
1529 error!("pane {} not found", pane_id);
1530 }
1531 }
1532
1533 pub fn split_pane(
1534 &mut self,
1535 pane: ViewHandle<Pane>,
1536 direction: SplitDirection,
1537 cx: &mut ViewContext<Self>,
1538 ) -> Option<ViewHandle<Pane>> {
1539 if &pane == self.dock_pane() {
1540 warn!("Can't split dock pane.");
1541 return None;
1542 }
1543
1544 let item = pane.read(cx).active_item()?;
1545 let new_pane = self.add_pane(cx);
1546 if let Some(clone) = item.clone_on_split(self.database_id(), cx.as_mut()) {
1547 Pane::add_item(self, &new_pane, clone, true, true, None, cx);
1548 }
1549 self.center.split(&pane, &new_pane, direction).unwrap();
1550 cx.notify();
1551 Some(new_pane)
1552 }
1553
1554 pub fn split_pane_with_item(&mut self, action: &SplitWithItem, cx: &mut ViewContext<Self>) {
1555 let Some(pane_to_split) = action.pane_to_split.upgrade(cx) else { return; };
1556 let Some(from) = action.from.upgrade(cx) else { return; };
1557 if &pane_to_split == self.dock_pane() {
1558 warn!("Can't split dock pane.");
1559 return;
1560 }
1561
1562 let new_pane = self.add_pane(cx);
1563 Pane::move_item(
1564 self,
1565 from.clone(),
1566 new_pane.clone(),
1567 action.item_id_to_move,
1568 0,
1569 cx,
1570 );
1571 self.center
1572 .split(&pane_to_split, &new_pane, action.split_direction)
1573 .unwrap();
1574 cx.notify();
1575 }
1576
1577 pub fn split_pane_with_project_entry(
1578 &mut self,
1579 action: &SplitWithProjectEntry,
1580 cx: &mut ViewContext<Self>,
1581 ) -> Option<Task<Result<()>>> {
1582 let pane_to_split = action.pane_to_split.upgrade(cx)?;
1583 if &pane_to_split == self.dock_pane() {
1584 warn!("Can't split dock pane.");
1585 return None;
1586 }
1587
1588 let new_pane = self.add_pane(cx);
1589 self.center
1590 .split(&pane_to_split, &new_pane, action.split_direction)
1591 .unwrap();
1592
1593 let path = self
1594 .project
1595 .read(cx)
1596 .path_for_entry(action.project_entry, cx)?;
1597 let task = self.open_path(path, Some(new_pane.downgrade()), true, cx);
1598 Some(cx.foreground().spawn(async move {
1599 task.await?;
1600 Ok(())
1601 }))
1602 }
1603
1604 fn remove_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
1605 if self.center.remove(&pane).unwrap() {
1606 self.panes.retain(|p| p != &pane);
1607 cx.focus(self.panes.last().unwrap().clone());
1608 self.unfollow(&pane, cx);
1609 self.last_leaders_by_pane.remove(&pane.downgrade());
1610 for removed_item in pane.read(cx).items() {
1611 self.panes_by_item.remove(&removed_item.id());
1612 }
1613 if self.last_active_center_pane == Some(pane.downgrade()) {
1614 self.last_active_center_pane = None;
1615 }
1616
1617 cx.notify();
1618 } else {
1619 self.active_item_path_changed(cx);
1620 }
1621 }
1622
1623 pub fn panes(&self) -> &[ViewHandle<Pane>] {
1624 &self.panes
1625 }
1626
1627 fn pane(&self, pane_id: usize) -> Option<ViewHandle<Pane>> {
1628 self.panes.iter().find(|pane| pane.id() == pane_id).cloned()
1629 }
1630
1631 pub fn active_pane(&self) -> &ViewHandle<Pane> {
1632 &self.active_pane
1633 }
1634
1635 pub fn dock_pane(&self) -> &ViewHandle<Pane> {
1636 self.dock.pane()
1637 }
1638
1639 fn project_remote_id_changed(&mut self, remote_id: Option<u64>, cx: &mut ViewContext<Self>) {
1640 if let Some(remote_id) = remote_id {
1641 self.remote_entity_subscription =
1642 Some(self.client.add_view_for_remote_entity(remote_id, cx));
1643 } else {
1644 self.remote_entity_subscription.take();
1645 }
1646 }
1647
1648 fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
1649 self.leader_state.followers.remove(&peer_id);
1650 if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) {
1651 for state in states_by_pane.into_values() {
1652 for item in state.items_by_leader_view_id.into_values() {
1653 item.set_leader_replica_id(None, cx);
1654 }
1655 }
1656 }
1657 cx.notify();
1658 }
1659
1660 pub fn toggle_follow(
1661 &mut self,
1662 ToggleFollow(leader_id): &ToggleFollow,
1663 cx: &mut ViewContext<Self>,
1664 ) -> Option<Task<Result<()>>> {
1665 let leader_id = *leader_id;
1666 let pane = self.active_pane().clone();
1667
1668 if let Some(prev_leader_id) = self.unfollow(&pane, cx) {
1669 if leader_id == prev_leader_id {
1670 return None;
1671 }
1672 }
1673
1674 self.last_leaders_by_pane
1675 .insert(pane.downgrade(), leader_id);
1676 self.follower_states_by_leader
1677 .entry(leader_id)
1678 .or_default()
1679 .insert(pane.clone(), Default::default());
1680 cx.notify();
1681
1682 let project_id = self.project.read(cx).remote_id()?;
1683 let request = self.client.request(proto::Follow {
1684 project_id,
1685 leader_id: Some(leader_id),
1686 });
1687
1688 Some(cx.spawn_weak(|this, mut cx| async move {
1689 let response = request.await?;
1690 if let Some(this) = this.upgrade(&cx) {
1691 this.update(&mut cx, |this, _| {
1692 let state = this
1693 .follower_states_by_leader
1694 .get_mut(&leader_id)
1695 .and_then(|states_by_pane| states_by_pane.get_mut(&pane))
1696 .ok_or_else(|| anyhow!("following interrupted"))?;
1697 state.active_view_id = if let Some(active_view_id) = response.active_view_id {
1698 Some(ViewId::from_proto(active_view_id)?)
1699 } else {
1700 None
1701 };
1702 Ok::<_, anyhow::Error>(())
1703 })?;
1704 Self::add_views_from_leader(
1705 this.clone(),
1706 leader_id,
1707 vec![pane],
1708 response.views,
1709 &mut cx,
1710 )
1711 .await?;
1712 this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx));
1713 }
1714 Ok(())
1715 }))
1716 }
1717
1718 pub fn follow_next_collaborator(
1719 &mut self,
1720 _: &FollowNextCollaborator,
1721 cx: &mut ViewContext<Self>,
1722 ) -> Option<Task<Result<()>>> {
1723 let collaborators = self.project.read(cx).collaborators();
1724 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
1725 let mut collaborators = collaborators.keys().copied();
1726 for peer_id in collaborators.by_ref() {
1727 if peer_id == leader_id {
1728 break;
1729 }
1730 }
1731 collaborators.next()
1732 } else if let Some(last_leader_id) =
1733 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
1734 {
1735 if collaborators.contains_key(last_leader_id) {
1736 Some(*last_leader_id)
1737 } else {
1738 None
1739 }
1740 } else {
1741 None
1742 };
1743
1744 next_leader_id
1745 .or_else(|| collaborators.keys().copied().next())
1746 .and_then(|leader_id| self.toggle_follow(&ToggleFollow(leader_id), cx))
1747 }
1748
1749 pub fn unfollow(
1750 &mut self,
1751 pane: &ViewHandle<Pane>,
1752 cx: &mut ViewContext<Self>,
1753 ) -> Option<PeerId> {
1754 for (leader_id, states_by_pane) in &mut self.follower_states_by_leader {
1755 let leader_id = *leader_id;
1756 if let Some(state) = states_by_pane.remove(pane) {
1757 for (_, item) in state.items_by_leader_view_id {
1758 item.set_leader_replica_id(None, cx);
1759 }
1760
1761 if states_by_pane.is_empty() {
1762 self.follower_states_by_leader.remove(&leader_id);
1763 if let Some(project_id) = self.project.read(cx).remote_id() {
1764 self.client
1765 .send(proto::Unfollow {
1766 project_id,
1767 leader_id: Some(leader_id),
1768 })
1769 .log_err();
1770 }
1771 }
1772
1773 cx.notify();
1774 return Some(leader_id);
1775 }
1776 }
1777 None
1778 }
1779
1780 pub fn is_following(&self, peer_id: PeerId) -> bool {
1781 self.follower_states_by_leader.contains_key(&peer_id)
1782 }
1783
1784 pub fn is_followed(&self, peer_id: PeerId) -> bool {
1785 self.leader_state.followers.contains(&peer_id)
1786 }
1787
1788 fn render_titlebar(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
1789 let project = &self.project.read(cx);
1790 let mut worktree_root_names = String::new();
1791 for (i, name) in project.worktree_root_names(cx).enumerate() {
1792 if i > 0 {
1793 worktree_root_names.push_str(", ");
1794 }
1795 worktree_root_names.push_str(name);
1796 }
1797
1798 // TODO: There should be a better system in place for this
1799 // (https://github.com/zed-industries/zed/issues/1290)
1800 let is_fullscreen = cx.window_is_fullscreen(cx.window_id());
1801 let container_theme = if is_fullscreen {
1802 let mut container_theme = theme.workspace.titlebar.container;
1803 container_theme.padding.left = container_theme.padding.right;
1804 container_theme
1805 } else {
1806 theme.workspace.titlebar.container
1807 };
1808
1809 enum TitleBar {}
1810 ConstrainedBox::new(
1811 MouseEventHandler::<TitleBar>::new(0, cx, |_, cx| {
1812 Container::new(
1813 Stack::new()
1814 .with_child(
1815 Label::new(worktree_root_names, theme.workspace.titlebar.title.clone())
1816 .aligned()
1817 .left()
1818 .boxed(),
1819 )
1820 .with_children(
1821 self.titlebar_item
1822 .as_ref()
1823 .map(|item| ChildView::new(item, cx).aligned().right().boxed()),
1824 )
1825 .boxed(),
1826 )
1827 .with_style(container_theme)
1828 .boxed()
1829 })
1830 .on_click(MouseButton::Left, |event, cx| {
1831 if event.click_count == 2 {
1832 cx.zoom_window(cx.window_id());
1833 }
1834 })
1835 .boxed(),
1836 )
1837 .with_height(theme.workspace.titlebar.height)
1838 .named("titlebar")
1839 }
1840
1841 fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
1842 let active_entry = self.active_project_path(cx);
1843 self.project
1844 .update(cx, |project, cx| project.set_active_path(active_entry, cx));
1845 self.update_window_title(cx);
1846 }
1847
1848 fn update_window_title(&mut self, cx: &mut ViewContext<Self>) {
1849 let mut title = String::new();
1850 let project = self.project().read(cx);
1851 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
1852 let filename = path
1853 .path
1854 .file_name()
1855 .map(|s| s.to_string_lossy())
1856 .or_else(|| {
1857 Some(Cow::Borrowed(
1858 project
1859 .worktree_for_id(path.worktree_id, cx)?
1860 .read(cx)
1861 .root_name(),
1862 ))
1863 });
1864 if let Some(filename) = filename {
1865 title.push_str(filename.as_ref());
1866 title.push_str(" — ");
1867 }
1868 }
1869 for (i, name) in project.worktree_root_names(cx).enumerate() {
1870 if i > 0 {
1871 title.push_str(", ");
1872 }
1873 title.push_str(name);
1874 }
1875 if title.is_empty() {
1876 title = "empty project".to_string();
1877 }
1878 cx.set_window_title(&title);
1879 }
1880
1881 fn update_window_edited(&mut self, cx: &mut ViewContext<Self>) {
1882 let is_edited = !self.project.read(cx).is_read_only()
1883 && self
1884 .items(cx)
1885 .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
1886 if is_edited != self.window_edited {
1887 self.window_edited = is_edited;
1888 cx.set_window_edited(self.window_edited)
1889 }
1890 }
1891
1892 fn render_disconnected_overlay(&self, cx: &mut RenderContext<Workspace>) -> Option<ElementBox> {
1893 if self.project.read(cx).is_read_only() {
1894 enum DisconnectedOverlay {}
1895 Some(
1896 MouseEventHandler::<DisconnectedOverlay>::new(0, cx, |_, cx| {
1897 let theme = &cx.global::<Settings>().theme;
1898 Label::new(
1899 "Your connection to the remote project has been lost.".to_string(),
1900 theme.workspace.disconnected_overlay.text.clone(),
1901 )
1902 .aligned()
1903 .contained()
1904 .with_style(theme.workspace.disconnected_overlay.container)
1905 .boxed()
1906 })
1907 .with_cursor_style(CursorStyle::Arrow)
1908 .capture_all()
1909 .boxed(),
1910 )
1911 } else {
1912 None
1913 }
1914 }
1915
1916 fn render_notifications(
1917 &self,
1918 theme: &theme::Workspace,
1919 cx: &AppContext,
1920 ) -> Option<ElementBox> {
1921 if self.notifications.is_empty() {
1922 None
1923 } else {
1924 Some(
1925 Flex::column()
1926 .with_children(self.notifications.iter().map(|(_, _, notification)| {
1927 ChildView::new(notification.as_ref(), cx)
1928 .contained()
1929 .with_style(theme.notification)
1930 .boxed()
1931 }))
1932 .constrained()
1933 .with_width(theme.notifications.width)
1934 .contained()
1935 .with_style(theme.notifications.container)
1936 .aligned()
1937 .bottom()
1938 .right()
1939 .boxed(),
1940 )
1941 }
1942 }
1943
1944 // RPC handlers
1945
1946 async fn handle_follow(
1947 this: ViewHandle<Self>,
1948 envelope: TypedEnvelope<proto::Follow>,
1949 _: Arc<Client>,
1950 mut cx: AsyncAppContext,
1951 ) -> Result<proto::FollowResponse> {
1952 this.update(&mut cx, |this, cx| {
1953 let client = &this.client;
1954 this.leader_state
1955 .followers
1956 .insert(envelope.original_sender_id()?);
1957
1958 let active_view_id = this.active_item(cx).and_then(|i| {
1959 Some(
1960 i.to_followable_item_handle(cx)?
1961 .remote_id(client, cx)?
1962 .to_proto(),
1963 )
1964 });
1965
1966 cx.notify();
1967
1968 Ok(proto::FollowResponse {
1969 active_view_id,
1970 views: this
1971 .panes()
1972 .iter()
1973 .flat_map(|pane| {
1974 let leader_id = this.leader_for_pane(pane);
1975 pane.read(cx).items().filter_map({
1976 let cx = &cx;
1977 move |item| {
1978 let item = item.to_followable_item_handle(cx)?;
1979 let id = item.remote_id(client, cx)?.to_proto();
1980 let variant = item.to_state_proto(cx)?;
1981 Some(proto::View {
1982 id: Some(id),
1983 leader_id,
1984 variant: Some(variant),
1985 })
1986 }
1987 })
1988 })
1989 .collect(),
1990 })
1991 })
1992 }
1993
1994 async fn handle_unfollow(
1995 this: ViewHandle<Self>,
1996 envelope: TypedEnvelope<proto::Unfollow>,
1997 _: Arc<Client>,
1998 mut cx: AsyncAppContext,
1999 ) -> Result<()> {
2000 this.update(&mut cx, |this, cx| {
2001 this.leader_state
2002 .followers
2003 .remove(&envelope.original_sender_id()?);
2004 cx.notify();
2005 Ok(())
2006 })
2007 }
2008
2009 async fn handle_update_followers(
2010 this: ViewHandle<Self>,
2011 envelope: TypedEnvelope<proto::UpdateFollowers>,
2012 _: Arc<Client>,
2013 cx: AsyncAppContext,
2014 ) -> Result<()> {
2015 let leader_id = envelope.original_sender_id()?;
2016 this.read_with(&cx, |this, _| {
2017 this.leader_updates_tx
2018 .unbounded_send((leader_id, envelope.payload))
2019 })?;
2020 Ok(())
2021 }
2022
2023 async fn process_leader_update(
2024 this: ViewHandle<Self>,
2025 leader_id: PeerId,
2026 update: proto::UpdateFollowers,
2027 cx: &mut AsyncAppContext,
2028 ) -> Result<()> {
2029 match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
2030 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
2031 this.update(cx, |this, _| {
2032 if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
2033 for state in state.values_mut() {
2034 state.active_view_id =
2035 if let Some(active_view_id) = update_active_view.id.clone() {
2036 Some(ViewId::from_proto(active_view_id)?)
2037 } else {
2038 None
2039 };
2040 }
2041 }
2042 anyhow::Ok(())
2043 })?;
2044 }
2045 proto::update_followers::Variant::UpdateView(update_view) => {
2046 let variant = update_view
2047 .variant
2048 .ok_or_else(|| anyhow!("missing update view variant"))?;
2049 let id = update_view
2050 .id
2051 .ok_or_else(|| anyhow!("missing update view id"))?;
2052 let mut tasks = Vec::new();
2053 this.update(cx, |this, cx| {
2054 let project = this.project.clone();
2055 if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
2056 for state in state.values_mut() {
2057 let view_id = ViewId::from_proto(id.clone())?;
2058 if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
2059 tasks.push(item.apply_update_proto(&project, variant.clone(), cx));
2060 }
2061 }
2062 }
2063 anyhow::Ok(())
2064 })?;
2065 try_join_all(tasks).await.log_err();
2066 }
2067 proto::update_followers::Variant::CreateView(view) => {
2068 let panes = this.read_with(cx, |this, _| {
2069 this.follower_states_by_leader
2070 .get(&leader_id)
2071 .into_iter()
2072 .flat_map(|states_by_pane| states_by_pane.keys())
2073 .cloned()
2074 .collect()
2075 });
2076 Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], cx).await?;
2077 }
2078 }
2079 this.update(cx, |this, cx| this.leader_updated(leader_id, cx));
2080 Ok(())
2081 }
2082
2083 async fn add_views_from_leader(
2084 this: ViewHandle<Self>,
2085 leader_id: PeerId,
2086 panes: Vec<ViewHandle<Pane>>,
2087 views: Vec<proto::View>,
2088 cx: &mut AsyncAppContext,
2089 ) -> Result<()> {
2090 let project = this.read_with(cx, |this, _| this.project.clone());
2091 let replica_id = project
2092 .read_with(cx, |project, _| {
2093 project
2094 .collaborators()
2095 .get(&leader_id)
2096 .map(|c| c.replica_id)
2097 })
2098 .ok_or_else(|| anyhow!("no such collaborator {}", leader_id))?;
2099
2100 let item_builders = cx.update(|cx| {
2101 cx.default_global::<FollowableItemBuilders>()
2102 .values()
2103 .map(|b| b.0)
2104 .collect::<Vec<_>>()
2105 });
2106
2107 let mut item_tasks_by_pane = HashMap::default();
2108 for pane in panes {
2109 let mut item_tasks = Vec::new();
2110 let mut leader_view_ids = Vec::new();
2111 for view in &views {
2112 let Some(id) = &view.id else { continue };
2113 let id = ViewId::from_proto(id.clone())?;
2114 let mut variant = view.variant.clone();
2115 if variant.is_none() {
2116 Err(anyhow!("missing variant"))?;
2117 }
2118 for build_item in &item_builders {
2119 let task = cx.update(|cx| {
2120 build_item(pane.clone(), project.clone(), id, &mut variant, cx)
2121 });
2122 if let Some(task) = task {
2123 item_tasks.push(task);
2124 leader_view_ids.push(id);
2125 break;
2126 } else {
2127 assert!(variant.is_some());
2128 }
2129 }
2130 }
2131
2132 item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids));
2133 }
2134
2135 for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
2136 let items = futures::future::try_join_all(item_tasks).await?;
2137 this.update(cx, |this, cx| {
2138 let state = this
2139 .follower_states_by_leader
2140 .get_mut(&leader_id)?
2141 .get_mut(&pane)?;
2142
2143 for (id, item) in leader_view_ids.into_iter().zip(items) {
2144 item.set_leader_replica_id(Some(replica_id), cx);
2145 state.items_by_leader_view_id.insert(id, item);
2146 }
2147
2148 Some(())
2149 });
2150 }
2151 Ok(())
2152 }
2153
2154 fn update_followers(
2155 &self,
2156 update: proto::update_followers::Variant,
2157 cx: &AppContext,
2158 ) -> Option<()> {
2159 let project_id = self.project.read(cx).remote_id()?;
2160 if !self.leader_state.followers.is_empty() {
2161 self.client
2162 .send(proto::UpdateFollowers {
2163 project_id,
2164 follower_ids: self.leader_state.followers.iter().copied().collect(),
2165 variant: Some(update),
2166 })
2167 .log_err();
2168 }
2169 None
2170 }
2171
2172 pub fn leader_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<PeerId> {
2173 self.follower_states_by_leader
2174 .iter()
2175 .find_map(|(leader_id, state)| {
2176 if state.contains_key(pane) {
2177 Some(*leader_id)
2178 } else {
2179 None
2180 }
2181 })
2182 }
2183
2184 fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
2185 cx.notify();
2186
2187 let call = self.active_call()?;
2188 let room = call.read(cx).room()?.read(cx);
2189 let participant = room.remote_participant_for_peer_id(leader_id)?;
2190 let mut items_to_activate = Vec::new();
2191 match participant.location {
2192 call::ParticipantLocation::SharedProject { project_id } => {
2193 if Some(project_id) == self.project.read(cx).remote_id() {
2194 for (pane, state) in self.follower_states_by_leader.get(&leader_id)? {
2195 if let Some(item) = state
2196 .active_view_id
2197 .and_then(|id| state.items_by_leader_view_id.get(&id))
2198 {
2199 items_to_activate.push((pane.clone(), item.boxed_clone()));
2200 } else {
2201 if let Some(shared_screen) =
2202 self.shared_screen_for_peer(leader_id, pane, cx)
2203 {
2204 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
2205 }
2206 }
2207 }
2208 }
2209 }
2210 call::ParticipantLocation::UnsharedProject => {}
2211 call::ParticipantLocation::External => {
2212 for (pane, _) in self.follower_states_by_leader.get(&leader_id)? {
2213 if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
2214 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
2215 }
2216 }
2217 }
2218 }
2219
2220 for (pane, item) in items_to_activate {
2221 let active_item_was_focused = pane
2222 .read(cx)
2223 .active_item()
2224 .map(|active_item| cx.is_child_focused(active_item.to_any()))
2225 .unwrap_or_default();
2226
2227 if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) {
2228 pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx));
2229 } else {
2230 Pane::add_item(self, &pane, item.boxed_clone(), false, false, None, cx);
2231 }
2232
2233 if active_item_was_focused {
2234 pane.update(cx, |pane, cx| pane.focus_active_item(cx));
2235 }
2236 }
2237
2238 None
2239 }
2240
2241 fn shared_screen_for_peer(
2242 &self,
2243 peer_id: PeerId,
2244 pane: &ViewHandle<Pane>,
2245 cx: &mut ViewContext<Self>,
2246 ) -> Option<ViewHandle<SharedScreen>> {
2247 let call = self.active_call()?;
2248 let room = call.read(cx).room()?.read(cx);
2249 let participant = room.remote_participant_for_peer_id(peer_id)?;
2250 let track = participant.tracks.values().next()?.clone();
2251 let user = participant.user.clone();
2252
2253 for item in pane.read(cx).items_of_type::<SharedScreen>() {
2254 if item.read(cx).peer_id == peer_id {
2255 return Some(item);
2256 }
2257 }
2258
2259 Some(cx.add_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
2260 }
2261
2262 pub fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
2263 if active {
2264 cx.background()
2265 .spawn(persistence::DB.update_timestamp(self.database_id()))
2266 .detach();
2267 } else {
2268 for pane in &self.panes {
2269 pane.update(cx, |pane, cx| {
2270 if let Some(item) = pane.active_item() {
2271 item.workspace_deactivated(cx);
2272 }
2273 if matches!(
2274 cx.global::<Settings>().autosave,
2275 Autosave::OnWindowChange | Autosave::OnFocusChange
2276 ) {
2277 for item in pane.items() {
2278 Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
2279 .detach_and_log_err(cx);
2280 }
2281 }
2282 });
2283 }
2284 }
2285 }
2286
2287 fn active_call(&self) -> Option<&ModelHandle<ActiveCall>> {
2288 self.active_call.as_ref().map(|(call, _)| call)
2289 }
2290
2291 fn on_active_call_event(
2292 &mut self,
2293 _: ModelHandle<ActiveCall>,
2294 event: &call::room::Event,
2295 cx: &mut ViewContext<Self>,
2296 ) {
2297 match event {
2298 call::room::Event::ParticipantLocationChanged { participant_id }
2299 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
2300 self.leader_updated(*participant_id, cx);
2301 }
2302 _ => {}
2303 }
2304 }
2305
2306 pub fn database_id(&self) -> WorkspaceId {
2307 self.database_id
2308 }
2309
2310 fn location(&self, cx: &AppContext) -> Option<WorkspaceLocation> {
2311 let project = self.project().read(cx);
2312
2313 if project.is_local() {
2314 Some(
2315 project
2316 .visible_worktrees(cx)
2317 .map(|worktree| worktree.read(cx).abs_path())
2318 .collect::<Vec<_>>()
2319 .into(),
2320 )
2321 } else {
2322 None
2323 }
2324 }
2325
2326 fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
2327 match member {
2328 Member::Axis(PaneAxis { members, .. }) => {
2329 for child in members.iter() {
2330 self.remove_panes(child.clone(), cx)
2331 }
2332 }
2333 Member::Pane(pane) => self.remove_pane(pane.clone(), cx),
2334 }
2335 }
2336
2337 fn serialize_workspace(&self, cx: &AppContext) {
2338 fn serialize_pane_handle(
2339 pane_handle: &ViewHandle<Pane>,
2340 cx: &AppContext,
2341 ) -> SerializedPane {
2342 let (items, active) = {
2343 let pane = pane_handle.read(cx);
2344 let active_item_id = pane.active_item().map(|item| item.id());
2345 (
2346 pane.items()
2347 .filter_map(|item_handle| {
2348 Some(SerializedItem {
2349 kind: Arc::from(item_handle.serialized_item_kind()?),
2350 item_id: item_handle.id(),
2351 active: Some(item_handle.id()) == active_item_id,
2352 })
2353 })
2354 .collect::<Vec<_>>(),
2355 pane.is_active(),
2356 )
2357 };
2358
2359 SerializedPane::new(items, active)
2360 }
2361
2362 fn build_serialized_pane_group(
2363 pane_group: &Member,
2364 cx: &AppContext,
2365 ) -> SerializedPaneGroup {
2366 match pane_group {
2367 Member::Axis(PaneAxis { axis, members }) => SerializedPaneGroup::Group {
2368 axis: *axis,
2369 children: members
2370 .iter()
2371 .map(|member| build_serialized_pane_group(member, cx))
2372 .collect::<Vec<_>>(),
2373 },
2374 Member::Pane(pane_handle) => {
2375 SerializedPaneGroup::Pane(serialize_pane_handle(&pane_handle, cx))
2376 }
2377 }
2378 }
2379
2380 if let Some(location) = self.location(cx) {
2381 // Load bearing special case:
2382 // - with_local_workspace() relies on this to not have other stuff open
2383 // when you open your log
2384 if !location.paths().is_empty() {
2385 let dock_pane = serialize_pane_handle(self.dock.pane(), cx);
2386 let center_group = build_serialized_pane_group(&self.center.root, cx);
2387
2388 let serialized_workspace = SerializedWorkspace {
2389 id: self.database_id,
2390 location,
2391 dock_position: self.dock.position(),
2392 dock_pane,
2393 center_group,
2394 left_sidebar_open: self.left_sidebar.read(cx).is_open(),
2395 bounds: Default::default(),
2396 display: Default::default(),
2397 };
2398
2399 cx.background()
2400 .spawn(persistence::DB.save_workspace(serialized_workspace))
2401 .detach();
2402 }
2403 }
2404 }
2405
2406 fn load_from_serialized_workspace(
2407 workspace: WeakViewHandle<Workspace>,
2408 serialized_workspace: SerializedWorkspace,
2409 cx: &mut MutableAppContext,
2410 ) {
2411 cx.spawn(|mut cx| async move {
2412 if let Some(workspace) = workspace.upgrade(&cx) {
2413 let (project, dock_pane_handle, old_center_pane) =
2414 workspace.read_with(&cx, |workspace, _| {
2415 (
2416 workspace.project().clone(),
2417 workspace.dock_pane().clone(),
2418 workspace.last_active_center_pane.clone(),
2419 )
2420 });
2421
2422 serialized_workspace
2423 .dock_pane
2424 .deserialize_to(
2425 &project,
2426 &dock_pane_handle,
2427 serialized_workspace.id,
2428 &workspace,
2429 &mut cx,
2430 )
2431 .await;
2432
2433 // Traverse the splits tree and add to things
2434 let center_group = serialized_workspace
2435 .center_group
2436 .deserialize(&project, serialized_workspace.id, &workspace, &mut cx)
2437 .await;
2438
2439 // Remove old panes from workspace panes list
2440 workspace.update(&mut cx, |workspace, cx| {
2441 if let Some((center_group, active_pane)) = center_group {
2442 workspace.remove_panes(workspace.center.root.clone(), cx);
2443
2444 // Swap workspace center group
2445 workspace.center = PaneGroup::with_root(center_group);
2446
2447 // Change the focus to the workspace first so that we retrigger focus in on the pane.
2448 cx.focus_self();
2449
2450 if let Some(active_pane) = active_pane {
2451 cx.focus(active_pane);
2452 } else {
2453 cx.focus(workspace.panes.last().unwrap().clone());
2454 }
2455 } else {
2456 let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade(cx));
2457 if let Some(old_center_handle) = old_center_handle {
2458 cx.focus(old_center_handle)
2459 } else {
2460 cx.focus_self()
2461 }
2462 }
2463
2464 if workspace.left_sidebar().read(cx).is_open()
2465 != serialized_workspace.left_sidebar_open
2466 {
2467 workspace.toggle_sidebar(SidebarSide::Left, cx);
2468 }
2469
2470 // Note that without after_window, the focus_self() and
2471 // the focus the dock generates start generating alternating
2472 // focus due to the deferred execution each triggering each other
2473 cx.after_window_update(move |workspace, cx| {
2474 Dock::set_dock_position(workspace, serialized_workspace.dock_position, cx);
2475 });
2476
2477 cx.notify();
2478 });
2479
2480 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
2481 workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))
2482 }
2483 })
2484 .detach();
2485 }
2486}
2487
2488fn notify_if_database_failed(workspace: &ViewHandle<Workspace>, cx: &mut AsyncAppContext) {
2489 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
2490 workspace.update(cx, |workspace, cx| {
2491 workspace.show_notification_once(0, cx, |cx| {
2492 cx.add_view(|_| {
2493 MessageNotification::new(
2494 indoc::indoc! {"
2495 Failed to load any database file :(
2496 "},
2497 OsOpen("https://github.com/zed-industries/feedback/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml".to_string()),
2498 "Click to let us know about this error"
2499 )
2500 })
2501 });
2502 });
2503 } else {
2504 let backup_path = (*db::BACKUP_DB_PATH).read();
2505 if let Some(backup_path) = &*backup_path {
2506 workspace.update(cx, |workspace, cx| {
2507 workspace.show_notification_once(0, cx, |cx| {
2508 cx.add_view(|_| {
2509 let backup_path = backup_path.to_string_lossy();
2510 MessageNotification::new(
2511 format!(
2512 indoc::indoc! {"
2513 Database file was corrupted :(
2514 Old database backed up to:
2515 {}
2516 "},
2517 backup_path
2518 ),
2519 OsOpen(backup_path.to_string()),
2520 "Click to show old database in finder",
2521 )
2522 })
2523 });
2524 });
2525 }
2526 }
2527}
2528
2529impl Entity for Workspace {
2530 type Event = Event;
2531}
2532
2533impl View for Workspace {
2534 fn ui_name() -> &'static str {
2535 "Workspace"
2536 }
2537
2538 fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
2539 let theme = cx.global::<Settings>().theme.clone();
2540 Stack::new()
2541 .with_child(
2542 Flex::column()
2543 .with_child(self.render_titlebar(&theme, cx))
2544 .with_child(
2545 Stack::new()
2546 .with_child({
2547 let project = self.project.clone();
2548 Flex::row()
2549 .with_children(
2550 if self.left_sidebar.read(cx).active_item().is_some() {
2551 Some(
2552 ChildView::new(&self.left_sidebar, cx)
2553 .constrained()
2554 .dynamically(|constraint, cx| {
2555 SizeConstraint::new(
2556 Vector2F::new(20., constraint.min.y()),
2557 Vector2F::new(
2558 cx.window_size.x() * 0.8,
2559 constraint.max.y(),
2560 ),
2561 )
2562 })
2563 .boxed(),
2564 )
2565 } else {
2566 None
2567 },
2568 )
2569 .with_child(
2570 FlexItem::new(
2571 Flex::column()
2572 .with_child(
2573 FlexItem::new(self.center.render(
2574 &project,
2575 &theme,
2576 &self.follower_states_by_leader,
2577 self.active_call(),
2578 self.active_pane(),
2579 cx,
2580 ))
2581 .flex(1., true)
2582 .boxed(),
2583 )
2584 .with_children(self.dock.render(
2585 &theme,
2586 DockAnchor::Bottom,
2587 cx,
2588 ))
2589 .boxed(),
2590 )
2591 .flex(1., true)
2592 .boxed(),
2593 )
2594 .with_children(self.dock.render(&theme, DockAnchor::Right, cx))
2595 .with_children(
2596 if self.right_sidebar.read(cx).active_item().is_some() {
2597 Some(
2598 ChildView::new(&self.right_sidebar, cx)
2599 .constrained()
2600 .dynamically(|constraint, cx| {
2601 SizeConstraint::new(
2602 Vector2F::new(20., constraint.min.y()),
2603 Vector2F::new(
2604 cx.window_size.x() * 0.8,
2605 constraint.max.y(),
2606 ),
2607 )
2608 })
2609 .boxed(),
2610 )
2611 } else {
2612 None
2613 },
2614 )
2615 .boxed()
2616 })
2617 .with_child(
2618 Overlay::new(
2619 Stack::new()
2620 .with_children(self.dock.render(
2621 &theme,
2622 DockAnchor::Expanded,
2623 cx,
2624 ))
2625 .with_children(self.modal.as_ref().map(|modal| {
2626 ChildView::new(modal, cx)
2627 .contained()
2628 .with_style(theme.workspace.modal)
2629 .aligned()
2630 .top()
2631 .boxed()
2632 }))
2633 .with_children(
2634 self.render_notifications(&theme.workspace, cx),
2635 )
2636 .boxed(),
2637 )
2638 .boxed(),
2639 )
2640 .flex(1.0, true)
2641 .boxed(),
2642 )
2643 .with_child(ChildView::new(&self.status_bar, cx).boxed())
2644 .contained()
2645 .with_background_color(theme.workspace.background)
2646 .boxed(),
2647 )
2648 .with_children(DragAndDrop::render(cx))
2649 .with_children(self.render_disconnected_overlay(cx))
2650 .named("workspace")
2651 }
2652
2653 fn focus_in(&mut self, view: AnyViewHandle, cx: &mut ViewContext<Self>) {
2654 if cx.is_self_focused() {
2655 cx.focus(&self.active_pane);
2656 } else {
2657 for pane in self.panes() {
2658 let view = view.clone();
2659 if pane.update(cx, |_, cx| view.id() == cx.view_id() || cx.is_child(view)) {
2660 self.handle_pane_focused(pane.clone(), cx);
2661 break;
2662 }
2663 }
2664 }
2665 }
2666
2667 fn keymap_context(&self, _: &AppContext) -> KeymapContext {
2668 let mut keymap = Self::default_keymap_context();
2669 if self.active_pane() == self.dock_pane() {
2670 keymap.set.insert("Dock".into());
2671 }
2672 keymap
2673 }
2674}
2675
2676impl ViewId {
2677 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
2678 Ok(Self {
2679 creator: message
2680 .creator
2681 .ok_or_else(|| anyhow!("creator is missing"))?,
2682 id: message.id,
2683 })
2684 }
2685
2686 pub(crate) fn to_proto(&self) -> proto::ViewId {
2687 proto::ViewId {
2688 creator: Some(self.creator),
2689 id: self.id,
2690 }
2691 }
2692}
2693
2694pub trait WorkspaceHandle {
2695 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
2696}
2697
2698impl WorkspaceHandle for ViewHandle<Workspace> {
2699 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
2700 self.read(cx)
2701 .worktrees(cx)
2702 .flat_map(|worktree| {
2703 let worktree_id = worktree.read(cx).id();
2704 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
2705 worktree_id,
2706 path: f.path.clone(),
2707 })
2708 })
2709 .collect::<Vec<_>>()
2710 }
2711}
2712
2713impl std::fmt::Debug for OpenPaths {
2714 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2715 f.debug_struct("OpenPaths")
2716 .field("paths", &self.paths)
2717 .finish()
2718 }
2719}
2720
2721fn open(_: &Open, cx: &mut MutableAppContext) {
2722 let mut paths = cx.prompt_for_paths(PathPromptOptions {
2723 files: true,
2724 directories: true,
2725 multiple: true,
2726 });
2727 cx.spawn(|mut cx| async move {
2728 if let Some(paths) = paths.recv().await.flatten() {
2729 cx.update(|cx| cx.dispatch_global_action(OpenPaths { paths }));
2730 }
2731 })
2732 .detach();
2733}
2734
2735pub struct WorkspaceCreated(WeakViewHandle<Workspace>);
2736
2737pub fn activate_workspace_for_project(
2738 cx: &mut MutableAppContext,
2739 predicate: impl Fn(&mut Project, &mut ModelContext<Project>) -> bool,
2740) -> Option<ViewHandle<Workspace>> {
2741 for window_id in cx.window_ids().collect::<Vec<_>>() {
2742 if let Some(workspace_handle) = cx.root_view::<Workspace>(window_id) {
2743 let project = workspace_handle.read(cx).project.clone();
2744 if project.update(cx, &predicate) {
2745 cx.activate_window(window_id);
2746 return Some(workspace_handle);
2747 }
2748 }
2749 }
2750 None
2751}
2752
2753pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
2754 DB.last_workspace().await.log_err().flatten()
2755}
2756
2757#[allow(clippy::type_complexity)]
2758pub fn open_paths(
2759 abs_paths: &[PathBuf],
2760 app_state: &Arc<AppState>,
2761 cx: &mut MutableAppContext,
2762) -> Task<(
2763 ViewHandle<Workspace>,
2764 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
2765)> {
2766 log::info!("open paths {:?}", abs_paths);
2767
2768 // Open paths in existing workspace if possible
2769 let existing =
2770 activate_workspace_for_project(cx, |project, cx| project.contains_paths(abs_paths, cx));
2771
2772 let app_state = app_state.clone();
2773 let abs_paths = abs_paths.to_vec();
2774 cx.spawn(|mut cx| async move {
2775 if let Some(existing) = existing {
2776 (
2777 existing.clone(),
2778 existing
2779 .update(&mut cx, |workspace, cx| {
2780 workspace.open_paths(abs_paths, true, cx)
2781 })
2782 .await,
2783 )
2784 } else {
2785 let contains_directory =
2786 futures::future::join_all(abs_paths.iter().map(|path| app_state.fs.is_file(path)))
2787 .await
2788 .contains(&false);
2789
2790 cx.update(|cx| {
2791 let task = Workspace::new_local(abs_paths, app_state.clone(), cx);
2792
2793 cx.spawn(|mut cx| async move {
2794 let (workspace, items) = task.await;
2795
2796 workspace.update(&mut cx, |workspace, cx| {
2797 if contains_directory {
2798 workspace.toggle_sidebar(SidebarSide::Left, cx);
2799 }
2800 });
2801
2802 (workspace, items)
2803 })
2804 })
2805 .await
2806 }
2807 })
2808}
2809
2810pub fn open_new(app_state: &Arc<AppState>, cx: &mut MutableAppContext) -> Task<()> {
2811 let task = Workspace::new_local(Vec::new(), app_state.clone(), cx);
2812 cx.spawn(|mut cx| async move {
2813 let (workspace, opened_paths) = task.await;
2814
2815 workspace.update(&mut cx, |_, cx| {
2816 if opened_paths.is_empty() {
2817 cx.dispatch_action(NewFile);
2818 }
2819 })
2820 })
2821}
2822
2823#[cfg(test)]
2824mod tests {
2825 use std::{cell::RefCell, rc::Rc};
2826
2827 use crate::item::test::{TestItem, TestItemEvent, TestProjectItem};
2828
2829 use super::*;
2830 use fs::FakeFs;
2831 use gpui::{executor::Deterministic, TestAppContext, ViewContext};
2832 use project::{Project, ProjectEntryId};
2833 use serde_json::json;
2834
2835 pub fn default_item_factory(
2836 _workspace: &mut Workspace,
2837 _cx: &mut ViewContext<Workspace>,
2838 ) -> Option<Box<dyn ItemHandle>> {
2839 unimplemented!()
2840 }
2841
2842 #[gpui::test]
2843 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
2844 cx.foreground().forbid_parking();
2845 Settings::test_async(cx);
2846
2847 let fs = FakeFs::new(cx.background());
2848 let project = Project::test(fs, [], cx).await;
2849 let (_, workspace) = cx.add_window(|cx| {
2850 Workspace::new(
2851 Default::default(),
2852 0,
2853 project.clone(),
2854 default_item_factory,
2855 cx,
2856 )
2857 });
2858
2859 // Adding an item with no ambiguity renders the tab without detail.
2860 let item1 = cx.add_view(&workspace, |_| {
2861 let mut item = TestItem::new();
2862 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
2863 item
2864 });
2865 workspace.update(cx, |workspace, cx| {
2866 workspace.add_item(Box::new(item1.clone()), cx);
2867 });
2868 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), None));
2869
2870 // Adding an item that creates ambiguity increases the level of detail on
2871 // both tabs.
2872 let item2 = cx.add_view(&workspace, |_| {
2873 let mut item = TestItem::new();
2874 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
2875 item
2876 });
2877 workspace.update(cx, |workspace, cx| {
2878 workspace.add_item(Box::new(item2.clone()), cx);
2879 });
2880 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
2881 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
2882
2883 // Adding an item that creates ambiguity increases the level of detail only
2884 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
2885 // we stop at the highest detail available.
2886 let item3 = cx.add_view(&workspace, |_| {
2887 let mut item = TestItem::new();
2888 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
2889 item
2890 });
2891 workspace.update(cx, |workspace, cx| {
2892 workspace.add_item(Box::new(item3.clone()), cx);
2893 });
2894 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
2895 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
2896 item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
2897 }
2898
2899 #[gpui::test]
2900 async fn test_tracking_active_path(cx: &mut TestAppContext) {
2901 cx.foreground().forbid_parking();
2902 Settings::test_async(cx);
2903 let fs = FakeFs::new(cx.background());
2904 fs.insert_tree(
2905 "/root1",
2906 json!({
2907 "one.txt": "",
2908 "two.txt": "",
2909 }),
2910 )
2911 .await;
2912 fs.insert_tree(
2913 "/root2",
2914 json!({
2915 "three.txt": "",
2916 }),
2917 )
2918 .await;
2919
2920 let project = Project::test(fs, ["root1".as_ref()], cx).await;
2921 let (window_id, workspace) = cx.add_window(|cx| {
2922 Workspace::new(
2923 Default::default(),
2924 0,
2925 project.clone(),
2926 default_item_factory,
2927 cx,
2928 )
2929 });
2930 let worktree_id = project.read_with(cx, |project, cx| {
2931 project.worktrees(cx).next().unwrap().read(cx).id()
2932 });
2933
2934 let item1 = cx.add_view(&workspace, |cx| {
2935 TestItem::new().with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
2936 });
2937 let item2 = cx.add_view(&workspace, |cx| {
2938 TestItem::new().with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
2939 });
2940
2941 // Add an item to an empty pane
2942 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
2943 project.read_with(cx, |project, cx| {
2944 assert_eq!(
2945 project.active_entry(),
2946 project
2947 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
2948 .map(|e| e.id)
2949 );
2950 });
2951 assert_eq!(
2952 cx.current_window_title(window_id).as_deref(),
2953 Some("one.txt — root1")
2954 );
2955
2956 // Add a second item to a non-empty pane
2957 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
2958 assert_eq!(
2959 cx.current_window_title(window_id).as_deref(),
2960 Some("two.txt — root1")
2961 );
2962 project.read_with(cx, |project, cx| {
2963 assert_eq!(
2964 project.active_entry(),
2965 project
2966 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
2967 .map(|e| e.id)
2968 );
2969 });
2970
2971 // Close the active item
2972 workspace
2973 .update(cx, |workspace, cx| {
2974 Pane::close_active_item(workspace, &Default::default(), cx).unwrap()
2975 })
2976 .await
2977 .unwrap();
2978 assert_eq!(
2979 cx.current_window_title(window_id).as_deref(),
2980 Some("one.txt — root1")
2981 );
2982 project.read_with(cx, |project, cx| {
2983 assert_eq!(
2984 project.active_entry(),
2985 project
2986 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
2987 .map(|e| e.id)
2988 );
2989 });
2990
2991 // Add a project folder
2992 project
2993 .update(cx, |project, cx| {
2994 project.find_or_create_local_worktree("/root2", true, cx)
2995 })
2996 .await
2997 .unwrap();
2998 assert_eq!(
2999 cx.current_window_title(window_id).as_deref(),
3000 Some("one.txt — root1, root2")
3001 );
3002
3003 // Remove a project folder
3004 project
3005 .update(cx, |project, cx| project.remove_worktree(worktree_id, cx))
3006 .await;
3007 assert_eq!(
3008 cx.current_window_title(window_id).as_deref(),
3009 Some("one.txt — root2")
3010 );
3011 }
3012
3013 #[gpui::test]
3014 async fn test_close_window(cx: &mut TestAppContext) {
3015 cx.foreground().forbid_parking();
3016 Settings::test_async(cx);
3017 let fs = FakeFs::new(cx.background());
3018 fs.insert_tree("/root", json!({ "one": "" })).await;
3019
3020 let project = Project::test(fs, ["root".as_ref()], cx).await;
3021 let (window_id, workspace) = cx.add_window(|cx| {
3022 Workspace::new(
3023 Default::default(),
3024 0,
3025 project.clone(),
3026 default_item_factory,
3027 cx,
3028 )
3029 });
3030
3031 // When there are no dirty items, there's nothing to do.
3032 let item1 = cx.add_view(&workspace, |_| TestItem::new());
3033 workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
3034 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
3035 assert!(task.await.unwrap());
3036
3037 // When there are dirty untitled items, prompt to save each one. If the user
3038 // cancels any prompt, then abort.
3039 let item2 = cx.add_view(&workspace, |_| TestItem::new().with_dirty(true));
3040 let item3 = cx.add_view(&workspace, |cx| {
3041 TestItem::new()
3042 .with_dirty(true)
3043 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3044 });
3045 workspace.update(cx, |w, cx| {
3046 w.add_item(Box::new(item2.clone()), cx);
3047 w.add_item(Box::new(item3.clone()), cx);
3048 });
3049 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
3050 cx.foreground().run_until_parked();
3051 cx.simulate_prompt_answer(window_id, 2 /* cancel */);
3052 cx.foreground().run_until_parked();
3053 assert!(!cx.has_pending_prompt(window_id));
3054 assert!(!task.await.unwrap());
3055 }
3056
3057 #[gpui::test]
3058 async fn test_close_pane_items(cx: &mut TestAppContext) {
3059 cx.foreground().forbid_parking();
3060 Settings::test_async(cx);
3061 let fs = FakeFs::new(cx.background());
3062
3063 let project = Project::test(fs, None, cx).await;
3064 let (window_id, workspace) = cx.add_window(|cx| {
3065 Workspace::new(Default::default(), 0, project, default_item_factory, cx)
3066 });
3067
3068 let item1 = cx.add_view(&workspace, |cx| {
3069 TestItem::new()
3070 .with_dirty(true)
3071 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3072 });
3073 let item2 = cx.add_view(&workspace, |cx| {
3074 TestItem::new()
3075 .with_dirty(true)
3076 .with_conflict(true)
3077 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
3078 });
3079 let item3 = cx.add_view(&workspace, |cx| {
3080 TestItem::new()
3081 .with_dirty(true)
3082 .with_conflict(true)
3083 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
3084 });
3085 let item4 = cx.add_view(&workspace, |cx| {
3086 TestItem::new()
3087 .with_dirty(true)
3088 .with_project_items(&[TestProjectItem::new_untitled(cx)])
3089 });
3090 let pane = workspace.update(cx, |workspace, cx| {
3091 workspace.add_item(Box::new(item1.clone()), cx);
3092 workspace.add_item(Box::new(item2.clone()), cx);
3093 workspace.add_item(Box::new(item3.clone()), cx);
3094 workspace.add_item(Box::new(item4.clone()), cx);
3095 workspace.active_pane().clone()
3096 });
3097
3098 let close_items = workspace.update(cx, |workspace, cx| {
3099 pane.update(cx, |pane, cx| {
3100 pane.activate_item(1, true, true, cx);
3101 assert_eq!(pane.active_item().unwrap().id(), item2.id());
3102 });
3103
3104 let item1_id = item1.id();
3105 let item3_id = item3.id();
3106 let item4_id = item4.id();
3107 Pane::close_items(workspace, pane.clone(), cx, move |id| {
3108 [item1_id, item3_id, item4_id].contains(&id)
3109 })
3110 });
3111 cx.foreground().run_until_parked();
3112
3113 // There's a prompt to save item 1.
3114 pane.read_with(cx, |pane, _| {
3115 assert_eq!(pane.items_len(), 4);
3116 assert_eq!(pane.active_item().unwrap().id(), item1.id());
3117 });
3118 assert!(cx.has_pending_prompt(window_id));
3119
3120 // Confirm saving item 1.
3121 cx.simulate_prompt_answer(window_id, 0);
3122 cx.foreground().run_until_parked();
3123
3124 // Item 1 is saved. There's a prompt to save item 3.
3125 pane.read_with(cx, |pane, cx| {
3126 assert_eq!(item1.read(cx).save_count, 1);
3127 assert_eq!(item1.read(cx).save_as_count, 0);
3128 assert_eq!(item1.read(cx).reload_count, 0);
3129 assert_eq!(pane.items_len(), 3);
3130 assert_eq!(pane.active_item().unwrap().id(), item3.id());
3131 });
3132 assert!(cx.has_pending_prompt(window_id));
3133
3134 // Cancel saving item 3.
3135 cx.simulate_prompt_answer(window_id, 1);
3136 cx.foreground().run_until_parked();
3137
3138 // Item 3 is reloaded. There's a prompt to save item 4.
3139 pane.read_with(cx, |pane, cx| {
3140 assert_eq!(item3.read(cx).save_count, 0);
3141 assert_eq!(item3.read(cx).save_as_count, 0);
3142 assert_eq!(item3.read(cx).reload_count, 1);
3143 assert_eq!(pane.items_len(), 2);
3144 assert_eq!(pane.active_item().unwrap().id(), item4.id());
3145 });
3146 assert!(cx.has_pending_prompt(window_id));
3147
3148 // Confirm saving item 4.
3149 cx.simulate_prompt_answer(window_id, 0);
3150 cx.foreground().run_until_parked();
3151
3152 // There's a prompt for a path for item 4.
3153 cx.simulate_new_path_selection(|_| Some(Default::default()));
3154 close_items.await.unwrap();
3155
3156 // The requested items are closed.
3157 pane.read_with(cx, |pane, cx| {
3158 assert_eq!(item4.read(cx).save_count, 0);
3159 assert_eq!(item4.read(cx).save_as_count, 1);
3160 assert_eq!(item4.read(cx).reload_count, 0);
3161 assert_eq!(pane.items_len(), 1);
3162 assert_eq!(pane.active_item().unwrap().id(), item2.id());
3163 });
3164 }
3165
3166 #[gpui::test]
3167 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
3168 cx.foreground().forbid_parking();
3169 Settings::test_async(cx);
3170 let fs = FakeFs::new(cx.background());
3171
3172 let project = Project::test(fs, [], cx).await;
3173 let (window_id, workspace) = cx.add_window(|cx| {
3174 Workspace::new(Default::default(), 0, project, default_item_factory, cx)
3175 });
3176
3177 // Create several workspace items with single project entries, and two
3178 // workspace items with multiple project entries.
3179 let single_entry_items = (0..=4)
3180 .map(|project_entry_id| {
3181 cx.add_view(&workspace, |cx| {
3182 TestItem::new()
3183 .with_dirty(true)
3184 .with_project_items(&[TestProjectItem::new(
3185 project_entry_id,
3186 &format!("{project_entry_id}.txt"),
3187 cx,
3188 )])
3189 })
3190 })
3191 .collect::<Vec<_>>();
3192 let item_2_3 = cx.add_view(&workspace, |cx| {
3193 TestItem::new()
3194 .with_dirty(true)
3195 .with_singleton(false)
3196 .with_project_items(&[
3197 single_entry_items[2].read(cx).project_items[0].clone(),
3198 single_entry_items[3].read(cx).project_items[0].clone(),
3199 ])
3200 });
3201 let item_3_4 = cx.add_view(&workspace, |cx| {
3202 TestItem::new()
3203 .with_dirty(true)
3204 .with_singleton(false)
3205 .with_project_items(&[
3206 single_entry_items[3].read(cx).project_items[0].clone(),
3207 single_entry_items[4].read(cx).project_items[0].clone(),
3208 ])
3209 });
3210
3211 // Create two panes that contain the following project entries:
3212 // left pane:
3213 // multi-entry items: (2, 3)
3214 // single-entry items: 0, 1, 2, 3, 4
3215 // right pane:
3216 // single-entry items: 1
3217 // multi-entry items: (3, 4)
3218 let left_pane = workspace.update(cx, |workspace, cx| {
3219 let left_pane = workspace.active_pane().clone();
3220 workspace.add_item(Box::new(item_2_3.clone()), cx);
3221 for item in single_entry_items {
3222 workspace.add_item(Box::new(item), cx);
3223 }
3224 left_pane.update(cx, |pane, cx| {
3225 pane.activate_item(2, true, true, cx);
3226 });
3227
3228 workspace
3229 .split_pane(left_pane.clone(), SplitDirection::Right, cx)
3230 .unwrap();
3231
3232 left_pane
3233 });
3234
3235 //Need to cause an effect flush in order to respect new focus
3236 workspace.update(cx, |workspace, cx| {
3237 workspace.add_item(Box::new(item_3_4.clone()), cx);
3238 cx.focus(left_pane.clone());
3239 });
3240
3241 // When closing all of the items in the left pane, we should be prompted twice:
3242 // once for project entry 0, and once for project entry 2. After those two
3243 // prompts, the task should complete.
3244
3245 let close = workspace.update(cx, |workspace, cx| {
3246 Pane::close_items(workspace, left_pane.clone(), cx, |_| true)
3247 });
3248
3249 cx.foreground().run_until_parked();
3250 left_pane.read_with(cx, |pane, cx| {
3251 assert_eq!(
3252 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
3253 &[ProjectEntryId::from_proto(0)]
3254 );
3255 });
3256 cx.simulate_prompt_answer(window_id, 0);
3257
3258 cx.foreground().run_until_parked();
3259 left_pane.read_with(cx, |pane, cx| {
3260 assert_eq!(
3261 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
3262 &[ProjectEntryId::from_proto(2)]
3263 );
3264 });
3265 cx.simulate_prompt_answer(window_id, 0);
3266
3267 cx.foreground().run_until_parked();
3268 close.await.unwrap();
3269 left_pane.read_with(cx, |pane, _| {
3270 assert_eq!(pane.items_len(), 0);
3271 });
3272 }
3273
3274 #[gpui::test]
3275 async fn test_autosave(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
3276 deterministic.forbid_parking();
3277
3278 Settings::test_async(cx);
3279 let fs = FakeFs::new(cx.background());
3280
3281 let project = Project::test(fs, [], cx).await;
3282 let (window_id, workspace) = cx.add_window(|cx| {
3283 Workspace::new(Default::default(), 0, project, default_item_factory, cx)
3284 });
3285
3286 let item = cx.add_view(&workspace, |cx| {
3287 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3288 });
3289 let item_id = item.id();
3290 workspace.update(cx, |workspace, cx| {
3291 workspace.add_item(Box::new(item.clone()), cx);
3292 });
3293
3294 // Autosave on window change.
3295 item.update(cx, |item, cx| {
3296 cx.update_global(|settings: &mut Settings, _| {
3297 settings.autosave = Autosave::OnWindowChange;
3298 });
3299 item.is_dirty = true;
3300 });
3301
3302 // Deactivating the window saves the file.
3303 cx.simulate_window_activation(None);
3304 deterministic.run_until_parked();
3305 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
3306
3307 // Autosave on focus change.
3308 item.update(cx, |item, cx| {
3309 cx.focus_self();
3310 cx.update_global(|settings: &mut Settings, _| {
3311 settings.autosave = Autosave::OnFocusChange;
3312 });
3313 item.is_dirty = true;
3314 });
3315
3316 // Blurring the item saves the file.
3317 item.update(cx, |_, cx| cx.blur());
3318 deterministic.run_until_parked();
3319 item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
3320
3321 // Deactivating the window still saves the file.
3322 cx.simulate_window_activation(Some(window_id));
3323 item.update(cx, |item, cx| {
3324 cx.focus_self();
3325 item.is_dirty = true;
3326 });
3327 cx.simulate_window_activation(None);
3328
3329 deterministic.run_until_parked();
3330 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
3331
3332 // Autosave after delay.
3333 item.update(cx, |item, cx| {
3334 cx.update_global(|settings: &mut Settings, _| {
3335 settings.autosave = Autosave::AfterDelay { milliseconds: 500 };
3336 });
3337 item.is_dirty = true;
3338 cx.emit(TestItemEvent::Edit);
3339 });
3340
3341 // Delay hasn't fully expired, so the file is still dirty and unsaved.
3342 deterministic.advance_clock(Duration::from_millis(250));
3343 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
3344
3345 // After delay expires, the file is saved.
3346 deterministic.advance_clock(Duration::from_millis(250));
3347 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
3348
3349 // Autosave on focus change, ensuring closing the tab counts as such.
3350 item.update(cx, |item, cx| {
3351 cx.update_global(|settings: &mut Settings, _| {
3352 settings.autosave = Autosave::OnFocusChange;
3353 });
3354 item.is_dirty = true;
3355 });
3356
3357 workspace
3358 .update(cx, |workspace, cx| {
3359 let pane = workspace.active_pane().clone();
3360 Pane::close_items(workspace, pane, cx, move |id| id == item_id)
3361 })
3362 .await
3363 .unwrap();
3364 assert!(!cx.has_pending_prompt(window_id));
3365 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3366
3367 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
3368 workspace.update(cx, |workspace, cx| {
3369 workspace.add_item(Box::new(item.clone()), cx);
3370 });
3371 item.update(cx, |item, cx| {
3372 item.project_items[0].update(cx, |item, _| {
3373 item.entry_id = None;
3374 });
3375 item.is_dirty = true;
3376 cx.blur();
3377 });
3378 deterministic.run_until_parked();
3379 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3380
3381 // Ensure autosave is prevented for deleted files also when closing the buffer.
3382 let _close_items = workspace.update(cx, |workspace, cx| {
3383 let pane = workspace.active_pane().clone();
3384 Pane::close_items(workspace, pane, cx, move |id| id == item_id)
3385 });
3386 deterministic.run_until_parked();
3387 assert!(cx.has_pending_prompt(window_id));
3388 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3389 }
3390
3391 #[gpui::test]
3392 async fn test_pane_navigation(
3393 deterministic: Arc<Deterministic>,
3394 cx: &mut gpui::TestAppContext,
3395 ) {
3396 deterministic.forbid_parking();
3397 Settings::test_async(cx);
3398 let fs = FakeFs::new(cx.background());
3399
3400 let project = Project::test(fs, [], cx).await;
3401 let (_, workspace) = cx.add_window(|cx| {
3402 Workspace::new(Default::default(), 0, project, default_item_factory, cx)
3403 });
3404
3405 let item = cx.add_view(&workspace, |cx| {
3406 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3407 });
3408 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3409 let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
3410 let toolbar_notify_count = Rc::new(RefCell::new(0));
3411
3412 workspace.update(cx, |workspace, cx| {
3413 workspace.add_item(Box::new(item.clone()), cx);
3414 let toolbar_notification_count = toolbar_notify_count.clone();
3415 cx.observe(&toolbar, move |_, _, _| {
3416 *toolbar_notification_count.borrow_mut() += 1
3417 })
3418 .detach();
3419 });
3420
3421 pane.read_with(cx, |pane, _| {
3422 assert!(!pane.can_navigate_backward());
3423 assert!(!pane.can_navigate_forward());
3424 });
3425
3426 item.update(cx, |item, cx| {
3427 item.set_state("one".to_string(), cx);
3428 });
3429
3430 // Toolbar must be notified to re-render the navigation buttons
3431 assert_eq!(*toolbar_notify_count.borrow(), 1);
3432
3433 pane.read_with(cx, |pane, _| {
3434 assert!(pane.can_navigate_backward());
3435 assert!(!pane.can_navigate_forward());
3436 });
3437
3438 workspace
3439 .update(cx, |workspace, cx| {
3440 Pane::go_back(workspace, Some(pane.clone()), cx)
3441 })
3442 .await;
3443
3444 assert_eq!(*toolbar_notify_count.borrow(), 3);
3445 pane.read_with(cx, |pane, _| {
3446 assert!(!pane.can_navigate_backward());
3447 assert!(pane.can_navigate_forward());
3448 });
3449 }
3450}