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