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