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