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