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 mut is_project_item = true;
2984 let mut update = proto::UpdateActiveView::default();
2985 if self.active_pane.read(cx).has_focus() {
2986 let item = self
2987 .active_item(cx)
2988 .and_then(|item| item.to_followable_item_handle(cx));
2989 if let Some(item) = item {
2990 is_project_item = item.is_project_item(cx);
2991 update = proto::UpdateActiveView {
2992 id: item
2993 .remote_id(&self.app_state.client, cx)
2994 .map(|id| id.to_proto()),
2995 leader_id: self.leader_for_pane(&self.active_pane),
2996 };
2997 }
2998 }
2999
3000 self.update_followers(
3001 is_project_item,
3002 proto::update_followers::Variant::UpdateActiveView(update),
3003 cx,
3004 );
3005 }
3006
3007 fn update_followers(
3008 &self,
3009 project_only: bool,
3010 update: proto::update_followers::Variant,
3011 cx: &AppContext,
3012 ) -> Option<()> {
3013 let project_id = if project_only {
3014 self.project.read(cx).remote_id()
3015 } else {
3016 None
3017 };
3018 self.app_state().workspace_store.read_with(cx, |store, cx| {
3019 store.update_followers(project_id, update, cx)
3020 })
3021 }
3022
3023 pub fn leader_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<PeerId> {
3024 self.follower_states_by_leader
3025 .iter()
3026 .find_map(|(leader_id, state)| {
3027 if state.contains_key(pane) {
3028 Some(*leader_id)
3029 } else {
3030 None
3031 }
3032 })
3033 }
3034
3035 fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
3036 cx.notify();
3037
3038 let call = self.active_call()?;
3039 let room = call.read(cx).room()?.read(cx);
3040 let participant = room.remote_participant_for_peer_id(leader_id)?;
3041 let mut items_to_activate = Vec::new();
3042
3043 let leader_in_this_app;
3044 let leader_in_this_project;
3045 match participant.location {
3046 call::ParticipantLocation::SharedProject { project_id } => {
3047 leader_in_this_app = true;
3048 leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
3049 }
3050 call::ParticipantLocation::UnsharedProject => {
3051 leader_in_this_app = true;
3052 leader_in_this_project = false;
3053 }
3054 call::ParticipantLocation::External => {
3055 leader_in_this_app = false;
3056 leader_in_this_project = false;
3057 }
3058 };
3059
3060 for (pane, state) in self.follower_states_by_leader.get(&leader_id)? {
3061 if leader_in_this_app {
3062 let item = state
3063 .active_view_id
3064 .and_then(|id| state.items_by_leader_view_id.get(&id));
3065 if let Some(item) = item {
3066 if leader_in_this_project || !item.is_project_item(cx) {
3067 items_to_activate.push((pane.clone(), item.boxed_clone()));
3068 }
3069 continue;
3070 }
3071 }
3072 if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
3073 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
3074 }
3075 }
3076
3077 for (pane, item) in items_to_activate {
3078 let pane_was_focused = pane.read(cx).has_focus();
3079 if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) {
3080 pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx));
3081 } else {
3082 pane.update(cx, |pane, cx| {
3083 pane.add_item(item.boxed_clone(), false, false, None, cx)
3084 });
3085 }
3086
3087 if pane_was_focused {
3088 pane.update(cx, |pane, cx| pane.focus_active_item(cx));
3089 }
3090 }
3091
3092 None
3093 }
3094
3095 fn shared_screen_for_peer(
3096 &self,
3097 peer_id: PeerId,
3098 pane: &ViewHandle<Pane>,
3099 cx: &mut ViewContext<Self>,
3100 ) -> Option<ViewHandle<SharedScreen>> {
3101 let call = self.active_call()?;
3102 let room = call.read(cx).room()?.read(cx);
3103 let participant = room.remote_participant_for_peer_id(peer_id)?;
3104 let track = participant.video_tracks.values().next()?.clone();
3105 let user = participant.user.clone();
3106
3107 for item in pane.read(cx).items_of_type::<SharedScreen>() {
3108 if item.read(cx).peer_id == peer_id {
3109 return Some(item);
3110 }
3111 }
3112
3113 Some(cx.add_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
3114 }
3115
3116 pub fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
3117 if active {
3118 self.update_active_view_for_followers(cx);
3119 cx.background()
3120 .spawn(persistence::DB.update_timestamp(self.database_id()))
3121 .detach();
3122 } else {
3123 for pane in &self.panes {
3124 pane.update(cx, |pane, cx| {
3125 if let Some(item) = pane.active_item() {
3126 item.workspace_deactivated(cx);
3127 }
3128 if matches!(
3129 settings::get::<WorkspaceSettings>(cx).autosave,
3130 AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
3131 ) {
3132 for item in pane.items() {
3133 Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
3134 .detach_and_log_err(cx);
3135 }
3136 }
3137 });
3138 }
3139 }
3140 }
3141
3142 fn active_call(&self) -> Option<&ModelHandle<ActiveCall>> {
3143 self.active_call.as_ref().map(|(call, _)| call)
3144 }
3145
3146 fn on_active_call_event(
3147 &mut self,
3148 _: ModelHandle<ActiveCall>,
3149 event: &call::room::Event,
3150 cx: &mut ViewContext<Self>,
3151 ) {
3152 match event {
3153 call::room::Event::ParticipantLocationChanged { participant_id }
3154 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
3155 self.leader_updated(*participant_id, cx);
3156 }
3157 _ => {}
3158 }
3159 }
3160
3161 pub fn database_id(&self) -> WorkspaceId {
3162 self.database_id
3163 }
3164
3165 fn location(&self, cx: &AppContext) -> Option<WorkspaceLocation> {
3166 let project = self.project().read(cx);
3167
3168 if project.is_local() {
3169 Some(
3170 project
3171 .visible_worktrees(cx)
3172 .map(|worktree| worktree.read(cx).abs_path())
3173 .collect::<Vec<_>>()
3174 .into(),
3175 )
3176 } else {
3177 None
3178 }
3179 }
3180
3181 fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
3182 match member {
3183 Member::Axis(PaneAxis { members, .. }) => {
3184 for child in members.iter() {
3185 self.remove_panes(child.clone(), cx)
3186 }
3187 }
3188 Member::Pane(pane) => {
3189 self.force_remove_pane(&pane, cx);
3190 }
3191 }
3192 }
3193
3194 fn force_remove_pane(&mut self, pane: &ViewHandle<Pane>, cx: &mut ViewContext<Workspace>) {
3195 self.panes.retain(|p| p != pane);
3196 cx.focus(self.panes.last().unwrap());
3197 if self.last_active_center_pane == Some(pane.downgrade()) {
3198 self.last_active_center_pane = None;
3199 }
3200 cx.notify();
3201 }
3202
3203 fn schedule_serialize(&mut self, cx: &mut ViewContext<Self>) {
3204 self._schedule_serialize = Some(cx.spawn(|this, cx| async move {
3205 cx.background().timer(Duration::from_millis(100)).await;
3206 this.read_with(&cx, |this, cx| this.serialize_workspace(cx))
3207 .ok();
3208 }));
3209 }
3210
3211 fn serialize_workspace(&self, cx: &ViewContext<Self>) {
3212 fn serialize_pane_handle(
3213 pane_handle: &ViewHandle<Pane>,
3214 cx: &AppContext,
3215 ) -> SerializedPane {
3216 let (items, active) = {
3217 let pane = pane_handle.read(cx);
3218 let active_item_id = pane.active_item().map(|item| item.id());
3219 (
3220 pane.items()
3221 .filter_map(|item_handle| {
3222 Some(SerializedItem {
3223 kind: Arc::from(item_handle.serialized_item_kind()?),
3224 item_id: item_handle.id(),
3225 active: Some(item_handle.id()) == active_item_id,
3226 })
3227 })
3228 .collect::<Vec<_>>(),
3229 pane.has_focus(),
3230 )
3231 };
3232
3233 SerializedPane::new(items, active)
3234 }
3235
3236 fn build_serialized_pane_group(
3237 pane_group: &Member,
3238 cx: &AppContext,
3239 ) -> SerializedPaneGroup {
3240 match pane_group {
3241 Member::Axis(PaneAxis {
3242 axis,
3243 members,
3244 flexes,
3245 bounding_boxes: _,
3246 }) => SerializedPaneGroup::Group {
3247 axis: *axis,
3248 children: members
3249 .iter()
3250 .map(|member| build_serialized_pane_group(member, cx))
3251 .collect::<Vec<_>>(),
3252 flexes: Some(flexes.borrow().clone()),
3253 },
3254 Member::Pane(pane_handle) => {
3255 SerializedPaneGroup::Pane(serialize_pane_handle(&pane_handle, cx))
3256 }
3257 }
3258 }
3259
3260 fn build_serialized_docks(this: &Workspace, cx: &ViewContext<Workspace>) -> DockStructure {
3261 let left_dock = this.left_dock.read(cx);
3262 let left_visible = left_dock.is_open();
3263 let left_active_panel = left_dock.visible_panel().and_then(|panel| {
3264 Some(
3265 cx.view_ui_name(panel.as_any().window(), panel.id())?
3266 .to_string(),
3267 )
3268 });
3269 let left_dock_zoom = left_dock
3270 .visible_panel()
3271 .map(|panel| panel.is_zoomed(cx))
3272 .unwrap_or(false);
3273
3274 let right_dock = this.right_dock.read(cx);
3275 let right_visible = right_dock.is_open();
3276 let right_active_panel = right_dock.visible_panel().and_then(|panel| {
3277 Some(
3278 cx.view_ui_name(panel.as_any().window(), panel.id())?
3279 .to_string(),
3280 )
3281 });
3282 let right_dock_zoom = right_dock
3283 .visible_panel()
3284 .map(|panel| panel.is_zoomed(cx))
3285 .unwrap_or(false);
3286
3287 let bottom_dock = this.bottom_dock.read(cx);
3288 let bottom_visible = bottom_dock.is_open();
3289 let bottom_active_panel = bottom_dock.visible_panel().and_then(|panel| {
3290 Some(
3291 cx.view_ui_name(panel.as_any().window(), panel.id())?
3292 .to_string(),
3293 )
3294 });
3295 let bottom_dock_zoom = bottom_dock
3296 .visible_panel()
3297 .map(|panel| panel.is_zoomed(cx))
3298 .unwrap_or(false);
3299
3300 DockStructure {
3301 left: DockData {
3302 visible: left_visible,
3303 active_panel: left_active_panel,
3304 zoom: left_dock_zoom,
3305 },
3306 right: DockData {
3307 visible: right_visible,
3308 active_panel: right_active_panel,
3309 zoom: right_dock_zoom,
3310 },
3311 bottom: DockData {
3312 visible: bottom_visible,
3313 active_panel: bottom_active_panel,
3314 zoom: bottom_dock_zoom,
3315 },
3316 }
3317 }
3318
3319 if let Some(location) = self.location(cx) {
3320 // Load bearing special case:
3321 // - with_local_workspace() relies on this to not have other stuff open
3322 // when you open your log
3323 if !location.paths().is_empty() {
3324 let center_group = build_serialized_pane_group(&self.center.root, cx);
3325 let docks = build_serialized_docks(self, cx);
3326
3327 let serialized_workspace = SerializedWorkspace {
3328 id: self.database_id,
3329 location,
3330 center_group,
3331 bounds: Default::default(),
3332 display: Default::default(),
3333 docks,
3334 };
3335
3336 cx.background()
3337 .spawn(persistence::DB.save_workspace(serialized_workspace))
3338 .detach();
3339 }
3340 }
3341 }
3342
3343 pub(crate) fn load_workspace(
3344 workspace: WeakViewHandle<Workspace>,
3345 serialized_workspace: SerializedWorkspace,
3346 paths_to_open: Vec<Option<ProjectPath>>,
3347 cx: &mut AppContext,
3348 ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
3349 cx.spawn(|mut cx| async move {
3350 let result = async_iife! {{
3351 let (project, old_center_pane) =
3352 workspace.read_with(&cx, |workspace, _| {
3353 (
3354 workspace.project().clone(),
3355 workspace.last_active_center_pane.clone(),
3356 )
3357 })?;
3358
3359 let mut center_items = None;
3360 let mut center_group = None;
3361 // Traverse the splits tree and add to things
3362 if let Some((group, active_pane, items)) = serialized_workspace
3363 .center_group
3364 .deserialize(&project, serialized_workspace.id, &workspace, &mut cx)
3365 .await {
3366 center_items = Some(items);
3367 center_group = Some((group, active_pane))
3368 }
3369
3370 let resulting_list = cx.read(|cx| {
3371 let mut opened_items = center_items
3372 .unwrap_or_default()
3373 .into_iter()
3374 .filter_map(|item| {
3375 let item = item?;
3376 let project_path = item.project_path(cx)?;
3377 Some((project_path, item))
3378 })
3379 .collect::<HashMap<_, _>>();
3380
3381 paths_to_open
3382 .into_iter()
3383 .map(|path_to_open| {
3384 path_to_open.map(|path_to_open| {
3385 Ok(opened_items.remove(&path_to_open))
3386 })
3387 .transpose()
3388 .map(|item| item.flatten())
3389 .transpose()
3390 })
3391 .collect::<Vec<_>>()
3392 });
3393
3394 // Remove old panes from workspace panes list
3395 workspace.update(&mut cx, |workspace, cx| {
3396 if let Some((center_group, active_pane)) = center_group {
3397 workspace.remove_panes(workspace.center.root.clone(), cx);
3398
3399 // Swap workspace center group
3400 workspace.center = PaneGroup::with_root(center_group);
3401
3402 // Change the focus to the workspace first so that we retrigger focus in on the pane.
3403 cx.focus_self();
3404
3405 if let Some(active_pane) = active_pane {
3406 cx.focus(&active_pane);
3407 } else {
3408 cx.focus(workspace.panes.last().unwrap());
3409 }
3410 } else {
3411 let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade(cx));
3412 if let Some(old_center_handle) = old_center_handle {
3413 cx.focus(&old_center_handle)
3414 } else {
3415 cx.focus_self()
3416 }
3417 }
3418
3419 let docks = serialized_workspace.docks;
3420 workspace.left_dock.update(cx, |dock, cx| {
3421 dock.set_open(docks.left.visible, cx);
3422 if let Some(active_panel) = docks.left.active_panel {
3423 if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
3424 dock.activate_panel(ix, cx);
3425 }
3426 }
3427 dock.active_panel()
3428 .map(|panel| {
3429 panel.set_zoomed(docks.left.zoom, cx)
3430 });
3431 if docks.left.visible && docks.left.zoom {
3432 cx.focus_self()
3433 }
3434 });
3435 // TODO: I think the bug is that setting zoom or active undoes the bottom zoom or something
3436 workspace.right_dock.update(cx, |dock, cx| {
3437 dock.set_open(docks.right.visible, cx);
3438 if let Some(active_panel) = docks.right.active_panel {
3439 if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
3440 dock.activate_panel(ix, cx);
3441
3442 }
3443 }
3444 dock.active_panel()
3445 .map(|panel| {
3446 panel.set_zoomed(docks.right.zoom, cx)
3447 });
3448
3449 if docks.right.visible && docks.right.zoom {
3450 cx.focus_self()
3451 }
3452 });
3453 workspace.bottom_dock.update(cx, |dock, cx| {
3454 dock.set_open(docks.bottom.visible, cx);
3455 if let Some(active_panel) = docks.bottom.active_panel {
3456 if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
3457 dock.activate_panel(ix, cx);
3458 }
3459 }
3460
3461 dock.active_panel()
3462 .map(|panel| {
3463 panel.set_zoomed(docks.bottom.zoom, cx)
3464 });
3465
3466 if docks.bottom.visible && docks.bottom.zoom {
3467 cx.focus_self()
3468 }
3469 });
3470
3471
3472 cx.notify();
3473 })?;
3474
3475 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
3476 workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))?;
3477
3478 Ok::<_, anyhow::Error>(resulting_list)
3479 }};
3480
3481 result.await.unwrap_or_default()
3482 })
3483 }
3484
3485 #[cfg(any(test, feature = "test-support"))]
3486 pub fn test_new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
3487 let client = project.read(cx).client();
3488 let user_store = project.read(cx).user_store();
3489
3490 let channel_store =
3491 cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
3492 let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx));
3493 let app_state = Arc::new(AppState {
3494 languages: project.read(cx).languages().clone(),
3495 workspace_store,
3496 client,
3497 user_store,
3498 channel_store,
3499 fs: project.read(cx).fs().clone(),
3500 build_window_options: |_, _, _| Default::default(),
3501 initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
3502 background_actions: || &[],
3503 });
3504 Self::new(0, project, app_state, cx)
3505 }
3506
3507 fn render_dock(&self, position: DockPosition, cx: &WindowContext) -> Option<AnyElement<Self>> {
3508 let dock = match position {
3509 DockPosition::Left => &self.left_dock,
3510 DockPosition::Right => &self.right_dock,
3511 DockPosition::Bottom => &self.bottom_dock,
3512 };
3513 let active_panel = dock.read(cx).visible_panel()?;
3514 let element = if Some(active_panel.id()) == self.zoomed.as_ref().map(|zoomed| zoomed.id()) {
3515 dock.read(cx).render_placeholder(cx)
3516 } else {
3517 ChildView::new(dock, cx).into_any()
3518 };
3519
3520 Some(
3521 element
3522 .constrained()
3523 .dynamically(move |constraint, _, cx| match position {
3524 DockPosition::Left | DockPosition::Right => SizeConstraint::new(
3525 Vector2F::new(20., constraint.min.y()),
3526 Vector2F::new(cx.window_size().x() * 0.8, constraint.max.y()),
3527 ),
3528 DockPosition::Bottom => SizeConstraint::new(
3529 Vector2F::new(constraint.min.x(), 20.),
3530 Vector2F::new(constraint.max.x(), cx.window_size().y() * 0.8),
3531 ),
3532 })
3533 .into_any(),
3534 )
3535 }
3536}
3537
3538fn window_bounds_env_override(cx: &AsyncAppContext) -> Option<WindowBounds> {
3539 ZED_WINDOW_POSITION
3540 .zip(*ZED_WINDOW_SIZE)
3541 .map(|(position, size)| {
3542 WindowBounds::Fixed(RectF::new(
3543 cx.platform().screens()[0].bounds().origin() + position,
3544 size,
3545 ))
3546 })
3547}
3548
3549async fn open_items(
3550 serialized_workspace: Option<SerializedWorkspace>,
3551 workspace: &WeakViewHandle<Workspace>,
3552 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
3553 app_state: Arc<AppState>,
3554 mut cx: AsyncAppContext,
3555) -> Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>> {
3556 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
3557
3558 if let Some(serialized_workspace) = serialized_workspace {
3559 let workspace = workspace.clone();
3560 let restored_items = cx
3561 .update(|cx| {
3562 Workspace::load_workspace(
3563 workspace,
3564 serialized_workspace,
3565 project_paths_to_open
3566 .iter()
3567 .map(|(_, project_path)| project_path)
3568 .cloned()
3569 .collect(),
3570 cx,
3571 )
3572 })
3573 .await;
3574
3575 let restored_project_paths = cx.read(|cx| {
3576 restored_items
3577 .iter()
3578 .filter_map(|item| item.as_ref()?.as_ref().ok()?.project_path(cx))
3579 .collect::<HashSet<_>>()
3580 });
3581
3582 opened_items = restored_items;
3583 project_paths_to_open
3584 .iter_mut()
3585 .for_each(|(_, project_path)| {
3586 if let Some(project_path_to_open) = project_path {
3587 if restored_project_paths.contains(project_path_to_open) {
3588 *project_path = None;
3589 }
3590 }
3591 });
3592 } else {
3593 for _ in 0..project_paths_to_open.len() {
3594 opened_items.push(None);
3595 }
3596 }
3597 assert!(opened_items.len() == project_paths_to_open.len());
3598
3599 let tasks =
3600 project_paths_to_open
3601 .into_iter()
3602 .enumerate()
3603 .map(|(i, (abs_path, project_path))| {
3604 let workspace = workspace.clone();
3605 cx.spawn(|mut cx| {
3606 let fs = app_state.fs.clone();
3607 async move {
3608 let file_project_path = project_path?;
3609 if fs.is_file(&abs_path).await {
3610 Some((
3611 i,
3612 workspace
3613 .update(&mut cx, |workspace, cx| {
3614 workspace.open_path(file_project_path, None, true, cx)
3615 })
3616 .log_err()?
3617 .await,
3618 ))
3619 } else {
3620 None
3621 }
3622 }
3623 })
3624 });
3625
3626 for maybe_opened_path in futures::future::join_all(tasks.into_iter())
3627 .await
3628 .into_iter()
3629 {
3630 if let Some((i, path_open_result)) = maybe_opened_path {
3631 opened_items[i] = Some(path_open_result);
3632 }
3633 }
3634
3635 opened_items
3636}
3637
3638fn notify_of_new_dock(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
3639 const NEW_PANEL_BLOG_POST: &str = "https://zed.dev/blog/new-panel-system";
3640 const NEW_DOCK_HINT_KEY: &str = "show_new_dock_key";
3641 const MESSAGE_ID: usize = 2;
3642
3643 if workspace
3644 .read_with(cx, |workspace, cx| {
3645 workspace.has_shown_notification_once::<MessageNotification>(MESSAGE_ID, cx)
3646 })
3647 .unwrap_or(false)
3648 {
3649 return;
3650 }
3651
3652 if db::kvp::KEY_VALUE_STORE
3653 .read_kvp(NEW_DOCK_HINT_KEY)
3654 .ok()
3655 .flatten()
3656 .is_some()
3657 {
3658 if !workspace
3659 .read_with(cx, |workspace, cx| {
3660 workspace.has_shown_notification_once::<MessageNotification>(MESSAGE_ID, cx)
3661 })
3662 .unwrap_or(false)
3663 {
3664 cx.update(|cx| {
3665 cx.update_global::<NotificationTracker, _, _>(|tracker, _| {
3666 let entry = tracker
3667 .entry(TypeId::of::<MessageNotification>())
3668 .or_default();
3669 if !entry.contains(&MESSAGE_ID) {
3670 entry.push(MESSAGE_ID);
3671 }
3672 });
3673 });
3674 }
3675
3676 return;
3677 }
3678
3679 cx.spawn(|_| async move {
3680 db::kvp::KEY_VALUE_STORE
3681 .write_kvp(NEW_DOCK_HINT_KEY.to_string(), "seen".to_string())
3682 .await
3683 .ok();
3684 })
3685 .detach();
3686
3687 workspace
3688 .update(cx, |workspace, cx| {
3689 workspace.show_notification_once(2, cx, |cx| {
3690 cx.add_view(|_| {
3691 MessageNotification::new_element(|text, _| {
3692 Text::new(
3693 "Looking for the dock? Try ctrl-`!\nshift-escape now zooms your pane.",
3694 text,
3695 )
3696 .with_custom_runs(vec![26..32, 34..46], |_, bounds, cx| {
3697 let code_span_background_color = settings::get::<ThemeSettings>(cx)
3698 .theme
3699 .editor
3700 .document_highlight_read_background;
3701
3702 cx.scene().push_quad(gpui::Quad {
3703 bounds,
3704 background: Some(code_span_background_color),
3705 border: Default::default(),
3706 corner_radii: (2.0).into(),
3707 })
3708 })
3709 .into_any()
3710 })
3711 .with_click_message("Read more about the new panel system")
3712 .on_click(|cx| cx.platform().open_url(NEW_PANEL_BLOG_POST))
3713 })
3714 })
3715 })
3716 .ok();
3717}
3718
3719fn notify_if_database_failed(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
3720 const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
3721
3722 workspace
3723 .update(cx, |workspace, cx| {
3724 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
3725 workspace.show_notification_once(0, cx, |cx| {
3726 cx.add_view(|_| {
3727 MessageNotification::new("Failed to load the database file.")
3728 .with_click_message("Click to let us know about this error")
3729 .on_click(|cx| cx.platform().open_url(REPORT_ISSUE_URL))
3730 })
3731 });
3732 }
3733 })
3734 .log_err();
3735}
3736
3737impl Entity for Workspace {
3738 type Event = Event;
3739
3740 fn release(&mut self, cx: &mut AppContext) {
3741 self.app_state.workspace_store.update(cx, |store, _| {
3742 store.workspaces.remove(&self.weak_self);
3743 })
3744 }
3745}
3746
3747impl View for Workspace {
3748 fn ui_name() -> &'static str {
3749 "Workspace"
3750 }
3751
3752 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
3753 let theme = theme::current(cx).clone();
3754 Stack::new()
3755 .with_child(
3756 Flex::column()
3757 .with_child(self.render_titlebar(&theme, cx))
3758 .with_child(
3759 Stack::new()
3760 .with_child({
3761 let project = self.project.clone();
3762 Flex::row()
3763 .with_children(self.render_dock(DockPosition::Left, cx))
3764 .with_child(
3765 Flex::column()
3766 .with_child(
3767 FlexItem::new(
3768 self.center.render(
3769 &project,
3770 &theme,
3771 &self.follower_states_by_leader,
3772 self.active_call(),
3773 self.active_pane(),
3774 self.zoomed
3775 .as_ref()
3776 .and_then(|zoomed| zoomed.upgrade(cx))
3777 .as_ref(),
3778 &self.app_state,
3779 cx,
3780 ),
3781 )
3782 .flex(1., true),
3783 )
3784 .with_children(
3785 self.render_dock(DockPosition::Bottom, cx),
3786 )
3787 .flex(1., true),
3788 )
3789 .with_children(self.render_dock(DockPosition::Right, cx))
3790 })
3791 .with_child(Overlay::new(
3792 Stack::new()
3793 .with_children(self.zoomed.as_ref().and_then(|zoomed| {
3794 enum ZoomBackground {}
3795 let zoomed = zoomed.upgrade(cx)?;
3796
3797 let mut foreground_style =
3798 theme.workspace.zoomed_pane_foreground;
3799 if let Some(zoomed_dock_position) = self.zoomed_position {
3800 foreground_style =
3801 theme.workspace.zoomed_panel_foreground;
3802 let margin = foreground_style.margin.top;
3803 let border = foreground_style.border.top;
3804
3805 // Only include a margin and border on the opposite side.
3806 foreground_style.margin.top = 0.;
3807 foreground_style.margin.left = 0.;
3808 foreground_style.margin.bottom = 0.;
3809 foreground_style.margin.right = 0.;
3810 foreground_style.border.top = false;
3811 foreground_style.border.left = false;
3812 foreground_style.border.bottom = false;
3813 foreground_style.border.right = false;
3814 match zoomed_dock_position {
3815 DockPosition::Left => {
3816 foreground_style.margin.right = margin;
3817 foreground_style.border.right = border;
3818 }
3819 DockPosition::Right => {
3820 foreground_style.margin.left = margin;
3821 foreground_style.border.left = border;
3822 }
3823 DockPosition::Bottom => {
3824 foreground_style.margin.top = margin;
3825 foreground_style.border.top = border;
3826 }
3827 }
3828 }
3829
3830 Some(
3831 ChildView::new(&zoomed, cx)
3832 .contained()
3833 .with_style(foreground_style)
3834 .aligned()
3835 .contained()
3836 .with_style(theme.workspace.zoomed_background)
3837 .mouse::<ZoomBackground>(0)
3838 .capture_all()
3839 .on_down(
3840 MouseButton::Left,
3841 |_, this: &mut Self, cx| {
3842 this.zoom_out(cx);
3843 },
3844 ),
3845 )
3846 }))
3847 .with_children(self.modal.as_ref().map(|modal| {
3848 // Prevent clicks within the modal from falling
3849 // through to the rest of the workspace.
3850 enum ModalBackground {}
3851 MouseEventHandler::new::<ModalBackground, _>(
3852 0,
3853 cx,
3854 |_, cx| ChildView::new(modal.view.as_any(), cx),
3855 )
3856 .on_click(MouseButton::Left, |_, _, _| {})
3857 .contained()
3858 .with_style(theme.workspace.modal)
3859 .aligned()
3860 .top()
3861 }))
3862 .with_children(self.render_notifications(&theme.workspace, cx)),
3863 ))
3864 .provide_resize_bounds::<WorkspaceBounds>()
3865 .flex(1.0, true),
3866 )
3867 .with_child(ChildView::new(&self.status_bar, cx))
3868 .contained()
3869 .with_background_color(theme.workspace.background),
3870 )
3871 .with_children(DragAndDrop::render(cx))
3872 .with_children(self.render_disconnected_overlay(cx))
3873 .into_any_named("workspace")
3874 }
3875
3876 fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
3877 if cx.is_self_focused() {
3878 cx.focus(&self.active_pane);
3879 }
3880 }
3881
3882 fn modifiers_changed(&mut self, e: &ModifiersChangedEvent, cx: &mut ViewContext<Self>) -> bool {
3883 DragAndDrop::<Workspace>::update_modifiers(e.modifiers, cx)
3884 }
3885}
3886
3887impl WorkspaceStore {
3888 pub fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
3889 Self {
3890 workspaces: Default::default(),
3891 followers: Default::default(),
3892 _subscriptions: vec![
3893 client.add_request_handler(cx.handle(), Self::handle_follow),
3894 client.add_message_handler(cx.handle(), Self::handle_unfollow),
3895 client.add_message_handler(cx.handle(), Self::handle_update_followers),
3896 ],
3897 client,
3898 }
3899 }
3900
3901 pub fn update_followers(
3902 &self,
3903 project_id: Option<u64>,
3904 update: proto::update_followers::Variant,
3905 cx: &AppContext,
3906 ) -> Option<()> {
3907 if !cx.has_global::<ModelHandle<ActiveCall>>() {
3908 return None;
3909 }
3910
3911 let room_id = ActiveCall::global(cx).read(cx).room()?.read(cx).id();
3912 let follower_ids: Vec<_> = self
3913 .followers
3914 .iter()
3915 .filter_map(|follower| {
3916 if follower.project_id == project_id || project_id.is_none() {
3917 Some(follower.peer_id.into())
3918 } else {
3919 None
3920 }
3921 })
3922 .collect();
3923 if follower_ids.is_empty() {
3924 return None;
3925 }
3926 self.client
3927 .send(proto::UpdateFollowers {
3928 room_id,
3929 project_id,
3930 follower_ids,
3931 variant: Some(update),
3932 })
3933 .log_err()
3934 }
3935
3936 async fn handle_follow(
3937 this: ModelHandle<Self>,
3938 envelope: TypedEnvelope<proto::Follow>,
3939 _: Arc<Client>,
3940 mut cx: AsyncAppContext,
3941 ) -> Result<proto::FollowResponse> {
3942 this.update(&mut cx, |this, cx| {
3943 let follower = Follower {
3944 project_id: envelope.payload.project_id,
3945 peer_id: envelope.original_sender_id()?,
3946 };
3947 let active_project = ActiveCall::global(cx)
3948 .read(cx)
3949 .location()
3950 .map(|project| project.id());
3951
3952 let mut response = proto::FollowResponse::default();
3953 for workspace in &this.workspaces {
3954 let Some(workspace) = workspace.upgrade(cx) else {
3955 continue;
3956 };
3957
3958 workspace.update(cx.as_mut(), |workspace, cx| {
3959 let handler_response = workspace.handle_follow(follower.project_id, cx);
3960 if response.views.is_empty() {
3961 response.views = handler_response.views;
3962 } else {
3963 response.views.extend_from_slice(&handler_response.views);
3964 }
3965
3966 if let Some(active_view_id) = handler_response.active_view_id.clone() {
3967 if response.active_view_id.is_none()
3968 || Some(workspace.project.id()) == active_project
3969 {
3970 response.active_view_id = Some(active_view_id);
3971 }
3972 }
3973 });
3974 }
3975
3976 if let Err(ix) = this.followers.binary_search(&follower) {
3977 this.followers.insert(ix, follower);
3978 }
3979
3980 Ok(response)
3981 })
3982 }
3983
3984 async fn handle_unfollow(
3985 this: ModelHandle<Self>,
3986 envelope: TypedEnvelope<proto::Unfollow>,
3987 _: Arc<Client>,
3988 mut cx: AsyncAppContext,
3989 ) -> Result<()> {
3990 this.update(&mut cx, |this, _| {
3991 let follower = Follower {
3992 project_id: envelope.payload.project_id,
3993 peer_id: envelope.original_sender_id()?,
3994 };
3995 if let Ok(ix) = this.followers.binary_search(&follower) {
3996 this.followers.remove(ix);
3997 }
3998 Ok(())
3999 })
4000 }
4001
4002 async fn handle_update_followers(
4003 this: ModelHandle<Self>,
4004 envelope: TypedEnvelope<proto::UpdateFollowers>,
4005 _: Arc<Client>,
4006 mut cx: AsyncAppContext,
4007 ) -> Result<()> {
4008 let leader_id = envelope.original_sender_id()?;
4009 let update = envelope.payload;
4010 this.update(&mut cx, |this, cx| {
4011 for workspace in &this.workspaces {
4012 let Some(workspace) = workspace.upgrade(cx) else {
4013 continue;
4014 };
4015 workspace.update(cx.as_mut(), |workspace, cx| {
4016 let project_id = workspace.project.read(cx).remote_id();
4017 if update.project_id != project_id && update.project_id.is_some() {
4018 return;
4019 }
4020 workspace.handle_update_followers(leader_id, update.clone(), cx);
4021 });
4022 }
4023 Ok(())
4024 })
4025 }
4026}
4027
4028impl Entity for WorkspaceStore {
4029 type Event = ();
4030}
4031
4032impl ViewId {
4033 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
4034 Ok(Self {
4035 creator: message
4036 .creator
4037 .ok_or_else(|| anyhow!("creator is missing"))?,
4038 id: message.id,
4039 })
4040 }
4041
4042 pub(crate) fn to_proto(&self) -> proto::ViewId {
4043 proto::ViewId {
4044 creator: Some(self.creator),
4045 id: self.id,
4046 }
4047 }
4048}
4049
4050pub trait WorkspaceHandle {
4051 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
4052}
4053
4054impl WorkspaceHandle for ViewHandle<Workspace> {
4055 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
4056 self.read(cx)
4057 .worktrees(cx)
4058 .flat_map(|worktree| {
4059 let worktree_id = worktree.read(cx).id();
4060 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
4061 worktree_id,
4062 path: f.path.clone(),
4063 })
4064 })
4065 .collect::<Vec<_>>()
4066 }
4067}
4068
4069impl std::fmt::Debug for OpenPaths {
4070 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4071 f.debug_struct("OpenPaths")
4072 .field("paths", &self.paths)
4073 .finish()
4074 }
4075}
4076
4077pub struct WorkspaceCreated(pub WeakViewHandle<Workspace>);
4078
4079pub fn activate_workspace_for_project(
4080 cx: &mut AsyncAppContext,
4081 predicate: impl Fn(&mut Project, &mut ModelContext<Project>) -> bool,
4082) -> Option<WeakViewHandle<Workspace>> {
4083 for window in cx.windows() {
4084 let handle = window
4085 .update(cx, |cx| {
4086 if let Some(workspace_handle) = cx.root_view().clone().downcast::<Workspace>() {
4087 let project = workspace_handle.read(cx).project.clone();
4088 if project.update(cx, &predicate) {
4089 cx.activate_window();
4090 return Some(workspace_handle.clone());
4091 }
4092 }
4093 None
4094 })
4095 .flatten();
4096
4097 if let Some(handle) = handle {
4098 return Some(handle.downgrade());
4099 }
4100 }
4101 None
4102}
4103
4104pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
4105 DB.last_workspace().await.log_err().flatten()
4106}
4107
4108#[allow(clippy::type_complexity)]
4109pub fn open_paths(
4110 abs_paths: &[PathBuf],
4111 app_state: &Arc<AppState>,
4112 requesting_window: Option<WindowHandle<Workspace>>,
4113 cx: &mut AppContext,
4114) -> Task<
4115 Result<(
4116 WeakViewHandle<Workspace>,
4117 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
4118 )>,
4119> {
4120 let app_state = app_state.clone();
4121 let abs_paths = abs_paths.to_vec();
4122 cx.spawn(|mut cx| async move {
4123 // Open paths in existing workspace if possible
4124 let existing = activate_workspace_for_project(&mut cx, |project, cx| {
4125 project.contains_paths(&abs_paths, cx)
4126 });
4127
4128 if let Some(existing) = existing {
4129 Ok((
4130 existing.clone(),
4131 existing
4132 .update(&mut cx, |workspace, cx| {
4133 workspace.open_paths(abs_paths, true, cx)
4134 })?
4135 .await,
4136 ))
4137 } else {
4138 Ok(cx
4139 .update(|cx| {
4140 Workspace::new_local(abs_paths, app_state.clone(), requesting_window, cx)
4141 })
4142 .await)
4143 }
4144 })
4145}
4146
4147pub fn open_new(
4148 app_state: &Arc<AppState>,
4149 cx: &mut AppContext,
4150 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static,
4151) -> Task<()> {
4152 let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx);
4153 cx.spawn(|mut cx| async move {
4154 let (workspace, opened_paths) = task.await;
4155
4156 workspace
4157 .update(&mut cx, |workspace, cx| {
4158 if opened_paths.is_empty() {
4159 init(workspace, cx)
4160 }
4161 })
4162 .log_err();
4163 })
4164}
4165
4166pub fn create_and_open_local_file(
4167 path: &'static Path,
4168 cx: &mut ViewContext<Workspace>,
4169 default_content: impl 'static + Send + FnOnce() -> Rope,
4170) -> Task<Result<Box<dyn ItemHandle>>> {
4171 cx.spawn(|workspace, mut cx| async move {
4172 let fs = workspace.read_with(&cx, |workspace, _| workspace.app_state().fs.clone())?;
4173 if !fs.is_file(path).await {
4174 fs.create_file(path, Default::default()).await?;
4175 fs.save(path, &default_content(), Default::default())
4176 .await?;
4177 }
4178
4179 let mut items = workspace
4180 .update(&mut cx, |workspace, cx| {
4181 workspace.with_local_workspace(cx, |workspace, cx| {
4182 workspace.open_paths(vec![path.to_path_buf()], false, cx)
4183 })
4184 })?
4185 .await?
4186 .await;
4187
4188 let item = items.pop().flatten();
4189 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
4190 })
4191}
4192
4193pub fn join_remote_project(
4194 project_id: u64,
4195 follow_user_id: u64,
4196 app_state: Arc<AppState>,
4197 cx: &mut AppContext,
4198) -> Task<Result<()>> {
4199 cx.spawn(|mut cx| async move {
4200 let existing_workspace = cx
4201 .windows()
4202 .into_iter()
4203 .find_map(|window| {
4204 window.downcast::<Workspace>().and_then(|window| {
4205 window.read_root_with(&cx, |workspace, cx| {
4206 if workspace.project().read(cx).remote_id() == Some(project_id) {
4207 Some(cx.handle().downgrade())
4208 } else {
4209 None
4210 }
4211 })
4212 })
4213 })
4214 .flatten();
4215
4216 let workspace = if let Some(existing_workspace) = existing_workspace {
4217 existing_workspace
4218 } else {
4219 let active_call = cx.read(ActiveCall::global);
4220 let room = active_call
4221 .read_with(&cx, |call, _| call.room().cloned())
4222 .ok_or_else(|| anyhow!("not in a call"))?;
4223 let project = room
4224 .update(&mut cx, |room, cx| {
4225 room.join_project(
4226 project_id,
4227 app_state.languages.clone(),
4228 app_state.fs.clone(),
4229 cx,
4230 )
4231 })
4232 .await?;
4233
4234 let window_bounds_override = window_bounds_env_override(&cx);
4235 let window = cx.add_window(
4236 (app_state.build_window_options)(
4237 window_bounds_override,
4238 None,
4239 cx.platform().as_ref(),
4240 ),
4241 |cx| Workspace::new(0, project, app_state.clone(), cx),
4242 );
4243 let workspace = window.root(&cx).unwrap();
4244 (app_state.initialize_workspace)(
4245 workspace.downgrade(),
4246 false,
4247 app_state.clone(),
4248 cx.clone(),
4249 )
4250 .await
4251 .log_err();
4252
4253 workspace.downgrade()
4254 };
4255
4256 workspace.window().activate(&mut cx);
4257 cx.platform().activate(true);
4258
4259 workspace.update(&mut cx, |workspace, cx| {
4260 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
4261 let follow_peer_id = room
4262 .read(cx)
4263 .remote_participants()
4264 .iter()
4265 .find(|(_, participant)| participant.user.id == follow_user_id)
4266 .map(|(_, p)| p.peer_id)
4267 .or_else(|| {
4268 // If we couldn't follow the given user, follow the host instead.
4269 let collaborator = workspace
4270 .project()
4271 .read(cx)
4272 .collaborators()
4273 .values()
4274 .find(|collaborator| collaborator.replica_id == 0)?;
4275 Some(collaborator.peer_id)
4276 });
4277
4278 if let Some(follow_peer_id) = follow_peer_id {
4279 if !workspace.is_being_followed(follow_peer_id) {
4280 workspace
4281 .toggle_follow(follow_peer_id, cx)
4282 .map(|follow| follow.detach_and_log_err(cx));
4283 }
4284 }
4285 }
4286 })?;
4287
4288 anyhow::Ok(())
4289 })
4290}
4291
4292pub fn restart(_: &Restart, cx: &mut AppContext) {
4293 let should_confirm = settings::get::<WorkspaceSettings>(cx).confirm_quit;
4294 cx.spawn(|mut cx| async move {
4295 let mut workspace_windows = cx
4296 .windows()
4297 .into_iter()
4298 .filter_map(|window| window.downcast::<Workspace>())
4299 .collect::<Vec<_>>();
4300
4301 // If multiple windows have unsaved changes, and need a save prompt,
4302 // prompt in the active window before switching to a different window.
4303 workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false));
4304
4305 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
4306 let answer = window.prompt(
4307 PromptLevel::Info,
4308 "Are you sure you want to restart?",
4309 &["Restart", "Cancel"],
4310 &mut cx,
4311 );
4312
4313 if let Some(mut answer) = answer {
4314 let answer = answer.next().await;
4315 if answer != Some(0) {
4316 return Ok(());
4317 }
4318 }
4319 }
4320
4321 // If the user cancels any save prompt, then keep the app open.
4322 for window in workspace_windows {
4323 if let Some(should_close) = window.update_root(&mut cx, |workspace, cx| {
4324 workspace.prepare_to_close(true, cx)
4325 }) {
4326 if !should_close.await? {
4327 return Ok(());
4328 }
4329 }
4330 }
4331 cx.platform().restart();
4332 anyhow::Ok(())
4333 })
4334 .detach_and_log_err(cx);
4335}
4336
4337fn parse_pixel_position_env_var(value: &str) -> Option<Vector2F> {
4338 let mut parts = value.split(',');
4339 let width: usize = parts.next()?.parse().ok()?;
4340 let height: usize = parts.next()?.parse().ok()?;
4341 Some(vec2f(width as f32, height as f32))
4342}
4343
4344#[cfg(test)]
4345mod tests {
4346 use super::*;
4347 use crate::{
4348 dock::test::{TestPanel, TestPanelEvent},
4349 item::test::{TestItem, TestItemEvent, TestProjectItem},
4350 };
4351 use fs::FakeFs;
4352 use gpui::{executor::Deterministic, test::EmptyView, TestAppContext};
4353 use project::{Project, ProjectEntryId};
4354 use serde_json::json;
4355 use settings::SettingsStore;
4356 use std::{cell::RefCell, rc::Rc};
4357
4358 #[gpui::test]
4359 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
4360 init_test(cx);
4361
4362 let fs = FakeFs::new(cx.background());
4363 let project = Project::test(fs, [], cx).await;
4364 let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
4365 let workspace = window.root(cx);
4366
4367 // Adding an item with no ambiguity renders the tab without detail.
4368 let item1 = window.add_view(cx, |_| {
4369 let mut item = TestItem::new();
4370 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
4371 item
4372 });
4373 workspace.update(cx, |workspace, cx| {
4374 workspace.add_item(Box::new(item1.clone()), cx);
4375 });
4376 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), None));
4377
4378 // Adding an item that creates ambiguity increases the level of detail on
4379 // both tabs.
4380 let item2 = window.add_view(cx, |_| {
4381 let mut item = TestItem::new();
4382 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
4383 item
4384 });
4385 workspace.update(cx, |workspace, cx| {
4386 workspace.add_item(Box::new(item2.clone()), cx);
4387 });
4388 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4389 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4390
4391 // Adding an item that creates ambiguity increases the level of detail only
4392 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
4393 // we stop at the highest detail available.
4394 let item3 = window.add_view(cx, |_| {
4395 let mut item = TestItem::new();
4396 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
4397 item
4398 });
4399 workspace.update(cx, |workspace, cx| {
4400 workspace.add_item(Box::new(item3.clone()), cx);
4401 });
4402 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4403 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
4404 item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
4405 }
4406
4407 #[gpui::test]
4408 async fn test_tracking_active_path(cx: &mut TestAppContext) {
4409 init_test(cx);
4410
4411 let fs = FakeFs::new(cx.background());
4412 fs.insert_tree(
4413 "/root1",
4414 json!({
4415 "one.txt": "",
4416 "two.txt": "",
4417 }),
4418 )
4419 .await;
4420 fs.insert_tree(
4421 "/root2",
4422 json!({
4423 "three.txt": "",
4424 }),
4425 )
4426 .await;
4427
4428 let project = Project::test(fs, ["root1".as_ref()], cx).await;
4429 let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
4430 let workspace = window.root(cx);
4431 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4432 let worktree_id = project.read_with(cx, |project, cx| {
4433 project.worktrees(cx).next().unwrap().read(cx).id()
4434 });
4435
4436 let item1 = window.add_view(cx, |cx| {
4437 TestItem::new().with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
4438 });
4439 let item2 = window.add_view(cx, |cx| {
4440 TestItem::new().with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
4441 });
4442
4443 // Add an item to an empty pane
4444 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
4445 project.read_with(cx, |project, cx| {
4446 assert_eq!(
4447 project.active_entry(),
4448 project
4449 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
4450 .map(|e| e.id)
4451 );
4452 });
4453 assert_eq!(window.current_title(cx).as_deref(), Some("one.txt β root1"));
4454
4455 // Add a second item to a non-empty pane
4456 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
4457 assert_eq!(window.current_title(cx).as_deref(), Some("two.txt β root1"));
4458 project.read_with(cx, |project, cx| {
4459 assert_eq!(
4460 project.active_entry(),
4461 project
4462 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
4463 .map(|e| e.id)
4464 );
4465 });
4466
4467 // Close the active item
4468 pane.update(cx, |pane, cx| {
4469 pane.close_active_item(&Default::default(), cx).unwrap()
4470 })
4471 .await
4472 .unwrap();
4473 assert_eq!(window.current_title(cx).as_deref(), Some("one.txt β root1"));
4474 project.read_with(cx, |project, cx| {
4475 assert_eq!(
4476 project.active_entry(),
4477 project
4478 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
4479 .map(|e| e.id)
4480 );
4481 });
4482
4483 // Add a project folder
4484 project
4485 .update(cx, |project, cx| {
4486 project.find_or_create_local_worktree("/root2", true, cx)
4487 })
4488 .await
4489 .unwrap();
4490 assert_eq!(
4491 window.current_title(cx).as_deref(),
4492 Some("one.txt β root1, root2")
4493 );
4494
4495 // Remove a project folder
4496 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
4497 assert_eq!(window.current_title(cx).as_deref(), Some("one.txt β root2"));
4498 }
4499
4500 #[gpui::test]
4501 async fn test_close_window(cx: &mut TestAppContext) {
4502 init_test(cx);
4503
4504 let fs = FakeFs::new(cx.background());
4505 fs.insert_tree("/root", json!({ "one": "" })).await;
4506
4507 let project = Project::test(fs, ["root".as_ref()], cx).await;
4508 let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
4509 let workspace = window.root(cx);
4510
4511 // When there are no dirty items, there's nothing to do.
4512 let item1 = window.add_view(cx, |_| TestItem::new());
4513 workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
4514 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4515 assert!(task.await.unwrap());
4516
4517 // When there are dirty untitled items, prompt to save each one. If the user
4518 // cancels any prompt, then abort.
4519 let item2 = window.add_view(cx, |_| TestItem::new().with_dirty(true));
4520 let item3 = window.add_view(cx, |cx| {
4521 TestItem::new()
4522 .with_dirty(true)
4523 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4524 });
4525 workspace.update(cx, |w, cx| {
4526 w.add_item(Box::new(item2.clone()), cx);
4527 w.add_item(Box::new(item3.clone()), cx);
4528 });
4529 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4530 cx.foreground().run_until_parked();
4531 window.simulate_prompt_answer(2, cx); // cancel save all
4532 cx.foreground().run_until_parked();
4533 window.simulate_prompt_answer(2, cx); // cancel save all
4534 cx.foreground().run_until_parked();
4535 assert!(!window.has_pending_prompt(cx));
4536 assert!(!task.await.unwrap());
4537 }
4538
4539 #[gpui::test]
4540 async fn test_close_pane_items(cx: &mut TestAppContext) {
4541 init_test(cx);
4542
4543 let fs = FakeFs::new(cx.background());
4544
4545 let project = Project::test(fs, None, cx).await;
4546 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
4547 let workspace = window.root(cx);
4548
4549 let item1 = window.add_view(cx, |cx| {
4550 TestItem::new()
4551 .with_dirty(true)
4552 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4553 });
4554 let item2 = window.add_view(cx, |cx| {
4555 TestItem::new()
4556 .with_dirty(true)
4557 .with_conflict(true)
4558 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
4559 });
4560 let item3 = window.add_view(cx, |cx| {
4561 TestItem::new()
4562 .with_dirty(true)
4563 .with_conflict(true)
4564 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
4565 });
4566 let item4 = window.add_view(cx, |cx| {
4567 TestItem::new()
4568 .with_dirty(true)
4569 .with_project_items(&[TestProjectItem::new_untitled(cx)])
4570 });
4571 let pane = workspace.update(cx, |workspace, cx| {
4572 workspace.add_item(Box::new(item1.clone()), cx);
4573 workspace.add_item(Box::new(item2.clone()), cx);
4574 workspace.add_item(Box::new(item3.clone()), cx);
4575 workspace.add_item(Box::new(item4.clone()), cx);
4576 workspace.active_pane().clone()
4577 });
4578
4579 let close_items = pane.update(cx, |pane, cx| {
4580 pane.activate_item(1, true, true, cx);
4581 assert_eq!(pane.active_item().unwrap().id(), item2.id());
4582 let item1_id = item1.id();
4583 let item3_id = item3.id();
4584 let item4_id = item4.id();
4585 pane.close_items(cx, SaveIntent::Close, move |id| {
4586 [item1_id, item3_id, item4_id].contains(&id)
4587 })
4588 });
4589 cx.foreground().run_until_parked();
4590
4591 assert!(window.has_pending_prompt(cx));
4592 // Ignore "Save all" prompt
4593 window.simulate_prompt_answer(2, cx);
4594 cx.foreground().run_until_parked();
4595 // There's a prompt to save item 1.
4596 pane.read_with(cx, |pane, _| {
4597 assert_eq!(pane.items_len(), 4);
4598 assert_eq!(pane.active_item().unwrap().id(), item1.id());
4599 });
4600 // Confirm saving item 1.
4601 window.simulate_prompt_answer(0, cx);
4602 cx.foreground().run_until_parked();
4603
4604 // Item 1 is saved. There's a prompt to save item 3.
4605 pane.read_with(cx, |pane, cx| {
4606 assert_eq!(item1.read(cx).save_count, 1);
4607 assert_eq!(item1.read(cx).save_as_count, 0);
4608 assert_eq!(item1.read(cx).reload_count, 0);
4609 assert_eq!(pane.items_len(), 3);
4610 assert_eq!(pane.active_item().unwrap().id(), item3.id());
4611 });
4612 assert!(window.has_pending_prompt(cx));
4613
4614 // Cancel saving item 3.
4615 window.simulate_prompt_answer(1, cx);
4616 cx.foreground().run_until_parked();
4617
4618 // Item 3 is reloaded. There's a prompt to save item 4.
4619 pane.read_with(cx, |pane, cx| {
4620 assert_eq!(item3.read(cx).save_count, 0);
4621 assert_eq!(item3.read(cx).save_as_count, 0);
4622 assert_eq!(item3.read(cx).reload_count, 1);
4623 assert_eq!(pane.items_len(), 2);
4624 assert_eq!(pane.active_item().unwrap().id(), item4.id());
4625 });
4626 assert!(window.has_pending_prompt(cx));
4627
4628 // Confirm saving item 4.
4629 window.simulate_prompt_answer(0, cx);
4630 cx.foreground().run_until_parked();
4631
4632 // There's a prompt for a path for item 4.
4633 cx.simulate_new_path_selection(|_| Some(Default::default()));
4634 close_items.await.unwrap();
4635
4636 // The requested items are closed.
4637 pane.read_with(cx, |pane, cx| {
4638 assert_eq!(item4.read(cx).save_count, 0);
4639 assert_eq!(item4.read(cx).save_as_count, 1);
4640 assert_eq!(item4.read(cx).reload_count, 0);
4641 assert_eq!(pane.items_len(), 1);
4642 assert_eq!(pane.active_item().unwrap().id(), item2.id());
4643 });
4644 }
4645
4646 #[gpui::test]
4647 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
4648 init_test(cx);
4649
4650 let fs = FakeFs::new(cx.background());
4651
4652 let project = Project::test(fs, [], cx).await;
4653 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
4654 let workspace = window.root(cx);
4655
4656 // Create several workspace items with single project entries, and two
4657 // workspace items with multiple project entries.
4658 let single_entry_items = (0..=4)
4659 .map(|project_entry_id| {
4660 window.add_view(cx, |cx| {
4661 TestItem::new()
4662 .with_dirty(true)
4663 .with_project_items(&[TestProjectItem::new(
4664 project_entry_id,
4665 &format!("{project_entry_id}.txt"),
4666 cx,
4667 )])
4668 })
4669 })
4670 .collect::<Vec<_>>();
4671 let item_2_3 = window.add_view(cx, |cx| {
4672 TestItem::new()
4673 .with_dirty(true)
4674 .with_singleton(false)
4675 .with_project_items(&[
4676 single_entry_items[2].read(cx).project_items[0].clone(),
4677 single_entry_items[3].read(cx).project_items[0].clone(),
4678 ])
4679 });
4680 let item_3_4 = window.add_view(cx, |cx| {
4681 TestItem::new()
4682 .with_dirty(true)
4683 .with_singleton(false)
4684 .with_project_items(&[
4685 single_entry_items[3].read(cx).project_items[0].clone(),
4686 single_entry_items[4].read(cx).project_items[0].clone(),
4687 ])
4688 });
4689
4690 // Create two panes that contain the following project entries:
4691 // left pane:
4692 // multi-entry items: (2, 3)
4693 // single-entry items: 0, 1, 2, 3, 4
4694 // right pane:
4695 // single-entry items: 1
4696 // multi-entry items: (3, 4)
4697 let left_pane = workspace.update(cx, |workspace, cx| {
4698 let left_pane = workspace.active_pane().clone();
4699 workspace.add_item(Box::new(item_2_3.clone()), cx);
4700 for item in single_entry_items {
4701 workspace.add_item(Box::new(item), cx);
4702 }
4703 left_pane.update(cx, |pane, cx| {
4704 pane.activate_item(2, true, true, cx);
4705 });
4706
4707 workspace
4708 .split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
4709 .unwrap();
4710
4711 left_pane
4712 });
4713
4714 //Need to cause an effect flush in order to respect new focus
4715 workspace.update(cx, |workspace, cx| {
4716 workspace.add_item(Box::new(item_3_4.clone()), cx);
4717 cx.focus(&left_pane);
4718 });
4719
4720 // When closing all of the items in the left pane, we should be prompted twice:
4721 // once for project entry 0, and once for project entry 2. After those two
4722 // prompts, the task should complete.
4723
4724 let close = left_pane.update(cx, |pane, cx| {
4725 pane.close_items(cx, SaveIntent::Close, move |_| true)
4726 });
4727 cx.foreground().run_until_parked();
4728 // Discard "Save all" prompt
4729 window.simulate_prompt_answer(2, cx);
4730
4731 cx.foreground().run_until_parked();
4732 left_pane.read_with(cx, |pane, cx| {
4733 assert_eq!(
4734 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
4735 &[ProjectEntryId::from_proto(0)]
4736 );
4737 });
4738 window.simulate_prompt_answer(0, cx);
4739
4740 cx.foreground().run_until_parked();
4741 left_pane.read_with(cx, |pane, cx| {
4742 assert_eq!(
4743 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
4744 &[ProjectEntryId::from_proto(2)]
4745 );
4746 });
4747 window.simulate_prompt_answer(0, cx);
4748
4749 cx.foreground().run_until_parked();
4750 close.await.unwrap();
4751 left_pane.read_with(cx, |pane, _| {
4752 assert_eq!(pane.items_len(), 0);
4753 });
4754 }
4755
4756 #[gpui::test]
4757 async fn test_autosave(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
4758 init_test(cx);
4759
4760 let fs = FakeFs::new(cx.background());
4761
4762 let project = Project::test(fs, [], cx).await;
4763 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
4764 let workspace = window.root(cx);
4765 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4766
4767 let item = window.add_view(cx, |cx| {
4768 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4769 });
4770 let item_id = item.id();
4771 workspace.update(cx, |workspace, cx| {
4772 workspace.add_item(Box::new(item.clone()), cx);
4773 });
4774
4775 // Autosave on window change.
4776 item.update(cx, |item, cx| {
4777 cx.update_global(|settings: &mut SettingsStore, cx| {
4778 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4779 settings.autosave = Some(AutosaveSetting::OnWindowChange);
4780 })
4781 });
4782 item.is_dirty = true;
4783 });
4784
4785 // Deactivating the window saves the file.
4786 window.simulate_deactivation(cx);
4787 deterministic.run_until_parked();
4788 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
4789
4790 // Autosave on focus change.
4791 item.update(cx, |item, cx| {
4792 cx.focus_self();
4793 cx.update_global(|settings: &mut SettingsStore, cx| {
4794 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4795 settings.autosave = Some(AutosaveSetting::OnFocusChange);
4796 })
4797 });
4798 item.is_dirty = true;
4799 });
4800
4801 // Blurring the item saves the file.
4802 item.update(cx, |_, cx| cx.blur());
4803 deterministic.run_until_parked();
4804 item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
4805
4806 // Deactivating the window still saves the file.
4807 window.simulate_activation(cx);
4808 item.update(cx, |item, cx| {
4809 cx.focus_self();
4810 item.is_dirty = true;
4811 });
4812 window.simulate_deactivation(cx);
4813
4814 deterministic.run_until_parked();
4815 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
4816
4817 // Autosave after delay.
4818 item.update(cx, |item, cx| {
4819 cx.update_global(|settings: &mut SettingsStore, cx| {
4820 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4821 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
4822 })
4823 });
4824 item.is_dirty = true;
4825 cx.emit(TestItemEvent::Edit);
4826 });
4827
4828 // Delay hasn't fully expired, so the file is still dirty and unsaved.
4829 deterministic.advance_clock(Duration::from_millis(250));
4830 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
4831
4832 // After delay expires, the file is saved.
4833 deterministic.advance_clock(Duration::from_millis(250));
4834 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
4835
4836 // Autosave on focus change, ensuring closing the tab counts as such.
4837 item.update(cx, |item, cx| {
4838 cx.update_global(|settings: &mut SettingsStore, cx| {
4839 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4840 settings.autosave = Some(AutosaveSetting::OnFocusChange);
4841 })
4842 });
4843 item.is_dirty = true;
4844 });
4845
4846 pane.update(cx, |pane, cx| {
4847 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
4848 })
4849 .await
4850 .unwrap();
4851 assert!(!window.has_pending_prompt(cx));
4852 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
4853
4854 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
4855 workspace.update(cx, |workspace, cx| {
4856 workspace.add_item(Box::new(item.clone()), cx);
4857 });
4858 item.update(cx, |item, cx| {
4859 item.project_items[0].update(cx, |item, _| {
4860 item.entry_id = None;
4861 });
4862 item.is_dirty = true;
4863 cx.blur();
4864 });
4865 deterministic.run_until_parked();
4866 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
4867
4868 // Ensure autosave is prevented for deleted files also when closing the buffer.
4869 let _close_items = pane.update(cx, |pane, cx| {
4870 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
4871 });
4872 deterministic.run_until_parked();
4873 assert!(window.has_pending_prompt(cx));
4874 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
4875 }
4876
4877 #[gpui::test]
4878 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
4879 init_test(cx);
4880
4881 let fs = FakeFs::new(cx.background());
4882
4883 let project = Project::test(fs, [], cx).await;
4884 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
4885 let workspace = window.root(cx);
4886
4887 let item = window.add_view(cx, |cx| {
4888 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4889 });
4890 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4891 let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
4892 let toolbar_notify_count = Rc::new(RefCell::new(0));
4893
4894 workspace.update(cx, |workspace, cx| {
4895 workspace.add_item(Box::new(item.clone()), cx);
4896 let toolbar_notification_count = toolbar_notify_count.clone();
4897 cx.observe(&toolbar, move |_, _, _| {
4898 *toolbar_notification_count.borrow_mut() += 1
4899 })
4900 .detach();
4901 });
4902
4903 pane.read_with(cx, |pane, _| {
4904 assert!(!pane.can_navigate_backward());
4905 assert!(!pane.can_navigate_forward());
4906 });
4907
4908 item.update(cx, |item, cx| {
4909 item.set_state("one".to_string(), cx);
4910 });
4911
4912 // Toolbar must be notified to re-render the navigation buttons
4913 assert_eq!(*toolbar_notify_count.borrow(), 1);
4914
4915 pane.read_with(cx, |pane, _| {
4916 assert!(pane.can_navigate_backward());
4917 assert!(!pane.can_navigate_forward());
4918 });
4919
4920 workspace
4921 .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
4922 .await
4923 .unwrap();
4924
4925 assert_eq!(*toolbar_notify_count.borrow(), 3);
4926 pane.read_with(cx, |pane, _| {
4927 assert!(!pane.can_navigate_backward());
4928 assert!(pane.can_navigate_forward());
4929 });
4930 }
4931
4932 #[gpui::test]
4933 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
4934 init_test(cx);
4935 let fs = FakeFs::new(cx.background());
4936
4937 let project = Project::test(fs, [], cx).await;
4938 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
4939 let workspace = window.root(cx);
4940
4941 let panel = workspace.update(cx, |workspace, cx| {
4942 let panel = cx.add_view(|_| TestPanel::new(DockPosition::Right));
4943 workspace.add_panel(panel.clone(), cx);
4944
4945 workspace
4946 .right_dock()
4947 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
4948
4949 panel
4950 });
4951
4952 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4953 pane.update(cx, |pane, cx| {
4954 let item = cx.add_view(|_| TestItem::new());
4955 pane.add_item(Box::new(item), true, true, None, cx);
4956 });
4957
4958 // Transfer focus from center to panel
4959 workspace.update(cx, |workspace, cx| {
4960 workspace.toggle_panel_focus::<TestPanel>(cx);
4961 });
4962
4963 workspace.read_with(cx, |workspace, cx| {
4964 assert!(workspace.right_dock().read(cx).is_open());
4965 assert!(!panel.is_zoomed(cx));
4966 assert!(panel.has_focus(cx));
4967 });
4968
4969 // Transfer focus from panel to center
4970 workspace.update(cx, |workspace, cx| {
4971 workspace.toggle_panel_focus::<TestPanel>(cx);
4972 });
4973
4974 workspace.read_with(cx, |workspace, cx| {
4975 assert!(workspace.right_dock().read(cx).is_open());
4976 assert!(!panel.is_zoomed(cx));
4977 assert!(!panel.has_focus(cx));
4978 });
4979
4980 // Close the dock
4981 workspace.update(cx, |workspace, cx| {
4982 workspace.toggle_dock(DockPosition::Right, cx);
4983 });
4984
4985 workspace.read_with(cx, |workspace, cx| {
4986 assert!(!workspace.right_dock().read(cx).is_open());
4987 assert!(!panel.is_zoomed(cx));
4988 assert!(!panel.has_focus(cx));
4989 });
4990
4991 // Open the dock
4992 workspace.update(cx, |workspace, cx| {
4993 workspace.toggle_dock(DockPosition::Right, cx);
4994 });
4995
4996 workspace.read_with(cx, |workspace, cx| {
4997 assert!(workspace.right_dock().read(cx).is_open());
4998 assert!(!panel.is_zoomed(cx));
4999 assert!(panel.has_focus(cx));
5000 });
5001
5002 // Focus and zoom panel
5003 panel.update(cx, |panel, cx| {
5004 cx.focus_self();
5005 panel.set_zoomed(true, cx)
5006 });
5007
5008 workspace.read_with(cx, |workspace, cx| {
5009 assert!(workspace.right_dock().read(cx).is_open());
5010 assert!(panel.is_zoomed(cx));
5011 assert!(panel.has_focus(cx));
5012 });
5013
5014 // Transfer focus to the center closes the dock
5015 workspace.update(cx, |workspace, cx| {
5016 workspace.toggle_panel_focus::<TestPanel>(cx);
5017 });
5018
5019 workspace.read_with(cx, |workspace, cx| {
5020 assert!(!workspace.right_dock().read(cx).is_open());
5021 assert!(panel.is_zoomed(cx));
5022 assert!(!panel.has_focus(cx));
5023 });
5024
5025 // Transferring focus back to the panel keeps it zoomed
5026 workspace.update(cx, |workspace, cx| {
5027 workspace.toggle_panel_focus::<TestPanel>(cx);
5028 });
5029
5030 workspace.read_with(cx, |workspace, cx| {
5031 assert!(workspace.right_dock().read(cx).is_open());
5032 assert!(panel.is_zoomed(cx));
5033 assert!(panel.has_focus(cx));
5034 });
5035
5036 // Close the dock while it is zoomed
5037 workspace.update(cx, |workspace, cx| {
5038 workspace.toggle_dock(DockPosition::Right, cx)
5039 });
5040
5041 workspace.read_with(cx, |workspace, cx| {
5042 assert!(!workspace.right_dock().read(cx).is_open());
5043 assert!(panel.is_zoomed(cx));
5044 assert!(workspace.zoomed.is_none());
5045 assert!(!panel.has_focus(cx));
5046 });
5047
5048 // Opening the dock, when it's zoomed, retains focus
5049 workspace.update(cx, |workspace, cx| {
5050 workspace.toggle_dock(DockPosition::Right, cx)
5051 });
5052
5053 workspace.read_with(cx, |workspace, cx| {
5054 assert!(workspace.right_dock().read(cx).is_open());
5055 assert!(panel.is_zoomed(cx));
5056 assert!(workspace.zoomed.is_some());
5057 assert!(panel.has_focus(cx));
5058 });
5059
5060 // Unzoom and close the panel, zoom the active pane.
5061 panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
5062 workspace.update(cx, |workspace, cx| {
5063 workspace.toggle_dock(DockPosition::Right, cx)
5064 });
5065 pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
5066
5067 // Opening a dock unzooms the pane.
5068 workspace.update(cx, |workspace, cx| {
5069 workspace.toggle_dock(DockPosition::Right, cx)
5070 });
5071 workspace.read_with(cx, |workspace, cx| {
5072 let pane = pane.read(cx);
5073 assert!(!pane.is_zoomed());
5074 assert!(!pane.has_focus());
5075 assert!(workspace.right_dock().read(cx).is_open());
5076 assert!(workspace.zoomed.is_none());
5077 });
5078 }
5079
5080 #[gpui::test]
5081 async fn test_panels(cx: &mut gpui::TestAppContext) {
5082 init_test(cx);
5083 let fs = FakeFs::new(cx.background());
5084
5085 let project = Project::test(fs, [], cx).await;
5086 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
5087 let workspace = window.root(cx);
5088
5089 let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
5090 // Add panel_1 on the left, panel_2 on the right.
5091 let panel_1 = cx.add_view(|_| TestPanel::new(DockPosition::Left));
5092 workspace.add_panel(panel_1.clone(), cx);
5093 workspace
5094 .left_dock()
5095 .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
5096 let panel_2 = cx.add_view(|_| TestPanel::new(DockPosition::Right));
5097 workspace.add_panel(panel_2.clone(), cx);
5098 workspace
5099 .right_dock()
5100 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
5101
5102 let left_dock = workspace.left_dock();
5103 assert_eq!(
5104 left_dock.read(cx).visible_panel().unwrap().id(),
5105 panel_1.id()
5106 );
5107 assert_eq!(
5108 left_dock.read(cx).active_panel_size(cx).unwrap(),
5109 panel_1.size(cx)
5110 );
5111
5112 left_dock.update(cx, |left_dock, cx| {
5113 left_dock.resize_active_panel(Some(1337.), cx)
5114 });
5115 assert_eq!(
5116 workspace
5117 .right_dock()
5118 .read(cx)
5119 .visible_panel()
5120 .unwrap()
5121 .id(),
5122 panel_2.id()
5123 );
5124
5125 (panel_1, panel_2)
5126 });
5127
5128 // Move panel_1 to the right
5129 panel_1.update(cx, |panel_1, cx| {
5130 panel_1.set_position(DockPosition::Right, cx)
5131 });
5132
5133 workspace.update(cx, |workspace, cx| {
5134 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
5135 // Since it was the only panel on the left, the left dock should now be closed.
5136 assert!(!workspace.left_dock().read(cx).is_open());
5137 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
5138 let right_dock = workspace.right_dock();
5139 assert_eq!(
5140 right_dock.read(cx).visible_panel().unwrap().id(),
5141 panel_1.id()
5142 );
5143 assert_eq!(right_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
5144
5145 // Now we move panel_2Β to the left
5146 panel_2.set_position(DockPosition::Left, cx);
5147 });
5148
5149 workspace.update(cx, |workspace, cx| {
5150 // Since panel_2 was not visible on the right, we don't open the left dock.
5151 assert!(!workspace.left_dock().read(cx).is_open());
5152 // And the right dock is unaffected in it's displaying of panel_1
5153 assert!(workspace.right_dock().read(cx).is_open());
5154 assert_eq!(
5155 workspace
5156 .right_dock()
5157 .read(cx)
5158 .visible_panel()
5159 .unwrap()
5160 .id(),
5161 panel_1.id()
5162 );
5163 });
5164
5165 // Move panel_1 back to the left
5166 panel_1.update(cx, |panel_1, cx| {
5167 panel_1.set_position(DockPosition::Left, cx)
5168 });
5169
5170 workspace.update(cx, |workspace, cx| {
5171 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
5172 let left_dock = workspace.left_dock();
5173 assert!(left_dock.read(cx).is_open());
5174 assert_eq!(
5175 left_dock.read(cx).visible_panel().unwrap().id(),
5176 panel_1.id()
5177 );
5178 assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
5179 // And right the dock should be closed as it no longer has any panels.
5180 assert!(!workspace.right_dock().read(cx).is_open());
5181
5182 // Now we move panel_1 to the bottom
5183 panel_1.set_position(DockPosition::Bottom, cx);
5184 });
5185
5186 workspace.update(cx, |workspace, cx| {
5187 // Since panel_1 was visible on the left, we close the left dock.
5188 assert!(!workspace.left_dock().read(cx).is_open());
5189 // The bottom dock is sized based on the panel's default size,
5190 // since the panel orientation changed from vertical to horizontal.
5191 let bottom_dock = workspace.bottom_dock();
5192 assert_eq!(
5193 bottom_dock.read(cx).active_panel_size(cx).unwrap(),
5194 panel_1.size(cx),
5195 );
5196 // Close bottom dock and move panel_1 back to the left.
5197 bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
5198 panel_1.set_position(DockPosition::Left, cx);
5199 });
5200
5201 // Emit activated event on panel 1
5202 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Activated));
5203
5204 // Now the left dock is open and panel_1 is active and focused.
5205 workspace.read_with(cx, |workspace, cx| {
5206 let left_dock = workspace.left_dock();
5207 assert!(left_dock.read(cx).is_open());
5208 assert_eq!(
5209 left_dock.read(cx).visible_panel().unwrap().id(),
5210 panel_1.id()
5211 );
5212 assert!(panel_1.is_focused(cx));
5213 });
5214
5215 // Emit closed event on panel 2, which is not active
5216 panel_2.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed));
5217
5218 // Wo don't close the left dock, because panel_2 wasn't the active panel
5219 workspace.read_with(cx, |workspace, cx| {
5220 let left_dock = workspace.left_dock();
5221 assert!(left_dock.read(cx).is_open());
5222 assert_eq!(
5223 left_dock.read(cx).visible_panel().unwrap().id(),
5224 panel_1.id()
5225 );
5226 });
5227
5228 // Emitting a ZoomIn event shows the panel as zoomed.
5229 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomIn));
5230 workspace.read_with(cx, |workspace, _| {
5231 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
5232 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
5233 });
5234
5235 // Move panel to another dock while it is zoomed
5236 panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
5237 workspace.read_with(cx, |workspace, _| {
5238 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
5239 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5240 });
5241
5242 // If focus is transferred to another view that's not a panel or another pane, we still show
5243 // the panel as zoomed.
5244 let focus_receiver = window.add_view(cx, |_| EmptyView);
5245 focus_receiver.update(cx, |_, cx| cx.focus_self());
5246 workspace.read_with(cx, |workspace, _| {
5247 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
5248 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5249 });
5250
5251 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
5252 workspace.update(cx, |_, cx| cx.focus_self());
5253 workspace.read_with(cx, |workspace, _| {
5254 assert_eq!(workspace.zoomed, None);
5255 assert_eq!(workspace.zoomed_position, None);
5256 });
5257
5258 // If focus is transferred again to another view that's not a panel or a pane, we won't
5259 // show the panel as zoomed because it wasn't zoomed before.
5260 focus_receiver.update(cx, |_, cx| cx.focus_self());
5261 workspace.read_with(cx, |workspace, _| {
5262 assert_eq!(workspace.zoomed, None);
5263 assert_eq!(workspace.zoomed_position, None);
5264 });
5265
5266 // When focus is transferred back to the panel, it is zoomed again.
5267 panel_1.update(cx, |_, cx| cx.focus_self());
5268 workspace.read_with(cx, |workspace, _| {
5269 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
5270 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5271 });
5272
5273 // Emitting a ZoomOut event unzooms the panel.
5274 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomOut));
5275 workspace.read_with(cx, |workspace, _| {
5276 assert_eq!(workspace.zoomed, None);
5277 assert_eq!(workspace.zoomed_position, None);
5278 });
5279
5280 // Emit closed event on panel 1, which is active
5281 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed));
5282
5283 // Now the left dock is closed, because panel_1 was the active panel
5284 workspace.read_with(cx, |workspace, cx| {
5285 let right_dock = workspace.right_dock();
5286 assert!(!right_dock.read(cx).is_open());
5287 });
5288 }
5289
5290 pub fn init_test(cx: &mut TestAppContext) {
5291 cx.foreground().forbid_parking();
5292 cx.update(|cx| {
5293 cx.set_global(SettingsStore::test(cx));
5294 theme::init((), cx);
5295 language::init(cx);
5296 crate::init_settings(cx);
5297 Project::init_settings(cx);
5298 });
5299 }
5300}