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