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