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