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