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