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