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()
1275 .telemetry()
1276 .report_app_event("open project".to_string());
1277 let paths = cx.prompt_for_paths(PathPromptOptions {
1278 files: true,
1279 directories: true,
1280 multiple: true,
1281 });
1282
1283 cx.spawn(|this, mut cx| async move {
1284 let Some(paths) = paths.await.log_err().flatten() else {
1285 return;
1286 };
1287
1288 if let Some(task) = this
1289 .update(&mut cx, |this, cx| this.open_workspace_for_paths(paths, cx))
1290 .log_err()
1291 {
1292 task.await.log_err();
1293 }
1294 })
1295 .detach()
1296 }
1297
1298 pub fn open_workspace_for_paths(
1299 &mut self,
1300 paths: Vec<PathBuf>,
1301 cx: &mut ViewContext<Self>,
1302 ) -> Task<Result<()>> {
1303 let window = cx.window_handle().downcast::<Self>();
1304 let is_remote = self.project.read(cx).is_remote();
1305 let has_worktree = self.project.read(cx).worktrees().next().is_some();
1306 let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx));
1307 let close_task = if is_remote || has_worktree || has_dirty_items {
1308 None
1309 } else {
1310 Some(self.prepare_to_close(false, cx))
1311 };
1312 let app_state = self.app_state.clone();
1313
1314 cx.spawn(|_, mut cx| async move {
1315 let window_to_replace = if let Some(close_task) = close_task {
1316 if !close_task.await? {
1317 return Ok(());
1318 }
1319 window
1320 } else {
1321 None
1322 };
1323 cx.update(|_, cx| open_paths(&paths, &app_state, window_to_replace, cx))?
1324 .await?;
1325 Ok(())
1326 })
1327 }
1328
1329 #[allow(clippy::type_complexity)]
1330 pub fn open_paths(
1331 &mut self,
1332 mut abs_paths: Vec<PathBuf>,
1333 visible: OpenVisible,
1334 pane: Option<WeakView<Pane>>,
1335 cx: &mut ViewContext<Self>,
1336 ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
1337 log::info!("open paths {abs_paths:?}");
1338
1339 let fs = self.app_state.fs.clone();
1340
1341 // Sort the paths to ensure we add worktrees for parents before their children.
1342 abs_paths.sort_unstable();
1343 cx.spawn(move |this, mut cx| async move {
1344 let mut tasks = Vec::with_capacity(abs_paths.len());
1345
1346 for abs_path in &abs_paths {
1347 let visible = match visible {
1348 OpenVisible::All => Some(true),
1349 OpenVisible::None => Some(false),
1350 OpenVisible::OnlyFiles => match fs.metadata(abs_path).await.log_err() {
1351 Some(Some(metadata)) => Some(!metadata.is_dir),
1352 Some(None) => {
1353 log::error!("No metadata for file {abs_path:?}");
1354 None
1355 }
1356 None => None,
1357 },
1358 OpenVisible::OnlyDirectories => match fs.metadata(abs_path).await.log_err() {
1359 Some(Some(metadata)) => Some(metadata.is_dir),
1360 Some(None) => {
1361 log::error!("No metadata for file {abs_path:?}");
1362 None
1363 }
1364 None => None,
1365 },
1366 };
1367 let project_path = match visible {
1368 Some(visible) => match this
1369 .update(&mut cx, |this, cx| {
1370 Workspace::project_path_for_path(
1371 this.project.clone(),
1372 abs_path,
1373 visible,
1374 cx,
1375 )
1376 })
1377 .log_err()
1378 {
1379 Some(project_path) => project_path.await.log_err(),
1380 None => None,
1381 },
1382 None => None,
1383 };
1384
1385 let this = this.clone();
1386 let abs_path = abs_path.clone();
1387 let fs = fs.clone();
1388 let pane = pane.clone();
1389 let task = cx.spawn(move |mut cx| async move {
1390 let (worktree, project_path) = project_path?;
1391 if fs.is_file(&abs_path).await {
1392 Some(
1393 this.update(&mut cx, |this, cx| {
1394 this.open_path(project_path, pane, true, cx)
1395 })
1396 .log_err()?
1397 .await,
1398 )
1399 } else {
1400 this.update(&mut cx, |workspace, cx| {
1401 let worktree = worktree.read(cx);
1402 let worktree_abs_path = worktree.abs_path();
1403 let entry_id = if abs_path == worktree_abs_path.as_ref() {
1404 worktree.root_entry()
1405 } else {
1406 abs_path
1407 .strip_prefix(worktree_abs_path.as_ref())
1408 .ok()
1409 .and_then(|relative_path| {
1410 worktree.entry_for_path(relative_path)
1411 })
1412 }
1413 .map(|entry| entry.id);
1414 if let Some(entry_id) = entry_id {
1415 workspace.project.update(cx, |_, cx| {
1416 cx.emit(project::Event::ActiveEntryChanged(Some(entry_id)));
1417 })
1418 }
1419 })
1420 .log_err()?;
1421 None
1422 }
1423 });
1424 tasks.push(task);
1425 }
1426
1427 futures::future::join_all(tasks).await
1428 })
1429 }
1430
1431 fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext<Self>) {
1432 let paths = cx.prompt_for_paths(PathPromptOptions {
1433 files: false,
1434 directories: true,
1435 multiple: true,
1436 });
1437 cx.spawn(|this, mut cx| async move {
1438 if let Some(paths) = paths.await.log_err().flatten() {
1439 let results = this
1440 .update(&mut cx, |this, cx| {
1441 this.open_paths(paths, OpenVisible::All, None, cx)
1442 })?
1443 .await;
1444 for result in results.into_iter().flatten() {
1445 result.log_err();
1446 }
1447 }
1448 anyhow::Ok(())
1449 })
1450 .detach_and_log_err(cx);
1451 }
1452
1453 fn project_path_for_path(
1454 project: Model<Project>,
1455 abs_path: &Path,
1456 visible: bool,
1457 cx: &mut AppContext,
1458 ) -> Task<Result<(Model<Worktree>, ProjectPath)>> {
1459 let entry = project.update(cx, |project, cx| {
1460 project.find_or_create_local_worktree(abs_path, visible, cx)
1461 });
1462 cx.spawn(|mut cx| async move {
1463 let (worktree, path) = entry.await?;
1464 let worktree_id = worktree.update(&mut cx, |t, _| t.id())?;
1465 Ok((
1466 worktree,
1467 ProjectPath {
1468 worktree_id,
1469 path: path.into(),
1470 },
1471 ))
1472 })
1473 }
1474
1475 pub fn items<'a>(
1476 &'a self,
1477 cx: &'a AppContext,
1478 ) -> impl 'a + Iterator<Item = &Box<dyn ItemHandle>> {
1479 self.panes.iter().flat_map(|pane| pane.read(cx).items())
1480 }
1481
1482 pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<View<T>> {
1483 self.items_of_type(cx).max_by_key(|item| item.item_id())
1484 }
1485
1486 pub fn items_of_type<'a, T: Item>(
1487 &'a self,
1488 cx: &'a AppContext,
1489 ) -> impl 'a + Iterator<Item = View<T>> {
1490 self.panes
1491 .iter()
1492 .flat_map(|pane| pane.read(cx).items_of_type())
1493 }
1494
1495 pub fn active_item(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
1496 self.active_pane().read(cx).active_item()
1497 }
1498
1499 pub fn active_item_as<I: 'static>(&self, cx: &AppContext) -> Option<View<I>> {
1500 let item = self.active_item(cx)?;
1501 item.to_any().downcast::<I>().ok()
1502 }
1503
1504 fn active_project_path(&self, cx: &ViewContext<Self>) -> Option<ProjectPath> {
1505 self.active_item(cx).and_then(|item| item.project_path(cx))
1506 }
1507
1508 pub fn save_active_item(
1509 &mut self,
1510 save_intent: SaveIntent,
1511 cx: &mut ViewContext<Self>,
1512 ) -> Task<Result<()>> {
1513 let project = self.project.clone();
1514 let pane = self.active_pane();
1515 let item_ix = pane.read(cx).active_item_index();
1516 let item = pane.read(cx).active_item();
1517 let pane = pane.downgrade();
1518
1519 cx.spawn(|_, mut cx| async move {
1520 if let Some(item) = item {
1521 Pane::save_item(project, &pane, item_ix, item.as_ref(), save_intent, &mut cx)
1522 .await
1523 .map(|_| ())
1524 } else {
1525 Ok(())
1526 }
1527 })
1528 }
1529
1530 pub fn close_inactive_items_and_panes(
1531 &mut self,
1532 _: &CloseInactiveTabsAndPanes,
1533 cx: &mut ViewContext<Self>,
1534 ) {
1535 self.close_all_internal(true, SaveIntent::Close, cx)
1536 .map(|task| task.detach_and_log_err(cx));
1537 }
1538
1539 pub fn close_all_items_and_panes(
1540 &mut self,
1541 action: &CloseAllItemsAndPanes,
1542 cx: &mut ViewContext<Self>,
1543 ) {
1544 self.close_all_internal(false, action.save_intent.unwrap_or(SaveIntent::Close), cx)
1545 .map(|task| task.detach_and_log_err(cx));
1546 }
1547
1548 fn close_all_internal(
1549 &mut self,
1550 retain_active_pane: bool,
1551 save_intent: SaveIntent,
1552 cx: &mut ViewContext<Self>,
1553 ) -> Option<Task<Result<()>>> {
1554 let current_pane = self.active_pane();
1555
1556 let mut tasks = Vec::new();
1557
1558 if retain_active_pane {
1559 if let Some(current_pane_close) = current_pane.update(cx, |pane, cx| {
1560 pane.close_inactive_items(&CloseInactiveItems, cx)
1561 }) {
1562 tasks.push(current_pane_close);
1563 };
1564 }
1565
1566 for pane in self.panes() {
1567 if retain_active_pane && pane.entity_id() == current_pane.entity_id() {
1568 continue;
1569 }
1570
1571 if let Some(close_pane_items) = pane.update(cx, |pane: &mut Pane, cx| {
1572 pane.close_all_items(
1573 &CloseAllItems {
1574 save_intent: Some(save_intent),
1575 },
1576 cx,
1577 )
1578 }) {
1579 tasks.push(close_pane_items)
1580 }
1581 }
1582
1583 if tasks.is_empty() {
1584 None
1585 } else {
1586 Some(cx.spawn(|_, _| async move {
1587 for task in tasks {
1588 task.await?
1589 }
1590 Ok(())
1591 }))
1592 }
1593 }
1594
1595 pub fn toggle_dock(&mut self, dock_side: DockPosition, cx: &mut ViewContext<Self>) {
1596 let dock = match dock_side {
1597 DockPosition::Left => &self.left_dock,
1598 DockPosition::Bottom => &self.bottom_dock,
1599 DockPosition::Right => &self.right_dock,
1600 };
1601 let mut focus_center = false;
1602 let mut reveal_dock = false;
1603 dock.update(cx, |dock, cx| {
1604 let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side);
1605 let was_visible = dock.is_open() && !other_is_zoomed;
1606 dock.set_open(!was_visible, cx);
1607
1608 if let Some(active_panel) = dock.active_panel() {
1609 if was_visible {
1610 if active_panel.focus_handle(cx).contains_focused(cx) {
1611 focus_center = true;
1612 }
1613 } else {
1614 let focus_handle = &active_panel.focus_handle(cx);
1615 cx.focus(focus_handle);
1616 reveal_dock = true;
1617 }
1618 }
1619 });
1620
1621 if reveal_dock {
1622 self.dismiss_zoomed_items_to_reveal(Some(dock_side), cx);
1623 }
1624
1625 if focus_center {
1626 self.active_pane.update(cx, |pane, cx| pane.focus(cx))
1627 }
1628
1629 cx.notify();
1630 self.serialize_workspace(cx);
1631 }
1632
1633 pub fn close_all_docks(&mut self, cx: &mut ViewContext<Self>) {
1634 let docks = [&self.left_dock, &self.bottom_dock, &self.right_dock];
1635
1636 for dock in docks {
1637 dock.update(cx, |dock, cx| {
1638 dock.set_open(false, cx);
1639 });
1640 }
1641
1642 cx.focus_self();
1643 cx.notify();
1644 self.serialize_workspace(cx);
1645 }
1646
1647 /// Transfer focus to the panel of the given type.
1648 pub fn focus_panel<T: Panel>(&mut self, cx: &mut ViewContext<Self>) -> Option<View<T>> {
1649 let panel = self.focus_or_unfocus_panel::<T>(cx, |_, _| true)?;
1650 panel.to_any().downcast().ok()
1651 }
1652
1653 /// Focus the panel of the given type if it isn't already focused. If it is
1654 /// already focused, then transfer focus back to the workspace center.
1655 pub fn toggle_panel_focus<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
1656 self.focus_or_unfocus_panel::<T>(cx, |panel, cx| {
1657 !panel.focus_handle(cx).contains_focused(cx)
1658 });
1659 }
1660
1661 /// Focus or unfocus the given panel type, depending on the given callback.
1662 fn focus_or_unfocus_panel<T: Panel>(
1663 &mut self,
1664 cx: &mut ViewContext<Self>,
1665 should_focus: impl Fn(&dyn PanelHandle, &mut ViewContext<Dock>) -> bool,
1666 ) -> Option<Arc<dyn PanelHandle>> {
1667 for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
1668 if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
1669 let mut focus_center = false;
1670 let panel = dock.update(cx, |dock, cx| {
1671 dock.activate_panel(panel_index, cx);
1672
1673 let panel = dock.active_panel().cloned();
1674 if let Some(panel) = panel.as_ref() {
1675 if should_focus(&**panel, cx) {
1676 dock.set_open(true, cx);
1677 panel.focus_handle(cx).focus(cx);
1678 } else {
1679 focus_center = true;
1680 }
1681 }
1682 panel
1683 });
1684
1685 if focus_center {
1686 self.active_pane.update(cx, |pane, cx| pane.focus(cx))
1687 }
1688
1689 self.serialize_workspace(cx);
1690 cx.notify();
1691 return panel;
1692 }
1693 }
1694 None
1695 }
1696
1697 pub fn panel<T: Panel>(&self, cx: &WindowContext) -> Option<View<T>> {
1698 for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
1699 let dock = dock.read(cx);
1700 if let Some(panel) = dock.panel::<T>() {
1701 return Some(panel);
1702 }
1703 }
1704 None
1705 }
1706
1707 fn dismiss_zoomed_items_to_reveal(
1708 &mut self,
1709 dock_to_reveal: Option<DockPosition>,
1710 cx: &mut ViewContext<Self>,
1711 ) {
1712 // If a center pane is zoomed, unzoom it.
1713 for pane in &self.panes {
1714 if pane != &self.active_pane || dock_to_reveal.is_some() {
1715 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
1716 }
1717 }
1718
1719 // If another dock is zoomed, hide it.
1720 let mut focus_center = false;
1721 for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] {
1722 dock.update(cx, |dock, cx| {
1723 if Some(dock.position()) != dock_to_reveal {
1724 if let Some(panel) = dock.active_panel() {
1725 if panel.is_zoomed(cx) {
1726 focus_center |= panel.focus_handle(cx).contains_focused(cx);
1727 dock.set_open(false, cx);
1728 }
1729 }
1730 }
1731 });
1732 }
1733
1734 if focus_center {
1735 self.active_pane.update(cx, |pane, cx| pane.focus(cx))
1736 }
1737
1738 if self.zoomed_position != dock_to_reveal {
1739 self.zoomed = None;
1740 self.zoomed_position = None;
1741 }
1742
1743 cx.notify();
1744 }
1745
1746 fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> View<Pane> {
1747 let pane = cx.new_view(|cx| {
1748 Pane::new(
1749 self.weak_handle(),
1750 self.project.clone(),
1751 self.pane_history_timestamp.clone(),
1752 None,
1753 cx,
1754 )
1755 });
1756 cx.subscribe(&pane, Self::handle_pane_event).detach();
1757 self.panes.push(pane.clone());
1758 cx.focus_view(&pane);
1759 cx.emit(Event::PaneAdded(pane.clone()));
1760 pane
1761 }
1762
1763 pub fn add_item_to_center(
1764 &mut self,
1765 item: Box<dyn ItemHandle>,
1766 cx: &mut ViewContext<Self>,
1767 ) -> bool {
1768 if let Some(center_pane) = self.last_active_center_pane.clone() {
1769 if let Some(center_pane) = center_pane.upgrade() {
1770 center_pane.update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
1771 true
1772 } else {
1773 false
1774 }
1775 } else {
1776 false
1777 }
1778 }
1779
1780 pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
1781 if let Some(text) = item.telemetry_event_text(cx) {
1782 self.client()
1783 .telemetry()
1784 .report_app_event(format!("{}: open", text));
1785 }
1786
1787 self.active_pane
1788 .update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
1789 }
1790
1791 pub fn split_item(
1792 &mut self,
1793 split_direction: SplitDirection,
1794 item: Box<dyn ItemHandle>,
1795 cx: &mut ViewContext<Self>,
1796 ) {
1797 let new_pane = self.split_pane(self.active_pane.clone(), split_direction, cx);
1798 new_pane.update(cx, move |new_pane, cx| {
1799 new_pane.add_item(item, true, true, None, cx)
1800 })
1801 }
1802
1803 pub fn open_abs_path(
1804 &mut self,
1805 abs_path: PathBuf,
1806 visible: bool,
1807 cx: &mut ViewContext<Self>,
1808 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
1809 cx.spawn(|workspace, mut cx| async move {
1810 let open_paths_task_result = workspace
1811 .update(&mut cx, |workspace, cx| {
1812 workspace.open_paths(
1813 vec![abs_path.clone()],
1814 if visible {
1815 OpenVisible::All
1816 } else {
1817 OpenVisible::None
1818 },
1819 None,
1820 cx,
1821 )
1822 })
1823 .with_context(|| format!("open abs path {abs_path:?} task spawn"))?
1824 .await;
1825 anyhow::ensure!(
1826 open_paths_task_result.len() == 1,
1827 "open abs path {abs_path:?} task returned incorrect number of results"
1828 );
1829 match open_paths_task_result
1830 .into_iter()
1831 .next()
1832 .expect("ensured single task result")
1833 {
1834 Some(open_result) => {
1835 open_result.with_context(|| format!("open abs path {abs_path:?} task join"))
1836 }
1837 None => anyhow::bail!("open abs path {abs_path:?} task returned None"),
1838 }
1839 })
1840 }
1841
1842 pub fn split_abs_path(
1843 &mut self,
1844 abs_path: PathBuf,
1845 visible: bool,
1846 cx: &mut ViewContext<Self>,
1847 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
1848 let project_path_task =
1849 Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx);
1850 cx.spawn(|this, mut cx| async move {
1851 let (_, path) = project_path_task.await?;
1852 this.update(&mut cx, |this, cx| this.split_path(path, cx))?
1853 .await
1854 })
1855 }
1856
1857 pub fn open_path(
1858 &mut self,
1859 path: impl Into<ProjectPath>,
1860 pane: Option<WeakView<Pane>>,
1861 focus_item: bool,
1862 cx: &mut ViewContext<Self>,
1863 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
1864 let pane = pane.unwrap_or_else(|| {
1865 self.last_active_center_pane.clone().unwrap_or_else(|| {
1866 self.panes
1867 .first()
1868 .expect("There must be an active pane")
1869 .downgrade()
1870 })
1871 });
1872
1873 let task = self.load_path(path.into(), cx);
1874 cx.spawn(move |_, mut cx| async move {
1875 let (project_entry_id, build_item) = task.await?;
1876 pane.update(&mut cx, |pane, cx| {
1877 pane.open_item(project_entry_id, focus_item, cx, build_item)
1878 })
1879 })
1880 }
1881
1882 pub fn split_path(
1883 &mut self,
1884 path: impl Into<ProjectPath>,
1885 cx: &mut ViewContext<Self>,
1886 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
1887 let pane = self.last_active_center_pane.clone().unwrap_or_else(|| {
1888 self.panes
1889 .first()
1890 .expect("There must be an active pane")
1891 .downgrade()
1892 });
1893
1894 if let Member::Pane(center_pane) = &self.center.root {
1895 if center_pane.read(cx).items_len() == 0 {
1896 return self.open_path(path, Some(pane), true, cx);
1897 }
1898 }
1899
1900 let task = self.load_path(path.into(), cx);
1901 cx.spawn(|this, mut cx| async move {
1902 let (project_entry_id, build_item) = task.await?;
1903 this.update(&mut cx, move |this, cx| -> Option<_> {
1904 let pane = pane.upgrade()?;
1905 let new_pane = this.split_pane(pane, SplitDirection::Right, cx);
1906 new_pane.update(cx, |new_pane, cx| {
1907 Some(new_pane.open_item(project_entry_id, true, cx, build_item))
1908 })
1909 })
1910 .map(|option| option.ok_or_else(|| anyhow!("pane was dropped")))?
1911 })
1912 }
1913
1914 fn load_path(
1915 &mut self,
1916 path: ProjectPath,
1917 cx: &mut ViewContext<Self>,
1918 ) -> Task<
1919 Result<(
1920 Option<ProjectEntryId>,
1921 impl 'static + Send + FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
1922 )>,
1923 > {
1924 let project = self.project().clone();
1925 let project_item = project.update(cx, |project, cx| project.open_path(path, cx));
1926 cx.spawn(|_, mut cx| async move {
1927 let (project_entry_id, project_item) = project_item.await?;
1928 let build_item = cx.update(|_, cx| {
1929 cx.default_global::<ProjectItemBuilders>()
1930 .get(&project_item.entity_type())
1931 .ok_or_else(|| anyhow!("no item builder for project item"))
1932 .cloned()
1933 })??;
1934 let build_item =
1935 move |cx: &mut ViewContext<Pane>| build_item(project, project_item, cx);
1936 Ok((project_entry_id, build_item))
1937 })
1938 }
1939
1940 pub fn open_project_item<T>(
1941 &mut self,
1942 project_item: Model<T::Item>,
1943 cx: &mut ViewContext<Self>,
1944 ) -> View<T>
1945 where
1946 T: ProjectItem,
1947 {
1948 use project::Item as _;
1949
1950 let entry_id = project_item.read(cx).entry_id(cx);
1951 if let Some(item) = entry_id
1952 .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx))
1953 .and_then(|item| item.downcast())
1954 {
1955 self.activate_item(&item, cx);
1956 return item;
1957 }
1958
1959 let item = cx.new_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
1960 self.add_item(Box::new(item.clone()), cx);
1961 item
1962 }
1963
1964 pub fn split_project_item<T>(
1965 &mut self,
1966 project_item: Model<T::Item>,
1967 cx: &mut ViewContext<Self>,
1968 ) -> View<T>
1969 where
1970 T: ProjectItem,
1971 {
1972 use project::Item as _;
1973
1974 let entry_id = project_item.read(cx).entry_id(cx);
1975 if let Some(item) = entry_id
1976 .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx))
1977 .and_then(|item| item.downcast())
1978 {
1979 self.activate_item(&item, cx);
1980 return item;
1981 }
1982
1983 let item = cx.new_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
1984 self.split_item(SplitDirection::Right, Box::new(item.clone()), cx);
1985 item
1986 }
1987
1988 pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
1989 if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &self.active_pane, cx) {
1990 self.active_pane.update(cx, |pane, cx| {
1991 pane.add_item(Box::new(shared_screen), false, true, None, cx)
1992 });
1993 }
1994 }
1995
1996 pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut ViewContext<Self>) -> bool {
1997 let result = self.panes.iter().find_map(|pane| {
1998 pane.read(cx)
1999 .index_for_item(item)
2000 .map(|ix| (pane.clone(), ix))
2001 });
2002 if let Some((pane, ix)) = result {
2003 pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx));
2004 true
2005 } else {
2006 false
2007 }
2008 }
2009
2010 fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext<Self>) {
2011 let panes = self.center.panes();
2012 if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
2013 cx.focus_view(&pane);
2014 } else {
2015 self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, cx);
2016 }
2017 }
2018
2019 pub fn activate_next_pane(&mut self, cx: &mut ViewContext<Self>) {
2020 let panes = self.center.panes();
2021 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
2022 let next_ix = (ix + 1) % panes.len();
2023 let next_pane = panes[next_ix].clone();
2024 cx.focus_view(&next_pane);
2025 }
2026 }
2027
2028 pub fn activate_previous_pane(&mut self, cx: &mut ViewContext<Self>) {
2029 let panes = self.center.panes();
2030 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
2031 let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
2032 let prev_pane = panes[prev_ix].clone();
2033 cx.focus_view(&prev_pane);
2034 }
2035 }
2036
2037 pub fn activate_pane_in_direction(
2038 &mut self,
2039 direction: SplitDirection,
2040 cx: &mut ViewContext<Self>,
2041 ) {
2042 if let Some(pane) = self.find_pane_in_direction(direction, cx) {
2043 cx.focus_view(pane);
2044 }
2045 }
2046
2047 pub fn swap_pane_in_direction(
2048 &mut self,
2049 direction: SplitDirection,
2050 cx: &mut ViewContext<Self>,
2051 ) {
2052 if let Some(to) = self
2053 .find_pane_in_direction(direction, cx)
2054 .map(|pane| pane.clone())
2055 {
2056 self.center.swap(&self.active_pane.clone(), &to);
2057 cx.notify();
2058 }
2059 }
2060
2061 fn find_pane_in_direction(
2062 &mut self,
2063 direction: SplitDirection,
2064 cx: &mut ViewContext<Self>,
2065 ) -> Option<&View<Pane>> {
2066 let Some(bounding_box) = self.center.bounding_box_for_pane(&self.active_pane) else {
2067 return None;
2068 };
2069 let cursor = self.active_pane.read(cx).pixel_position_of_cursor(cx);
2070 let center = match cursor {
2071 Some(cursor) if bounding_box.contains(&cursor) => cursor,
2072 _ => bounding_box.center(),
2073 };
2074
2075 let distance_to_next = pane_group::HANDLE_HITBOX_SIZE;
2076
2077 let target = match direction {
2078 SplitDirection::Left => {
2079 Point::new(bounding_box.left() - distance_to_next.into(), center.y)
2080 }
2081 SplitDirection::Right => {
2082 Point::new(bounding_box.right() + distance_to_next.into(), center.y)
2083 }
2084 SplitDirection::Up => {
2085 Point::new(center.x, bounding_box.top() - distance_to_next.into())
2086 }
2087 SplitDirection::Down => {
2088 Point::new(center.x, bounding_box.bottom() + distance_to_next.into())
2089 }
2090 };
2091 self.center.pane_at_pixel_position(target)
2092 }
2093
2094 fn handle_pane_focused(&mut self, pane: View<Pane>, cx: &mut ViewContext<Self>) {
2095 if self.active_pane != pane {
2096 self.active_pane = pane.clone();
2097 self.status_bar.update(cx, |status_bar, cx| {
2098 status_bar.set_active_pane(&self.active_pane, cx);
2099 });
2100 self.active_item_path_changed(cx);
2101 self.last_active_center_pane = Some(pane.downgrade());
2102 }
2103
2104 self.dismiss_zoomed_items_to_reveal(None, cx);
2105 if pane.read(cx).is_zoomed() {
2106 self.zoomed = Some(pane.downgrade().into());
2107 } else {
2108 self.zoomed = None;
2109 }
2110 self.zoomed_position = None;
2111 self.update_active_view_for_followers(cx);
2112
2113 cx.notify();
2114 }
2115
2116 fn handle_pane_event(
2117 &mut self,
2118 pane: View<Pane>,
2119 event: &pane::Event,
2120 cx: &mut ViewContext<Self>,
2121 ) {
2122 match event {
2123 pane::Event::AddItem { item } => item.added_to_pane(self, pane, cx),
2124 pane::Event::Split(direction) => {
2125 self.split_and_clone(pane, *direction, cx);
2126 }
2127 pane::Event::Remove => self.remove_pane(pane, cx),
2128 pane::Event::ActivateItem { local } => {
2129 if *local {
2130 self.unfollow(&pane, cx);
2131 }
2132 if &pane == self.active_pane() {
2133 self.active_item_path_changed(cx);
2134 self.update_active_view_for_followers(cx);
2135 }
2136 }
2137 pane::Event::ChangeItemTitle => {
2138 if pane == self.active_pane {
2139 self.active_item_path_changed(cx);
2140 }
2141 self.update_window_edited(cx);
2142 }
2143 pane::Event::RemoveItem { item_id } => {
2144 self.update_window_edited(cx);
2145 if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) {
2146 if entry.get().entity_id() == pane.entity_id() {
2147 entry.remove();
2148 }
2149 }
2150 }
2151 pane::Event::Focus => {
2152 self.handle_pane_focused(pane.clone(), cx);
2153 }
2154 pane::Event::ZoomIn => {
2155 if pane == self.active_pane {
2156 pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
2157 if pane.read(cx).has_focus(cx) {
2158 self.zoomed = Some(pane.downgrade().into());
2159 self.zoomed_position = None;
2160 }
2161 cx.notify();
2162 }
2163 }
2164 pane::Event::ZoomOut => {
2165 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
2166 if self.zoomed_position.is_none() {
2167 self.zoomed = None;
2168 }
2169 cx.notify();
2170 }
2171 }
2172
2173 self.serialize_workspace(cx);
2174 }
2175
2176 pub fn split_pane(
2177 &mut self,
2178 pane_to_split: View<Pane>,
2179 split_direction: SplitDirection,
2180 cx: &mut ViewContext<Self>,
2181 ) -> View<Pane> {
2182 let new_pane = self.add_pane(cx);
2183 self.center
2184 .split(&pane_to_split, &new_pane, split_direction)
2185 .unwrap();
2186 cx.notify();
2187 new_pane
2188 }
2189
2190 pub fn split_and_clone(
2191 &mut self,
2192 pane: View<Pane>,
2193 direction: SplitDirection,
2194 cx: &mut ViewContext<Self>,
2195 ) -> Option<View<Pane>> {
2196 let item = pane.read(cx).active_item()?;
2197 let maybe_pane_handle = if let Some(clone) = item.clone_on_split(self.database_id(), cx) {
2198 let new_pane = self.add_pane(cx);
2199 new_pane.update(cx, |pane, cx| pane.add_item(clone, true, true, None, cx));
2200 self.center.split(&pane, &new_pane, direction).unwrap();
2201 Some(new_pane)
2202 } else {
2203 None
2204 };
2205 cx.notify();
2206 maybe_pane_handle
2207 }
2208
2209 pub fn split_pane_with_item(
2210 &mut self,
2211 pane_to_split: WeakView<Pane>,
2212 split_direction: SplitDirection,
2213 from: WeakView<Pane>,
2214 item_id_to_move: EntityId,
2215 cx: &mut ViewContext<Self>,
2216 ) {
2217 let Some(pane_to_split) = pane_to_split.upgrade() else {
2218 return;
2219 };
2220 let Some(from) = from.upgrade() else {
2221 return;
2222 };
2223
2224 let new_pane = self.add_pane(cx);
2225 self.move_item(from.clone(), new_pane.clone(), item_id_to_move, 0, cx);
2226 self.center
2227 .split(&pane_to_split, &new_pane, split_direction)
2228 .unwrap();
2229 cx.notify();
2230 }
2231
2232 pub fn split_pane_with_project_entry(
2233 &mut self,
2234 pane_to_split: WeakView<Pane>,
2235 split_direction: SplitDirection,
2236 project_entry: ProjectEntryId,
2237 cx: &mut ViewContext<Self>,
2238 ) -> Option<Task<Result<()>>> {
2239 let pane_to_split = pane_to_split.upgrade()?;
2240 let new_pane = self.add_pane(cx);
2241 self.center
2242 .split(&pane_to_split, &new_pane, split_direction)
2243 .unwrap();
2244
2245 let path = self.project.read(cx).path_for_entry(project_entry, cx)?;
2246 let task = self.open_path(path, Some(new_pane.downgrade()), true, cx);
2247 Some(cx.foreground_executor().spawn(async move {
2248 task.await?;
2249 Ok(())
2250 }))
2251 }
2252
2253 pub fn move_item(
2254 &mut self,
2255 source: View<Pane>,
2256 destination: View<Pane>,
2257 item_id_to_move: EntityId,
2258 destination_index: usize,
2259 cx: &mut ViewContext<Self>,
2260 ) {
2261 let Some((item_ix, item_handle)) = source
2262 .read(cx)
2263 .items()
2264 .enumerate()
2265 .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
2266 else {
2267 // Tab was closed during drag
2268 return;
2269 };
2270
2271 let item_handle = item_handle.clone();
2272
2273 if source != destination {
2274 // Close item from previous pane
2275 source.update(cx, |source, cx| {
2276 source.remove_item(item_ix, false, cx);
2277 });
2278 }
2279
2280 // This automatically removes duplicate items in the pane
2281 destination.update(cx, |destination, cx| {
2282 destination.add_item(item_handle, true, true, Some(destination_index), cx);
2283 destination.focus(cx)
2284 });
2285 }
2286
2287 fn remove_pane(&mut self, pane: View<Pane>, cx: &mut ViewContext<Self>) {
2288 if self.center.remove(&pane).unwrap() {
2289 self.force_remove_pane(&pane, cx);
2290 self.unfollow(&pane, cx);
2291 self.last_leaders_by_pane.remove(&pane.downgrade());
2292 for removed_item in pane.read(cx).items() {
2293 self.panes_by_item.remove(&removed_item.item_id());
2294 }
2295
2296 cx.notify();
2297 } else {
2298 self.active_item_path_changed(cx);
2299 }
2300 }
2301
2302 pub fn panes(&self) -> &[View<Pane>] {
2303 &self.panes
2304 }
2305
2306 pub fn active_pane(&self) -> &View<Pane> {
2307 &self.active_pane
2308 }
2309
2310 pub fn pane_for(&self, handle: &dyn ItemHandle) -> Option<View<Pane>> {
2311 let weak_pane = self.panes_by_item.get(&handle.item_id())?;
2312 weak_pane.upgrade()
2313 }
2314
2315 fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
2316 self.follower_states.retain(|_, state| {
2317 if state.leader_id == peer_id {
2318 for item in state.items_by_leader_view_id.values() {
2319 item.set_leader_peer_id(None, cx);
2320 }
2321 false
2322 } else {
2323 true
2324 }
2325 });
2326 cx.notify();
2327 }
2328
2329 pub fn start_following(
2330 &mut self,
2331 leader_id: PeerId,
2332 cx: &mut ViewContext<Self>,
2333 ) -> Option<Task<Result<()>>> {
2334 let pane = self.active_pane().clone();
2335
2336 self.last_leaders_by_pane
2337 .insert(pane.downgrade(), leader_id);
2338 self.unfollow(&pane, cx);
2339 self.follower_states.insert(
2340 pane.clone(),
2341 FollowerState {
2342 leader_id,
2343 active_view_id: None,
2344 items_by_leader_view_id: Default::default(),
2345 },
2346 );
2347 cx.notify();
2348
2349 let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
2350 let project_id = self.project.read(cx).remote_id();
2351 let request = self.app_state.client.request(proto::Follow {
2352 room_id,
2353 project_id,
2354 leader_id: Some(leader_id),
2355 });
2356
2357 Some(cx.spawn(|this, mut cx| async move {
2358 let response = request.await?;
2359 this.update(&mut cx, |this, _| {
2360 let state = this
2361 .follower_states
2362 .get_mut(&pane)
2363 .ok_or_else(|| anyhow!("following interrupted"))?;
2364 state.active_view_id = if let Some(active_view_id) = response.active_view_id {
2365 Some(ViewId::from_proto(active_view_id)?)
2366 } else {
2367 None
2368 };
2369 Ok::<_, anyhow::Error>(())
2370 })??;
2371 Self::add_views_from_leader(
2372 this.clone(),
2373 leader_id,
2374 vec![pane],
2375 response.views,
2376 &mut cx,
2377 )
2378 .await?;
2379 this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?;
2380 Ok(())
2381 }))
2382 }
2383
2384 pub fn follow_next_collaborator(
2385 &mut self,
2386 _: &FollowNextCollaborator,
2387 cx: &mut ViewContext<Self>,
2388 ) {
2389 let collaborators = self.project.read(cx).collaborators();
2390 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
2391 let mut collaborators = collaborators.keys().copied();
2392 for peer_id in collaborators.by_ref() {
2393 if peer_id == leader_id {
2394 break;
2395 }
2396 }
2397 collaborators.next()
2398 } else if let Some(last_leader_id) =
2399 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
2400 {
2401 if collaborators.contains_key(last_leader_id) {
2402 Some(*last_leader_id)
2403 } else {
2404 None
2405 }
2406 } else {
2407 None
2408 };
2409
2410 let pane = self.active_pane.clone();
2411 let Some(leader_id) = next_leader_id.or_else(|| collaborators.keys().copied().next())
2412 else {
2413 return;
2414 };
2415 if Some(leader_id) == self.unfollow(&pane, cx) {
2416 return;
2417 }
2418 self.start_following(leader_id, cx)
2419 .map(|task| task.detach_and_log_err(cx));
2420 }
2421
2422 pub fn follow(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) {
2423 let Some(room) = ActiveCall::global(cx).read(cx).room() else {
2424 return;
2425 };
2426 let room = room.read(cx);
2427 let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else {
2428 return;
2429 };
2430
2431 let project = self.project.read(cx);
2432
2433 let other_project_id = match remote_participant.location {
2434 call::ParticipantLocation::External => None,
2435 call::ParticipantLocation::UnsharedProject => None,
2436 call::ParticipantLocation::SharedProject { project_id } => {
2437 if Some(project_id) == project.remote_id() {
2438 None
2439 } else {
2440 Some(project_id)
2441 }
2442 }
2443 };
2444
2445 // if they are active in another project, follow there.
2446 if let Some(project_id) = other_project_id {
2447 let app_state = self.app_state.clone();
2448 crate::join_remote_project(project_id, remote_participant.user.id, app_state, cx)
2449 .detach_and_log_err(cx);
2450 }
2451
2452 // if you're already following, find the right pane and focus it.
2453 for (pane, state) in &self.follower_states {
2454 if leader_id == state.leader_id {
2455 cx.focus_view(pane);
2456 return;
2457 }
2458 }
2459
2460 // Otherwise, follow.
2461 self.start_following(leader_id, cx)
2462 .map(|task| task.detach_and_log_err(cx));
2463 }
2464
2465 pub fn unfollow(&mut self, pane: &View<Pane>, cx: &mut ViewContext<Self>) -> Option<PeerId> {
2466 let state = self.follower_states.remove(pane)?;
2467 let leader_id = state.leader_id;
2468 for (_, item) in state.items_by_leader_view_id {
2469 item.set_leader_peer_id(None, cx);
2470 }
2471
2472 if self
2473 .follower_states
2474 .values()
2475 .all(|state| state.leader_id != state.leader_id)
2476 {
2477 let project_id = self.project.read(cx).remote_id();
2478 let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
2479 self.app_state
2480 .client
2481 .send(proto::Unfollow {
2482 room_id,
2483 project_id,
2484 leader_id: Some(leader_id),
2485 })
2486 .log_err();
2487 }
2488
2489 cx.notify();
2490 Some(leader_id)
2491 }
2492
2493 pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
2494 self.follower_states
2495 .values()
2496 .any(|state| state.leader_id == peer_id)
2497 }
2498
2499 fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
2500 let active_entry = self.active_project_path(cx);
2501 self.project
2502 .update(cx, |project, cx| project.set_active_path(active_entry, cx));
2503 self.update_window_title(cx);
2504 }
2505
2506 fn update_window_title(&mut self, cx: &mut ViewContext<Self>) {
2507 let project = self.project().read(cx);
2508 let mut title = String::new();
2509
2510 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
2511 let filename = path
2512 .path
2513 .file_name()
2514 .map(|s| s.to_string_lossy())
2515 .or_else(|| {
2516 Some(Cow::Borrowed(
2517 project
2518 .worktree_for_id(path.worktree_id, cx)?
2519 .read(cx)
2520 .root_name(),
2521 ))
2522 });
2523
2524 if let Some(filename) = filename {
2525 title.push_str(filename.as_ref());
2526 title.push_str(" — ");
2527 }
2528 }
2529
2530 for (i, name) in project.worktree_root_names(cx).enumerate() {
2531 if i > 0 {
2532 title.push_str(", ");
2533 }
2534 title.push_str(name);
2535 }
2536
2537 if title.is_empty() {
2538 title = "empty project".to_string();
2539 }
2540
2541 if project.is_remote() {
2542 title.push_str(" ↙");
2543 } else if project.is_shared() {
2544 title.push_str(" ↗");
2545 }
2546
2547 cx.set_window_title(&title);
2548 }
2549
2550 fn update_window_edited(&mut self, cx: &mut ViewContext<Self>) {
2551 let is_edited = !self.project.read(cx).is_disconnected()
2552 && self
2553 .items(cx)
2554 .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
2555 if is_edited != self.window_edited {
2556 self.window_edited = is_edited;
2557 cx.set_window_edited(self.window_edited)
2558 }
2559 }
2560
2561 fn render_notifications(&self, _cx: &ViewContext<Self>) -> Option<Div> {
2562 if self.notifications.is_empty() {
2563 None
2564 } else {
2565 Some(
2566 div()
2567 .absolute()
2568 .z_index(100)
2569 .right_3()
2570 .bottom_3()
2571 .w_96()
2572 .h_full()
2573 .flex()
2574 .flex_col()
2575 .justify_end()
2576 .gap_2()
2577 .children(
2578 self.notifications
2579 .iter()
2580 .map(|(_, _, notification)| notification.to_any()),
2581 ),
2582 )
2583 }
2584 }
2585
2586 // RPC handlers
2587
2588 fn handle_follow(
2589 &mut self,
2590 follower_project_id: Option<u64>,
2591 cx: &mut ViewContext<Self>,
2592 ) -> proto::FollowResponse {
2593 let client = &self.app_state.client;
2594 let project_id = self.project.read(cx).remote_id();
2595
2596 let active_view_id = self.active_item(cx).and_then(|i| {
2597 Some(
2598 i.to_followable_item_handle(cx)?
2599 .remote_id(client, cx)?
2600 .to_proto(),
2601 )
2602 });
2603
2604 cx.notify();
2605
2606 self.last_active_view_id = active_view_id.clone();
2607 proto::FollowResponse {
2608 active_view_id,
2609 views: self
2610 .panes()
2611 .iter()
2612 .flat_map(|pane| {
2613 let leader_id = self.leader_for_pane(pane);
2614 pane.read(cx).items().filter_map({
2615 let cx = &cx;
2616 move |item| {
2617 let item = item.to_followable_item_handle(cx)?;
2618
2619 // If the item belongs to a particular project, then it should
2620 // only be included if this project is shared, and the follower
2621 // is in thie project.
2622 //
2623 // Some items, like channel notes, do not belong to a particular
2624 // project, so they should be included regardless of whether the
2625 // current project is shared, or what project the follower is in.
2626 if item.is_project_item(cx)
2627 && (project_id.is_none() || project_id != follower_project_id)
2628 {
2629 return None;
2630 }
2631
2632 let id = item.remote_id(client, cx)?.to_proto();
2633 let variant = item.to_state_proto(cx)?;
2634 Some(proto::View {
2635 id: Some(id),
2636 leader_id,
2637 variant: Some(variant),
2638 })
2639 }
2640 })
2641 })
2642 .collect(),
2643 }
2644 }
2645
2646 fn handle_update_followers(
2647 &mut self,
2648 leader_id: PeerId,
2649 message: proto::UpdateFollowers,
2650 _cx: &mut ViewContext<Self>,
2651 ) {
2652 self.leader_updates_tx
2653 .unbounded_send((leader_id, message))
2654 .ok();
2655 }
2656
2657 async fn process_leader_update(
2658 this: &WeakView<Self>,
2659 leader_id: PeerId,
2660 update: proto::UpdateFollowers,
2661 cx: &mut AsyncWindowContext,
2662 ) -> Result<()> {
2663 match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
2664 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
2665 this.update(cx, |this, _| {
2666 for (_, state) in &mut this.follower_states {
2667 if state.leader_id == leader_id {
2668 state.active_view_id =
2669 if let Some(active_view_id) = update_active_view.id.clone() {
2670 Some(ViewId::from_proto(active_view_id)?)
2671 } else {
2672 None
2673 };
2674 }
2675 }
2676 anyhow::Ok(())
2677 })??;
2678 }
2679 proto::update_followers::Variant::UpdateView(update_view) => {
2680 let variant = update_view
2681 .variant
2682 .ok_or_else(|| anyhow!("missing update view variant"))?;
2683 let id = update_view
2684 .id
2685 .ok_or_else(|| anyhow!("missing update view id"))?;
2686 let mut tasks = Vec::new();
2687 this.update(cx, |this, cx| {
2688 let project = this.project.clone();
2689 for (_, state) in &mut this.follower_states {
2690 if state.leader_id == leader_id {
2691 let view_id = ViewId::from_proto(id.clone())?;
2692 if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
2693 tasks.push(item.apply_update_proto(&project, variant.clone(), cx));
2694 }
2695 }
2696 }
2697 anyhow::Ok(())
2698 })??;
2699 try_join_all(tasks).await.log_err();
2700 }
2701 proto::update_followers::Variant::CreateView(view) => {
2702 let panes = this.update(cx, |this, _| {
2703 this.follower_states
2704 .iter()
2705 .filter_map(|(pane, state)| (state.leader_id == leader_id).then_some(pane))
2706 .cloned()
2707 .collect()
2708 })?;
2709 Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], cx).await?;
2710 }
2711 }
2712 this.update(cx, |this, cx| this.leader_updated(leader_id, cx))?;
2713 Ok(())
2714 }
2715
2716 async fn add_views_from_leader(
2717 this: WeakView<Self>,
2718 leader_id: PeerId,
2719 panes: Vec<View<Pane>>,
2720 views: Vec<proto::View>,
2721 cx: &mut AsyncWindowContext,
2722 ) -> Result<()> {
2723 let this = this.upgrade().context("workspace dropped")?;
2724
2725 let item_builders = cx.update(|_, cx| {
2726 cx.default_global::<FollowableItemBuilders>()
2727 .values()
2728 .map(|b| b.0)
2729 .collect::<Vec<_>>()
2730 })?;
2731
2732 let mut item_tasks_by_pane = HashMap::default();
2733 for pane in panes {
2734 let mut item_tasks = Vec::new();
2735 let mut leader_view_ids = Vec::new();
2736 for view in &views {
2737 let Some(id) = &view.id else { continue };
2738 let id = ViewId::from_proto(id.clone())?;
2739 let mut variant = view.variant.clone();
2740 if variant.is_none() {
2741 Err(anyhow!("missing view variant"))?;
2742 }
2743 for build_item in &item_builders {
2744 let task = cx.update(|_, cx| {
2745 build_item(pane.clone(), this.clone(), id, &mut variant, cx)
2746 })?;
2747 if let Some(task) = task {
2748 item_tasks.push(task);
2749 leader_view_ids.push(id);
2750 break;
2751 } else {
2752 assert!(variant.is_some());
2753 }
2754 }
2755 }
2756
2757 item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids));
2758 }
2759
2760 for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
2761 let items = futures::future::try_join_all(item_tasks).await?;
2762 this.update(cx, |this, cx| {
2763 let state = this.follower_states.get_mut(&pane)?;
2764 for (id, item) in leader_view_ids.into_iter().zip(items) {
2765 item.set_leader_peer_id(Some(leader_id), cx);
2766 state.items_by_leader_view_id.insert(id, item);
2767 }
2768
2769 Some(())
2770 })?;
2771 }
2772 Ok(())
2773 }
2774
2775 fn update_active_view_for_followers(&mut self, cx: &mut ViewContext<Self>) {
2776 let mut is_project_item = true;
2777 let mut update = proto::UpdateActiveView::default();
2778
2779 if let Some(item) = self.active_item(cx) {
2780 if item.focus_handle(cx).contains_focused(cx) {
2781 if let Some(item) = item.to_followable_item_handle(cx) {
2782 is_project_item = item.is_project_item(cx);
2783 update = proto::UpdateActiveView {
2784 id: item
2785 .remote_id(&self.app_state.client, cx)
2786 .map(|id| id.to_proto()),
2787 leader_id: self.leader_for_pane(&self.active_pane),
2788 };
2789 }
2790 }
2791 }
2792
2793 if update.id != self.last_active_view_id {
2794 self.last_active_view_id = update.id.clone();
2795 self.update_followers(
2796 is_project_item,
2797 proto::update_followers::Variant::UpdateActiveView(update),
2798 cx,
2799 );
2800 }
2801 }
2802
2803 fn update_followers(
2804 &self,
2805 project_only: bool,
2806 update: proto::update_followers::Variant,
2807 cx: &mut WindowContext,
2808 ) -> Option<()> {
2809 // If this update only applies to for followers in the current project,
2810 // then skip it unless this project is shared. If it applies to all
2811 // followers, regardless of project, then set `project_id` to none,
2812 // indicating that it goes to all followers.
2813 let project_id = if project_only {
2814 Some(self.project.read(cx).remote_id()?)
2815 } else {
2816 None
2817 };
2818 self.app_state().workspace_store.update(cx, |store, cx| {
2819 store.update_followers(project_id, update, cx)
2820 })
2821 }
2822
2823 pub fn leader_for_pane(&self, pane: &View<Pane>) -> Option<PeerId> {
2824 self.follower_states.get(pane).map(|state| state.leader_id)
2825 }
2826
2827 fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
2828 cx.notify();
2829
2830 let call = self.active_call()?;
2831 let room = call.read(cx).room()?.read(cx);
2832 let participant = room.remote_participant_for_peer_id(leader_id)?;
2833 let mut items_to_activate = Vec::new();
2834
2835 let leader_in_this_app;
2836 let leader_in_this_project;
2837 match participant.location {
2838 call::ParticipantLocation::SharedProject { project_id } => {
2839 leader_in_this_app = true;
2840 leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
2841 }
2842 call::ParticipantLocation::UnsharedProject => {
2843 leader_in_this_app = true;
2844 leader_in_this_project = false;
2845 }
2846 call::ParticipantLocation::External => {
2847 leader_in_this_app = false;
2848 leader_in_this_project = false;
2849 }
2850 };
2851
2852 for (pane, state) in &self.follower_states {
2853 if state.leader_id != leader_id {
2854 continue;
2855 }
2856 if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
2857 if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) {
2858 if leader_in_this_project || !item.is_project_item(cx) {
2859 items_to_activate.push((pane.clone(), item.boxed_clone()));
2860 }
2861 }
2862 continue;
2863 }
2864
2865 if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
2866 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
2867 }
2868 }
2869
2870 for (pane, item) in items_to_activate {
2871 let pane_was_focused = pane.read(cx).has_focus(cx);
2872 if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) {
2873 pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx));
2874 } else {
2875 pane.update(cx, |pane, cx| {
2876 pane.add_item(item.boxed_clone(), false, false, None, cx)
2877 });
2878 }
2879
2880 if pane_was_focused {
2881 pane.update(cx, |pane, cx| pane.focus_active_item(cx));
2882 }
2883 }
2884
2885 None
2886 }
2887
2888 fn shared_screen_for_peer(
2889 &self,
2890 peer_id: PeerId,
2891 pane: &View<Pane>,
2892 cx: &mut ViewContext<Self>,
2893 ) -> Option<View<SharedScreen>> {
2894 let call = self.active_call()?;
2895 let room = call.read(cx).room()?.read(cx);
2896 let participant = room.remote_participant_for_peer_id(peer_id)?;
2897 let track = participant.video_tracks.values().next()?.clone();
2898 let user = participant.user.clone();
2899
2900 for item in pane.read(cx).items_of_type::<SharedScreen>() {
2901 if item.read(cx).peer_id == peer_id {
2902 return Some(item);
2903 }
2904 }
2905
2906 Some(cx.new_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
2907 }
2908
2909 pub fn on_window_activation_changed(&mut self, cx: &mut ViewContext<Self>) {
2910 if cx.is_window_active() {
2911 self.update_active_view_for_followers(cx);
2912 cx.background_executor()
2913 .spawn(persistence::DB.update_timestamp(self.database_id()))
2914 .detach();
2915 } else {
2916 for pane in &self.panes {
2917 pane.update(cx, |pane, cx| {
2918 if let Some(item) = pane.active_item() {
2919 item.workspace_deactivated(cx);
2920 }
2921 if matches!(
2922 WorkspaceSettings::get_global(cx).autosave,
2923 AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
2924 ) {
2925 for item in pane.items() {
2926 Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
2927 .detach_and_log_err(cx);
2928 }
2929 }
2930 });
2931 }
2932 }
2933 }
2934
2935 fn active_call(&self) -> Option<&Model<ActiveCall>> {
2936 self.active_call.as_ref().map(|(call, _)| call)
2937 }
2938
2939 fn on_active_call_event(
2940 &mut self,
2941 _: Model<ActiveCall>,
2942 event: &call::room::Event,
2943 cx: &mut ViewContext<Self>,
2944 ) {
2945 match event {
2946 call::room::Event::ParticipantLocationChanged { participant_id }
2947 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
2948 self.leader_updated(*participant_id, cx);
2949 }
2950 _ => {}
2951 }
2952 }
2953
2954 pub fn database_id(&self) -> WorkspaceId {
2955 self.database_id
2956 }
2957
2958 fn location(&self, cx: &AppContext) -> Option<WorkspaceLocation> {
2959 let project = self.project().read(cx);
2960
2961 if project.is_local() {
2962 Some(
2963 project
2964 .visible_worktrees(cx)
2965 .map(|worktree| worktree.read(cx).abs_path())
2966 .collect::<Vec<_>>()
2967 .into(),
2968 )
2969 } else {
2970 None
2971 }
2972 }
2973
2974 fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
2975 match member {
2976 Member::Axis(PaneAxis { members, .. }) => {
2977 for child in members.iter() {
2978 self.remove_panes(child.clone(), cx)
2979 }
2980 }
2981 Member::Pane(pane) => {
2982 self.force_remove_pane(&pane, cx);
2983 }
2984 }
2985 }
2986
2987 fn force_remove_pane(&mut self, pane: &View<Pane>, cx: &mut ViewContext<Workspace>) {
2988 self.panes.retain(|p| p != pane);
2989 self.panes
2990 .last()
2991 .unwrap()
2992 .update(cx, |pane, cx| pane.focus(cx));
2993 if self.last_active_center_pane == Some(pane.downgrade()) {
2994 self.last_active_center_pane = None;
2995 }
2996 cx.notify();
2997 }
2998
2999 fn schedule_serialize(&mut self, cx: &mut ViewContext<Self>) {
3000 self._schedule_serialize = Some(cx.spawn(|this, mut cx| async move {
3001 cx.background_executor()
3002 .timer(Duration::from_millis(100))
3003 .await;
3004 this.update(&mut cx, |this, cx| this.serialize_workspace(cx))
3005 .log_err();
3006 }));
3007 }
3008
3009 fn serialize_workspace(&self, cx: &mut ViewContext<Self>) {
3010 fn serialize_pane_handle(pane_handle: &View<Pane>, cx: &WindowContext) -> SerializedPane {
3011 let (items, active) = {
3012 let pane = pane_handle.read(cx);
3013 let active_item_id = pane.active_item().map(|item| item.item_id());
3014 (
3015 pane.items()
3016 .filter_map(|item_handle| {
3017 Some(SerializedItem {
3018 kind: Arc::from(item_handle.serialized_item_kind()?),
3019 item_id: item_handle.item_id().as_u64(),
3020 active: Some(item_handle.item_id()) == active_item_id,
3021 })
3022 })
3023 .collect::<Vec<_>>(),
3024 pane.has_focus(cx),
3025 )
3026 };
3027
3028 SerializedPane::new(items, active)
3029 }
3030
3031 fn build_serialized_pane_group(
3032 pane_group: &Member,
3033 cx: &WindowContext,
3034 ) -> SerializedPaneGroup {
3035 match pane_group {
3036 Member::Axis(PaneAxis {
3037 axis,
3038 members,
3039 flexes,
3040 bounding_boxes: _,
3041 }) => SerializedPaneGroup::Group {
3042 axis: SerializedAxis(*axis),
3043 children: members
3044 .iter()
3045 .map(|member| build_serialized_pane_group(member, cx))
3046 .collect::<Vec<_>>(),
3047 flexes: Some(flexes.lock().clone()),
3048 },
3049 Member::Pane(pane_handle) => {
3050 SerializedPaneGroup::Pane(serialize_pane_handle(&pane_handle, cx))
3051 }
3052 }
3053 }
3054
3055 fn build_serialized_docks(
3056 this: &Workspace,
3057 cx: &mut ViewContext<Workspace>,
3058 ) -> DockStructure {
3059 let left_dock = this.left_dock.read(cx);
3060 let left_visible = left_dock.is_open();
3061 let left_active_panel = left_dock
3062 .visible_panel()
3063 .and_then(|panel| Some(panel.persistent_name().to_string()));
3064 let left_dock_zoom = left_dock
3065 .visible_panel()
3066 .map(|panel| panel.is_zoomed(cx))
3067 .unwrap_or(false);
3068
3069 let right_dock = this.right_dock.read(cx);
3070 let right_visible = right_dock.is_open();
3071 let right_active_panel = right_dock
3072 .visible_panel()
3073 .and_then(|panel| Some(panel.persistent_name().to_string()));
3074 let right_dock_zoom = right_dock
3075 .visible_panel()
3076 .map(|panel| panel.is_zoomed(cx))
3077 .unwrap_or(false);
3078
3079 let bottom_dock = this.bottom_dock.read(cx);
3080 let bottom_visible = bottom_dock.is_open();
3081 let bottom_active_panel = bottom_dock
3082 .visible_panel()
3083 .and_then(|panel| Some(panel.persistent_name().to_string()));
3084 let bottom_dock_zoom = bottom_dock
3085 .visible_panel()
3086 .map(|panel| panel.is_zoomed(cx))
3087 .unwrap_or(false);
3088
3089 DockStructure {
3090 left: DockData {
3091 visible: left_visible,
3092 active_panel: left_active_panel,
3093 zoom: left_dock_zoom,
3094 },
3095 right: DockData {
3096 visible: right_visible,
3097 active_panel: right_active_panel,
3098 zoom: right_dock_zoom,
3099 },
3100 bottom: DockData {
3101 visible: bottom_visible,
3102 active_panel: bottom_active_panel,
3103 zoom: bottom_dock_zoom,
3104 },
3105 }
3106 }
3107
3108 if let Some(location) = self.location(cx) {
3109 // Load bearing special case:
3110 // - with_local_workspace() relies on this to not have other stuff open
3111 // when you open your log
3112 if !location.paths().is_empty() {
3113 let center_group = build_serialized_pane_group(&self.center.root, cx);
3114 let docks = build_serialized_docks(self, cx);
3115
3116 let serialized_workspace = SerializedWorkspace {
3117 id: self.database_id,
3118 location,
3119 center_group,
3120 bounds: Default::default(),
3121 display: Default::default(),
3122 docks,
3123 };
3124
3125 cx.spawn(|_, _| persistence::DB.save_workspace(serialized_workspace))
3126 .detach();
3127 }
3128 }
3129 }
3130
3131 pub(crate) fn load_workspace(
3132 serialized_workspace: SerializedWorkspace,
3133 paths_to_open: Vec<Option<ProjectPath>>,
3134 cx: &mut ViewContext<Workspace>,
3135 ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
3136 cx.spawn(|workspace, mut cx| async move {
3137 let project = workspace.update(&mut cx, |workspace, _| workspace.project().clone())?;
3138
3139 let mut center_group = None;
3140 let mut center_items = None;
3141
3142 // Traverse the splits tree and add to things
3143 if let Some((group, active_pane, items)) = serialized_workspace
3144 .center_group
3145 .deserialize(
3146 &project,
3147 serialized_workspace.id,
3148 workspace.clone(),
3149 &mut cx,
3150 )
3151 .await
3152 {
3153 center_items = Some(items);
3154 center_group = Some((group, active_pane))
3155 }
3156
3157 let mut items_by_project_path = cx.update(|_, cx| {
3158 center_items
3159 .unwrap_or_default()
3160 .into_iter()
3161 .filter_map(|item| {
3162 let item = item?;
3163 let project_path = item.project_path(cx)?;
3164 Some((project_path, item))
3165 })
3166 .collect::<HashMap<_, _>>()
3167 })?;
3168
3169 let opened_items = paths_to_open
3170 .into_iter()
3171 .map(|path_to_open| {
3172 path_to_open
3173 .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
3174 })
3175 .collect::<Vec<_>>();
3176
3177 // Remove old panes from workspace panes list
3178 workspace.update(&mut cx, |workspace, cx| {
3179 if let Some((center_group, active_pane)) = center_group {
3180 workspace.remove_panes(workspace.center.root.clone(), cx);
3181
3182 // Swap workspace center group
3183 workspace.center = PaneGroup::with_root(center_group);
3184 workspace.last_active_center_pane = active_pane.as_ref().map(|p| p.downgrade());
3185 if let Some(active_pane) = active_pane {
3186 workspace.active_pane = active_pane;
3187 cx.focus_self();
3188 } else {
3189 workspace.active_pane = workspace.center.first_pane().clone();
3190 }
3191 }
3192
3193 let docks = serialized_workspace.docks;
3194 workspace.left_dock.update(cx, |dock, cx| {
3195 dock.set_open(docks.left.visible, cx);
3196 if let Some(active_panel) = docks.left.active_panel {
3197 if let Some(ix) = dock.panel_index_for_persistent_name(&active_panel, cx) {
3198 dock.activate_panel(ix, cx);
3199 }
3200 }
3201 dock.active_panel()
3202 .map(|panel| panel.set_zoomed(docks.left.zoom, cx));
3203 if docks.left.visible && docks.left.zoom {
3204 cx.focus_self()
3205 }
3206 });
3207 // TODO: I think the bug is that setting zoom or active undoes the bottom zoom or something
3208 workspace.right_dock.update(cx, |dock, cx| {
3209 dock.set_open(docks.right.visible, cx);
3210 if let Some(active_panel) = docks.right.active_panel {
3211 if let Some(ix) = dock.panel_index_for_persistent_name(&active_panel, cx) {
3212 dock.activate_panel(ix, cx);
3213 }
3214 }
3215 dock.active_panel()
3216 .map(|panel| panel.set_zoomed(docks.right.zoom, cx));
3217
3218 if docks.right.visible && docks.right.zoom {
3219 cx.focus_self()
3220 }
3221 });
3222 workspace.bottom_dock.update(cx, |dock, cx| {
3223 dock.set_open(docks.bottom.visible, cx);
3224 if let Some(active_panel) = docks.bottom.active_panel {
3225 if let Some(ix) = dock.panel_index_for_persistent_name(&active_panel, cx) {
3226 dock.activate_panel(ix, cx);
3227 }
3228 }
3229
3230 dock.active_panel()
3231 .map(|panel| panel.set_zoomed(docks.bottom.zoom, cx));
3232
3233 if docks.bottom.visible && docks.bottom.zoom {
3234 cx.focus_self()
3235 }
3236 });
3237
3238 cx.notify();
3239 })?;
3240
3241 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
3242 workspace.update(&mut cx, |workspace, cx| workspace.serialize_workspace(cx))?;
3243
3244 Ok(opened_items)
3245 })
3246 }
3247
3248 fn actions(&self, div: Div, cx: &mut ViewContext<Self>) -> Div {
3249 self.add_workspace_actions_listeners(div, cx)
3250 .on_action(cx.listener(Self::close_inactive_items_and_panes))
3251 .on_action(cx.listener(Self::close_all_items_and_panes))
3252 .on_action(cx.listener(Self::save_all))
3253 .on_action(cx.listener(Self::add_folder_to_project))
3254 .on_action(cx.listener(Self::follow_next_collaborator))
3255 .on_action(cx.listener(|workspace, _: &Unfollow, cx| {
3256 let pane = workspace.active_pane().clone();
3257 workspace.unfollow(&pane, cx);
3258 }))
3259 .on_action(cx.listener(|workspace, action: &Save, cx| {
3260 workspace
3261 .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx)
3262 .detach_and_log_err(cx);
3263 }))
3264 .on_action(cx.listener(|workspace, _: &SaveAs, cx| {
3265 workspace
3266 .save_active_item(SaveIntent::SaveAs, cx)
3267 .detach_and_log_err(cx);
3268 }))
3269 .on_action(cx.listener(|workspace, _: &ActivatePreviousPane, cx| {
3270 workspace.activate_previous_pane(cx)
3271 }))
3272 .on_action(
3273 cx.listener(|workspace, _: &ActivateNextPane, cx| workspace.activate_next_pane(cx)),
3274 )
3275 .on_action(
3276 cx.listener(|workspace, action: &ActivatePaneInDirection, cx| {
3277 workspace.activate_pane_in_direction(action.0, cx)
3278 }),
3279 )
3280 .on_action(cx.listener(|workspace, action: &SwapPaneInDirection, cx| {
3281 workspace.swap_pane_in_direction(action.0, cx)
3282 }))
3283 .on_action(cx.listener(|this, _: &ToggleLeftDock, cx| {
3284 this.toggle_dock(DockPosition::Left, cx);
3285 }))
3286 .on_action(
3287 cx.listener(|workspace: &mut Workspace, _: &ToggleRightDock, cx| {
3288 workspace.toggle_dock(DockPosition::Right, cx);
3289 }),
3290 )
3291 .on_action(
3292 cx.listener(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| {
3293 workspace.toggle_dock(DockPosition::Bottom, cx);
3294 }),
3295 )
3296 .on_action(
3297 cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, cx| {
3298 workspace.close_all_docks(cx);
3299 }),
3300 )
3301 .on_action(cx.listener(Workspace::open))
3302 .on_action(cx.listener(Workspace::close_window))
3303 .on_action(cx.listener(Workspace::activate_pane_at_index))
3304 .on_action(
3305 cx.listener(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
3306 workspace.reopen_closed_item(cx).detach();
3307 }),
3308 )
3309 }
3310
3311 #[cfg(any(test, feature = "test-support"))]
3312 pub fn test_new(project: Model<Project>, cx: &mut ViewContext<Self>) -> Self {
3313 use node_runtime::FakeNodeRuntime;
3314
3315 let client = project.read(cx).client();
3316 let user_store = project.read(cx).user_store();
3317
3318 let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx));
3319 cx.activate_window();
3320 let app_state = Arc::new(AppState {
3321 languages: project.read(cx).languages().clone(),
3322 workspace_store,
3323 client,
3324 user_store,
3325 fs: project.read(cx).fs().clone(),
3326 build_window_options: |_, _, _| Default::default(),
3327 node_runtime: FakeNodeRuntime::new(),
3328 });
3329 let workspace = Self::new(0, project, app_state, cx);
3330 workspace.active_pane.update(cx, |pane, cx| pane.focus(cx));
3331 workspace
3332 }
3333
3334 pub fn register_action<A: Action>(
3335 &mut self,
3336 callback: impl Fn(&mut Self, &A, &mut ViewContext<Self>) + 'static,
3337 ) -> &mut Self {
3338 let callback = Arc::new(callback);
3339
3340 self.workspace_actions.push(Box::new(move |div, cx| {
3341 let callback = callback.clone();
3342 div.on_action(
3343 cx.listener(move |workspace, event, cx| (callback.clone())(workspace, event, cx)),
3344 )
3345 }));
3346 self
3347 }
3348
3349 fn add_workspace_actions_listeners(&self, div: Div, cx: &mut ViewContext<Self>) -> Div {
3350 let mut div = div
3351 .on_action(cx.listener(Self::close_inactive_items_and_panes))
3352 .on_action(cx.listener(Self::close_all_items_and_panes))
3353 .on_action(cx.listener(Self::add_folder_to_project))
3354 .on_action(cx.listener(Self::save_all))
3355 .on_action(cx.listener(Self::open));
3356 for action in self.workspace_actions.iter() {
3357 div = (action)(div, cx)
3358 }
3359 div
3360 }
3361
3362 pub fn has_active_modal(&self, cx: &WindowContext<'_>) -> bool {
3363 self.modal_layer.read(cx).has_active_modal()
3364 }
3365
3366 pub fn active_modal<V: ManagedView + 'static>(
3367 &mut self,
3368 cx: &ViewContext<Self>,
3369 ) -> Option<View<V>> {
3370 self.modal_layer.read(cx).active_modal()
3371 }
3372
3373 pub fn toggle_modal<V: ModalView, B>(&mut self, cx: &mut ViewContext<Self>, build: B)
3374 where
3375 B: FnOnce(&mut ViewContext<V>) -> V,
3376 {
3377 self.modal_layer
3378 .update(cx, |modal_layer, cx| modal_layer.toggle_modal(cx, build))
3379 }
3380}
3381
3382fn window_bounds_env_override(cx: &AsyncAppContext) -> Option<WindowBounds> {
3383 let display_origin = cx
3384 .update(|cx| Some(cx.displays().first()?.bounds().origin))
3385 .ok()??;
3386 ZED_WINDOW_POSITION
3387 .zip(*ZED_WINDOW_SIZE)
3388 .map(|(position, size)| {
3389 WindowBounds::Fixed(Bounds {
3390 origin: display_origin + position,
3391 size,
3392 })
3393 })
3394}
3395
3396fn open_items(
3397 serialized_workspace: Option<SerializedWorkspace>,
3398 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
3399 app_state: Arc<AppState>,
3400 cx: &mut ViewContext<Workspace>,
3401) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> {
3402 let restored_items = serialized_workspace.map(|serialized_workspace| {
3403 Workspace::load_workspace(
3404 serialized_workspace,
3405 project_paths_to_open
3406 .iter()
3407 .map(|(_, project_path)| project_path)
3408 .cloned()
3409 .collect(),
3410 cx,
3411 )
3412 });
3413
3414 cx.spawn(|workspace, mut cx| async move {
3415 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
3416
3417 if let Some(restored_items) = restored_items {
3418 let restored_items = restored_items.await?;
3419
3420 let restored_project_paths = restored_items
3421 .iter()
3422 .filter_map(|item| {
3423 cx.update(|_, cx| item.as_ref()?.project_path(cx))
3424 .ok()
3425 .flatten()
3426 })
3427 .collect::<HashSet<_>>();
3428
3429 for restored_item in restored_items {
3430 opened_items.push(restored_item.map(Ok));
3431 }
3432
3433 project_paths_to_open
3434 .iter_mut()
3435 .for_each(|(_, project_path)| {
3436 if let Some(project_path_to_open) = project_path {
3437 if restored_project_paths.contains(project_path_to_open) {
3438 *project_path = None;
3439 }
3440 }
3441 });
3442 } else {
3443 for _ in 0..project_paths_to_open.len() {
3444 opened_items.push(None);
3445 }
3446 }
3447 assert!(opened_items.len() == project_paths_to_open.len());
3448
3449 let tasks =
3450 project_paths_to_open
3451 .into_iter()
3452 .enumerate()
3453 .map(|(i, (abs_path, project_path))| {
3454 let workspace = workspace.clone();
3455 cx.spawn(|mut cx| {
3456 let fs = app_state.fs.clone();
3457 async move {
3458 let file_project_path = project_path?;
3459 if fs.is_file(&abs_path).await {
3460 Some((
3461 i,
3462 workspace
3463 .update(&mut cx, |workspace, cx| {
3464 workspace.open_path(file_project_path, None, true, cx)
3465 })
3466 .log_err()?
3467 .await,
3468 ))
3469 } else {
3470 None
3471 }
3472 }
3473 })
3474 });
3475
3476 let tasks = tasks.collect::<Vec<_>>();
3477
3478 let tasks = futures::future::join_all(tasks.into_iter());
3479 for maybe_opened_path in tasks.await.into_iter() {
3480 if let Some((i, path_open_result)) = maybe_opened_path {
3481 opened_items[i] = Some(path_open_result);
3482 }
3483 }
3484
3485 Ok(opened_items)
3486 })
3487}
3488
3489fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncAppContext) {
3490 const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
3491
3492 workspace
3493 .update(cx, |workspace, cx| {
3494 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
3495 workspace.show_notification_once(0, cx, |cx| {
3496 cx.new_view(|_| {
3497 MessageNotification::new("Failed to load the database file.")
3498 .with_click_message("Click to let us know about this error")
3499 .on_click(|cx| cx.open_url(REPORT_ISSUE_URL))
3500 })
3501 });
3502 }
3503 })
3504 .log_err();
3505}
3506
3507impl FocusableView for Workspace {
3508 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
3509 self.active_pane.focus_handle(cx)
3510 }
3511}
3512
3513#[derive(Clone, Render)]
3514struct DraggedDock(DockPosition);
3515
3516impl Render for Workspace {
3517 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3518 let mut context = KeyContext::default();
3519 context.add("Workspace");
3520
3521 let (ui_font, ui_font_size) = {
3522 let theme_settings = ThemeSettings::get_global(cx);
3523 (
3524 theme_settings.ui_font.family.clone(),
3525 theme_settings.ui_font_size.clone(),
3526 )
3527 };
3528
3529 let theme = cx.theme().clone();
3530 let colors = theme.colors();
3531 cx.set_rem_size(ui_font_size);
3532
3533 self.actions(div(), cx)
3534 .key_context(context)
3535 .relative()
3536 .size_full()
3537 .flex()
3538 .flex_col()
3539 .font(ui_font)
3540 .gap_0()
3541 .justify_start()
3542 .items_start()
3543 .text_color(colors.text)
3544 .bg(colors.background)
3545 .border()
3546 .border_color(colors.border)
3547 .children(self.titlebar_item.clone())
3548 .child(
3549 div()
3550 .id("workspace")
3551 .relative()
3552 .flex_1()
3553 .w_full()
3554 .flex()
3555 .flex_col()
3556 .overflow_hidden()
3557 .border_t()
3558 .border_b()
3559 .border_color(colors.border)
3560 .child(
3561 canvas(cx.listener(|workspace, bounds, _| {
3562 workspace.bounds = *bounds;
3563 }))
3564 .absolute()
3565 .size_full(),
3566 )
3567 .on_drag_move(
3568 cx.listener(|workspace, e: &DragMoveEvent<DraggedDock>, cx| {
3569 match e.drag(cx).0 {
3570 DockPosition::Left => {
3571 let size = workspace.bounds.left() + e.event.position.x;
3572 workspace.left_dock.update(cx, |left_dock, cx| {
3573 left_dock.resize_active_panel(Some(size), cx);
3574 });
3575 }
3576 DockPosition::Right => {
3577 let size = workspace.bounds.right() - e.event.position.x;
3578 workspace.right_dock.update(cx, |right_dock, cx| {
3579 right_dock.resize_active_panel(Some(size), cx);
3580 });
3581 }
3582 DockPosition::Bottom => {
3583 let size = workspace.bounds.bottom() - e.event.position.y;
3584 workspace.bottom_dock.update(cx, |bottom_dock, cx| {
3585 bottom_dock.resize_active_panel(Some(size), cx);
3586 });
3587 }
3588 }
3589 }),
3590 )
3591 .child(self.modal_layer.clone())
3592 .child(
3593 div()
3594 .flex()
3595 .flex_row()
3596 .h_full()
3597 // Left Dock
3598 .children(self.zoomed_position.ne(&Some(DockPosition::Left)).then(
3599 || {
3600 div()
3601 .flex()
3602 .flex_none()
3603 .overflow_hidden()
3604 .child(self.left_dock.clone())
3605 },
3606 ))
3607 // Panes
3608 .child(
3609 div()
3610 .flex()
3611 .flex_col()
3612 .flex_1()
3613 .overflow_hidden()
3614 .child(self.center.render(
3615 &self.project,
3616 &self.follower_states,
3617 self.active_call(),
3618 &self.active_pane,
3619 self.zoomed.as_ref(),
3620 &self.app_state,
3621 cx,
3622 ))
3623 .children(
3624 self.zoomed_position
3625 .ne(&Some(DockPosition::Bottom))
3626 .then(|| self.bottom_dock.clone()),
3627 ),
3628 )
3629 // Right Dock
3630 .children(self.zoomed_position.ne(&Some(DockPosition::Right)).then(
3631 || {
3632 div()
3633 .flex()
3634 .flex_none()
3635 .overflow_hidden()
3636 .child(self.right_dock.clone())
3637 },
3638 )),
3639 )
3640 .children(self.render_notifications(cx))
3641 .children(self.zoomed.as_ref().and_then(|view| {
3642 let zoomed_view = view.upgrade()?;
3643 let div = div()
3644 .z_index(1)
3645 .absolute()
3646 .overflow_hidden()
3647 .border_color(colors.border)
3648 .bg(colors.background)
3649 .child(zoomed_view)
3650 .inset_0()
3651 .shadow_lg();
3652
3653 Some(match self.zoomed_position {
3654 Some(DockPosition::Left) => div.right_2().border_r(),
3655 Some(DockPosition::Right) => div.left_2().border_l(),
3656 Some(DockPosition::Bottom) => div.top_2().border_t(),
3657 None => div.top_2().bottom_2().left_2().right_2().border(),
3658 })
3659 })),
3660 )
3661 .child(self.status_bar.clone())
3662 .children(if self.project.read(cx).is_disconnected() {
3663 Some(DisconnectedOverlay)
3664 } else {
3665 None
3666 })
3667 }
3668}
3669
3670impl WorkspaceStore {
3671 pub fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
3672 Self {
3673 workspaces: Default::default(),
3674 followers: Default::default(),
3675 _subscriptions: vec![
3676 client.add_request_handler(cx.weak_model(), Self::handle_follow),
3677 client.add_message_handler(cx.weak_model(), Self::handle_unfollow),
3678 client.add_message_handler(cx.weak_model(), Self::handle_update_followers),
3679 ],
3680 client,
3681 }
3682 }
3683
3684 pub fn update_followers(
3685 &self,
3686 project_id: Option<u64>,
3687 update: proto::update_followers::Variant,
3688 cx: &AppContext,
3689 ) -> Option<()> {
3690 if !cx.has_global::<Model<ActiveCall>>() {
3691 return None;
3692 }
3693
3694 let room_id = ActiveCall::global(cx).read(cx).room()?.read(cx).id();
3695 let follower_ids: Vec<_> = self
3696 .followers
3697 .iter()
3698 .filter_map(|follower| {
3699 if follower.project_id == project_id || project_id.is_none() {
3700 Some(follower.peer_id.into())
3701 } else {
3702 None
3703 }
3704 })
3705 .collect();
3706 if follower_ids.is_empty() {
3707 return None;
3708 }
3709 self.client
3710 .send(proto::UpdateFollowers {
3711 room_id,
3712 project_id,
3713 follower_ids,
3714 variant: Some(update),
3715 })
3716 .log_err()
3717 }
3718
3719 pub async fn handle_follow(
3720 this: Model<Self>,
3721 envelope: TypedEnvelope<proto::Follow>,
3722 _: Arc<Client>,
3723 mut cx: AsyncAppContext,
3724 ) -> Result<proto::FollowResponse> {
3725 this.update(&mut cx, |this, cx| {
3726 let follower = Follower {
3727 project_id: envelope.payload.project_id,
3728 peer_id: envelope.original_sender_id()?,
3729 };
3730 let active_project = ActiveCall::global(cx).read(cx).location().cloned();
3731
3732 let mut response = proto::FollowResponse::default();
3733 this.workspaces.retain(|workspace| {
3734 workspace
3735 .update(cx, |workspace, cx| {
3736 let handler_response = workspace.handle_follow(follower.project_id, cx);
3737 if response.views.is_empty() {
3738 response.views = handler_response.views;
3739 } else {
3740 response.views.extend_from_slice(&handler_response.views);
3741 }
3742
3743 if let Some(active_view_id) = handler_response.active_view_id.clone() {
3744 if response.active_view_id.is_none()
3745 || Some(workspace.project.downgrade()) == active_project
3746 {
3747 response.active_view_id = Some(active_view_id);
3748 }
3749 }
3750 })
3751 .is_ok()
3752 });
3753
3754 if let Err(ix) = this.followers.binary_search(&follower) {
3755 this.followers.insert(ix, follower);
3756 }
3757
3758 Ok(response)
3759 })?
3760 }
3761
3762 async fn handle_unfollow(
3763 model: Model<Self>,
3764 envelope: TypedEnvelope<proto::Unfollow>,
3765 _: Arc<Client>,
3766 mut cx: AsyncAppContext,
3767 ) -> Result<()> {
3768 model.update(&mut cx, |this, _| {
3769 let follower = Follower {
3770 project_id: envelope.payload.project_id,
3771 peer_id: envelope.original_sender_id()?,
3772 };
3773 if let Ok(ix) = this.followers.binary_search(&follower) {
3774 this.followers.remove(ix);
3775 }
3776 Ok(())
3777 })?
3778 }
3779
3780 async fn handle_update_followers(
3781 this: Model<Self>,
3782 envelope: TypedEnvelope<proto::UpdateFollowers>,
3783 _: Arc<Client>,
3784 mut cx: AsyncAppContext,
3785 ) -> Result<()> {
3786 let leader_id = envelope.original_sender_id()?;
3787 let update = envelope.payload;
3788
3789 this.update(&mut cx, |this, cx| {
3790 this.workspaces.retain(|workspace| {
3791 workspace
3792 .update(cx, |workspace, cx| {
3793 let project_id = workspace.project.read(cx).remote_id();
3794 if update.project_id != project_id && update.project_id.is_some() {
3795 return;
3796 }
3797 workspace.handle_update_followers(leader_id, update.clone(), cx);
3798 })
3799 .is_ok()
3800 });
3801 Ok(())
3802 })?
3803 }
3804}
3805
3806impl ViewId {
3807 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
3808 Ok(Self {
3809 creator: message
3810 .creator
3811 .ok_or_else(|| anyhow!("creator is missing"))?,
3812 id: message.id,
3813 })
3814 }
3815
3816 pub(crate) fn to_proto(&self) -> proto::ViewId {
3817 proto::ViewId {
3818 creator: Some(self.creator),
3819 id: self.id,
3820 }
3821 }
3822}
3823
3824pub trait WorkspaceHandle {
3825 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
3826}
3827
3828impl WorkspaceHandle for View<Workspace> {
3829 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
3830 self.read(cx)
3831 .worktrees(cx)
3832 .flat_map(|worktree| {
3833 let worktree_id = worktree.read(cx).id();
3834 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
3835 worktree_id,
3836 path: f.path.clone(),
3837 })
3838 })
3839 .collect::<Vec<_>>()
3840 }
3841}
3842
3843impl std::fmt::Debug for OpenPaths {
3844 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3845 f.debug_struct("OpenPaths")
3846 .field("paths", &self.paths)
3847 .finish()
3848 }
3849}
3850
3851pub fn activate_workspace_for_project(
3852 cx: &mut AppContext,
3853 predicate: impl Fn(&Project, &AppContext) -> bool + Send + 'static,
3854) -> Option<WindowHandle<Workspace>> {
3855 for window in cx.windows() {
3856 let Some(workspace) = window.downcast::<Workspace>() else {
3857 continue;
3858 };
3859
3860 let predicate = workspace
3861 .update(cx, |workspace, cx| {
3862 let project = workspace.project.read(cx);
3863 if predicate(project, cx) {
3864 cx.activate_window();
3865 true
3866 } else {
3867 false
3868 }
3869 })
3870 .log_err()
3871 .unwrap_or(false);
3872
3873 if predicate {
3874 return Some(workspace);
3875 }
3876 }
3877
3878 None
3879}
3880
3881pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
3882 DB.last_workspace().await.log_err().flatten()
3883}
3884
3885async fn join_channel_internal(
3886 channel_id: u64,
3887 app_state: &Arc<AppState>,
3888 requesting_window: Option<WindowHandle<Workspace>>,
3889 active_call: &Model<ActiveCall>,
3890 cx: &mut AsyncAppContext,
3891) -> Result<bool> {
3892 let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
3893 let Some(room) = active_call.room().map(|room| room.read(cx)) else {
3894 return (false, None);
3895 };
3896
3897 let already_in_channel = room.channel_id() == Some(channel_id);
3898 let should_prompt = room.is_sharing_project()
3899 && room.remote_participants().len() > 0
3900 && !already_in_channel;
3901 let open_room = if already_in_channel {
3902 active_call.room().cloned()
3903 } else {
3904 None
3905 };
3906 (should_prompt, open_room)
3907 })?;
3908
3909 if let Some(room) = open_room {
3910 let task = room.update(cx, |room, cx| {
3911 if let Some((project, host)) = room.most_active_project(cx) {
3912 return Some(join_remote_project(project, host, app_state.clone(), cx));
3913 }
3914
3915 None
3916 })?;
3917 if let Some(task) = task {
3918 task.await?;
3919 }
3920 return anyhow::Ok(true);
3921 }
3922
3923 if should_prompt {
3924 if let Some(workspace) = requesting_window {
3925 let answer = workspace.update(cx, |_, cx| {
3926 cx.prompt(
3927 PromptLevel::Warning,
3928 "Leaving this call will unshare your current project.\nDo you want to switch channels?",
3929 &["Yes, Join Channel", "Cancel"],
3930 )
3931 })?.await;
3932
3933 if answer == Ok(1) {
3934 return Ok(false);
3935 }
3936 } else {
3937 return Ok(false); // unreachable!() hopefully
3938 }
3939 }
3940
3941 let client = cx.update(|cx| active_call.read(cx).client())?;
3942
3943 let mut client_status = client.status();
3944
3945 // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
3946 'outer: loop {
3947 let Some(status) = client_status.recv().await else {
3948 return Err(anyhow!("error connecting"));
3949 };
3950
3951 match status {
3952 Status::Connecting
3953 | Status::Authenticating
3954 | Status::Reconnecting
3955 | Status::Reauthenticating => continue,
3956 Status::Connected { .. } => break 'outer,
3957 Status::SignedOut => return Err(anyhow!("not signed in")),
3958 Status::UpgradeRequired => return Err(anyhow!("zed is out of date")),
3959 Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
3960 return Err(anyhow!("zed is offline"))
3961 }
3962 }
3963 }
3964
3965 let room = active_call
3966 .update(cx, |active_call, cx| {
3967 active_call.join_channel(channel_id, cx)
3968 })?
3969 .await?;
3970
3971 let Some(room) = room else {
3972 return anyhow::Ok(true);
3973 };
3974
3975 room.update(cx, |room, _| room.room_update_completed())?
3976 .await;
3977
3978 let task = room.update(cx, |room, cx| {
3979 if let Some((project, host)) = room.most_active_project(cx) {
3980 return Some(join_remote_project(project, host, app_state.clone(), cx));
3981 }
3982
3983 None
3984 })?;
3985 if let Some(task) = task {
3986 task.await?;
3987 return anyhow::Ok(true);
3988 }
3989 anyhow::Ok(false)
3990}
3991
3992pub fn join_channel(
3993 channel_id: u64,
3994 app_state: Arc<AppState>,
3995 requesting_window: Option<WindowHandle<Workspace>>,
3996 cx: &mut AppContext,
3997) -> Task<Result<()>> {
3998 let active_call = ActiveCall::global(cx);
3999 cx.spawn(|mut cx| async move {
4000 let result = join_channel_internal(
4001 channel_id,
4002 &app_state,
4003 requesting_window,
4004 &active_call,
4005 &mut cx,
4006 )
4007 .await;
4008
4009 // join channel succeeded, and opened a window
4010 if matches!(result, Ok(true)) {
4011 return anyhow::Ok(());
4012 }
4013
4014 // find an existing workspace to focus and show call controls
4015 let mut active_window =
4016 requesting_window.or_else(|| activate_any_workspace_window(&mut cx));
4017 if active_window.is_none() {
4018 // no open workspaces, make one to show the error in (blergh)
4019 let (window_handle, _) = cx
4020 .update(|cx| {
4021 Workspace::new_local(vec![], app_state.clone(), requesting_window, cx)
4022 })?
4023 .await?;
4024
4025 active_window = Some(window_handle);
4026 }
4027
4028 if let Err(err) = result {
4029 log::error!("failed to join channel: {}", err);
4030 if let Some(active_window) = active_window {
4031 active_window
4032 .update(&mut cx, |_, cx| {
4033 cx.prompt(
4034 PromptLevel::Critical,
4035 &format!("Failed to join channel: {}", err),
4036 &["Ok"],
4037 )
4038 })?
4039 .await
4040 .ok();
4041 }
4042 }
4043
4044 // return ok, we showed the error to the user.
4045 return anyhow::Ok(());
4046 })
4047}
4048
4049pub async fn get_any_active_workspace(
4050 app_state: Arc<AppState>,
4051 mut cx: AsyncAppContext,
4052) -> anyhow::Result<WindowHandle<Workspace>> {
4053 // find an existing workspace to focus and show call controls
4054 let active_window = activate_any_workspace_window(&mut cx);
4055 if active_window.is_none() {
4056 cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, cx))?
4057 .await?;
4058 }
4059 activate_any_workspace_window(&mut cx).context("could not open zed")
4060}
4061
4062fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<WindowHandle<Workspace>> {
4063 cx.update(|cx| {
4064 for window in cx.windows() {
4065 if let Some(workspace_window) = window.downcast::<Workspace>() {
4066 workspace_window
4067 .update(cx, |_, cx| cx.activate_window())
4068 .ok();
4069 return Some(workspace_window);
4070 }
4071 }
4072 None
4073 })
4074 .ok()
4075 .flatten()
4076}
4077
4078#[allow(clippy::type_complexity)]
4079pub fn open_paths(
4080 abs_paths: &[PathBuf],
4081 app_state: &Arc<AppState>,
4082 requesting_window: Option<WindowHandle<Workspace>>,
4083 cx: &mut AppContext,
4084) -> Task<
4085 anyhow::Result<(
4086 WindowHandle<Workspace>,
4087 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
4088 )>,
4089> {
4090 let app_state = app_state.clone();
4091 let abs_paths = abs_paths.to_vec();
4092 // Open paths in existing workspace if possible
4093 let existing = activate_workspace_for_project(cx, {
4094 let abs_paths = abs_paths.clone();
4095 move |project, cx| project.contains_paths(&abs_paths, cx)
4096 });
4097 cx.spawn(move |mut cx| async move {
4098 if let Some(existing) = existing {
4099 Ok((
4100 existing.clone(),
4101 existing
4102 .update(&mut cx, |workspace, cx| {
4103 workspace.open_paths(abs_paths, OpenVisible::All, None, cx)
4104 })?
4105 .await,
4106 ))
4107 } else {
4108 cx.update(move |cx| {
4109 Workspace::new_local(abs_paths, app_state.clone(), requesting_window, cx)
4110 })?
4111 .await
4112 }
4113 })
4114}
4115
4116pub fn open_new(
4117 app_state: &Arc<AppState>,
4118 cx: &mut AppContext,
4119 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static + Send,
4120) -> Task<()> {
4121 let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx);
4122 cx.spawn(|mut cx| async move {
4123 if let Some((workspace, opened_paths)) = task.await.log_err() {
4124 workspace
4125 .update(&mut cx, |workspace, cx| {
4126 if opened_paths.is_empty() {
4127 init(workspace, cx)
4128 }
4129 })
4130 .log_err();
4131 }
4132 })
4133}
4134
4135pub fn create_and_open_local_file(
4136 path: &'static Path,
4137 cx: &mut ViewContext<Workspace>,
4138 default_content: impl 'static + Send + FnOnce() -> Rope,
4139) -> Task<Result<Box<dyn ItemHandle>>> {
4140 cx.spawn(|workspace, mut cx| async move {
4141 let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
4142 if !fs.is_file(path).await {
4143 fs.create_file(path, Default::default()).await?;
4144 fs.save(path, &default_content(), Default::default())
4145 .await?;
4146 }
4147
4148 let mut items = workspace
4149 .update(&mut cx, |workspace, cx| {
4150 workspace.with_local_workspace(cx, |workspace, cx| {
4151 workspace.open_paths(vec![path.to_path_buf()], OpenVisible::None, None, cx)
4152 })
4153 })?
4154 .await?
4155 .await;
4156
4157 let item = items.pop().flatten();
4158 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
4159 })
4160}
4161
4162pub fn join_remote_project(
4163 project_id: u64,
4164 follow_user_id: u64,
4165 app_state: Arc<AppState>,
4166 cx: &mut AppContext,
4167) -> Task<Result<()>> {
4168 let windows = cx.windows();
4169 cx.spawn(|mut cx| async move {
4170 let existing_workspace = windows.into_iter().find_map(|window| {
4171 window.downcast::<Workspace>().and_then(|window| {
4172 window
4173 .update(&mut cx, |workspace, cx| {
4174 if workspace.project().read(cx).remote_id() == Some(project_id) {
4175 Some(window)
4176 } else {
4177 None
4178 }
4179 })
4180 .unwrap_or(None)
4181 })
4182 });
4183
4184 let workspace = if let Some(existing_workspace) = existing_workspace {
4185 existing_workspace
4186 } else {
4187 let active_call = cx.update(|cx| ActiveCall::global(cx))?;
4188 let room = active_call
4189 .read_with(&cx, |call, _| call.room().cloned())?
4190 .ok_or_else(|| anyhow!("not in a call"))?;
4191 let project = room
4192 .update(&mut cx, |room, cx| {
4193 room.join_project(
4194 project_id,
4195 app_state.languages.clone(),
4196 app_state.fs.clone(),
4197 cx,
4198 )
4199 })?
4200 .await?;
4201
4202 let window_bounds_override = window_bounds_env_override(&cx);
4203 cx.update(|cx| {
4204 let options = (app_state.build_window_options)(window_bounds_override, None, cx);
4205 cx.open_window(options, |cx| {
4206 cx.new_view(|cx| Workspace::new(0, project, app_state.clone(), cx))
4207 })
4208 })?
4209 };
4210
4211 workspace.update(&mut cx, |workspace, cx| {
4212 cx.activate(true);
4213 cx.activate_window();
4214
4215 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
4216 let follow_peer_id = room
4217 .read(cx)
4218 .remote_participants()
4219 .iter()
4220 .find(|(_, participant)| participant.user.id == follow_user_id)
4221 .map(|(_, p)| p.peer_id)
4222 .or_else(|| {
4223 // If we couldn't follow the given user, follow the host instead.
4224 let collaborator = workspace
4225 .project()
4226 .read(cx)
4227 .collaborators()
4228 .values()
4229 .find(|collaborator| collaborator.replica_id == 0)?;
4230 Some(collaborator.peer_id)
4231 });
4232
4233 if let Some(follow_peer_id) = follow_peer_id {
4234 workspace.follow(follow_peer_id, cx);
4235 }
4236 }
4237 })?;
4238
4239 anyhow::Ok(())
4240 })
4241}
4242
4243pub fn restart(_: &Restart, cx: &mut AppContext) {
4244 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
4245 let mut workspace_windows = cx
4246 .windows()
4247 .into_iter()
4248 .filter_map(|window| window.downcast::<Workspace>())
4249 .collect::<Vec<_>>();
4250
4251 // If multiple windows have unsaved changes, and need a save prompt,
4252 // prompt in the active window before switching to a different window.
4253 workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false));
4254
4255 let mut prompt = None;
4256 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
4257 prompt = window
4258 .update(cx, |_, cx| {
4259 cx.prompt(
4260 PromptLevel::Info,
4261 "Are you sure you want to restart?",
4262 &["Restart", "Cancel"],
4263 )
4264 })
4265 .ok();
4266 }
4267
4268 cx.spawn(|mut cx| async move {
4269 if let Some(prompt) = prompt {
4270 let answer = prompt.await?;
4271 if answer != 0 {
4272 return Ok(());
4273 }
4274 }
4275
4276 // If the user cancels any save prompt, then keep the app open.
4277 for window in workspace_windows {
4278 if let Ok(should_close) = window.update(&mut cx, |workspace, cx| {
4279 workspace.prepare_to_close(true, cx)
4280 }) {
4281 if !should_close.await? {
4282 return Ok(());
4283 }
4284 }
4285 }
4286
4287 cx.update(|cx| cx.restart())
4288 })
4289 .detach_and_log_err(cx);
4290}
4291
4292fn parse_pixel_position_env_var(value: &str) -> Option<Point<GlobalPixels>> {
4293 let mut parts = value.split(',');
4294 let x: usize = parts.next()?.parse().ok()?;
4295 let y: usize = parts.next()?.parse().ok()?;
4296 Some(point((x as f64).into(), (y as f64).into()))
4297}
4298
4299fn parse_pixel_size_env_var(value: &str) -> Option<Size<GlobalPixels>> {
4300 let mut parts = value.split(',');
4301 let width: usize = parts.next()?.parse().ok()?;
4302 let height: usize = parts.next()?.parse().ok()?;
4303 Some(size((width as f64).into(), (height as f64).into()))
4304}
4305
4306struct DisconnectedOverlay;
4307
4308impl Element for DisconnectedOverlay {
4309 type State = AnyElement;
4310
4311 fn request_layout(
4312 &mut self,
4313 _: Option<Self::State>,
4314 cx: &mut WindowContext,
4315 ) -> (LayoutId, Self::State) {
4316 let mut background = cx.theme().colors().elevated_surface_background;
4317 background.fade_out(0.2);
4318 let mut overlay = div()
4319 .bg(background)
4320 .absolute()
4321 .left_0()
4322 .top_0()
4323 .size_full()
4324 .flex()
4325 .items_center()
4326 .justify_center()
4327 .capture_any_mouse_down(|_, cx| cx.stop_propagation())
4328 .capture_any_mouse_up(|_, cx| cx.stop_propagation())
4329 .child(Label::new(
4330 "Your connection to the remote project has been lost.",
4331 ))
4332 .into_any();
4333 (overlay.request_layout(cx), overlay)
4334 }
4335
4336 fn paint(&mut self, bounds: Bounds<Pixels>, overlay: &mut Self::State, cx: &mut WindowContext) {
4337 cx.with_z_index(u8::MAX, |cx| {
4338 cx.add_opaque_layer(bounds);
4339 overlay.paint(cx);
4340 })
4341 }
4342}
4343
4344impl IntoElement for DisconnectedOverlay {
4345 type Element = Self;
4346
4347 fn element_id(&self) -> Option<ui::prelude::ElementId> {
4348 None
4349 }
4350
4351 fn into_element(self) -> Self::Element {
4352 self
4353 }
4354}
4355
4356#[cfg(test)]
4357mod tests {
4358 use std::{cell::RefCell, rc::Rc};
4359
4360 use super::*;
4361 use crate::{
4362 dock::{test::TestPanel, PanelEvent},
4363 item::{
4364 test::{TestItem, TestProjectItem},
4365 ItemEvent,
4366 },
4367 };
4368 use fs::FakeFs;
4369 use gpui::{px, DismissEvent, TestAppContext, VisualTestContext};
4370 use project::{Project, ProjectEntryId};
4371 use serde_json::json;
4372 use settings::SettingsStore;
4373
4374 #[gpui::test]
4375 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
4376 init_test(cx);
4377
4378 let fs = FakeFs::new(cx.executor());
4379 let project = Project::test(fs, [], cx).await;
4380 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4381
4382 // Adding an item with no ambiguity renders the tab without detail.
4383 let item1 = cx.new_view(|cx| {
4384 let mut item = TestItem::new(cx);
4385 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
4386 item
4387 });
4388 workspace.update(cx, |workspace, cx| {
4389 workspace.add_item(Box::new(item1.clone()), cx);
4390 });
4391 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
4392
4393 // Adding an item that creates ambiguity increases the level of detail on
4394 // both tabs.
4395 let item2 = cx.new_view(|cx| {
4396 let mut item = TestItem::new(cx);
4397 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
4398 item
4399 });
4400 workspace.update(cx, |workspace, cx| {
4401 workspace.add_item(Box::new(item2.clone()), cx);
4402 });
4403 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4404 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4405
4406 // Adding an item that creates ambiguity increases the level of detail only
4407 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
4408 // we stop at the highest detail available.
4409 let item3 = cx.new_view(|cx| {
4410 let mut item = TestItem::new(cx);
4411 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
4412 item
4413 });
4414 workspace.update(cx, |workspace, cx| {
4415 workspace.add_item(Box::new(item3.clone()), cx);
4416 });
4417 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4418 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
4419 item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
4420 }
4421
4422 #[gpui::test]
4423 async fn test_tracking_active_path(cx: &mut TestAppContext) {
4424 init_test(cx);
4425
4426 let fs = FakeFs::new(cx.executor());
4427 fs.insert_tree(
4428 "/root1",
4429 json!({
4430 "one.txt": "",
4431 "two.txt": "",
4432 }),
4433 )
4434 .await;
4435 fs.insert_tree(
4436 "/root2",
4437 json!({
4438 "three.txt": "",
4439 }),
4440 )
4441 .await;
4442
4443 let project = Project::test(fs, ["root1".as_ref()], cx).await;
4444 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4445 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4446 let worktree_id = project.update(cx, |project, cx| {
4447 project.worktrees().next().unwrap().read(cx).id()
4448 });
4449
4450 let item1 = cx.new_view(|cx| {
4451 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
4452 });
4453 let item2 = cx.new_view(|cx| {
4454 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
4455 });
4456
4457 // Add an item to an empty pane
4458 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
4459 project.update(cx, |project, cx| {
4460 assert_eq!(
4461 project.active_entry(),
4462 project
4463 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
4464 .map(|e| e.id)
4465 );
4466 });
4467 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1"));
4468
4469 // Add a second item to a non-empty pane
4470 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
4471 assert_eq!(cx.window_title().as_deref(), Some("two.txt — root1"));
4472 project.update(cx, |project, cx| {
4473 assert_eq!(
4474 project.active_entry(),
4475 project
4476 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
4477 .map(|e| e.id)
4478 );
4479 });
4480
4481 // Close the active item
4482 pane.update(cx, |pane, cx| {
4483 pane.close_active_item(&Default::default(), cx).unwrap()
4484 })
4485 .await
4486 .unwrap();
4487 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1"));
4488 project.update(cx, |project, cx| {
4489 assert_eq!(
4490 project.active_entry(),
4491 project
4492 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
4493 .map(|e| e.id)
4494 );
4495 });
4496
4497 // Add a project folder
4498 project
4499 .update(cx, |project, cx| {
4500 project.find_or_create_local_worktree("/root2", true, cx)
4501 })
4502 .await
4503 .unwrap();
4504 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1, root2"));
4505
4506 // Remove a project folder
4507 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
4508 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root2"));
4509 }
4510
4511 #[gpui::test]
4512 async fn test_close_window(cx: &mut TestAppContext) {
4513 init_test(cx);
4514
4515 let fs = FakeFs::new(cx.executor());
4516 fs.insert_tree("/root", json!({ "one": "" })).await;
4517
4518 let project = Project::test(fs, ["root".as_ref()], cx).await;
4519 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4520
4521 // When there are no dirty items, there's nothing to do.
4522 let item1 = cx.new_view(|cx| TestItem::new(cx));
4523 workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
4524 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4525 assert!(task.await.unwrap());
4526
4527 // When there are dirty untitled items, prompt to save each one. If the user
4528 // cancels any prompt, then abort.
4529 let item2 = cx.new_view(|cx| TestItem::new(cx).with_dirty(true));
4530 let item3 = cx.new_view(|cx| {
4531 TestItem::new(cx)
4532 .with_dirty(true)
4533 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4534 });
4535 workspace.update(cx, |w, cx| {
4536 w.add_item(Box::new(item2.clone()), cx);
4537 w.add_item(Box::new(item3.clone()), cx);
4538 });
4539 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4540 cx.executor().run_until_parked();
4541 cx.simulate_prompt_answer(2); // cancel save all
4542 cx.executor().run_until_parked();
4543 cx.simulate_prompt_answer(2); // cancel save all
4544 cx.executor().run_until_parked();
4545 assert!(!cx.has_pending_prompt());
4546 assert!(!task.await.unwrap());
4547 }
4548
4549 #[gpui::test]
4550 async fn test_close_pane_items(cx: &mut TestAppContext) {
4551 init_test(cx);
4552
4553 let fs = FakeFs::new(cx.executor());
4554
4555 let project = Project::test(fs, None, cx).await;
4556 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4557
4558 let item1 = cx.new_view(|cx| {
4559 TestItem::new(cx)
4560 .with_dirty(true)
4561 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4562 });
4563 let item2 = cx.new_view(|cx| {
4564 TestItem::new(cx)
4565 .with_dirty(true)
4566 .with_conflict(true)
4567 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
4568 });
4569 let item3 = cx.new_view(|cx| {
4570 TestItem::new(cx)
4571 .with_dirty(true)
4572 .with_conflict(true)
4573 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
4574 });
4575 let item4 = cx.new_view(|cx| {
4576 TestItem::new(cx)
4577 .with_dirty(true)
4578 .with_project_items(&[TestProjectItem::new_untitled(cx)])
4579 });
4580 let pane = workspace.update(cx, |workspace, cx| {
4581 workspace.add_item(Box::new(item1.clone()), cx);
4582 workspace.add_item(Box::new(item2.clone()), cx);
4583 workspace.add_item(Box::new(item3.clone()), cx);
4584 workspace.add_item(Box::new(item4.clone()), cx);
4585 workspace.active_pane().clone()
4586 });
4587
4588 let close_items = pane.update(cx, |pane, cx| {
4589 pane.activate_item(1, true, true, cx);
4590 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
4591 let item1_id = item1.item_id();
4592 let item3_id = item3.item_id();
4593 let item4_id = item4.item_id();
4594 pane.close_items(cx, SaveIntent::Close, move |id| {
4595 [item1_id, item3_id, item4_id].contains(&id)
4596 })
4597 });
4598 cx.executor().run_until_parked();
4599
4600 assert!(cx.has_pending_prompt());
4601 // Ignore "Save all" prompt
4602 cx.simulate_prompt_answer(2);
4603 cx.executor().run_until_parked();
4604 // There's a prompt to save item 1.
4605 pane.update(cx, |pane, _| {
4606 assert_eq!(pane.items_len(), 4);
4607 assert_eq!(pane.active_item().unwrap().item_id(), item1.item_id());
4608 });
4609 // Confirm saving item 1.
4610 cx.simulate_prompt_answer(0);
4611 cx.executor().run_until_parked();
4612
4613 // Item 1 is saved. There's a prompt to save item 3.
4614 pane.update(cx, |pane, cx| {
4615 assert_eq!(item1.read(cx).save_count, 1);
4616 assert_eq!(item1.read(cx).save_as_count, 0);
4617 assert_eq!(item1.read(cx).reload_count, 0);
4618 assert_eq!(pane.items_len(), 3);
4619 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
4620 });
4621 assert!(cx.has_pending_prompt());
4622
4623 // Cancel saving item 3.
4624 cx.simulate_prompt_answer(1);
4625 cx.executor().run_until_parked();
4626
4627 // Item 3 is reloaded. There's a prompt to save item 4.
4628 pane.update(cx, |pane, cx| {
4629 assert_eq!(item3.read(cx).save_count, 0);
4630 assert_eq!(item3.read(cx).save_as_count, 0);
4631 assert_eq!(item3.read(cx).reload_count, 1);
4632 assert_eq!(pane.items_len(), 2);
4633 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
4634 });
4635 assert!(cx.has_pending_prompt());
4636
4637 // Confirm saving item 4.
4638 cx.simulate_prompt_answer(0);
4639 cx.executor().run_until_parked();
4640
4641 // There's a prompt for a path for item 4.
4642 cx.simulate_new_path_selection(|_| Some(Default::default()));
4643 close_items.await.unwrap();
4644
4645 // The requested items are closed.
4646 pane.update(cx, |pane, cx| {
4647 assert_eq!(item4.read(cx).save_count, 0);
4648 assert_eq!(item4.read(cx).save_as_count, 1);
4649 assert_eq!(item4.read(cx).reload_count, 0);
4650 assert_eq!(pane.items_len(), 1);
4651 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
4652 });
4653 }
4654
4655 #[gpui::test]
4656 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
4657 init_test(cx);
4658
4659 let fs = FakeFs::new(cx.executor());
4660 let project = Project::test(fs, [], cx).await;
4661 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4662
4663 // Create several workspace items with single project entries, and two
4664 // workspace items with multiple project entries.
4665 let single_entry_items = (0..=4)
4666 .map(|project_entry_id| {
4667 cx.new_view(|cx| {
4668 TestItem::new(cx)
4669 .with_dirty(true)
4670 .with_project_items(&[TestProjectItem::new(
4671 project_entry_id,
4672 &format!("{project_entry_id}.txt"),
4673 cx,
4674 )])
4675 })
4676 })
4677 .collect::<Vec<_>>();
4678 let item_2_3 = cx.new_view(|cx| {
4679 TestItem::new(cx)
4680 .with_dirty(true)
4681 .with_singleton(false)
4682 .with_project_items(&[
4683 single_entry_items[2].read(cx).project_items[0].clone(),
4684 single_entry_items[3].read(cx).project_items[0].clone(),
4685 ])
4686 });
4687 let item_3_4 = cx.new_view(|cx| {
4688 TestItem::new(cx)
4689 .with_dirty(true)
4690 .with_singleton(false)
4691 .with_project_items(&[
4692 single_entry_items[3].read(cx).project_items[0].clone(),
4693 single_entry_items[4].read(cx).project_items[0].clone(),
4694 ])
4695 });
4696
4697 // Create two panes that contain the following project entries:
4698 // left pane:
4699 // multi-entry items: (2, 3)
4700 // single-entry items: 0, 1, 2, 3, 4
4701 // right pane:
4702 // single-entry items: 1
4703 // multi-entry items: (3, 4)
4704 let left_pane = workspace.update(cx, |workspace, cx| {
4705 let left_pane = workspace.active_pane().clone();
4706 workspace.add_item(Box::new(item_2_3.clone()), cx);
4707 for item in single_entry_items {
4708 workspace.add_item(Box::new(item), cx);
4709 }
4710 left_pane.update(cx, |pane, cx| {
4711 pane.activate_item(2, true, true, cx);
4712 });
4713
4714 let right_pane = workspace
4715 .split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
4716 .unwrap();
4717
4718 right_pane.update(cx, |pane, cx| {
4719 pane.add_item(Box::new(item_3_4.clone()), true, true, None, cx);
4720 });
4721
4722 left_pane
4723 });
4724
4725 cx.focus_view(&left_pane);
4726
4727 // When closing all of the items in the left pane, we should be prompted twice:
4728 // once for project entry 0, and once for project entry 2. Project entries 1,
4729 // 3, and 4 are all still open in the other paten. After those two
4730 // prompts, the task should complete.
4731
4732 let close = left_pane.update(cx, |pane, cx| {
4733 pane.close_all_items(&CloseAllItems::default(), cx).unwrap()
4734 });
4735 cx.executor().run_until_parked();
4736
4737 // Discard "Save all" prompt
4738 cx.simulate_prompt_answer(2);
4739
4740 cx.executor().run_until_parked();
4741 left_pane.update(cx, |pane, cx| {
4742 assert_eq!(
4743 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
4744 &[ProjectEntryId::from_proto(0)]
4745 );
4746 });
4747 cx.simulate_prompt_answer(0);
4748
4749 cx.executor().run_until_parked();
4750 left_pane.update(cx, |pane, cx| {
4751 assert_eq!(
4752 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
4753 &[ProjectEntryId::from_proto(2)]
4754 );
4755 });
4756 cx.simulate_prompt_answer(0);
4757
4758 cx.executor().run_until_parked();
4759 close.await.unwrap();
4760 left_pane.update(cx, |pane, _| {
4761 assert_eq!(pane.items_len(), 0);
4762 });
4763 }
4764
4765 #[gpui::test]
4766 async fn test_autosave(cx: &mut gpui::TestAppContext) {
4767 init_test(cx);
4768
4769 let fs = FakeFs::new(cx.executor());
4770 let project = Project::test(fs, [], cx).await;
4771 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4772 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4773
4774 let item = cx.new_view(|cx| {
4775 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4776 });
4777 let item_id = item.entity_id();
4778 workspace.update(cx, |workspace, cx| {
4779 workspace.add_item(Box::new(item.clone()), cx);
4780 });
4781
4782 // Autosave on window change.
4783 item.update(cx, |item, cx| {
4784 cx.update_global(|settings: &mut SettingsStore, cx| {
4785 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4786 settings.autosave = Some(AutosaveSetting::OnWindowChange);
4787 })
4788 });
4789 item.is_dirty = true;
4790 });
4791
4792 // Deactivating the window saves the file.
4793 cx.deactivate_window();
4794 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
4795
4796 // Autosave on focus change.
4797 item.update(cx, |item, cx| {
4798 cx.focus_self();
4799 cx.update_global(|settings: &mut SettingsStore, cx| {
4800 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4801 settings.autosave = Some(AutosaveSetting::OnFocusChange);
4802 })
4803 });
4804 item.is_dirty = true;
4805 });
4806
4807 // Blurring the item saves the file.
4808 item.update(cx, |_, cx| cx.blur());
4809 cx.executor().run_until_parked();
4810 item.update(cx, |item, _| assert_eq!(item.save_count, 2));
4811
4812 // Deactivating the window still saves the file.
4813 cx.update(|cx| cx.activate_window());
4814 item.update(cx, |item, cx| {
4815 cx.focus_self();
4816 item.is_dirty = true;
4817 });
4818 cx.deactivate_window();
4819
4820 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
4821
4822 // Autosave after delay.
4823 item.update(cx, |item, cx| {
4824 cx.update_global(|settings: &mut SettingsStore, cx| {
4825 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4826 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
4827 })
4828 });
4829 item.is_dirty = true;
4830 cx.emit(ItemEvent::Edit);
4831 });
4832
4833 // Delay hasn't fully expired, so the file is still dirty and unsaved.
4834 cx.executor().advance_clock(Duration::from_millis(250));
4835 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
4836
4837 // After delay expires, the file is saved.
4838 cx.executor().advance_clock(Duration::from_millis(250));
4839 item.update(cx, |item, _| assert_eq!(item.save_count, 4));
4840
4841 // Autosave on focus change, ensuring closing the tab counts as such.
4842 item.update(cx, |item, cx| {
4843 cx.update_global(|settings: &mut SettingsStore, cx| {
4844 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4845 settings.autosave = Some(AutosaveSetting::OnFocusChange);
4846 })
4847 });
4848 item.is_dirty = true;
4849 });
4850
4851 pane.update(cx, |pane, cx| {
4852 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
4853 })
4854 .await
4855 .unwrap();
4856 assert!(!cx.has_pending_prompt());
4857 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
4858
4859 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
4860 workspace.update(cx, |workspace, cx| {
4861 workspace.add_item(Box::new(item.clone()), cx);
4862 });
4863 item.update(cx, |item, cx| {
4864 item.project_items[0].update(cx, |item, _| {
4865 item.entry_id = None;
4866 });
4867 item.is_dirty = true;
4868 cx.blur();
4869 });
4870 cx.run_until_parked();
4871 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
4872
4873 // Ensure autosave is prevented for deleted files also when closing the buffer.
4874 let _close_items = pane.update(cx, |pane, cx| {
4875 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
4876 });
4877 cx.run_until_parked();
4878 assert!(cx.has_pending_prompt());
4879 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
4880 }
4881
4882 #[gpui::test]
4883 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
4884 init_test(cx);
4885
4886 let fs = FakeFs::new(cx.executor());
4887
4888 let project = Project::test(fs, [], cx).await;
4889 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4890
4891 let item = cx.new_view(|cx| {
4892 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4893 });
4894 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4895 let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
4896 let toolbar_notify_count = Rc::new(RefCell::new(0));
4897
4898 workspace.update(cx, |workspace, cx| {
4899 workspace.add_item(Box::new(item.clone()), cx);
4900 let toolbar_notification_count = toolbar_notify_count.clone();
4901 cx.observe(&toolbar, move |_, _, _| {
4902 *toolbar_notification_count.borrow_mut() += 1
4903 })
4904 .detach();
4905 });
4906
4907 pane.update(cx, |pane, _| {
4908 assert!(!pane.can_navigate_backward());
4909 assert!(!pane.can_navigate_forward());
4910 });
4911
4912 item.update(cx, |item, cx| {
4913 item.set_state("one".to_string(), cx);
4914 });
4915
4916 // Toolbar must be notified to re-render the navigation buttons
4917 assert_eq!(*toolbar_notify_count.borrow(), 1);
4918
4919 pane.update(cx, |pane, _| {
4920 assert!(pane.can_navigate_backward());
4921 assert!(!pane.can_navigate_forward());
4922 });
4923
4924 workspace
4925 .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
4926 .await
4927 .unwrap();
4928
4929 assert_eq!(*toolbar_notify_count.borrow(), 2);
4930 pane.update(cx, |pane, _| {
4931 assert!(!pane.can_navigate_backward());
4932 assert!(pane.can_navigate_forward());
4933 });
4934 }
4935
4936 #[gpui::test]
4937 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
4938 init_test(cx);
4939 let fs = FakeFs::new(cx.executor());
4940
4941 let project = Project::test(fs, [], cx).await;
4942 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4943
4944 let panel = workspace.update(cx, |workspace, cx| {
4945 let panel = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
4946 workspace.add_panel(panel.clone(), cx);
4947
4948 workspace
4949 .right_dock()
4950 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
4951
4952 panel
4953 });
4954
4955 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4956 pane.update(cx, |pane, cx| {
4957 let item = cx.new_view(|cx| TestItem::new(cx));
4958 pane.add_item(Box::new(item), true, true, None, cx);
4959 });
4960
4961 // Transfer focus from center to panel
4962 workspace.update(cx, |workspace, cx| {
4963 workspace.toggle_panel_focus::<TestPanel>(cx);
4964 });
4965
4966 workspace.update(cx, |workspace, cx| {
4967 assert!(workspace.right_dock().read(cx).is_open());
4968 assert!(!panel.is_zoomed(cx));
4969 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
4970 });
4971
4972 // Transfer focus from panel to center
4973 workspace.update(cx, |workspace, cx| {
4974 workspace.toggle_panel_focus::<TestPanel>(cx);
4975 });
4976
4977 workspace.update(cx, |workspace, cx| {
4978 assert!(workspace.right_dock().read(cx).is_open());
4979 assert!(!panel.is_zoomed(cx));
4980 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
4981 });
4982
4983 // Close the dock
4984 workspace.update(cx, |workspace, cx| {
4985 workspace.toggle_dock(DockPosition::Right, cx);
4986 });
4987
4988 workspace.update(cx, |workspace, cx| {
4989 assert!(!workspace.right_dock().read(cx).is_open());
4990 assert!(!panel.is_zoomed(cx));
4991 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
4992 });
4993
4994 // Open the dock
4995 workspace.update(cx, |workspace, cx| {
4996 workspace.toggle_dock(DockPosition::Right, cx);
4997 });
4998
4999 workspace.update(cx, |workspace, cx| {
5000 assert!(workspace.right_dock().read(cx).is_open());
5001 assert!(!panel.is_zoomed(cx));
5002 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5003 });
5004
5005 // Focus and zoom panel
5006 panel.update(cx, |panel, cx| {
5007 cx.focus_self();
5008 panel.set_zoomed(true, cx)
5009 });
5010
5011 workspace.update(cx, |workspace, cx| {
5012 assert!(workspace.right_dock().read(cx).is_open());
5013 assert!(panel.is_zoomed(cx));
5014 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5015 });
5016
5017 // Transfer focus to the center closes the dock
5018 workspace.update(cx, |workspace, cx| {
5019 workspace.toggle_panel_focus::<TestPanel>(cx);
5020 });
5021
5022 workspace.update(cx, |workspace, cx| {
5023 assert!(!workspace.right_dock().read(cx).is_open());
5024 assert!(panel.is_zoomed(cx));
5025 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5026 });
5027
5028 // Transferring focus back to the panel keeps it zoomed
5029 workspace.update(cx, |workspace, cx| {
5030 workspace.toggle_panel_focus::<TestPanel>(cx);
5031 });
5032
5033 workspace.update(cx, |workspace, cx| {
5034 assert!(workspace.right_dock().read(cx).is_open());
5035 assert!(panel.is_zoomed(cx));
5036 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5037 });
5038
5039 // Close the dock while it is zoomed
5040 workspace.update(cx, |workspace, cx| {
5041 workspace.toggle_dock(DockPosition::Right, cx)
5042 });
5043
5044 workspace.update(cx, |workspace, cx| {
5045 assert!(!workspace.right_dock().read(cx).is_open());
5046 assert!(panel.is_zoomed(cx));
5047 assert!(workspace.zoomed.is_none());
5048 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5049 });
5050
5051 // Opening the dock, when it's zoomed, retains focus
5052 workspace.update(cx, |workspace, cx| {
5053 workspace.toggle_dock(DockPosition::Right, cx)
5054 });
5055
5056 workspace.update(cx, |workspace, cx| {
5057 assert!(workspace.right_dock().read(cx).is_open());
5058 assert!(panel.is_zoomed(cx));
5059 assert!(workspace.zoomed.is_some());
5060 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5061 });
5062
5063 // Unzoom and close the panel, zoom the active pane.
5064 panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
5065 workspace.update(cx, |workspace, cx| {
5066 workspace.toggle_dock(DockPosition::Right, cx)
5067 });
5068 pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
5069
5070 // Opening a dock unzooms the pane.
5071 workspace.update(cx, |workspace, cx| {
5072 workspace.toggle_dock(DockPosition::Right, cx)
5073 });
5074 workspace.update(cx, |workspace, cx| {
5075 let pane = pane.read(cx);
5076 assert!(!pane.is_zoomed());
5077 assert!(!pane.focus_handle(cx).is_focused(cx));
5078 assert!(workspace.right_dock().read(cx).is_open());
5079 assert!(workspace.zoomed.is_none());
5080 });
5081 }
5082
5083 struct TestModal(FocusHandle);
5084
5085 impl TestModal {
5086 fn new(cx: &mut ViewContext<Self>) -> Self {
5087 Self(cx.focus_handle())
5088 }
5089 }
5090
5091 impl EventEmitter<DismissEvent> for TestModal {}
5092
5093 impl FocusableView for TestModal {
5094 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
5095 self.0.clone()
5096 }
5097 }
5098
5099 impl ModalView for TestModal {}
5100
5101 impl Render for TestModal {
5102 fn render(&mut self, _cx: &mut ViewContext<TestModal>) -> impl IntoElement {
5103 div().track_focus(&self.0)
5104 }
5105 }
5106
5107 #[gpui::test]
5108 async fn test_panels(cx: &mut gpui::TestAppContext) {
5109 init_test(cx);
5110 let fs = FakeFs::new(cx.executor());
5111
5112 let project = Project::test(fs, [], cx).await;
5113 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5114
5115 let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
5116 let panel_1 = cx.new_view(|cx| TestPanel::new(DockPosition::Left, cx));
5117 workspace.add_panel(panel_1.clone(), cx);
5118 workspace
5119 .left_dock()
5120 .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
5121 let panel_2 = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
5122 workspace.add_panel(panel_2.clone(), cx);
5123 workspace
5124 .right_dock()
5125 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
5126
5127 let left_dock = workspace.left_dock();
5128 assert_eq!(
5129 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5130 panel_1.panel_id()
5131 );
5132 assert_eq!(
5133 left_dock.read(cx).active_panel_size(cx).unwrap(),
5134 panel_1.size(cx)
5135 );
5136
5137 left_dock.update(cx, |left_dock, cx| {
5138 left_dock.resize_active_panel(Some(px(1337.)), cx)
5139 });
5140 assert_eq!(
5141 workspace
5142 .right_dock()
5143 .read(cx)
5144 .visible_panel()
5145 .unwrap()
5146 .panel_id(),
5147 panel_2.panel_id(),
5148 );
5149
5150 (panel_1, panel_2)
5151 });
5152
5153 // Move panel_1 to the right
5154 panel_1.update(cx, |panel_1, cx| {
5155 panel_1.set_position(DockPosition::Right, cx)
5156 });
5157
5158 workspace.update(cx, |workspace, cx| {
5159 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
5160 // Since it was the only panel on the left, the left dock should now be closed.
5161 assert!(!workspace.left_dock().read(cx).is_open());
5162 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
5163 let right_dock = workspace.right_dock();
5164 assert_eq!(
5165 right_dock.read(cx).visible_panel().unwrap().panel_id(),
5166 panel_1.panel_id()
5167 );
5168 assert_eq!(
5169 right_dock.read(cx).active_panel_size(cx).unwrap(),
5170 px(1337.)
5171 );
5172
5173 // Now we move panel_2 to the left
5174 panel_2.set_position(DockPosition::Left, cx);
5175 });
5176
5177 workspace.update(cx, |workspace, cx| {
5178 // Since panel_2 was not visible on the right, we don't open the left dock.
5179 assert!(!workspace.left_dock().read(cx).is_open());
5180 // And the right dock is unaffected in it's displaying of panel_1
5181 assert!(workspace.right_dock().read(cx).is_open());
5182 assert_eq!(
5183 workspace
5184 .right_dock()
5185 .read(cx)
5186 .visible_panel()
5187 .unwrap()
5188 .panel_id(),
5189 panel_1.panel_id(),
5190 );
5191 });
5192
5193 // Move panel_1 back to the left
5194 panel_1.update(cx, |panel_1, cx| {
5195 panel_1.set_position(DockPosition::Left, cx)
5196 });
5197
5198 workspace.update(cx, |workspace, cx| {
5199 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
5200 let left_dock = workspace.left_dock();
5201 assert!(left_dock.read(cx).is_open());
5202 assert_eq!(
5203 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5204 panel_1.panel_id()
5205 );
5206 assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), px(1337.));
5207 // And the right dock should be closed as it no longer has any panels.
5208 assert!(!workspace.right_dock().read(cx).is_open());
5209
5210 // Now we move panel_1 to the bottom
5211 panel_1.set_position(DockPosition::Bottom, cx);
5212 });
5213
5214 workspace.update(cx, |workspace, cx| {
5215 // Since panel_1 was visible on the left, we close the left dock.
5216 assert!(!workspace.left_dock().read(cx).is_open());
5217 // The bottom dock is sized based on the panel's default size,
5218 // since the panel orientation changed from vertical to horizontal.
5219 let bottom_dock = workspace.bottom_dock();
5220 assert_eq!(
5221 bottom_dock.read(cx).active_panel_size(cx).unwrap(),
5222 panel_1.size(cx),
5223 );
5224 // Close bottom dock and move panel_1 back to the left.
5225 bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
5226 panel_1.set_position(DockPosition::Left, cx);
5227 });
5228
5229 // Emit activated event on panel 1
5230 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
5231
5232 // Now the left dock is open and panel_1 is active and focused.
5233 workspace.update(cx, |workspace, cx| {
5234 let left_dock = workspace.left_dock();
5235 assert!(left_dock.read(cx).is_open());
5236 assert_eq!(
5237 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5238 panel_1.panel_id(),
5239 );
5240 assert!(panel_1.focus_handle(cx).is_focused(cx));
5241 });
5242
5243 // Emit closed event on panel 2, which is not active
5244 panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
5245
5246 // Wo don't close the left dock, because panel_2 wasn't the active panel
5247 workspace.update(cx, |workspace, cx| {
5248 let left_dock = workspace.left_dock();
5249 assert!(left_dock.read(cx).is_open());
5250 assert_eq!(
5251 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5252 panel_1.panel_id(),
5253 );
5254 });
5255
5256 // Emitting a ZoomIn event shows the panel as zoomed.
5257 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
5258 workspace.update(cx, |workspace, _| {
5259 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5260 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
5261 });
5262
5263 // Move panel to another dock while it is zoomed
5264 panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
5265 workspace.update(cx, |workspace, _| {
5266 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5267
5268 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5269 });
5270
5271 // This is a helper for getting a:
5272 // - valid focus on an element,
5273 // - that isn't a part of the panes and panels system of the Workspace,
5274 // - and doesn't trigger the 'on_focus_lost' API.
5275 let focus_other_view = {
5276 let workspace = workspace.clone();
5277 move |cx: &mut VisualTestContext| {
5278 workspace.update(cx, |workspace, cx| {
5279 if let Some(_) = workspace.active_modal::<TestModal>(cx) {
5280 workspace.toggle_modal(cx, TestModal::new);
5281 workspace.toggle_modal(cx, TestModal::new);
5282 } else {
5283 workspace.toggle_modal(cx, TestModal::new);
5284 }
5285 })
5286 }
5287 };
5288
5289 // If focus is transferred to another view that's not a panel or another pane, we still show
5290 // the panel as zoomed.
5291 focus_other_view(cx);
5292 workspace.update(cx, |workspace, _| {
5293 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5294 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5295 });
5296
5297 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
5298 workspace.update(cx, |_, cx| cx.focus_self());
5299 workspace.update(cx, |workspace, _| {
5300 assert_eq!(workspace.zoomed, None);
5301 assert_eq!(workspace.zoomed_position, None);
5302 });
5303
5304 // If focus is transferred again to another view that's not a panel or a pane, we won't
5305 // show the panel as zoomed because it wasn't zoomed before.
5306 focus_other_view(cx);
5307 workspace.update(cx, |workspace, _| {
5308 assert_eq!(workspace.zoomed, None);
5309 assert_eq!(workspace.zoomed_position, None);
5310 });
5311
5312 // When the panel is activated, it is zoomed again.
5313 cx.dispatch_action(ToggleRightDock);
5314 workspace.update(cx, |workspace, _| {
5315 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5316 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5317 });
5318
5319 // Emitting a ZoomOut event unzooms the panel.
5320 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
5321 workspace.update(cx, |workspace, _| {
5322 assert_eq!(workspace.zoomed, None);
5323 assert_eq!(workspace.zoomed_position, None);
5324 });
5325
5326 // Emit closed event on panel 1, which is active
5327 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
5328
5329 // Now the left dock is closed, because panel_1 was the active panel
5330 workspace.update(cx, |workspace, cx| {
5331 let right_dock = workspace.right_dock();
5332 assert!(!right_dock.read(cx).is_open());
5333 });
5334 }
5335
5336 pub fn init_test(cx: &mut TestAppContext) {
5337 cx.update(|cx| {
5338 let settings_store = SettingsStore::test(cx);
5339 cx.set_global(settings_store);
5340 theme::init(theme::LoadThemes::JustBase, cx);
5341 language::init(cx);
5342 crate::init_settings(cx);
5343 Project::init_settings(cx);
5344 });
5345 }
5346}