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