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