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