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