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