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