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