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