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 cx.focus(active_panel.as_any());
1602 reveal_dock = true;
1603 }
1604 }
1605 });
1606
1607 if reveal_dock {
1608 self.dismiss_zoomed_items_to_reveal(Some(dock_side), cx);
1609 }
1610
1611 if focus_center {
1612 cx.focus_self();
1613 }
1614
1615 cx.notify();
1616 self.serialize_workspace(cx);
1617 }
1618
1619 /// Transfer focus to the panel of the given type.
1620 pub fn focus_panel<T: Panel>(&mut self, cx: &mut ViewContext<Self>) -> Option<ViewHandle<T>> {
1621 self.focus_or_unfocus_panel::<T>(cx, |_, _| true)?
1622 .as_any()
1623 .clone()
1624 .downcast()
1625 }
1626
1627 /// Focus the panel of the given type if it isn't already focused. If it is
1628 /// already focused, then transfer focus back to the workspace center.
1629 pub fn toggle_panel_focus<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
1630 self.focus_or_unfocus_panel::<T>(cx, |panel, cx| !panel.has_focus(cx));
1631 }
1632
1633 /// Focus or unfocus the given panel type, depending on the given callback.
1634 fn focus_or_unfocus_panel<T: Panel>(
1635 &mut self,
1636 cx: &mut ViewContext<Self>,
1637 should_focus: impl Fn(&dyn PanelHandle, &mut ViewContext<Dock>) -> bool,
1638 ) -> Option<Rc<dyn PanelHandle>> {
1639 for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
1640 if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
1641 let mut focus_center = false;
1642 let mut reveal_dock = false;
1643 let panel = dock.update(cx, |dock, cx| {
1644 dock.activate_panel(panel_index, cx);
1645
1646 let panel = dock.active_panel().cloned();
1647 if let Some(panel) = panel.as_ref() {
1648 if should_focus(&**panel, cx) {
1649 dock.set_open(true, cx);
1650 cx.focus(panel.as_any());
1651 reveal_dock = true;
1652 } else {
1653 // if panel.is_zoomed(cx) {
1654 // dock.set_open(false, cx);
1655 // }
1656 focus_center = true;
1657 }
1658 }
1659 panel
1660 });
1661
1662 if focus_center {
1663 cx.focus_self();
1664 }
1665
1666 self.serialize_workspace(cx);
1667 cx.notify();
1668 return panel;
1669 }
1670 }
1671 None
1672 }
1673
1674 pub fn panel<T: Panel>(&self, cx: &WindowContext) -> Option<ViewHandle<T>> {
1675 for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
1676 let dock = dock.read(cx);
1677 if let Some(panel) = dock.panel::<T>() {
1678 return Some(panel);
1679 }
1680 }
1681 None
1682 }
1683
1684 fn zoom_out(&mut self, cx: &mut ViewContext<Self>) {
1685 for pane in &self.panes {
1686 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
1687 }
1688
1689 self.left_dock.update(cx, |dock, cx| dock.zoom_out(cx));
1690 self.bottom_dock.update(cx, |dock, cx| dock.zoom_out(cx));
1691 self.right_dock.update(cx, |dock, cx| dock.zoom_out(cx));
1692 self.zoomed = None;
1693 self.zoomed_position = None;
1694
1695 cx.notify();
1696 }
1697
1698 fn dismiss_zoomed_items_to_reveal(
1699 &mut self,
1700 dock_to_reveal: Option<DockPosition>,
1701 cx: &mut ViewContext<Self>,
1702 ) {
1703 // If a center pane is zoomed, unzoom it.
1704 for pane in &self.panes {
1705 if pane != &self.active_pane || dock_to_reveal.is_some() {
1706 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
1707 }
1708 }
1709
1710 // If another dock is zoomed, hide it.
1711 let mut focus_center = false;
1712 for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] {
1713 dock.update(cx, |dock, cx| {
1714 if Some(dock.position()) != dock_to_reveal {
1715 if let Some(panel) = dock.active_panel() {
1716 if panel.is_zoomed(cx) {
1717 focus_center |= panel.has_focus(cx);
1718 dock.set_open(false, cx);
1719 }
1720 }
1721 }
1722 });
1723 }
1724
1725 if focus_center {
1726 cx.focus_self();
1727 }
1728
1729 if self.zoomed_position != dock_to_reveal {
1730 self.zoomed = None;
1731 self.zoomed_position = None;
1732 }
1733
1734 cx.notify();
1735 }
1736
1737 fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
1738 let pane = cx.add_view(|cx| {
1739 Pane::new(
1740 self.weak_handle(),
1741 self.project.clone(),
1742 self.app_state.background_actions,
1743 self.pane_history_timestamp.clone(),
1744 cx,
1745 )
1746 });
1747 cx.subscribe(&pane, Self::handle_pane_event).detach();
1748 self.panes.push(pane.clone());
1749 cx.focus(&pane);
1750 cx.emit(Event::PaneAdded(pane.clone()));
1751 pane
1752 }
1753
1754 pub fn add_item_to_center(
1755 &mut self,
1756 item: Box<dyn ItemHandle>,
1757 cx: &mut ViewContext<Self>,
1758 ) -> bool {
1759 if let Some(center_pane) = self.last_active_center_pane.clone() {
1760 if let Some(center_pane) = center_pane.upgrade(cx) {
1761 center_pane.update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
1762 true
1763 } else {
1764 false
1765 }
1766 } else {
1767 false
1768 }
1769 }
1770
1771 pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
1772 self.active_pane
1773 .update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
1774 }
1775
1776 pub fn open_abs_path(
1777 &mut self,
1778 abs_path: PathBuf,
1779 visible: bool,
1780 cx: &mut ViewContext<Self>,
1781 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
1782 cx.spawn(|workspace, mut cx| async move {
1783 let open_paths_task_result = workspace
1784 .update(&mut cx, |workspace, cx| {
1785 workspace.open_paths(vec![abs_path.clone()], visible, cx)
1786 })
1787 .with_context(|| format!("open abs path {abs_path:?} task spawn"))?
1788 .await;
1789 anyhow::ensure!(
1790 open_paths_task_result.len() == 1,
1791 "open abs path {abs_path:?} task returned incorrect number of results"
1792 );
1793 match open_paths_task_result
1794 .into_iter()
1795 .next()
1796 .expect("ensured single task result")
1797 {
1798 Some(open_result) => {
1799 open_result.with_context(|| format!("open abs path {abs_path:?} task join"))
1800 }
1801 None => anyhow::bail!("open abs path {abs_path:?} task returned None"),
1802 }
1803 })
1804 }
1805
1806 pub fn open_path(
1807 &mut self,
1808 path: impl Into<ProjectPath>,
1809 pane: Option<WeakViewHandle<Pane>>,
1810 focus_item: bool,
1811 cx: &mut ViewContext<Self>,
1812 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
1813 let pane = pane.unwrap_or_else(|| {
1814 self.last_active_center_pane.clone().unwrap_or_else(|| {
1815 self.panes
1816 .first()
1817 .expect("There must be an active pane")
1818 .downgrade()
1819 })
1820 });
1821
1822 let task = self.load_path(path.into(), cx);
1823 cx.spawn(|_, mut cx| async move {
1824 let (project_entry_id, build_item) = task.await?;
1825 pane.update(&mut cx, |pane, cx| {
1826 pane.open_item(project_entry_id, focus_item, cx, build_item)
1827 })
1828 })
1829 }
1830
1831 pub(crate) fn load_path(
1832 &mut self,
1833 path: ProjectPath,
1834 cx: &mut ViewContext<Self>,
1835 ) -> Task<
1836 Result<(
1837 ProjectEntryId,
1838 impl 'static + FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
1839 )>,
1840 > {
1841 let project = self.project().clone();
1842 let project_item = project.update(cx, |project, cx| project.open_path(path, cx));
1843 cx.spawn(|_, mut cx| async move {
1844 let (project_entry_id, project_item) = project_item.await?;
1845 let build_item = cx.update(|cx| {
1846 cx.default_global::<ProjectItemBuilders>()
1847 .get(&project_item.model_type())
1848 .ok_or_else(|| anyhow!("no item builder for project item"))
1849 .cloned()
1850 })?;
1851 let build_item =
1852 move |cx: &mut ViewContext<Pane>| build_item(project, project_item, cx);
1853 Ok((project_entry_id, build_item))
1854 })
1855 }
1856
1857 pub fn open_project_item<T>(
1858 &mut self,
1859 project_item: ModelHandle<T::Item>,
1860 cx: &mut ViewContext<Self>,
1861 ) -> ViewHandle<T>
1862 where
1863 T: ProjectItem,
1864 {
1865 use project::Item as _;
1866
1867 let entry_id = project_item.read(cx).entry_id(cx);
1868 if let Some(item) = entry_id
1869 .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx))
1870 .and_then(|item| item.downcast())
1871 {
1872 self.activate_item(&item, cx);
1873 return item;
1874 }
1875
1876 let item = cx.add_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
1877 self.add_item(Box::new(item.clone()), cx);
1878 item
1879 }
1880
1881 pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
1882 if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &self.active_pane, cx) {
1883 self.active_pane.update(cx, |pane, cx| {
1884 pane.add_item(Box::new(shared_screen), false, true, None, cx)
1885 });
1886 }
1887 }
1888
1889 pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut ViewContext<Self>) -> bool {
1890 let result = self.panes.iter().find_map(|pane| {
1891 pane.read(cx)
1892 .index_for_item(item)
1893 .map(|ix| (pane.clone(), ix))
1894 });
1895 if let Some((pane, ix)) = result {
1896 pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx));
1897 true
1898 } else {
1899 false
1900 }
1901 }
1902
1903 fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext<Self>) {
1904 let panes = self.center.panes();
1905 if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
1906 cx.focus(&pane);
1907 } else {
1908 self.split_pane(self.active_pane.clone(), SplitDirection::Right, cx);
1909 }
1910 }
1911
1912 pub fn activate_next_pane(&mut self, cx: &mut ViewContext<Self>) {
1913 let panes = self.center.panes();
1914 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
1915 let next_ix = (ix + 1) % panes.len();
1916 let next_pane = panes[next_ix].clone();
1917 cx.focus(&next_pane);
1918 }
1919 }
1920
1921 pub fn activate_previous_pane(&mut self, cx: &mut ViewContext<Self>) {
1922 let panes = self.center.panes();
1923 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
1924 let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
1925 let prev_pane = panes[prev_ix].clone();
1926 cx.focus(&prev_pane);
1927 }
1928 }
1929
1930 fn handle_pane_focused(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
1931 if self.active_pane != pane {
1932 self.active_pane = pane.clone();
1933 self.status_bar.update(cx, |status_bar, cx| {
1934 status_bar.set_active_pane(&self.active_pane, cx);
1935 });
1936 self.active_item_path_changed(cx);
1937 self.last_active_center_pane = Some(pane.downgrade());
1938 }
1939
1940 self.dismiss_zoomed_items_to_reveal(None, cx);
1941 if pane.read(cx).is_zoomed() {
1942 self.zoomed = Some(pane.downgrade().into_any());
1943 } else {
1944 self.zoomed = None;
1945 }
1946 self.zoomed_position = None;
1947
1948 self.update_followers(
1949 proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
1950 id: self.active_item(cx).and_then(|item| {
1951 item.to_followable_item_handle(cx)?
1952 .remote_id(&self.app_state.client, cx)
1953 .map(|id| id.to_proto())
1954 }),
1955 leader_id: self.leader_for_pane(&pane),
1956 }),
1957 cx,
1958 );
1959
1960 cx.notify();
1961 }
1962
1963 fn handle_pane_event(
1964 &mut self,
1965 pane: ViewHandle<Pane>,
1966 event: &pane::Event,
1967 cx: &mut ViewContext<Self>,
1968 ) {
1969 match event {
1970 pane::Event::AddItem { item } => item.added_to_pane(self, pane, cx),
1971 pane::Event::Split(direction) => {
1972 self.split_pane(pane, *direction, cx);
1973 }
1974 pane::Event::Remove => self.remove_pane(pane, cx),
1975 pane::Event::ActivateItem { local } => {
1976 if *local {
1977 self.unfollow(&pane, cx);
1978 }
1979 if &pane == self.active_pane() {
1980 self.active_item_path_changed(cx);
1981 }
1982 }
1983 pane::Event::ChangeItemTitle => {
1984 if pane == self.active_pane {
1985 self.active_item_path_changed(cx);
1986 }
1987 self.update_window_edited(cx);
1988 }
1989 pane::Event::RemoveItem { item_id } => {
1990 self.update_window_edited(cx);
1991 if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) {
1992 if entry.get().id() == pane.id() {
1993 entry.remove();
1994 }
1995 }
1996 }
1997 pane::Event::Focus => {
1998 self.handle_pane_focused(pane.clone(), cx);
1999 }
2000 pane::Event::ZoomIn => {
2001 if pane == self.active_pane {
2002 pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
2003 if pane.read(cx).has_focus() {
2004 self.zoomed = Some(pane.downgrade().into_any());
2005 self.zoomed_position = None;
2006 }
2007 cx.notify();
2008 }
2009 }
2010 pane::Event::ZoomOut => {
2011 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
2012 if self.zoomed_position.is_none() {
2013 self.zoomed = None;
2014 }
2015 cx.notify();
2016 }
2017 }
2018
2019 self.serialize_workspace(cx);
2020 }
2021
2022 pub fn split_pane(
2023 &mut self,
2024 pane: ViewHandle<Pane>,
2025 direction: SplitDirection,
2026 cx: &mut ViewContext<Self>,
2027 ) -> Option<ViewHandle<Pane>> {
2028 let item = pane.read(cx).active_item()?;
2029 let maybe_pane_handle = if let Some(clone) = item.clone_on_split(self.database_id(), cx) {
2030 let new_pane = self.add_pane(cx);
2031 new_pane.update(cx, |pane, cx| pane.add_item(clone, true, true, None, cx));
2032 self.center.split(&pane, &new_pane, direction).unwrap();
2033 Some(new_pane)
2034 } else {
2035 None
2036 };
2037 cx.notify();
2038 maybe_pane_handle
2039 }
2040
2041 pub fn split_pane_with_item(
2042 &mut self,
2043 pane_to_split: WeakViewHandle<Pane>,
2044 split_direction: SplitDirection,
2045 from: WeakViewHandle<Pane>,
2046 item_id_to_move: usize,
2047 cx: &mut ViewContext<Self>,
2048 ) {
2049 let Some(pane_to_split) = pane_to_split.upgrade(cx) else { return; };
2050 let Some(from) = from.upgrade(cx) else { return; };
2051
2052 let new_pane = self.add_pane(cx);
2053 self.move_item(from.clone(), new_pane.clone(), item_id_to_move, 0, cx);
2054 self.center
2055 .split(&pane_to_split, &new_pane, split_direction)
2056 .unwrap();
2057 cx.notify();
2058 }
2059
2060 pub fn split_pane_with_project_entry(
2061 &mut self,
2062 pane_to_split: WeakViewHandle<Pane>,
2063 split_direction: SplitDirection,
2064 project_entry: ProjectEntryId,
2065 cx: &mut ViewContext<Self>,
2066 ) -> Option<Task<Result<()>>> {
2067 let pane_to_split = pane_to_split.upgrade(cx)?;
2068 let new_pane = self.add_pane(cx);
2069 self.center
2070 .split(&pane_to_split, &new_pane, split_direction)
2071 .unwrap();
2072
2073 let path = self.project.read(cx).path_for_entry(project_entry, cx)?;
2074 let task = self.open_path(path, Some(new_pane.downgrade()), true, cx);
2075 Some(cx.foreground().spawn(async move {
2076 task.await?;
2077 Ok(())
2078 }))
2079 }
2080
2081 pub fn move_item(
2082 &mut self,
2083 source: ViewHandle<Pane>,
2084 destination: ViewHandle<Pane>,
2085 item_id_to_move: usize,
2086 destination_index: usize,
2087 cx: &mut ViewContext<Self>,
2088 ) {
2089 let item_to_move = source
2090 .read(cx)
2091 .items()
2092 .enumerate()
2093 .find(|(_, item_handle)| item_handle.id() == item_id_to_move);
2094
2095 if item_to_move.is_none() {
2096 log::warn!("Tried to move item handle which was not in `from` pane. Maybe tab was closed during drop");
2097 return;
2098 }
2099 let (item_ix, item_handle) = item_to_move.unwrap();
2100 let item_handle = item_handle.clone();
2101
2102 if source != destination {
2103 // Close item from previous pane
2104 source.update(cx, |source, cx| {
2105 source.remove_item(item_ix, false, cx);
2106 });
2107 }
2108
2109 // This automatically removes duplicate items in the pane
2110 destination.update(cx, |destination, cx| {
2111 destination.add_item(item_handle, true, true, Some(destination_index), cx);
2112 cx.focus_self();
2113 });
2114 }
2115
2116 fn remove_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
2117 if self.center.remove(&pane).unwrap() {
2118 self.force_remove_pane(&pane, cx);
2119 self.unfollow(&pane, cx);
2120 self.last_leaders_by_pane.remove(&pane.downgrade());
2121 for removed_item in pane.read(cx).items() {
2122 self.panes_by_item.remove(&removed_item.id());
2123 }
2124
2125 cx.notify();
2126 } else {
2127 self.active_item_path_changed(cx);
2128 }
2129 }
2130
2131 pub fn panes(&self) -> &[ViewHandle<Pane>] {
2132 &self.panes
2133 }
2134
2135 pub fn active_pane(&self) -> &ViewHandle<Pane> {
2136 &self.active_pane
2137 }
2138
2139 fn project_remote_id_changed(&mut self, remote_id: Option<u64>, cx: &mut ViewContext<Self>) {
2140 if let Some(remote_id) = remote_id {
2141 self.remote_entity_subscription = Some(
2142 self.app_state
2143 .client
2144 .add_view_for_remote_entity(remote_id, cx),
2145 );
2146 } else {
2147 self.remote_entity_subscription.take();
2148 }
2149 }
2150
2151 fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
2152 self.leader_state.followers.remove(&peer_id);
2153 if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) {
2154 for state in states_by_pane.into_values() {
2155 for item in state.items_by_leader_view_id.into_values() {
2156 item.set_leader_replica_id(None, cx);
2157 }
2158 }
2159 }
2160 cx.notify();
2161 }
2162
2163 pub fn toggle_follow(
2164 &mut self,
2165 leader_id: PeerId,
2166 cx: &mut ViewContext<Self>,
2167 ) -> Option<Task<Result<()>>> {
2168 let pane = self.active_pane().clone();
2169
2170 if let Some(prev_leader_id) = self.unfollow(&pane, cx) {
2171 if leader_id == prev_leader_id {
2172 return None;
2173 }
2174 }
2175
2176 self.last_leaders_by_pane
2177 .insert(pane.downgrade(), leader_id);
2178 self.follower_states_by_leader
2179 .entry(leader_id)
2180 .or_default()
2181 .insert(pane.clone(), Default::default());
2182 cx.notify();
2183
2184 let project_id = self.project.read(cx).remote_id()?;
2185 let request = self.app_state.client.request(proto::Follow {
2186 project_id,
2187 leader_id: Some(leader_id),
2188 });
2189
2190 Some(cx.spawn(|this, mut cx| async move {
2191 let response = request.await?;
2192 this.update(&mut cx, |this, _| {
2193 let state = this
2194 .follower_states_by_leader
2195 .get_mut(&leader_id)
2196 .and_then(|states_by_pane| states_by_pane.get_mut(&pane))
2197 .ok_or_else(|| anyhow!("following interrupted"))?;
2198 state.active_view_id = if let Some(active_view_id) = response.active_view_id {
2199 Some(ViewId::from_proto(active_view_id)?)
2200 } else {
2201 None
2202 };
2203 Ok::<_, anyhow::Error>(())
2204 })??;
2205 Self::add_views_from_leader(
2206 this.clone(),
2207 leader_id,
2208 vec![pane],
2209 response.views,
2210 &mut cx,
2211 )
2212 .await?;
2213 this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?;
2214 Ok(())
2215 }))
2216 }
2217
2218 pub fn follow_next_collaborator(
2219 &mut self,
2220 _: &FollowNextCollaborator,
2221 cx: &mut ViewContext<Self>,
2222 ) -> Option<Task<Result<()>>> {
2223 let collaborators = self.project.read(cx).collaborators();
2224 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
2225 let mut collaborators = collaborators.keys().copied();
2226 for peer_id in collaborators.by_ref() {
2227 if peer_id == leader_id {
2228 break;
2229 }
2230 }
2231 collaborators.next()
2232 } else if let Some(last_leader_id) =
2233 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
2234 {
2235 if collaborators.contains_key(last_leader_id) {
2236 Some(*last_leader_id)
2237 } else {
2238 None
2239 }
2240 } else {
2241 None
2242 };
2243
2244 next_leader_id
2245 .or_else(|| collaborators.keys().copied().next())
2246 .and_then(|leader_id| self.toggle_follow(leader_id, cx))
2247 }
2248
2249 pub fn unfollow(
2250 &mut self,
2251 pane: &ViewHandle<Pane>,
2252 cx: &mut ViewContext<Self>,
2253 ) -> Option<PeerId> {
2254 for (leader_id, states_by_pane) in &mut self.follower_states_by_leader {
2255 let leader_id = *leader_id;
2256 if let Some(state) = states_by_pane.remove(pane) {
2257 for (_, item) in state.items_by_leader_view_id {
2258 item.set_leader_replica_id(None, cx);
2259 }
2260
2261 if states_by_pane.is_empty() {
2262 self.follower_states_by_leader.remove(&leader_id);
2263 if let Some(project_id) = self.project.read(cx).remote_id() {
2264 self.app_state
2265 .client
2266 .send(proto::Unfollow {
2267 project_id,
2268 leader_id: Some(leader_id),
2269 })
2270 .log_err();
2271 }
2272 }
2273
2274 cx.notify();
2275 return Some(leader_id);
2276 }
2277 }
2278 None
2279 }
2280
2281 pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
2282 self.follower_states_by_leader.contains_key(&peer_id)
2283 }
2284
2285 pub fn is_followed_by(&self, peer_id: PeerId) -> bool {
2286 self.leader_state.followers.contains(&peer_id)
2287 }
2288
2289 fn render_titlebar(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
2290 // TODO: There should be a better system in place for this
2291 // (https://github.com/zed-industries/zed/issues/1290)
2292 let is_fullscreen = cx.window_is_fullscreen();
2293 let container_theme = if is_fullscreen {
2294 let mut container_theme = theme.workspace.titlebar.container;
2295 container_theme.padding.left = container_theme.padding.right;
2296 container_theme
2297 } else {
2298 theme.workspace.titlebar.container
2299 };
2300
2301 enum TitleBar {}
2302 MouseEventHandler::<TitleBar, _>::new(0, cx, |_, cx| {
2303 Stack::new()
2304 .with_children(
2305 self.titlebar_item
2306 .as_ref()
2307 .map(|item| ChildView::new(item, cx)),
2308 )
2309 .contained()
2310 .with_style(container_theme)
2311 })
2312 .on_click(MouseButton::Left, |event, _, cx| {
2313 if event.click_count == 2 {
2314 cx.zoom_window();
2315 }
2316 })
2317 .constrained()
2318 .with_height(theme.workspace.titlebar.height)
2319 .into_any_named("titlebar")
2320 }
2321
2322 fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
2323 let active_entry = self.active_project_path(cx);
2324 self.project
2325 .update(cx, |project, cx| project.set_active_path(active_entry, cx));
2326 self.update_window_title(cx);
2327 }
2328
2329 fn update_window_title(&mut self, cx: &mut ViewContext<Self>) {
2330 let project = self.project().read(cx);
2331 let mut title = String::new();
2332
2333 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
2334 let filename = path
2335 .path
2336 .file_name()
2337 .map(|s| s.to_string_lossy())
2338 .or_else(|| {
2339 Some(Cow::Borrowed(
2340 project
2341 .worktree_for_id(path.worktree_id, cx)?
2342 .read(cx)
2343 .root_name(),
2344 ))
2345 });
2346
2347 if let Some(filename) = filename {
2348 title.push_str(filename.as_ref());
2349 title.push_str(" β ");
2350 }
2351 }
2352
2353 for (i, name) in project.worktree_root_names(cx).enumerate() {
2354 if i > 0 {
2355 title.push_str(", ");
2356 }
2357 title.push_str(name);
2358 }
2359
2360 if title.is_empty() {
2361 title = "empty project".to_string();
2362 }
2363
2364 if project.is_remote() {
2365 title.push_str(" β");
2366 } else if project.is_shared() {
2367 title.push_str(" β");
2368 }
2369
2370 cx.set_window_title(&title);
2371 }
2372
2373 fn update_window_edited(&mut self, cx: &mut ViewContext<Self>) {
2374 let is_edited = !self.project.read(cx).is_read_only()
2375 && self
2376 .items(cx)
2377 .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
2378 if is_edited != self.window_edited {
2379 self.window_edited = is_edited;
2380 cx.set_window_edited(self.window_edited)
2381 }
2382 }
2383
2384 fn render_disconnected_overlay(
2385 &self,
2386 cx: &mut ViewContext<Workspace>,
2387 ) -> Option<AnyElement<Workspace>> {
2388 if self.project.read(cx).is_read_only() {
2389 enum DisconnectedOverlay {}
2390 Some(
2391 MouseEventHandler::<DisconnectedOverlay, _>::new(0, cx, |_, cx| {
2392 let theme = &theme::current(cx);
2393 Label::new(
2394 "Your connection to the remote project has been lost.",
2395 theme.workspace.disconnected_overlay.text.clone(),
2396 )
2397 .aligned()
2398 .contained()
2399 .with_style(theme.workspace.disconnected_overlay.container)
2400 })
2401 .with_cursor_style(CursorStyle::Arrow)
2402 .capture_all()
2403 .into_any_named("disconnected overlay"),
2404 )
2405 } else {
2406 None
2407 }
2408 }
2409
2410 fn render_notifications(
2411 &self,
2412 theme: &theme::Workspace,
2413 cx: &AppContext,
2414 ) -> Option<AnyElement<Workspace>> {
2415 if self.notifications.is_empty() {
2416 None
2417 } else {
2418 Some(
2419 Flex::column()
2420 .with_children(self.notifications.iter().map(|(_, _, notification)| {
2421 ChildView::new(notification.as_any(), cx)
2422 .contained()
2423 .with_style(theme.notification)
2424 }))
2425 .constrained()
2426 .with_width(theme.notifications.width)
2427 .contained()
2428 .with_style(theme.notifications.container)
2429 .aligned()
2430 .bottom()
2431 .right()
2432 .into_any(),
2433 )
2434 }
2435 }
2436
2437 // RPC handlers
2438
2439 async fn handle_follow(
2440 this: WeakViewHandle<Self>,
2441 envelope: TypedEnvelope<proto::Follow>,
2442 _: Arc<Client>,
2443 mut cx: AsyncAppContext,
2444 ) -> Result<proto::FollowResponse> {
2445 this.update(&mut cx, |this, cx| {
2446 let client = &this.app_state.client;
2447 this.leader_state
2448 .followers
2449 .insert(envelope.original_sender_id()?);
2450
2451 let active_view_id = this.active_item(cx).and_then(|i| {
2452 Some(
2453 i.to_followable_item_handle(cx)?
2454 .remote_id(client, cx)?
2455 .to_proto(),
2456 )
2457 });
2458
2459 cx.notify();
2460
2461 Ok(proto::FollowResponse {
2462 active_view_id,
2463 views: this
2464 .panes()
2465 .iter()
2466 .flat_map(|pane| {
2467 let leader_id = this.leader_for_pane(pane);
2468 pane.read(cx).items().filter_map({
2469 let cx = &cx;
2470 move |item| {
2471 let item = item.to_followable_item_handle(cx)?;
2472 let id = item.remote_id(client, cx)?.to_proto();
2473 let variant = item.to_state_proto(cx)?;
2474 Some(proto::View {
2475 id: Some(id),
2476 leader_id,
2477 variant: Some(variant),
2478 })
2479 }
2480 })
2481 })
2482 .collect(),
2483 })
2484 })?
2485 }
2486
2487 async fn handle_unfollow(
2488 this: WeakViewHandle<Self>,
2489 envelope: TypedEnvelope<proto::Unfollow>,
2490 _: Arc<Client>,
2491 mut cx: AsyncAppContext,
2492 ) -> Result<()> {
2493 this.update(&mut cx, |this, cx| {
2494 this.leader_state
2495 .followers
2496 .remove(&envelope.original_sender_id()?);
2497 cx.notify();
2498 Ok(())
2499 })?
2500 }
2501
2502 async fn handle_update_followers(
2503 this: WeakViewHandle<Self>,
2504 envelope: TypedEnvelope<proto::UpdateFollowers>,
2505 _: Arc<Client>,
2506 cx: AsyncAppContext,
2507 ) -> Result<()> {
2508 let leader_id = envelope.original_sender_id()?;
2509 this.read_with(&cx, |this, _| {
2510 this.leader_updates_tx
2511 .unbounded_send((leader_id, envelope.payload))
2512 })??;
2513 Ok(())
2514 }
2515
2516 async fn process_leader_update(
2517 this: &WeakViewHandle<Self>,
2518 leader_id: PeerId,
2519 update: proto::UpdateFollowers,
2520 cx: &mut AsyncAppContext,
2521 ) -> Result<()> {
2522 match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
2523 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
2524 this.update(cx, |this, _| {
2525 if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
2526 for state in state.values_mut() {
2527 state.active_view_id =
2528 if let Some(active_view_id) = update_active_view.id.clone() {
2529 Some(ViewId::from_proto(active_view_id)?)
2530 } else {
2531 None
2532 };
2533 }
2534 }
2535 anyhow::Ok(())
2536 })??;
2537 }
2538 proto::update_followers::Variant::UpdateView(update_view) => {
2539 let variant = update_view
2540 .variant
2541 .ok_or_else(|| anyhow!("missing update view variant"))?;
2542 let id = update_view
2543 .id
2544 .ok_or_else(|| anyhow!("missing update view id"))?;
2545 let mut tasks = Vec::new();
2546 this.update(cx, |this, cx| {
2547 let project = this.project.clone();
2548 if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
2549 for state in state.values_mut() {
2550 let view_id = ViewId::from_proto(id.clone())?;
2551 if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
2552 tasks.push(item.apply_update_proto(&project, variant.clone(), cx));
2553 }
2554 }
2555 }
2556 anyhow::Ok(())
2557 })??;
2558 try_join_all(tasks).await.log_err();
2559 }
2560 proto::update_followers::Variant::CreateView(view) => {
2561 let panes = this.read_with(cx, |this, _| {
2562 this.follower_states_by_leader
2563 .get(&leader_id)
2564 .into_iter()
2565 .flat_map(|states_by_pane| states_by_pane.keys())
2566 .cloned()
2567 .collect()
2568 })?;
2569 Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], cx).await?;
2570 }
2571 }
2572 this.update(cx, |this, cx| this.leader_updated(leader_id, cx))?;
2573 Ok(())
2574 }
2575
2576 async fn add_views_from_leader(
2577 this: WeakViewHandle<Self>,
2578 leader_id: PeerId,
2579 panes: Vec<ViewHandle<Pane>>,
2580 views: Vec<proto::View>,
2581 cx: &mut AsyncAppContext,
2582 ) -> Result<()> {
2583 let project = this.read_with(cx, |this, _| this.project.clone())?;
2584 let replica_id = project
2585 .read_with(cx, |project, _| {
2586 project
2587 .collaborators()
2588 .get(&leader_id)
2589 .map(|c| c.replica_id)
2590 })
2591 .ok_or_else(|| anyhow!("no such collaborator {}", leader_id))?;
2592
2593 let item_builders = cx.update(|cx| {
2594 cx.default_global::<FollowableItemBuilders>()
2595 .values()
2596 .map(|b| b.0)
2597 .collect::<Vec<_>>()
2598 });
2599
2600 let mut item_tasks_by_pane = HashMap::default();
2601 for pane in panes {
2602 let mut item_tasks = Vec::new();
2603 let mut leader_view_ids = Vec::new();
2604 for view in &views {
2605 let Some(id) = &view.id else { continue };
2606 let id = ViewId::from_proto(id.clone())?;
2607 let mut variant = view.variant.clone();
2608 if variant.is_none() {
2609 Err(anyhow!("missing variant"))?;
2610 }
2611 for build_item in &item_builders {
2612 let task = cx.update(|cx| {
2613 build_item(pane.clone(), project.clone(), id, &mut variant, cx)
2614 });
2615 if let Some(task) = task {
2616 item_tasks.push(task);
2617 leader_view_ids.push(id);
2618 break;
2619 } else {
2620 assert!(variant.is_some());
2621 }
2622 }
2623 }
2624
2625 item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids));
2626 }
2627
2628 for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
2629 let items = futures::future::try_join_all(item_tasks).await?;
2630 this.update(cx, |this, cx| {
2631 let state = this
2632 .follower_states_by_leader
2633 .get_mut(&leader_id)?
2634 .get_mut(&pane)?;
2635
2636 for (id, item) in leader_view_ids.into_iter().zip(items) {
2637 item.set_leader_replica_id(Some(replica_id), cx);
2638 state.items_by_leader_view_id.insert(id, item);
2639 }
2640
2641 Some(())
2642 })?;
2643 }
2644 Ok(())
2645 }
2646
2647 fn update_followers(
2648 &self,
2649 update: proto::update_followers::Variant,
2650 cx: &AppContext,
2651 ) -> Option<()> {
2652 let project_id = self.project.read(cx).remote_id()?;
2653 if !self.leader_state.followers.is_empty() {
2654 self.app_state
2655 .client
2656 .send(proto::UpdateFollowers {
2657 project_id,
2658 follower_ids: self.leader_state.followers.iter().copied().collect(),
2659 variant: Some(update),
2660 })
2661 .log_err();
2662 }
2663 None
2664 }
2665
2666 pub fn leader_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<PeerId> {
2667 self.follower_states_by_leader
2668 .iter()
2669 .find_map(|(leader_id, state)| {
2670 if state.contains_key(pane) {
2671 Some(*leader_id)
2672 } else {
2673 None
2674 }
2675 })
2676 }
2677
2678 fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
2679 cx.notify();
2680
2681 let call = self.active_call()?;
2682 let room = call.read(cx).room()?.read(cx);
2683 let participant = room.remote_participant_for_peer_id(leader_id)?;
2684 let mut items_to_activate = Vec::new();
2685 match participant.location {
2686 call::ParticipantLocation::SharedProject { project_id } => {
2687 if Some(project_id) == self.project.read(cx).remote_id() {
2688 for (pane, state) in self.follower_states_by_leader.get(&leader_id)? {
2689 if let Some(item) = state
2690 .active_view_id
2691 .and_then(|id| state.items_by_leader_view_id.get(&id))
2692 {
2693 items_to_activate.push((pane.clone(), item.boxed_clone()));
2694 } else {
2695 if let Some(shared_screen) =
2696 self.shared_screen_for_peer(leader_id, pane, cx)
2697 {
2698 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
2699 }
2700 }
2701 }
2702 }
2703 }
2704 call::ParticipantLocation::UnsharedProject => {}
2705 call::ParticipantLocation::External => {
2706 for (pane, _) in self.follower_states_by_leader.get(&leader_id)? {
2707 if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
2708 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
2709 }
2710 }
2711 }
2712 }
2713
2714 for (pane, item) in items_to_activate {
2715 let pane_was_focused = pane.read(cx).has_focus();
2716 if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) {
2717 pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx));
2718 } else {
2719 pane.update(cx, |pane, cx| {
2720 pane.add_item(item.boxed_clone(), false, false, None, cx)
2721 });
2722 }
2723
2724 if pane_was_focused {
2725 pane.update(cx, |pane, cx| pane.focus_active_item(cx));
2726 }
2727 }
2728
2729 None
2730 }
2731
2732 fn shared_screen_for_peer(
2733 &self,
2734 peer_id: PeerId,
2735 pane: &ViewHandle<Pane>,
2736 cx: &mut ViewContext<Self>,
2737 ) -> Option<ViewHandle<SharedScreen>> {
2738 let call = self.active_call()?;
2739 let room = call.read(cx).room()?.read(cx);
2740 let participant = room.remote_participant_for_peer_id(peer_id)?;
2741 let track = participant.tracks.values().next()?.clone();
2742 let user = participant.user.clone();
2743
2744 for item in pane.read(cx).items_of_type::<SharedScreen>() {
2745 if item.read(cx).peer_id == peer_id {
2746 return Some(item);
2747 }
2748 }
2749
2750 Some(cx.add_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
2751 }
2752
2753 pub fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
2754 if active {
2755 cx.background()
2756 .spawn(persistence::DB.update_timestamp(self.database_id()))
2757 .detach();
2758 } else {
2759 for pane in &self.panes {
2760 pane.update(cx, |pane, cx| {
2761 if let Some(item) = pane.active_item() {
2762 item.workspace_deactivated(cx);
2763 }
2764 if matches!(
2765 settings::get::<WorkspaceSettings>(cx).autosave,
2766 AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
2767 ) {
2768 for item in pane.items() {
2769 Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
2770 .detach_and_log_err(cx);
2771 }
2772 }
2773 });
2774 }
2775 }
2776 }
2777
2778 fn active_call(&self) -> Option<&ModelHandle<ActiveCall>> {
2779 self.active_call.as_ref().map(|(call, _)| call)
2780 }
2781
2782 fn on_active_call_event(
2783 &mut self,
2784 _: ModelHandle<ActiveCall>,
2785 event: &call::room::Event,
2786 cx: &mut ViewContext<Self>,
2787 ) {
2788 match event {
2789 call::room::Event::ParticipantLocationChanged { participant_id }
2790 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
2791 self.leader_updated(*participant_id, cx);
2792 }
2793 _ => {}
2794 }
2795 }
2796
2797 pub fn database_id(&self) -> WorkspaceId {
2798 self.database_id
2799 }
2800
2801 fn location(&self, cx: &AppContext) -> Option<WorkspaceLocation> {
2802 let project = self.project().read(cx);
2803
2804 if project.is_local() {
2805 Some(
2806 project
2807 .visible_worktrees(cx)
2808 .map(|worktree| worktree.read(cx).abs_path())
2809 .collect::<Vec<_>>()
2810 .into(),
2811 )
2812 } else {
2813 None
2814 }
2815 }
2816
2817 fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
2818 match member {
2819 Member::Axis(PaneAxis { members, .. }) => {
2820 for child in members.iter() {
2821 self.remove_panes(child.clone(), cx)
2822 }
2823 }
2824 Member::Pane(pane) => {
2825 self.force_remove_pane(&pane, cx);
2826 }
2827 }
2828 }
2829
2830 fn force_remove_pane(&mut self, pane: &ViewHandle<Pane>, cx: &mut ViewContext<Workspace>) {
2831 self.panes.retain(|p| p != pane);
2832 cx.focus(self.panes.last().unwrap());
2833 if self.last_active_center_pane == Some(pane.downgrade()) {
2834 self.last_active_center_pane = None;
2835 }
2836 cx.notify();
2837 }
2838
2839 fn serialize_workspace(&self, cx: &ViewContext<Self>) {
2840 fn serialize_pane_handle(
2841 pane_handle: &ViewHandle<Pane>,
2842 cx: &AppContext,
2843 ) -> SerializedPane {
2844 let (items, active) = {
2845 let pane = pane_handle.read(cx);
2846 let active_item_id = pane.active_item().map(|item| item.id());
2847 (
2848 pane.items()
2849 .filter_map(|item_handle| {
2850 Some(SerializedItem {
2851 kind: Arc::from(item_handle.serialized_item_kind()?),
2852 item_id: item_handle.id(),
2853 active: Some(item_handle.id()) == active_item_id,
2854 })
2855 })
2856 .collect::<Vec<_>>(),
2857 pane.has_focus(),
2858 )
2859 };
2860
2861 SerializedPane::new(items, active)
2862 }
2863
2864 fn build_serialized_pane_group(
2865 pane_group: &Member,
2866 cx: &AppContext,
2867 ) -> SerializedPaneGroup {
2868 match pane_group {
2869 Member::Axis(PaneAxis { axis, members }) => SerializedPaneGroup::Group {
2870 axis: *axis,
2871 children: members
2872 .iter()
2873 .map(|member| build_serialized_pane_group(member, cx))
2874 .collect::<Vec<_>>(),
2875 },
2876 Member::Pane(pane_handle) => {
2877 SerializedPaneGroup::Pane(serialize_pane_handle(&pane_handle, cx))
2878 }
2879 }
2880 }
2881
2882 fn build_serialized_docks(this: &Workspace, cx: &ViewContext<Workspace>) -> DockStructure {
2883 let left_dock = this.left_dock.read(cx);
2884 let left_visible = left_dock.is_open();
2885 let left_active_panel = left_dock.visible_panel().and_then(|panel| {
2886 Some(
2887 cx.view_ui_name(panel.as_any().window_id(), panel.id())?
2888 .to_string(),
2889 )
2890 });
2891 let left_dock_zoom = left_dock
2892 .visible_panel()
2893 .map(|panel| panel.is_zoomed(cx))
2894 .unwrap_or(false);
2895
2896 let right_dock = this.right_dock.read(cx);
2897 let right_visible = right_dock.is_open();
2898 let right_active_panel = right_dock.visible_panel().and_then(|panel| {
2899 Some(
2900 cx.view_ui_name(panel.as_any().window_id(), panel.id())?
2901 .to_string(),
2902 )
2903 });
2904 let right_dock_zoom = right_dock
2905 .visible_panel()
2906 .map(|panel| panel.is_zoomed(cx))
2907 .unwrap_or(false);
2908
2909 let bottom_dock = this.bottom_dock.read(cx);
2910 let bottom_visible = bottom_dock.is_open();
2911 let bottom_active_panel = bottom_dock.visible_panel().and_then(|panel| {
2912 Some(
2913 cx.view_ui_name(panel.as_any().window_id(), panel.id())?
2914 .to_string(),
2915 )
2916 });
2917 let bottom_dock_zoom = bottom_dock
2918 .visible_panel()
2919 .map(|panel| panel.is_zoomed(cx))
2920 .unwrap_or(false);
2921
2922 DockStructure {
2923 left: DockData {
2924 visible: left_visible,
2925 active_panel: left_active_panel,
2926 zoom: left_dock_zoom,
2927 },
2928 right: DockData {
2929 visible: right_visible,
2930 active_panel: right_active_panel,
2931 zoom: right_dock_zoom,
2932 },
2933 bottom: DockData {
2934 visible: bottom_visible,
2935 active_panel: bottom_active_panel,
2936 zoom: bottom_dock_zoom,
2937 },
2938 }
2939 }
2940
2941 if let Some(location) = self.location(cx) {
2942 // Load bearing special case:
2943 // - with_local_workspace() relies on this to not have other stuff open
2944 // when you open your log
2945 if !location.paths().is_empty() {
2946 let center_group = build_serialized_pane_group(&self.center.root, cx);
2947 let docks = build_serialized_docks(self, cx);
2948
2949 let serialized_workspace = SerializedWorkspace {
2950 id: self.database_id,
2951 location,
2952 center_group,
2953 bounds: Default::default(),
2954 display: Default::default(),
2955 docks,
2956 };
2957
2958 cx.background()
2959 .spawn(persistence::DB.save_workspace(serialized_workspace))
2960 .detach();
2961 }
2962 }
2963 }
2964
2965 pub(crate) fn load_workspace(
2966 workspace: WeakViewHandle<Workspace>,
2967 serialized_workspace: SerializedWorkspace,
2968 paths_to_open: Vec<Option<ProjectPath>>,
2969 cx: &mut AppContext,
2970 ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
2971 cx.spawn(|mut cx| async move {
2972 let result = async_iife! {{
2973 let (project, old_center_pane) =
2974 workspace.read_with(&cx, |workspace, _| {
2975 (
2976 workspace.project().clone(),
2977 workspace.last_active_center_pane.clone(),
2978 )
2979 })?;
2980
2981 let mut center_items = None;
2982 let mut center_group = None;
2983 // Traverse the splits tree and add to things
2984 if let Some((group, active_pane, items)) = serialized_workspace
2985 .center_group
2986 .deserialize(&project, serialized_workspace.id, &workspace, &mut cx)
2987 .await {
2988 center_items = Some(items);
2989 center_group = Some((group, active_pane))
2990 }
2991
2992 let resulting_list = cx.read(|cx| {
2993 let mut opened_items = center_items
2994 .unwrap_or_default()
2995 .into_iter()
2996 .filter_map(|item| {
2997 let item = item?;
2998 let project_path = item.project_path(cx)?;
2999 Some((project_path, item))
3000 })
3001 .collect::<HashMap<_, _>>();
3002
3003 paths_to_open
3004 .into_iter()
3005 .map(|path_to_open| {
3006 path_to_open.map(|path_to_open| {
3007 Ok(opened_items.remove(&path_to_open))
3008 })
3009 .transpose()
3010 .map(|item| item.flatten())
3011 .transpose()
3012 })
3013 .collect::<Vec<_>>()
3014 });
3015
3016 // Remove old panes from workspace panes list
3017 workspace.update(&mut cx, |workspace, cx| {
3018 if let Some((center_group, active_pane)) = center_group {
3019 workspace.remove_panes(workspace.center.root.clone(), cx);
3020
3021 // Swap workspace center group
3022 workspace.center = PaneGroup::with_root(center_group);
3023
3024 // Change the focus to the workspace first so that we retrigger focus in on the pane.
3025 cx.focus_self();
3026
3027 if let Some(active_pane) = active_pane {
3028 cx.focus(&active_pane);
3029 } else {
3030 cx.focus(workspace.panes.last().unwrap());
3031 }
3032 } else {
3033 let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade(cx));
3034 if let Some(old_center_handle) = old_center_handle {
3035 cx.focus(&old_center_handle)
3036 } else {
3037 cx.focus_self()
3038 }
3039 }
3040
3041 let docks = serialized_workspace.docks;
3042 workspace.left_dock.update(cx, |dock, cx| {
3043 dock.set_open(docks.left.visible, cx);
3044 if let Some(active_panel) = docks.left.active_panel {
3045 if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
3046 dock.activate_panel(ix, cx);
3047 }
3048 }
3049 dock.active_panel()
3050 .map(|panel| {
3051 panel.set_zoomed(docks.left.zoom, cx)
3052 });
3053 if docks.left.visible && docks.left.zoom {
3054 cx.focus_self()
3055 }
3056 });
3057 // TODO: I think the bug is that setting zoom or active undoes the bottom zoom or something
3058 workspace.right_dock.update(cx, |dock, cx| {
3059 dock.set_open(docks.right.visible, cx);
3060 if let Some(active_panel) = docks.right.active_panel {
3061 if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
3062 dock.activate_panel(ix, cx);
3063
3064 }
3065 }
3066 dock.active_panel()
3067 .map(|panel| {
3068 panel.set_zoomed(docks.right.zoom, cx)
3069 });
3070
3071 if docks.right.visible && docks.right.zoom {
3072 cx.focus_self()
3073 }
3074 });
3075 workspace.bottom_dock.update(cx, |dock, cx| {
3076 dock.set_open(docks.bottom.visible, cx);
3077 if let Some(active_panel) = docks.bottom.active_panel {
3078 if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
3079 dock.activate_panel(ix, cx);
3080 }
3081 }
3082
3083 dock.active_panel()
3084 .map(|panel| {
3085 panel.set_zoomed(docks.bottom.zoom, cx)
3086 });
3087
3088 if docks.bottom.visible && docks.bottom.zoom {
3089 cx.focus_self()
3090 }
3091 });
3092
3093
3094 cx.notify();
3095 })?;
3096
3097 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
3098 workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))?;
3099
3100 Ok::<_, anyhow::Error>(resulting_list)
3101 }};
3102
3103 result.await.unwrap_or_default()
3104 })
3105 }
3106
3107 #[cfg(any(test, feature = "test-support"))]
3108 pub fn test_new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
3109 let app_state = Arc::new(AppState {
3110 languages: project.read(cx).languages().clone(),
3111 client: project.read(cx).client(),
3112 user_store: project.read(cx).user_store(),
3113 fs: project.read(cx).fs().clone(),
3114 build_window_options: |_, _, _| Default::default(),
3115 initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
3116 background_actions: || &[],
3117 });
3118 Self::new(0, project, app_state, cx)
3119 }
3120
3121 fn render_dock(&self, position: DockPosition, cx: &WindowContext) -> Option<AnyElement<Self>> {
3122 let dock = match position {
3123 DockPosition::Left => &self.left_dock,
3124 DockPosition::Right => &self.right_dock,
3125 DockPosition::Bottom => &self.bottom_dock,
3126 };
3127 let active_panel = dock.read(cx).visible_panel()?;
3128 let element = if Some(active_panel.id()) == self.zoomed.as_ref().map(|zoomed| zoomed.id()) {
3129 dock.read(cx).render_placeholder(cx)
3130 } else {
3131 ChildView::new(dock, cx).into_any()
3132 };
3133
3134 Some(
3135 element
3136 .constrained()
3137 .dynamically(move |constraint, _, cx| match position {
3138 DockPosition::Left | DockPosition::Right => SizeConstraint::new(
3139 Vector2F::new(20., constraint.min.y()),
3140 Vector2F::new(cx.window_size().x() * 0.8, constraint.max.y()),
3141 ),
3142 DockPosition::Bottom => SizeConstraint::new(
3143 Vector2F::new(constraint.min.x(), 20.),
3144 Vector2F::new(constraint.max.x(), cx.window_size().y() * 0.8),
3145 ),
3146 })
3147 .into_any(),
3148 )
3149 }
3150}
3151
3152fn window_bounds_env_override(cx: &AsyncAppContext) -> Option<WindowBounds> {
3153 ZED_WINDOW_POSITION
3154 .zip(*ZED_WINDOW_SIZE)
3155 .map(|(position, size)| {
3156 WindowBounds::Fixed(RectF::new(
3157 cx.platform().screens()[0].bounds().origin() + position,
3158 size,
3159 ))
3160 })
3161}
3162
3163async fn open_items(
3164 serialized_workspace: Option<SerializedWorkspace>,
3165 workspace: &WeakViewHandle<Workspace>,
3166 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
3167 app_state: Arc<AppState>,
3168 mut cx: AsyncAppContext,
3169) -> Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>> {
3170 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
3171
3172 if let Some(serialized_workspace) = serialized_workspace {
3173 let workspace = workspace.clone();
3174 let restored_items = cx
3175 .update(|cx| {
3176 Workspace::load_workspace(
3177 workspace,
3178 serialized_workspace,
3179 project_paths_to_open
3180 .iter()
3181 .map(|(_, project_path)| project_path)
3182 .cloned()
3183 .collect(),
3184 cx,
3185 )
3186 })
3187 .await;
3188
3189 let restored_project_paths = cx.read(|cx| {
3190 restored_items
3191 .iter()
3192 .filter_map(|item| item.as_ref()?.as_ref().ok()?.project_path(cx))
3193 .collect::<HashSet<_>>()
3194 });
3195
3196 opened_items = restored_items;
3197 project_paths_to_open
3198 .iter_mut()
3199 .for_each(|(_, project_path)| {
3200 if let Some(project_path_to_open) = project_path {
3201 if restored_project_paths.contains(project_path_to_open) {
3202 *project_path = None;
3203 }
3204 }
3205 });
3206 } else {
3207 for _ in 0..project_paths_to_open.len() {
3208 opened_items.push(None);
3209 }
3210 }
3211 assert!(opened_items.len() == project_paths_to_open.len());
3212
3213 let tasks =
3214 project_paths_to_open
3215 .into_iter()
3216 .enumerate()
3217 .map(|(i, (abs_path, project_path))| {
3218 let workspace = workspace.clone();
3219 cx.spawn(|mut cx| {
3220 let fs = app_state.fs.clone();
3221 async move {
3222 let file_project_path = project_path?;
3223 if fs.is_file(&abs_path).await {
3224 Some((
3225 i,
3226 workspace
3227 .update(&mut cx, |workspace, cx| {
3228 workspace.open_path(file_project_path, None, true, cx)
3229 })
3230 .log_err()?
3231 .await,
3232 ))
3233 } else {
3234 None
3235 }
3236 }
3237 })
3238 });
3239
3240 for maybe_opened_path in futures::future::join_all(tasks.into_iter())
3241 .await
3242 .into_iter()
3243 {
3244 if let Some((i, path_open_result)) = maybe_opened_path {
3245 opened_items[i] = Some(path_open_result);
3246 }
3247 }
3248
3249 opened_items
3250}
3251
3252fn notify_of_new_dock(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
3253 const NEW_PANEL_BLOG_POST: &str = "https://zed.dev/blog/new-panel-system";
3254 const NEW_DOCK_HINT_KEY: &str = "show_new_dock_key";
3255 const MESSAGE_ID: usize = 2;
3256
3257 if workspace
3258 .read_with(cx, |workspace, cx| {
3259 workspace.has_shown_notification_once::<MessageNotification>(MESSAGE_ID, cx)
3260 })
3261 .unwrap_or(false)
3262 {
3263 return;
3264 }
3265
3266 if db::kvp::KEY_VALUE_STORE
3267 .read_kvp(NEW_DOCK_HINT_KEY)
3268 .ok()
3269 .flatten()
3270 .is_some()
3271 {
3272 if !workspace
3273 .read_with(cx, |workspace, cx| {
3274 workspace.has_shown_notification_once::<MessageNotification>(MESSAGE_ID, cx)
3275 })
3276 .unwrap_or(false)
3277 {
3278 cx.update(|cx| {
3279 cx.update_global::<NotificationTracker, _, _>(|tracker, _| {
3280 let entry = tracker
3281 .entry(TypeId::of::<MessageNotification>())
3282 .or_default();
3283 if !entry.contains(&MESSAGE_ID) {
3284 entry.push(MESSAGE_ID);
3285 }
3286 });
3287 });
3288 }
3289
3290 return;
3291 }
3292
3293 cx.spawn(|_| async move {
3294 db::kvp::KEY_VALUE_STORE
3295 .write_kvp(NEW_DOCK_HINT_KEY.to_string(), "seen".to_string())
3296 .await
3297 .ok();
3298 })
3299 .detach();
3300
3301 workspace
3302 .update(cx, |workspace, cx| {
3303 workspace.show_notification_once(2, cx, |cx| {
3304 cx.add_view(|_| {
3305 MessageNotification::new_element(|text, _| {
3306 Text::new(
3307 "Looking for the dock? Try ctrl-`!\nshift-escape now zooms your pane.",
3308 text,
3309 )
3310 .with_custom_runs(vec![26..32, 34..46], |_, bounds, scene, cx| {
3311 let code_span_background_color = settings::get::<ThemeSettings>(cx)
3312 .theme
3313 .editor
3314 .document_highlight_read_background;
3315
3316 scene.push_quad(gpui::Quad {
3317 bounds,
3318 background: Some(code_span_background_color),
3319 border: Default::default(),
3320 corner_radius: 2.0,
3321 })
3322 })
3323 .into_any()
3324 })
3325 .with_click_message("Read more about the new panel system")
3326 .on_click(|cx| cx.platform().open_url(NEW_PANEL_BLOG_POST))
3327 })
3328 })
3329 })
3330 .ok();
3331}
3332
3333fn notify_if_database_failed(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
3334 const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
3335
3336 workspace
3337 .update(cx, |workspace, cx| {
3338 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
3339 workspace.show_notification_once(0, cx, |cx| {
3340 cx.add_view(|_| {
3341 MessageNotification::new("Failed to load any database file.")
3342 .with_click_message("Click to let us know about this error")
3343 .on_click(|cx| cx.platform().open_url(REPORT_ISSUE_URL))
3344 })
3345 });
3346 } else {
3347 let backup_path = (*db::BACKUP_DB_PATH).read();
3348 if let Some(backup_path) = backup_path.clone() {
3349 workspace.show_notification_once(1, cx, move |cx| {
3350 cx.add_view(move |_| {
3351 MessageNotification::new(format!(
3352 "Database file was corrupted. Old database backed up to {}",
3353 backup_path.display()
3354 ))
3355 .with_click_message("Click to show old database in finder")
3356 .on_click(move |cx| {
3357 cx.platform().open_url(&backup_path.to_string_lossy())
3358 })
3359 })
3360 });
3361 }
3362 }
3363 })
3364 .log_err();
3365}
3366
3367impl Entity for Workspace {
3368 type Event = Event;
3369}
3370
3371impl View for Workspace {
3372 fn ui_name() -> &'static str {
3373 "Workspace"
3374 }
3375
3376 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
3377 let theme = theme::current(cx).clone();
3378 Stack::new()
3379 .with_child(
3380 Flex::column()
3381 .with_child(self.render_titlebar(&theme, cx))
3382 .with_child(
3383 Stack::new()
3384 .with_child({
3385 let project = self.project.clone();
3386 Flex::row()
3387 .with_children(self.render_dock(DockPosition::Left, cx))
3388 .with_child(
3389 Flex::column()
3390 .with_child(
3391 FlexItem::new(
3392 self.center.render(
3393 &project,
3394 &theme,
3395 &self.follower_states_by_leader,
3396 self.active_call(),
3397 self.active_pane(),
3398 self.zoomed
3399 .as_ref()
3400 .and_then(|zoomed| zoomed.upgrade(cx))
3401 .as_ref(),
3402 &self.app_state,
3403 cx,
3404 ),
3405 )
3406 .flex(1., true),
3407 )
3408 .with_children(
3409 self.render_dock(DockPosition::Bottom, cx),
3410 )
3411 .flex(1., true),
3412 )
3413 .with_children(self.render_dock(DockPosition::Right, cx))
3414 })
3415 .with_child(Overlay::new(
3416 Stack::new()
3417 .with_children(self.zoomed.as_ref().and_then(|zoomed| {
3418 enum ZoomBackground {}
3419 let zoomed = zoomed.upgrade(cx)?;
3420
3421 let mut foreground_style =
3422 theme.workspace.zoomed_pane_foreground;
3423 if let Some(zoomed_dock_position) = self.zoomed_position {
3424 foreground_style =
3425 theme.workspace.zoomed_panel_foreground;
3426 let margin = foreground_style.margin.top;
3427 let border = foreground_style.border.top;
3428
3429 // Only include a margin and border on the opposite side.
3430 foreground_style.margin.top = 0.;
3431 foreground_style.margin.left = 0.;
3432 foreground_style.margin.bottom = 0.;
3433 foreground_style.margin.right = 0.;
3434 foreground_style.border.top = false;
3435 foreground_style.border.left = false;
3436 foreground_style.border.bottom = false;
3437 foreground_style.border.right = false;
3438 match zoomed_dock_position {
3439 DockPosition::Left => {
3440 foreground_style.margin.right = margin;
3441 foreground_style.border.right = border;
3442 }
3443 DockPosition::Right => {
3444 foreground_style.margin.left = margin;
3445 foreground_style.border.left = border;
3446 }
3447 DockPosition::Bottom => {
3448 foreground_style.margin.top = margin;
3449 foreground_style.border.top = border;
3450 }
3451 }
3452 }
3453
3454 Some(
3455 ChildView::new(&zoomed, cx)
3456 .contained()
3457 .with_style(foreground_style)
3458 .aligned()
3459 .contained()
3460 .with_style(theme.workspace.zoomed_background)
3461 .mouse::<ZoomBackground>(0)
3462 .capture_all()
3463 .on_down(
3464 MouseButton::Left,
3465 |_, this: &mut Self, cx| {
3466 this.zoom_out(cx);
3467 },
3468 ),
3469 )
3470 }))
3471 .with_children(self.modal.as_ref().map(|modal| {
3472 ChildView::new(modal, cx)
3473 .contained()
3474 .with_style(theme.workspace.modal)
3475 .aligned()
3476 .top()
3477 }))
3478 .with_children(self.render_notifications(&theme.workspace, cx)),
3479 ))
3480 .flex(1.0, true),
3481 )
3482 .with_child(ChildView::new(&self.status_bar, cx))
3483 .contained()
3484 .with_background_color(theme.workspace.background),
3485 )
3486 .with_children(DragAndDrop::render(cx))
3487 .with_children(self.render_disconnected_overlay(cx))
3488 .into_any_named("workspace")
3489 }
3490
3491 fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
3492 if cx.is_self_focused() {
3493 cx.focus(&self.active_pane);
3494 }
3495 }
3496}
3497
3498impl ViewId {
3499 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
3500 Ok(Self {
3501 creator: message
3502 .creator
3503 .ok_or_else(|| anyhow!("creator is missing"))?,
3504 id: message.id,
3505 })
3506 }
3507
3508 pub(crate) fn to_proto(&self) -> proto::ViewId {
3509 proto::ViewId {
3510 creator: Some(self.creator),
3511 id: self.id,
3512 }
3513 }
3514}
3515
3516pub trait WorkspaceHandle {
3517 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
3518}
3519
3520impl WorkspaceHandle for ViewHandle<Workspace> {
3521 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
3522 self.read(cx)
3523 .worktrees(cx)
3524 .flat_map(|worktree| {
3525 let worktree_id = worktree.read(cx).id();
3526 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
3527 worktree_id,
3528 path: f.path.clone(),
3529 })
3530 })
3531 .collect::<Vec<_>>()
3532 }
3533}
3534
3535impl std::fmt::Debug for OpenPaths {
3536 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3537 f.debug_struct("OpenPaths")
3538 .field("paths", &self.paths)
3539 .finish()
3540 }
3541}
3542
3543pub struct WorkspaceCreated(pub WeakViewHandle<Workspace>);
3544
3545pub fn activate_workspace_for_project(
3546 cx: &mut AsyncAppContext,
3547 predicate: impl Fn(&mut Project, &mut ModelContext<Project>) -> bool,
3548) -> Option<WeakViewHandle<Workspace>> {
3549 for window_id in cx.window_ids() {
3550 let handle = cx
3551 .update_window(window_id, |cx| {
3552 if let Some(workspace_handle) = cx.root_view().clone().downcast::<Workspace>() {
3553 let project = workspace_handle.read(cx).project.clone();
3554 if project.update(cx, &predicate) {
3555 cx.activate_window();
3556 return Some(workspace_handle.clone());
3557 }
3558 }
3559 None
3560 })
3561 .flatten();
3562
3563 if let Some(handle) = handle {
3564 return Some(handle.downgrade());
3565 }
3566 }
3567 None
3568}
3569
3570pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
3571 DB.last_workspace().await.log_err().flatten()
3572}
3573
3574#[allow(clippy::type_complexity)]
3575pub fn open_paths(
3576 abs_paths: &[PathBuf],
3577 app_state: &Arc<AppState>,
3578 requesting_window_id: Option<usize>,
3579 cx: &mut AppContext,
3580) -> Task<
3581 Result<(
3582 WeakViewHandle<Workspace>,
3583 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
3584 )>,
3585> {
3586 let app_state = app_state.clone();
3587 let abs_paths = abs_paths.to_vec();
3588 cx.spawn(|mut cx| async move {
3589 // Open paths in existing workspace if possible
3590 let existing = activate_workspace_for_project(&mut cx, |project, cx| {
3591 project.contains_paths(&abs_paths, cx)
3592 });
3593
3594 if let Some(existing) = existing {
3595 Ok((
3596 existing.clone(),
3597 existing
3598 .update(&mut cx, |workspace, cx| {
3599 workspace.open_paths(abs_paths, true, cx)
3600 })?
3601 .await,
3602 ))
3603 } else {
3604 Ok(cx
3605 .update(|cx| {
3606 Workspace::new_local(abs_paths, app_state.clone(), requesting_window_id, cx)
3607 })
3608 .await)
3609 }
3610 })
3611}
3612
3613pub fn open_new(
3614 app_state: &Arc<AppState>,
3615 cx: &mut AppContext,
3616 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static,
3617) -> Task<()> {
3618 let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx);
3619 cx.spawn(|mut cx| async move {
3620 let (workspace, opened_paths) = task.await;
3621
3622 workspace
3623 .update(&mut cx, |workspace, cx| {
3624 if opened_paths.is_empty() {
3625 init(workspace, cx)
3626 }
3627 })
3628 .log_err();
3629 })
3630}
3631
3632pub fn create_and_open_local_file(
3633 path: &'static Path,
3634 cx: &mut ViewContext<Workspace>,
3635 default_content: impl 'static + Send + FnOnce() -> Rope,
3636) -> Task<Result<Box<dyn ItemHandle>>> {
3637 cx.spawn(|workspace, mut cx| async move {
3638 let fs = workspace.read_with(&cx, |workspace, _| workspace.app_state().fs.clone())?;
3639 if !fs.is_file(path).await {
3640 fs.create_file(path, Default::default()).await?;
3641 fs.save(path, &default_content(), Default::default())
3642 .await?;
3643 }
3644
3645 let mut items = workspace
3646 .update(&mut cx, |workspace, cx| {
3647 workspace.with_local_workspace(cx, |workspace, cx| {
3648 workspace.open_paths(vec![path.to_path_buf()], false, cx)
3649 })
3650 })?
3651 .await?
3652 .await;
3653
3654 let item = items.pop().flatten();
3655 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
3656 })
3657}
3658
3659pub fn join_remote_project(
3660 project_id: u64,
3661 follow_user_id: u64,
3662 app_state: Arc<AppState>,
3663 cx: &mut AppContext,
3664) -> Task<Result<()>> {
3665 cx.spawn(|mut cx| async move {
3666 let existing_workspace = cx
3667 .window_ids()
3668 .into_iter()
3669 .filter_map(|window_id| cx.root_view(window_id)?.clone().downcast::<Workspace>())
3670 .find(|workspace| {
3671 cx.read_window(workspace.window_id(), |cx| {
3672 workspace.read(cx).project().read(cx).remote_id() == Some(project_id)
3673 })
3674 .unwrap_or(false)
3675 });
3676
3677 let workspace = if let Some(existing_workspace) = existing_workspace {
3678 existing_workspace.downgrade()
3679 } else {
3680 let active_call = cx.read(ActiveCall::global);
3681 let room = active_call
3682 .read_with(&cx, |call, _| call.room().cloned())
3683 .ok_or_else(|| anyhow!("not in a call"))?;
3684 let project = room
3685 .update(&mut cx, |room, cx| {
3686 room.join_project(
3687 project_id,
3688 app_state.languages.clone(),
3689 app_state.fs.clone(),
3690 cx,
3691 )
3692 })
3693 .await?;
3694
3695 let window_bounds_override = window_bounds_env_override(&cx);
3696 let (_, workspace) = cx.add_window(
3697 (app_state.build_window_options)(
3698 window_bounds_override,
3699 None,
3700 cx.platform().as_ref(),
3701 ),
3702 |cx| Workspace::new(0, project, app_state.clone(), cx),
3703 );
3704 (app_state.initialize_workspace)(
3705 workspace.downgrade(),
3706 false,
3707 app_state.clone(),
3708 cx.clone(),
3709 )
3710 .await
3711 .log_err();
3712
3713 workspace.downgrade()
3714 };
3715
3716 cx.activate_window(workspace.window_id());
3717 cx.platform().activate(true);
3718
3719 workspace.update(&mut cx, |workspace, cx| {
3720 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
3721 let follow_peer_id = room
3722 .read(cx)
3723 .remote_participants()
3724 .iter()
3725 .find(|(_, participant)| participant.user.id == follow_user_id)
3726 .map(|(_, p)| p.peer_id)
3727 .or_else(|| {
3728 // If we couldn't follow the given user, follow the host instead.
3729 let collaborator = workspace
3730 .project()
3731 .read(cx)
3732 .collaborators()
3733 .values()
3734 .find(|collaborator| collaborator.replica_id == 0)?;
3735 Some(collaborator.peer_id)
3736 });
3737
3738 if let Some(follow_peer_id) = follow_peer_id {
3739 if !workspace.is_being_followed(follow_peer_id) {
3740 workspace
3741 .toggle_follow(follow_peer_id, cx)
3742 .map(|follow| follow.detach_and_log_err(cx));
3743 }
3744 }
3745 }
3746 })?;
3747
3748 anyhow::Ok(())
3749 })
3750}
3751
3752pub fn restart(_: &Restart, cx: &mut AppContext) {
3753 let should_confirm = settings::get::<WorkspaceSettings>(cx).confirm_quit;
3754 cx.spawn(|mut cx| async move {
3755 let mut workspaces = cx
3756 .window_ids()
3757 .into_iter()
3758 .filter_map(|window_id| {
3759 Some(
3760 cx.root_view(window_id)?
3761 .clone()
3762 .downcast::<Workspace>()?
3763 .downgrade(),
3764 )
3765 })
3766 .collect::<Vec<_>>();
3767
3768 // If multiple windows have unsaved changes, and need a save prompt,
3769 // prompt in the active window before switching to a different window.
3770 workspaces.sort_by_key(|workspace| !cx.window_is_active(workspace.window_id()));
3771
3772 if let (true, Some(workspace)) = (should_confirm, workspaces.first()) {
3773 let answer = cx.prompt(
3774 workspace.window_id(),
3775 PromptLevel::Info,
3776 "Are you sure you want to restart?",
3777 &["Restart", "Cancel"],
3778 );
3779
3780 if let Some(mut answer) = answer {
3781 let answer = answer.next().await;
3782 if answer != Some(0) {
3783 return Ok(());
3784 }
3785 }
3786 }
3787
3788 // If the user cancels any save prompt, then keep the app open.
3789 for workspace in workspaces {
3790 if !workspace
3791 .update(&mut cx, |workspace, cx| {
3792 workspace.prepare_to_close(true, cx)
3793 })?
3794 .await?
3795 {
3796 return Ok(());
3797 }
3798 }
3799 cx.platform().restart();
3800 anyhow::Ok(())
3801 })
3802 .detach_and_log_err(cx);
3803}
3804
3805fn parse_pixel_position_env_var(value: &str) -> Option<Vector2F> {
3806 let mut parts = value.split(',');
3807 let width: usize = parts.next()?.parse().ok()?;
3808 let height: usize = parts.next()?.parse().ok()?;
3809 Some(vec2f(width as f32, height as f32))
3810}
3811
3812#[cfg(test)]
3813mod tests {
3814 use super::*;
3815 use crate::{
3816 dock::test::{TestPanel, TestPanelEvent},
3817 item::test::{TestItem, TestItemEvent, TestProjectItem},
3818 };
3819 use fs::FakeFs;
3820 use gpui::{executor::Deterministic, test::EmptyView, TestAppContext};
3821 use project::{Project, ProjectEntryId};
3822 use serde_json::json;
3823 use settings::SettingsStore;
3824 use std::{cell::RefCell, rc::Rc};
3825
3826 #[gpui::test]
3827 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
3828 init_test(cx);
3829
3830 let fs = FakeFs::new(cx.background());
3831 let project = Project::test(fs, [], cx).await;
3832 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
3833
3834 // Adding an item with no ambiguity renders the tab without detail.
3835 let item1 = cx.add_view(window_id, |_| {
3836 let mut item = TestItem::new();
3837 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
3838 item
3839 });
3840 workspace.update(cx, |workspace, cx| {
3841 workspace.add_item(Box::new(item1.clone()), cx);
3842 });
3843 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), None));
3844
3845 // Adding an item that creates ambiguity increases the level of detail on
3846 // both tabs.
3847 let item2 = cx.add_view(window_id, |_| {
3848 let mut item = TestItem::new();
3849 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
3850 item
3851 });
3852 workspace.update(cx, |workspace, cx| {
3853 workspace.add_item(Box::new(item2.clone()), cx);
3854 });
3855 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3856 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3857
3858 // Adding an item that creates ambiguity increases the level of detail only
3859 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
3860 // we stop at the highest detail available.
3861 let item3 = cx.add_view(window_id, |_| {
3862 let mut item = TestItem::new();
3863 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
3864 item
3865 });
3866 workspace.update(cx, |workspace, cx| {
3867 workspace.add_item(Box::new(item3.clone()), cx);
3868 });
3869 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3870 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
3871 item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
3872 }
3873
3874 #[gpui::test]
3875 async fn test_tracking_active_path(cx: &mut TestAppContext) {
3876 init_test(cx);
3877
3878 let fs = FakeFs::new(cx.background());
3879 fs.insert_tree(
3880 "/root1",
3881 json!({
3882 "one.txt": "",
3883 "two.txt": "",
3884 }),
3885 )
3886 .await;
3887 fs.insert_tree(
3888 "/root2",
3889 json!({
3890 "three.txt": "",
3891 }),
3892 )
3893 .await;
3894
3895 let project = Project::test(fs, ["root1".as_ref()], cx).await;
3896 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
3897 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3898 let worktree_id = project.read_with(cx, |project, cx| {
3899 project.worktrees(cx).next().unwrap().read(cx).id()
3900 });
3901
3902 let item1 = cx.add_view(window_id, |cx| {
3903 TestItem::new().with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
3904 });
3905 let item2 = cx.add_view(window_id, |cx| {
3906 TestItem::new().with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
3907 });
3908
3909 // Add an item to an empty pane
3910 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
3911 project.read_with(cx, |project, cx| {
3912 assert_eq!(
3913 project.active_entry(),
3914 project
3915 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
3916 .map(|e| e.id)
3917 );
3918 });
3919 assert_eq!(
3920 cx.current_window_title(window_id).as_deref(),
3921 Some("one.txt β root1")
3922 );
3923
3924 // Add a second item to a non-empty pane
3925 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
3926 assert_eq!(
3927 cx.current_window_title(window_id).as_deref(),
3928 Some("two.txt β root1")
3929 );
3930 project.read_with(cx, |project, cx| {
3931 assert_eq!(
3932 project.active_entry(),
3933 project
3934 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
3935 .map(|e| e.id)
3936 );
3937 });
3938
3939 // Close the active item
3940 pane.update(cx, |pane, cx| {
3941 pane.close_active_item(&Default::default(), cx).unwrap()
3942 })
3943 .await
3944 .unwrap();
3945 assert_eq!(
3946 cx.current_window_title(window_id).as_deref(),
3947 Some("one.txt β root1")
3948 );
3949 project.read_with(cx, |project, cx| {
3950 assert_eq!(
3951 project.active_entry(),
3952 project
3953 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
3954 .map(|e| e.id)
3955 );
3956 });
3957
3958 // Add a project folder
3959 project
3960 .update(cx, |project, cx| {
3961 project.find_or_create_local_worktree("/root2", true, cx)
3962 })
3963 .await
3964 .unwrap();
3965 assert_eq!(
3966 cx.current_window_title(window_id).as_deref(),
3967 Some("one.txt β root1, root2")
3968 );
3969
3970 // Remove a project folder
3971 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
3972 assert_eq!(
3973 cx.current_window_title(window_id).as_deref(),
3974 Some("one.txt β root2")
3975 );
3976 }
3977
3978 #[gpui::test]
3979 async fn test_close_window(cx: &mut TestAppContext) {
3980 init_test(cx);
3981
3982 let fs = FakeFs::new(cx.background());
3983 fs.insert_tree("/root", json!({ "one": "" })).await;
3984
3985 let project = Project::test(fs, ["root".as_ref()], cx).await;
3986 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
3987
3988 // When there are no dirty items, there's nothing to do.
3989 let item1 = cx.add_view(window_id, |_| TestItem::new());
3990 workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
3991 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
3992 assert!(task.await.unwrap());
3993
3994 // When there are dirty untitled items, prompt to save each one. If the user
3995 // cancels any prompt, then abort.
3996 let item2 = cx.add_view(window_id, |_| TestItem::new().with_dirty(true));
3997 let item3 = cx.add_view(window_id, |cx| {
3998 TestItem::new()
3999 .with_dirty(true)
4000 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4001 });
4002 workspace.update(cx, |w, cx| {
4003 w.add_item(Box::new(item2.clone()), cx);
4004 w.add_item(Box::new(item3.clone()), cx);
4005 });
4006 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4007 cx.foreground().run_until_parked();
4008 cx.simulate_prompt_answer(window_id, 2 /* cancel */);
4009 cx.foreground().run_until_parked();
4010 assert!(!cx.has_pending_prompt(window_id));
4011 assert!(!task.await.unwrap());
4012 }
4013
4014 #[gpui::test]
4015 async fn test_close_pane_items(cx: &mut TestAppContext) {
4016 init_test(cx);
4017
4018 let fs = FakeFs::new(cx.background());
4019
4020 let project = Project::test(fs, None, cx).await;
4021 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
4022
4023 let item1 = cx.add_view(window_id, |cx| {
4024 TestItem::new()
4025 .with_dirty(true)
4026 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4027 });
4028 let item2 = cx.add_view(window_id, |cx| {
4029 TestItem::new()
4030 .with_dirty(true)
4031 .with_conflict(true)
4032 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
4033 });
4034 let item3 = cx.add_view(window_id, |cx| {
4035 TestItem::new()
4036 .with_dirty(true)
4037 .with_conflict(true)
4038 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
4039 });
4040 let item4 = cx.add_view(window_id, |cx| {
4041 TestItem::new()
4042 .with_dirty(true)
4043 .with_project_items(&[TestProjectItem::new_untitled(cx)])
4044 });
4045 let pane = workspace.update(cx, |workspace, cx| {
4046 workspace.add_item(Box::new(item1.clone()), cx);
4047 workspace.add_item(Box::new(item2.clone()), cx);
4048 workspace.add_item(Box::new(item3.clone()), cx);
4049 workspace.add_item(Box::new(item4.clone()), cx);
4050 workspace.active_pane().clone()
4051 });
4052
4053 let close_items = pane.update(cx, |pane, cx| {
4054 pane.activate_item(1, true, true, cx);
4055 assert_eq!(pane.active_item().unwrap().id(), item2.id());
4056 let item1_id = item1.id();
4057 let item3_id = item3.id();
4058 let item4_id = item4.id();
4059 pane.close_items(cx, move |id| [item1_id, item3_id, item4_id].contains(&id))
4060 });
4061 cx.foreground().run_until_parked();
4062
4063 // There's a prompt to save item 1.
4064 pane.read_with(cx, |pane, _| {
4065 assert_eq!(pane.items_len(), 4);
4066 assert_eq!(pane.active_item().unwrap().id(), item1.id());
4067 });
4068 assert!(cx.has_pending_prompt(window_id));
4069
4070 // Confirm saving item 1.
4071 cx.simulate_prompt_answer(window_id, 0);
4072 cx.foreground().run_until_parked();
4073
4074 // Item 1 is saved. There's a prompt to save item 3.
4075 pane.read_with(cx, |pane, cx| {
4076 assert_eq!(item1.read(cx).save_count, 1);
4077 assert_eq!(item1.read(cx).save_as_count, 0);
4078 assert_eq!(item1.read(cx).reload_count, 0);
4079 assert_eq!(pane.items_len(), 3);
4080 assert_eq!(pane.active_item().unwrap().id(), item3.id());
4081 });
4082 assert!(cx.has_pending_prompt(window_id));
4083
4084 // Cancel saving item 3.
4085 cx.simulate_prompt_answer(window_id, 1);
4086 cx.foreground().run_until_parked();
4087
4088 // Item 3 is reloaded. There's a prompt to save item 4.
4089 pane.read_with(cx, |pane, cx| {
4090 assert_eq!(item3.read(cx).save_count, 0);
4091 assert_eq!(item3.read(cx).save_as_count, 0);
4092 assert_eq!(item3.read(cx).reload_count, 1);
4093 assert_eq!(pane.items_len(), 2);
4094 assert_eq!(pane.active_item().unwrap().id(), item4.id());
4095 });
4096 assert!(cx.has_pending_prompt(window_id));
4097
4098 // Confirm saving item 4.
4099 cx.simulate_prompt_answer(window_id, 0);
4100 cx.foreground().run_until_parked();
4101
4102 // There's a prompt for a path for item 4.
4103 cx.simulate_new_path_selection(|_| Some(Default::default()));
4104 close_items.await.unwrap();
4105
4106 // The requested items are closed.
4107 pane.read_with(cx, |pane, cx| {
4108 assert_eq!(item4.read(cx).save_count, 0);
4109 assert_eq!(item4.read(cx).save_as_count, 1);
4110 assert_eq!(item4.read(cx).reload_count, 0);
4111 assert_eq!(pane.items_len(), 1);
4112 assert_eq!(pane.active_item().unwrap().id(), item2.id());
4113 });
4114 }
4115
4116 #[gpui::test]
4117 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
4118 init_test(cx);
4119
4120 let fs = FakeFs::new(cx.background());
4121
4122 let project = Project::test(fs, [], cx).await;
4123 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
4124
4125 // Create several workspace items with single project entries, and two
4126 // workspace items with multiple project entries.
4127 let single_entry_items = (0..=4)
4128 .map(|project_entry_id| {
4129 cx.add_view(window_id, |cx| {
4130 TestItem::new()
4131 .with_dirty(true)
4132 .with_project_items(&[TestProjectItem::new(
4133 project_entry_id,
4134 &format!("{project_entry_id}.txt"),
4135 cx,
4136 )])
4137 })
4138 })
4139 .collect::<Vec<_>>();
4140 let item_2_3 = cx.add_view(window_id, |cx| {
4141 TestItem::new()
4142 .with_dirty(true)
4143 .with_singleton(false)
4144 .with_project_items(&[
4145 single_entry_items[2].read(cx).project_items[0].clone(),
4146 single_entry_items[3].read(cx).project_items[0].clone(),
4147 ])
4148 });
4149 let item_3_4 = cx.add_view(window_id, |cx| {
4150 TestItem::new()
4151 .with_dirty(true)
4152 .with_singleton(false)
4153 .with_project_items(&[
4154 single_entry_items[3].read(cx).project_items[0].clone(),
4155 single_entry_items[4].read(cx).project_items[0].clone(),
4156 ])
4157 });
4158
4159 // Create two panes that contain the following project entries:
4160 // left pane:
4161 // multi-entry items: (2, 3)
4162 // single-entry items: 0, 1, 2, 3, 4
4163 // right pane:
4164 // single-entry items: 1
4165 // multi-entry items: (3, 4)
4166 let left_pane = workspace.update(cx, |workspace, cx| {
4167 let left_pane = workspace.active_pane().clone();
4168 workspace.add_item(Box::new(item_2_3.clone()), cx);
4169 for item in single_entry_items {
4170 workspace.add_item(Box::new(item), cx);
4171 }
4172 left_pane.update(cx, |pane, cx| {
4173 pane.activate_item(2, true, true, cx);
4174 });
4175
4176 workspace
4177 .split_pane(left_pane.clone(), SplitDirection::Right, cx)
4178 .unwrap();
4179
4180 left_pane
4181 });
4182
4183 //Need to cause an effect flush in order to respect new focus
4184 workspace.update(cx, |workspace, cx| {
4185 workspace.add_item(Box::new(item_3_4.clone()), cx);
4186 cx.focus(&left_pane);
4187 });
4188
4189 // When closing all of the items in the left pane, we should be prompted twice:
4190 // once for project entry 0, and once for project entry 2. After those two
4191 // prompts, the task should complete.
4192
4193 let close = left_pane.update(cx, |pane, cx| pane.close_items(cx, |_| true));
4194 cx.foreground().run_until_parked();
4195 left_pane.read_with(cx, |pane, cx| {
4196 assert_eq!(
4197 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
4198 &[ProjectEntryId::from_proto(0)]
4199 );
4200 });
4201 cx.simulate_prompt_answer(window_id, 0);
4202
4203 cx.foreground().run_until_parked();
4204 left_pane.read_with(cx, |pane, cx| {
4205 assert_eq!(
4206 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
4207 &[ProjectEntryId::from_proto(2)]
4208 );
4209 });
4210 cx.simulate_prompt_answer(window_id, 0);
4211
4212 cx.foreground().run_until_parked();
4213 close.await.unwrap();
4214 left_pane.read_with(cx, |pane, _| {
4215 assert_eq!(pane.items_len(), 0);
4216 });
4217 }
4218
4219 #[gpui::test]
4220 async fn test_autosave(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
4221 init_test(cx);
4222
4223 let fs = FakeFs::new(cx.background());
4224
4225 let project = Project::test(fs, [], cx).await;
4226 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
4227 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4228
4229 let item = cx.add_view(window_id, |cx| {
4230 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4231 });
4232 let item_id = item.id();
4233 workspace.update(cx, |workspace, cx| {
4234 workspace.add_item(Box::new(item.clone()), cx);
4235 });
4236
4237 // Autosave on window change.
4238 item.update(cx, |item, cx| {
4239 cx.update_global(|settings: &mut SettingsStore, cx| {
4240 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4241 settings.autosave = Some(AutosaveSetting::OnWindowChange);
4242 })
4243 });
4244 item.is_dirty = true;
4245 });
4246
4247 // Deactivating the window saves the file.
4248 cx.simulate_window_activation(None);
4249 deterministic.run_until_parked();
4250 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
4251
4252 // Autosave on focus change.
4253 item.update(cx, |item, cx| {
4254 cx.focus_self();
4255 cx.update_global(|settings: &mut SettingsStore, cx| {
4256 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4257 settings.autosave = Some(AutosaveSetting::OnFocusChange);
4258 })
4259 });
4260 item.is_dirty = true;
4261 });
4262
4263 // Blurring the item saves the file.
4264 item.update(cx, |_, cx| cx.blur());
4265 deterministic.run_until_parked();
4266 item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
4267
4268 // Deactivating the window still saves the file.
4269 cx.simulate_window_activation(Some(window_id));
4270 item.update(cx, |item, cx| {
4271 cx.focus_self();
4272 item.is_dirty = true;
4273 });
4274 cx.simulate_window_activation(None);
4275
4276 deterministic.run_until_parked();
4277 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
4278
4279 // Autosave after delay.
4280 item.update(cx, |item, cx| {
4281 cx.update_global(|settings: &mut SettingsStore, cx| {
4282 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4283 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
4284 })
4285 });
4286 item.is_dirty = true;
4287 cx.emit(TestItemEvent::Edit);
4288 });
4289
4290 // Delay hasn't fully expired, so the file is still dirty and unsaved.
4291 deterministic.advance_clock(Duration::from_millis(250));
4292 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
4293
4294 // After delay expires, the file is saved.
4295 deterministic.advance_clock(Duration::from_millis(250));
4296 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
4297
4298 // Autosave on focus change, ensuring closing the tab counts as such.
4299 item.update(cx, |item, cx| {
4300 cx.update_global(|settings: &mut SettingsStore, cx| {
4301 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4302 settings.autosave = Some(AutosaveSetting::OnFocusChange);
4303 })
4304 });
4305 item.is_dirty = true;
4306 });
4307
4308 pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id))
4309 .await
4310 .unwrap();
4311 assert!(!cx.has_pending_prompt(window_id));
4312 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
4313
4314 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
4315 workspace.update(cx, |workspace, cx| {
4316 workspace.add_item(Box::new(item.clone()), cx);
4317 });
4318 item.update(cx, |item, cx| {
4319 item.project_items[0].update(cx, |item, _| {
4320 item.entry_id = None;
4321 });
4322 item.is_dirty = true;
4323 cx.blur();
4324 });
4325 deterministic.run_until_parked();
4326 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
4327
4328 // Ensure autosave is prevented for deleted files also when closing the buffer.
4329 let _close_items =
4330 pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id));
4331 deterministic.run_until_parked();
4332 assert!(cx.has_pending_prompt(window_id));
4333 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
4334 }
4335
4336 #[gpui::test]
4337 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
4338 init_test(cx);
4339
4340 let fs = FakeFs::new(cx.background());
4341
4342 let project = Project::test(fs, [], cx).await;
4343 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
4344
4345 let item = cx.add_view(window_id, |cx| {
4346 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4347 });
4348 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4349 let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
4350 let toolbar_notify_count = Rc::new(RefCell::new(0));
4351
4352 workspace.update(cx, |workspace, cx| {
4353 workspace.add_item(Box::new(item.clone()), cx);
4354 let toolbar_notification_count = toolbar_notify_count.clone();
4355 cx.observe(&toolbar, move |_, _, _| {
4356 *toolbar_notification_count.borrow_mut() += 1
4357 })
4358 .detach();
4359 });
4360
4361 pane.read_with(cx, |pane, _| {
4362 assert!(!pane.can_navigate_backward());
4363 assert!(!pane.can_navigate_forward());
4364 });
4365
4366 item.update(cx, |item, cx| {
4367 item.set_state("one".to_string(), cx);
4368 });
4369
4370 // Toolbar must be notified to re-render the navigation buttons
4371 assert_eq!(*toolbar_notify_count.borrow(), 1);
4372
4373 pane.read_with(cx, |pane, _| {
4374 assert!(pane.can_navigate_backward());
4375 assert!(!pane.can_navigate_forward());
4376 });
4377
4378 workspace
4379 .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
4380 .await
4381 .unwrap();
4382
4383 assert_eq!(*toolbar_notify_count.borrow(), 3);
4384 pane.read_with(cx, |pane, _| {
4385 assert!(!pane.can_navigate_backward());
4386 assert!(pane.can_navigate_forward());
4387 });
4388 }
4389
4390 #[gpui::test]
4391 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
4392 init_test(cx);
4393 let fs = FakeFs::new(cx.background());
4394
4395 let project = Project::test(fs, [], cx).await;
4396 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
4397
4398 let panel = workspace.update(cx, |workspace, cx| {
4399 let panel = cx.add_view(|_| TestPanel::new(DockPosition::Right));
4400 workspace.add_panel(panel.clone(), cx);
4401
4402 workspace
4403 .right_dock()
4404 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
4405
4406 panel
4407 });
4408
4409 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4410 pane.update(cx, |pane, cx| {
4411 let item = cx.add_view(|_| TestItem::new());
4412 pane.add_item(Box::new(item), true, true, None, cx);
4413 });
4414
4415 // Transfer focus from center to panel
4416 workspace.update(cx, |workspace, cx| {
4417 workspace.toggle_panel_focus::<TestPanel>(cx);
4418 });
4419
4420 workspace.read_with(cx, |workspace, cx| {
4421 assert!(workspace.right_dock().read(cx).is_open());
4422 assert!(!panel.is_zoomed(cx));
4423 assert!(panel.has_focus(cx));
4424 });
4425
4426 // Transfer focus from panel to center
4427 workspace.update(cx, |workspace, cx| {
4428 workspace.toggle_panel_focus::<TestPanel>(cx);
4429 });
4430
4431 workspace.read_with(cx, |workspace, cx| {
4432 assert!(workspace.right_dock().read(cx).is_open());
4433 assert!(!panel.is_zoomed(cx));
4434 assert!(!panel.has_focus(cx));
4435 });
4436
4437 // Close the dock
4438 workspace.update(cx, |workspace, cx| {
4439 workspace.toggle_dock(DockPosition::Right, cx);
4440 });
4441
4442 workspace.read_with(cx, |workspace, cx| {
4443 assert!(!workspace.right_dock().read(cx).is_open());
4444 assert!(!panel.is_zoomed(cx));
4445 assert!(!panel.has_focus(cx));
4446 });
4447
4448 // Open the dock
4449 workspace.update(cx, |workspace, cx| {
4450 workspace.toggle_dock(DockPosition::Right, cx);
4451 });
4452
4453 workspace.read_with(cx, |workspace, cx| {
4454 assert!(workspace.right_dock().read(cx).is_open());
4455 assert!(!panel.is_zoomed(cx));
4456 assert!(!panel.has_focus(cx));
4457 });
4458
4459 // Focus and zoom panel
4460 panel.update(cx, |panel, cx| {
4461 cx.focus_self();
4462 panel.set_zoomed(true, cx)
4463 });
4464
4465 workspace.read_with(cx, |workspace, cx| {
4466 assert!(workspace.right_dock().read(cx).is_open());
4467 assert!(panel.is_zoomed(cx));
4468 assert!(panel.has_focus(cx));
4469 });
4470
4471 // Transfer focus to the center closes the dock
4472 workspace.update(cx, |workspace, cx| {
4473 workspace.toggle_panel_focus::<TestPanel>(cx);
4474 });
4475
4476 workspace.read_with(cx, |workspace, cx| {
4477 assert!(!workspace.right_dock().read(cx).is_open());
4478 assert!(panel.is_zoomed(cx));
4479 assert!(!panel.has_focus(cx));
4480 });
4481
4482 // Transferring focus back to the panel keeps it zoomed
4483 workspace.update(cx, |workspace, cx| {
4484 workspace.toggle_panel_focus::<TestPanel>(cx);
4485 });
4486
4487 workspace.read_with(cx, |workspace, cx| {
4488 assert!(workspace.right_dock().read(cx).is_open());
4489 assert!(panel.is_zoomed(cx));
4490 assert!(panel.has_focus(cx));
4491 });
4492
4493 // Close the dock while it is zoomed
4494 workspace.update(cx, |workspace, cx| {
4495 workspace.toggle_dock(DockPosition::Right, cx)
4496 });
4497
4498 workspace.read_with(cx, |workspace, cx| {
4499 assert!(!workspace.right_dock().read(cx).is_open());
4500 assert!(panel.is_zoomed(cx));
4501 assert!(workspace.zoomed.is_none());
4502 assert!(!panel.has_focus(cx));
4503 });
4504
4505 // Opening the dock, when it's zoomed, retains focus
4506 workspace.update(cx, |workspace, cx| {
4507 workspace.toggle_dock(DockPosition::Right, cx)
4508 });
4509
4510 workspace.read_with(cx, |workspace, cx| {
4511 assert!(workspace.right_dock().read(cx).is_open());
4512 assert!(panel.is_zoomed(cx));
4513 assert!(workspace.zoomed.is_some());
4514 assert!(panel.has_focus(cx));
4515 });
4516
4517 // Unzoom and close the panel, zoom the active pane.
4518 panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
4519 workspace.update(cx, |workspace, cx| {
4520 workspace.toggle_dock(DockPosition::Right, cx)
4521 });
4522 pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
4523
4524 // Opening a dock unzooms the pane.
4525 workspace.update(cx, |workspace, cx| {
4526 workspace.toggle_dock(DockPosition::Right, cx)
4527 });
4528 workspace.read_with(cx, |workspace, cx| {
4529 let pane = pane.read(cx);
4530 assert!(!pane.is_zoomed());
4531 assert!(pane.has_focus());
4532 assert!(workspace.right_dock().read(cx).is_open());
4533 assert!(workspace.zoomed.is_none());
4534 });
4535 }
4536
4537 #[gpui::test]
4538 async fn test_panels(cx: &mut gpui::TestAppContext) {
4539 init_test(cx);
4540 let fs = FakeFs::new(cx.background());
4541
4542 let project = Project::test(fs, [], cx).await;
4543 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
4544
4545 let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
4546 // Add panel_1 on the left, panel_2 on the right.
4547 let panel_1 = cx.add_view(|_| TestPanel::new(DockPosition::Left));
4548 workspace.add_panel(panel_1.clone(), cx);
4549 workspace
4550 .left_dock()
4551 .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
4552 let panel_2 = cx.add_view(|_| TestPanel::new(DockPosition::Right));
4553 workspace.add_panel(panel_2.clone(), cx);
4554 workspace
4555 .right_dock()
4556 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
4557
4558 let left_dock = workspace.left_dock();
4559 assert_eq!(
4560 left_dock.read(cx).visible_panel().unwrap().id(),
4561 panel_1.id()
4562 );
4563 assert_eq!(
4564 left_dock.read(cx).active_panel_size(cx).unwrap(),
4565 panel_1.size(cx)
4566 );
4567
4568 left_dock.update(cx, |left_dock, cx| left_dock.resize_active_panel(1337., cx));
4569 assert_eq!(
4570 workspace
4571 .right_dock()
4572 .read(cx)
4573 .visible_panel()
4574 .unwrap()
4575 .id(),
4576 panel_2.id()
4577 );
4578
4579 (panel_1, panel_2)
4580 });
4581
4582 // Move panel_1 to the right
4583 panel_1.update(cx, |panel_1, cx| {
4584 panel_1.set_position(DockPosition::Right, cx)
4585 });
4586
4587 workspace.update(cx, |workspace, cx| {
4588 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
4589 // Since it was the only panel on the left, the left dock should now be closed.
4590 assert!(!workspace.left_dock().read(cx).is_open());
4591 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
4592 let right_dock = workspace.right_dock();
4593 assert_eq!(
4594 right_dock.read(cx).visible_panel().unwrap().id(),
4595 panel_1.id()
4596 );
4597 assert_eq!(right_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
4598
4599 // Now we move panel_2Β to the left
4600 panel_2.set_position(DockPosition::Left, cx);
4601 });
4602
4603 workspace.update(cx, |workspace, cx| {
4604 // Since panel_2 was not visible on the right, we don't open the left dock.
4605 assert!(!workspace.left_dock().read(cx).is_open());
4606 // And the right dock is unaffected in it's displaying of panel_1
4607 assert!(workspace.right_dock().read(cx).is_open());
4608 assert_eq!(
4609 workspace
4610 .right_dock()
4611 .read(cx)
4612 .visible_panel()
4613 .unwrap()
4614 .id(),
4615 panel_1.id()
4616 );
4617 });
4618
4619 // Move panel_1 back to the left
4620 panel_1.update(cx, |panel_1, cx| {
4621 panel_1.set_position(DockPosition::Left, cx)
4622 });
4623
4624 workspace.update(cx, |workspace, cx| {
4625 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
4626 let left_dock = workspace.left_dock();
4627 assert!(left_dock.read(cx).is_open());
4628 assert_eq!(
4629 left_dock.read(cx).visible_panel().unwrap().id(),
4630 panel_1.id()
4631 );
4632 assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
4633 // And right the dock should be closed as it no longer has any panels.
4634 assert!(!workspace.right_dock().read(cx).is_open());
4635
4636 // Now we move panel_1 to the bottom
4637 panel_1.set_position(DockPosition::Bottom, cx);
4638 });
4639
4640 workspace.update(cx, |workspace, cx| {
4641 // Since panel_1 was visible on the left, we close the left dock.
4642 assert!(!workspace.left_dock().read(cx).is_open());
4643 // The bottom dock is sized based on the panel's default size,
4644 // since the panel orientation changed from vertical to horizontal.
4645 let bottom_dock = workspace.bottom_dock();
4646 assert_eq!(
4647 bottom_dock.read(cx).active_panel_size(cx).unwrap(),
4648 panel_1.size(cx),
4649 );
4650 // Close bottom dock and move panel_1 back to the left.
4651 bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
4652 panel_1.set_position(DockPosition::Left, cx);
4653 });
4654
4655 // Emit activated event on panel 1
4656 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Activated));
4657
4658 // Now the left dock is open and panel_1 is active and focused.
4659 workspace.read_with(cx, |workspace, cx| {
4660 let left_dock = workspace.left_dock();
4661 assert!(left_dock.read(cx).is_open());
4662 assert_eq!(
4663 left_dock.read(cx).visible_panel().unwrap().id(),
4664 panel_1.id()
4665 );
4666 assert!(panel_1.is_focused(cx));
4667 });
4668
4669 // Emit closed event on panel 2, which is not active
4670 panel_2.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed));
4671
4672 // Wo don't close the left dock, because panel_2 wasn't the active panel
4673 workspace.read_with(cx, |workspace, cx| {
4674 let left_dock = workspace.left_dock();
4675 assert!(left_dock.read(cx).is_open());
4676 assert_eq!(
4677 left_dock.read(cx).visible_panel().unwrap().id(),
4678 panel_1.id()
4679 );
4680 });
4681
4682 // Emitting a ZoomIn event shows the panel as zoomed.
4683 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomIn));
4684 workspace.read_with(cx, |workspace, _| {
4685 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
4686 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
4687 });
4688
4689 // Move panel to another dock while it is zoomed
4690 panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
4691 workspace.read_with(cx, |workspace, _| {
4692 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
4693 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
4694 });
4695
4696 // If focus is transferred to another view that's not a panel or another pane, we still show
4697 // the panel as zoomed.
4698 let focus_receiver = cx.add_view(window_id, |_| EmptyView);
4699 focus_receiver.update(cx, |_, cx| cx.focus_self());
4700 workspace.read_with(cx, |workspace, _| {
4701 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
4702 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
4703 });
4704
4705 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
4706 workspace.update(cx, |_, cx| cx.focus_self());
4707 workspace.read_with(cx, |workspace, _| {
4708 assert_eq!(workspace.zoomed, None);
4709 assert_eq!(workspace.zoomed_position, None);
4710 });
4711
4712 // If focus is transferred again to another view that's not a panel or a pane, we won't
4713 // show the panel as zoomed because it wasn't zoomed before.
4714 focus_receiver.update(cx, |_, cx| cx.focus_self());
4715 workspace.read_with(cx, |workspace, _| {
4716 assert_eq!(workspace.zoomed, None);
4717 assert_eq!(workspace.zoomed_position, None);
4718 });
4719
4720 // When focus is transferred back to the panel, it is zoomed again.
4721 panel_1.update(cx, |_, cx| cx.focus_self());
4722 workspace.read_with(cx, |workspace, _| {
4723 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
4724 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
4725 });
4726
4727 // Emitting a ZoomOut event unzooms the panel.
4728 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomOut));
4729 workspace.read_with(cx, |workspace, _| {
4730 assert_eq!(workspace.zoomed, None);
4731 assert_eq!(workspace.zoomed_position, None);
4732 });
4733
4734 // Emit closed event on panel 1, which is active
4735 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed));
4736
4737 // Now the left dock is closed, because panel_1 was the active panel
4738 workspace.read_with(cx, |workspace, cx| {
4739 let right_dock = workspace.right_dock();
4740 assert!(!right_dock.read(cx).is_open());
4741 });
4742 }
4743
4744 pub fn init_test(cx: &mut TestAppContext) {
4745 cx.foreground().forbid_parking();
4746 cx.update(|cx| {
4747 cx.set_global(SettingsStore::test(cx));
4748 theme::init((), cx);
4749 language::init(cx);
4750 crate::init_settings(cx);
4751 });
4752 }
4753}