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