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