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