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