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