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