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