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