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