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 item_to_move = source
2262 .read(cx)
2263 .items()
2264 .enumerate()
2265 .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move);
2266
2267 if item_to_move.is_none() {
2268 log::warn!("Tried to move item handle which was not in `from` pane. Maybe tab was closed during drop");
2269 return;
2270 }
2271 let (item_ix, item_handle) = item_to_move.unwrap();
2272 let item_handle = item_handle.clone();
2273
2274 if source != destination {
2275 // Close item from previous pane
2276 source.update(cx, |source, cx| {
2277 source.remove_item(item_ix, false, cx);
2278 });
2279 }
2280
2281 // This automatically removes duplicate items in the pane
2282 destination.update(cx, |destination, cx| {
2283 destination.add_item(item_handle, true, true, Some(destination_index), cx);
2284 destination.focus(cx)
2285 });
2286 }
2287
2288 fn remove_pane(&mut self, pane: View<Pane>, cx: &mut ViewContext<Self>) {
2289 if self.center.remove(&pane).unwrap() {
2290 self.force_remove_pane(&pane, cx);
2291 self.unfollow(&pane, cx);
2292 self.last_leaders_by_pane.remove(&pane.downgrade());
2293 for removed_item in pane.read(cx).items() {
2294 self.panes_by_item.remove(&removed_item.item_id());
2295 }
2296
2297 cx.notify();
2298 } else {
2299 self.active_item_path_changed(cx);
2300 }
2301 }
2302
2303 pub fn panes(&self) -> &[View<Pane>] {
2304 &self.panes
2305 }
2306
2307 pub fn active_pane(&self) -> &View<Pane> {
2308 &self.active_pane
2309 }
2310
2311 pub fn pane_for(&self, handle: &dyn ItemHandle) -> Option<View<Pane>> {
2312 let weak_pane = self.panes_by_item.get(&handle.item_id())?;
2313 weak_pane.upgrade()
2314 }
2315
2316 fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
2317 self.follower_states.retain(|_, state| {
2318 if state.leader_id == peer_id {
2319 for item in state.items_by_leader_view_id.values() {
2320 item.set_leader_peer_id(None, cx);
2321 }
2322 false
2323 } else {
2324 true
2325 }
2326 });
2327 cx.notify();
2328 }
2329
2330 pub fn start_following(
2331 &mut self,
2332 leader_id: PeerId,
2333 cx: &mut ViewContext<Self>,
2334 ) -> Option<Task<Result<()>>> {
2335 let pane = self.active_pane().clone();
2336
2337 self.last_leaders_by_pane
2338 .insert(pane.downgrade(), leader_id);
2339 self.unfollow(&pane, cx);
2340 self.follower_states.insert(
2341 pane.clone(),
2342 FollowerState {
2343 leader_id,
2344 active_view_id: None,
2345 items_by_leader_view_id: Default::default(),
2346 },
2347 );
2348 cx.notify();
2349
2350 let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
2351 let project_id = self.project.read(cx).remote_id();
2352 let request = self.app_state.client.request(proto::Follow {
2353 room_id,
2354 project_id,
2355 leader_id: Some(leader_id),
2356 });
2357
2358 Some(cx.spawn(|this, mut cx| async move {
2359 let response = request.await?;
2360 this.update(&mut cx, |this, _| {
2361 let state = this
2362 .follower_states
2363 .get_mut(&pane)
2364 .ok_or_else(|| anyhow!("following interrupted"))?;
2365 state.active_view_id = if let Some(active_view_id) = response.active_view_id {
2366 Some(ViewId::from_proto(active_view_id)?)
2367 } else {
2368 None
2369 };
2370 Ok::<_, anyhow::Error>(())
2371 })??;
2372 Self::add_views_from_leader(
2373 this.clone(),
2374 leader_id,
2375 vec![pane],
2376 response.views,
2377 &mut cx,
2378 )
2379 .await?;
2380 this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?;
2381 Ok(())
2382 }))
2383 }
2384
2385 pub fn follow_next_collaborator(
2386 &mut self,
2387 _: &FollowNextCollaborator,
2388 cx: &mut ViewContext<Self>,
2389 ) {
2390 let collaborators = self.project.read(cx).collaborators();
2391 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
2392 let mut collaborators = collaborators.keys().copied();
2393 for peer_id in collaborators.by_ref() {
2394 if peer_id == leader_id {
2395 break;
2396 }
2397 }
2398 collaborators.next()
2399 } else if let Some(last_leader_id) =
2400 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
2401 {
2402 if collaborators.contains_key(last_leader_id) {
2403 Some(*last_leader_id)
2404 } else {
2405 None
2406 }
2407 } else {
2408 None
2409 };
2410
2411 let pane = self.active_pane.clone();
2412 let Some(leader_id) = next_leader_id.or_else(|| collaborators.keys().copied().next())
2413 else {
2414 return;
2415 };
2416 if Some(leader_id) == self.unfollow(&pane, cx) {
2417 return;
2418 }
2419 self.start_following(leader_id, cx)
2420 .map(|task| task.detach_and_log_err(cx));
2421 }
2422
2423 pub fn follow(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) {
2424 let Some(room) = ActiveCall::global(cx).read(cx).room() else {
2425 return;
2426 };
2427 let room = room.read(cx);
2428 let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else {
2429 return;
2430 };
2431
2432 let project = self.project.read(cx);
2433
2434 let other_project_id = match remote_participant.location {
2435 call::ParticipantLocation::External => None,
2436 call::ParticipantLocation::UnsharedProject => None,
2437 call::ParticipantLocation::SharedProject { project_id } => {
2438 if Some(project_id) == project.remote_id() {
2439 None
2440 } else {
2441 Some(project_id)
2442 }
2443 }
2444 };
2445
2446 // if they are active in another project, follow there.
2447 if let Some(project_id) = other_project_id {
2448 let app_state = self.app_state.clone();
2449 crate::join_remote_project(project_id, remote_participant.user.id, app_state, cx)
2450 .detach_and_log_err(cx);
2451 }
2452
2453 // if you're already following, find the right pane and focus it.
2454 for (pane, state) in &self.follower_states {
2455 if leader_id == state.leader_id {
2456 cx.focus_view(pane);
2457 return;
2458 }
2459 }
2460
2461 // Otherwise, follow.
2462 self.start_following(leader_id, cx)
2463 .map(|task| task.detach_and_log_err(cx));
2464 }
2465
2466 pub fn unfollow(&mut self, pane: &View<Pane>, cx: &mut ViewContext<Self>) -> Option<PeerId> {
2467 let state = self.follower_states.remove(pane)?;
2468 let leader_id = state.leader_id;
2469 for (_, item) in state.items_by_leader_view_id {
2470 item.set_leader_peer_id(None, cx);
2471 }
2472
2473 if self
2474 .follower_states
2475 .values()
2476 .all(|state| state.leader_id != state.leader_id)
2477 {
2478 let project_id = self.project.read(cx).remote_id();
2479 let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
2480 self.app_state
2481 .client
2482 .send(proto::Unfollow {
2483 room_id,
2484 project_id,
2485 leader_id: Some(leader_id),
2486 })
2487 .log_err();
2488 }
2489
2490 cx.notify();
2491 Some(leader_id)
2492 }
2493
2494 pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
2495 self.follower_states
2496 .values()
2497 .any(|state| state.leader_id == peer_id)
2498 }
2499
2500 fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
2501 let active_entry = self.active_project_path(cx);
2502 self.project
2503 .update(cx, |project, cx| project.set_active_path(active_entry, cx));
2504 self.update_window_title(cx);
2505 }
2506
2507 fn update_window_title(&mut self, cx: &mut ViewContext<Self>) {
2508 let project = self.project().read(cx);
2509 let mut title = String::new();
2510
2511 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
2512 let filename = path
2513 .path
2514 .file_name()
2515 .map(|s| s.to_string_lossy())
2516 .or_else(|| {
2517 Some(Cow::Borrowed(
2518 project
2519 .worktree_for_id(path.worktree_id, cx)?
2520 .read(cx)
2521 .root_name(),
2522 ))
2523 });
2524
2525 if let Some(filename) = filename {
2526 title.push_str(filename.as_ref());
2527 title.push_str(" — ");
2528 }
2529 }
2530
2531 for (i, name) in project.worktree_root_names(cx).enumerate() {
2532 if i > 0 {
2533 title.push_str(", ");
2534 }
2535 title.push_str(name);
2536 }
2537
2538 if title.is_empty() {
2539 title = "empty project".to_string();
2540 }
2541
2542 if project.is_remote() {
2543 title.push_str(" ↙");
2544 } else if project.is_shared() {
2545 title.push_str(" ↗");
2546 }
2547
2548 cx.set_window_title(&title);
2549 }
2550
2551 fn update_window_edited(&mut self, cx: &mut ViewContext<Self>) {
2552 let is_edited = !self.project.read(cx).is_disconnected()
2553 && self
2554 .items(cx)
2555 .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
2556 if is_edited != self.window_edited {
2557 self.window_edited = is_edited;
2558 cx.set_window_edited(self.window_edited)
2559 }
2560 }
2561
2562 fn render_notifications(&self, _cx: &ViewContext<Self>) -> Option<Div> {
2563 if self.notifications.is_empty() {
2564 None
2565 } else {
2566 Some(
2567 div()
2568 .absolute()
2569 .z_index(100)
2570 .right_3()
2571 .bottom_3()
2572 .w_96()
2573 .h_full()
2574 .flex()
2575 .flex_col()
2576 .justify_end()
2577 .gap_2()
2578 .children(
2579 self.notifications
2580 .iter()
2581 .map(|(_, _, notification)| notification.to_any()),
2582 ),
2583 )
2584 }
2585 }
2586
2587 // RPC handlers
2588
2589 fn handle_follow(
2590 &mut self,
2591 follower_project_id: Option<u64>,
2592 cx: &mut ViewContext<Self>,
2593 ) -> proto::FollowResponse {
2594 let client = &self.app_state.client;
2595 let project_id = self.project.read(cx).remote_id();
2596
2597 let active_view_id = self.active_item(cx).and_then(|i| {
2598 Some(
2599 i.to_followable_item_handle(cx)?
2600 .remote_id(client, cx)?
2601 .to_proto(),
2602 )
2603 });
2604
2605 cx.notify();
2606
2607 self.last_active_view_id = active_view_id.clone();
2608 proto::FollowResponse {
2609 active_view_id,
2610 views: self
2611 .panes()
2612 .iter()
2613 .flat_map(|pane| {
2614 let leader_id = self.leader_for_pane(pane);
2615 pane.read(cx).items().filter_map({
2616 let cx = &cx;
2617 move |item| {
2618 let item = item.to_followable_item_handle(cx)?;
2619
2620 // If the item belongs to a particular project, then it should
2621 // only be included if this project is shared, and the follower
2622 // is in thie project.
2623 //
2624 // Some items, like channel notes, do not belong to a particular
2625 // project, so they should be included regardless of whether the
2626 // current project is shared, or what project the follower is in.
2627 if item.is_project_item(cx)
2628 && (project_id.is_none() || project_id != follower_project_id)
2629 {
2630 return None;
2631 }
2632
2633 let id = item.remote_id(client, cx)?.to_proto();
2634 let variant = item.to_state_proto(cx)?;
2635 Some(proto::View {
2636 id: Some(id),
2637 leader_id,
2638 variant: Some(variant),
2639 })
2640 }
2641 })
2642 })
2643 .collect(),
2644 }
2645 }
2646
2647 fn handle_update_followers(
2648 &mut self,
2649 leader_id: PeerId,
2650 message: proto::UpdateFollowers,
2651 _cx: &mut ViewContext<Self>,
2652 ) {
2653 self.leader_updates_tx
2654 .unbounded_send((leader_id, message))
2655 .ok();
2656 }
2657
2658 async fn process_leader_update(
2659 this: &WeakView<Self>,
2660 leader_id: PeerId,
2661 update: proto::UpdateFollowers,
2662 cx: &mut AsyncWindowContext,
2663 ) -> Result<()> {
2664 match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
2665 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
2666 this.update(cx, |this, _| {
2667 for (_, state) in &mut this.follower_states {
2668 if state.leader_id == leader_id {
2669 state.active_view_id =
2670 if let Some(active_view_id) = update_active_view.id.clone() {
2671 Some(ViewId::from_proto(active_view_id)?)
2672 } else {
2673 None
2674 };
2675 }
2676 }
2677 anyhow::Ok(())
2678 })??;
2679 }
2680 proto::update_followers::Variant::UpdateView(update_view) => {
2681 let variant = update_view
2682 .variant
2683 .ok_or_else(|| anyhow!("missing update view variant"))?;
2684 let id = update_view
2685 .id
2686 .ok_or_else(|| anyhow!("missing update view id"))?;
2687 let mut tasks = Vec::new();
2688 this.update(cx, |this, cx| {
2689 let project = this.project.clone();
2690 for (_, state) in &mut this.follower_states {
2691 if state.leader_id == leader_id {
2692 let view_id = ViewId::from_proto(id.clone())?;
2693 if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
2694 tasks.push(item.apply_update_proto(&project, variant.clone(), cx));
2695 }
2696 }
2697 }
2698 anyhow::Ok(())
2699 })??;
2700 try_join_all(tasks).await.log_err();
2701 }
2702 proto::update_followers::Variant::CreateView(view) => {
2703 let panes = this.update(cx, |this, _| {
2704 this.follower_states
2705 .iter()
2706 .filter_map(|(pane, state)| (state.leader_id == leader_id).then_some(pane))
2707 .cloned()
2708 .collect()
2709 })?;
2710 Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], cx).await?;
2711 }
2712 }
2713 this.update(cx, |this, cx| this.leader_updated(leader_id, cx))?;
2714 Ok(())
2715 }
2716
2717 async fn add_views_from_leader(
2718 this: WeakView<Self>,
2719 leader_id: PeerId,
2720 panes: Vec<View<Pane>>,
2721 views: Vec<proto::View>,
2722 cx: &mut AsyncWindowContext,
2723 ) -> Result<()> {
2724 let this = this.upgrade().context("workspace dropped")?;
2725
2726 let item_builders = cx.update(|_, cx| {
2727 cx.default_global::<FollowableItemBuilders>()
2728 .values()
2729 .map(|b| b.0)
2730 .collect::<Vec<_>>()
2731 })?;
2732
2733 let mut item_tasks_by_pane = HashMap::default();
2734 for pane in panes {
2735 let mut item_tasks = Vec::new();
2736 let mut leader_view_ids = Vec::new();
2737 for view in &views {
2738 let Some(id) = &view.id else { continue };
2739 let id = ViewId::from_proto(id.clone())?;
2740 let mut variant = view.variant.clone();
2741 if variant.is_none() {
2742 Err(anyhow!("missing view variant"))?;
2743 }
2744 for build_item in &item_builders {
2745 let task = cx.update(|_, cx| {
2746 build_item(pane.clone(), this.clone(), id, &mut variant, cx)
2747 })?;
2748 if let Some(task) = task {
2749 item_tasks.push(task);
2750 leader_view_ids.push(id);
2751 break;
2752 } else {
2753 assert!(variant.is_some());
2754 }
2755 }
2756 }
2757
2758 item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids));
2759 }
2760
2761 for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
2762 let items = futures::future::try_join_all(item_tasks).await?;
2763 this.update(cx, |this, cx| {
2764 let state = this.follower_states.get_mut(&pane)?;
2765 for (id, item) in leader_view_ids.into_iter().zip(items) {
2766 item.set_leader_peer_id(Some(leader_id), cx);
2767 state.items_by_leader_view_id.insert(id, item);
2768 }
2769
2770 Some(())
2771 })?;
2772 }
2773 Ok(())
2774 }
2775
2776 fn update_active_view_for_followers(&mut self, cx: &mut ViewContext<Self>) {
2777 let mut is_project_item = true;
2778 let mut update = proto::UpdateActiveView::default();
2779
2780 if let Some(item) = self.active_item(cx) {
2781 if item.focus_handle(cx).contains_focused(cx) {
2782 if let Some(item) = item.to_followable_item_handle(cx) {
2783 is_project_item = item.is_project_item(cx);
2784 update = proto::UpdateActiveView {
2785 id: item
2786 .remote_id(&self.app_state.client, cx)
2787 .map(|id| id.to_proto()),
2788 leader_id: self.leader_for_pane(&self.active_pane),
2789 };
2790 }
2791 }
2792 }
2793
2794 if update.id != self.last_active_view_id {
2795 self.last_active_view_id = update.id.clone();
2796 self.update_followers(
2797 is_project_item,
2798 proto::update_followers::Variant::UpdateActiveView(update),
2799 cx,
2800 );
2801 }
2802 }
2803
2804 fn update_followers(
2805 &self,
2806 project_only: bool,
2807 update: proto::update_followers::Variant,
2808 cx: &mut WindowContext,
2809 ) -> Option<()> {
2810 // If this update only applies to for followers in the current project,
2811 // then skip it unless this project is shared. If it applies to all
2812 // followers, regardless of project, then set `project_id` to none,
2813 // indicating that it goes to all followers.
2814 let project_id = if project_only {
2815 Some(self.project.read(cx).remote_id()?)
2816 } else {
2817 None
2818 };
2819 self.app_state().workspace_store.update(cx, |store, cx| {
2820 store.update_followers(project_id, update, cx)
2821 })
2822 }
2823
2824 pub fn leader_for_pane(&self, pane: &View<Pane>) -> Option<PeerId> {
2825 self.follower_states.get(pane).map(|state| state.leader_id)
2826 }
2827
2828 fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
2829 cx.notify();
2830
2831 let call = self.active_call()?;
2832 let room = call.read(cx).room()?.read(cx);
2833 let participant = room.remote_participant_for_peer_id(leader_id)?;
2834 let mut items_to_activate = Vec::new();
2835
2836 let leader_in_this_app;
2837 let leader_in_this_project;
2838 match participant.location {
2839 call::ParticipantLocation::SharedProject { project_id } => {
2840 leader_in_this_app = true;
2841 leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
2842 }
2843 call::ParticipantLocation::UnsharedProject => {
2844 leader_in_this_app = true;
2845 leader_in_this_project = false;
2846 }
2847 call::ParticipantLocation::External => {
2848 leader_in_this_app = false;
2849 leader_in_this_project = false;
2850 }
2851 };
2852
2853 for (pane, state) in &self.follower_states {
2854 if state.leader_id != leader_id {
2855 continue;
2856 }
2857 if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
2858 if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) {
2859 if leader_in_this_project || !item.is_project_item(cx) {
2860 items_to_activate.push((pane.clone(), item.boxed_clone()));
2861 }
2862 }
2863 continue;
2864 }
2865
2866 if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
2867 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
2868 }
2869 }
2870
2871 for (pane, item) in items_to_activate {
2872 let pane_was_focused = pane.read(cx).has_focus(cx);
2873 if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) {
2874 pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx));
2875 } else {
2876 pane.update(cx, |pane, cx| {
2877 pane.add_item(item.boxed_clone(), false, false, None, cx)
2878 });
2879 }
2880
2881 if pane_was_focused {
2882 pane.update(cx, |pane, cx| pane.focus_active_item(cx));
2883 }
2884 }
2885
2886 None
2887 }
2888
2889 fn shared_screen_for_peer(
2890 &self,
2891 peer_id: PeerId,
2892 pane: &View<Pane>,
2893 cx: &mut ViewContext<Self>,
2894 ) -> Option<View<SharedScreen>> {
2895 let call = self.active_call()?;
2896 let room = call.read(cx).room()?.read(cx);
2897 let participant = room.remote_participant_for_peer_id(peer_id)?;
2898 let track = participant.video_tracks.values().next()?.clone();
2899 let user = participant.user.clone();
2900
2901 for item in pane.read(cx).items_of_type::<SharedScreen>() {
2902 if item.read(cx).peer_id == peer_id {
2903 return Some(item);
2904 }
2905 }
2906
2907 Some(cx.new_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
2908 }
2909
2910 pub fn on_window_activation_changed(&mut self, cx: &mut ViewContext<Self>) {
2911 if cx.is_window_active() {
2912 self.update_active_view_for_followers(cx);
2913 cx.background_executor()
2914 .spawn(persistence::DB.update_timestamp(self.database_id()))
2915 .detach();
2916 } else {
2917 for pane in &self.panes {
2918 pane.update(cx, |pane, cx| {
2919 if let Some(item) = pane.active_item() {
2920 item.workspace_deactivated(cx);
2921 }
2922 if matches!(
2923 WorkspaceSettings::get_global(cx).autosave,
2924 AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
2925 ) {
2926 for item in pane.items() {
2927 Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
2928 .detach_and_log_err(cx);
2929 }
2930 }
2931 });
2932 }
2933 }
2934 }
2935
2936 fn active_call(&self) -> Option<&Model<ActiveCall>> {
2937 self.active_call.as_ref().map(|(call, _)| call)
2938 }
2939
2940 fn on_active_call_event(
2941 &mut self,
2942 _: Model<ActiveCall>,
2943 event: &call::room::Event,
2944 cx: &mut ViewContext<Self>,
2945 ) {
2946 match event {
2947 call::room::Event::ParticipantLocationChanged { participant_id }
2948 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
2949 self.leader_updated(*participant_id, cx);
2950 }
2951 _ => {}
2952 }
2953 }
2954
2955 pub fn database_id(&self) -> WorkspaceId {
2956 self.database_id
2957 }
2958
2959 fn location(&self, cx: &AppContext) -> Option<WorkspaceLocation> {
2960 let project = self.project().read(cx);
2961
2962 if project.is_local() {
2963 Some(
2964 project
2965 .visible_worktrees(cx)
2966 .map(|worktree| worktree.read(cx).abs_path())
2967 .collect::<Vec<_>>()
2968 .into(),
2969 )
2970 } else {
2971 None
2972 }
2973 }
2974
2975 fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
2976 match member {
2977 Member::Axis(PaneAxis { members, .. }) => {
2978 for child in members.iter() {
2979 self.remove_panes(child.clone(), cx)
2980 }
2981 }
2982 Member::Pane(pane) => {
2983 self.force_remove_pane(&pane, cx);
2984 }
2985 }
2986 }
2987
2988 fn force_remove_pane(&mut self, pane: &View<Pane>, cx: &mut ViewContext<Workspace>) {
2989 self.panes.retain(|p| p != pane);
2990 self.panes
2991 .last()
2992 .unwrap()
2993 .update(cx, |pane, cx| pane.focus(cx));
2994 if self.last_active_center_pane == Some(pane.downgrade()) {
2995 self.last_active_center_pane = None;
2996 }
2997 cx.notify();
2998 }
2999
3000 fn schedule_serialize(&mut self, cx: &mut ViewContext<Self>) {
3001 self._schedule_serialize = Some(cx.spawn(|this, mut cx| async move {
3002 cx.background_executor()
3003 .timer(Duration::from_millis(100))
3004 .await;
3005 this.update(&mut cx, |this, cx| this.serialize_workspace(cx))
3006 .log_err();
3007 }));
3008 }
3009
3010 fn serialize_workspace(&self, cx: &mut ViewContext<Self>) {
3011 fn serialize_pane_handle(pane_handle: &View<Pane>, cx: &WindowContext) -> SerializedPane {
3012 let (items, active) = {
3013 let pane = pane_handle.read(cx);
3014 let active_item_id = pane.active_item().map(|item| item.item_id());
3015 (
3016 pane.items()
3017 .filter_map(|item_handle| {
3018 Some(SerializedItem {
3019 kind: Arc::from(item_handle.serialized_item_kind()?),
3020 item_id: item_handle.item_id().as_u64(),
3021 active: Some(item_handle.item_id()) == active_item_id,
3022 })
3023 })
3024 .collect::<Vec<_>>(),
3025 pane.has_focus(cx),
3026 )
3027 };
3028
3029 SerializedPane::new(items, active)
3030 }
3031
3032 fn build_serialized_pane_group(
3033 pane_group: &Member,
3034 cx: &WindowContext,
3035 ) -> SerializedPaneGroup {
3036 match pane_group {
3037 Member::Axis(PaneAxis {
3038 axis,
3039 members,
3040 flexes,
3041 bounding_boxes: _,
3042 }) => SerializedPaneGroup::Group {
3043 axis: SerializedAxis(*axis),
3044 children: members
3045 .iter()
3046 .map(|member| build_serialized_pane_group(member, cx))
3047 .collect::<Vec<_>>(),
3048 flexes: Some(flexes.lock().clone()),
3049 },
3050 Member::Pane(pane_handle) => {
3051 SerializedPaneGroup::Pane(serialize_pane_handle(&pane_handle, cx))
3052 }
3053 }
3054 }
3055
3056 fn build_serialized_docks(
3057 this: &Workspace,
3058 cx: &mut ViewContext<Workspace>,
3059 ) -> DockStructure {
3060 let left_dock = this.left_dock.read(cx);
3061 let left_visible = left_dock.is_open();
3062 let left_active_panel = left_dock
3063 .visible_panel()
3064 .and_then(|panel| Some(panel.persistent_name().to_string()));
3065 let left_dock_zoom = left_dock
3066 .visible_panel()
3067 .map(|panel| panel.is_zoomed(cx))
3068 .unwrap_or(false);
3069
3070 let right_dock = this.right_dock.read(cx);
3071 let right_visible = right_dock.is_open();
3072 let right_active_panel = right_dock
3073 .visible_panel()
3074 .and_then(|panel| Some(panel.persistent_name().to_string()));
3075 let right_dock_zoom = right_dock
3076 .visible_panel()
3077 .map(|panel| panel.is_zoomed(cx))
3078 .unwrap_or(false);
3079
3080 let bottom_dock = this.bottom_dock.read(cx);
3081 let bottom_visible = bottom_dock.is_open();
3082 let bottom_active_panel = bottom_dock
3083 .visible_panel()
3084 .and_then(|panel| Some(panel.persistent_name().to_string()));
3085 let bottom_dock_zoom = bottom_dock
3086 .visible_panel()
3087 .map(|panel| panel.is_zoomed(cx))
3088 .unwrap_or(false);
3089
3090 DockStructure {
3091 left: DockData {
3092 visible: left_visible,
3093 active_panel: left_active_panel,
3094 zoom: left_dock_zoom,
3095 },
3096 right: DockData {
3097 visible: right_visible,
3098 active_panel: right_active_panel,
3099 zoom: right_dock_zoom,
3100 },
3101 bottom: DockData {
3102 visible: bottom_visible,
3103 active_panel: bottom_active_panel,
3104 zoom: bottom_dock_zoom,
3105 },
3106 }
3107 }
3108
3109 if let Some(location) = self.location(cx) {
3110 // Load bearing special case:
3111 // - with_local_workspace() relies on this to not have other stuff open
3112 // when you open your log
3113 if !location.paths().is_empty() {
3114 let center_group = build_serialized_pane_group(&self.center.root, cx);
3115 let docks = build_serialized_docks(self, cx);
3116
3117 let serialized_workspace = SerializedWorkspace {
3118 id: self.database_id,
3119 location,
3120 center_group,
3121 bounds: Default::default(),
3122 display: Default::default(),
3123 docks,
3124 };
3125
3126 cx.spawn(|_, _| persistence::DB.save_workspace(serialized_workspace))
3127 .detach();
3128 }
3129 }
3130 }
3131
3132 pub(crate) fn load_workspace(
3133 serialized_workspace: SerializedWorkspace,
3134 paths_to_open: Vec<Option<ProjectPath>>,
3135 cx: &mut ViewContext<Workspace>,
3136 ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
3137 cx.spawn(|workspace, mut cx| async move {
3138 let project = workspace.update(&mut cx, |workspace, _| workspace.project().clone())?;
3139
3140 let mut center_group = None;
3141 let mut center_items = None;
3142
3143 // Traverse the splits tree and add to things
3144 if let Some((group, active_pane, items)) = serialized_workspace
3145 .center_group
3146 .deserialize(
3147 &project,
3148 serialized_workspace.id,
3149 workspace.clone(),
3150 &mut cx,
3151 )
3152 .await
3153 {
3154 center_items = Some(items);
3155 center_group = Some((group, active_pane))
3156 }
3157
3158 let mut items_by_project_path = cx.update(|_, cx| {
3159 center_items
3160 .unwrap_or_default()
3161 .into_iter()
3162 .filter_map(|item| {
3163 let item = item?;
3164 let project_path = item.project_path(cx)?;
3165 Some((project_path, item))
3166 })
3167 .collect::<HashMap<_, _>>()
3168 })?;
3169
3170 let opened_items = paths_to_open
3171 .into_iter()
3172 .map(|path_to_open| {
3173 path_to_open
3174 .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
3175 })
3176 .collect::<Vec<_>>();
3177
3178 // Remove old panes from workspace panes list
3179 workspace.update(&mut cx, |workspace, cx| {
3180 if let Some((center_group, active_pane)) = center_group {
3181 workspace.remove_panes(workspace.center.root.clone(), cx);
3182
3183 // Swap workspace center group
3184 workspace.center = PaneGroup::with_root(center_group);
3185 workspace.last_active_center_pane = active_pane.as_ref().map(|p| p.downgrade());
3186 if let Some(active_pane) = active_pane {
3187 workspace.active_pane = active_pane;
3188 cx.focus_self();
3189 } else {
3190 workspace.active_pane = workspace.center.first_pane().clone();
3191 }
3192 }
3193
3194 let docks = serialized_workspace.docks;
3195 workspace.left_dock.update(cx, |dock, cx| {
3196 dock.set_open(docks.left.visible, cx);
3197 if let Some(active_panel) = docks.left.active_panel {
3198 if let Some(ix) = dock.panel_index_for_persistent_name(&active_panel, cx) {
3199 dock.activate_panel(ix, cx);
3200 }
3201 }
3202 dock.active_panel()
3203 .map(|panel| panel.set_zoomed(docks.left.zoom, cx));
3204 if docks.left.visible && docks.left.zoom {
3205 cx.focus_self()
3206 }
3207 });
3208 // TODO: I think the bug is that setting zoom or active undoes the bottom zoom or something
3209 workspace.right_dock.update(cx, |dock, cx| {
3210 dock.set_open(docks.right.visible, cx);
3211 if let Some(active_panel) = docks.right.active_panel {
3212 if let Some(ix) = dock.panel_index_for_persistent_name(&active_panel, cx) {
3213 dock.activate_panel(ix, cx);
3214 }
3215 }
3216 dock.active_panel()
3217 .map(|panel| panel.set_zoomed(docks.right.zoom, cx));
3218
3219 if docks.right.visible && docks.right.zoom {
3220 cx.focus_self()
3221 }
3222 });
3223 workspace.bottom_dock.update(cx, |dock, cx| {
3224 dock.set_open(docks.bottom.visible, cx);
3225 if let Some(active_panel) = docks.bottom.active_panel {
3226 if let Some(ix) = dock.panel_index_for_persistent_name(&active_panel, cx) {
3227 dock.activate_panel(ix, cx);
3228 }
3229 }
3230
3231 dock.active_panel()
3232 .map(|panel| panel.set_zoomed(docks.bottom.zoom, cx));
3233
3234 if docks.bottom.visible && docks.bottom.zoom {
3235 cx.focus_self()
3236 }
3237 });
3238
3239 cx.notify();
3240 })?;
3241
3242 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
3243 workspace.update(&mut cx, |workspace, cx| workspace.serialize_workspace(cx))?;
3244
3245 Ok(opened_items)
3246 })
3247 }
3248
3249 fn actions(&self, div: Div, cx: &mut ViewContext<Self>) -> Div {
3250 self.add_workspace_actions_listeners(div, cx)
3251 .on_action(cx.listener(Self::close_inactive_items_and_panes))
3252 .on_action(cx.listener(Self::close_all_items_and_panes))
3253 .on_action(cx.listener(Self::save_all))
3254 .on_action(cx.listener(Self::add_folder_to_project))
3255 .on_action(cx.listener(Self::follow_next_collaborator))
3256 .on_action(cx.listener(|workspace, _: &Unfollow, cx| {
3257 let pane = workspace.active_pane().clone();
3258 workspace.unfollow(&pane, cx);
3259 }))
3260 .on_action(cx.listener(|workspace, action: &Save, cx| {
3261 workspace
3262 .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx)
3263 .detach_and_log_err(cx);
3264 }))
3265 .on_action(cx.listener(|workspace, _: &SaveAs, cx| {
3266 workspace
3267 .save_active_item(SaveIntent::SaveAs, cx)
3268 .detach_and_log_err(cx);
3269 }))
3270 .on_action(cx.listener(|workspace, _: &ActivatePreviousPane, cx| {
3271 workspace.activate_previous_pane(cx)
3272 }))
3273 .on_action(
3274 cx.listener(|workspace, _: &ActivateNextPane, cx| workspace.activate_next_pane(cx)),
3275 )
3276 .on_action(
3277 cx.listener(|workspace, action: &ActivatePaneInDirection, cx| {
3278 workspace.activate_pane_in_direction(action.0, cx)
3279 }),
3280 )
3281 .on_action(cx.listener(|workspace, action: &SwapPaneInDirection, cx| {
3282 workspace.swap_pane_in_direction(action.0, cx)
3283 }))
3284 .on_action(cx.listener(|this, _: &ToggleLeftDock, cx| {
3285 this.toggle_dock(DockPosition::Left, cx);
3286 }))
3287 .on_action(
3288 cx.listener(|workspace: &mut Workspace, _: &ToggleRightDock, cx| {
3289 workspace.toggle_dock(DockPosition::Right, cx);
3290 }),
3291 )
3292 .on_action(
3293 cx.listener(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| {
3294 workspace.toggle_dock(DockPosition::Bottom, cx);
3295 }),
3296 )
3297 .on_action(
3298 cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, cx| {
3299 workspace.close_all_docks(cx);
3300 }),
3301 )
3302 .on_action(cx.listener(Workspace::open))
3303 .on_action(cx.listener(Workspace::close_window))
3304 .on_action(cx.listener(Workspace::activate_pane_at_index))
3305 .on_action(
3306 cx.listener(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
3307 workspace.reopen_closed_item(cx).detach();
3308 }),
3309 )
3310 }
3311
3312 #[cfg(any(test, feature = "test-support"))]
3313 pub fn test_new(project: Model<Project>, cx: &mut ViewContext<Self>) -> Self {
3314 use node_runtime::FakeNodeRuntime;
3315
3316 let client = project.read(cx).client();
3317 let user_store = project.read(cx).user_store();
3318
3319 let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx));
3320 cx.activate_window();
3321 let app_state = Arc::new(AppState {
3322 languages: project.read(cx).languages().clone(),
3323 workspace_store,
3324 client,
3325 user_store,
3326 fs: project.read(cx).fs().clone(),
3327 build_window_options: |_, _, _| Default::default(),
3328 node_runtime: FakeNodeRuntime::new(),
3329 });
3330 let workspace = Self::new(0, project, app_state, cx);
3331 workspace.active_pane.update(cx, |pane, cx| pane.focus(cx));
3332 workspace
3333 }
3334
3335 pub fn register_action<A: Action>(
3336 &mut self,
3337 callback: impl Fn(&mut Self, &A, &mut ViewContext<Self>) + 'static,
3338 ) -> &mut Self {
3339 let callback = Arc::new(callback);
3340
3341 self.workspace_actions.push(Box::new(move |div, cx| {
3342 let callback = callback.clone();
3343 div.on_action(
3344 cx.listener(move |workspace, event, cx| (callback.clone())(workspace, event, cx)),
3345 )
3346 }));
3347 self
3348 }
3349
3350 fn add_workspace_actions_listeners(&self, div: Div, cx: &mut ViewContext<Self>) -> Div {
3351 let mut div = div
3352 .on_action(cx.listener(Self::close_inactive_items_and_panes))
3353 .on_action(cx.listener(Self::close_all_items_and_panes))
3354 .on_action(cx.listener(Self::add_folder_to_project))
3355 .on_action(cx.listener(Self::save_all))
3356 .on_action(cx.listener(Self::open));
3357 for action in self.workspace_actions.iter() {
3358 div = (action)(div, cx)
3359 }
3360 div
3361 }
3362
3363 pub fn has_active_modal(&self, cx: &WindowContext<'_>) -> bool {
3364 self.modal_layer.read(cx).has_active_modal()
3365 }
3366
3367 pub fn active_modal<V: ManagedView + 'static>(
3368 &mut self,
3369 cx: &ViewContext<Self>,
3370 ) -> Option<View<V>> {
3371 self.modal_layer.read(cx).active_modal()
3372 }
3373
3374 pub fn toggle_modal<V: ModalView, B>(&mut self, cx: &mut ViewContext<Self>, build: B)
3375 where
3376 B: FnOnce(&mut ViewContext<V>) -> V,
3377 {
3378 self.modal_layer
3379 .update(cx, |modal_layer, cx| modal_layer.toggle_modal(cx, build))
3380 }
3381}
3382
3383fn window_bounds_env_override(cx: &AsyncAppContext) -> Option<WindowBounds> {
3384 let display_origin = cx
3385 .update(|cx| Some(cx.displays().first()?.bounds().origin))
3386 .ok()??;
3387 ZED_WINDOW_POSITION
3388 .zip(*ZED_WINDOW_SIZE)
3389 .map(|(position, size)| {
3390 WindowBounds::Fixed(Bounds {
3391 origin: display_origin + position,
3392 size,
3393 })
3394 })
3395}
3396
3397fn open_items(
3398 serialized_workspace: Option<SerializedWorkspace>,
3399 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
3400 app_state: Arc<AppState>,
3401 cx: &mut ViewContext<Workspace>,
3402) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> {
3403 let restored_items = serialized_workspace.map(|serialized_workspace| {
3404 Workspace::load_workspace(
3405 serialized_workspace,
3406 project_paths_to_open
3407 .iter()
3408 .map(|(_, project_path)| project_path)
3409 .cloned()
3410 .collect(),
3411 cx,
3412 )
3413 });
3414
3415 cx.spawn(|workspace, mut cx| async move {
3416 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
3417
3418 if let Some(restored_items) = restored_items {
3419 let restored_items = restored_items.await?;
3420
3421 let restored_project_paths = restored_items
3422 .iter()
3423 .filter_map(|item| {
3424 cx.update(|_, cx| item.as_ref()?.project_path(cx))
3425 .ok()
3426 .flatten()
3427 })
3428 .collect::<HashSet<_>>();
3429
3430 for restored_item in restored_items {
3431 opened_items.push(restored_item.map(Ok));
3432 }
3433
3434 project_paths_to_open
3435 .iter_mut()
3436 .for_each(|(_, project_path)| {
3437 if let Some(project_path_to_open) = project_path {
3438 if restored_project_paths.contains(project_path_to_open) {
3439 *project_path = None;
3440 }
3441 }
3442 });
3443 } else {
3444 for _ in 0..project_paths_to_open.len() {
3445 opened_items.push(None);
3446 }
3447 }
3448 assert!(opened_items.len() == project_paths_to_open.len());
3449
3450 let tasks =
3451 project_paths_to_open
3452 .into_iter()
3453 .enumerate()
3454 .map(|(i, (abs_path, project_path))| {
3455 let workspace = workspace.clone();
3456 cx.spawn(|mut cx| {
3457 let fs = app_state.fs.clone();
3458 async move {
3459 let file_project_path = project_path?;
3460 if fs.is_file(&abs_path).await {
3461 Some((
3462 i,
3463 workspace
3464 .update(&mut cx, |workspace, cx| {
3465 workspace.open_path(file_project_path, None, true, cx)
3466 })
3467 .log_err()?
3468 .await,
3469 ))
3470 } else {
3471 None
3472 }
3473 }
3474 })
3475 });
3476
3477 let tasks = tasks.collect::<Vec<_>>();
3478
3479 let tasks = futures::future::join_all(tasks.into_iter());
3480 for maybe_opened_path in tasks.await.into_iter() {
3481 if let Some((i, path_open_result)) = maybe_opened_path {
3482 opened_items[i] = Some(path_open_result);
3483 }
3484 }
3485
3486 Ok(opened_items)
3487 })
3488}
3489
3490fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncAppContext) {
3491 const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
3492
3493 workspace
3494 .update(cx, |workspace, cx| {
3495 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
3496 workspace.show_notification_once(0, cx, |cx| {
3497 cx.new_view(|_| {
3498 MessageNotification::new("Failed to load the database file.")
3499 .with_click_message("Click to let us know about this error")
3500 .on_click(|cx| cx.open_url(REPORT_ISSUE_URL))
3501 })
3502 });
3503 }
3504 })
3505 .log_err();
3506}
3507
3508impl FocusableView for Workspace {
3509 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
3510 self.active_pane.focus_handle(cx)
3511 }
3512}
3513
3514#[derive(Clone, Render)]
3515struct DraggedDock(DockPosition);
3516
3517impl Render for Workspace {
3518 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3519 let mut context = KeyContext::default();
3520 context.add("Workspace");
3521
3522 let (ui_font, ui_font_size) = {
3523 let theme_settings = ThemeSettings::get_global(cx);
3524 (
3525 theme_settings.ui_font.family.clone(),
3526 theme_settings.ui_font_size.clone(),
3527 )
3528 };
3529
3530 let theme = cx.theme().clone();
3531 let colors = theme.colors();
3532 cx.set_rem_size(ui_font_size);
3533
3534 self.actions(div(), cx)
3535 .key_context(context)
3536 .relative()
3537 .size_full()
3538 .flex()
3539 .flex_col()
3540 .font(ui_font)
3541 .gap_0()
3542 .justify_start()
3543 .items_start()
3544 .text_color(colors.text)
3545 .bg(colors.background)
3546 .border()
3547 .border_color(colors.border)
3548 .children(self.titlebar_item.clone())
3549 .child(
3550 div()
3551 .id("workspace")
3552 .relative()
3553 .flex_1()
3554 .w_full()
3555 .flex()
3556 .flex_col()
3557 .overflow_hidden()
3558 .border_t()
3559 .border_b()
3560 .border_color(colors.border)
3561 .child(
3562 canvas(cx.listener(|workspace, bounds, _| {
3563 workspace.bounds = *bounds;
3564 }))
3565 .absolute()
3566 .size_full(),
3567 )
3568 .on_drag_move(
3569 cx.listener(|workspace, e: &DragMoveEvent<DraggedDock>, cx| {
3570 match e.drag(cx).0 {
3571 DockPosition::Left => {
3572 let size = workspace.bounds.left() + e.event.position.x;
3573 workspace.left_dock.update(cx, |left_dock, cx| {
3574 left_dock.resize_active_panel(Some(size), cx);
3575 });
3576 }
3577 DockPosition::Right => {
3578 let size = workspace.bounds.right() - e.event.position.x;
3579 workspace.right_dock.update(cx, |right_dock, cx| {
3580 right_dock.resize_active_panel(Some(size), cx);
3581 });
3582 }
3583 DockPosition::Bottom => {
3584 let size = workspace.bounds.bottom() - e.event.position.y;
3585 workspace.bottom_dock.update(cx, |bottom_dock, cx| {
3586 bottom_dock.resize_active_panel(Some(size), cx);
3587 });
3588 }
3589 }
3590 }),
3591 )
3592 .child(self.modal_layer.clone())
3593 .child(
3594 div()
3595 .flex()
3596 .flex_row()
3597 .h_full()
3598 // Left Dock
3599 .children(self.zoomed_position.ne(&Some(DockPosition::Left)).then(
3600 || {
3601 div()
3602 .flex()
3603 .flex_none()
3604 .overflow_hidden()
3605 .child(self.left_dock.clone())
3606 },
3607 ))
3608 // Panes
3609 .child(
3610 div()
3611 .flex()
3612 .flex_col()
3613 .flex_1()
3614 .overflow_hidden()
3615 .child(self.center.render(
3616 &self.project,
3617 &self.follower_states,
3618 self.active_call(),
3619 &self.active_pane,
3620 self.zoomed.as_ref(),
3621 &self.app_state,
3622 cx,
3623 ))
3624 .children(
3625 self.zoomed_position
3626 .ne(&Some(DockPosition::Bottom))
3627 .then(|| self.bottom_dock.clone()),
3628 ),
3629 )
3630 // Right Dock
3631 .children(self.zoomed_position.ne(&Some(DockPosition::Right)).then(
3632 || {
3633 div()
3634 .flex()
3635 .flex_none()
3636 .overflow_hidden()
3637 .child(self.right_dock.clone())
3638 },
3639 )),
3640 )
3641 .children(self.render_notifications(cx))
3642 .children(self.zoomed.as_ref().and_then(|view| {
3643 let zoomed_view = view.upgrade()?;
3644 let div = div()
3645 .z_index(1)
3646 .absolute()
3647 .overflow_hidden()
3648 .border_color(colors.border)
3649 .bg(colors.background)
3650 .child(zoomed_view)
3651 .inset_0()
3652 .shadow_lg();
3653
3654 Some(match self.zoomed_position {
3655 Some(DockPosition::Left) => div.right_2().border_r(),
3656 Some(DockPosition::Right) => div.left_2().border_l(),
3657 Some(DockPosition::Bottom) => div.top_2().border_t(),
3658 None => div.top_2().bottom_2().left_2().right_2().border(),
3659 })
3660 })),
3661 )
3662 .child(self.status_bar.clone())
3663 .children(if self.project.read(cx).is_disconnected() {
3664 Some(DisconnectedOverlay)
3665 } else {
3666 None
3667 })
3668 }
3669}
3670
3671impl WorkspaceStore {
3672 pub fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
3673 Self {
3674 workspaces: Default::default(),
3675 followers: Default::default(),
3676 _subscriptions: vec![
3677 client.add_request_handler(cx.weak_model(), Self::handle_follow),
3678 client.add_message_handler(cx.weak_model(), Self::handle_unfollow),
3679 client.add_message_handler(cx.weak_model(), Self::handle_update_followers),
3680 ],
3681 client,
3682 }
3683 }
3684
3685 pub fn update_followers(
3686 &self,
3687 project_id: Option<u64>,
3688 update: proto::update_followers::Variant,
3689 cx: &AppContext,
3690 ) -> Option<()> {
3691 if !cx.has_global::<Model<ActiveCall>>() {
3692 return None;
3693 }
3694
3695 let room_id = ActiveCall::global(cx).read(cx).room()?.read(cx).id();
3696 let follower_ids: Vec<_> = self
3697 .followers
3698 .iter()
3699 .filter_map(|follower| {
3700 if follower.project_id == project_id || project_id.is_none() {
3701 Some(follower.peer_id.into())
3702 } else {
3703 None
3704 }
3705 })
3706 .collect();
3707 if follower_ids.is_empty() {
3708 return None;
3709 }
3710 self.client
3711 .send(proto::UpdateFollowers {
3712 room_id,
3713 project_id,
3714 follower_ids,
3715 variant: Some(update),
3716 })
3717 .log_err()
3718 }
3719
3720 pub async fn handle_follow(
3721 this: Model<Self>,
3722 envelope: TypedEnvelope<proto::Follow>,
3723 _: Arc<Client>,
3724 mut cx: AsyncAppContext,
3725 ) -> Result<proto::FollowResponse> {
3726 this.update(&mut cx, |this, cx| {
3727 let follower = Follower {
3728 project_id: envelope.payload.project_id,
3729 peer_id: envelope.original_sender_id()?,
3730 };
3731 let active_project = ActiveCall::global(cx).read(cx).location().cloned();
3732
3733 let mut response = proto::FollowResponse::default();
3734 this.workspaces.retain(|workspace| {
3735 workspace
3736 .update(cx, |workspace, cx| {
3737 let handler_response = workspace.handle_follow(follower.project_id, cx);
3738 if response.views.is_empty() {
3739 response.views = handler_response.views;
3740 } else {
3741 response.views.extend_from_slice(&handler_response.views);
3742 }
3743
3744 if let Some(active_view_id) = handler_response.active_view_id.clone() {
3745 if response.active_view_id.is_none()
3746 || Some(workspace.project.downgrade()) == active_project
3747 {
3748 response.active_view_id = Some(active_view_id);
3749 }
3750 }
3751 })
3752 .is_ok()
3753 });
3754
3755 if let Err(ix) = this.followers.binary_search(&follower) {
3756 this.followers.insert(ix, follower);
3757 }
3758
3759 Ok(response)
3760 })?
3761 }
3762
3763 async fn handle_unfollow(
3764 model: Model<Self>,
3765 envelope: TypedEnvelope<proto::Unfollow>,
3766 _: Arc<Client>,
3767 mut cx: AsyncAppContext,
3768 ) -> Result<()> {
3769 model.update(&mut cx, |this, _| {
3770 let follower = Follower {
3771 project_id: envelope.payload.project_id,
3772 peer_id: envelope.original_sender_id()?,
3773 };
3774 if let Ok(ix) = this.followers.binary_search(&follower) {
3775 this.followers.remove(ix);
3776 }
3777 Ok(())
3778 })?
3779 }
3780
3781 async fn handle_update_followers(
3782 this: Model<Self>,
3783 envelope: TypedEnvelope<proto::UpdateFollowers>,
3784 _: Arc<Client>,
3785 mut cx: AsyncAppContext,
3786 ) -> Result<()> {
3787 let leader_id = envelope.original_sender_id()?;
3788 let update = envelope.payload;
3789
3790 this.update(&mut cx, |this, cx| {
3791 this.workspaces.retain(|workspace| {
3792 workspace
3793 .update(cx, |workspace, cx| {
3794 let project_id = workspace.project.read(cx).remote_id();
3795 if update.project_id != project_id && update.project_id.is_some() {
3796 return;
3797 }
3798 workspace.handle_update_followers(leader_id, update.clone(), cx);
3799 })
3800 .is_ok()
3801 });
3802 Ok(())
3803 })?
3804 }
3805}
3806
3807impl ViewId {
3808 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
3809 Ok(Self {
3810 creator: message
3811 .creator
3812 .ok_or_else(|| anyhow!("creator is missing"))?,
3813 id: message.id,
3814 })
3815 }
3816
3817 pub(crate) fn to_proto(&self) -> proto::ViewId {
3818 proto::ViewId {
3819 creator: Some(self.creator),
3820 id: self.id,
3821 }
3822 }
3823}
3824
3825pub trait WorkspaceHandle {
3826 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
3827}
3828
3829impl WorkspaceHandle for View<Workspace> {
3830 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
3831 self.read(cx)
3832 .worktrees(cx)
3833 .flat_map(|worktree| {
3834 let worktree_id = worktree.read(cx).id();
3835 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
3836 worktree_id,
3837 path: f.path.clone(),
3838 })
3839 })
3840 .collect::<Vec<_>>()
3841 }
3842}
3843
3844impl std::fmt::Debug for OpenPaths {
3845 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3846 f.debug_struct("OpenPaths")
3847 .field("paths", &self.paths)
3848 .finish()
3849 }
3850}
3851
3852pub fn activate_workspace_for_project(
3853 cx: &mut AppContext,
3854 predicate: impl Fn(&Project, &AppContext) -> bool + Send + 'static,
3855) -> Option<WindowHandle<Workspace>> {
3856 for window in cx.windows() {
3857 let Some(workspace) = window.downcast::<Workspace>() else {
3858 continue;
3859 };
3860
3861 let predicate = workspace
3862 .update(cx, |workspace, cx| {
3863 let project = workspace.project.read(cx);
3864 if predicate(project, cx) {
3865 cx.activate_window();
3866 true
3867 } else {
3868 false
3869 }
3870 })
3871 .log_err()
3872 .unwrap_or(false);
3873
3874 if predicate {
3875 return Some(workspace);
3876 }
3877 }
3878
3879 None
3880}
3881
3882pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
3883 DB.last_workspace().await.log_err().flatten()
3884}
3885
3886async fn join_channel_internal(
3887 channel_id: u64,
3888 app_state: &Arc<AppState>,
3889 requesting_window: Option<WindowHandle<Workspace>>,
3890 active_call: &Model<ActiveCall>,
3891 cx: &mut AsyncAppContext,
3892) -> Result<bool> {
3893 let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
3894 let Some(room) = active_call.room().map(|room| room.read(cx)) else {
3895 return (false, None);
3896 };
3897
3898 let already_in_channel = room.channel_id() == Some(channel_id);
3899 let should_prompt = room.is_sharing_project()
3900 && room.remote_participants().len() > 0
3901 && !already_in_channel;
3902 let open_room = if already_in_channel {
3903 active_call.room().cloned()
3904 } else {
3905 None
3906 };
3907 (should_prompt, open_room)
3908 })?;
3909
3910 if let Some(room) = open_room {
3911 let task = room.update(cx, |room, cx| {
3912 if let Some((project, host)) = room.most_active_project(cx) {
3913 return Some(join_remote_project(project, host, app_state.clone(), cx));
3914 }
3915
3916 None
3917 })?;
3918 if let Some(task) = task {
3919 task.await?;
3920 }
3921 return anyhow::Ok(true);
3922 }
3923
3924 if should_prompt {
3925 if let Some(workspace) = requesting_window {
3926 let answer = workspace.update(cx, |_, cx| {
3927 cx.prompt(
3928 PromptLevel::Warning,
3929 "Leaving this call will unshare your current project.\nDo you want to switch channels?",
3930 &["Yes, Join Channel", "Cancel"],
3931 )
3932 })?.await;
3933
3934 if answer == Ok(1) {
3935 return Ok(false);
3936 }
3937 } else {
3938 return Ok(false); // unreachable!() hopefully
3939 }
3940 }
3941
3942 let client = cx.update(|cx| active_call.read(cx).client())?;
3943
3944 let mut client_status = client.status();
3945
3946 // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
3947 'outer: loop {
3948 let Some(status) = client_status.recv().await else {
3949 return Err(anyhow!("error connecting"));
3950 };
3951
3952 match status {
3953 Status::Connecting
3954 | Status::Authenticating
3955 | Status::Reconnecting
3956 | Status::Reauthenticating => continue,
3957 Status::Connected { .. } => break 'outer,
3958 Status::SignedOut => return Err(anyhow!("not signed in")),
3959 Status::UpgradeRequired => return Err(anyhow!("zed is out of date")),
3960 Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
3961 return Err(anyhow!("zed is offline"))
3962 }
3963 }
3964 }
3965
3966 let room = active_call
3967 .update(cx, |active_call, cx| {
3968 active_call.join_channel(channel_id, cx)
3969 })?
3970 .await?;
3971
3972 let Some(room) = room else {
3973 return anyhow::Ok(true);
3974 };
3975
3976 room.update(cx, |room, _| room.room_update_completed())?
3977 .await;
3978
3979 let task = room.update(cx, |room, cx| {
3980 if let Some((project, host)) = room.most_active_project(cx) {
3981 return Some(join_remote_project(project, host, app_state.clone(), cx));
3982 }
3983
3984 None
3985 })?;
3986 if let Some(task) = task {
3987 task.await?;
3988 return anyhow::Ok(true);
3989 }
3990 anyhow::Ok(false)
3991}
3992
3993pub fn join_channel(
3994 channel_id: u64,
3995 app_state: Arc<AppState>,
3996 requesting_window: Option<WindowHandle<Workspace>>,
3997 cx: &mut AppContext,
3998) -> Task<Result<()>> {
3999 let active_call = ActiveCall::global(cx);
4000 cx.spawn(|mut cx| async move {
4001 let result = join_channel_internal(
4002 channel_id,
4003 &app_state,
4004 requesting_window,
4005 &active_call,
4006 &mut cx,
4007 )
4008 .await;
4009
4010 // join channel succeeded, and opened a window
4011 if matches!(result, Ok(true)) {
4012 return anyhow::Ok(());
4013 }
4014
4015 // find an existing workspace to focus and show call controls
4016 let mut active_window =
4017 requesting_window.or_else(|| activate_any_workspace_window(&mut cx));
4018 if active_window.is_none() {
4019 // no open workspaces, make one to show the error in (blergh)
4020 let (window_handle, _) = cx
4021 .update(|cx| {
4022 Workspace::new_local(vec![], app_state.clone(), requesting_window, cx)
4023 })?
4024 .await?;
4025
4026 active_window = Some(window_handle);
4027 }
4028
4029 if let Err(err) = result {
4030 log::error!("failed to join channel: {}", err);
4031 if let Some(active_window) = active_window {
4032 active_window
4033 .update(&mut cx, |_, cx| {
4034 cx.prompt(
4035 PromptLevel::Critical,
4036 &format!("Failed to join channel: {}", err),
4037 &["Ok"],
4038 )
4039 })?
4040 .await
4041 .ok();
4042 }
4043 }
4044
4045 // return ok, we showed the error to the user.
4046 return anyhow::Ok(());
4047 })
4048}
4049
4050pub async fn get_any_active_workspace(
4051 app_state: Arc<AppState>,
4052 mut cx: AsyncAppContext,
4053) -> anyhow::Result<WindowHandle<Workspace>> {
4054 // find an existing workspace to focus and show call controls
4055 let active_window = activate_any_workspace_window(&mut cx);
4056 if active_window.is_none() {
4057 cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, cx))?
4058 .await?;
4059 }
4060 activate_any_workspace_window(&mut cx).context("could not open zed")
4061}
4062
4063fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<WindowHandle<Workspace>> {
4064 cx.update(|cx| {
4065 for window in cx.windows() {
4066 if let Some(workspace_window) = window.downcast::<Workspace>() {
4067 workspace_window
4068 .update(cx, |_, cx| cx.activate_window())
4069 .ok();
4070 return Some(workspace_window);
4071 }
4072 }
4073 None
4074 })
4075 .ok()
4076 .flatten()
4077}
4078
4079#[allow(clippy::type_complexity)]
4080pub fn open_paths(
4081 abs_paths: &[PathBuf],
4082 app_state: &Arc<AppState>,
4083 requesting_window: Option<WindowHandle<Workspace>>,
4084 cx: &mut AppContext,
4085) -> Task<
4086 anyhow::Result<(
4087 WindowHandle<Workspace>,
4088 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
4089 )>,
4090> {
4091 let app_state = app_state.clone();
4092 let abs_paths = abs_paths.to_vec();
4093 // Open paths in existing workspace if possible
4094 let existing = activate_workspace_for_project(cx, {
4095 let abs_paths = abs_paths.clone();
4096 move |project, cx| project.contains_paths(&abs_paths, cx)
4097 });
4098 cx.spawn(move |mut cx| async move {
4099 if let Some(existing) = existing {
4100 Ok((
4101 existing.clone(),
4102 existing
4103 .update(&mut cx, |workspace, cx| {
4104 workspace.open_paths(abs_paths, OpenVisible::All, None, cx)
4105 })?
4106 .await,
4107 ))
4108 } else {
4109 cx.update(move |cx| {
4110 Workspace::new_local(abs_paths, app_state.clone(), requesting_window, cx)
4111 })?
4112 .await
4113 }
4114 })
4115}
4116
4117pub fn open_new(
4118 app_state: &Arc<AppState>,
4119 cx: &mut AppContext,
4120 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static + Send,
4121) -> Task<()> {
4122 let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx);
4123 cx.spawn(|mut cx| async move {
4124 if let Some((workspace, opened_paths)) = task.await.log_err() {
4125 workspace
4126 .update(&mut cx, |workspace, cx| {
4127 if opened_paths.is_empty() {
4128 init(workspace, cx)
4129 }
4130 })
4131 .log_err();
4132 }
4133 })
4134}
4135
4136pub fn create_and_open_local_file(
4137 path: &'static Path,
4138 cx: &mut ViewContext<Workspace>,
4139 default_content: impl 'static + Send + FnOnce() -> Rope,
4140) -> Task<Result<Box<dyn ItemHandle>>> {
4141 cx.spawn(|workspace, mut cx| async move {
4142 let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
4143 if !fs.is_file(path).await {
4144 fs.create_file(path, Default::default()).await?;
4145 fs.save(path, &default_content(), Default::default())
4146 .await?;
4147 }
4148
4149 let mut items = workspace
4150 .update(&mut cx, |workspace, cx| {
4151 workspace.with_local_workspace(cx, |workspace, cx| {
4152 workspace.open_paths(vec![path.to_path_buf()], OpenVisible::None, None, cx)
4153 })
4154 })?
4155 .await?
4156 .await;
4157
4158 let item = items.pop().flatten();
4159 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
4160 })
4161}
4162
4163pub fn join_remote_project(
4164 project_id: u64,
4165 follow_user_id: u64,
4166 app_state: Arc<AppState>,
4167 cx: &mut AppContext,
4168) -> Task<Result<()>> {
4169 let windows = cx.windows();
4170 cx.spawn(|mut cx| async move {
4171 let existing_workspace = windows.into_iter().find_map(|window| {
4172 window.downcast::<Workspace>().and_then(|window| {
4173 window
4174 .update(&mut cx, |workspace, cx| {
4175 if workspace.project().read(cx).remote_id() == Some(project_id) {
4176 Some(window)
4177 } else {
4178 None
4179 }
4180 })
4181 .unwrap_or(None)
4182 })
4183 });
4184
4185 let workspace = if let Some(existing_workspace) = existing_workspace {
4186 existing_workspace
4187 } else {
4188 let active_call = cx.update(|cx| ActiveCall::global(cx))?;
4189 let room = active_call
4190 .read_with(&cx, |call, _| call.room().cloned())?
4191 .ok_or_else(|| anyhow!("not in a call"))?;
4192 let project = room
4193 .update(&mut cx, |room, cx| {
4194 room.join_project(
4195 project_id,
4196 app_state.languages.clone(),
4197 app_state.fs.clone(),
4198 cx,
4199 )
4200 })?
4201 .await?;
4202
4203 let window_bounds_override = window_bounds_env_override(&cx);
4204 cx.update(|cx| {
4205 let options = (app_state.build_window_options)(window_bounds_override, None, cx);
4206 cx.open_window(options, |cx| {
4207 cx.new_view(|cx| Workspace::new(0, project, app_state.clone(), cx))
4208 })
4209 })?
4210 };
4211
4212 workspace.update(&mut cx, |workspace, cx| {
4213 cx.activate(true);
4214 cx.activate_window();
4215
4216 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
4217 let follow_peer_id = room
4218 .read(cx)
4219 .remote_participants()
4220 .iter()
4221 .find(|(_, participant)| participant.user.id == follow_user_id)
4222 .map(|(_, p)| p.peer_id)
4223 .or_else(|| {
4224 // If we couldn't follow the given user, follow the host instead.
4225 let collaborator = workspace
4226 .project()
4227 .read(cx)
4228 .collaborators()
4229 .values()
4230 .find(|collaborator| collaborator.replica_id == 0)?;
4231 Some(collaborator.peer_id)
4232 });
4233
4234 if let Some(follow_peer_id) = follow_peer_id {
4235 workspace.follow(follow_peer_id, cx);
4236 }
4237 }
4238 })?;
4239
4240 anyhow::Ok(())
4241 })
4242}
4243
4244pub fn restart(_: &Restart, cx: &mut AppContext) {
4245 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
4246 let mut workspace_windows = cx
4247 .windows()
4248 .into_iter()
4249 .filter_map(|window| window.downcast::<Workspace>())
4250 .collect::<Vec<_>>();
4251
4252 // If multiple windows have unsaved changes, and need a save prompt,
4253 // prompt in the active window before switching to a different window.
4254 workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false));
4255
4256 let mut prompt = None;
4257 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
4258 prompt = window
4259 .update(cx, |_, cx| {
4260 cx.prompt(
4261 PromptLevel::Info,
4262 "Are you sure you want to restart?",
4263 &["Restart", "Cancel"],
4264 )
4265 })
4266 .ok();
4267 }
4268
4269 cx.spawn(|mut cx| async move {
4270 if let Some(prompt) = prompt {
4271 let answer = prompt.await?;
4272 if answer != 0 {
4273 return Ok(());
4274 }
4275 }
4276
4277 // If the user cancels any save prompt, then keep the app open.
4278 for window in workspace_windows {
4279 if let Ok(should_close) = window.update(&mut cx, |workspace, cx| {
4280 workspace.prepare_to_close(true, cx)
4281 }) {
4282 if !should_close.await? {
4283 return Ok(());
4284 }
4285 }
4286 }
4287
4288 cx.update(|cx| cx.restart())
4289 })
4290 .detach_and_log_err(cx);
4291}
4292
4293fn parse_pixel_position_env_var(value: &str) -> Option<Point<GlobalPixels>> {
4294 let mut parts = value.split(',');
4295 let x: usize = parts.next()?.parse().ok()?;
4296 let y: usize = parts.next()?.parse().ok()?;
4297 Some(point((x as f64).into(), (y as f64).into()))
4298}
4299
4300fn parse_pixel_size_env_var(value: &str) -> Option<Size<GlobalPixels>> {
4301 let mut parts = value.split(',');
4302 let width: usize = parts.next()?.parse().ok()?;
4303 let height: usize = parts.next()?.parse().ok()?;
4304 Some(size((width as f64).into(), (height as f64).into()))
4305}
4306
4307struct DisconnectedOverlay;
4308
4309impl Element for DisconnectedOverlay {
4310 type State = AnyElement;
4311
4312 fn request_layout(
4313 &mut self,
4314 _: Option<Self::State>,
4315 cx: &mut WindowContext,
4316 ) -> (LayoutId, Self::State) {
4317 let mut background = cx.theme().colors().elevated_surface_background;
4318 background.fade_out(0.2);
4319 let mut overlay = div()
4320 .bg(background)
4321 .absolute()
4322 .left_0()
4323 .top_0()
4324 .size_full()
4325 .flex()
4326 .items_center()
4327 .justify_center()
4328 .capture_any_mouse_down(|_, cx| cx.stop_propagation())
4329 .capture_any_mouse_up(|_, cx| cx.stop_propagation())
4330 .child(Label::new(
4331 "Your connection to the remote project has been lost.",
4332 ))
4333 .into_any();
4334 (overlay.request_layout(cx), overlay)
4335 }
4336
4337 fn paint(&mut self, bounds: Bounds<Pixels>, overlay: &mut Self::State, cx: &mut WindowContext) {
4338 cx.with_z_index(u8::MAX, |cx| {
4339 cx.add_opaque_layer(bounds);
4340 overlay.paint(cx);
4341 })
4342 }
4343}
4344
4345impl IntoElement for DisconnectedOverlay {
4346 type Element = Self;
4347
4348 fn element_id(&self) -> Option<ui::prelude::ElementId> {
4349 None
4350 }
4351
4352 fn into_element(self) -> Self::Element {
4353 self
4354 }
4355}
4356
4357#[cfg(test)]
4358mod tests {
4359 use std::{cell::RefCell, rc::Rc};
4360
4361 use super::*;
4362 use crate::{
4363 dock::{test::TestPanel, PanelEvent},
4364 item::{
4365 test::{TestItem, TestProjectItem},
4366 ItemEvent,
4367 },
4368 };
4369 use fs::FakeFs;
4370 use gpui::{px, DismissEvent, TestAppContext, VisualTestContext};
4371 use project::{Project, ProjectEntryId};
4372 use serde_json::json;
4373 use settings::SettingsStore;
4374
4375 #[gpui::test]
4376 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
4377 init_test(cx);
4378
4379 let fs = FakeFs::new(cx.executor());
4380 let project = Project::test(fs, [], cx).await;
4381 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4382
4383 // Adding an item with no ambiguity renders the tab without detail.
4384 let item1 = cx.new_view(|cx| {
4385 let mut item = TestItem::new(cx);
4386 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
4387 item
4388 });
4389 workspace.update(cx, |workspace, cx| {
4390 workspace.add_item(Box::new(item1.clone()), cx);
4391 });
4392 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
4393
4394 // Adding an item that creates ambiguity increases the level of detail on
4395 // both tabs.
4396 let item2 = cx.new_view(|cx| {
4397 let mut item = TestItem::new(cx);
4398 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
4399 item
4400 });
4401 workspace.update(cx, |workspace, cx| {
4402 workspace.add_item(Box::new(item2.clone()), cx);
4403 });
4404 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4405 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4406
4407 // Adding an item that creates ambiguity increases the level of detail only
4408 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
4409 // we stop at the highest detail available.
4410 let item3 = cx.new_view(|cx| {
4411 let mut item = TestItem::new(cx);
4412 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
4413 item
4414 });
4415 workspace.update(cx, |workspace, cx| {
4416 workspace.add_item(Box::new(item3.clone()), cx);
4417 });
4418 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4419 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
4420 item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
4421 }
4422
4423 #[gpui::test]
4424 async fn test_tracking_active_path(cx: &mut TestAppContext) {
4425 init_test(cx);
4426
4427 let fs = FakeFs::new(cx.executor());
4428 fs.insert_tree(
4429 "/root1",
4430 json!({
4431 "one.txt": "",
4432 "two.txt": "",
4433 }),
4434 )
4435 .await;
4436 fs.insert_tree(
4437 "/root2",
4438 json!({
4439 "three.txt": "",
4440 }),
4441 )
4442 .await;
4443
4444 let project = Project::test(fs, ["root1".as_ref()], cx).await;
4445 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4446 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4447 let worktree_id = project.update(cx, |project, cx| {
4448 project.worktrees().next().unwrap().read(cx).id()
4449 });
4450
4451 let item1 = cx.new_view(|cx| {
4452 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
4453 });
4454 let item2 = cx.new_view(|cx| {
4455 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
4456 });
4457
4458 // Add an item to an empty pane
4459 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
4460 project.update(cx, |project, cx| {
4461 assert_eq!(
4462 project.active_entry(),
4463 project
4464 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
4465 .map(|e| e.id)
4466 );
4467 });
4468 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1"));
4469
4470 // Add a second item to a non-empty pane
4471 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
4472 assert_eq!(cx.window_title().as_deref(), Some("two.txt — root1"));
4473 project.update(cx, |project, cx| {
4474 assert_eq!(
4475 project.active_entry(),
4476 project
4477 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
4478 .map(|e| e.id)
4479 );
4480 });
4481
4482 // Close the active item
4483 pane.update(cx, |pane, cx| {
4484 pane.close_active_item(&Default::default(), cx).unwrap()
4485 })
4486 .await
4487 .unwrap();
4488 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1"));
4489 project.update(cx, |project, cx| {
4490 assert_eq!(
4491 project.active_entry(),
4492 project
4493 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
4494 .map(|e| e.id)
4495 );
4496 });
4497
4498 // Add a project folder
4499 project
4500 .update(cx, |project, cx| {
4501 project.find_or_create_local_worktree("/root2", true, cx)
4502 })
4503 .await
4504 .unwrap();
4505 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1, root2"));
4506
4507 // Remove a project folder
4508 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
4509 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root2"));
4510 }
4511
4512 #[gpui::test]
4513 async fn test_close_window(cx: &mut TestAppContext) {
4514 init_test(cx);
4515
4516 let fs = FakeFs::new(cx.executor());
4517 fs.insert_tree("/root", json!({ "one": "" })).await;
4518
4519 let project = Project::test(fs, ["root".as_ref()], cx).await;
4520 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4521
4522 // When there are no dirty items, there's nothing to do.
4523 let item1 = cx.new_view(|cx| TestItem::new(cx));
4524 workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
4525 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4526 assert!(task.await.unwrap());
4527
4528 // When there are dirty untitled items, prompt to save each one. If the user
4529 // cancels any prompt, then abort.
4530 let item2 = cx.new_view(|cx| TestItem::new(cx).with_dirty(true));
4531 let item3 = cx.new_view(|cx| {
4532 TestItem::new(cx)
4533 .with_dirty(true)
4534 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4535 });
4536 workspace.update(cx, |w, cx| {
4537 w.add_item(Box::new(item2.clone()), cx);
4538 w.add_item(Box::new(item3.clone()), cx);
4539 });
4540 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4541 cx.executor().run_until_parked();
4542 cx.simulate_prompt_answer(2); // cancel save all
4543 cx.executor().run_until_parked();
4544 cx.simulate_prompt_answer(2); // cancel save all
4545 cx.executor().run_until_parked();
4546 assert!(!cx.has_pending_prompt());
4547 assert!(!task.await.unwrap());
4548 }
4549
4550 #[gpui::test]
4551 async fn test_close_pane_items(cx: &mut TestAppContext) {
4552 init_test(cx);
4553
4554 let fs = FakeFs::new(cx.executor());
4555
4556 let project = Project::test(fs, None, cx).await;
4557 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4558
4559 let item1 = cx.new_view(|cx| {
4560 TestItem::new(cx)
4561 .with_dirty(true)
4562 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4563 });
4564 let item2 = cx.new_view(|cx| {
4565 TestItem::new(cx)
4566 .with_dirty(true)
4567 .with_conflict(true)
4568 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
4569 });
4570 let item3 = cx.new_view(|cx| {
4571 TestItem::new(cx)
4572 .with_dirty(true)
4573 .with_conflict(true)
4574 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
4575 });
4576 let item4 = cx.new_view(|cx| {
4577 TestItem::new(cx)
4578 .with_dirty(true)
4579 .with_project_items(&[TestProjectItem::new_untitled(cx)])
4580 });
4581 let pane = workspace.update(cx, |workspace, cx| {
4582 workspace.add_item(Box::new(item1.clone()), cx);
4583 workspace.add_item(Box::new(item2.clone()), cx);
4584 workspace.add_item(Box::new(item3.clone()), cx);
4585 workspace.add_item(Box::new(item4.clone()), cx);
4586 workspace.active_pane().clone()
4587 });
4588
4589 let close_items = pane.update(cx, |pane, cx| {
4590 pane.activate_item(1, true, true, cx);
4591 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
4592 let item1_id = item1.item_id();
4593 let item3_id = item3.item_id();
4594 let item4_id = item4.item_id();
4595 pane.close_items(cx, SaveIntent::Close, move |id| {
4596 [item1_id, item3_id, item4_id].contains(&id)
4597 })
4598 });
4599 cx.executor().run_until_parked();
4600
4601 assert!(cx.has_pending_prompt());
4602 // Ignore "Save all" prompt
4603 cx.simulate_prompt_answer(2);
4604 cx.executor().run_until_parked();
4605 // There's a prompt to save item 1.
4606 pane.update(cx, |pane, _| {
4607 assert_eq!(pane.items_len(), 4);
4608 assert_eq!(pane.active_item().unwrap().item_id(), item1.item_id());
4609 });
4610 // Confirm saving item 1.
4611 cx.simulate_prompt_answer(0);
4612 cx.executor().run_until_parked();
4613
4614 // Item 1 is saved. There's a prompt to save item 3.
4615 pane.update(cx, |pane, cx| {
4616 assert_eq!(item1.read(cx).save_count, 1);
4617 assert_eq!(item1.read(cx).save_as_count, 0);
4618 assert_eq!(item1.read(cx).reload_count, 0);
4619 assert_eq!(pane.items_len(), 3);
4620 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
4621 });
4622 assert!(cx.has_pending_prompt());
4623
4624 // Cancel saving item 3.
4625 cx.simulate_prompt_answer(1);
4626 cx.executor().run_until_parked();
4627
4628 // Item 3 is reloaded. There's a prompt to save item 4.
4629 pane.update(cx, |pane, cx| {
4630 assert_eq!(item3.read(cx).save_count, 0);
4631 assert_eq!(item3.read(cx).save_as_count, 0);
4632 assert_eq!(item3.read(cx).reload_count, 1);
4633 assert_eq!(pane.items_len(), 2);
4634 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
4635 });
4636 assert!(cx.has_pending_prompt());
4637
4638 // Confirm saving item 4.
4639 cx.simulate_prompt_answer(0);
4640 cx.executor().run_until_parked();
4641
4642 // There's a prompt for a path for item 4.
4643 cx.simulate_new_path_selection(|_| Some(Default::default()));
4644 close_items.await.unwrap();
4645
4646 // The requested items are closed.
4647 pane.update(cx, |pane, cx| {
4648 assert_eq!(item4.read(cx).save_count, 0);
4649 assert_eq!(item4.read(cx).save_as_count, 1);
4650 assert_eq!(item4.read(cx).reload_count, 0);
4651 assert_eq!(pane.items_len(), 1);
4652 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
4653 });
4654 }
4655
4656 #[gpui::test]
4657 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
4658 init_test(cx);
4659
4660 let fs = FakeFs::new(cx.executor());
4661 let project = Project::test(fs, [], cx).await;
4662 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4663
4664 // Create several workspace items with single project entries, and two
4665 // workspace items with multiple project entries.
4666 let single_entry_items = (0..=4)
4667 .map(|project_entry_id| {
4668 cx.new_view(|cx| {
4669 TestItem::new(cx)
4670 .with_dirty(true)
4671 .with_project_items(&[TestProjectItem::new(
4672 project_entry_id,
4673 &format!("{project_entry_id}.txt"),
4674 cx,
4675 )])
4676 })
4677 })
4678 .collect::<Vec<_>>();
4679 let item_2_3 = cx.new_view(|cx| {
4680 TestItem::new(cx)
4681 .with_dirty(true)
4682 .with_singleton(false)
4683 .with_project_items(&[
4684 single_entry_items[2].read(cx).project_items[0].clone(),
4685 single_entry_items[3].read(cx).project_items[0].clone(),
4686 ])
4687 });
4688 let item_3_4 = cx.new_view(|cx| {
4689 TestItem::new(cx)
4690 .with_dirty(true)
4691 .with_singleton(false)
4692 .with_project_items(&[
4693 single_entry_items[3].read(cx).project_items[0].clone(),
4694 single_entry_items[4].read(cx).project_items[0].clone(),
4695 ])
4696 });
4697
4698 // Create two panes that contain the following project entries:
4699 // left pane:
4700 // multi-entry items: (2, 3)
4701 // single-entry items: 0, 1, 2, 3, 4
4702 // right pane:
4703 // single-entry items: 1
4704 // multi-entry items: (3, 4)
4705 let left_pane = workspace.update(cx, |workspace, cx| {
4706 let left_pane = workspace.active_pane().clone();
4707 workspace.add_item(Box::new(item_2_3.clone()), cx);
4708 for item in single_entry_items {
4709 workspace.add_item(Box::new(item), cx);
4710 }
4711 left_pane.update(cx, |pane, cx| {
4712 pane.activate_item(2, true, true, cx);
4713 });
4714
4715 let right_pane = workspace
4716 .split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
4717 .unwrap();
4718
4719 right_pane.update(cx, |pane, cx| {
4720 pane.add_item(Box::new(item_3_4.clone()), true, true, None, cx);
4721 });
4722
4723 left_pane
4724 });
4725
4726 cx.focus_view(&left_pane);
4727
4728 // When closing all of the items in the left pane, we should be prompted twice:
4729 // once for project entry 0, and once for project entry 2. Project entries 1,
4730 // 3, and 4 are all still open in the other paten. After those two
4731 // prompts, the task should complete.
4732
4733 let close = left_pane.update(cx, |pane, cx| {
4734 pane.close_all_items(&CloseAllItems::default(), cx).unwrap()
4735 });
4736 cx.executor().run_until_parked();
4737
4738 // Discard "Save all" prompt
4739 cx.simulate_prompt_answer(2);
4740
4741 cx.executor().run_until_parked();
4742 left_pane.update(cx, |pane, cx| {
4743 assert_eq!(
4744 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
4745 &[ProjectEntryId::from_proto(0)]
4746 );
4747 });
4748 cx.simulate_prompt_answer(0);
4749
4750 cx.executor().run_until_parked();
4751 left_pane.update(cx, |pane, cx| {
4752 assert_eq!(
4753 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
4754 &[ProjectEntryId::from_proto(2)]
4755 );
4756 });
4757 cx.simulate_prompt_answer(0);
4758
4759 cx.executor().run_until_parked();
4760 close.await.unwrap();
4761 left_pane.update(cx, |pane, _| {
4762 assert_eq!(pane.items_len(), 0);
4763 });
4764 }
4765
4766 #[gpui::test]
4767 async fn test_autosave(cx: &mut gpui::TestAppContext) {
4768 init_test(cx);
4769
4770 let fs = FakeFs::new(cx.executor());
4771 let project = Project::test(fs, [], cx).await;
4772 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4773 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4774
4775 let item = cx.new_view(|cx| {
4776 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4777 });
4778 let item_id = item.entity_id();
4779 workspace.update(cx, |workspace, cx| {
4780 workspace.add_item(Box::new(item.clone()), cx);
4781 });
4782
4783 // Autosave on window change.
4784 item.update(cx, |item, cx| {
4785 cx.update_global(|settings: &mut SettingsStore, cx| {
4786 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4787 settings.autosave = Some(AutosaveSetting::OnWindowChange);
4788 })
4789 });
4790 item.is_dirty = true;
4791 });
4792
4793 // Deactivating the window saves the file.
4794 cx.deactivate_window();
4795 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
4796
4797 // Autosave on focus change.
4798 item.update(cx, |item, cx| {
4799 cx.focus_self();
4800 cx.update_global(|settings: &mut SettingsStore, cx| {
4801 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4802 settings.autosave = Some(AutosaveSetting::OnFocusChange);
4803 })
4804 });
4805 item.is_dirty = true;
4806 });
4807
4808 // Blurring the item saves the file.
4809 item.update(cx, |_, cx| cx.blur());
4810 cx.executor().run_until_parked();
4811 item.update(cx, |item, _| assert_eq!(item.save_count, 2));
4812
4813 // Deactivating the window still saves the file.
4814 cx.update(|cx| cx.activate_window());
4815 item.update(cx, |item, cx| {
4816 cx.focus_self();
4817 item.is_dirty = true;
4818 });
4819 cx.deactivate_window();
4820
4821 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
4822
4823 // Autosave after delay.
4824 item.update(cx, |item, cx| {
4825 cx.update_global(|settings: &mut SettingsStore, cx| {
4826 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4827 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
4828 })
4829 });
4830 item.is_dirty = true;
4831 cx.emit(ItemEvent::Edit);
4832 });
4833
4834 // Delay hasn't fully expired, so the file is still dirty and unsaved.
4835 cx.executor().advance_clock(Duration::from_millis(250));
4836 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
4837
4838 // After delay expires, the file is saved.
4839 cx.executor().advance_clock(Duration::from_millis(250));
4840 item.update(cx, |item, _| assert_eq!(item.save_count, 4));
4841
4842 // Autosave on focus change, ensuring closing the tab counts as such.
4843 item.update(cx, |item, cx| {
4844 cx.update_global(|settings: &mut SettingsStore, cx| {
4845 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4846 settings.autosave = Some(AutosaveSetting::OnFocusChange);
4847 })
4848 });
4849 item.is_dirty = true;
4850 });
4851
4852 pane.update(cx, |pane, cx| {
4853 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
4854 })
4855 .await
4856 .unwrap();
4857 assert!(!cx.has_pending_prompt());
4858 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
4859
4860 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
4861 workspace.update(cx, |workspace, cx| {
4862 workspace.add_item(Box::new(item.clone()), cx);
4863 });
4864 item.update(cx, |item, cx| {
4865 item.project_items[0].update(cx, |item, _| {
4866 item.entry_id = None;
4867 });
4868 item.is_dirty = true;
4869 cx.blur();
4870 });
4871 cx.run_until_parked();
4872 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
4873
4874 // Ensure autosave is prevented for deleted files also when closing the buffer.
4875 let _close_items = pane.update(cx, |pane, cx| {
4876 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
4877 });
4878 cx.run_until_parked();
4879 assert!(cx.has_pending_prompt());
4880 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
4881 }
4882
4883 #[gpui::test]
4884 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
4885 init_test(cx);
4886
4887 let fs = FakeFs::new(cx.executor());
4888
4889 let project = Project::test(fs, [], cx).await;
4890 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4891
4892 let item = cx.new_view(|cx| {
4893 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4894 });
4895 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4896 let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
4897 let toolbar_notify_count = Rc::new(RefCell::new(0));
4898
4899 workspace.update(cx, |workspace, cx| {
4900 workspace.add_item(Box::new(item.clone()), cx);
4901 let toolbar_notification_count = toolbar_notify_count.clone();
4902 cx.observe(&toolbar, move |_, _, _| {
4903 *toolbar_notification_count.borrow_mut() += 1
4904 })
4905 .detach();
4906 });
4907
4908 pane.update(cx, |pane, _| {
4909 assert!(!pane.can_navigate_backward());
4910 assert!(!pane.can_navigate_forward());
4911 });
4912
4913 item.update(cx, |item, cx| {
4914 item.set_state("one".to_string(), cx);
4915 });
4916
4917 // Toolbar must be notified to re-render the navigation buttons
4918 assert_eq!(*toolbar_notify_count.borrow(), 1);
4919
4920 pane.update(cx, |pane, _| {
4921 assert!(pane.can_navigate_backward());
4922 assert!(!pane.can_navigate_forward());
4923 });
4924
4925 workspace
4926 .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
4927 .await
4928 .unwrap();
4929
4930 assert_eq!(*toolbar_notify_count.borrow(), 2);
4931 pane.update(cx, |pane, _| {
4932 assert!(!pane.can_navigate_backward());
4933 assert!(pane.can_navigate_forward());
4934 });
4935 }
4936
4937 #[gpui::test]
4938 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
4939 init_test(cx);
4940 let fs = FakeFs::new(cx.executor());
4941
4942 let project = Project::test(fs, [], cx).await;
4943 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4944
4945 let panel = workspace.update(cx, |workspace, cx| {
4946 let panel = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
4947 workspace.add_panel(panel.clone(), cx);
4948
4949 workspace
4950 .right_dock()
4951 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
4952
4953 panel
4954 });
4955
4956 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4957 pane.update(cx, |pane, cx| {
4958 let item = cx.new_view(|cx| TestItem::new(cx));
4959 pane.add_item(Box::new(item), true, true, None, cx);
4960 });
4961
4962 // Transfer focus from center to panel
4963 workspace.update(cx, |workspace, cx| {
4964 workspace.toggle_panel_focus::<TestPanel>(cx);
4965 });
4966
4967 workspace.update(cx, |workspace, cx| {
4968 assert!(workspace.right_dock().read(cx).is_open());
4969 assert!(!panel.is_zoomed(cx));
4970 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
4971 });
4972
4973 // Transfer focus from panel to center
4974 workspace.update(cx, |workspace, cx| {
4975 workspace.toggle_panel_focus::<TestPanel>(cx);
4976 });
4977
4978 workspace.update(cx, |workspace, cx| {
4979 assert!(workspace.right_dock().read(cx).is_open());
4980 assert!(!panel.is_zoomed(cx));
4981 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
4982 });
4983
4984 // Close the dock
4985 workspace.update(cx, |workspace, cx| {
4986 workspace.toggle_dock(DockPosition::Right, cx);
4987 });
4988
4989 workspace.update(cx, |workspace, cx| {
4990 assert!(!workspace.right_dock().read(cx).is_open());
4991 assert!(!panel.is_zoomed(cx));
4992 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
4993 });
4994
4995 // Open the dock
4996 workspace.update(cx, |workspace, cx| {
4997 workspace.toggle_dock(DockPosition::Right, cx);
4998 });
4999
5000 workspace.update(cx, |workspace, cx| {
5001 assert!(workspace.right_dock().read(cx).is_open());
5002 assert!(!panel.is_zoomed(cx));
5003 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5004 });
5005
5006 // Focus and zoom panel
5007 panel.update(cx, |panel, cx| {
5008 cx.focus_self();
5009 panel.set_zoomed(true, cx)
5010 });
5011
5012 workspace.update(cx, |workspace, cx| {
5013 assert!(workspace.right_dock().read(cx).is_open());
5014 assert!(panel.is_zoomed(cx));
5015 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5016 });
5017
5018 // Transfer focus to the center closes the dock
5019 workspace.update(cx, |workspace, cx| {
5020 workspace.toggle_panel_focus::<TestPanel>(cx);
5021 });
5022
5023 workspace.update(cx, |workspace, cx| {
5024 assert!(!workspace.right_dock().read(cx).is_open());
5025 assert!(panel.is_zoomed(cx));
5026 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5027 });
5028
5029 // Transferring focus back to the panel keeps it zoomed
5030 workspace.update(cx, |workspace, cx| {
5031 workspace.toggle_panel_focus::<TestPanel>(cx);
5032 });
5033
5034 workspace.update(cx, |workspace, cx| {
5035 assert!(workspace.right_dock().read(cx).is_open());
5036 assert!(panel.is_zoomed(cx));
5037 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5038 });
5039
5040 // Close the dock while it is zoomed
5041 workspace.update(cx, |workspace, cx| {
5042 workspace.toggle_dock(DockPosition::Right, cx)
5043 });
5044
5045 workspace.update(cx, |workspace, cx| {
5046 assert!(!workspace.right_dock().read(cx).is_open());
5047 assert!(panel.is_zoomed(cx));
5048 assert!(workspace.zoomed.is_none());
5049 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5050 });
5051
5052 // Opening the dock, when it's zoomed, retains focus
5053 workspace.update(cx, |workspace, cx| {
5054 workspace.toggle_dock(DockPosition::Right, cx)
5055 });
5056
5057 workspace.update(cx, |workspace, cx| {
5058 assert!(workspace.right_dock().read(cx).is_open());
5059 assert!(panel.is_zoomed(cx));
5060 assert!(workspace.zoomed.is_some());
5061 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5062 });
5063
5064 // Unzoom and close the panel, zoom the active pane.
5065 panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
5066 workspace.update(cx, |workspace, cx| {
5067 workspace.toggle_dock(DockPosition::Right, cx)
5068 });
5069 pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
5070
5071 // Opening a dock unzooms the pane.
5072 workspace.update(cx, |workspace, cx| {
5073 workspace.toggle_dock(DockPosition::Right, cx)
5074 });
5075 workspace.update(cx, |workspace, cx| {
5076 let pane = pane.read(cx);
5077 assert!(!pane.is_zoomed());
5078 assert!(!pane.focus_handle(cx).is_focused(cx));
5079 assert!(workspace.right_dock().read(cx).is_open());
5080 assert!(workspace.zoomed.is_none());
5081 });
5082 }
5083
5084 struct TestModal(FocusHandle);
5085
5086 impl TestModal {
5087 fn new(cx: &mut ViewContext<Self>) -> Self {
5088 Self(cx.focus_handle())
5089 }
5090 }
5091
5092 impl EventEmitter<DismissEvent> for TestModal {}
5093
5094 impl FocusableView for TestModal {
5095 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
5096 self.0.clone()
5097 }
5098 }
5099
5100 impl ModalView for TestModal {}
5101
5102 impl Render for TestModal {
5103 fn render(&mut self, _cx: &mut ViewContext<TestModal>) -> impl IntoElement {
5104 div().track_focus(&self.0)
5105 }
5106 }
5107
5108 #[gpui::test]
5109 async fn test_panels(cx: &mut gpui::TestAppContext) {
5110 init_test(cx);
5111 let fs = FakeFs::new(cx.executor());
5112
5113 let project = Project::test(fs, [], cx).await;
5114 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5115
5116 let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
5117 let panel_1 = cx.new_view(|cx| TestPanel::new(DockPosition::Left, cx));
5118 workspace.add_panel(panel_1.clone(), cx);
5119 workspace
5120 .left_dock()
5121 .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
5122 let panel_2 = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
5123 workspace.add_panel(panel_2.clone(), cx);
5124 workspace
5125 .right_dock()
5126 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
5127
5128 let left_dock = workspace.left_dock();
5129 assert_eq!(
5130 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5131 panel_1.panel_id()
5132 );
5133 assert_eq!(
5134 left_dock.read(cx).active_panel_size(cx).unwrap(),
5135 panel_1.size(cx)
5136 );
5137
5138 left_dock.update(cx, |left_dock, cx| {
5139 left_dock.resize_active_panel(Some(px(1337.)), cx)
5140 });
5141 assert_eq!(
5142 workspace
5143 .right_dock()
5144 .read(cx)
5145 .visible_panel()
5146 .unwrap()
5147 .panel_id(),
5148 panel_2.panel_id(),
5149 );
5150
5151 (panel_1, panel_2)
5152 });
5153
5154 // Move panel_1 to the right
5155 panel_1.update(cx, |panel_1, cx| {
5156 panel_1.set_position(DockPosition::Right, cx)
5157 });
5158
5159 workspace.update(cx, |workspace, cx| {
5160 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
5161 // Since it was the only panel on the left, the left dock should now be closed.
5162 assert!(!workspace.left_dock().read(cx).is_open());
5163 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
5164 let right_dock = workspace.right_dock();
5165 assert_eq!(
5166 right_dock.read(cx).visible_panel().unwrap().panel_id(),
5167 panel_1.panel_id()
5168 );
5169 assert_eq!(
5170 right_dock.read(cx).active_panel_size(cx).unwrap(),
5171 px(1337.)
5172 );
5173
5174 // Now we move panel_2 to the left
5175 panel_2.set_position(DockPosition::Left, cx);
5176 });
5177
5178 workspace.update(cx, |workspace, cx| {
5179 // Since panel_2 was not visible on the right, we don't open the left dock.
5180 assert!(!workspace.left_dock().read(cx).is_open());
5181 // And the right dock is unaffected in it's displaying of panel_1
5182 assert!(workspace.right_dock().read(cx).is_open());
5183 assert_eq!(
5184 workspace
5185 .right_dock()
5186 .read(cx)
5187 .visible_panel()
5188 .unwrap()
5189 .panel_id(),
5190 panel_1.panel_id(),
5191 );
5192 });
5193
5194 // Move panel_1 back to the left
5195 panel_1.update(cx, |panel_1, cx| {
5196 panel_1.set_position(DockPosition::Left, cx)
5197 });
5198
5199 workspace.update(cx, |workspace, cx| {
5200 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
5201 let left_dock = workspace.left_dock();
5202 assert!(left_dock.read(cx).is_open());
5203 assert_eq!(
5204 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5205 panel_1.panel_id()
5206 );
5207 assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), px(1337.));
5208 // And the right dock should be closed as it no longer has any panels.
5209 assert!(!workspace.right_dock().read(cx).is_open());
5210
5211 // Now we move panel_1 to the bottom
5212 panel_1.set_position(DockPosition::Bottom, cx);
5213 });
5214
5215 workspace.update(cx, |workspace, cx| {
5216 // Since panel_1 was visible on the left, we close the left dock.
5217 assert!(!workspace.left_dock().read(cx).is_open());
5218 // The bottom dock is sized based on the panel's default size,
5219 // since the panel orientation changed from vertical to horizontal.
5220 let bottom_dock = workspace.bottom_dock();
5221 assert_eq!(
5222 bottom_dock.read(cx).active_panel_size(cx).unwrap(),
5223 panel_1.size(cx),
5224 );
5225 // Close bottom dock and move panel_1 back to the left.
5226 bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
5227 panel_1.set_position(DockPosition::Left, cx);
5228 });
5229
5230 // Emit activated event on panel 1
5231 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
5232
5233 // Now the left dock is open and panel_1 is active and focused.
5234 workspace.update(cx, |workspace, cx| {
5235 let left_dock = workspace.left_dock();
5236 assert!(left_dock.read(cx).is_open());
5237 assert_eq!(
5238 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5239 panel_1.panel_id(),
5240 );
5241 assert!(panel_1.focus_handle(cx).is_focused(cx));
5242 });
5243
5244 // Emit closed event on panel 2, which is not active
5245 panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
5246
5247 // Wo don't close the left dock, because panel_2 wasn't the active panel
5248 workspace.update(cx, |workspace, cx| {
5249 let left_dock = workspace.left_dock();
5250 assert!(left_dock.read(cx).is_open());
5251 assert_eq!(
5252 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5253 panel_1.panel_id(),
5254 );
5255 });
5256
5257 // Emitting a ZoomIn event shows the panel as zoomed.
5258 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
5259 workspace.update(cx, |workspace, _| {
5260 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5261 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
5262 });
5263
5264 // Move panel to another dock while it is zoomed
5265 panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
5266 workspace.update(cx, |workspace, _| {
5267 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5268
5269 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5270 });
5271
5272 // This is a helper for getting a:
5273 // - valid focus on an element,
5274 // - that isn't a part of the panes and panels system of the Workspace,
5275 // - and doesn't trigger the 'on_focus_lost' API.
5276 let focus_other_view = {
5277 let workspace = workspace.clone();
5278 move |cx: &mut VisualTestContext| {
5279 workspace.update(cx, |workspace, cx| {
5280 if let Some(_) = workspace.active_modal::<TestModal>(cx) {
5281 workspace.toggle_modal(cx, TestModal::new);
5282 workspace.toggle_modal(cx, TestModal::new);
5283 } else {
5284 workspace.toggle_modal(cx, TestModal::new);
5285 }
5286 })
5287 }
5288 };
5289
5290 // If focus is transferred to another view that's not a panel or another pane, we still show
5291 // the panel as zoomed.
5292 focus_other_view(cx);
5293 workspace.update(cx, |workspace, _| {
5294 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5295 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5296 });
5297
5298 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
5299 workspace.update(cx, |_, cx| cx.focus_self());
5300 workspace.update(cx, |workspace, _| {
5301 assert_eq!(workspace.zoomed, None);
5302 assert_eq!(workspace.zoomed_position, None);
5303 });
5304
5305 // If focus is transferred again to another view that's not a panel or a pane, we won't
5306 // show the panel as zoomed because it wasn't zoomed before.
5307 focus_other_view(cx);
5308 workspace.update(cx, |workspace, _| {
5309 assert_eq!(workspace.zoomed, None);
5310 assert_eq!(workspace.zoomed_position, None);
5311 });
5312
5313 // When the panel is activated, it is zoomed again.
5314 cx.dispatch_action(ToggleRightDock);
5315 workspace.update(cx, |workspace, _| {
5316 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5317 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5318 });
5319
5320 // Emitting a ZoomOut event unzooms the panel.
5321 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
5322 workspace.update(cx, |workspace, _| {
5323 assert_eq!(workspace.zoomed, None);
5324 assert_eq!(workspace.zoomed_position, None);
5325 });
5326
5327 // Emit closed event on panel 1, which is active
5328 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
5329
5330 // Now the left dock is closed, because panel_1 was the active panel
5331 workspace.update(cx, |workspace, cx| {
5332 let right_dock = workspace.right_dock();
5333 assert!(!right_dock.read(cx).is_open());
5334 });
5335 }
5336
5337 pub fn init_test(cx: &mut TestAppContext) {
5338 cx.update(|cx| {
5339 let settings_store = SettingsStore::test(cx);
5340 cx.set_global(settings_store);
5341 theme::init(theme::LoadThemes::JustBase, cx);
5342 language::init(cx);
5343 crate::init_settings(cx);
5344 Project::init_settings(cx);
5345 });
5346 }
5347}