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