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