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