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