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