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