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