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