1pub mod dock;
2pub mod item;
3pub mod notifications;
4pub mod pane;
5pub mod pane_group;
6mod persistence;
7pub mod searchable;
8pub mod shared_screen;
9mod status_bar;
10mod toolbar;
11mod workspace_settings;
12
13use anyhow::{anyhow, Context, Result};
14use call::ActiveCall;
15use client::{
16 proto::{self, PeerId},
17 Client, Status, TypedEnvelope, UserStore,
18};
19use collections::{hash_map, HashMap, HashSet};
20use drag_and_drop::DragAndDrop;
21use futures::{
22 channel::{mpsc, oneshot},
23 future::try_join_all,
24 FutureExt, StreamExt,
25};
26use gpui::{
27 actions,
28 elements::*,
29 geometry::{
30 rect::RectF,
31 vector::{vec2f, Vector2F},
32 },
33 impl_actions,
34 platform::{
35 CursorStyle, ModifiersChangedEvent, MouseButton, PathPromptOptions, Platform, PromptLevel,
36 WindowBounds, WindowOptions,
37 },
38 AnyModelHandle, AnyViewHandle, AnyWeakViewHandle, AnyWindowHandle, AppContext, AsyncAppContext,
39 Entity, ModelContext, ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext,
40 ViewHandle, WeakViewHandle, WindowContext, WindowHandle,
41};
42use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem};
43use itertools::Itertools;
44use language::{LanguageRegistry, Rope};
45use std::{
46 any::TypeId,
47 borrow::Cow,
48 cmp, env,
49 future::Future,
50 path::{Path, PathBuf},
51 rc::Rc,
52 str,
53 sync::{atomic::AtomicUsize, Arc},
54 time::Duration,
55};
56
57use crate::{
58 notifications::{simple_message_notification::MessageNotification, NotificationTracker},
59 persistence::model::{
60 DockData, DockStructure, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
61 },
62};
63use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle};
64use lazy_static::lazy_static;
65use notifications::{NotificationHandle, NotifyResultExt};
66pub use pane::*;
67pub use pane_group::*;
68use persistence::{model::SerializedItem, DB};
69pub use persistence::{
70 model::{ItemId, WorkspaceLocation},
71 WorkspaceDb, DB as WORKSPACE_DB,
72};
73use postage::prelude::Stream;
74use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
75use serde::Deserialize;
76use shared_screen::SharedScreen;
77use status_bar::StatusBar;
78pub use status_bar::StatusItemView;
79use theme::{Theme, ThemeSettings};
80pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
81use util::ResultExt;
82pub use workspace_settings::{AutosaveSetting, GitGutterSetting, WorkspaceSettings};
83
84lazy_static! {
85 static ref ZED_WINDOW_SIZE: Option<Vector2F> = env::var("ZED_WINDOW_SIZE")
86 .ok()
87 .as_deref()
88 .and_then(parse_pixel_position_env_var);
89 static ref ZED_WINDOW_POSITION: Option<Vector2F> = env::var("ZED_WINDOW_POSITION")
90 .ok()
91 .as_deref()
92 .and_then(parse_pixel_position_env_var);
93}
94
95pub trait Modal: View {
96 fn has_focus(&self) -> bool;
97 fn dismiss_on_event(event: &Self::Event) -> bool;
98}
99
100trait ModalHandle {
101 fn as_any(&self) -> &AnyViewHandle;
102 fn has_focus(&self, cx: &WindowContext) -> bool;
103}
104
105impl<T: Modal> ModalHandle for ViewHandle<T> {
106 fn as_any(&self) -> &AnyViewHandle {
107 self
108 }
109
110 fn has_focus(&self, cx: &WindowContext) -> bool {
111 self.read(cx).has_focus()
112 }
113}
114
115#[derive(Clone, PartialEq)]
116pub struct RemoveWorktreeFromProject(pub WorktreeId);
117
118actions!(
119 workspace,
120 [
121 Open,
122 NewFile,
123 NewWindow,
124 CloseWindow,
125 CloseInactiveTabsAndPanes,
126 AddFolderToProject,
127 Unfollow,
128 SaveAs,
129 ReloadActiveItem,
130 ActivatePreviousPane,
131 ActivateNextPane,
132 FollowNextCollaborator,
133 NewTerminal,
134 NewCenterTerminal,
135 ToggleTerminalFocus,
136 NewSearch,
137 Feedback,
138 Restart,
139 Welcome,
140 ToggleZoom,
141 ToggleLeftDock,
142 ToggleRightDock,
143 ToggleBottomDock,
144 CloseAllDocks,
145 ]
146);
147
148#[derive(Clone, PartialEq)]
149pub struct OpenPaths {
150 pub paths: Vec<PathBuf>,
151}
152
153#[derive(Clone, Deserialize, PartialEq)]
154pub struct ActivatePane(pub usize);
155
156#[derive(Clone, Deserialize, PartialEq)]
157pub struct ActivatePaneInDirection(pub SplitDirection);
158
159#[derive(Clone, Deserialize, PartialEq)]
160pub struct SwapPaneInDirection(pub SplitDirection);
161
162#[derive(Clone, Deserialize, PartialEq)]
163pub struct NewFileInDirection(pub SplitDirection);
164
165#[derive(Clone, PartialEq, Debug, Deserialize)]
166#[serde(rename_all = "camelCase")]
167pub struct SaveAll {
168 pub save_intent: Option<SaveIntent>,
169}
170
171#[derive(Clone, PartialEq, Debug, Deserialize)]
172#[serde(rename_all = "camelCase")]
173pub struct Save {
174 pub save_intent: Option<SaveIntent>,
175}
176
177#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
178#[serde(rename_all = "camelCase")]
179pub struct CloseAllItemsAndPanes {
180 pub save_intent: Option<SaveIntent>,
181}
182
183#[derive(Deserialize)]
184pub struct Toast {
185 id: usize,
186 msg: Cow<'static, str>,
187 #[serde(skip)]
188 on_click: Option<(Cow<'static, str>, Arc<dyn Fn(&mut WindowContext)>)>,
189}
190
191impl Toast {
192 pub fn new<I: Into<Cow<'static, str>>>(id: usize, msg: I) -> Self {
193 Toast {
194 id,
195 msg: msg.into(),
196 on_click: None,
197 }
198 }
199
200 pub fn on_click<F, M>(mut self, message: M, on_click: F) -> Self
201 where
202 M: Into<Cow<'static, str>>,
203 F: Fn(&mut WindowContext) + 'static,
204 {
205 self.on_click = Some((message.into(), Arc::new(on_click)));
206 self
207 }
208}
209
210impl PartialEq for Toast {
211 fn eq(&self, other: &Self) -> bool {
212 self.id == other.id
213 && self.msg == other.msg
214 && self.on_click.is_some() == other.on_click.is_some()
215 }
216}
217
218impl Clone for Toast {
219 fn clone(&self) -> Self {
220 Toast {
221 id: self.id,
222 msg: self.msg.to_owned(),
223 on_click: self.on_click.clone(),
224 }
225 }
226}
227
228#[derive(Clone, Deserialize, PartialEq)]
229pub struct OpenTerminal {
230 pub working_directory: PathBuf,
231}
232
233impl_actions!(
234 workspace,
235 [
236 ActivatePane,
237 ActivatePaneInDirection,
238 SwapPaneInDirection,
239 NewFileInDirection,
240 Toast,
241 OpenTerminal,
242 SaveAll,
243 Save,
244 CloseAllItemsAndPanes,
245 ]
246);
247
248pub type WorkspaceId = i64;
249
250pub fn init_settings(cx: &mut AppContext) {
251 settings::register::<WorkspaceSettings>(cx);
252 settings::register::<item::ItemSettings>(cx);
253}
254
255pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
256 init_settings(cx);
257 pane::init(cx);
258 notifications::init(cx);
259
260 cx.add_global_action({
261 let app_state = Arc::downgrade(&app_state);
262 move |_: &Open, cx: &mut AppContext| {
263 let mut paths = cx.prompt_for_paths(PathPromptOptions {
264 files: true,
265 directories: true,
266 multiple: true,
267 });
268
269 if let Some(app_state) = app_state.upgrade() {
270 cx.spawn(move |mut cx| async move {
271 if let Some(paths) = paths.recv().await.flatten() {
272 cx.update(|cx| {
273 open_paths(&paths, &app_state, None, cx).detach_and_log_err(cx)
274 });
275 }
276 })
277 .detach();
278 }
279 }
280 });
281 cx.add_async_action(Workspace::open);
282
283 cx.add_async_action(Workspace::follow_next_collaborator);
284 cx.add_async_action(Workspace::close);
285 cx.add_async_action(Workspace::close_inactive_items_and_panes);
286 cx.add_async_action(Workspace::close_all_items_and_panes);
287 cx.add_global_action(Workspace::close_global);
288 cx.add_global_action(restart);
289 cx.add_async_action(Workspace::save_all);
290 cx.add_action(Workspace::add_folder_to_project);
291 cx.add_action(
292 |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext<Workspace>| {
293 let pane = workspace.active_pane().clone();
294 workspace.unfollow(&pane, cx);
295 },
296 );
297 cx.add_action(
298 |workspace: &mut Workspace, action: &Save, cx: &mut ViewContext<Workspace>| {
299 workspace
300 .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx)
301 .detach_and_log_err(cx);
302 },
303 );
304 cx.add_action(
305 |workspace: &mut Workspace, _: &SaveAs, cx: &mut ViewContext<Workspace>| {
306 workspace
307 .save_active_item(SaveIntent::SaveAs, cx)
308 .detach_and_log_err(cx);
309 },
310 );
311 cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| {
312 workspace.activate_previous_pane(cx)
313 });
314 cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| {
315 workspace.activate_next_pane(cx)
316 });
317
318 cx.add_action(
319 |workspace: &mut Workspace, action: &ActivatePaneInDirection, cx| {
320 workspace.activate_pane_in_direction(action.0, cx)
321 },
322 );
323
324 cx.add_action(
325 |workspace: &mut Workspace, action: &SwapPaneInDirection, cx| {
326 workspace.swap_pane_in_direction(action.0, cx)
327 },
328 );
329
330 cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftDock, cx| {
331 workspace.toggle_dock(DockPosition::Left, cx);
332 });
333 cx.add_action(|workspace: &mut Workspace, _: &ToggleRightDock, cx| {
334 workspace.toggle_dock(DockPosition::Right, cx);
335 });
336 cx.add_action(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| {
337 workspace.toggle_dock(DockPosition::Bottom, cx);
338 });
339 cx.add_action(|workspace: &mut Workspace, _: &CloseAllDocks, cx| {
340 workspace.close_all_docks(cx);
341 });
342 cx.add_action(Workspace::activate_pane_at_index);
343 cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
344 workspace.reopen_closed_item(cx).detach();
345 });
346 cx.add_action(|workspace: &mut Workspace, _: &GoBack, cx| {
347 workspace
348 .go_back(workspace.active_pane().downgrade(), cx)
349 .detach();
350 });
351 cx.add_action(|workspace: &mut Workspace, _: &GoForward, cx| {
352 workspace
353 .go_forward(workspace.active_pane().downgrade(), cx)
354 .detach();
355 });
356
357 cx.add_action(|_: &mut Workspace, _: &install_cli::Install, cx| {
358 cx.spawn(|workspace, mut cx| async move {
359 let err = install_cli::install_cli(&cx)
360 .await
361 .context("Failed to create CLI symlink");
362
363 workspace.update(&mut cx, |workspace, cx| {
364 if matches!(err, Err(_)) {
365 err.notify_err(workspace, cx);
366 } else {
367 workspace.show_notification(1, cx, |cx| {
368 cx.add_view(|_| {
369 MessageNotification::new("Successfully installed the `zed` binary")
370 })
371 });
372 }
373 })
374 })
375 .detach();
376 });
377}
378
379type ProjectItemBuilders = HashMap<
380 TypeId,
381 fn(ModelHandle<Project>, AnyModelHandle, &mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
382>;
383pub fn register_project_item<I: ProjectItem>(cx: &mut AppContext) {
384 cx.update_default_global(|builders: &mut ProjectItemBuilders, _| {
385 builders.insert(TypeId::of::<I::Item>(), |project, model, cx| {
386 let item = model.downcast::<I::Item>().unwrap();
387 Box::new(cx.add_view(|cx| I::for_project_item(project, item, cx)))
388 });
389 });
390}
391
392type FollowableItemBuilder = fn(
393 ViewHandle<Pane>,
394 ViewHandle<Workspace>,
395 ViewId,
396 &mut Option<proto::view::Variant>,
397 &mut AppContext,
398) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>;
399type FollowableItemBuilders = HashMap<
400 TypeId,
401 (
402 FollowableItemBuilder,
403 fn(&AnyViewHandle) -> Box<dyn FollowableItemHandle>,
404 ),
405>;
406pub fn register_followable_item<I: FollowableItem>(cx: &mut AppContext) {
407 cx.update_default_global(|builders: &mut FollowableItemBuilders, _| {
408 builders.insert(
409 TypeId::of::<I>(),
410 (
411 |pane, workspace, id, state, cx| {
412 I::from_state_proto(pane, workspace, id, state, cx).map(|task| {
413 cx.foreground()
414 .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
415 })
416 },
417 |this| Box::new(this.clone().downcast::<I>().unwrap()),
418 ),
419 );
420 });
421}
422
423type ItemDeserializers = HashMap<
424 Arc<str>,
425 fn(
426 ModelHandle<Project>,
427 WeakViewHandle<Workspace>,
428 WorkspaceId,
429 ItemId,
430 &mut ViewContext<Pane>,
431 ) -> Task<Result<Box<dyn ItemHandle>>>,
432>;
433pub fn register_deserializable_item<I: Item>(cx: &mut AppContext) {
434 cx.update_default_global(|deserializers: &mut ItemDeserializers, _cx| {
435 if let Some(serialized_item_kind) = I::serialized_item_kind() {
436 deserializers.insert(
437 Arc::from(serialized_item_kind),
438 |project, workspace, workspace_id, item_id, cx| {
439 let task = I::deserialize(project, workspace, workspace_id, item_id, cx);
440 cx.foreground()
441 .spawn(async { Ok(Box::new(task.await?) as Box<_>) })
442 },
443 );
444 }
445 });
446}
447
448pub struct AppState {
449 pub languages: Arc<LanguageRegistry>,
450 pub client: Arc<Client>,
451 pub user_store: ModelHandle<UserStore>,
452 pub workspace_store: ModelHandle<WorkspaceStore>,
453 pub fs: Arc<dyn fs::Fs>,
454 pub build_window_options:
455 fn(Option<WindowBounds>, Option<uuid::Uuid>, &dyn Platform) -> WindowOptions<'static>,
456 pub initialize_workspace:
457 fn(WeakViewHandle<Workspace>, bool, Arc<AppState>, AsyncAppContext) -> Task<Result<()>>,
458 pub background_actions: BackgroundActions,
459}
460
461pub struct WorkspaceStore {
462 workspaces: HashSet<WeakViewHandle<Workspace>>,
463 followers: Vec<Follower>,
464 client: Arc<Client>,
465 _subscriptions: Vec<client::Subscription>,
466}
467
468#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
469struct Follower {
470 project_id: Option<u64>,
471 peer_id: PeerId,
472}
473
474impl AppState {
475 #[cfg(any(test, feature = "test-support"))]
476 pub fn test(cx: &mut AppContext) -> Arc<Self> {
477 use settings::SettingsStore;
478
479 if !cx.has_global::<SettingsStore>() {
480 cx.set_global(SettingsStore::test(cx));
481 }
482
483 let fs = fs::FakeFs::new(cx.background().clone());
484 let languages = Arc::new(LanguageRegistry::test());
485 let http_client = util::http::FakeHttpClient::with_404_response();
486 let client = Client::new(http_client.clone(), cx);
487 let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
488 let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx));
489
490 theme::init((), cx);
491 client::init(&client, cx);
492 crate::init_settings(cx);
493
494 Arc::new(Self {
495 client,
496 fs,
497 languages,
498 user_store,
499 // channel_store,
500 workspace_store,
501 initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
502 build_window_options: |_, _, _| Default::default(),
503 background_actions: || &[],
504 })
505 }
506}
507
508struct DelayedDebouncedEditAction {
509 task: Option<Task<()>>,
510 cancel_channel: Option<oneshot::Sender<()>>,
511}
512
513impl DelayedDebouncedEditAction {
514 fn new() -> DelayedDebouncedEditAction {
515 DelayedDebouncedEditAction {
516 task: None,
517 cancel_channel: None,
518 }
519 }
520
521 fn fire_new<F>(&mut self, delay: Duration, cx: &mut ViewContext<Workspace>, func: F)
522 where
523 F: 'static + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> Task<Result<()>>,
524 {
525 if let Some(channel) = self.cancel_channel.take() {
526 _ = channel.send(());
527 }
528
529 let (sender, mut receiver) = oneshot::channel::<()>();
530 self.cancel_channel = Some(sender);
531
532 let previous_task = self.task.take();
533 self.task = Some(cx.spawn(|workspace, mut cx| async move {
534 let mut timer = cx.background().timer(delay).fuse();
535 if let Some(previous_task) = previous_task {
536 previous_task.await;
537 }
538
539 futures::select_biased! {
540 _ = receiver => return,
541 _ = timer => {}
542 }
543
544 if let Some(result) = workspace
545 .update(&mut cx, |workspace, cx| (func)(workspace, cx))
546 .log_err()
547 {
548 result.await.log_err();
549 }
550 }));
551 }
552}
553
554pub enum Event {
555 PaneAdded(ViewHandle<Pane>),
556 ContactRequestedJoin(u64),
557}
558
559pub struct Workspace {
560 weak_self: WeakViewHandle<Self>,
561 modal: Option<ActiveModal>,
562 zoomed: Option<AnyWeakViewHandle>,
563 zoomed_position: Option<DockPosition>,
564 center: PaneGroup,
565 left_dock: ViewHandle<Dock>,
566 bottom_dock: ViewHandle<Dock>,
567 right_dock: ViewHandle<Dock>,
568 panes: Vec<ViewHandle<Pane>>,
569 panes_by_item: HashMap<usize, WeakViewHandle<Pane>>,
570 active_pane: ViewHandle<Pane>,
571 last_active_center_pane: Option<WeakViewHandle<Pane>>,
572 last_active_view_id: Option<proto::ViewId>,
573 status_bar: ViewHandle<StatusBar>,
574 titlebar_item: Option<AnyViewHandle>,
575 notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
576 project: ModelHandle<Project>,
577 follower_states: HashMap<ViewHandle<Pane>, FollowerState>,
578 last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
579 window_edited: bool,
580 active_call: Option<(ModelHandle<ActiveCall>, Vec<Subscription>)>,
581 leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
582 database_id: WorkspaceId,
583 app_state: Arc<AppState>,
584 subscriptions: Vec<Subscription>,
585 _apply_leader_updates: Task<Result<()>>,
586 _observe_current_user: Task<Result<()>>,
587 _schedule_serialize: Option<Task<()>>,
588 pane_history_timestamp: Arc<AtomicUsize>,
589}
590
591struct ActiveModal {
592 view: Box<dyn ModalHandle>,
593 previously_focused_view_id: Option<usize>,
594}
595
596#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
597pub struct ViewId {
598 pub creator: PeerId,
599 pub id: u64,
600}
601
602#[derive(Default)]
603struct FollowerState {
604 leader_id: PeerId,
605 active_view_id: Option<ViewId>,
606 items_by_leader_view_id: HashMap<ViewId, Box<dyn FollowableItemHandle>>,
607}
608
609enum WorkspaceBounds {}
610
611impl Workspace {
612 pub fn new(
613 workspace_id: WorkspaceId,
614 project: ModelHandle<Project>,
615 app_state: Arc<AppState>,
616 cx: &mut ViewContext<Self>,
617 ) -> Self {
618 cx.observe(&project, |_, _, cx| cx.notify()).detach();
619 cx.subscribe(&project, move |this, _, event, cx| {
620 match event {
621 project::Event::RemoteIdChanged(_) => {
622 this.update_window_title(cx);
623 }
624
625 project::Event::CollaboratorLeft(peer_id) => {
626 this.collaborator_left(*peer_id, cx);
627 }
628
629 project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded => {
630 this.update_window_title(cx);
631 this.serialize_workspace(cx);
632 }
633
634 project::Event::DisconnectedFromHost => {
635 this.update_window_edited(cx);
636 cx.blur();
637 }
638
639 project::Event::Closed => {
640 cx.remove_window();
641 }
642
643 project::Event::DeletedEntry(entry_id) => {
644 for pane in this.panes.iter() {
645 pane.update(cx, |pane, cx| {
646 pane.handle_deleted_project_item(*entry_id, cx)
647 });
648 }
649 }
650
651 project::Event::Notification(message) => this.show_notification(0, cx, |cx| {
652 cx.add_view(|_| MessageNotification::new(message.clone()))
653 }),
654
655 _ => {}
656 }
657 cx.notify()
658 })
659 .detach();
660
661 let weak_handle = cx.weak_handle();
662 let pane_history_timestamp = Arc::new(AtomicUsize::new(0));
663
664 let center_pane = cx.add_view(|cx| {
665 Pane::new(
666 weak_handle.clone(),
667 project.clone(),
668 app_state.background_actions,
669 pane_history_timestamp.clone(),
670 cx,
671 )
672 });
673 cx.subscribe(¢er_pane, Self::handle_pane_event).detach();
674 cx.focus(¢er_pane);
675 cx.emit(Event::PaneAdded(center_pane.clone()));
676
677 app_state.workspace_store.update(cx, |store, _| {
678 store.workspaces.insert(weak_handle.clone());
679 });
680
681 let mut current_user = app_state.user_store.read(cx).watch_current_user();
682 let mut connection_status = app_state.client.status();
683 let _observe_current_user = cx.spawn(|this, mut cx| async move {
684 current_user.recv().await;
685 connection_status.recv().await;
686 let mut stream =
687 Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
688
689 while stream.recv().await.is_some() {
690 this.update(&mut cx, |_, cx| cx.notify())?;
691 }
692 anyhow::Ok(())
693 });
694
695 // All leader updates are enqueued and then processed in a single task, so
696 // that each asynchronous operation can be run in order.
697 let (leader_updates_tx, mut leader_updates_rx) =
698 mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>();
699 let _apply_leader_updates = cx.spawn(|this, mut cx| async move {
700 while let Some((leader_id, update)) = leader_updates_rx.next().await {
701 Self::process_leader_update(&this, leader_id, update, &mut cx)
702 .await
703 .log_err();
704 }
705
706 Ok(())
707 });
708
709 cx.emit_global(WorkspaceCreated(weak_handle.clone()));
710
711 let left_dock = cx.add_view(|_| Dock::new(DockPosition::Left));
712 let bottom_dock = cx.add_view(|_| Dock::new(DockPosition::Bottom));
713 let right_dock = cx.add_view(|_| Dock::new(DockPosition::Right));
714 let left_dock_buttons =
715 cx.add_view(|cx| PanelButtons::new(left_dock.clone(), weak_handle.clone(), cx));
716 let bottom_dock_buttons =
717 cx.add_view(|cx| PanelButtons::new(bottom_dock.clone(), weak_handle.clone(), cx));
718 let right_dock_buttons =
719 cx.add_view(|cx| PanelButtons::new(right_dock.clone(), weak_handle.clone(), cx));
720 let status_bar = cx.add_view(|cx| {
721 let mut status_bar = StatusBar::new(¢er_pane.clone(), cx);
722 status_bar.add_left_item(left_dock_buttons, cx);
723 status_bar.add_right_item(right_dock_buttons, cx);
724 status_bar.add_right_item(bottom_dock_buttons, cx);
725 status_bar
726 });
727
728 cx.update_default_global::<DragAndDrop<Workspace>, _, _>(|drag_and_drop, _| {
729 drag_and_drop.register_container(weak_handle.clone());
730 });
731
732 let mut active_call = None;
733 if cx.has_global::<ModelHandle<ActiveCall>>() {
734 let call = cx.global::<ModelHandle<ActiveCall>>().clone();
735 let mut subscriptions = Vec::new();
736 subscriptions.push(cx.subscribe(&call, Self::on_active_call_event));
737 active_call = Some((call, subscriptions));
738 }
739
740 let subscriptions = vec![
741 cx.observe_fullscreen(|_, _, cx| cx.notify()),
742 cx.observe_window_activation(Self::on_window_activation_changed),
743 cx.observe_window_bounds(move |_, mut bounds, display, cx| {
744 // Transform fixed bounds to be stored in terms of the containing display
745 if let WindowBounds::Fixed(mut window_bounds) = bounds {
746 if let Some(screen) = cx.platform().screen_by_id(display) {
747 let screen_bounds = screen.bounds();
748 window_bounds
749 .set_origin_x(window_bounds.origin_x() - screen_bounds.origin_x());
750 window_bounds
751 .set_origin_y(window_bounds.origin_y() - screen_bounds.origin_y());
752 bounds = WindowBounds::Fixed(window_bounds);
753 }
754 }
755
756 cx.background()
757 .spawn(DB.set_window_bounds(workspace_id, bounds, display))
758 .detach_and_log_err(cx);
759 }),
760 cx.observe(&left_dock, |this, _, cx| {
761 this.serialize_workspace(cx);
762 cx.notify();
763 }),
764 cx.observe(&bottom_dock, |this, _, cx| {
765 this.serialize_workspace(cx);
766 cx.notify();
767 }),
768 cx.observe(&right_dock, |this, _, cx| {
769 this.serialize_workspace(cx);
770 cx.notify();
771 }),
772 ];
773
774 cx.defer(|this, cx| this.update_window_title(cx));
775 Workspace {
776 weak_self: weak_handle.clone(),
777 modal: None,
778 zoomed: None,
779 zoomed_position: None,
780 center: PaneGroup::new(center_pane.clone()),
781 panes: vec![center_pane.clone()],
782 panes_by_item: Default::default(),
783 active_pane: center_pane.clone(),
784 last_active_center_pane: Some(center_pane.downgrade()),
785 last_active_view_id: None,
786 status_bar,
787 titlebar_item: None,
788 notifications: Default::default(),
789 left_dock,
790 bottom_dock,
791 right_dock,
792 project: project.clone(),
793 follower_states: Default::default(),
794 last_leaders_by_pane: Default::default(),
795 window_edited: false,
796 active_call,
797 database_id: workspace_id,
798 app_state,
799 _observe_current_user,
800 _apply_leader_updates,
801 _schedule_serialize: None,
802 leader_updates_tx,
803 subscriptions,
804 pane_history_timestamp,
805 }
806 }
807
808 fn new_local(
809 abs_paths: Vec<PathBuf>,
810 app_state: Arc<AppState>,
811 requesting_window: Option<WindowHandle<Workspace>>,
812 cx: &mut AppContext,
813 ) -> Task<(
814 WeakViewHandle<Workspace>,
815 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
816 )> {
817 let project_handle = Project::local(
818 app_state.client.clone(),
819 app_state.user_store.clone(),
820 app_state.languages.clone(),
821 app_state.fs.clone(),
822 cx,
823 );
824
825 cx.spawn(|mut cx| async move {
826 let serialized_workspace = persistence::DB.workspace_for_roots(&abs_paths.as_slice());
827
828 let paths_to_open = Arc::new(abs_paths);
829
830 // Get project paths for all of the abs_paths
831 let mut worktree_roots: HashSet<Arc<Path>> = Default::default();
832 let mut project_paths: Vec<(PathBuf, Option<ProjectPath>)> =
833 Vec::with_capacity(paths_to_open.len());
834 for path in paths_to_open.iter().cloned() {
835 if let Some((worktree, project_entry)) = cx
836 .update(|cx| {
837 Workspace::project_path_for_path(project_handle.clone(), &path, true, cx)
838 })
839 .await
840 .log_err()
841 {
842 worktree_roots.insert(worktree.read_with(&mut cx, |tree, _| tree.abs_path()));
843 project_paths.push((path, Some(project_entry)));
844 } else {
845 project_paths.push((path, None));
846 }
847 }
848
849 let workspace_id = if let Some(serialized_workspace) = serialized_workspace.as_ref() {
850 serialized_workspace.id
851 } else {
852 DB.next_id().await.unwrap_or(0)
853 };
854
855 let window = if let Some(window) = requesting_window {
856 window.replace_root(&mut cx, |cx| {
857 Workspace::new(workspace_id, project_handle.clone(), app_state.clone(), cx)
858 });
859 window
860 } else {
861 {
862 let window_bounds_override = window_bounds_env_override(&cx);
863 let (bounds, display) = if let Some(bounds) = window_bounds_override {
864 (Some(bounds), None)
865 } else {
866 serialized_workspace
867 .as_ref()
868 .and_then(|serialized_workspace| {
869 let display = serialized_workspace.display?;
870 let mut bounds = serialized_workspace.bounds?;
871
872 // Stored bounds are relative to the containing display.
873 // So convert back to global coordinates if that screen still exists
874 if let WindowBounds::Fixed(mut window_bounds) = bounds {
875 if let Some(screen) = cx.platform().screen_by_id(display) {
876 let screen_bounds = screen.bounds();
877 window_bounds.set_origin_x(
878 window_bounds.origin_x() + screen_bounds.origin_x(),
879 );
880 window_bounds.set_origin_y(
881 window_bounds.origin_y() + screen_bounds.origin_y(),
882 );
883 bounds = WindowBounds::Fixed(window_bounds);
884 } else {
885 // Screen no longer exists. Return none here.
886 return None;
887 }
888 }
889
890 Some((bounds, display))
891 })
892 .unzip()
893 };
894
895 // Use the serialized workspace to construct the new window
896 cx.add_window(
897 (app_state.build_window_options)(bounds, display, cx.platform().as_ref()),
898 |cx| {
899 Workspace::new(
900 workspace_id,
901 project_handle.clone(),
902 app_state.clone(),
903 cx,
904 )
905 },
906 )
907 }
908 };
909
910 // We haven't yielded the main thread since obtaining the window handle,
911 // so the window exists.
912 let workspace = window.root(&cx).unwrap();
913
914 (app_state.initialize_workspace)(
915 workspace.downgrade(),
916 serialized_workspace.is_some(),
917 app_state.clone(),
918 cx.clone(),
919 )
920 .await
921 .log_err();
922
923 window.update(&mut cx, |cx| cx.activate_window());
924
925 let workspace = workspace.downgrade();
926 notify_if_database_failed(&workspace, &mut cx);
927 let opened_items = open_items(
928 serialized_workspace,
929 &workspace,
930 project_paths,
931 app_state,
932 cx,
933 )
934 .await
935 .unwrap_or_default();
936
937 (workspace, opened_items)
938 })
939 }
940
941 pub fn weak_handle(&self) -> WeakViewHandle<Self> {
942 self.weak_self.clone()
943 }
944
945 pub fn left_dock(&self) -> &ViewHandle<Dock> {
946 &self.left_dock
947 }
948
949 pub fn bottom_dock(&self) -> &ViewHandle<Dock> {
950 &self.bottom_dock
951 }
952
953 pub fn right_dock(&self) -> &ViewHandle<Dock> {
954 &self.right_dock
955 }
956
957 pub fn add_panel<T: Panel>(&mut self, panel: ViewHandle<T>, cx: &mut ViewContext<Self>)
958 where
959 T::Event: std::fmt::Debug,
960 {
961 self.add_panel_with_extra_event_handler(panel, cx, |_, _, _, _| {})
962 }
963
964 pub fn add_panel_with_extra_event_handler<T: Panel, F>(
965 &mut self,
966 panel: ViewHandle<T>,
967 cx: &mut ViewContext<Self>,
968 handler: F,
969 ) where
970 T::Event: std::fmt::Debug,
971 F: Fn(&mut Self, &ViewHandle<T>, &T::Event, &mut ViewContext<Self>) + 'static,
972 {
973 let dock = match panel.position(cx) {
974 DockPosition::Left => &self.left_dock,
975 DockPosition::Bottom => &self.bottom_dock,
976 DockPosition::Right => &self.right_dock,
977 };
978
979 self.subscriptions.push(cx.subscribe(&panel, {
980 let mut dock = dock.clone();
981 let mut prev_position = panel.position(cx);
982 move |this, panel, event, cx| {
983 if T::should_change_position_on_event(event) {
984 let new_position = panel.read(cx).position(cx);
985 let mut was_visible = false;
986 dock.update(cx, |dock, cx| {
987 prev_position = new_position;
988
989 was_visible = dock.is_open()
990 && dock
991 .visible_panel()
992 .map_or(false, |active_panel| active_panel.id() == panel.id());
993 dock.remove_panel(&panel, cx);
994 });
995
996 if panel.is_zoomed(cx) {
997 this.zoomed_position = Some(new_position);
998 }
999
1000 dock = match panel.read(cx).position(cx) {
1001 DockPosition::Left => &this.left_dock,
1002 DockPosition::Bottom => &this.bottom_dock,
1003 DockPosition::Right => &this.right_dock,
1004 }
1005 .clone();
1006 dock.update(cx, |dock, cx| {
1007 dock.add_panel(panel.clone(), cx);
1008 if was_visible {
1009 dock.set_open(true, cx);
1010 dock.activate_panel(dock.panels_len() - 1, cx);
1011 }
1012 });
1013 } else if T::should_zoom_in_on_event(event) {
1014 dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, true, cx));
1015 if !panel.has_focus(cx) {
1016 cx.focus(&panel);
1017 }
1018 this.zoomed = Some(panel.downgrade().into_any());
1019 this.zoomed_position = Some(panel.read(cx).position(cx));
1020 } else if T::should_zoom_out_on_event(event) {
1021 dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, false, cx));
1022 if this.zoomed_position == Some(prev_position) {
1023 this.zoomed = None;
1024 this.zoomed_position = None;
1025 }
1026 cx.notify();
1027 } else if T::is_focus_event(event) {
1028 let position = panel.read(cx).position(cx);
1029 this.dismiss_zoomed_items_to_reveal(Some(position), cx);
1030 if panel.is_zoomed(cx) {
1031 this.zoomed = Some(panel.downgrade().into_any());
1032 this.zoomed_position = Some(position);
1033 } else {
1034 this.zoomed = None;
1035 this.zoomed_position = None;
1036 }
1037 this.update_active_view_for_followers(cx);
1038 cx.notify();
1039 } else {
1040 handler(this, &panel, event, cx)
1041 }
1042 }
1043 }));
1044
1045 dock.update(cx, |dock, cx| dock.add_panel(panel, cx));
1046 }
1047
1048 pub fn status_bar(&self) -> &ViewHandle<StatusBar> {
1049 &self.status_bar
1050 }
1051
1052 pub fn app_state(&self) -> &Arc<AppState> {
1053 &self.app_state
1054 }
1055
1056 pub fn user_store(&self) -> &ModelHandle<UserStore> {
1057 &self.app_state.user_store
1058 }
1059
1060 pub fn project(&self) -> &ModelHandle<Project> {
1061 &self.project
1062 }
1063
1064 pub fn recent_navigation_history(
1065 &self,
1066 limit: Option<usize>,
1067 cx: &AppContext,
1068 ) -> Vec<(ProjectPath, Option<PathBuf>)> {
1069 let mut abs_paths_opened: HashMap<PathBuf, HashSet<ProjectPath>> = HashMap::default();
1070 let mut history: HashMap<ProjectPath, (Option<PathBuf>, usize)> = HashMap::default();
1071 for pane in &self.panes {
1072 let pane = pane.read(cx);
1073 pane.nav_history()
1074 .for_each_entry(cx, |entry, (project_path, fs_path)| {
1075 if let Some(fs_path) = &fs_path {
1076 abs_paths_opened
1077 .entry(fs_path.clone())
1078 .or_default()
1079 .insert(project_path.clone());
1080 }
1081 let timestamp = entry.timestamp;
1082 match history.entry(project_path) {
1083 hash_map::Entry::Occupied(mut entry) => {
1084 let (_, old_timestamp) = entry.get();
1085 if ×tamp > old_timestamp {
1086 entry.insert((fs_path, timestamp));
1087 }
1088 }
1089 hash_map::Entry::Vacant(entry) => {
1090 entry.insert((fs_path, timestamp));
1091 }
1092 }
1093 });
1094 }
1095
1096 history
1097 .into_iter()
1098 .sorted_by_key(|(_, (_, timestamp))| *timestamp)
1099 .map(|(project_path, (fs_path, _))| (project_path, fs_path))
1100 .rev()
1101 .filter(|(history_path, abs_path)| {
1102 let latest_project_path_opened = abs_path
1103 .as_ref()
1104 .and_then(|abs_path| abs_paths_opened.get(abs_path))
1105 .and_then(|project_paths| {
1106 project_paths
1107 .iter()
1108 .max_by(|b1, b2| b1.worktree_id.cmp(&b2.worktree_id))
1109 });
1110
1111 match latest_project_path_opened {
1112 Some(latest_project_path_opened) => latest_project_path_opened == history_path,
1113 None => true,
1114 }
1115 })
1116 .take(limit.unwrap_or(usize::MAX))
1117 .collect()
1118 }
1119
1120 fn navigate_history(
1121 &mut self,
1122 pane: WeakViewHandle<Pane>,
1123 mode: NavigationMode,
1124 cx: &mut ViewContext<Workspace>,
1125 ) -> Task<Result<()>> {
1126 let to_load = if let Some(pane) = pane.upgrade(cx) {
1127 cx.focus(&pane);
1128
1129 pane.update(cx, |pane, cx| {
1130 loop {
1131 // Retrieve the weak item handle from the history.
1132 let entry = pane.nav_history_mut().pop(mode, cx)?;
1133
1134 // If the item is still present in this pane, then activate it.
1135 if let Some(index) = entry
1136 .item
1137 .upgrade(cx)
1138 .and_then(|v| pane.index_for_item(v.as_ref()))
1139 {
1140 let prev_active_item_index = pane.active_item_index();
1141 pane.nav_history_mut().set_mode(mode);
1142 pane.activate_item(index, true, true, cx);
1143 pane.nav_history_mut().set_mode(NavigationMode::Normal);
1144
1145 let mut navigated = prev_active_item_index != pane.active_item_index();
1146 if let Some(data) = entry.data {
1147 navigated |= pane.active_item()?.navigate(data, cx);
1148 }
1149
1150 if navigated {
1151 break None;
1152 }
1153 }
1154 // If the item is no longer present in this pane, then retrieve its
1155 // project path in order to reopen it.
1156 else {
1157 break pane
1158 .nav_history()
1159 .path_for_item(entry.item.id())
1160 .map(|(project_path, _)| (project_path, entry));
1161 }
1162 }
1163 })
1164 } else {
1165 None
1166 };
1167
1168 if let Some((project_path, entry)) = to_load {
1169 // If the item was no longer present, then load it again from its previous path.
1170 let task = self.load_path(project_path, cx);
1171 cx.spawn(|workspace, mut cx| async move {
1172 let task = task.await;
1173 let mut navigated = false;
1174 if let Some((project_entry_id, build_item)) = task.log_err() {
1175 let prev_active_item_id = pane.update(&mut cx, |pane, _| {
1176 pane.nav_history_mut().set_mode(mode);
1177 pane.active_item().map(|p| p.id())
1178 })?;
1179
1180 pane.update(&mut cx, |pane, cx| {
1181 let item = pane.open_item(project_entry_id, true, cx, build_item);
1182 navigated |= Some(item.id()) != prev_active_item_id;
1183 pane.nav_history_mut().set_mode(NavigationMode::Normal);
1184 if let Some(data) = entry.data {
1185 navigated |= item.navigate(data, cx);
1186 }
1187 })?;
1188 }
1189
1190 if !navigated {
1191 workspace
1192 .update(&mut cx, |workspace, cx| {
1193 Self::navigate_history(workspace, pane, mode, cx)
1194 })?
1195 .await?;
1196 }
1197
1198 Ok(())
1199 })
1200 } else {
1201 Task::ready(Ok(()))
1202 }
1203 }
1204
1205 pub fn go_back(
1206 &mut self,
1207 pane: WeakViewHandle<Pane>,
1208 cx: &mut ViewContext<Workspace>,
1209 ) -> Task<Result<()>> {
1210 self.navigate_history(pane, NavigationMode::GoingBack, cx)
1211 }
1212
1213 pub fn go_forward(
1214 &mut self,
1215 pane: WeakViewHandle<Pane>,
1216 cx: &mut ViewContext<Workspace>,
1217 ) -> Task<Result<()>> {
1218 self.navigate_history(pane, NavigationMode::GoingForward, cx)
1219 }
1220
1221 pub fn reopen_closed_item(&mut self, cx: &mut ViewContext<Workspace>) -> Task<Result<()>> {
1222 self.navigate_history(
1223 self.active_pane().downgrade(),
1224 NavigationMode::ReopeningClosedItem,
1225 cx,
1226 )
1227 }
1228
1229 pub fn client(&self) -> &Client {
1230 &self.app_state.client
1231 }
1232
1233 pub fn set_titlebar_item(&mut self, item: AnyViewHandle, cx: &mut ViewContext<Self>) {
1234 self.titlebar_item = Some(item);
1235 cx.notify();
1236 }
1237
1238 pub fn titlebar_item(&self) -> Option<AnyViewHandle> {
1239 self.titlebar_item.clone()
1240 }
1241
1242 /// Call the given callback with a workspace whose project is local.
1243 ///
1244 /// If the given workspace has a local project, then it will be passed
1245 /// to the callback. Otherwise, a new empty window will be created.
1246 pub fn with_local_workspace<T, F>(
1247 &mut self,
1248 cx: &mut ViewContext<Self>,
1249 callback: F,
1250 ) -> Task<Result<T>>
1251 where
1252 T: 'static,
1253 F: 'static + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
1254 {
1255 if self.project.read(cx).is_local() {
1256 Task::Ready(Some(Ok(callback(self, cx))))
1257 } else {
1258 let task = Self::new_local(Vec::new(), self.app_state.clone(), None, cx);
1259 cx.spawn(|_vh, mut cx| async move {
1260 let (workspace, _) = task.await;
1261 workspace.update(&mut cx, callback)
1262 })
1263 }
1264 }
1265
1266 pub fn worktrees<'a>(
1267 &self,
1268 cx: &'a AppContext,
1269 ) -> impl 'a + Iterator<Item = ModelHandle<Worktree>> {
1270 self.project.read(cx).worktrees(cx)
1271 }
1272
1273 pub fn visible_worktrees<'a>(
1274 &self,
1275 cx: &'a AppContext,
1276 ) -> impl 'a + Iterator<Item = ModelHandle<Worktree>> {
1277 self.project.read(cx).visible_worktrees(cx)
1278 }
1279
1280 pub fn worktree_scans_complete(&self, cx: &AppContext) -> impl Future<Output = ()> + 'static {
1281 let futures = self
1282 .worktrees(cx)
1283 .filter_map(|worktree| worktree.read(cx).as_local())
1284 .map(|worktree| worktree.scan_complete())
1285 .collect::<Vec<_>>();
1286 async move {
1287 for future in futures {
1288 future.await;
1289 }
1290 }
1291 }
1292
1293 pub fn close_global(_: &CloseWindow, cx: &mut AppContext) {
1294 cx.spawn(|mut cx| async move {
1295 let window = cx
1296 .windows()
1297 .into_iter()
1298 .find(|window| window.is_active(&cx).unwrap_or(false));
1299 if let Some(window) = window {
1300 //This can only get called when the window's project connection has been lost
1301 //so we don't need to prompt the user for anything and instead just close the window
1302 window.remove(&mut cx);
1303 }
1304 })
1305 .detach();
1306 }
1307
1308 pub fn close(
1309 &mut self,
1310 _: &CloseWindow,
1311 cx: &mut ViewContext<Self>,
1312 ) -> Option<Task<Result<()>>> {
1313 let window = cx.window();
1314 let prepare = self.prepare_to_close(false, cx);
1315 Some(cx.spawn(|_, mut cx| async move {
1316 if prepare.await? {
1317 window.remove(&mut cx);
1318 }
1319 Ok(())
1320 }))
1321 }
1322
1323 pub fn prepare_to_close(
1324 &mut self,
1325 quitting: bool,
1326 cx: &mut ViewContext<Self>,
1327 ) -> Task<Result<bool>> {
1328 let active_call = self.active_call().cloned();
1329 let window = cx.window();
1330
1331 cx.spawn(|this, mut cx| async move {
1332 let workspace_count = cx
1333 .windows()
1334 .into_iter()
1335 .filter(|window| window.root_is::<Workspace>())
1336 .count();
1337
1338 if let Some(active_call) = active_call {
1339 if !quitting
1340 && workspace_count == 1
1341 && active_call.read_with(&cx, |call, _| call.room().is_some())
1342 {
1343 let answer = window.prompt(
1344 PromptLevel::Warning,
1345 "Do you want to leave the current call?",
1346 &["Close window and hang up", "Cancel"],
1347 &mut cx,
1348 );
1349
1350 if let Some(mut answer) = answer {
1351 if answer.next().await == Some(1) {
1352 return anyhow::Ok(false);
1353 } else {
1354 active_call
1355 .update(&mut cx, |call, cx| call.hang_up(cx))
1356 .await
1357 .log_err();
1358 }
1359 }
1360 }
1361 }
1362
1363 Ok(this
1364 .update(&mut cx, |this, cx| {
1365 this.save_all_internal(SaveIntent::Close, cx)
1366 })?
1367 .await?)
1368 })
1369 }
1370
1371 fn save_all(
1372 &mut self,
1373 action: &SaveAll,
1374 cx: &mut ViewContext<Self>,
1375 ) -> Option<Task<Result<()>>> {
1376 let save_all =
1377 self.save_all_internal(action.save_intent.unwrap_or(SaveIntent::SaveAll), cx);
1378 Some(cx.foreground().spawn(async move {
1379 save_all.await?;
1380 Ok(())
1381 }))
1382 }
1383
1384 fn save_all_internal(
1385 &mut self,
1386 mut save_intent: SaveIntent,
1387 cx: &mut ViewContext<Self>,
1388 ) -> Task<Result<bool>> {
1389 if self.project.read(cx).is_read_only() {
1390 return Task::ready(Ok(true));
1391 }
1392 let dirty_items = self
1393 .panes
1394 .iter()
1395 .flat_map(|pane| {
1396 pane.read(cx).items().filter_map(|item| {
1397 if item.is_dirty(cx) {
1398 Some((pane.downgrade(), item.boxed_clone()))
1399 } else {
1400 None
1401 }
1402 })
1403 })
1404 .collect::<Vec<_>>();
1405
1406 let project = self.project.clone();
1407 cx.spawn(|workspace, mut cx| async move {
1408 // Override save mode and display "Save all files" prompt
1409 if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
1410 let mut answer = workspace.update(&mut cx, |_, cx| {
1411 let prompt = Pane::file_names_for_prompt(
1412 &mut dirty_items.iter().map(|(_, handle)| handle),
1413 dirty_items.len(),
1414 cx,
1415 );
1416 cx.prompt(
1417 PromptLevel::Warning,
1418 &prompt,
1419 &["Save all", "Discard all", "Cancel"],
1420 )
1421 })?;
1422 match answer.next().await {
1423 Some(0) => save_intent = SaveIntent::SaveAll,
1424 Some(1) => save_intent = SaveIntent::Skip,
1425 _ => {}
1426 }
1427 }
1428 for (pane, item) in dirty_items {
1429 let (singleton, project_entry_ids) =
1430 cx.read(|cx| (item.is_singleton(cx), item.project_entry_ids(cx)));
1431 if singleton || !project_entry_ids.is_empty() {
1432 if let Some(ix) =
1433 pane.read_with(&cx, |pane, _| pane.index_for_item(item.as_ref()))?
1434 {
1435 if !Pane::save_item(
1436 project.clone(),
1437 &pane,
1438 ix,
1439 &*item,
1440 save_intent,
1441 &mut cx,
1442 )
1443 .await?
1444 {
1445 return Ok(false);
1446 }
1447 }
1448 }
1449 }
1450 Ok(true)
1451 })
1452 }
1453
1454 pub fn open(&mut self, _: &Open, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
1455 let mut paths = cx.prompt_for_paths(PathPromptOptions {
1456 files: true,
1457 directories: true,
1458 multiple: true,
1459 });
1460
1461 Some(cx.spawn(|this, mut cx| async move {
1462 if let Some(paths) = paths.recv().await.flatten() {
1463 if let Some(task) = this
1464 .update(&mut cx, |this, cx| this.open_workspace_for_paths(paths, cx))
1465 .log_err()
1466 {
1467 task.await?
1468 }
1469 }
1470 Ok(())
1471 }))
1472 }
1473
1474 pub fn open_workspace_for_paths(
1475 &mut self,
1476 paths: Vec<PathBuf>,
1477 cx: &mut ViewContext<Self>,
1478 ) -> Task<Result<()>> {
1479 let window = cx.window().downcast::<Self>();
1480 let is_remote = self.project.read(cx).is_remote();
1481 let has_worktree = self.project.read(cx).worktrees(cx).next().is_some();
1482 let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx));
1483 let close_task = if is_remote || has_worktree || has_dirty_items {
1484 None
1485 } else {
1486 Some(self.prepare_to_close(false, cx))
1487 };
1488 let app_state = self.app_state.clone();
1489
1490 cx.spawn(|_, mut cx| async move {
1491 let window_to_replace = if let Some(close_task) = close_task {
1492 if !close_task.await? {
1493 return Ok(());
1494 }
1495 window
1496 } else {
1497 None
1498 };
1499 cx.update(|cx| open_paths(&paths, &app_state, window_to_replace, cx))
1500 .await?;
1501 Ok(())
1502 })
1503 }
1504
1505 #[allow(clippy::type_complexity)]
1506 pub fn open_paths(
1507 &mut self,
1508 mut abs_paths: Vec<PathBuf>,
1509 visible: bool,
1510 cx: &mut ViewContext<Self>,
1511 ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
1512 log::info!("open paths {:?}", abs_paths);
1513
1514 let fs = self.app_state.fs.clone();
1515
1516 // Sort the paths to ensure we add worktrees for parents before their children.
1517 abs_paths.sort_unstable();
1518 cx.spawn(|this, mut cx| async move {
1519 let mut tasks = Vec::with_capacity(abs_paths.len());
1520 for abs_path in &abs_paths {
1521 let project_path = match this
1522 .update(&mut cx, |this, cx| {
1523 Workspace::project_path_for_path(
1524 this.project.clone(),
1525 abs_path,
1526 visible,
1527 cx,
1528 )
1529 })
1530 .log_err()
1531 {
1532 Some(project_path) => project_path.await.log_err(),
1533 None => None,
1534 };
1535
1536 let this = this.clone();
1537 let task = cx.spawn(|mut cx| {
1538 let fs = fs.clone();
1539 let abs_path = abs_path.clone();
1540 async move {
1541 let (worktree, project_path) = project_path?;
1542 if fs.is_file(&abs_path).await {
1543 Some(
1544 this.update(&mut cx, |this, cx| {
1545 this.open_path(project_path, None, true, cx)
1546 })
1547 .log_err()?
1548 .await,
1549 )
1550 } else {
1551 this.update(&mut cx, |workspace, cx| {
1552 let worktree = worktree.read(cx);
1553 let worktree_abs_path = worktree.abs_path();
1554 let entry_id = if abs_path == worktree_abs_path.as_ref() {
1555 worktree.root_entry()
1556 } else {
1557 abs_path
1558 .strip_prefix(worktree_abs_path.as_ref())
1559 .ok()
1560 .and_then(|relative_path| {
1561 worktree.entry_for_path(relative_path)
1562 })
1563 }
1564 .map(|entry| entry.id);
1565 if let Some(entry_id) = entry_id {
1566 workspace.project().update(cx, |_, cx| {
1567 cx.emit(project::Event::ActiveEntryChanged(Some(entry_id)));
1568 })
1569 }
1570 })
1571 .log_err()?;
1572 None
1573 }
1574 }
1575 });
1576 tasks.push(task);
1577 }
1578
1579 futures::future::join_all(tasks).await
1580 })
1581 }
1582
1583 fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext<Self>) {
1584 let mut paths = cx.prompt_for_paths(PathPromptOptions {
1585 files: false,
1586 directories: true,
1587 multiple: true,
1588 });
1589 cx.spawn(|this, mut cx| async move {
1590 if let Some(paths) = paths.recv().await.flatten() {
1591 let results = this
1592 .update(&mut cx, |this, cx| this.open_paths(paths, true, cx))?
1593 .await;
1594 for result in results.into_iter().flatten() {
1595 result.log_err();
1596 }
1597 }
1598 anyhow::Ok(())
1599 })
1600 .detach_and_log_err(cx);
1601 }
1602
1603 fn project_path_for_path(
1604 project: ModelHandle<Project>,
1605 abs_path: &Path,
1606 visible: bool,
1607 cx: &mut AppContext,
1608 ) -> Task<Result<(ModelHandle<Worktree>, ProjectPath)>> {
1609 let entry = project.update(cx, |project, cx| {
1610 project.find_or_create_local_worktree(abs_path, visible, cx)
1611 });
1612 cx.spawn(|cx| async move {
1613 let (worktree, path) = entry.await?;
1614 let worktree_id = worktree.read_with(&cx, |t, _| t.id());
1615 Ok((
1616 worktree,
1617 ProjectPath {
1618 worktree_id,
1619 path: path.into(),
1620 },
1621 ))
1622 })
1623 }
1624
1625 /// Returns the modal that was toggled closed if it was open.
1626 pub fn toggle_modal<V, F>(
1627 &mut self,
1628 cx: &mut ViewContext<Self>,
1629 add_view: F,
1630 ) -> Option<ViewHandle<V>>
1631 where
1632 V: 'static + Modal,
1633 F: FnOnce(&mut Self, &mut ViewContext<Self>) -> ViewHandle<V>,
1634 {
1635 cx.notify();
1636 // Whatever modal was visible is getting clobbered. If its the same type as V, then return
1637 // it. Otherwise, create a new modal and set it as active.
1638 if let Some(already_open_modal) = self
1639 .dismiss_modal(cx)
1640 .and_then(|modal| modal.downcast::<V>())
1641 {
1642 cx.focus_self();
1643 Some(already_open_modal)
1644 } else {
1645 let modal = add_view(self, cx);
1646 cx.subscribe(&modal, |this, _, event, cx| {
1647 if V::dismiss_on_event(event) {
1648 this.dismiss_modal(cx);
1649 }
1650 })
1651 .detach();
1652 let previously_focused_view_id = cx.focused_view_id();
1653 cx.focus(&modal);
1654 self.modal = Some(ActiveModal {
1655 view: Box::new(modal),
1656 previously_focused_view_id,
1657 });
1658 None
1659 }
1660 }
1661
1662 pub fn modal<V: 'static + View>(&self) -> Option<ViewHandle<V>> {
1663 self.modal
1664 .as_ref()
1665 .and_then(|modal| modal.view.as_any().clone().downcast::<V>())
1666 }
1667
1668 pub fn dismiss_modal(&mut self, cx: &mut ViewContext<Self>) -> Option<AnyViewHandle> {
1669 if let Some(modal) = self.modal.take() {
1670 if let Some(previously_focused_view_id) = modal.previously_focused_view_id {
1671 if modal.view.has_focus(cx) {
1672 cx.window_context().focus(Some(previously_focused_view_id));
1673 }
1674 }
1675 cx.notify();
1676 Some(modal.view.as_any().clone())
1677 } else {
1678 None
1679 }
1680 }
1681
1682 pub fn items<'a>(
1683 &'a self,
1684 cx: &'a AppContext,
1685 ) -> impl 'a + Iterator<Item = &Box<dyn ItemHandle>> {
1686 self.panes.iter().flat_map(|pane| pane.read(cx).items())
1687 }
1688
1689 pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<ViewHandle<T>> {
1690 self.items_of_type(cx).max_by_key(|item| item.id())
1691 }
1692
1693 pub fn items_of_type<'a, T: Item>(
1694 &'a self,
1695 cx: &'a AppContext,
1696 ) -> impl 'a + Iterator<Item = ViewHandle<T>> {
1697 self.panes
1698 .iter()
1699 .flat_map(|pane| pane.read(cx).items_of_type())
1700 }
1701
1702 pub fn active_item(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
1703 self.active_pane().read(cx).active_item()
1704 }
1705
1706 fn active_project_path(&self, cx: &ViewContext<Self>) -> Option<ProjectPath> {
1707 self.active_item(cx).and_then(|item| item.project_path(cx))
1708 }
1709
1710 pub fn save_active_item(
1711 &mut self,
1712 save_intent: SaveIntent,
1713 cx: &mut ViewContext<Self>,
1714 ) -> Task<Result<()>> {
1715 let project = self.project.clone();
1716 let pane = self.active_pane();
1717 let item_ix = pane.read(cx).active_item_index();
1718 let item = pane.read(cx).active_item();
1719 let pane = pane.downgrade();
1720
1721 cx.spawn(|_, mut cx| async move {
1722 if let Some(item) = item {
1723 Pane::save_item(project, &pane, item_ix, item.as_ref(), save_intent, &mut cx)
1724 .await
1725 .map(|_| ())
1726 } else {
1727 Ok(())
1728 }
1729 })
1730 }
1731
1732 pub fn close_inactive_items_and_panes(
1733 &mut self,
1734 _: &CloseInactiveTabsAndPanes,
1735 cx: &mut ViewContext<Self>,
1736 ) -> Option<Task<Result<()>>> {
1737 self.close_all_internal(true, SaveIntent::Close, cx)
1738 }
1739
1740 pub fn close_all_items_and_panes(
1741 &mut self,
1742 action: &CloseAllItemsAndPanes,
1743 cx: &mut ViewContext<Self>,
1744 ) -> Option<Task<Result<()>>> {
1745 self.close_all_internal(false, action.save_intent.unwrap_or(SaveIntent::Close), cx)
1746 }
1747
1748 fn close_all_internal(
1749 &mut self,
1750 retain_active_pane: bool,
1751 save_intent: SaveIntent,
1752 cx: &mut ViewContext<Self>,
1753 ) -> Option<Task<Result<()>>> {
1754 let current_pane = self.active_pane();
1755
1756 let mut tasks = Vec::new();
1757
1758 if retain_active_pane {
1759 if let Some(current_pane_close) = current_pane.update(cx, |pane, cx| {
1760 pane.close_inactive_items(&CloseInactiveItems, cx)
1761 }) {
1762 tasks.push(current_pane_close);
1763 };
1764 }
1765
1766 for pane in self.panes() {
1767 if retain_active_pane && pane.id() == current_pane.id() {
1768 continue;
1769 }
1770
1771 if let Some(close_pane_items) = pane.update(cx, |pane: &mut Pane, cx| {
1772 pane.close_all_items(
1773 &CloseAllItems {
1774 save_intent: Some(save_intent),
1775 },
1776 cx,
1777 )
1778 }) {
1779 tasks.push(close_pane_items)
1780 }
1781 }
1782
1783 if tasks.is_empty() {
1784 None
1785 } else {
1786 Some(cx.spawn(|_, _| async move {
1787 for task in tasks {
1788 task.await?
1789 }
1790 Ok(())
1791 }))
1792 }
1793 }
1794
1795 pub fn toggle_dock(&mut self, dock_side: DockPosition, cx: &mut ViewContext<Self>) {
1796 let dock = match dock_side {
1797 DockPosition::Left => &self.left_dock,
1798 DockPosition::Bottom => &self.bottom_dock,
1799 DockPosition::Right => &self.right_dock,
1800 };
1801 let mut focus_center = false;
1802 let mut reveal_dock = false;
1803 dock.update(cx, |dock, cx| {
1804 let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side);
1805 let was_visible = dock.is_open() && !other_is_zoomed;
1806 dock.set_open(!was_visible, cx);
1807
1808 if let Some(active_panel) = dock.active_panel() {
1809 if was_visible {
1810 if active_panel.has_focus(cx) {
1811 focus_center = true;
1812 }
1813 } else {
1814 cx.focus(active_panel.as_any());
1815 reveal_dock = true;
1816 }
1817 }
1818 });
1819
1820 if reveal_dock {
1821 self.dismiss_zoomed_items_to_reveal(Some(dock_side), cx);
1822 }
1823
1824 if focus_center {
1825 cx.focus_self();
1826 }
1827
1828 cx.notify();
1829 self.serialize_workspace(cx);
1830 }
1831
1832 pub fn close_all_docks(&mut self, cx: &mut ViewContext<Self>) {
1833 let docks = [&self.left_dock, &self.bottom_dock, &self.right_dock];
1834
1835 for dock in docks {
1836 dock.update(cx, |dock, cx| {
1837 dock.set_open(false, cx);
1838 });
1839 }
1840
1841 cx.focus_self();
1842 cx.notify();
1843 self.serialize_workspace(cx);
1844 }
1845
1846 /// Transfer focus to the panel of the given type.
1847 pub fn focus_panel<T: Panel>(&mut self, cx: &mut ViewContext<Self>) -> Option<ViewHandle<T>> {
1848 self.focus_or_unfocus_panel::<T>(cx, |_, _| true)?
1849 .as_any()
1850 .clone()
1851 .downcast()
1852 }
1853
1854 /// Focus the panel of the given type if it isn't already focused. If it is
1855 /// already focused, then transfer focus back to the workspace center.
1856 pub fn toggle_panel_focus<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
1857 self.focus_or_unfocus_panel::<T>(cx, |panel, cx| !panel.has_focus(cx));
1858 }
1859
1860 /// Focus or unfocus the given panel type, depending on the given callback.
1861 fn focus_or_unfocus_panel<T: Panel>(
1862 &mut self,
1863 cx: &mut ViewContext<Self>,
1864 should_focus: impl Fn(&dyn PanelHandle, &mut ViewContext<Dock>) -> bool,
1865 ) -> Option<Rc<dyn PanelHandle>> {
1866 for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
1867 if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
1868 let mut focus_center = false;
1869 let mut reveal_dock = false;
1870 let panel = dock.update(cx, |dock, cx| {
1871 dock.activate_panel(panel_index, cx);
1872
1873 let panel = dock.active_panel().cloned();
1874 if let Some(panel) = panel.as_ref() {
1875 if should_focus(&**panel, cx) {
1876 dock.set_open(true, cx);
1877 cx.focus(panel.as_any());
1878 reveal_dock = true;
1879 } else {
1880 // if panel.is_zoomed(cx) {
1881 // dock.set_open(false, cx);
1882 // }
1883 focus_center = true;
1884 }
1885 }
1886 panel
1887 });
1888
1889 if focus_center {
1890 cx.focus_self();
1891 }
1892
1893 self.serialize_workspace(cx);
1894 cx.notify();
1895 return panel;
1896 }
1897 }
1898 None
1899 }
1900
1901 pub fn panel<T: Panel>(&self, cx: &WindowContext) -> Option<ViewHandle<T>> {
1902 for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
1903 let dock = dock.read(cx);
1904 if let Some(panel) = dock.panel::<T>() {
1905 return Some(panel);
1906 }
1907 }
1908 None
1909 }
1910
1911 fn zoom_out(&mut self, cx: &mut ViewContext<Self>) {
1912 for pane in &self.panes {
1913 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
1914 }
1915
1916 self.left_dock.update(cx, |dock, cx| dock.zoom_out(cx));
1917 self.bottom_dock.update(cx, |dock, cx| dock.zoom_out(cx));
1918 self.right_dock.update(cx, |dock, cx| dock.zoom_out(cx));
1919 self.zoomed = None;
1920 self.zoomed_position = None;
1921
1922 cx.notify();
1923 }
1924
1925 #[cfg(any(test, feature = "test-support"))]
1926 pub fn zoomed_view(&self, cx: &AppContext) -> Option<AnyViewHandle> {
1927 self.zoomed.and_then(|view| view.upgrade(cx))
1928 }
1929
1930 fn dismiss_zoomed_items_to_reveal(
1931 &mut self,
1932 dock_to_reveal: Option<DockPosition>,
1933 cx: &mut ViewContext<Self>,
1934 ) {
1935 // If a center pane is zoomed, unzoom it.
1936 for pane in &self.panes {
1937 if pane != &self.active_pane || dock_to_reveal.is_some() {
1938 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
1939 }
1940 }
1941
1942 // If another dock is zoomed, hide it.
1943 let mut focus_center = false;
1944 for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] {
1945 dock.update(cx, |dock, cx| {
1946 if Some(dock.position()) != dock_to_reveal {
1947 if let Some(panel) = dock.active_panel() {
1948 if panel.is_zoomed(cx) {
1949 focus_center |= panel.has_focus(cx);
1950 dock.set_open(false, cx);
1951 }
1952 }
1953 }
1954 });
1955 }
1956
1957 if focus_center {
1958 cx.focus_self();
1959 }
1960
1961 if self.zoomed_position != dock_to_reveal {
1962 self.zoomed = None;
1963 self.zoomed_position = None;
1964 }
1965
1966 cx.notify();
1967 }
1968
1969 fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
1970 let pane = cx.add_view(|cx| {
1971 Pane::new(
1972 self.weak_handle(),
1973 self.project.clone(),
1974 self.app_state.background_actions,
1975 self.pane_history_timestamp.clone(),
1976 cx,
1977 )
1978 });
1979 cx.subscribe(&pane, Self::handle_pane_event).detach();
1980 self.panes.push(pane.clone());
1981 cx.focus(&pane);
1982 cx.emit(Event::PaneAdded(pane.clone()));
1983 pane
1984 }
1985
1986 pub fn add_item_to_center(
1987 &mut self,
1988 item: Box<dyn ItemHandle>,
1989 cx: &mut ViewContext<Self>,
1990 ) -> bool {
1991 if let Some(center_pane) = self.last_active_center_pane.clone() {
1992 if let Some(center_pane) = center_pane.upgrade(cx) {
1993 center_pane.update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
1994 true
1995 } else {
1996 false
1997 }
1998 } else {
1999 false
2000 }
2001 }
2002
2003 pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
2004 self.active_pane
2005 .update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
2006 }
2007
2008 pub fn split_item(
2009 &mut self,
2010 split_direction: SplitDirection,
2011 item: Box<dyn ItemHandle>,
2012 cx: &mut ViewContext<Self>,
2013 ) {
2014 let new_pane = self.split_pane(self.active_pane.clone(), split_direction, cx);
2015 new_pane.update(cx, move |new_pane, cx| {
2016 new_pane.add_item(item, true, true, None, cx)
2017 })
2018 }
2019
2020 pub fn open_abs_path(
2021 &mut self,
2022 abs_path: PathBuf,
2023 visible: bool,
2024 cx: &mut ViewContext<Self>,
2025 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
2026 cx.spawn(|workspace, mut cx| async move {
2027 let open_paths_task_result = workspace
2028 .update(&mut cx, |workspace, cx| {
2029 workspace.open_paths(vec![abs_path.clone()], visible, cx)
2030 })
2031 .with_context(|| format!("open abs path {abs_path:?} task spawn"))?
2032 .await;
2033 anyhow::ensure!(
2034 open_paths_task_result.len() == 1,
2035 "open abs path {abs_path:?} task returned incorrect number of results"
2036 );
2037 match open_paths_task_result
2038 .into_iter()
2039 .next()
2040 .expect("ensured single task result")
2041 {
2042 Some(open_result) => {
2043 open_result.with_context(|| format!("open abs path {abs_path:?} task join"))
2044 }
2045 None => anyhow::bail!("open abs path {abs_path:?} task returned None"),
2046 }
2047 })
2048 }
2049
2050 pub fn split_abs_path(
2051 &mut self,
2052 abs_path: PathBuf,
2053 visible: bool,
2054 cx: &mut ViewContext<Self>,
2055 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
2056 let project_path_task =
2057 Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx);
2058 cx.spawn(|this, mut cx| async move {
2059 let (_, path) = project_path_task.await?;
2060 this.update(&mut cx, |this, cx| this.split_path(path, cx))?
2061 .await
2062 })
2063 }
2064
2065 pub fn open_path(
2066 &mut self,
2067 path: impl Into<ProjectPath>,
2068 pane: Option<WeakViewHandle<Pane>>,
2069 focus_item: bool,
2070 cx: &mut ViewContext<Self>,
2071 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2072 let pane = pane.unwrap_or_else(|| {
2073 self.last_active_center_pane.clone().unwrap_or_else(|| {
2074 self.panes
2075 .first()
2076 .expect("There must be an active pane")
2077 .downgrade()
2078 })
2079 });
2080
2081 let task = self.load_path(path.into(), cx);
2082 cx.spawn(|_, mut cx| async move {
2083 let (project_entry_id, build_item) = task.await?;
2084 pane.update(&mut cx, |pane, cx| {
2085 pane.open_item(project_entry_id, focus_item, cx, build_item)
2086 })
2087 })
2088 }
2089
2090 pub fn split_path(
2091 &mut self,
2092 path: impl Into<ProjectPath>,
2093 cx: &mut ViewContext<Self>,
2094 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2095 let pane = self.last_active_center_pane.clone().unwrap_or_else(|| {
2096 self.panes
2097 .first()
2098 .expect("There must be an active pane")
2099 .downgrade()
2100 });
2101
2102 if let Member::Pane(center_pane) = &self.center.root {
2103 if center_pane.read(cx).items_len() == 0 {
2104 return self.open_path(path, Some(pane), true, cx);
2105 }
2106 }
2107
2108 let task = self.load_path(path.into(), cx);
2109 cx.spawn(|this, mut cx| async move {
2110 let (project_entry_id, build_item) = task.await?;
2111 this.update(&mut cx, move |this, cx| -> Option<_> {
2112 let pane = pane.upgrade(cx)?;
2113 let new_pane = this.split_pane(pane, SplitDirection::Right, cx);
2114 new_pane.update(cx, |new_pane, cx| {
2115 Some(new_pane.open_item(project_entry_id, true, cx, build_item))
2116 })
2117 })
2118 .map(|option| option.ok_or_else(|| anyhow!("pane was dropped")))?
2119 })
2120 }
2121
2122 pub(crate) fn load_path(
2123 &mut self,
2124 path: ProjectPath,
2125 cx: &mut ViewContext<Self>,
2126 ) -> Task<
2127 Result<(
2128 ProjectEntryId,
2129 impl 'static + FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
2130 )>,
2131 > {
2132 let project = self.project().clone();
2133 let project_item = project.update(cx, |project, cx| project.open_path(path, cx));
2134 cx.spawn(|_, mut cx| async move {
2135 let (project_entry_id, project_item) = project_item.await?;
2136 let build_item = cx.update(|cx| {
2137 cx.default_global::<ProjectItemBuilders>()
2138 .get(&project_item.model_type())
2139 .ok_or_else(|| anyhow!("no item builder for project item"))
2140 .cloned()
2141 })?;
2142 let build_item =
2143 move |cx: &mut ViewContext<Pane>| build_item(project, project_item, cx);
2144 Ok((project_entry_id, build_item))
2145 })
2146 }
2147
2148 pub fn open_project_item<T>(
2149 &mut self,
2150 project_item: ModelHandle<T::Item>,
2151 cx: &mut ViewContext<Self>,
2152 ) -> ViewHandle<T>
2153 where
2154 T: ProjectItem,
2155 {
2156 use project::Item as _;
2157
2158 let entry_id = project_item.read(cx).entry_id(cx);
2159 if let Some(item) = entry_id
2160 .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx))
2161 .and_then(|item| item.downcast())
2162 {
2163 self.activate_item(&item, cx);
2164 return item;
2165 }
2166
2167 let item = cx.add_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
2168 self.add_item(Box::new(item.clone()), cx);
2169 item
2170 }
2171
2172 pub fn split_project_item<T>(
2173 &mut self,
2174 project_item: ModelHandle<T::Item>,
2175 cx: &mut ViewContext<Self>,
2176 ) -> ViewHandle<T>
2177 where
2178 T: ProjectItem,
2179 {
2180 use project::Item as _;
2181
2182 let entry_id = project_item.read(cx).entry_id(cx);
2183 if let Some(item) = entry_id
2184 .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx))
2185 .and_then(|item| item.downcast())
2186 {
2187 self.activate_item(&item, cx);
2188 return item;
2189 }
2190
2191 let item = cx.add_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
2192 self.split_item(SplitDirection::Right, Box::new(item.clone()), cx);
2193 item
2194 }
2195
2196 pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
2197 if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &self.active_pane, cx) {
2198 self.active_pane.update(cx, |pane, cx| {
2199 pane.add_item(Box::new(shared_screen), false, true, None, cx)
2200 });
2201 }
2202 }
2203
2204 pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut ViewContext<Self>) -> bool {
2205 let result = self.panes.iter().find_map(|pane| {
2206 pane.read(cx)
2207 .index_for_item(item)
2208 .map(|ix| (pane.clone(), ix))
2209 });
2210 if let Some((pane, ix)) = result {
2211 pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx));
2212 true
2213 } else {
2214 false
2215 }
2216 }
2217
2218 fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext<Self>) {
2219 let panes = self.center.panes();
2220 if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
2221 cx.focus(&pane);
2222 } else {
2223 self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, cx);
2224 }
2225 }
2226
2227 pub fn activate_next_pane(&mut self, cx: &mut ViewContext<Self>) {
2228 let panes = self.center.panes();
2229 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
2230 let next_ix = (ix + 1) % panes.len();
2231 let next_pane = panes[next_ix].clone();
2232 cx.focus(&next_pane);
2233 }
2234 }
2235
2236 pub fn activate_previous_pane(&mut self, cx: &mut ViewContext<Self>) {
2237 let panes = self.center.panes();
2238 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
2239 let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
2240 let prev_pane = panes[prev_ix].clone();
2241 cx.focus(&prev_pane);
2242 }
2243 }
2244
2245 pub fn activate_pane_in_direction(
2246 &mut self,
2247 direction: SplitDirection,
2248 cx: &mut ViewContext<Self>,
2249 ) {
2250 if let Some(pane) = self.find_pane_in_direction(direction, cx) {
2251 cx.focus(pane);
2252 }
2253 }
2254
2255 pub fn swap_pane_in_direction(
2256 &mut self,
2257 direction: SplitDirection,
2258 cx: &mut ViewContext<Self>,
2259 ) {
2260 if let Some(to) = self
2261 .find_pane_in_direction(direction, cx)
2262 .map(|pane| pane.clone())
2263 {
2264 self.center.swap(&self.active_pane.clone(), &to);
2265 cx.notify();
2266 }
2267 }
2268
2269 fn find_pane_in_direction(
2270 &mut self,
2271 direction: SplitDirection,
2272 cx: &mut ViewContext<Self>,
2273 ) -> Option<&ViewHandle<Pane>> {
2274 let Some(bounding_box) = self.center.bounding_box_for_pane(&self.active_pane) else {
2275 return None;
2276 };
2277 let cursor = self.active_pane.read(cx).pixel_position_of_cursor(cx);
2278 let center = match cursor {
2279 Some(cursor) if bounding_box.contains_point(cursor) => cursor,
2280 _ => bounding_box.center(),
2281 };
2282
2283 let distance_to_next = theme::current(cx).workspace.pane_divider.width + 1.;
2284
2285 let target = match direction {
2286 SplitDirection::Left => vec2f(bounding_box.origin_x() - distance_to_next, center.y()),
2287 SplitDirection::Right => vec2f(bounding_box.max_x() + distance_to_next, center.y()),
2288 SplitDirection::Up => vec2f(center.x(), bounding_box.origin_y() - distance_to_next),
2289 SplitDirection::Down => vec2f(center.x(), bounding_box.max_y() + distance_to_next),
2290 };
2291 self.center.pane_at_pixel_position(target)
2292 }
2293
2294 fn handle_pane_focused(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
2295 if self.active_pane != pane {
2296 self.active_pane = pane.clone();
2297 self.status_bar.update(cx, |status_bar, cx| {
2298 status_bar.set_active_pane(&self.active_pane, cx);
2299 });
2300 self.active_item_path_changed(cx);
2301 self.last_active_center_pane = Some(pane.downgrade());
2302 }
2303
2304 self.dismiss_zoomed_items_to_reveal(None, cx);
2305 if pane.read(cx).is_zoomed() {
2306 self.zoomed = Some(pane.downgrade().into_any());
2307 } else {
2308 self.zoomed = None;
2309 }
2310 self.zoomed_position = None;
2311 self.update_active_view_for_followers(cx);
2312
2313 cx.notify();
2314 }
2315
2316 fn handle_pane_event(
2317 &mut self,
2318 pane: ViewHandle<Pane>,
2319 event: &pane::Event,
2320 cx: &mut ViewContext<Self>,
2321 ) {
2322 match event {
2323 pane::Event::AddItem { item } => item.added_to_pane(self, pane, cx),
2324 pane::Event::Split(direction) => {
2325 self.split_and_clone(pane, *direction, cx);
2326 }
2327 pane::Event::Remove => self.remove_pane(pane, cx),
2328 pane::Event::ActivateItem { local } => {
2329 if *local {
2330 self.unfollow(&pane, cx);
2331 }
2332 if &pane == self.active_pane() {
2333 self.active_item_path_changed(cx);
2334 }
2335 }
2336 pane::Event::ChangeItemTitle => {
2337 if pane == self.active_pane {
2338 self.active_item_path_changed(cx);
2339 }
2340 self.update_window_edited(cx);
2341 }
2342 pane::Event::RemoveItem { item_id } => {
2343 self.update_window_edited(cx);
2344 if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) {
2345 if entry.get().id() == pane.id() {
2346 entry.remove();
2347 }
2348 }
2349 }
2350 pane::Event::Focus => {
2351 self.handle_pane_focused(pane.clone(), cx);
2352 }
2353 pane::Event::ZoomIn => {
2354 if pane == self.active_pane {
2355 pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
2356 if pane.read(cx).has_focus() {
2357 self.zoomed = Some(pane.downgrade().into_any());
2358 self.zoomed_position = None;
2359 }
2360 cx.notify();
2361 }
2362 }
2363 pane::Event::ZoomOut => {
2364 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
2365 if self.zoomed_position.is_none() {
2366 self.zoomed = None;
2367 }
2368 cx.notify();
2369 }
2370 }
2371
2372 self.serialize_workspace(cx);
2373 }
2374
2375 pub fn split_pane(
2376 &mut self,
2377 pane_to_split: ViewHandle<Pane>,
2378 split_direction: SplitDirection,
2379 cx: &mut ViewContext<Self>,
2380 ) -> ViewHandle<Pane> {
2381 let new_pane = self.add_pane(cx);
2382 self.center
2383 .split(&pane_to_split, &new_pane, split_direction)
2384 .unwrap();
2385 cx.notify();
2386 new_pane
2387 }
2388
2389 pub fn split_and_clone(
2390 &mut self,
2391 pane: ViewHandle<Pane>,
2392 direction: SplitDirection,
2393 cx: &mut ViewContext<Self>,
2394 ) -> Option<ViewHandle<Pane>> {
2395 let item = pane.read(cx).active_item()?;
2396 let maybe_pane_handle = if let Some(clone) = item.clone_on_split(self.database_id(), cx) {
2397 let new_pane = self.add_pane(cx);
2398 new_pane.update(cx, |pane, cx| pane.add_item(clone, true, true, None, cx));
2399 self.center.split(&pane, &new_pane, direction).unwrap();
2400 Some(new_pane)
2401 } else {
2402 None
2403 };
2404 cx.notify();
2405 maybe_pane_handle
2406 }
2407
2408 pub fn split_pane_with_item(
2409 &mut self,
2410 pane_to_split: WeakViewHandle<Pane>,
2411 split_direction: SplitDirection,
2412 from: WeakViewHandle<Pane>,
2413 item_id_to_move: usize,
2414 cx: &mut ViewContext<Self>,
2415 ) {
2416 let Some(pane_to_split) = pane_to_split.upgrade(cx) else {
2417 return;
2418 };
2419 let Some(from) = from.upgrade(cx) else {
2420 return;
2421 };
2422
2423 let new_pane = self.add_pane(cx);
2424 self.move_item(from.clone(), new_pane.clone(), item_id_to_move, 0, cx);
2425 self.center
2426 .split(&pane_to_split, &new_pane, split_direction)
2427 .unwrap();
2428 cx.notify();
2429 }
2430
2431 pub fn split_pane_with_project_entry(
2432 &mut self,
2433 pane_to_split: WeakViewHandle<Pane>,
2434 split_direction: SplitDirection,
2435 project_entry: ProjectEntryId,
2436 cx: &mut ViewContext<Self>,
2437 ) -> Option<Task<Result<()>>> {
2438 let pane_to_split = pane_to_split.upgrade(cx)?;
2439 let new_pane = self.add_pane(cx);
2440 self.center
2441 .split(&pane_to_split, &new_pane, split_direction)
2442 .unwrap();
2443
2444 let path = self.project.read(cx).path_for_entry(project_entry, cx)?;
2445 let task = self.open_path(path, Some(new_pane.downgrade()), true, cx);
2446 Some(cx.foreground().spawn(async move {
2447 task.await?;
2448 Ok(())
2449 }))
2450 }
2451
2452 pub fn move_item(
2453 &mut self,
2454 source: ViewHandle<Pane>,
2455 destination: ViewHandle<Pane>,
2456 item_id_to_move: usize,
2457 destination_index: usize,
2458 cx: &mut ViewContext<Self>,
2459 ) {
2460 let item_to_move = source
2461 .read(cx)
2462 .items()
2463 .enumerate()
2464 .find(|(_, item_handle)| item_handle.id() == item_id_to_move);
2465
2466 if item_to_move.is_none() {
2467 log::warn!("Tried to move item handle which was not in `from` pane. Maybe tab was closed during drop");
2468 return;
2469 }
2470 let (item_ix, item_handle) = item_to_move.unwrap();
2471 let item_handle = item_handle.clone();
2472
2473 if source != destination {
2474 // Close item from previous pane
2475 source.update(cx, |source, cx| {
2476 source.remove_item(item_ix, false, cx);
2477 });
2478 }
2479
2480 // This automatically removes duplicate items in the pane
2481 destination.update(cx, |destination, cx| {
2482 destination.add_item(item_handle, true, true, Some(destination_index), cx);
2483 cx.focus_self();
2484 });
2485 }
2486
2487 fn remove_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
2488 if self.center.remove(&pane).unwrap() {
2489 self.force_remove_pane(&pane, cx);
2490 self.unfollow(&pane, cx);
2491 self.last_leaders_by_pane.remove(&pane.downgrade());
2492 for removed_item in pane.read(cx).items() {
2493 self.panes_by_item.remove(&removed_item.id());
2494 }
2495
2496 cx.notify();
2497 } else {
2498 self.active_item_path_changed(cx);
2499 }
2500 }
2501
2502 pub fn panes(&self) -> &[ViewHandle<Pane>] {
2503 &self.panes
2504 }
2505
2506 pub fn active_pane(&self) -> &ViewHandle<Pane> {
2507 &self.active_pane
2508 }
2509
2510 fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
2511 self.follower_states.retain(|_, state| {
2512 if state.leader_id == peer_id {
2513 for item in state.items_by_leader_view_id.values() {
2514 item.set_leader_peer_id(None, cx);
2515 }
2516 false
2517 } else {
2518 true
2519 }
2520 });
2521 cx.notify();
2522 }
2523
2524 fn start_following(
2525 &mut self,
2526 leader_id: PeerId,
2527 cx: &mut ViewContext<Self>,
2528 ) -> Option<Task<Result<()>>> {
2529 let pane = self.active_pane().clone();
2530
2531 self.last_leaders_by_pane
2532 .insert(pane.downgrade(), leader_id);
2533 self.unfollow(&pane, cx);
2534 self.follower_states.insert(
2535 pane.clone(),
2536 FollowerState {
2537 leader_id,
2538 active_view_id: None,
2539 items_by_leader_view_id: Default::default(),
2540 },
2541 );
2542 cx.notify();
2543
2544 let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
2545 let project_id = self.project.read(cx).remote_id();
2546 let request = self.app_state.client.request(proto::Follow {
2547 room_id,
2548 project_id,
2549 leader_id: Some(leader_id),
2550 });
2551
2552 Some(cx.spawn(|this, mut cx| async move {
2553 let response = request.await?;
2554 this.update(&mut cx, |this, _| {
2555 let state = this
2556 .follower_states
2557 .get_mut(&pane)
2558 .ok_or_else(|| anyhow!("following interrupted"))?;
2559 state.active_view_id = if let Some(active_view_id) = response.active_view_id {
2560 Some(ViewId::from_proto(active_view_id)?)
2561 } else {
2562 None
2563 };
2564 Ok::<_, anyhow::Error>(())
2565 })??;
2566 Self::add_views_from_leader(
2567 this.clone(),
2568 leader_id,
2569 vec![pane],
2570 response.views,
2571 &mut cx,
2572 )
2573 .await?;
2574 this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?;
2575 Ok(())
2576 }))
2577 }
2578
2579 pub fn follow_next_collaborator(
2580 &mut self,
2581 _: &FollowNextCollaborator,
2582 cx: &mut ViewContext<Self>,
2583 ) -> Option<Task<Result<()>>> {
2584 let collaborators = self.project.read(cx).collaborators();
2585 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
2586 let mut collaborators = collaborators.keys().copied();
2587 for peer_id in collaborators.by_ref() {
2588 if peer_id == leader_id {
2589 break;
2590 }
2591 }
2592 collaborators.next()
2593 } else if let Some(last_leader_id) =
2594 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
2595 {
2596 if collaborators.contains_key(last_leader_id) {
2597 Some(*last_leader_id)
2598 } else {
2599 None
2600 }
2601 } else {
2602 None
2603 };
2604
2605 let pane = self.active_pane.clone();
2606 let Some(leader_id) = next_leader_id.or_else(|| collaborators.keys().copied().next())
2607 else {
2608 return None;
2609 };
2610 if Some(leader_id) == self.unfollow(&pane, cx) {
2611 return None;
2612 }
2613 self.follow(leader_id, cx)
2614 }
2615
2616 pub fn follow(
2617 &mut self,
2618 leader_id: PeerId,
2619 cx: &mut ViewContext<Self>,
2620 ) -> Option<Task<Result<()>>> {
2621 let room = ActiveCall::global(cx).read(cx).room()?.read(cx);
2622 let project = self.project.read(cx);
2623
2624 let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else {
2625 return None;
2626 };
2627
2628 let other_project_id = match remote_participant.location {
2629 call::ParticipantLocation::External => None,
2630 call::ParticipantLocation::UnsharedProject => None,
2631 call::ParticipantLocation::SharedProject { project_id } => {
2632 if Some(project_id) == project.remote_id() {
2633 None
2634 } else {
2635 Some(project_id)
2636 }
2637 }
2638 };
2639
2640 // if they are active in another project, follow there.
2641 if let Some(project_id) = other_project_id {
2642 let app_state = self.app_state.clone();
2643 return Some(crate::join_remote_project(
2644 project_id,
2645 remote_participant.user.id,
2646 app_state,
2647 cx,
2648 ));
2649 }
2650
2651 // if you're already following, find the right pane and focus it.
2652 for (pane, state) in &self.follower_states {
2653 if leader_id == state.leader_id {
2654 cx.focus(pane);
2655 return None;
2656 }
2657 }
2658
2659 // Otherwise, follow.
2660 self.start_following(leader_id, cx)
2661 }
2662
2663 pub fn unfollow(
2664 &mut self,
2665 pane: &ViewHandle<Pane>,
2666 cx: &mut ViewContext<Self>,
2667 ) -> Option<PeerId> {
2668 let state = self.follower_states.remove(pane)?;
2669 let leader_id = state.leader_id;
2670 for (_, item) in state.items_by_leader_view_id {
2671 item.set_leader_peer_id(None, cx);
2672 }
2673
2674 if self
2675 .follower_states
2676 .values()
2677 .all(|state| state.leader_id != state.leader_id)
2678 {
2679 let project_id = self.project.read(cx).remote_id();
2680 let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
2681 self.app_state
2682 .client
2683 .send(proto::Unfollow {
2684 room_id,
2685 project_id,
2686 leader_id: Some(leader_id),
2687 })
2688 .log_err();
2689 }
2690
2691 cx.notify();
2692 Some(leader_id)
2693 }
2694
2695 pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
2696 self.follower_states
2697 .values()
2698 .any(|state| state.leader_id == peer_id)
2699 }
2700
2701 fn render_titlebar(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
2702 // TODO: There should be a better system in place for this
2703 // (https://github.com/zed-industries/zed/issues/1290)
2704 let is_fullscreen = cx.window_is_fullscreen();
2705 let container_theme = if is_fullscreen {
2706 let mut container_theme = theme.titlebar.container;
2707 container_theme.padding.left = container_theme.padding.right;
2708 container_theme
2709 } else {
2710 theme.titlebar.container
2711 };
2712
2713 enum TitleBar {}
2714 MouseEventHandler::new::<TitleBar, _>(0, cx, |_, cx| {
2715 Stack::new()
2716 .with_children(
2717 self.titlebar_item
2718 .as_ref()
2719 .map(|item| ChildView::new(item, cx)),
2720 )
2721 .contained()
2722 .with_style(container_theme)
2723 })
2724 .on_click(MouseButton::Left, |event, _, cx| {
2725 if event.click_count == 2 {
2726 cx.zoom_window();
2727 }
2728 })
2729 .constrained()
2730 .with_height(theme.titlebar.height)
2731 .into_any_named("titlebar")
2732 }
2733
2734 fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
2735 let active_entry = self.active_project_path(cx);
2736 self.project
2737 .update(cx, |project, cx| project.set_active_path(active_entry, cx));
2738 self.update_window_title(cx);
2739 }
2740
2741 fn update_window_title(&mut self, cx: &mut ViewContext<Self>) {
2742 let project = self.project().read(cx);
2743 let mut title = String::new();
2744
2745 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
2746 let filename = path
2747 .path
2748 .file_name()
2749 .map(|s| s.to_string_lossy())
2750 .or_else(|| {
2751 Some(Cow::Borrowed(
2752 project
2753 .worktree_for_id(path.worktree_id, cx)?
2754 .read(cx)
2755 .root_name(),
2756 ))
2757 });
2758
2759 if let Some(filename) = filename {
2760 title.push_str(filename.as_ref());
2761 title.push_str(" β ");
2762 }
2763 }
2764
2765 for (i, name) in project.worktree_root_names(cx).enumerate() {
2766 if i > 0 {
2767 title.push_str(", ");
2768 }
2769 title.push_str(name);
2770 }
2771
2772 if title.is_empty() {
2773 title = "empty project".to_string();
2774 }
2775
2776 if project.is_remote() {
2777 title.push_str(" β");
2778 } else if project.is_shared() {
2779 title.push_str(" β");
2780 }
2781
2782 cx.set_window_title(&title);
2783 }
2784
2785 fn update_window_edited(&mut self, cx: &mut ViewContext<Self>) {
2786 let is_edited = !self.project.read(cx).is_read_only()
2787 && self
2788 .items(cx)
2789 .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
2790 if is_edited != self.window_edited {
2791 self.window_edited = is_edited;
2792 cx.set_window_edited(self.window_edited)
2793 }
2794 }
2795
2796 fn render_disconnected_overlay(
2797 &self,
2798 cx: &mut ViewContext<Workspace>,
2799 ) -> Option<AnyElement<Workspace>> {
2800 if self.project.read(cx).is_read_only() {
2801 enum DisconnectedOverlay {}
2802 Some(
2803 MouseEventHandler::new::<DisconnectedOverlay, _>(0, cx, |_, cx| {
2804 let theme = &theme::current(cx);
2805 Label::new(
2806 "Your connection to the remote project has been lost.",
2807 theme.workspace.disconnected_overlay.text.clone(),
2808 )
2809 .aligned()
2810 .contained()
2811 .with_style(theme.workspace.disconnected_overlay.container)
2812 })
2813 .with_cursor_style(CursorStyle::Arrow)
2814 .capture_all()
2815 .into_any_named("disconnected overlay"),
2816 )
2817 } else {
2818 None
2819 }
2820 }
2821
2822 fn render_notifications(
2823 &self,
2824 theme: &theme::Workspace,
2825 cx: &AppContext,
2826 ) -> Option<AnyElement<Workspace>> {
2827 if self.notifications.is_empty() {
2828 None
2829 } else {
2830 Some(
2831 Flex::column()
2832 .with_children(self.notifications.iter().map(|(_, _, notification)| {
2833 ChildView::new(notification.as_any(), cx)
2834 .contained()
2835 .with_style(theme.notification)
2836 }))
2837 .constrained()
2838 .with_width(theme.notifications.width)
2839 .contained()
2840 .with_style(theme.notifications.container)
2841 .aligned()
2842 .bottom()
2843 .right()
2844 .into_any(),
2845 )
2846 }
2847 }
2848
2849 // RPC handlers
2850
2851 fn handle_follow(
2852 &mut self,
2853 follower_project_id: Option<u64>,
2854 cx: &mut ViewContext<Self>,
2855 ) -> proto::FollowResponse {
2856 let client = &self.app_state.client;
2857 let project_id = self.project.read(cx).remote_id();
2858
2859 let active_view_id = self.active_item(cx).and_then(|i| {
2860 Some(
2861 i.to_followable_item_handle(cx)?
2862 .remote_id(client, cx)?
2863 .to_proto(),
2864 )
2865 });
2866
2867 cx.notify();
2868
2869 self.last_active_view_id = active_view_id.clone();
2870 proto::FollowResponse {
2871 active_view_id,
2872 views: self
2873 .panes()
2874 .iter()
2875 .flat_map(|pane| {
2876 let leader_id = self.leader_for_pane(pane);
2877 pane.read(cx).items().filter_map({
2878 let cx = &cx;
2879 move |item| {
2880 let item = item.to_followable_item_handle(cx)?;
2881 if (project_id.is_none() || project_id != follower_project_id)
2882 && item.is_project_item(cx)
2883 {
2884 return None;
2885 }
2886 let id = item.remote_id(client, cx)?.to_proto();
2887 let variant = item.to_state_proto(cx)?;
2888 Some(proto::View {
2889 id: Some(id),
2890 leader_id,
2891 variant: Some(variant),
2892 })
2893 }
2894 })
2895 })
2896 .collect(),
2897 }
2898 }
2899
2900 fn handle_update_followers(
2901 &mut self,
2902 leader_id: PeerId,
2903 message: proto::UpdateFollowers,
2904 _cx: &mut ViewContext<Self>,
2905 ) {
2906 self.leader_updates_tx
2907 .unbounded_send((leader_id, message))
2908 .ok();
2909 }
2910
2911 async fn process_leader_update(
2912 this: &WeakViewHandle<Self>,
2913 leader_id: PeerId,
2914 update: proto::UpdateFollowers,
2915 cx: &mut AsyncAppContext,
2916 ) -> Result<()> {
2917 match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
2918 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
2919 this.update(cx, |this, _| {
2920 for (_, state) in &mut this.follower_states {
2921 if state.leader_id == leader_id {
2922 state.active_view_id =
2923 if let Some(active_view_id) = update_active_view.id.clone() {
2924 Some(ViewId::from_proto(active_view_id)?)
2925 } else {
2926 None
2927 };
2928 }
2929 }
2930 anyhow::Ok(())
2931 })??;
2932 }
2933 proto::update_followers::Variant::UpdateView(update_view) => {
2934 let variant = update_view
2935 .variant
2936 .ok_or_else(|| anyhow!("missing update view variant"))?;
2937 let id = update_view
2938 .id
2939 .ok_or_else(|| anyhow!("missing update view id"))?;
2940 let mut tasks = Vec::new();
2941 this.update(cx, |this, cx| {
2942 let project = this.project.clone();
2943 for (_, state) in &mut this.follower_states {
2944 if state.leader_id == leader_id {
2945 let view_id = ViewId::from_proto(id.clone())?;
2946 if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
2947 tasks.push(item.apply_update_proto(&project, variant.clone(), cx));
2948 }
2949 }
2950 }
2951 anyhow::Ok(())
2952 })??;
2953 try_join_all(tasks).await.log_err();
2954 }
2955 proto::update_followers::Variant::CreateView(view) => {
2956 let panes = this.read_with(cx, |this, _| {
2957 this.follower_states
2958 .iter()
2959 .filter_map(|(pane, state)| (state.leader_id == leader_id).then_some(pane))
2960 .cloned()
2961 .collect()
2962 })?;
2963 Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], cx).await?;
2964 }
2965 }
2966 this.update(cx, |this, cx| this.leader_updated(leader_id, cx))?;
2967 Ok(())
2968 }
2969
2970 async fn add_views_from_leader(
2971 this: WeakViewHandle<Self>,
2972 leader_id: PeerId,
2973 panes: Vec<ViewHandle<Pane>>,
2974 views: Vec<proto::View>,
2975 cx: &mut AsyncAppContext,
2976 ) -> Result<()> {
2977 let this = this
2978 .upgrade(cx)
2979 .ok_or_else(|| anyhow!("workspace dropped"))?;
2980
2981 let item_builders = cx.update(|cx| {
2982 cx.default_global::<FollowableItemBuilders>()
2983 .values()
2984 .map(|b| b.0)
2985 .collect::<Vec<_>>()
2986 });
2987
2988 let mut item_tasks_by_pane = HashMap::default();
2989 for pane in panes {
2990 let mut item_tasks = Vec::new();
2991 let mut leader_view_ids = Vec::new();
2992 for view in &views {
2993 let Some(id) = &view.id else { continue };
2994 let id = ViewId::from_proto(id.clone())?;
2995 let mut variant = view.variant.clone();
2996 if variant.is_none() {
2997 Err(anyhow!("missing view variant"))?;
2998 }
2999 for build_item in &item_builders {
3000 let task = cx
3001 .update(|cx| build_item(pane.clone(), this.clone(), id, &mut variant, cx));
3002 if let Some(task) = task {
3003 item_tasks.push(task);
3004 leader_view_ids.push(id);
3005 break;
3006 } else {
3007 assert!(variant.is_some());
3008 }
3009 }
3010 }
3011
3012 item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids));
3013 }
3014
3015 for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
3016 let items = futures::future::try_join_all(item_tasks).await?;
3017 this.update(cx, |this, cx| {
3018 let state = this.follower_states.get_mut(&pane)?;
3019 for (id, item) in leader_view_ids.into_iter().zip(items) {
3020 item.set_leader_peer_id(Some(leader_id), cx);
3021 state.items_by_leader_view_id.insert(id, item);
3022 }
3023
3024 Some(())
3025 });
3026 }
3027 Ok(())
3028 }
3029
3030 fn update_active_view_for_followers(&mut self, cx: &AppContext) {
3031 let mut is_project_item = true;
3032 let mut update = proto::UpdateActiveView::default();
3033 if self.active_pane.read(cx).has_focus() {
3034 let item = self
3035 .active_item(cx)
3036 .and_then(|item| item.to_followable_item_handle(cx));
3037 if let Some(item) = item {
3038 is_project_item = item.is_project_item(cx);
3039 update = proto::UpdateActiveView {
3040 id: item
3041 .remote_id(&self.app_state.client, cx)
3042 .map(|id| id.to_proto()),
3043 leader_id: self.leader_for_pane(&self.active_pane),
3044 };
3045 }
3046 }
3047
3048 if update.id != self.last_active_view_id {
3049 self.last_active_view_id = update.id.clone();
3050 self.update_followers(
3051 is_project_item,
3052 proto::update_followers::Variant::UpdateActiveView(update),
3053 cx,
3054 );
3055 }
3056 }
3057
3058 fn update_followers(
3059 &self,
3060 project_only: bool,
3061 update: proto::update_followers::Variant,
3062 cx: &AppContext,
3063 ) -> Option<()> {
3064 let project_id = if project_only {
3065 self.project.read(cx).remote_id()
3066 } else {
3067 None
3068 };
3069 self.app_state().workspace_store.read_with(cx, |store, cx| {
3070 store.update_followers(project_id, update, cx)
3071 })
3072 }
3073
3074 pub fn leader_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<PeerId> {
3075 self.follower_states.get(pane).map(|state| state.leader_id)
3076 }
3077
3078 fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
3079 cx.notify();
3080
3081 let call = self.active_call()?;
3082 let room = call.read(cx).room()?.read(cx);
3083 let participant = room.remote_participant_for_peer_id(leader_id)?;
3084 let mut items_to_activate = Vec::new();
3085
3086 let leader_in_this_app;
3087 let leader_in_this_project;
3088 match participant.location {
3089 call::ParticipantLocation::SharedProject { project_id } => {
3090 leader_in_this_app = true;
3091 leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
3092 }
3093 call::ParticipantLocation::UnsharedProject => {
3094 leader_in_this_app = true;
3095 leader_in_this_project = false;
3096 }
3097 call::ParticipantLocation::External => {
3098 leader_in_this_app = false;
3099 leader_in_this_project = false;
3100 }
3101 };
3102
3103 for (pane, state) in &self.follower_states {
3104 if state.leader_id != leader_id {
3105 continue;
3106 }
3107 if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
3108 if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) {
3109 if leader_in_this_project || !item.is_project_item(cx) {
3110 items_to_activate.push((pane.clone(), item.boxed_clone()));
3111 }
3112 } else {
3113 log::warn!(
3114 "unknown view id {:?} for leader {:?}",
3115 active_view_id,
3116 leader_id
3117 );
3118 }
3119 continue;
3120 }
3121 if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
3122 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
3123 }
3124 }
3125
3126 for (pane, item) in items_to_activate {
3127 let pane_was_focused = pane.read(cx).has_focus();
3128 if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) {
3129 pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx));
3130 } else {
3131 pane.update(cx, |pane, cx| {
3132 pane.add_item(item.boxed_clone(), false, false, None, cx)
3133 });
3134 }
3135
3136 if pane_was_focused {
3137 pane.update(cx, |pane, cx| pane.focus_active_item(cx));
3138 }
3139 }
3140
3141 None
3142 }
3143
3144 fn shared_screen_for_peer(
3145 &self,
3146 peer_id: PeerId,
3147 pane: &ViewHandle<Pane>,
3148 cx: &mut ViewContext<Self>,
3149 ) -> Option<ViewHandle<SharedScreen>> {
3150 let call = self.active_call()?;
3151 let room = call.read(cx).room()?.read(cx);
3152 let participant = room.remote_participant_for_peer_id(peer_id)?;
3153 let track = participant.video_tracks.values().next()?.clone();
3154 let user = participant.user.clone();
3155
3156 for item in pane.read(cx).items_of_type::<SharedScreen>() {
3157 if item.read(cx).peer_id == peer_id {
3158 return Some(item);
3159 }
3160 }
3161
3162 Some(cx.add_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
3163 }
3164
3165 pub fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
3166 if active {
3167 self.update_active_view_for_followers(cx);
3168 cx.background()
3169 .spawn(persistence::DB.update_timestamp(self.database_id()))
3170 .detach();
3171 } else {
3172 for pane in &self.panes {
3173 pane.update(cx, |pane, cx| {
3174 if let Some(item) = pane.active_item() {
3175 item.workspace_deactivated(cx);
3176 }
3177 if matches!(
3178 settings::get::<WorkspaceSettings>(cx).autosave,
3179 AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
3180 ) {
3181 for item in pane.items() {
3182 Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
3183 .detach_and_log_err(cx);
3184 }
3185 }
3186 });
3187 }
3188 }
3189 }
3190
3191 fn active_call(&self) -> Option<&ModelHandle<ActiveCall>> {
3192 self.active_call.as_ref().map(|(call, _)| call)
3193 }
3194
3195 fn on_active_call_event(
3196 &mut self,
3197 _: ModelHandle<ActiveCall>,
3198 event: &call::room::Event,
3199 cx: &mut ViewContext<Self>,
3200 ) {
3201 match event {
3202 call::room::Event::ParticipantLocationChanged { participant_id }
3203 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
3204 self.leader_updated(*participant_id, cx);
3205 }
3206 _ => {}
3207 }
3208 }
3209
3210 pub fn database_id(&self) -> WorkspaceId {
3211 self.database_id
3212 }
3213
3214 fn location(&self, cx: &AppContext) -> Option<WorkspaceLocation> {
3215 let project = self.project().read(cx);
3216
3217 if project.is_local() {
3218 Some(
3219 project
3220 .visible_worktrees(cx)
3221 .map(|worktree| worktree.read(cx).abs_path())
3222 .collect::<Vec<_>>()
3223 .into(),
3224 )
3225 } else {
3226 None
3227 }
3228 }
3229
3230 fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
3231 match member {
3232 Member::Axis(PaneAxis { members, .. }) => {
3233 for child in members.iter() {
3234 self.remove_panes(child.clone(), cx)
3235 }
3236 }
3237 Member::Pane(pane) => {
3238 self.force_remove_pane(&pane, cx);
3239 }
3240 }
3241 }
3242
3243 fn force_remove_pane(&mut self, pane: &ViewHandle<Pane>, cx: &mut ViewContext<Workspace>) {
3244 self.panes.retain(|p| p != pane);
3245 cx.focus(self.panes.last().unwrap());
3246 if self.last_active_center_pane == Some(pane.downgrade()) {
3247 self.last_active_center_pane = None;
3248 }
3249 cx.notify();
3250 }
3251
3252 fn schedule_serialize(&mut self, cx: &mut ViewContext<Self>) {
3253 self._schedule_serialize = Some(cx.spawn(|this, cx| async move {
3254 cx.background().timer(Duration::from_millis(100)).await;
3255 this.read_with(&cx, |this, cx| this.serialize_workspace(cx))
3256 .ok();
3257 }));
3258 }
3259
3260 fn serialize_workspace(&self, cx: &ViewContext<Self>) {
3261 fn serialize_pane_handle(
3262 pane_handle: &ViewHandle<Pane>,
3263 cx: &AppContext,
3264 ) -> SerializedPane {
3265 let (items, active) = {
3266 let pane = pane_handle.read(cx);
3267 let active_item_id = pane.active_item().map(|item| item.id());
3268 (
3269 pane.items()
3270 .filter_map(|item_handle| {
3271 Some(SerializedItem {
3272 kind: Arc::from(item_handle.serialized_item_kind()?),
3273 item_id: item_handle.id(),
3274 active: Some(item_handle.id()) == active_item_id,
3275 })
3276 })
3277 .collect::<Vec<_>>(),
3278 pane.has_focus(),
3279 )
3280 };
3281
3282 SerializedPane::new(items, active)
3283 }
3284
3285 fn build_serialized_pane_group(
3286 pane_group: &Member,
3287 cx: &AppContext,
3288 ) -> SerializedPaneGroup {
3289 match pane_group {
3290 Member::Axis(PaneAxis {
3291 axis,
3292 members,
3293 flexes,
3294 bounding_boxes: _,
3295 }) => SerializedPaneGroup::Group {
3296 axis: *axis,
3297 children: members
3298 .iter()
3299 .map(|member| build_serialized_pane_group(member, cx))
3300 .collect::<Vec<_>>(),
3301 flexes: Some(flexes.borrow().clone()),
3302 },
3303 Member::Pane(pane_handle) => {
3304 SerializedPaneGroup::Pane(serialize_pane_handle(&pane_handle, cx))
3305 }
3306 }
3307 }
3308
3309 fn build_serialized_docks(this: &Workspace, cx: &ViewContext<Workspace>) -> DockStructure {
3310 let left_dock = this.left_dock.read(cx);
3311 let left_visible = left_dock.is_open();
3312 let left_active_panel = left_dock.visible_panel().and_then(|panel| {
3313 Some(
3314 cx.view_ui_name(panel.as_any().window(), panel.id())?
3315 .to_string(),
3316 )
3317 });
3318 let left_dock_zoom = left_dock
3319 .visible_panel()
3320 .map(|panel| panel.is_zoomed(cx))
3321 .unwrap_or(false);
3322
3323 let right_dock = this.right_dock.read(cx);
3324 let right_visible = right_dock.is_open();
3325 let right_active_panel = right_dock.visible_panel().and_then(|panel| {
3326 Some(
3327 cx.view_ui_name(panel.as_any().window(), panel.id())?
3328 .to_string(),
3329 )
3330 });
3331 let right_dock_zoom = right_dock
3332 .visible_panel()
3333 .map(|panel| panel.is_zoomed(cx))
3334 .unwrap_or(false);
3335
3336 let bottom_dock = this.bottom_dock.read(cx);
3337 let bottom_visible = bottom_dock.is_open();
3338 let bottom_active_panel = bottom_dock.visible_panel().and_then(|panel| {
3339 Some(
3340 cx.view_ui_name(panel.as_any().window(), panel.id())?
3341 .to_string(),
3342 )
3343 });
3344 let bottom_dock_zoom = bottom_dock
3345 .visible_panel()
3346 .map(|panel| panel.is_zoomed(cx))
3347 .unwrap_or(false);
3348
3349 DockStructure {
3350 left: DockData {
3351 visible: left_visible,
3352 active_panel: left_active_panel,
3353 zoom: left_dock_zoom,
3354 },
3355 right: DockData {
3356 visible: right_visible,
3357 active_panel: right_active_panel,
3358 zoom: right_dock_zoom,
3359 },
3360 bottom: DockData {
3361 visible: bottom_visible,
3362 active_panel: bottom_active_panel,
3363 zoom: bottom_dock_zoom,
3364 },
3365 }
3366 }
3367
3368 if let Some(location) = self.location(cx) {
3369 // Load bearing special case:
3370 // - with_local_workspace() relies on this to not have other stuff open
3371 // when you open your log
3372 if !location.paths().is_empty() {
3373 let center_group = build_serialized_pane_group(&self.center.root, cx);
3374 let docks = build_serialized_docks(self, cx);
3375
3376 let serialized_workspace = SerializedWorkspace {
3377 id: self.database_id,
3378 location,
3379 center_group,
3380 bounds: Default::default(),
3381 display: Default::default(),
3382 docks,
3383 };
3384
3385 cx.background()
3386 .spawn(persistence::DB.save_workspace(serialized_workspace))
3387 .detach();
3388 }
3389 }
3390 }
3391
3392 pub(crate) fn load_workspace(
3393 workspace: WeakViewHandle<Workspace>,
3394 serialized_workspace: SerializedWorkspace,
3395 paths_to_open: Vec<Option<ProjectPath>>,
3396 cx: &mut AppContext,
3397 ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
3398 cx.spawn(|mut cx| async move {
3399 let (project, old_center_pane) = workspace.read_with(&cx, |workspace, _| {
3400 (
3401 workspace.project().clone(),
3402 workspace.last_active_center_pane.clone(),
3403 )
3404 })?;
3405
3406 let mut center_group = None;
3407 let mut center_items = None;
3408 // Traverse the splits tree and add to things
3409 if let Some((group, active_pane, items)) = serialized_workspace
3410 .center_group
3411 .deserialize(&project, serialized_workspace.id, &workspace, &mut cx)
3412 .await
3413 {
3414 center_items = Some(items);
3415 center_group = Some((group, active_pane))
3416 }
3417
3418 let mut items_by_project_path = cx.read(|cx| {
3419 center_items
3420 .unwrap_or_default()
3421 .into_iter()
3422 .filter_map(|item| {
3423 let item = item?;
3424 let project_path = item.project_path(cx)?;
3425 Some((project_path, item))
3426 })
3427 .collect::<HashMap<_, _>>()
3428 });
3429
3430 let opened_items = paths_to_open
3431 .into_iter()
3432 .map(|path_to_open| {
3433 path_to_open
3434 .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
3435 })
3436 .collect::<Vec<_>>();
3437
3438 // Remove old panes from workspace panes list
3439 workspace.update(&mut cx, |workspace, cx| {
3440 if let Some((center_group, active_pane)) = center_group {
3441 workspace.remove_panes(workspace.center.root.clone(), cx);
3442
3443 // Swap workspace center group
3444 workspace.center = PaneGroup::with_root(center_group);
3445
3446 // Change the focus to the workspace first so that we retrigger focus in on the pane.
3447 cx.focus_self();
3448
3449 if let Some(active_pane) = active_pane {
3450 cx.focus(&active_pane);
3451 } else {
3452 cx.focus(workspace.panes.last().unwrap());
3453 }
3454 } else {
3455 let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade(cx));
3456 if let Some(old_center_handle) = old_center_handle {
3457 cx.focus(&old_center_handle)
3458 } else {
3459 cx.focus_self()
3460 }
3461 }
3462
3463 let docks = serialized_workspace.docks;
3464 workspace.left_dock.update(cx, |dock, cx| {
3465 dock.set_open(docks.left.visible, cx);
3466 if let Some(active_panel) = docks.left.active_panel {
3467 if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
3468 dock.activate_panel(ix, cx);
3469 }
3470 }
3471 dock.active_panel()
3472 .map(|panel| panel.set_zoomed(docks.left.zoom, cx));
3473 if docks.left.visible && docks.left.zoom {
3474 cx.focus_self()
3475 }
3476 });
3477 // TODO: I think the bug is that setting zoom or active undoes the bottom zoom or something
3478 workspace.right_dock.update(cx, |dock, cx| {
3479 dock.set_open(docks.right.visible, cx);
3480 if let Some(active_panel) = docks.right.active_panel {
3481 if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
3482 dock.activate_panel(ix, cx);
3483 }
3484 }
3485 dock.active_panel()
3486 .map(|panel| panel.set_zoomed(docks.right.zoom, cx));
3487
3488 if docks.right.visible && docks.right.zoom {
3489 cx.focus_self()
3490 }
3491 });
3492 workspace.bottom_dock.update(cx, |dock, cx| {
3493 dock.set_open(docks.bottom.visible, cx);
3494 if let Some(active_panel) = docks.bottom.active_panel {
3495 if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
3496 dock.activate_panel(ix, cx);
3497 }
3498 }
3499
3500 dock.active_panel()
3501 .map(|panel| panel.set_zoomed(docks.bottom.zoom, cx));
3502
3503 if docks.bottom.visible && docks.bottom.zoom {
3504 cx.focus_self()
3505 }
3506 });
3507
3508 cx.notify();
3509 })?;
3510
3511 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
3512 workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))?;
3513
3514 Ok(opened_items)
3515 })
3516 }
3517
3518 #[cfg(any(test, feature = "test-support"))]
3519 pub fn test_new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
3520 let client = project.read(cx).client();
3521 let user_store = project.read(cx).user_store();
3522
3523 let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx));
3524 let app_state = Arc::new(AppState {
3525 languages: project.read(cx).languages().clone(),
3526 workspace_store,
3527 client,
3528 user_store,
3529 fs: project.read(cx).fs().clone(),
3530 build_window_options: |_, _, _| Default::default(),
3531 initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
3532 background_actions: || &[],
3533 });
3534 Self::new(0, project, app_state, cx)
3535 }
3536
3537 fn render_dock(&self, position: DockPosition, cx: &WindowContext) -> Option<AnyElement<Self>> {
3538 let dock = match position {
3539 DockPosition::Left => &self.left_dock,
3540 DockPosition::Right => &self.right_dock,
3541 DockPosition::Bottom => &self.bottom_dock,
3542 };
3543 let active_panel = dock.read(cx).visible_panel()?;
3544 let element = if Some(active_panel.id()) == self.zoomed.as_ref().map(|zoomed| zoomed.id()) {
3545 dock.read(cx).render_placeholder(cx)
3546 } else {
3547 ChildView::new(dock, cx).into_any()
3548 };
3549
3550 Some(
3551 element
3552 .constrained()
3553 .dynamically(move |constraint, _, cx| match position {
3554 DockPosition::Left | DockPosition::Right => SizeConstraint::new(
3555 Vector2F::new(20., constraint.min.y()),
3556 Vector2F::new(cx.window_size().x() * 0.8, constraint.max.y()),
3557 ),
3558 DockPosition::Bottom => SizeConstraint::new(
3559 Vector2F::new(constraint.min.x(), 20.),
3560 Vector2F::new(constraint.max.x(), cx.window_size().y() * 0.8),
3561 ),
3562 })
3563 .into_any(),
3564 )
3565 }
3566}
3567
3568fn window_bounds_env_override(cx: &AsyncAppContext) -> Option<WindowBounds> {
3569 ZED_WINDOW_POSITION
3570 .zip(*ZED_WINDOW_SIZE)
3571 .map(|(position, size)| {
3572 WindowBounds::Fixed(RectF::new(
3573 cx.platform().screens()[0].bounds().origin() + position,
3574 size,
3575 ))
3576 })
3577}
3578
3579async fn open_items(
3580 serialized_workspace: Option<SerializedWorkspace>,
3581 workspace: &WeakViewHandle<Workspace>,
3582 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
3583 app_state: Arc<AppState>,
3584 mut cx: AsyncAppContext,
3585) -> Result<Vec<Option<Result<Box<dyn ItemHandle>>>>> {
3586 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
3587
3588 if let Some(serialized_workspace) = serialized_workspace {
3589 let workspace = workspace.clone();
3590 let restored_items = cx
3591 .update(|cx| {
3592 Workspace::load_workspace(
3593 workspace,
3594 serialized_workspace,
3595 project_paths_to_open
3596 .iter()
3597 .map(|(_, project_path)| project_path)
3598 .cloned()
3599 .collect(),
3600 cx,
3601 )
3602 })
3603 .await?;
3604
3605 let restored_project_paths = cx.read(|cx| {
3606 restored_items
3607 .iter()
3608 .filter_map(|item| item.as_ref()?.project_path(cx))
3609 .collect::<HashSet<_>>()
3610 });
3611
3612 for restored_item in restored_items {
3613 opened_items.push(restored_item.map(Ok));
3614 }
3615
3616 project_paths_to_open
3617 .iter_mut()
3618 .for_each(|(_, project_path)| {
3619 if let Some(project_path_to_open) = project_path {
3620 if restored_project_paths.contains(project_path_to_open) {
3621 *project_path = None;
3622 }
3623 }
3624 });
3625 } else {
3626 for _ in 0..project_paths_to_open.len() {
3627 opened_items.push(None);
3628 }
3629 }
3630 assert!(opened_items.len() == project_paths_to_open.len());
3631
3632 let tasks =
3633 project_paths_to_open
3634 .into_iter()
3635 .enumerate()
3636 .map(|(i, (abs_path, project_path))| {
3637 let workspace = workspace.clone();
3638 cx.spawn(|mut cx| {
3639 let fs = app_state.fs.clone();
3640 async move {
3641 let file_project_path = project_path?;
3642 if fs.is_file(&abs_path).await {
3643 Some((
3644 i,
3645 workspace
3646 .update(&mut cx, |workspace, cx| {
3647 workspace.open_path(file_project_path, None, true, cx)
3648 })
3649 .log_err()?
3650 .await,
3651 ))
3652 } else {
3653 None
3654 }
3655 }
3656 })
3657 });
3658
3659 for maybe_opened_path in futures::future::join_all(tasks.into_iter())
3660 .await
3661 .into_iter()
3662 {
3663 if let Some((i, path_open_result)) = maybe_opened_path {
3664 opened_items[i] = Some(path_open_result);
3665 }
3666 }
3667
3668 Ok(opened_items)
3669}
3670
3671fn notify_of_new_dock(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
3672 const NEW_PANEL_BLOG_POST: &str = "https://zed.dev/blog/new-panel-system";
3673 const NEW_DOCK_HINT_KEY: &str = "show_new_dock_key";
3674 const MESSAGE_ID: usize = 2;
3675
3676 if workspace
3677 .read_with(cx, |workspace, cx| {
3678 workspace.has_shown_notification_once::<MessageNotification>(MESSAGE_ID, cx)
3679 })
3680 .unwrap_or(false)
3681 {
3682 return;
3683 }
3684
3685 if db::kvp::KEY_VALUE_STORE
3686 .read_kvp(NEW_DOCK_HINT_KEY)
3687 .ok()
3688 .flatten()
3689 .is_some()
3690 {
3691 if !workspace
3692 .read_with(cx, |workspace, cx| {
3693 workspace.has_shown_notification_once::<MessageNotification>(MESSAGE_ID, cx)
3694 })
3695 .unwrap_or(false)
3696 {
3697 cx.update(|cx| {
3698 cx.update_global::<NotificationTracker, _, _>(|tracker, _| {
3699 let entry = tracker
3700 .entry(TypeId::of::<MessageNotification>())
3701 .or_default();
3702 if !entry.contains(&MESSAGE_ID) {
3703 entry.push(MESSAGE_ID);
3704 }
3705 });
3706 });
3707 }
3708
3709 return;
3710 }
3711
3712 cx.spawn(|_| async move {
3713 db::kvp::KEY_VALUE_STORE
3714 .write_kvp(NEW_DOCK_HINT_KEY.to_string(), "seen".to_string())
3715 .await
3716 .ok();
3717 })
3718 .detach();
3719
3720 workspace
3721 .update(cx, |workspace, cx| {
3722 workspace.show_notification_once(2, cx, |cx| {
3723 cx.add_view(|_| {
3724 MessageNotification::new_element(|text, _| {
3725 Text::new(
3726 "Looking for the dock? Try ctrl-`!\nshift-escape now zooms your pane.",
3727 text,
3728 )
3729 .with_custom_runs(vec![26..32, 34..46], |_, bounds, cx| {
3730 let code_span_background_color = settings::get::<ThemeSettings>(cx)
3731 .theme
3732 .editor
3733 .document_highlight_read_background;
3734
3735 cx.scene().push_quad(gpui::Quad {
3736 bounds,
3737 background: Some(code_span_background_color),
3738 border: Default::default(),
3739 corner_radii: (2.0).into(),
3740 })
3741 })
3742 .into_any()
3743 })
3744 .with_click_message("Read more about the new panel system")
3745 .on_click(|cx| cx.platform().open_url(NEW_PANEL_BLOG_POST))
3746 })
3747 })
3748 })
3749 .ok();
3750}
3751
3752fn notify_if_database_failed(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
3753 const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
3754
3755 workspace
3756 .update(cx, |workspace, cx| {
3757 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
3758 workspace.show_notification_once(0, cx, |cx| {
3759 cx.add_view(|_| {
3760 MessageNotification::new("Failed to load the database file.")
3761 .with_click_message("Click to let us know about this error")
3762 .on_click(|cx| cx.platform().open_url(REPORT_ISSUE_URL))
3763 })
3764 });
3765 }
3766 })
3767 .log_err();
3768}
3769
3770impl Entity for Workspace {
3771 type Event = Event;
3772
3773 fn release(&mut self, cx: &mut AppContext) {
3774 self.app_state.workspace_store.update(cx, |store, _| {
3775 store.workspaces.remove(&self.weak_self);
3776 })
3777 }
3778}
3779
3780impl View for Workspace {
3781 fn ui_name() -> &'static str {
3782 "Workspace"
3783 }
3784
3785 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
3786 let theme = theme::current(cx).clone();
3787 Stack::new()
3788 .with_child(
3789 Flex::column()
3790 .with_child(self.render_titlebar(&theme, cx))
3791 .with_child(
3792 Stack::new()
3793 .with_child({
3794 let project = self.project.clone();
3795 Flex::row()
3796 .with_children(self.render_dock(DockPosition::Left, cx))
3797 .with_child(
3798 Flex::column()
3799 .with_child(
3800 FlexItem::new(
3801 self.center.render(
3802 &project,
3803 &theme,
3804 &self.follower_states,
3805 self.active_call(),
3806 self.active_pane(),
3807 self.zoomed
3808 .as_ref()
3809 .and_then(|zoomed| zoomed.upgrade(cx))
3810 .as_ref(),
3811 &self.app_state,
3812 cx,
3813 ),
3814 )
3815 .flex(1., true),
3816 )
3817 .with_children(
3818 self.render_dock(DockPosition::Bottom, cx),
3819 )
3820 .flex(1., true),
3821 )
3822 .with_children(self.render_dock(DockPosition::Right, cx))
3823 })
3824 .with_child(Overlay::new(
3825 Stack::new()
3826 .with_children(self.zoomed.as_ref().and_then(|zoomed| {
3827 enum ZoomBackground {}
3828 let zoomed = zoomed.upgrade(cx)?;
3829
3830 let mut foreground_style =
3831 theme.workspace.zoomed_pane_foreground;
3832 if let Some(zoomed_dock_position) = self.zoomed_position {
3833 foreground_style =
3834 theme.workspace.zoomed_panel_foreground;
3835 let margin = foreground_style.margin.top;
3836 let border = foreground_style.border.top;
3837
3838 // Only include a margin and border on the opposite side.
3839 foreground_style.margin.top = 0.;
3840 foreground_style.margin.left = 0.;
3841 foreground_style.margin.bottom = 0.;
3842 foreground_style.margin.right = 0.;
3843 foreground_style.border.top = false;
3844 foreground_style.border.left = false;
3845 foreground_style.border.bottom = false;
3846 foreground_style.border.right = false;
3847 match zoomed_dock_position {
3848 DockPosition::Left => {
3849 foreground_style.margin.right = margin;
3850 foreground_style.border.right = border;
3851 }
3852 DockPosition::Right => {
3853 foreground_style.margin.left = margin;
3854 foreground_style.border.left = border;
3855 }
3856 DockPosition::Bottom => {
3857 foreground_style.margin.top = margin;
3858 foreground_style.border.top = border;
3859 }
3860 }
3861 }
3862
3863 Some(
3864 ChildView::new(&zoomed, cx)
3865 .contained()
3866 .with_style(foreground_style)
3867 .aligned()
3868 .contained()
3869 .with_style(theme.workspace.zoomed_background)
3870 .mouse::<ZoomBackground>(0)
3871 .capture_all()
3872 .on_down(
3873 MouseButton::Left,
3874 |_, this: &mut Self, cx| {
3875 this.zoom_out(cx);
3876 },
3877 ),
3878 )
3879 }))
3880 .with_children(self.modal.as_ref().map(|modal| {
3881 // Prevent clicks within the modal from falling
3882 // through to the rest of the workspace.
3883 enum ModalBackground {}
3884 MouseEventHandler::new::<ModalBackground, _>(
3885 0,
3886 cx,
3887 |_, cx| ChildView::new(modal.view.as_any(), cx),
3888 )
3889 .on_click(MouseButton::Left, |_, _, _| {})
3890 .contained()
3891 .with_style(theme.workspace.modal)
3892 .aligned()
3893 .top()
3894 }))
3895 .with_children(self.render_notifications(&theme.workspace, cx)),
3896 ))
3897 .provide_resize_bounds::<WorkspaceBounds>()
3898 .flex(1.0, true),
3899 )
3900 .with_child(ChildView::new(&self.status_bar, cx))
3901 .contained()
3902 .with_background_color(theme.workspace.background),
3903 )
3904 .with_children(DragAndDrop::render(cx))
3905 .with_children(self.render_disconnected_overlay(cx))
3906 .into_any_named("workspace")
3907 }
3908
3909 fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
3910 if cx.is_self_focused() {
3911 cx.focus(&self.active_pane);
3912 }
3913 }
3914
3915 fn modifiers_changed(&mut self, e: &ModifiersChangedEvent, cx: &mut ViewContext<Self>) -> bool {
3916 DragAndDrop::<Workspace>::update_modifiers(e.modifiers, cx)
3917 }
3918}
3919
3920impl WorkspaceStore {
3921 pub fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
3922 Self {
3923 workspaces: Default::default(),
3924 followers: Default::default(),
3925 _subscriptions: vec![
3926 client.add_request_handler(cx.handle(), Self::handle_follow),
3927 client.add_message_handler(cx.handle(), Self::handle_unfollow),
3928 client.add_message_handler(cx.handle(), Self::handle_update_followers),
3929 ],
3930 client,
3931 }
3932 }
3933
3934 pub fn update_followers(
3935 &self,
3936 project_id: Option<u64>,
3937 update: proto::update_followers::Variant,
3938 cx: &AppContext,
3939 ) -> Option<()> {
3940 if !cx.has_global::<ModelHandle<ActiveCall>>() {
3941 return None;
3942 }
3943
3944 let room_id = ActiveCall::global(cx).read(cx).room()?.read(cx).id();
3945 let follower_ids: Vec<_> = self
3946 .followers
3947 .iter()
3948 .filter_map(|follower| {
3949 if follower.project_id == project_id || project_id.is_none() {
3950 Some(follower.peer_id.into())
3951 } else {
3952 None
3953 }
3954 })
3955 .collect();
3956 if follower_ids.is_empty() {
3957 return None;
3958 }
3959 self.client
3960 .send(proto::UpdateFollowers {
3961 room_id,
3962 project_id,
3963 follower_ids,
3964 variant: Some(update),
3965 })
3966 .log_err()
3967 }
3968
3969 async fn handle_follow(
3970 this: ModelHandle<Self>,
3971 envelope: TypedEnvelope<proto::Follow>,
3972 _: Arc<Client>,
3973 mut cx: AsyncAppContext,
3974 ) -> Result<proto::FollowResponse> {
3975 this.update(&mut cx, |this, cx| {
3976 let follower = Follower {
3977 project_id: envelope.payload.project_id,
3978 peer_id: envelope.original_sender_id()?,
3979 };
3980 let active_project = ActiveCall::global(cx)
3981 .read(cx)
3982 .location()
3983 .map(|project| project.id());
3984
3985 let mut response = proto::FollowResponse::default();
3986 for workspace in &this.workspaces {
3987 let Some(workspace) = workspace.upgrade(cx) else {
3988 continue;
3989 };
3990
3991 workspace.update(cx.as_mut(), |workspace, cx| {
3992 let handler_response = workspace.handle_follow(follower.project_id, cx);
3993 if response.views.is_empty() {
3994 response.views = handler_response.views;
3995 } else {
3996 response.views.extend_from_slice(&handler_response.views);
3997 }
3998
3999 if let Some(active_view_id) = handler_response.active_view_id.clone() {
4000 if response.active_view_id.is_none()
4001 || Some(workspace.project.id()) == active_project
4002 {
4003 response.active_view_id = Some(active_view_id);
4004 }
4005 }
4006 });
4007 }
4008
4009 if let Err(ix) = this.followers.binary_search(&follower) {
4010 this.followers.insert(ix, follower);
4011 }
4012
4013 Ok(response)
4014 })
4015 }
4016
4017 async fn handle_unfollow(
4018 this: ModelHandle<Self>,
4019 envelope: TypedEnvelope<proto::Unfollow>,
4020 _: Arc<Client>,
4021 mut cx: AsyncAppContext,
4022 ) -> Result<()> {
4023 this.update(&mut cx, |this, _| {
4024 let follower = Follower {
4025 project_id: envelope.payload.project_id,
4026 peer_id: envelope.original_sender_id()?,
4027 };
4028 if let Ok(ix) = this.followers.binary_search(&follower) {
4029 this.followers.remove(ix);
4030 }
4031 Ok(())
4032 })
4033 }
4034
4035 async fn handle_update_followers(
4036 this: ModelHandle<Self>,
4037 envelope: TypedEnvelope<proto::UpdateFollowers>,
4038 _: Arc<Client>,
4039 mut cx: AsyncAppContext,
4040 ) -> Result<()> {
4041 let leader_id = envelope.original_sender_id()?;
4042 let update = envelope.payload;
4043 this.update(&mut cx, |this, cx| {
4044 for workspace in &this.workspaces {
4045 let Some(workspace) = workspace.upgrade(cx) else {
4046 continue;
4047 };
4048 workspace.update(cx.as_mut(), |workspace, cx| {
4049 let project_id = workspace.project.read(cx).remote_id();
4050 if update.project_id != project_id && update.project_id.is_some() {
4051 return;
4052 }
4053 workspace.handle_update_followers(leader_id, update.clone(), cx);
4054 });
4055 }
4056 Ok(())
4057 })
4058 }
4059}
4060
4061impl Entity for WorkspaceStore {
4062 type Event = ();
4063}
4064
4065impl ViewId {
4066 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
4067 Ok(Self {
4068 creator: message
4069 .creator
4070 .ok_or_else(|| anyhow!("creator is missing"))?,
4071 id: message.id,
4072 })
4073 }
4074
4075 pub(crate) fn to_proto(&self) -> proto::ViewId {
4076 proto::ViewId {
4077 creator: Some(self.creator),
4078 id: self.id,
4079 }
4080 }
4081}
4082
4083pub trait WorkspaceHandle {
4084 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
4085}
4086
4087impl WorkspaceHandle for ViewHandle<Workspace> {
4088 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
4089 self.read(cx)
4090 .worktrees(cx)
4091 .flat_map(|worktree| {
4092 let worktree_id = worktree.read(cx).id();
4093 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
4094 worktree_id,
4095 path: f.path.clone(),
4096 })
4097 })
4098 .collect::<Vec<_>>()
4099 }
4100}
4101
4102impl std::fmt::Debug for OpenPaths {
4103 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4104 f.debug_struct("OpenPaths")
4105 .field("paths", &self.paths)
4106 .finish()
4107 }
4108}
4109
4110pub struct WorkspaceCreated(pub WeakViewHandle<Workspace>);
4111
4112pub fn activate_workspace_for_project(
4113 cx: &mut AsyncAppContext,
4114 predicate: impl Fn(&mut Project, &mut ModelContext<Project>) -> bool,
4115) -> Option<WeakViewHandle<Workspace>> {
4116 for window in cx.windows() {
4117 let handle = window
4118 .update(cx, |cx| {
4119 if let Some(workspace_handle) = cx.root_view().clone().downcast::<Workspace>() {
4120 let project = workspace_handle.read(cx).project.clone();
4121 if project.update(cx, &predicate) {
4122 cx.activate_window();
4123 return Some(workspace_handle.clone());
4124 }
4125 }
4126 None
4127 })
4128 .flatten();
4129
4130 if let Some(handle) = handle {
4131 return Some(handle.downgrade());
4132 }
4133 }
4134 None
4135}
4136
4137pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
4138 DB.last_workspace().await.log_err().flatten()
4139}
4140
4141async fn join_channel_internal(
4142 channel_id: u64,
4143 app_state: &Arc<AppState>,
4144 requesting_window: Option<WindowHandle<Workspace>>,
4145 active_call: &ModelHandle<ActiveCall>,
4146 cx: &mut AsyncAppContext,
4147) -> Result<bool> {
4148 let (should_prompt, open_room) = active_call.read_with(cx, |active_call, cx| {
4149 let Some(room) = active_call.room().map(|room| room.read(cx)) else {
4150 return (false, None);
4151 };
4152
4153 let already_in_channel = room.channel_id() == Some(channel_id);
4154 let should_prompt = room.is_sharing_project()
4155 && room.remote_participants().len() > 0
4156 && !already_in_channel;
4157 let open_room = if already_in_channel {
4158 active_call.room().cloned()
4159 } else {
4160 None
4161 };
4162 (should_prompt, open_room)
4163 });
4164
4165 if let Some(room) = open_room {
4166 let task = room.update(cx, |room, cx| {
4167 if let Some((project, host)) = room.most_active_project(cx) {
4168 return Some(join_remote_project(project, host, app_state.clone(), cx));
4169 }
4170
4171 None
4172 });
4173 if let Some(task) = task {
4174 task.await?;
4175 }
4176 return anyhow::Ok(true);
4177 }
4178
4179 if should_prompt {
4180 if let Some(workspace) = requesting_window {
4181 if let Some(window) = workspace.update(cx, |cx| cx.window()) {
4182 let answer = window.prompt(
4183 PromptLevel::Warning,
4184 "Leaving this call will unshare your current project.\nDo you want to switch channels?",
4185 &["Yes, Join Channel", "Cancel"],
4186 cx,
4187 );
4188
4189 if let Some(mut answer) = answer {
4190 if answer.next().await == Some(1) {
4191 return Ok(false);
4192 }
4193 }
4194 } else {
4195 return Ok(false); // unreachable!() hopefully
4196 }
4197 } else {
4198 return Ok(false); // unreachable!() hopefully
4199 }
4200 }
4201
4202 let client = cx.read(|cx| active_call.read(cx).client());
4203
4204 let mut client_status = client.status();
4205
4206 // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
4207 'outer: loop {
4208 let Some(status) = client_status.recv().await else {
4209 return Err(anyhow!("error connecting"));
4210 };
4211
4212 match status {
4213 Status::Connecting
4214 | Status::Authenticating
4215 | Status::Reconnecting
4216 | Status::Reauthenticating => continue,
4217 Status::Connected { .. } => break 'outer,
4218 Status::SignedOut => return Err(anyhow!("not signed in")),
4219 Status::UpgradeRequired => return Err(anyhow!("zed is out of date")),
4220 Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
4221 return Err(anyhow!("zed is offline"))
4222 }
4223 }
4224 }
4225
4226 let room = active_call
4227 .update(cx, |active_call, cx| {
4228 active_call.join_channel(channel_id, cx)
4229 })
4230 .await?;
4231
4232 room.update(cx, |room, _| room.room_update_completed())
4233 .await;
4234
4235 let task = room.update(cx, |room, cx| {
4236 if let Some((project, host)) = room.most_active_project(cx) {
4237 return Some(join_remote_project(project, host, app_state.clone(), cx));
4238 }
4239
4240 None
4241 });
4242 if let Some(task) = task {
4243 task.await?;
4244 return anyhow::Ok(true);
4245 }
4246 anyhow::Ok(false)
4247}
4248
4249pub fn join_channel(
4250 channel_id: u64,
4251 app_state: Arc<AppState>,
4252 requesting_window: Option<WindowHandle<Workspace>>,
4253 cx: &mut AppContext,
4254) -> Task<Result<()>> {
4255 let active_call = ActiveCall::global(cx);
4256 cx.spawn(|mut cx| async move {
4257 let result = join_channel_internal(
4258 channel_id,
4259 &app_state,
4260 requesting_window,
4261 &active_call,
4262 &mut cx,
4263 )
4264 .await;
4265
4266 // join channel succeeded, and opened a window
4267 if matches!(result, Ok(true)) {
4268 return anyhow::Ok(());
4269 }
4270
4271 if requesting_window.is_some() {
4272 return anyhow::Ok(());
4273 }
4274
4275 // find an existing workspace to focus and show call controls
4276 let mut active_window = activate_any_workspace_window(&mut cx);
4277 if active_window.is_none() {
4278 // no open workspaces, make one to show the error in (blergh)
4279 cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), requesting_window, cx))
4280 .await;
4281 }
4282
4283 active_window = activate_any_workspace_window(&mut cx);
4284 if active_window.is_none() {
4285 return result.map(|_| ()); // unreachable!() assuming new_local always opens a window
4286 }
4287
4288 if let Err(err) = result {
4289 let prompt = active_window.unwrap().prompt(
4290 PromptLevel::Critical,
4291 &format!("Failed to join channel: {}", err),
4292 &["Ok"],
4293 &mut cx,
4294 );
4295 if let Some(mut prompt) = prompt {
4296 prompt.next().await;
4297 } else {
4298 return Err(err);
4299 }
4300 }
4301
4302 // return ok, we showed the error to the user.
4303 return anyhow::Ok(());
4304 })
4305}
4306
4307pub fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<AnyWindowHandle> {
4308 for window in cx.windows() {
4309 let found = window.update(cx, |cx| {
4310 let is_workspace = cx.root_view().clone().downcast::<Workspace>().is_some();
4311 if is_workspace {
4312 cx.activate_window();
4313 }
4314 is_workspace
4315 });
4316 if found == Some(true) {
4317 return Some(window);
4318 }
4319 }
4320 None
4321}
4322
4323#[allow(clippy::type_complexity)]
4324pub fn open_paths(
4325 abs_paths: &[PathBuf],
4326 app_state: &Arc<AppState>,
4327 requesting_window: Option<WindowHandle<Workspace>>,
4328 cx: &mut AppContext,
4329) -> Task<
4330 Result<(
4331 WeakViewHandle<Workspace>,
4332 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
4333 )>,
4334> {
4335 let app_state = app_state.clone();
4336 let abs_paths = abs_paths.to_vec();
4337 cx.spawn(|mut cx| async move {
4338 // Open paths in existing workspace if possible
4339 let existing = activate_workspace_for_project(&mut cx, |project, cx| {
4340 project.contains_paths(&abs_paths, cx)
4341 });
4342
4343 if let Some(existing) = existing {
4344 Ok((
4345 existing.clone(),
4346 existing
4347 .update(&mut cx, |workspace, cx| {
4348 workspace.open_paths(abs_paths, true, cx)
4349 })?
4350 .await,
4351 ))
4352 } else {
4353 Ok(cx
4354 .update(|cx| {
4355 Workspace::new_local(abs_paths, app_state.clone(), requesting_window, cx)
4356 })
4357 .await)
4358 }
4359 })
4360}
4361
4362pub fn open_new(
4363 app_state: &Arc<AppState>,
4364 cx: &mut AppContext,
4365 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static,
4366) -> Task<()> {
4367 let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx);
4368 cx.spawn(|mut cx| async move {
4369 let (workspace, opened_paths) = task.await;
4370
4371 workspace
4372 .update(&mut cx, |workspace, cx| {
4373 if opened_paths.is_empty() {
4374 init(workspace, cx)
4375 }
4376 })
4377 .log_err();
4378 })
4379}
4380
4381pub fn create_and_open_local_file(
4382 path: &'static Path,
4383 cx: &mut ViewContext<Workspace>,
4384 default_content: impl 'static + Send + FnOnce() -> Rope,
4385) -> Task<Result<Box<dyn ItemHandle>>> {
4386 cx.spawn(|workspace, mut cx| async move {
4387 let fs = workspace.read_with(&cx, |workspace, _| workspace.app_state().fs.clone())?;
4388 if !fs.is_file(path).await {
4389 fs.create_file(path, Default::default()).await?;
4390 fs.save(path, &default_content(), Default::default())
4391 .await?;
4392 }
4393
4394 let mut items = workspace
4395 .update(&mut cx, |workspace, cx| {
4396 workspace.with_local_workspace(cx, |workspace, cx| {
4397 workspace.open_paths(vec![path.to_path_buf()], false, cx)
4398 })
4399 })?
4400 .await?
4401 .await;
4402
4403 let item = items.pop().flatten();
4404 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
4405 })
4406}
4407
4408pub fn join_remote_project(
4409 project_id: u64,
4410 follow_user_id: u64,
4411 app_state: Arc<AppState>,
4412 cx: &mut AppContext,
4413) -> Task<Result<()>> {
4414 cx.spawn(|mut cx| async move {
4415 let windows = cx.windows();
4416 let existing_workspace = windows.into_iter().find_map(|window| {
4417 window.downcast::<Workspace>().and_then(|window| {
4418 window
4419 .read_root_with(&cx, |workspace, cx| {
4420 if workspace.project().read(cx).remote_id() == Some(project_id) {
4421 Some(cx.handle().downgrade())
4422 } else {
4423 None
4424 }
4425 })
4426 .unwrap_or(None)
4427 })
4428 });
4429
4430 let workspace = if let Some(existing_workspace) = existing_workspace {
4431 existing_workspace
4432 } else {
4433 let active_call = cx.read(ActiveCall::global);
4434 let room = active_call
4435 .read_with(&cx, |call, _| call.room().cloned())
4436 .ok_or_else(|| anyhow!("not in a call"))?;
4437 let project = room
4438 .update(&mut cx, |room, cx| {
4439 room.join_project(
4440 project_id,
4441 app_state.languages.clone(),
4442 app_state.fs.clone(),
4443 cx,
4444 )
4445 })
4446 .await?;
4447
4448 let window_bounds_override = window_bounds_env_override(&cx);
4449 let window = cx.add_window(
4450 (app_state.build_window_options)(
4451 window_bounds_override,
4452 None,
4453 cx.platform().as_ref(),
4454 ),
4455 |cx| Workspace::new(0, project, app_state.clone(), cx),
4456 );
4457 let workspace = window.root(&cx).unwrap();
4458 (app_state.initialize_workspace)(
4459 workspace.downgrade(),
4460 false,
4461 app_state.clone(),
4462 cx.clone(),
4463 )
4464 .await
4465 .log_err();
4466
4467 workspace.downgrade()
4468 };
4469
4470 workspace.window().activate(&mut cx);
4471 cx.platform().activate(true);
4472
4473 workspace.update(&mut cx, |workspace, cx| {
4474 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
4475 let follow_peer_id = room
4476 .read(cx)
4477 .remote_participants()
4478 .iter()
4479 .find(|(_, participant)| participant.user.id == follow_user_id)
4480 .map(|(_, p)| p.peer_id)
4481 .or_else(|| {
4482 // If we couldn't follow the given user, follow the host instead.
4483 let collaborator = workspace
4484 .project()
4485 .read(cx)
4486 .collaborators()
4487 .values()
4488 .find(|collaborator| collaborator.replica_id == 0)?;
4489 Some(collaborator.peer_id)
4490 });
4491
4492 if let Some(follow_peer_id) = follow_peer_id {
4493 workspace
4494 .follow(follow_peer_id, cx)
4495 .map(|follow| follow.detach_and_log_err(cx));
4496 }
4497 }
4498 })?;
4499
4500 anyhow::Ok(())
4501 })
4502}
4503
4504pub fn restart(_: &Restart, cx: &mut AppContext) {
4505 let should_confirm = settings::get::<WorkspaceSettings>(cx).confirm_quit;
4506 cx.spawn(|mut cx| async move {
4507 let mut workspace_windows = cx
4508 .windows()
4509 .into_iter()
4510 .filter_map(|window| window.downcast::<Workspace>())
4511 .collect::<Vec<_>>();
4512
4513 // If multiple windows have unsaved changes, and need a save prompt,
4514 // prompt in the active window before switching to a different window.
4515 workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false));
4516
4517 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
4518 let answer = window.prompt(
4519 PromptLevel::Info,
4520 "Are you sure you want to restart?",
4521 &["Restart", "Cancel"],
4522 &mut cx,
4523 );
4524
4525 if let Some(mut answer) = answer {
4526 let answer = answer.next().await;
4527 if answer != Some(0) {
4528 return Ok(());
4529 }
4530 }
4531 }
4532
4533 // If the user cancels any save prompt, then keep the app open.
4534 for window in workspace_windows {
4535 if let Some(should_close) = window.update_root(&mut cx, |workspace, cx| {
4536 workspace.prepare_to_close(true, cx)
4537 }) {
4538 if !should_close.await? {
4539 return Ok(());
4540 }
4541 }
4542 }
4543 cx.platform().restart();
4544 anyhow::Ok(())
4545 })
4546 .detach_and_log_err(cx);
4547}
4548
4549fn parse_pixel_position_env_var(value: &str) -> Option<Vector2F> {
4550 let mut parts = value.split(',');
4551 let width: usize = parts.next()?.parse().ok()?;
4552 let height: usize = parts.next()?.parse().ok()?;
4553 Some(vec2f(width as f32, height as f32))
4554}
4555
4556#[cfg(test)]
4557mod tests {
4558 use super::*;
4559 use crate::{
4560 dock::test::{TestPanel, TestPanelEvent},
4561 item::test::{TestItem, TestItemEvent, TestProjectItem},
4562 };
4563 use fs::FakeFs;
4564 use gpui::{executor::Deterministic, test::EmptyView, TestAppContext};
4565 use project::{Project, ProjectEntryId};
4566 use serde_json::json;
4567 use settings::SettingsStore;
4568 use std::{cell::RefCell, rc::Rc};
4569
4570 #[gpui::test]
4571 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
4572 init_test(cx);
4573
4574 let fs = FakeFs::new(cx.background());
4575 let project = Project::test(fs, [], cx).await;
4576 let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
4577 let workspace = window.root(cx);
4578
4579 // Adding an item with no ambiguity renders the tab without detail.
4580 let item1 = window.add_view(cx, |_| {
4581 let mut item = TestItem::new();
4582 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
4583 item
4584 });
4585 workspace.update(cx, |workspace, cx| {
4586 workspace.add_item(Box::new(item1.clone()), cx);
4587 });
4588 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), None));
4589
4590 // Adding an item that creates ambiguity increases the level of detail on
4591 // both tabs.
4592 let item2 = window.add_view(cx, |_| {
4593 let mut item = TestItem::new();
4594 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
4595 item
4596 });
4597 workspace.update(cx, |workspace, cx| {
4598 workspace.add_item(Box::new(item2.clone()), cx);
4599 });
4600 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4601 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4602
4603 // Adding an item that creates ambiguity increases the level of detail only
4604 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
4605 // we stop at the highest detail available.
4606 let item3 = window.add_view(cx, |_| {
4607 let mut item = TestItem::new();
4608 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
4609 item
4610 });
4611 workspace.update(cx, |workspace, cx| {
4612 workspace.add_item(Box::new(item3.clone()), cx);
4613 });
4614 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4615 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
4616 item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
4617 }
4618
4619 #[gpui::test]
4620 async fn test_tracking_active_path(cx: &mut TestAppContext) {
4621 init_test(cx);
4622
4623 let fs = FakeFs::new(cx.background());
4624 fs.insert_tree(
4625 "/root1",
4626 json!({
4627 "one.txt": "",
4628 "two.txt": "",
4629 }),
4630 )
4631 .await;
4632 fs.insert_tree(
4633 "/root2",
4634 json!({
4635 "three.txt": "",
4636 }),
4637 )
4638 .await;
4639
4640 let project = Project::test(fs, ["root1".as_ref()], cx).await;
4641 let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
4642 let workspace = window.root(cx);
4643 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4644 let worktree_id = project.read_with(cx, |project, cx| {
4645 project.worktrees(cx).next().unwrap().read(cx).id()
4646 });
4647
4648 let item1 = window.add_view(cx, |cx| {
4649 TestItem::new().with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
4650 });
4651 let item2 = window.add_view(cx, |cx| {
4652 TestItem::new().with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
4653 });
4654
4655 // Add an item to an empty pane
4656 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
4657 project.read_with(cx, |project, cx| {
4658 assert_eq!(
4659 project.active_entry(),
4660 project
4661 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
4662 .map(|e| e.id)
4663 );
4664 });
4665 assert_eq!(window.current_title(cx).as_deref(), Some("one.txt β root1"));
4666
4667 // Add a second item to a non-empty pane
4668 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
4669 assert_eq!(window.current_title(cx).as_deref(), Some("two.txt β root1"));
4670 project.read_with(cx, |project, cx| {
4671 assert_eq!(
4672 project.active_entry(),
4673 project
4674 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
4675 .map(|e| e.id)
4676 );
4677 });
4678
4679 // Close the active item
4680 pane.update(cx, |pane, cx| {
4681 pane.close_active_item(&Default::default(), cx).unwrap()
4682 })
4683 .await
4684 .unwrap();
4685 assert_eq!(window.current_title(cx).as_deref(), Some("one.txt β root1"));
4686 project.read_with(cx, |project, cx| {
4687 assert_eq!(
4688 project.active_entry(),
4689 project
4690 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
4691 .map(|e| e.id)
4692 );
4693 });
4694
4695 // Add a project folder
4696 project
4697 .update(cx, |project, cx| {
4698 project.find_or_create_local_worktree("/root2", true, cx)
4699 })
4700 .await
4701 .unwrap();
4702 assert_eq!(
4703 window.current_title(cx).as_deref(),
4704 Some("one.txt β root1, root2")
4705 );
4706
4707 // Remove a project folder
4708 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
4709 assert_eq!(window.current_title(cx).as_deref(), Some("one.txt β root2"));
4710 }
4711
4712 #[gpui::test]
4713 async fn test_close_window(cx: &mut TestAppContext) {
4714 init_test(cx);
4715
4716 let fs = FakeFs::new(cx.background());
4717 fs.insert_tree("/root", json!({ "one": "" })).await;
4718
4719 let project = Project::test(fs, ["root".as_ref()], cx).await;
4720 let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
4721 let workspace = window.root(cx);
4722
4723 // When there are no dirty items, there's nothing to do.
4724 let item1 = window.add_view(cx, |_| TestItem::new());
4725 workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
4726 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4727 assert!(task.await.unwrap());
4728
4729 // When there are dirty untitled items, prompt to save each one. If the user
4730 // cancels any prompt, then abort.
4731 let item2 = window.add_view(cx, |_| TestItem::new().with_dirty(true));
4732 let item3 = window.add_view(cx, |cx| {
4733 TestItem::new()
4734 .with_dirty(true)
4735 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4736 });
4737 workspace.update(cx, |w, cx| {
4738 w.add_item(Box::new(item2.clone()), cx);
4739 w.add_item(Box::new(item3.clone()), cx);
4740 });
4741 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4742 cx.foreground().run_until_parked();
4743 window.simulate_prompt_answer(2, cx); // cancel save all
4744 cx.foreground().run_until_parked();
4745 window.simulate_prompt_answer(2, cx); // cancel save all
4746 cx.foreground().run_until_parked();
4747 assert!(!window.has_pending_prompt(cx));
4748 assert!(!task.await.unwrap());
4749 }
4750
4751 #[gpui::test]
4752 async fn test_close_pane_items(cx: &mut TestAppContext) {
4753 init_test(cx);
4754
4755 let fs = FakeFs::new(cx.background());
4756
4757 let project = Project::test(fs, None, cx).await;
4758 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
4759 let workspace = window.root(cx);
4760
4761 let item1 = window.add_view(cx, |cx| {
4762 TestItem::new()
4763 .with_dirty(true)
4764 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4765 });
4766 let item2 = window.add_view(cx, |cx| {
4767 TestItem::new()
4768 .with_dirty(true)
4769 .with_conflict(true)
4770 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
4771 });
4772 let item3 = window.add_view(cx, |cx| {
4773 TestItem::new()
4774 .with_dirty(true)
4775 .with_conflict(true)
4776 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
4777 });
4778 let item4 = window.add_view(cx, |cx| {
4779 TestItem::new()
4780 .with_dirty(true)
4781 .with_project_items(&[TestProjectItem::new_untitled(cx)])
4782 });
4783 let pane = workspace.update(cx, |workspace, cx| {
4784 workspace.add_item(Box::new(item1.clone()), cx);
4785 workspace.add_item(Box::new(item2.clone()), cx);
4786 workspace.add_item(Box::new(item3.clone()), cx);
4787 workspace.add_item(Box::new(item4.clone()), cx);
4788 workspace.active_pane().clone()
4789 });
4790
4791 let close_items = pane.update(cx, |pane, cx| {
4792 pane.activate_item(1, true, true, cx);
4793 assert_eq!(pane.active_item().unwrap().id(), item2.id());
4794 let item1_id = item1.id();
4795 let item3_id = item3.id();
4796 let item4_id = item4.id();
4797 pane.close_items(cx, SaveIntent::Close, move |id| {
4798 [item1_id, item3_id, item4_id].contains(&id)
4799 })
4800 });
4801 cx.foreground().run_until_parked();
4802
4803 assert!(window.has_pending_prompt(cx));
4804 // Ignore "Save all" prompt
4805 window.simulate_prompt_answer(2, cx);
4806 cx.foreground().run_until_parked();
4807 // There's a prompt to save item 1.
4808 pane.read_with(cx, |pane, _| {
4809 assert_eq!(pane.items_len(), 4);
4810 assert_eq!(pane.active_item().unwrap().id(), item1.id());
4811 });
4812 // Confirm saving item 1.
4813 window.simulate_prompt_answer(0, cx);
4814 cx.foreground().run_until_parked();
4815
4816 // Item 1 is saved. There's a prompt to save item 3.
4817 pane.read_with(cx, |pane, cx| {
4818 assert_eq!(item1.read(cx).save_count, 1);
4819 assert_eq!(item1.read(cx).save_as_count, 0);
4820 assert_eq!(item1.read(cx).reload_count, 0);
4821 assert_eq!(pane.items_len(), 3);
4822 assert_eq!(pane.active_item().unwrap().id(), item3.id());
4823 });
4824 assert!(window.has_pending_prompt(cx));
4825
4826 // Cancel saving item 3.
4827 window.simulate_prompt_answer(1, cx);
4828 cx.foreground().run_until_parked();
4829
4830 // Item 3 is reloaded. There's a prompt to save item 4.
4831 pane.read_with(cx, |pane, cx| {
4832 assert_eq!(item3.read(cx).save_count, 0);
4833 assert_eq!(item3.read(cx).save_as_count, 0);
4834 assert_eq!(item3.read(cx).reload_count, 1);
4835 assert_eq!(pane.items_len(), 2);
4836 assert_eq!(pane.active_item().unwrap().id(), item4.id());
4837 });
4838 assert!(window.has_pending_prompt(cx));
4839
4840 // Confirm saving item 4.
4841 window.simulate_prompt_answer(0, cx);
4842 cx.foreground().run_until_parked();
4843
4844 // There's a prompt for a path for item 4.
4845 cx.simulate_new_path_selection(|_| Some(Default::default()));
4846 close_items.await.unwrap();
4847
4848 // The requested items are closed.
4849 pane.read_with(cx, |pane, cx| {
4850 assert_eq!(item4.read(cx).save_count, 0);
4851 assert_eq!(item4.read(cx).save_as_count, 1);
4852 assert_eq!(item4.read(cx).reload_count, 0);
4853 assert_eq!(pane.items_len(), 1);
4854 assert_eq!(pane.active_item().unwrap().id(), item2.id());
4855 });
4856 }
4857
4858 #[gpui::test]
4859 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
4860 init_test(cx);
4861
4862 let fs = FakeFs::new(cx.background());
4863
4864 let project = Project::test(fs, [], cx).await;
4865 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
4866 let workspace = window.root(cx);
4867
4868 // Create several workspace items with single project entries, and two
4869 // workspace items with multiple project entries.
4870 let single_entry_items = (0..=4)
4871 .map(|project_entry_id| {
4872 window.add_view(cx, |cx| {
4873 TestItem::new()
4874 .with_dirty(true)
4875 .with_project_items(&[TestProjectItem::new(
4876 project_entry_id,
4877 &format!("{project_entry_id}.txt"),
4878 cx,
4879 )])
4880 })
4881 })
4882 .collect::<Vec<_>>();
4883 let item_2_3 = window.add_view(cx, |cx| {
4884 TestItem::new()
4885 .with_dirty(true)
4886 .with_singleton(false)
4887 .with_project_items(&[
4888 single_entry_items[2].read(cx).project_items[0].clone(),
4889 single_entry_items[3].read(cx).project_items[0].clone(),
4890 ])
4891 });
4892 let item_3_4 = window.add_view(cx, |cx| {
4893 TestItem::new()
4894 .with_dirty(true)
4895 .with_singleton(false)
4896 .with_project_items(&[
4897 single_entry_items[3].read(cx).project_items[0].clone(),
4898 single_entry_items[4].read(cx).project_items[0].clone(),
4899 ])
4900 });
4901
4902 // Create two panes that contain the following project entries:
4903 // left pane:
4904 // multi-entry items: (2, 3)
4905 // single-entry items: 0, 1, 2, 3, 4
4906 // right pane:
4907 // single-entry items: 1
4908 // multi-entry items: (3, 4)
4909 let left_pane = workspace.update(cx, |workspace, cx| {
4910 let left_pane = workspace.active_pane().clone();
4911 workspace.add_item(Box::new(item_2_3.clone()), cx);
4912 for item in single_entry_items {
4913 workspace.add_item(Box::new(item), cx);
4914 }
4915 left_pane.update(cx, |pane, cx| {
4916 pane.activate_item(2, true, true, cx);
4917 });
4918
4919 workspace
4920 .split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
4921 .unwrap();
4922
4923 left_pane
4924 });
4925
4926 //Need to cause an effect flush in order to respect new focus
4927 workspace.update(cx, |workspace, cx| {
4928 workspace.add_item(Box::new(item_3_4.clone()), cx);
4929 cx.focus(&left_pane);
4930 });
4931
4932 // When closing all of the items in the left pane, we should be prompted twice:
4933 // once for project entry 0, and once for project entry 2. After those two
4934 // prompts, the task should complete.
4935
4936 let close = left_pane.update(cx, |pane, cx| {
4937 pane.close_items(cx, SaveIntent::Close, move |_| true)
4938 });
4939 cx.foreground().run_until_parked();
4940 // Discard "Save all" prompt
4941 window.simulate_prompt_answer(2, cx);
4942
4943 cx.foreground().run_until_parked();
4944 left_pane.read_with(cx, |pane, cx| {
4945 assert_eq!(
4946 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
4947 &[ProjectEntryId::from_proto(0)]
4948 );
4949 });
4950 window.simulate_prompt_answer(0, cx);
4951
4952 cx.foreground().run_until_parked();
4953 left_pane.read_with(cx, |pane, cx| {
4954 assert_eq!(
4955 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
4956 &[ProjectEntryId::from_proto(2)]
4957 );
4958 });
4959 window.simulate_prompt_answer(0, cx);
4960
4961 cx.foreground().run_until_parked();
4962 close.await.unwrap();
4963 left_pane.read_with(cx, |pane, _| {
4964 assert_eq!(pane.items_len(), 0);
4965 });
4966 }
4967
4968 #[gpui::test]
4969 async fn test_autosave(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
4970 init_test(cx);
4971
4972 let fs = FakeFs::new(cx.background());
4973
4974 let project = Project::test(fs, [], cx).await;
4975 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
4976 let workspace = window.root(cx);
4977 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4978
4979 let item = window.add_view(cx, |cx| {
4980 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4981 });
4982 let item_id = item.id();
4983 workspace.update(cx, |workspace, cx| {
4984 workspace.add_item(Box::new(item.clone()), cx);
4985 });
4986
4987 // Autosave on window change.
4988 item.update(cx, |item, cx| {
4989 cx.update_global(|settings: &mut SettingsStore, cx| {
4990 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4991 settings.autosave = Some(AutosaveSetting::OnWindowChange);
4992 })
4993 });
4994 item.is_dirty = true;
4995 });
4996
4997 // Deactivating the window saves the file.
4998 window.simulate_deactivation(cx);
4999 deterministic.run_until_parked();
5000 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
5001
5002 // Autosave on focus change.
5003 item.update(cx, |item, cx| {
5004 cx.focus_self();
5005 cx.update_global(|settings: &mut SettingsStore, cx| {
5006 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
5007 settings.autosave = Some(AutosaveSetting::OnFocusChange);
5008 })
5009 });
5010 item.is_dirty = true;
5011 });
5012
5013 // Blurring the item saves the file.
5014 item.update(cx, |_, cx| cx.blur());
5015 deterministic.run_until_parked();
5016 item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
5017
5018 // Deactivating the window still saves the file.
5019 window.simulate_activation(cx);
5020 item.update(cx, |item, cx| {
5021 cx.focus_self();
5022 item.is_dirty = true;
5023 });
5024 window.simulate_deactivation(cx);
5025
5026 deterministic.run_until_parked();
5027 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
5028
5029 // Autosave after delay.
5030 item.update(cx, |item, cx| {
5031 cx.update_global(|settings: &mut SettingsStore, cx| {
5032 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
5033 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
5034 })
5035 });
5036 item.is_dirty = true;
5037 cx.emit(TestItemEvent::Edit);
5038 });
5039
5040 // Delay hasn't fully expired, so the file is still dirty and unsaved.
5041 deterministic.advance_clock(Duration::from_millis(250));
5042 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
5043
5044 // After delay expires, the file is saved.
5045 deterministic.advance_clock(Duration::from_millis(250));
5046 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
5047
5048 // Autosave on focus change, ensuring closing the tab counts as such.
5049 item.update(cx, |item, cx| {
5050 cx.update_global(|settings: &mut SettingsStore, cx| {
5051 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
5052 settings.autosave = Some(AutosaveSetting::OnFocusChange);
5053 })
5054 });
5055 item.is_dirty = true;
5056 });
5057
5058 pane.update(cx, |pane, cx| {
5059 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
5060 })
5061 .await
5062 .unwrap();
5063 assert!(!window.has_pending_prompt(cx));
5064 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
5065
5066 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
5067 workspace.update(cx, |workspace, cx| {
5068 workspace.add_item(Box::new(item.clone()), cx);
5069 });
5070 item.update(cx, |item, cx| {
5071 item.project_items[0].update(cx, |item, _| {
5072 item.entry_id = None;
5073 });
5074 item.is_dirty = true;
5075 cx.blur();
5076 });
5077 deterministic.run_until_parked();
5078 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
5079
5080 // Ensure autosave is prevented for deleted files also when closing the buffer.
5081 let _close_items = pane.update(cx, |pane, cx| {
5082 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
5083 });
5084 deterministic.run_until_parked();
5085 assert!(window.has_pending_prompt(cx));
5086 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
5087 }
5088
5089 #[gpui::test]
5090 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
5091 init_test(cx);
5092
5093 let fs = FakeFs::new(cx.background());
5094
5095 let project = Project::test(fs, [], cx).await;
5096 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
5097 let workspace = window.root(cx);
5098
5099 let item = window.add_view(cx, |cx| {
5100 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5101 });
5102 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5103 let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
5104 let toolbar_notify_count = Rc::new(RefCell::new(0));
5105
5106 workspace.update(cx, |workspace, cx| {
5107 workspace.add_item(Box::new(item.clone()), cx);
5108 let toolbar_notification_count = toolbar_notify_count.clone();
5109 cx.observe(&toolbar, move |_, _, _| {
5110 *toolbar_notification_count.borrow_mut() += 1
5111 })
5112 .detach();
5113 });
5114
5115 pane.read_with(cx, |pane, _| {
5116 assert!(!pane.can_navigate_backward());
5117 assert!(!pane.can_navigate_forward());
5118 });
5119
5120 item.update(cx, |item, cx| {
5121 item.set_state("one".to_string(), cx);
5122 });
5123
5124 // Toolbar must be notified to re-render the navigation buttons
5125 assert_eq!(*toolbar_notify_count.borrow(), 1);
5126
5127 pane.read_with(cx, |pane, _| {
5128 assert!(pane.can_navigate_backward());
5129 assert!(!pane.can_navigate_forward());
5130 });
5131
5132 workspace
5133 .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
5134 .await
5135 .unwrap();
5136
5137 assert_eq!(*toolbar_notify_count.borrow(), 3);
5138 pane.read_with(cx, |pane, _| {
5139 assert!(!pane.can_navigate_backward());
5140 assert!(pane.can_navigate_forward());
5141 });
5142 }
5143
5144 #[gpui::test]
5145 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
5146 init_test(cx);
5147 let fs = FakeFs::new(cx.background());
5148
5149 let project = Project::test(fs, [], cx).await;
5150 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
5151 let workspace = window.root(cx);
5152
5153 let panel = workspace.update(cx, |workspace, cx| {
5154 let panel = cx.add_view(|_| TestPanel::new(DockPosition::Right));
5155 workspace.add_panel(panel.clone(), cx);
5156
5157 workspace
5158 .right_dock()
5159 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
5160
5161 panel
5162 });
5163
5164 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5165 pane.update(cx, |pane, cx| {
5166 let item = cx.add_view(|_| TestItem::new());
5167 pane.add_item(Box::new(item), true, true, None, cx);
5168 });
5169
5170 // Transfer focus from center to panel
5171 workspace.update(cx, |workspace, cx| {
5172 workspace.toggle_panel_focus::<TestPanel>(cx);
5173 });
5174
5175 workspace.read_with(cx, |workspace, cx| {
5176 assert!(workspace.right_dock().read(cx).is_open());
5177 assert!(!panel.is_zoomed(cx));
5178 assert!(panel.has_focus(cx));
5179 });
5180
5181 // Transfer focus from panel to center
5182 workspace.update(cx, |workspace, cx| {
5183 workspace.toggle_panel_focus::<TestPanel>(cx);
5184 });
5185
5186 workspace.read_with(cx, |workspace, cx| {
5187 assert!(workspace.right_dock().read(cx).is_open());
5188 assert!(!panel.is_zoomed(cx));
5189 assert!(!panel.has_focus(cx));
5190 });
5191
5192 // Close the dock
5193 workspace.update(cx, |workspace, cx| {
5194 workspace.toggle_dock(DockPosition::Right, cx);
5195 });
5196
5197 workspace.read_with(cx, |workspace, cx| {
5198 assert!(!workspace.right_dock().read(cx).is_open());
5199 assert!(!panel.is_zoomed(cx));
5200 assert!(!panel.has_focus(cx));
5201 });
5202
5203 // Open the dock
5204 workspace.update(cx, |workspace, cx| {
5205 workspace.toggle_dock(DockPosition::Right, cx);
5206 });
5207
5208 workspace.read_with(cx, |workspace, cx| {
5209 assert!(workspace.right_dock().read(cx).is_open());
5210 assert!(!panel.is_zoomed(cx));
5211 assert!(panel.has_focus(cx));
5212 });
5213
5214 // Focus and zoom panel
5215 panel.update(cx, |panel, cx| {
5216 cx.focus_self();
5217 panel.set_zoomed(true, cx)
5218 });
5219
5220 workspace.read_with(cx, |workspace, cx| {
5221 assert!(workspace.right_dock().read(cx).is_open());
5222 assert!(panel.is_zoomed(cx));
5223 assert!(panel.has_focus(cx));
5224 });
5225
5226 // Transfer focus to the center closes the dock
5227 workspace.update(cx, |workspace, cx| {
5228 workspace.toggle_panel_focus::<TestPanel>(cx);
5229 });
5230
5231 workspace.read_with(cx, |workspace, cx| {
5232 assert!(!workspace.right_dock().read(cx).is_open());
5233 assert!(panel.is_zoomed(cx));
5234 assert!(!panel.has_focus(cx));
5235 });
5236
5237 // Transferring focus back to the panel keeps it zoomed
5238 workspace.update(cx, |workspace, cx| {
5239 workspace.toggle_panel_focus::<TestPanel>(cx);
5240 });
5241
5242 workspace.read_with(cx, |workspace, cx| {
5243 assert!(workspace.right_dock().read(cx).is_open());
5244 assert!(panel.is_zoomed(cx));
5245 assert!(panel.has_focus(cx));
5246 });
5247
5248 // Close the dock while it is zoomed
5249 workspace.update(cx, |workspace, cx| {
5250 workspace.toggle_dock(DockPosition::Right, cx)
5251 });
5252
5253 workspace.read_with(cx, |workspace, cx| {
5254 assert!(!workspace.right_dock().read(cx).is_open());
5255 assert!(panel.is_zoomed(cx));
5256 assert!(workspace.zoomed.is_none());
5257 assert!(!panel.has_focus(cx));
5258 });
5259
5260 // Opening the dock, when it's zoomed, retains focus
5261 workspace.update(cx, |workspace, cx| {
5262 workspace.toggle_dock(DockPosition::Right, cx)
5263 });
5264
5265 workspace.read_with(cx, |workspace, cx| {
5266 assert!(workspace.right_dock().read(cx).is_open());
5267 assert!(panel.is_zoomed(cx));
5268 assert!(workspace.zoomed.is_some());
5269 assert!(panel.has_focus(cx));
5270 });
5271
5272 // Unzoom and close the panel, zoom the active pane.
5273 panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
5274 workspace.update(cx, |workspace, cx| {
5275 workspace.toggle_dock(DockPosition::Right, cx)
5276 });
5277 pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
5278
5279 // Opening a dock unzooms the pane.
5280 workspace.update(cx, |workspace, cx| {
5281 workspace.toggle_dock(DockPosition::Right, cx)
5282 });
5283 workspace.read_with(cx, |workspace, cx| {
5284 let pane = pane.read(cx);
5285 assert!(!pane.is_zoomed());
5286 assert!(!pane.has_focus());
5287 assert!(workspace.right_dock().read(cx).is_open());
5288 assert!(workspace.zoomed.is_none());
5289 });
5290 }
5291
5292 #[gpui::test]
5293 async fn test_panels(cx: &mut gpui::TestAppContext) {
5294 init_test(cx);
5295 let fs = FakeFs::new(cx.background());
5296
5297 let project = Project::test(fs, [], cx).await;
5298 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
5299 let workspace = window.root(cx);
5300
5301 let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
5302 // Add panel_1 on the left, panel_2 on the right.
5303 let panel_1 = cx.add_view(|_| TestPanel::new(DockPosition::Left));
5304 workspace.add_panel(panel_1.clone(), cx);
5305 workspace
5306 .left_dock()
5307 .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
5308 let panel_2 = cx.add_view(|_| TestPanel::new(DockPosition::Right));
5309 workspace.add_panel(panel_2.clone(), cx);
5310 workspace
5311 .right_dock()
5312 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
5313
5314 let left_dock = workspace.left_dock();
5315 assert_eq!(
5316 left_dock.read(cx).visible_panel().unwrap().id(),
5317 panel_1.id()
5318 );
5319 assert_eq!(
5320 left_dock.read(cx).active_panel_size(cx).unwrap(),
5321 panel_1.size(cx)
5322 );
5323
5324 left_dock.update(cx, |left_dock, cx| {
5325 left_dock.resize_active_panel(Some(1337.), cx)
5326 });
5327 assert_eq!(
5328 workspace
5329 .right_dock()
5330 .read(cx)
5331 .visible_panel()
5332 .unwrap()
5333 .id(),
5334 panel_2.id()
5335 );
5336
5337 (panel_1, panel_2)
5338 });
5339
5340 // Move panel_1 to the right
5341 panel_1.update(cx, |panel_1, cx| {
5342 panel_1.set_position(DockPosition::Right, cx)
5343 });
5344
5345 workspace.update(cx, |workspace, cx| {
5346 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
5347 // Since it was the only panel on the left, the left dock should now be closed.
5348 assert!(!workspace.left_dock().read(cx).is_open());
5349 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
5350 let right_dock = workspace.right_dock();
5351 assert_eq!(
5352 right_dock.read(cx).visible_panel().unwrap().id(),
5353 panel_1.id()
5354 );
5355 assert_eq!(right_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
5356
5357 // Now we move panel_2Β to the left
5358 panel_2.set_position(DockPosition::Left, cx);
5359 });
5360
5361 workspace.update(cx, |workspace, cx| {
5362 // Since panel_2 was not visible on the right, we don't open the left dock.
5363 assert!(!workspace.left_dock().read(cx).is_open());
5364 // And the right dock is unaffected in it's displaying of panel_1
5365 assert!(workspace.right_dock().read(cx).is_open());
5366 assert_eq!(
5367 workspace
5368 .right_dock()
5369 .read(cx)
5370 .visible_panel()
5371 .unwrap()
5372 .id(),
5373 panel_1.id()
5374 );
5375 });
5376
5377 // Move panel_1 back to the left
5378 panel_1.update(cx, |panel_1, cx| {
5379 panel_1.set_position(DockPosition::Left, cx)
5380 });
5381
5382 workspace.update(cx, |workspace, cx| {
5383 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
5384 let left_dock = workspace.left_dock();
5385 assert!(left_dock.read(cx).is_open());
5386 assert_eq!(
5387 left_dock.read(cx).visible_panel().unwrap().id(),
5388 panel_1.id()
5389 );
5390 assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
5391 // And right the dock should be closed as it no longer has any panels.
5392 assert!(!workspace.right_dock().read(cx).is_open());
5393
5394 // Now we move panel_1 to the bottom
5395 panel_1.set_position(DockPosition::Bottom, cx);
5396 });
5397
5398 workspace.update(cx, |workspace, cx| {
5399 // Since panel_1 was visible on the left, we close the left dock.
5400 assert!(!workspace.left_dock().read(cx).is_open());
5401 // The bottom dock is sized based on the panel's default size,
5402 // since the panel orientation changed from vertical to horizontal.
5403 let bottom_dock = workspace.bottom_dock();
5404 assert_eq!(
5405 bottom_dock.read(cx).active_panel_size(cx).unwrap(),
5406 panel_1.size(cx),
5407 );
5408 // Close bottom dock and move panel_1 back to the left.
5409 bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
5410 panel_1.set_position(DockPosition::Left, cx);
5411 });
5412
5413 // Emit activated event on panel 1
5414 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Activated));
5415
5416 // Now the left dock is open and panel_1 is active and focused.
5417 workspace.read_with(cx, |workspace, cx| {
5418 let left_dock = workspace.left_dock();
5419 assert!(left_dock.read(cx).is_open());
5420 assert_eq!(
5421 left_dock.read(cx).visible_panel().unwrap().id(),
5422 panel_1.id()
5423 );
5424 assert!(panel_1.is_focused(cx));
5425 });
5426
5427 // Emit closed event on panel 2, which is not active
5428 panel_2.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed));
5429
5430 // Wo don't close the left dock, because panel_2 wasn't the active panel
5431 workspace.read_with(cx, |workspace, cx| {
5432 let left_dock = workspace.left_dock();
5433 assert!(left_dock.read(cx).is_open());
5434 assert_eq!(
5435 left_dock.read(cx).visible_panel().unwrap().id(),
5436 panel_1.id()
5437 );
5438 });
5439
5440 // Emitting a ZoomIn event shows the panel as zoomed.
5441 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomIn));
5442 workspace.read_with(cx, |workspace, _| {
5443 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
5444 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
5445 });
5446
5447 // Move panel to another dock while it is zoomed
5448 panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
5449 workspace.read_with(cx, |workspace, _| {
5450 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
5451 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5452 });
5453
5454 // If focus is transferred to another view that's not a panel or another pane, we still show
5455 // the panel as zoomed.
5456 let focus_receiver = window.add_view(cx, |_| EmptyView);
5457 focus_receiver.update(cx, |_, cx| cx.focus_self());
5458 workspace.read_with(cx, |workspace, _| {
5459 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
5460 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5461 });
5462
5463 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
5464 workspace.update(cx, |_, cx| cx.focus_self());
5465 workspace.read_with(cx, |workspace, _| {
5466 assert_eq!(workspace.zoomed, None);
5467 assert_eq!(workspace.zoomed_position, None);
5468 });
5469
5470 // If focus is transferred again to another view that's not a panel or a pane, we won't
5471 // show the panel as zoomed because it wasn't zoomed before.
5472 focus_receiver.update(cx, |_, cx| cx.focus_self());
5473 workspace.read_with(cx, |workspace, _| {
5474 assert_eq!(workspace.zoomed, None);
5475 assert_eq!(workspace.zoomed_position, None);
5476 });
5477
5478 // When focus is transferred back to the panel, it is zoomed again.
5479 panel_1.update(cx, |_, cx| cx.focus_self());
5480 workspace.read_with(cx, |workspace, _| {
5481 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
5482 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5483 });
5484
5485 // Emitting a ZoomOut event unzooms the panel.
5486 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomOut));
5487 workspace.read_with(cx, |workspace, _| {
5488 assert_eq!(workspace.zoomed, None);
5489 assert_eq!(workspace.zoomed_position, None);
5490 });
5491
5492 // Emit closed event on panel 1, which is active
5493 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed));
5494
5495 // Now the left dock is closed, because panel_1 was the active panel
5496 workspace.read_with(cx, |workspace, cx| {
5497 let right_dock = workspace.right_dock();
5498 assert!(!right_dock.read(cx).is_open());
5499 });
5500 }
5501
5502 pub fn init_test(cx: &mut TestAppContext) {
5503 cx.foreground().forbid_parking();
5504 cx.update(|cx| {
5505 cx.set_global(SettingsStore::test(cx));
5506 theme::init((), cx);
5507 language::init(cx);
5508 crate::init_settings(cx);
5509 Project::init_settings(cx);
5510 });
5511 }
5512}