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